Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa3cb6ad25 |
+1
-1
@@ -154,7 +154,7 @@ gitVersioning.apply {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
rev {
|
rev {
|
||||||
version = '1.15.0-SNAPSHOT'
|
version = '${describe.tag.version.major}.${describe.tag.version.minor.next}.0-SNAPSHOT'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
-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);
|
||||||
|
|||||||
-16
@@ -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);
|
||||||
@@ -477,8 +463,6 @@ public class ProductToGLAccountMappingReadPlatformServiceImpl implements Product
|
|||||||
accountMappingDetails.put(SavingProductAccountingDataParams.SAVINGS_CONTROL.getValue(), glAccountData);
|
accountMappingDetails.put(SavingProductAccountingDataParams.SAVINGS_CONTROL.getValue(), glAccountData);
|
||||||
} else if (glAccountForSavings.equals(CashAccountsForSavings.TRANSFERS_SUSPENSE)) {
|
} else if (glAccountForSavings.equals(CashAccountsForSavings.TRANSFERS_SUSPENSE)) {
|
||||||
accountMappingDetails.put(SavingProductAccountingDataParams.TRANSFERS_SUSPENSE.getValue(), glAccountData);
|
accountMappingDetails.put(SavingProductAccountingDataParams.TRANSFERS_SUSPENSE.getValue(), glAccountData);
|
||||||
} else if (glAccountForSavings.equals(CashAccountsForSavings.INTEREST_PAYABLE)) {
|
|
||||||
accountMappingDetails.put(SavingProductAccountingDataParams.INTEREST_PAYABLE.getValue(), glAccountData);
|
|
||||||
// Income
|
// Income
|
||||||
} else if (glAccountForSavings.equals(CashAccountsForSavings.INCOME_FROM_FEES)) {
|
} else if (glAccountForSavings.equals(CashAccountsForSavings.INCOME_FROM_FEES)) {
|
||||||
accountMappingDetails.put(SavingProductAccountingDataParams.INCOME_FROM_FEES.getValue(), glAccountData);
|
accountMappingDetails.put(SavingProductAccountingDataParams.INCOME_FROM_FEES.getValue(), glAccountData);
|
||||||
|
|||||||
-7
@@ -264,13 +264,6 @@ public class SavingsProductToGLAccountMappingHelper extends ProductToGLAccountMa
|
|||||||
savingsProductId, CashAccountsForSavings.ESCHEAT_LIABILITY.getValue(), changes);
|
savingsProductId, CashAccountsForSavings.ESCHEAT_LIABILITY.getValue(), changes);
|
||||||
createOrmergeSavingsToExpenseAccountMappingChanges(element, SavingProductAccountingParams.ROUNDING_ADJUSTMENT.getValue(),
|
createOrmergeSavingsToExpenseAccountMappingChanges(element, SavingProductAccountingParams.ROUNDING_ADJUSTMENT.getValue(),
|
||||||
savingsProductId, CashAccountsForSavings.ROUNDING_ADJUSTMENT.getValue(), changes);
|
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
|
||||||
|
|||||||
-2
@@ -33,6 +33,4 @@ public interface TellerRepository extends JpaRepository<Teller, Long>, JpaSpecif
|
|||||||
Collection<Teller> findTellerByOfficeId(Long officeId);
|
Collection<Teller> findTellerByOfficeId(Long officeId);
|
||||||
|
|
||||||
long countByDeletedFalse();
|
long countByDeletedFalse();
|
||||||
|
|
||||||
long countByOfficeIdAndDeletedFalse(Long officeId);
|
|
||||||
}
|
}
|
||||||
|
|||||||
-5
@@ -58,9 +58,4 @@ public class TellerRepositoryWrapper {
|
|||||||
public boolean existsAny() {
|
public boolean existsAny() {
|
||||||
return this.repository.countByDeletedFalse() > 0;
|
return this.repository.countByDeletedFalse() > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Whether the branch already has a (not deleted) teller — each branch keeps one vault of its own. */
|
|
||||||
public boolean existsInOffice(final Long officeId) {
|
|
||||||
return this.repository.countByOfficeIdAndDeletedFalse(officeId) > 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
-5
@@ -28,9 +28,4 @@ public class TellerAlreadyExistsException extends AbstractPlatformDomainRuleExce
|
|||||||
public TellerAlreadyExistsException() {
|
public TellerAlreadyExistsException() {
|
||||||
super(ERROR_MESSAGE_CODE, DEFAULT_ERROR_MESSAGE);
|
super(ERROR_MESSAGE_CODE, DEFAULT_ERROR_MESSAGE);
|
||||||
}
|
}
|
||||||
|
|
||||||
public TellerAlreadyExistsException(final String officeName) {
|
|
||||||
super(ERROR_MESSAGE_CODE, "Cannot create teller: " + officeName + " already has a teller. Each branch has one teller.",
|
|
||||||
officeName);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,7 +80,6 @@ public class Charge extends AbstractPersistableCustom<Long> {
|
|||||||
@Column(name = "currency_code", length = 3)
|
@Column(name = "currency_code", length = 3)
|
||||||
private String currencyCode;
|
private String currencyCode;
|
||||||
|
|
||||||
@Getter
|
|
||||||
@Column(name = "charge_applies_to_enum", nullable = false)
|
@Column(name = "charge_applies_to_enum", nullable = false)
|
||||||
private Integer chargeAppliesTo;
|
private Integer chargeAppliesTo;
|
||||||
|
|
||||||
|
|||||||
-1
@@ -270,7 +270,6 @@ public final class AccountingConstants {
|
|||||||
INCOME_FROM_INTEREST(12), //
|
INCOME_FROM_INTEREST(12), //
|
||||||
LOSSES_WRITTEN_OFF(13), //
|
LOSSES_WRITTEN_OFF(13), //
|
||||||
ESCHEAT_LIABILITY(14), //
|
ESCHEAT_LIABILITY(14), //
|
||||||
INTEREST_PAYABLE(17), //
|
|
||||||
/**
|
/**
|
||||||
* Where the paise dropped from a cash closure are absorbed.
|
* Where the paise dropped from a cash closure are absorbed.
|
||||||
*
|
*
|
||||||
|
|||||||
-4
@@ -157,16 +157,12 @@ public class CommandSourceService {
|
|||||||
}
|
}
|
||||||
if ("GLACCOUNT".equalsIgnoreCase(commandSource.getEntityName())
|
if ("GLACCOUNT".equalsIgnoreCase(commandSource.getEntityName())
|
||||||
|| "CBSBASEINTERESTRATE".equalsIgnoreCase(commandSource.getEntityName())
|
|| "CBSBASEINTERESTRATE".equalsIgnoreCase(commandSource.getEntityName())
|
||||||
|| "LOANBASEINTERESTRATE".equalsIgnoreCase(commandSource.getEntityName())
|
|
||||||
|| "DELINQUENCY_BUCKET".equalsIgnoreCase(commandSource.getEntityName())
|
|
||||||
|| "DELINQUENCY_RANGE".equalsIgnoreCase(commandSource.getEntityName())
|
|
||||||
|| "CHARGE".equalsIgnoreCase(commandSource.getEntityName())
|
|| "CHARGE".equalsIgnoreCase(commandSource.getEntityName())
|
||||||
|| "LOANPRODUCT".equalsIgnoreCase(commandSource.getEntityName())
|
|| "LOANPRODUCT".equalsIgnoreCase(commandSource.getEntityName())
|
||||||
|| "SAVINGSPRODUCT".equalsIgnoreCase(commandSource.getEntityName())
|
|| "SAVINGSPRODUCT".equalsIgnoreCase(commandSource.getEntityName())
|
||||||
|| "CURRENTPRODUCT".equalsIgnoreCase(commandSource.getEntityName())
|
|| "CURRENTPRODUCT".equalsIgnoreCase(commandSource.getEntityName())
|
||||||
|| "SHAREPRODUCT".equalsIgnoreCase(commandSource.getEntityName())
|
|| "SHAREPRODUCT".equalsIgnoreCase(commandSource.getEntityName())
|
||||||
|| "FIXEDDEPOSITPRODUCT".equalsIgnoreCase(commandSource.getEntityName())
|
|| "FIXEDDEPOSITPRODUCT".equalsIgnoreCase(commandSource.getEntityName())
|
||||||
|| "RECURRINGDEPOSITPRODUCT".equalsIgnoreCase(commandSource.getEntityName())
|
|
||||||
|| "USER".equalsIgnoreCase(commandSource.getEntityName())
|
|| "USER".equalsIgnoreCase(commandSource.getEntityName())
|
||||||
|| "ROLE".equalsIgnoreCase(commandSource.getEntityName())
|
|| "ROLE".equalsIgnoreCase(commandSource.getEntityName())
|
||||||
|| isDdsEntity(commandSource.getEntityName())) {
|
|| isDdsEntity(commandSource.getEntityName())) {
|
||||||
|
|||||||
+48
-128
@@ -95,6 +95,54 @@ public class CommandWrapperBuilder {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public CommandWrapperBuilder createLockerClass() {
|
||||||
|
this.actionName = "CREATE";
|
||||||
|
this.entityName = "LOCKERCLASS";
|
||||||
|
this.entityId = null;
|
||||||
|
this.href = "/lockerclasses";
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandWrapperBuilder updateLockerClass(final Long lockerClassId) {
|
||||||
|
this.actionName = "UPDATE";
|
||||||
|
this.entityName = "LOCKERCLASS";
|
||||||
|
this.entityId = lockerClassId;
|
||||||
|
this.href = "/lockerclasses/" + lockerClassId;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandWrapperBuilder deleteLockerClass(final Long lockerClassId) {
|
||||||
|
this.actionName = "DELETE";
|
||||||
|
this.entityName = "LOCKERCLASS";
|
||||||
|
this.entityId = lockerClassId;
|
||||||
|
this.href = "/lockerclasses/" + lockerClassId;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandWrapperBuilder createLockerWaitingList() {
|
||||||
|
this.actionName = "CREATE";
|
||||||
|
this.entityName = "LOCKERWAITINGLIST";
|
||||||
|
this.entityId = null;
|
||||||
|
this.href = "/lockerwaitinglist";
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandWrapperBuilder updateLockerWaitingList(final Long waitingListId) {
|
||||||
|
this.actionName = "UPDATE";
|
||||||
|
this.entityName = "LOCKERWAITINGLIST";
|
||||||
|
this.entityId = waitingListId;
|
||||||
|
this.href = "/lockerwaitinglist/" + waitingListId;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandWrapperBuilder cancelLockerWaitingList(final Long waitingListId) {
|
||||||
|
this.actionName = "CANCEL";
|
||||||
|
this.entityName = "LOCKERWAITINGLIST";
|
||||||
|
this.entityId = waitingListId;
|
||||||
|
this.href = "/lockerwaitinglist/" + waitingListId + "?command=cancel";
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public CommandWrapperBuilder createChequeStockLot() {
|
public CommandWrapperBuilder createChequeStockLot() {
|
||||||
this.actionName = "CREATE";
|
this.actionName = "CREATE";
|
||||||
this.entityName = "CHEQUESTOCKLOT";
|
this.entityName = "CHEQUESTOCKLOT";
|
||||||
@@ -917,14 +965,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";
|
||||||
@@ -1053,70 +1093,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";
|
||||||
@@ -2460,14 +2436,6 @@ public class CommandWrapperBuilder {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public CommandWrapperBuilder createLoanBaseInterestRate() {
|
|
||||||
this.actionName = "CREATE";
|
|
||||||
this.entityName = "LOANBASEINTERESTRATE";
|
|
||||||
this.entityId = null;
|
|
||||||
this.href = "/loanproducts/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";
|
||||||
@@ -3326,15 +3294,6 @@ public class CommandWrapperBuilder {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public CommandWrapperBuilder extendTenureRecurringDepositAccount(final Long accountId) {
|
|
||||||
this.actionName = "EXTENDTENURE";
|
|
||||||
this.entityName = "RECURRINGDEPOSITACCOUNT";
|
|
||||||
this.entityId = accountId;
|
|
||||||
this.savingsId = accountId;
|
|
||||||
this.href = "/recurringdepositaccounts/" + accountId + "?command=extendTenure";
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CommandWrapperBuilder prematureCloseRecurringDepositAccount(final Long accountId) {
|
public CommandWrapperBuilder prematureCloseRecurringDepositAccount(final Long accountId) {
|
||||||
this.actionName = "PREMATURECLOSE";
|
this.actionName = "PREMATURECLOSE";
|
||||||
this.entityName = "RECURRINGDEPOSITACCOUNT";
|
this.entityName = "RECURRINGDEPOSITACCOUNT";
|
||||||
@@ -3640,13 +3599,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";
|
||||||
@@ -4987,36 +4939,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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
-1
@@ -32,7 +32,6 @@ public final class GlobalConfigurationConstants {
|
|||||||
public static final String FORCE_PASSWORD_RESET_DAYS = "force-password-reset-days";
|
public static final String FORCE_PASSWORD_RESET_DAYS = "force-password-reset-days";
|
||||||
public static final String GRACE_ON_PENALTY_POSTING = "grace-on-penalty-posting";
|
public static final String GRACE_ON_PENALTY_POSTING = "grace-on-penalty-posting";
|
||||||
public static final String SAVINGS_INTEREST_POSTING_CURRENT_PERIOD_END = "savings-interest-posting-current-period-end";
|
public static final String SAVINGS_INTEREST_POSTING_CURRENT_PERIOD_END = "savings-interest-posting-current-period-end";
|
||||||
public static final String FD_INTEREST_POSTING_APPROVAL = "fd-interest-posting-approval";
|
|
||||||
public static final String FINANCIAL_YEAR_BEGINNING_MONTH = "financial-year-beginning-month";
|
public static final String FINANCIAL_YEAR_BEGINNING_MONTH = "financial-year-beginning-month";
|
||||||
public static final String MIN_CLIENTS_IN_GROUP = "min-clients-in-group";
|
public static final String MIN_CLIENTS_IN_GROUP = "min-clients-in-group";
|
||||||
public static final String MAX_CLIENTS_IN_GROUP = "max-clients-in-group";
|
public static final String MAX_CLIENTS_IN_GROUP = "max-clients-in-group";
|
||||||
|
|||||||
-6
@@ -69,12 +69,6 @@ public interface ConfigurationDomainService {
|
|||||||
|
|
||||||
boolean isSavingsInterestPostingAtCurrentPeriodEnd();
|
boolean isSavingsInterestPostingAtCurrentPeriodEnd();
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether fixed deposit interest is reported and approved before it is posted, instead of being
|
|
||||||
* posted directly by the nightly job.
|
|
||||||
*/
|
|
||||||
boolean isFixedDepositInterestPostingApprovalEnabled();
|
|
||||||
|
|
||||||
Integer retrieveFinancialYearBeginningMonth();
|
Integer retrieveFinancialYearBeginningMonth();
|
||||||
|
|
||||||
Integer retrieveMinAllowedClientsInGroup();
|
Integer retrieveMinAllowedClientsInGroup();
|
||||||
|
|||||||
-1
@@ -505,7 +505,6 @@ public class FineractProperties {
|
|||||||
private FineractTransactionProcessorItemProperties heavensFamily;
|
private FineractTransactionProcessorItemProperties heavensFamily;
|
||||||
private FineractTransactionProcessorItemProperties interestPrincipalPenaltiesFees;
|
private FineractTransactionProcessorItemProperties interestPrincipalPenaltiesFees;
|
||||||
private FineractTransactionProcessorItemProperties principalInterestPenaltiesFees;
|
private FineractTransactionProcessorItemProperties principalInterestPenaltiesFees;
|
||||||
private FineractTransactionProcessorItemProperties feesPenaltiesInterestPrincipal;
|
|
||||||
private FineractTransactionProcessorItemProperties rbiIndia;
|
private FineractTransactionProcessorItemProperties rbiIndia;
|
||||||
private FineractTransactionProcessorItemProperties duePenaltyFeeInterestPrincipalInAdvancePrincipalPenaltyFeeInterest;
|
private FineractTransactionProcessorItemProperties duePenaltyFeeInterestPrincipalInAdvancePrincipalPenaltyFeeInterest;
|
||||||
private FineractTransactionProcessorItemProperties duePenaltyInterestPrincipalFeeInAdvancePenaltyInterestPrincipalFee;
|
private FineractTransactionProcessorItemProperties duePenaltyInterestPrincipalFeeInAdvancePenaltyInterestPrincipalFee;
|
||||||
|
|||||||
-7
@@ -42,11 +42,7 @@ public enum JobName {
|
|||||||
POST_SHARE_ENTRY_FEE_TO_PROFIT("Post Share Entry Fee To Profit"), //
|
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"), //
|
APPLY_KYC_EXPIRY_RESTRICTIONS("Apply KYC Expiry Restrictions"), //
|
||||||
SEND_KYC_REMINDERS("Send KYC Reminders"), //
|
|
||||||
PROCESS_KYC_NOTICE_QUEUE("Process KYC Notice Queue"), //
|
|
||||||
SAVINGS_DAILY_INTEREST("Savings Daily Interest Accrual And Posting"), //
|
SAVINGS_DAILY_INTEREST("Savings Daily Interest Accrual And Posting"), //
|
||||||
SAVINGS_INTEREST_POSTING_REPORT("Generate Savings Interest Posting Report"), //
|
|
||||||
FD_INTEREST_POSTING_REPORT("Generate Fixed Deposit Interest Posting Report"), //
|
|
||||||
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"), //
|
||||||
@@ -78,9 +74,6 @@ public enum JobName {
|
|||||||
AML_STR_WEEKLY_DIGEST("AML STR Weekly Digest"), //
|
AML_STR_WEEKLY_DIGEST("AML STR Weekly Digest"), //
|
||||||
POST_DDS_INTEREST("Post DDS Interest"), //
|
POST_DDS_INTEREST("Post DDS Interest"), //
|
||||||
CLIENT_BEHAVIOUR_WINDOW("Client Behaviour Window Computation"), //
|
CLIENT_BEHAVIOUR_WINDOW("Client Behaviour Window Computation"), //
|
||||||
SAVE_RBI_FORM_A_RETURN("Save RBI Form A Return"), //
|
|
||||||
EXECUTE_RD_AUTO_DEBIT("Execute RD Auto Debit"), //
|
|
||||||
APPLY_RD_MISSED_INSTALLMENT_PENALTY("Apply RD Missed Installment Penalty"), //
|
|
||||||
; //
|
; //
|
||||||
|
|
||||||
private final String name;
|
private final String name;
|
||||||
|
|||||||
+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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+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
-10
@@ -83,24 +83,15 @@ public class SavingsAccountSummaryData {
|
|||||||
* on. Lets a client's account list collapse a renewal chain into one FD instead of listing each term separately.
|
* on. Lets a client's account list collapse a renewal chain into one FD instead of listing each term separately.
|
||||||
*/
|
*/
|
||||||
private final Integer renewalCycleNo;
|
private final Integer renewalCycleNo;
|
||||||
/**
|
|
||||||
* The branch this account is held at: the account's own office when it was opened at another branch than the
|
|
||||||
* client's, else the client's (or the group's) office. Lets a customer with accounts at several branches tell
|
|
||||||
* them apart on the client page.
|
|
||||||
*/
|
|
||||||
private final Long officeId;
|
|
||||||
private final String officeName;
|
|
||||||
|
|
||||||
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, final Long officeId, final String officeName) {
|
final String fdAccountNumber, final Integer renewalCycleNo) {
|
||||||
this.fdAccountNumber = fdAccountNumber;
|
this.fdAccountNumber = fdAccountNumber;
|
||||||
this.renewalCycleNo = renewalCycleNo;
|
this.renewalCycleNo = renewalCycleNo;
|
||||||
this.officeId = officeId;
|
|
||||||
this.officeName = officeName;
|
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.accountNo = accountNo;
|
this.accountNo = accountNo;
|
||||||
this.externalId = externalId;
|
this.externalId = externalId;
|
||||||
|
|||||||
+1
-5
@@ -21,11 +21,7 @@ package org.apache.fineract.portfolio.aml.domain;
|
|||||||
/** Which transaction type(s) an {@link AmlTransactionRule} evaluates against. */
|
/** Which transaction type(s) an {@link AmlTransactionRule} evaluates against. */
|
||||||
public enum AmlAppliesTo {
|
public enum AmlAppliesTo {
|
||||||
|
|
||||||
/**
|
SAVINGS, LOAN, TRANSFER, ALL;
|
||||||
* CURRENT is a current / Cash Credit account. The engine is not yet fed current-account transactions, so a rule written
|
|
||||||
* for it is catalogued but does not fire until it is.
|
|
||||||
*/
|
|
||||||
SAVINGS, LOAN, TRANSFER, CURRENT, ALL;
|
|
||||||
|
|
||||||
public boolean matches(AmlTransactionType transactionType) {
|
public boolean matches(AmlTransactionType transactionType) {
|
||||||
return this == ALL || this.name().equals(transactionType.name());
|
return this == ALL || this.name().equals(transactionType.name());
|
||||||
|
|||||||
+12
-2
@@ -20,12 +20,14 @@ package org.apache.fineract.portfolio.aml.domain;
|
|||||||
|
|
||||||
import jakarta.persistence.Column;
|
import jakarta.persistence.Column;
|
||||||
import jakarta.persistence.Entity;
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
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.LocalDate;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
|
|
||||||
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;
|
||||||
|
|
||||||
@@ -38,7 +40,11 @@ import org.apache.fineract.useradministration.domain.AppUser;
|
|||||||
*/
|
*/
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "m_aml_recurring_review")
|
@Table(name = "m_aml_recurring_review")
|
||||||
public class AmlRecurringReview extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
public class AmlRecurringReview {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
@ManyToOne(optional = false)
|
@ManyToOne(optional = false)
|
||||||
@JoinColumn(name = "client_id", nullable = false)
|
@JoinColumn(name = "client_id", nullable = false)
|
||||||
@@ -101,6 +107,10 @@ public class AmlRecurringReview extends AbstractAuditableWithUTCDateTimeCustom<L
|
|||||||
this.alertsActioned = count;
|
this.alertsActioned = count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
public Client getClient() {
|
public Client getClient() {
|
||||||
return client;
|
return client;
|
||||||
}
|
}
|
||||||
|
|||||||
-9
@@ -152,15 +152,6 @@ public class AmlTransactionRule {
|
|||||||
return rule;
|
return rule;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Narrows a rule to one direction - CREDIT (money in) or DEBIT (money out), null for either - and, when asked, to cash
|
|
||||||
* only. The engine already honours both; a rule written without them applies to every credit and debit, cash or not.
|
|
||||||
*/
|
|
||||||
public void restrictTo(final String direction, final boolean cashOnly) {
|
|
||||||
this.direction = direction;
|
|
||||||
this.cashOnly = cashOnly;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Long getId() {
|
public Long getId() {
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|||||||
-61
@@ -1,61 +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.util.regex.Pattern;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The comparable fingerprint of an Aadhaar number: the SHA-256 of the 12 digits, as 64 lowercase hex characters.
|
|
||||||
*
|
|
||||||
* <p>
|
|
||||||
* The hash is computed by the client (browser) and only the hash is sent. The KYC create/update calls go through the
|
|
||||||
* command source, which records the request body in an audit table and in the log, so the 12-digit number must not be
|
|
||||||
* in the body. The server therefore never sees the number and only checks that what it was given has the shape of a
|
|
||||||
* SHA-256 digest.
|
|
||||||
*
|
|
||||||
* <p>
|
|
||||||
* A plain, unkeyed SHA-256 of a 12-digit number can be reversed by trying every possible number, so this column is a
|
|
||||||
* lookup key, not protection against someone who can read the table.
|
|
||||||
*/
|
|
||||||
public final class AadhaarHash {
|
|
||||||
|
|
||||||
private static final Pattern SHA256_HEX = Pattern.compile("^[0-9a-f]{64}$");
|
|
||||||
|
|
||||||
private AadhaarHash() {}
|
|
||||||
|
|
||||||
/** True when the value has the shape of a lowercase hex SHA-256 digest. */
|
|
||||||
public static boolean isValid(final String value) {
|
|
||||||
return value != null && SHA256_HEX.matcher(value).matches();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The value trimmed and lower-cased when it is a well-formed digest; null for blank input. A non-blank value that
|
|
||||||
* is not a digest is rejected rather than silently dropped, so a client bug cannot disable the duplicate check.
|
|
||||||
*/
|
|
||||||
public static String normalizeOrNull(final String raw) {
|
|
||||||
if (raw == null || raw.isBlank()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
final String candidate = raw.trim().toLowerCase();
|
|
||||||
if (!isValid(candidate)) {
|
|
||||||
throw new IllegalArgumentException("aadhaarHash must be a 64-character hex SHA-256 digest");
|
|
||||||
}
|
|
||||||
return candidate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-27
@@ -151,13 +151,6 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
@Column(name = "aadhaar_number_masked", length = 12)
|
@Column(name = "aadhaar_number_masked", length = 12)
|
||||||
private String aadhaarNumberMasked;
|
private String aadhaarNumberMasked;
|
||||||
|
|
||||||
/**
|
|
||||||
* SHA-256 of the 12-digit Aadhaar, computed client-side (see {@link AadhaarHash}). The masked form cannot tell two
|
|
||||||
* people apart; this can. Null for rows that predate the column and for clients with no Aadhaar.
|
|
||||||
*/
|
|
||||||
@Column(name = "aadhaar_hash", length = 64)
|
|
||||||
private String aadhaarHash;
|
|
||||||
|
|
||||||
@Column(name = "aadhaar_verified", nullable = false)
|
@Column(name = "aadhaar_verified", nullable = false)
|
||||||
private boolean aadhaarVerified;
|
private boolean aadhaarVerified;
|
||||||
|
|
||||||
@@ -398,7 +391,6 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
command.stringValueOfParameterNamed("aadhaarNumberMasked"),
|
command.stringValueOfParameterNamed("aadhaarNumberMasked"),
|
||||||
command.stringValueOfParameterNamed("gstinNumber"),
|
command.stringValueOfParameterNamed("gstinNumber"),
|
||||||
command.stringValueOfParameterNamed("remarks"));
|
command.stringValueOfParameterNamed("remarks"));
|
||||||
kyc.aadhaarHash = AadhaarHash.normalizeOrNull(command.stringValueOfParameterNamed("aadhaarHash"));
|
|
||||||
kyc.applyExtendedFields(command);
|
kyc.applyExtendedFields(command);
|
||||||
return kyc;
|
return kyc;
|
||||||
}
|
}
|
||||||
@@ -501,21 +493,6 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
actualChanges.put(aadhaarParam, newValue);
|
actualChanges.put(aadhaarParam, newValue);
|
||||||
recordOverwrite(aadhaarParam, this.aadhaarNumberMasked);
|
recordOverwrite(aadhaarParam, this.aadhaarNumberMasked);
|
||||||
this.aadhaarNumberMasked = StringUtils.defaultIfEmpty(newValue, null);
|
this.aadhaarNumberMasked = StringUtils.defaultIfEmpty(newValue, null);
|
||||||
// The Aadhaar was removed: the fingerprint of the old number must not outlive it.
|
|
||||||
if (this.aadhaarNumberMasked == null) {
|
|
||||||
this.aadhaarHash = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only the fingerprint is sent, never the number. Reported as a flag, not a value, because the changes map
|
|
||||||
// is echoed back in the API response.
|
|
||||||
final String aadhaarHashParam = "aadhaarHash";
|
|
||||||
if (command.parameterExists(aadhaarHashParam)) {
|
|
||||||
final String newHash = AadhaarHash.normalizeOrNull(command.stringValueOfParameterNamed(aadhaarHashParam));
|
|
||||||
if (newHash != null && !newHash.equals(this.aadhaarHash)) {
|
|
||||||
this.aadhaarHash = newHash;
|
|
||||||
actualChanges.put("aadhaarIdentityUpdated", true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final String gstinParam = "gstinNumber";
|
final String gstinParam = "gstinNumber";
|
||||||
@@ -1073,10 +1050,6 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
return aadhaarNumberMasked;
|
return aadhaarNumberMasked;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getAadhaarHash() {
|
|
||||||
return aadhaarHash;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isAadhaarVerified() {
|
public boolean isAadhaarVerified() {
|
||||||
return aadhaarVerified;
|
return aadhaarVerified;
|
||||||
}
|
}
|
||||||
|
|||||||
-7
@@ -27,13 +27,6 @@ public interface ClientKycContactLogRepository extends JpaRepository<ClientKycCo
|
|||||||
|
|
||||||
List<ClientKycContactLog> findByClientIdOrderByContactedAtAsc(Long clientId);
|
List<ClientKycContactLog> findByClientIdOrderByContactedAtAsc(Long clientId);
|
||||||
|
|
||||||
/** True when a notice carrying this marker has already reached a final outcome on the channel. */
|
|
||||||
boolean existsByClientIdAndChannelAndNotesStartingWithAndDeliveryStatusIn(Long clientId, KycContactChannel channel,
|
|
||||||
String markerPrefix, java.util.Collection<KycContactDeliveryStatus> statuses);
|
|
||||||
|
|
||||||
/** How many times a marked notice has been tried on the channel — bounds the retries of a failing gateway. */
|
|
||||||
long countByClientIdAndChannelAndNotesStartingWith(Long clientId, KycContactChannel channel, String markerPrefix);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Every contact log for the given clients in one round trip, so the exceptions grid can render
|
* Every contact log for the given clients in one round trip, so the exceptions grid can render
|
||||||
* "last contacted" for the whole page without an N+1 query per row.
|
* "last contacted" for the whole page without an N+1 query per row.
|
||||||
|
|||||||
-12
@@ -61,18 +61,6 @@ public interface ClientKycRepository extends JpaRepository<ClientKyc, Long> {
|
|||||||
+ "AND k.kycStatus <> :rejectedStatus ORDER BY k.expiryDate ASC")
|
+ "AND k.kycStatus <> :rejectedStatus ORDER BY k.expiryDate ASC")
|
||||||
List<ClientKyc> findLapsedAsOn(@Param("asOn") LocalDate asOn, @Param("rejectedStatus") Integer rejectedStatus);
|
List<ClientKyc> findLapsedAsOn(@Param("asOn") LocalDate asOn, @Param("rejectedStatus") Integer rejectedStatus);
|
||||||
|
|
||||||
/**
|
|
||||||
* KYC records whose re-KYC falls due after {@code asOn} but no later than {@code until}, soonest first.
|
|
||||||
*
|
|
||||||
* <p>The complement of {@link #findLapsedAsOn}: a record is in one list or the other, never both, so the
|
|
||||||
* two together cover everything that needs the branch's attention. Rejected records are excluded for
|
|
||||||
* the same reason as there.
|
|
||||||
*/
|
|
||||||
@Query("SELECT k FROM ClientKyc k WHERE k.expiryDate IS NOT NULL AND k.expiryDate > :asOn "
|
|
||||||
+ "AND k.expiryDate <= :until AND k.kycStatus <> :rejectedStatus ORDER BY k.expiryDate ASC")
|
|
||||||
List<ClientKyc> findExpiringBetween(@Param("asOn") LocalDate asOn, @Param("until") LocalDate until,
|
|
||||||
@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
|
||||||
|
|||||||
-47
@@ -27,7 +27,6 @@ import jakarta.persistence.JoinColumn;
|
|||||||
import jakarta.persistence.ManyToOne;
|
import jakarta.persistence.ManyToOne;
|
||||||
import jakarta.persistence.Table;
|
import jakarta.persistence.Table;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDate;
|
|
||||||
import org.apache.fineract.infrastructure.codes.domain.CodeValue;
|
import org.apache.fineract.infrastructure.codes.domain.CodeValue;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@@ -111,57 +110,11 @@ public class ClientRiskRule {
|
|||||||
@Column(name = "rule_kind", length = 10)
|
@Column(name = "rule_kind", length = 10)
|
||||||
private String ruleKind;
|
private String ruleKind;
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether this scenario applies to Cash Credit accounts - a current account that is allowed to draw an overdraft
|
|
||||||
* against a sanctioned limit. Lets the Rules Master show, in one place, every scenario that watches a CC account,
|
|
||||||
* whatever its coverage status.
|
|
||||||
*/
|
|
||||||
@Column(name = "cc_relevant", nullable = false)
|
|
||||||
private boolean ccRelevant;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A rule authored from the Rules Master starts on trial: it is listed first and marked new until an officer marks it
|
|
||||||
* normal. Every rule that was already in the catalog is normal.
|
|
||||||
*/
|
|
||||||
@Column(name = "on_trial", nullable = false)
|
|
||||||
private boolean onTrial;
|
|
||||||
|
|
||||||
@Column(name = "trial_started_on")
|
|
||||||
private LocalDate trialStartedOn;
|
|
||||||
|
|
||||||
public static final String KIND_KYC = "KYC";
|
public static final String KIND_KYC = "KYC";
|
||||||
public static final String KIND_AML = "AML";
|
public static final String KIND_AML = "AML";
|
||||||
|
|
||||||
protected ClientRiskRule() {}
|
protected ClientRiskRule() {}
|
||||||
|
|
||||||
public boolean isCcRelevant() {
|
|
||||||
return ccRelevant;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isOnTrial() {
|
|
||||||
return onTrial;
|
|
||||||
}
|
|
||||||
|
|
||||||
public LocalDate getTrialStartedOn() {
|
|
||||||
return trialStartedOn;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Puts a newly authored rule on trial from {@code startedOn}. */
|
|
||||||
public void startTrial(final LocalDate startedOn) {
|
|
||||||
this.onTrial = true;
|
|
||||||
this.trialStartedOn = startedOn;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Ends the trial: the rule is a normal part of the catalog. */
|
|
||||||
public void markNormal() {
|
|
||||||
this.onTrial = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Marks the scenario as one that watches Cash Credit accounts, so the Rules Master CC chip lists it. */
|
|
||||||
public void markCcRelevant(final boolean ccRelevant) {
|
|
||||||
this.ccRelevant = ccRelevant;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds a rule authored from the AML Rules Master screen. Occupation and occupation-category
|
* Builds a rule authored from the AML Rules Master screen. Occupation and occupation-category
|
||||||
* are attached separately by the caller, which owns the CodeValue lookup.
|
* are attached separately by the caller, which owns the CodeValue lookup.
|
||||||
|
|||||||
+1
-2
@@ -26,8 +26,7 @@ public interface SearchingClientRepository {
|
|||||||
/**
|
/**
|
||||||
* @param legalFormValue null = no filter; 1 = Individual only; 2 = Non-Individual only.
|
* @param legalFormValue null = no filter; 1 = Individual only; 2 = Non-Individual only.
|
||||||
* Driven by the legal-form enum on Client (LegalForm.PERSON / .ENTITY).
|
* Driven by the legal-form enum on Client (LegalForm.PERSON / .ENTITY).
|
||||||
* @param officeId null = no filter; otherwise only clients of that office, still inside officeHierarchy.
|
|
||||||
*/
|
*/
|
||||||
Page<SearchedClient> searchByText(String searchText, Pageable pageable, String officeHierarchy, boolean activeOnly,
|
Page<SearchedClient> searchByText(String searchText, Pageable pageable, String officeHierarchy, boolean activeOnly,
|
||||||
Integer legalFormValue, Long officeId);
|
Integer legalFormValue);
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-34
@@ -53,7 +53,7 @@ public class SearchingClientRepositoryImpl implements SearchingClientRepository
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Page<SearchedClient> searchByText(String searchText, Pageable pageable, String officeHierarchy, boolean activeOnly,
|
public Page<SearchedClient> searchByText(String searchText, Pageable pageable, String officeHierarchy, boolean activeOnly,
|
||||||
Integer legalFormValue, Long officeId) {
|
Integer legalFormValue) {
|
||||||
/*
|
/*
|
||||||
* this whole thing can be replaced with Spring Data JPA 3+ with a findBy(Specification, Pageable) call but at
|
* this whole thing can be replaced with Spring Data JPA 3+ with a findBy(Specification, Pageable) call but at
|
||||||
* this point the upgrade is too costly
|
* this point the upgrade is too costly
|
||||||
@@ -62,9 +62,6 @@ public class SearchingClientRepositoryImpl implements SearchingClientRepository
|
|||||||
*/
|
*/
|
||||||
String hierarchyLikeValue = officeHierarchy + "%";
|
String hierarchyLikeValue = officeHierarchy + "%";
|
||||||
|
|
||||||
// Customers of other branches that hold an account at the caller's branch are listed here too.
|
|
||||||
final List<Long> linkedClientIds = clientIdsWithAccountAtBranch(hierarchyLikeValue);
|
|
||||||
|
|
||||||
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
|
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
|
||||||
CriteriaQuery<SearchedClient> query = cb.createQuery(SearchedClient.class);
|
CriteriaQuery<SearchedClient> query = cb.createQuery(SearchedClient.class);
|
||||||
|
|
||||||
@@ -77,8 +74,7 @@ public class SearchingClientRepositoryImpl implements SearchingClientRepository
|
|||||||
Join<Client, ClientIdentifier> identity = r.join("identifiers", JoinType.LEFT);
|
Join<Client, ClientIdentifier> identity = r.join("identifiers", JoinType.LEFT);
|
||||||
|
|
||||||
List<Predicate> predicates = new ArrayList<>();
|
List<Predicate> predicates = new ArrayList<>();
|
||||||
final Predicate inHierarchy = cb.like(o.get("hierarchy"), hierarchyLikeValue);
|
predicates.add(cb.like(o.get("hierarchy"), hierarchyLikeValue));
|
||||||
predicates.add(linkedClientIds.isEmpty() ? inHierarchy : cb.or(inHierarchy, r.get("id").in(linkedClientIds)));
|
|
||||||
|
|
||||||
String searchLikeValue = "%" + searchText + "%";
|
String searchLikeValue = "%" + searchText + "%";
|
||||||
predicates.add(cb.or(cb.like(r.get("accountNumber"), searchLikeValue), cb.like(r.get("cifNumber"), searchLikeValue),
|
predicates.add(cb.or(cb.like(r.get("accountNumber"), searchLikeValue), cb.like(r.get("cifNumber"), searchLikeValue),
|
||||||
@@ -97,11 +93,6 @@ public class SearchingClientRepositoryImpl implements SearchingClientRepository
|
|||||||
predicates.add(cb.equal(r.get("legalForm"), legalFormValue));
|
predicates.add(cb.equal(r.get("legalForm"), legalFormValue));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Branch filter. ANDed with the hierarchy predicate above, so it can only narrow results.
|
|
||||||
if (officeId != null) {
|
|
||||||
predicates.add(cb.equal(o.get("id"), officeId));
|
|
||||||
}
|
|
||||||
|
|
||||||
return cb.and(predicates.toArray(new Predicate[0]));
|
return cb.and(predicates.toArray(new Predicate[0]));
|
||||||
};
|
};
|
||||||
criteriaQueryFactory.applySpecificationToCriteria(root, spec, query);
|
criteriaQueryFactory.applySpecificationToCriteria(root, spec, query);
|
||||||
@@ -117,27 +108,4 @@ public class SearchingClientRepositoryImpl implements SearchingClientRepository
|
|||||||
|
|
||||||
return criteriaQueryFactory.readPage(queryToExecute, Client.class, pageable, spec);
|
return criteriaQueryFactory.readPage(queryToExecute, Client.class, pageable, spec);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Ids of clients holding an account (savings, fixed or recurring deposit, or current) at an office at or below the
|
|
||||||
* given hierarchy pattern, whatever office the client itself belongs to. Only accounts opened at a branch other than
|
|
||||||
* the client's carry their own office, so this is empty for a branch with no such customers, and for Head Office.
|
|
||||||
* Being linked to a branch is deliberately not enough to be listed there.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
private List<Long> clientIdsWithAccountAtBranch(final String hierarchyLikeValue) {
|
|
||||||
final List<Object> rows = entityManager.createNativeQuery(
|
|
||||||
"SELECT sa.client_id FROM m_savings_account sa JOIN m_office ao ON ao.id = sa.office_id "
|
|
||||||
+ "WHERE sa.client_id IS NOT NULL AND ao.hierarchy LIKE ?1 "
|
|
||||||
+ "UNION SELECT ca.client_id FROM m_current_account ca JOIN m_office co ON co.id = ca.office_id "
|
|
||||||
+ "WHERE ca.client_id IS NOT NULL AND co.hierarchy LIKE ?2")
|
|
||||||
// Positional, not named: EclipseLink passes a native query's ":h" to PostgreSQL as written, which fails
|
|
||||||
// with a syntax error and took the whole client list down with it.
|
|
||||||
.setParameter(1, hierarchyLikeValue).setParameter(2, hierarchyLikeValue).getResultList();
|
|
||||||
final List<Long> ids = new ArrayList<>(rows.size());
|
|
||||||
for (final Object row : rows) {
|
|
||||||
ids.add(((Number) row).longValue());
|
|
||||||
}
|
|
||||||
return ids;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ public enum NoteType {
|
|||||||
GROUP(600, "noteType.group", "groups", "Group note"), //
|
GROUP(600, "noteType.group", "groups", "Group note"), //
|
||||||
SHARE_ACCOUNT(700, "noteType.shares", "accounts/share", "Share account note"), //
|
SHARE_ACCOUNT(700, "noteType.shares", "accounts/share", "Share account note"), //
|
||||||
SAVINGS_TRANSACTION(800, "noteType.savings.transaction", "savingsTransactions", "Savings transaction note"), //
|
SAVINGS_TRANSACTION(800, "noteType.savings.transaction", "savingsTransactions", "Savings transaction note"), //
|
||||||
CURRENT_ACCOUNT(900, "noteType.currentaccount", "currentaccounts", "Current account note"), //
|
|
||||||
;
|
;
|
||||||
|
|
||||||
public static final NoteType[] VALUES = values();
|
public static final NoteType[] VALUES = values();
|
||||||
|
|||||||
+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
-14
@@ -32,8 +32,7 @@ public enum DepositAccountOnClosureType {
|
|||||||
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"), //
|
PARTIAL_REINVEST(500, "depositAccountClosureType.partialReinvest"); //
|
||||||
TRANSFER_TO_GL(600, "depositAccountClosureType.transferToGl"); //
|
|
||||||
|
|
||||||
private final Integer value;
|
private final Integer value;
|
||||||
private final String code;
|
private final String code;
|
||||||
@@ -54,7 +53,6 @@ public enum DepositAccountOnClosureType {
|
|||||||
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;
|
case 500 -> PARTIAL_REINVEST;
|
||||||
case 600 -> TRANSFER_TO_GL;
|
|
||||||
default -> INVALID;
|
default -> INVALID;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -93,17 +91,6 @@ public enum DepositAccountOnClosureType {
|
|||||||
return this.equals(PARTIAL_REINVEST);
|
return this.equals(PARTIAL_REINVEST);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Pays the closure proceeds into a GL account the maker picks, instead of a savings account or cash.
|
|
||||||
* Only the fixed-deposit premature-closure handler implements it -- see
|
|
||||||
* DepositAccountDomainServiceJpa#handleFDAccountPreMatureClosure; every other closure path rejects it
|
|
||||||
* in DepositAccountTransactionDataValidator#validateClosing rather than falling through to the
|
|
||||||
* terminal "pay the balance out in cash" branch.
|
|
||||||
*/
|
|
||||||
public boolean isTransferToGl() {
|
|
||||||
return this.equals(TRANSFER_TO_GL);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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();
|
||||||
|
|||||||
+3
-35
@@ -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,18 +208,7 @@ public final class DepositsApiConstants {
|
|||||||
public static final String onAccountClosureIdParamName = "onAccountClosureId";
|
public static final String onAccountClosureIdParamName = "onAccountClosureId";
|
||||||
public static final String transferDescriptionParamName = "transferDescription";
|
public static final String transferDescriptionParamName = "transferDescription";
|
||||||
public static final String toSavingsAccountIdParamName = "toSavingsAccountId";
|
public static final String toSavingsAccountIdParamName = "toSavingsAccountId";
|
||||||
// Destination GL account for DepositAccountOnClosureType.TRANSFER_TO_GL -- the closure proceeds are
|
|
||||||
// credited straight to this GL instead of a savings account or cash.
|
|
||||||
public static final String toGlAccountIdParamName = "toGlAccountId";
|
|
||||||
public static final String reinvestAmountParamName = "reinvestAmount";
|
public static final String reinvestAmountParamName = "reinvestAmount";
|
||||||
// Lets a maker waive the product's configured pre-closure penalty for one premature closure --
|
|
||||||
// see FixedDepositAccount#waivePreClosurePenaltyForThisRequest for why this is a transient
|
|
||||||
// per-request flag rather than a stored account field.
|
|
||||||
public static final String waivePreClosurePenaltyParamName = "waivePreClosurePenalty";
|
|
||||||
// Why the deposit was closed before maturity -- see PrematureClosureReasonType. Persisted (unlike
|
|
||||||
// waivePreClosurePenaltyParamName above) as the RBI audit trail of which closures waived their
|
|
||||||
// penalty and why.
|
|
||||||
public static final String prematureClosureReasonIdParamName = "prematureClosureReasonId";
|
|
||||||
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";
|
||||||
@@ -252,7 +237,7 @@ public final class DepositsApiConstants {
|
|||||||
SavingProductAccountingParams.PENALTY_INCOME_ACCOUNT_MAPPING.getValue(),
|
SavingProductAccountingParams.PENALTY_INCOME_ACCOUNT_MAPPING.getValue(),
|
||||||
SavingProductAccountingParams.INTEREST_PAYABLE.getValue(), SavingProductAccountingParams.PENALTIES_RECEIVABLE.getValue(),
|
SavingProductAccountingParams.INTEREST_PAYABLE.getValue(), SavingProductAccountingParams.PENALTIES_RECEIVABLE.getValue(),
|
||||||
SavingProductAccountingParams.FEES_RECEIVABLE.getValue(), chartsParamName, SavingsApiConstants.withHoldTaxParamName,
|
SavingProductAccountingParams.FEES_RECEIVABLE.getValue(), chartsParamName, SavingsApiConstants.withHoldTaxParamName,
|
||||||
SavingsApiConstants.taxGroupIdParamName, SavingsApiConstants.otherGlMappingsParamName));
|
SavingsApiConstants.taxGroupIdParamName));
|
||||||
|
|
||||||
private static final Set<String> PRECLOSURE_REQUEST_DATA_PARAMETERS = new HashSet<>(
|
private static final Set<String> PRECLOSURE_REQUEST_DATA_PARAMETERS = new HashSet<>(
|
||||||
Arrays.asList(preClosurePenalApplicableParamName, preClosurePenalInterestParamName, preClosurePenalInterestOnTypeIdParamName));
|
Arrays.asList(preClosurePenalApplicableParamName, preClosurePenalInterestParamName, preClosurePenalInterestOnTypeIdParamName));
|
||||||
@@ -262,13 +247,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,
|
||||||
@@ -390,8 +373,6 @@ public final class DepositsApiConstants {
|
|||||||
fixedDepositRequestData.add("paymentChannelToFundSourceMappings");
|
fixedDepositRequestData.add("paymentChannelToFundSourceMappings");
|
||||||
fixedDepositRequestData.add("feeToIncomeAccountMappings");
|
fixedDepositRequestData.add("feeToIncomeAccountMappings");
|
||||||
fixedDepositRequestData.add("penaltyToIncomeAccountMappings");
|
fixedDepositRequestData.add("penaltyToIncomeAccountMappings");
|
||||||
fixedDepositRequestData.add(SavingsApiConstants.otherGlMappingsParamName);
|
|
||||||
fixedDepositRequestData.add("otherGlMappingsMap");
|
|
||||||
return fixedDepositRequestData;
|
return fixedDepositRequestData;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -432,10 +413,6 @@ public final class DepositsApiConstants {
|
|||||||
recurringDepositRequestData.add("prematureWithdrawalOption");
|
recurringDepositRequestData.add("prematureWithdrawalOption");
|
||||||
recurringDepositRequestData.add("tenureFlexibility");
|
recurringDepositRequestData.add("tenureFlexibility");
|
||||||
recurringDepositRequestData.add("lienMarked");
|
recurringDepositRequestData.add("lienMarked");
|
||||||
recurringDepositRequestData.add("lienPercentage");
|
|
||||||
recurringDepositRequestData.add("missedInstallmentGraceDays");
|
|
||||||
recurringDepositRequestData.add("missedInstallmentPenaltyType");
|
|
||||||
recurringDepositRequestData.add("missedInstallmentPenaltyAmount");
|
|
||||||
recurringDepositRequestData.add("withdrawalFeeForTransfers");
|
recurringDepositRequestData.add("withdrawalFeeForTransfers");
|
||||||
recurringDepositRequestData.add("enableLockinPeriod");
|
recurringDepositRequestData.add("enableLockinPeriod");
|
||||||
recurringDepositRequestData.add("maturityInstructions");
|
recurringDepositRequestData.add("maturityInstructions");
|
||||||
@@ -495,10 +472,6 @@ public final class DepositsApiConstants {
|
|||||||
recurringDepositRequestData.add("prematureWithdrawalOption");
|
recurringDepositRequestData.add("prematureWithdrawalOption");
|
||||||
recurringDepositRequestData.add("tenureFlexibility");
|
recurringDepositRequestData.add("tenureFlexibility");
|
||||||
recurringDepositRequestData.add("lienMarked");
|
recurringDepositRequestData.add("lienMarked");
|
||||||
recurringDepositRequestData.add("lienPercentage");
|
|
||||||
recurringDepositRequestData.add("missedInstallmentGraceDays");
|
|
||||||
recurringDepositRequestData.add("missedInstallmentPenaltyType");
|
|
||||||
recurringDepositRequestData.add("missedInstallmentPenaltyAmount");
|
|
||||||
recurringDepositRequestData.add("maturityInstructions");
|
recurringDepositRequestData.add("maturityInstructions");
|
||||||
recurringDepositRequestData.add("creditMaturityProceeds");
|
recurringDepositRequestData.add("creditMaturityProceeds");
|
||||||
recurringDepositRequestData.add("penaltyGracePeriodMissedInstallments");
|
recurringDepositRequestData.add("penaltyGracePeriodMissedInstallments");
|
||||||
@@ -511,8 +484,6 @@ public final class DepositsApiConstants {
|
|||||||
recurringDepositRequestData.add("paymentChannelToFundSourceMappings");
|
recurringDepositRequestData.add("paymentChannelToFundSourceMappings");
|
||||||
recurringDepositRequestData.add("feeToIncomeAccountMappings");
|
recurringDepositRequestData.add("feeToIncomeAccountMappings");
|
||||||
recurringDepositRequestData.add("penaltyToIncomeAccountMappings");
|
recurringDepositRequestData.add("penaltyToIncomeAccountMappings");
|
||||||
recurringDepositRequestData.add(SavingsApiConstants.otherGlMappingsParamName);
|
|
||||||
recurringDepositRequestData.add("otherGlMappingsMap");
|
|
||||||
return recurringDepositRequestData;
|
return recurringDepositRequestData;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -540,7 +511,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
|
||||||
@@ -656,8 +626,6 @@ public final class DepositsApiConstants {
|
|||||||
recurringDepositRequestData.add(SavingsApiConstants.balanceSwapOptionParamName);
|
recurringDepositRequestData.add(SavingsApiConstants.balanceSwapOptionParamName);
|
||||||
recurringDepositRequestData.add(SavingsApiConstants.msmeParamName);
|
recurringDepositRequestData.add(SavingsApiConstants.msmeParamName);
|
||||||
recurringDepositRequestData.add(SavingsApiConstants.autoDebitFacilityParamName);
|
recurringDepositRequestData.add(SavingsApiConstants.autoDebitFacilityParamName);
|
||||||
// Lien Marked: how much of this deposit may be given as loan, capped by its product's percentage.
|
|
||||||
recurringDepositRequestData.add("lienPercentage");
|
|
||||||
recurringDepositRequestData.add(isSeniorCitizenParamName);
|
recurringDepositRequestData.add(isSeniorCitizenParamName);
|
||||||
recurringDepositRequestData.add(seniorCitizenInterestRateParamName);
|
recurringDepositRequestData.add(seniorCitizenInterestRateParamName);
|
||||||
return recurringDepositRequestData;
|
return recurringDepositRequestData;
|
||||||
|
|||||||
-100
@@ -1,100 +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.savings;
|
|
||||||
|
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The deposit scheme a fixed deposit product is sold under, held as {@code m_savings_product.fd_type}.
|
|
||||||
*
|
|
||||||
* <p>
|
|
||||||
* The three schemes differ in where a posting of interest ends up, which is what the interest posting
|
|
||||||
* report has to show and the posting itself has to honour:
|
|
||||||
*
|
|
||||||
* <ul>
|
|
||||||
* <li>{@link #SINGLE} — short deposits, priced from a slab table in days. Interest stays in the
|
|
||||||
* deposit and is paid with the principal at maturity.</li>
|
|
||||||
* <li>{@link #DOUBLE} — deposits of a year or more, priced from a slab table in years. Interest is
|
|
||||||
* compounded back into the deposit, so the maturity value is principal plus interest.</li>
|
|
||||||
* <li>{@link #SPECIAL} — the monthly income scheme. Interest is posted monthly and swept out to the
|
|
||||||
* customer's linked savings account, which is why a special deposit cannot be opened without one;
|
|
||||||
* the deposit itself matures at its principal, the interest having already been paid away.</li>
|
|
||||||
* </ul>
|
|
||||||
*
|
|
||||||
* <p>
|
|
||||||
* The database column is a string rather than an enum ordinal, so the names here are the stored
|
|
||||||
* values and must not be renamed. {@link #fromString(String)} is lenient about case and blanks
|
|
||||||
* because the column carries a default of SINGLE and older products predate the column entirely.
|
|
||||||
*/
|
|
||||||
public enum FixedDepositType {
|
|
||||||
|
|
||||||
/** Interest is held in the deposit and paid at maturity. The default for a product that names none. */
|
|
||||||
SINGLE,
|
|
||||||
/** Interest is compounded into the deposit; the maturity value includes it. */
|
|
||||||
DOUBLE,
|
|
||||||
/** Interest is posted monthly and transferred to the linked savings account. */
|
|
||||||
SPECIAL;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The scheme named by a stored {@code fd_type}, defaulting to {@link #SINGLE}.
|
|
||||||
*
|
|
||||||
* <p>
|
|
||||||
* A null, blank or unrecognised value reads as SINGLE: that is the column's own default, and a
|
|
||||||
* product carrying something else is a misconfiguration that must not stop its customers' interest
|
|
||||||
* from being reported.
|
|
||||||
*/
|
|
||||||
public static FixedDepositType fromString(final String fdType) {
|
|
||||||
if (fdType == null || fdType.isBlank()) {
|
|
||||||
return SINGLE;
|
|
||||||
}
|
|
||||||
for (final FixedDepositType candidate : values()) {
|
|
||||||
if (candidate.name().equalsIgnoreCase(fdType.trim())) {
|
|
||||||
return candidate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return SINGLE;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isValid(final String fdType) {
|
|
||||||
if (fdType == null || fdType.isBlank()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
for (final FixedDepositType candidate : values()) {
|
|
||||||
if (candidate.name().equalsIgnoreCase(fdType.trim())) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Whether interest leaves the deposit for the linked savings account each time it is posted. */
|
|
||||||
public boolean isPaidOutMonthly() {
|
|
||||||
return this == SPECIAL;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Whether a posting of interest adds to the deposit's own balance. */
|
|
||||||
public boolean isRetainedInDeposit() {
|
|
||||||
return this != SPECIAL;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** What a customer sees: "FD Special", "FD Double", "FD Single". */
|
|
||||||
public String displayName() {
|
|
||||||
return "FD " + name().charAt(0) + name().substring(1).toLowerCase(Locale.ENGLISH);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-70
@@ -1,70 +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.savings;
|
|
||||||
|
|
||||||
import lombok.Getter;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Why a Fixed/Recurring Deposit was closed before maturity -- captured on every premature closure, not
|
|
||||||
* just the ones that waive the penalty, so the audit trail always shows a reason rather than only
|
|
||||||
* recording one when it happens to matter. {@link #isAutoWaived()} is the RBI-driven business rule this
|
|
||||||
* type exists to encode: a maker cannot leave the platform's own penalty-waiver decision to memory or a
|
|
||||||
* free-text note for these four reasons -- picking the reason IS the waiver decision, enforced on the
|
|
||||||
* server regardless of what a client happened to also send for the plain waive-penalty flag.
|
|
||||||
*/
|
|
||||||
@Getter
|
|
||||||
public enum PrematureClosureReasonType {
|
|
||||||
|
|
||||||
INVALID(0, "prematureClosureReasonType.invalid", false), //
|
|
||||||
NORMAL(100, "prematureClosureReasonType.normal", false), //
|
|
||||||
PREMATURE_EXTENSION(200, "prematureClosureReasonType.prematureExtension", true), //
|
|
||||||
CONVERSION_TO_ANOTHER_TD(300, "prematureClosureReasonType.conversionToAnotherTd", true), //
|
|
||||||
DECEASED_SETTLEMENT(400, "prematureClosureReasonType.deceasedSettlement", true), //
|
|
||||||
NOMINEE_LEGAL_HEIR_CLOSURE(500, "prematureClosureReasonType.nomineeLegalHeirClosure", true); //
|
|
||||||
|
|
||||||
private final Integer value;
|
|
||||||
private final String code;
|
|
||||||
/** True when this reason, by itself, waives the pre-closure penalty regardless of any manual
|
|
||||||
* waive-penalty flag also sent -- see the class javadoc. */
|
|
||||||
private final boolean autoWaived;
|
|
||||||
|
|
||||||
PrematureClosureReasonType(final Integer value, final String code, final boolean autoWaived) {
|
|
||||||
this.value = value;
|
|
||||||
this.code = code;
|
|
||||||
this.autoWaived = autoWaived;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static PrematureClosureReasonType fromInt(final Integer v) {
|
|
||||||
if (v == null) {
|
|
||||||
return INVALID;
|
|
||||||
}
|
|
||||||
return switch (v) {
|
|
||||||
case 100 -> NORMAL;
|
|
||||||
case 200 -> PREMATURE_EXTENSION;
|
|
||||||
case 300 -> CONVERSION_TO_ANOTHER_TD;
|
|
||||||
case 400 -> DECEASED_SETTLEMENT;
|
|
||||||
case 500 -> NOMINEE_LEGAL_HEIR_CLOSURE;
|
|
||||||
default -> INVALID;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isInvalid() {
|
|
||||||
return this.equals(INVALID);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-9
@@ -287,15 +287,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";
|
||||||
|
|||||||
-14
@@ -234,20 +234,6 @@ public final class SavingsAccountData implements Serializable {
|
|||||||
return this.transferInterestToLinkedAccount;
|
return this.transferInterestToLinkedAccount;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* True for a recurring deposit opened under a product whose Interest Calculation Type is "Simple": its interest
|
|
||||||
* never compounds. Transient because it is only read by the batch interest-posting engine.
|
|
||||||
*/
|
|
||||||
private transient boolean simpleInterest;
|
|
||||||
|
|
||||||
public void setSimpleInterest(final boolean simpleInterest) {
|
|
||||||
this.simpleInterest = simpleInterest;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isSimpleInterest() {
|
|
||||||
return this.simpleInterest;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static SavingsAccountData importInstanceIndividual(Long clientId, Long productId, Long fieldOfficerId, LocalDate submittedOnDate,
|
public static SavingsAccountData importInstanceIndividual(Long clientId, Long productId, Long fieldOfficerId, LocalDate submittedOnDate,
|
||||||
BigDecimal nominalAnnualInterestRate, EnumOptionData interestCompoundingPeriodTypeEnum,
|
BigDecimal nominalAnnualInterestRate, EnumOptionData interestCompoundingPeriodTypeEnum,
|
||||||
EnumOptionData interestPostingPeriodTypeEnum, EnumOptionData interestCalculationTypeEnum,
|
EnumOptionData interestPostingPeriodTypeEnum, EnumOptionData interestCalculationTypeEnum,
|
||||||
|
|||||||
-10
@@ -153,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;
|
||||||
@@ -388,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());
|
||||||
|
|||||||
-6
@@ -180,12 +180,6 @@ public final class SavingsHelper {
|
|||||||
immediateWithdrawalOfInterest);
|
immediateWithdrawalOfInterest);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Money calculateInterestForAllPostingPeriods(final MonetaryCurrency currency, final List<PostingPeriod> allPeriods,
|
|
||||||
LocalDate accountLockedUntil, Boolean immediateWithdrawalOfInterest, final boolean simpleInterest) {
|
|
||||||
return COMPOUND_INTEREST_HELPER.calculateInterestForAllPostingPeriods(currency, allPeriods, accountLockedUntil,
|
|
||||||
immediateWithdrawalOfInterest, simpleInterest);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Collection<Long> fetchPostInterestTransactionIds(Long accountId) {
|
public Collection<Long> fetchPostInterestTransactionIds(Long accountId) {
|
||||||
return this.accountTransfersReadPlatformService.fetchPostInterestTransactionIds(accountId);
|
return this.accountTransfersReadPlatformService.fetchPostInterestTransactionIds(accountId);
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-35
@@ -40,18 +40,6 @@ public class CompoundInterestHelper {
|
|||||||
*/
|
*/
|
||||||
public Money calculateInterestForAllPostingPeriods(final MonetaryCurrency currency, final List<PostingPeriod> allPeriods,
|
public Money calculateInterestForAllPostingPeriods(final MonetaryCurrency currency, final List<PostingPeriod> allPeriods,
|
||||||
LocalDate lockUntil, Boolean interestTransferEnabled) {
|
LocalDate lockUntil, Boolean interestTransferEnabled) {
|
||||||
return calculateInterestForAllPostingPeriods(currency, allPeriods, lockUntil, interestTransferEnabled, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param simpleInterest
|
|
||||||
* - when true, earned interest is never added back to the balance it is earned on, so every period
|
|
||||||
* earns on the principal alone (a recurring deposit whose product's Interest Calculation Type is
|
|
||||||
* "Simple"). Compounding and posting periods still decide when interest is recognised and posted;
|
|
||||||
* they just stop changing how much is earned.
|
|
||||||
*/
|
|
||||||
public Money calculateInterestForAllPostingPeriods(final MonetaryCurrency currency, final List<PostingPeriod> allPeriods,
|
|
||||||
LocalDate lockUntil, Boolean interestTransferEnabled, final boolean simpleInterest) {
|
|
||||||
|
|
||||||
// sum up the 'rounded' values that are posted each posting period
|
// sum up the 'rounded' values that are posted each posting period
|
||||||
Money interestEarned = Money.zero(currency);
|
Money interestEarned = Money.zero(currency);
|
||||||
@@ -59,8 +47,7 @@ public class CompoundInterestHelper {
|
|||||||
// total interest earned in previous periods but not yet recognised
|
// total interest earned in previous periods but not yet recognised
|
||||||
BigDecimal compoundedInterest = BigDecimal.ZERO;
|
BigDecimal compoundedInterest = BigDecimal.ZERO;
|
||||||
BigDecimal unCompoundedInterest = BigDecimal.ZERO;
|
BigDecimal unCompoundedInterest = BigDecimal.ZERO;
|
||||||
final CompoundInterestValues compoundInterestValues = simpleInterest ? new SimpleInterestValues()
|
final CompoundInterestValues compoundInterestValues = new CompoundInterestValues(compoundedInterest, unCompoundedInterest);
|
||||||
: new CompoundInterestValues(compoundedInterest, unCompoundedInterest);
|
|
||||||
for (final PostingPeriod postingPeriod : allPeriods) {
|
for (final PostingPeriod postingPeriod : allPeriods) {
|
||||||
|
|
||||||
final BigDecimal interestEarnedThisPeriod = postingPeriod.calculateInterest(compoundInterestValues);
|
final BigDecimal interestEarnedThisPeriod = postingPeriod.calculateInterest(compoundInterestValues);
|
||||||
@@ -80,25 +67,4 @@ public class CompoundInterestHelper {
|
|||||||
|
|
||||||
return interestEarned;
|
return interestEarned;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Interest values that refuse to compound: whatever a compounding period hands over to be compounded is dropped,
|
|
||||||
* so the balance interest is earned on never includes interest already earned.
|
|
||||||
*/
|
|
||||||
private static final class SimpleInterestValues extends CompoundInterestValues {
|
|
||||||
|
|
||||||
SimpleInterestValues() {
|
|
||||||
super(BigDecimal.ZERO, BigDecimal.ZERO);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setCompoundedInterest(final BigDecimal interestToBeCompounded) {
|
|
||||||
super.setCompoundedInterest(BigDecimal.ZERO);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isSimpleInterest() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
-5
@@ -50,9 +50,4 @@ public class CompoundInterestValues {
|
|||||||
this.uncompoundedInterest = BigDecimal.ZERO;
|
this.uncompoundedInterest = BigDecimal.ZERO;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** True when earned interest must never earn interest itself - not across periods, and not within one either. */
|
|
||||||
public boolean isSimpleInterest() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
-12
@@ -94,18 +94,6 @@ public final class DailyCompoundingPeriod implements CompoundingPeriod {
|
|||||||
return interestEarned;
|
return interestEarned;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Interest on each day's balance alone, none of it added to the balance - for simple interest. */
|
|
||||||
public BigDecimal calculateSimpleInterest(final BigDecimal interestRateAsFraction, final long daysInYear,
|
|
||||||
final BigDecimal minBalanceForInterestCalculation, final BigDecimal overdraftInterestRateAsFraction,
|
|
||||||
final BigDecimal minOverdraftForInterestCalculation) {
|
|
||||||
BigDecimal interestEarned = BigDecimal.ZERO;
|
|
||||||
for (final EndOfDayBalance balance : this.endOfDayBalances) {
|
|
||||||
interestEarned = interestEarned.add(balance.calculateInterestOnBalance(BigDecimal.ZERO, interestRateAsFraction, daysInYear,
|
|
||||||
minBalanceForInterestCalculation, overdraftInterestRateAsFraction, minOverdraftForInterestCalculation));
|
|
||||||
}
|
|
||||||
return interestEarned;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public LocalDateInterval getPeriodInterval() {
|
public LocalDateInterval getPeriodInterval() {
|
||||||
return this.periodInterval;
|
return this.periodInterval;
|
||||||
|
|||||||
+4
-11
@@ -301,17 +301,10 @@ public final class PostingPeriod {
|
|||||||
// to be applied to the balanced for interest calculation
|
// to be applied to the balanced for interest calculation
|
||||||
for (final CompoundingPeriod compoundingPeriod : this.compoundingPeriods) {
|
for (final CompoundingPeriod compoundingPeriod : this.compoundingPeriods) {
|
||||||
|
|
||||||
// A daily compounding period compounds day by day inside itself, so simple interest has to bypass it - zeroing
|
final BigDecimal interestUnrounded = compoundingPeriod.calculateInterest(this.interestCompoundingType,
|
||||||
// what is carried between periods alone still left a month's interest earning interest within the month.
|
this.interestCalculationType, compoundInterestValues.getcompoundedInterest(), this.interestRateAsFraction,
|
||||||
final BigDecimal interestUnrounded = compoundInterestValues.isSimpleInterest()
|
this.daysInYear, this.minBalanceForInterestCalculation.getAmount(), this.overdraftInterestRateAsFraction,
|
||||||
&& compoundingPeriod instanceof DailyCompoundingPeriod dailyPeriod
|
this.minOverdraftForInterestCalculation.getAmount());
|
||||||
? dailyPeriod.calculateSimpleInterest(this.interestRateAsFraction, this.daysInYear,
|
|
||||||
this.minBalanceForInterestCalculation.getAmount(), this.overdraftInterestRateAsFraction,
|
|
||||||
this.minOverdraftForInterestCalculation.getAmount())
|
|
||||||
: compoundingPeriod.calculateInterest(this.interestCompoundingType, this.interestCalculationType,
|
|
||||||
compoundInterestValues.getcompoundedInterest(), this.interestRateAsFraction, this.daysInYear,
|
|
||||||
this.minBalanceForInterestCalculation.getAmount(), this.overdraftInterestRateAsFraction,
|
|
||||||
this.minOverdraftForInterestCalculation.getAmount());
|
|
||||||
BigDecimal unCompoundedInterest = compoundInterestValues.getuncompoundedInterest().add(interestUnrounded);
|
BigDecimal unCompoundedInterest = compoundInterestValues.getuncompoundedInterest().add(interestUnrounded);
|
||||||
compoundInterestValues.setuncompoundedInterest(unCompoundedInterest);
|
compoundInterestValues.setuncompoundedInterest(unCompoundedInterest);
|
||||||
LocalDate compoundingPeriodEndDate = compoundingPeriod.getPeriodInterval().endDate();
|
LocalDate compoundingPeriodEndDate = compoundingPeriod.getPeriodInterval().endDate();
|
||||||
|
|||||||
-4
@@ -801,10 +801,6 @@ public final class SavingsEnumerations {
|
|||||||
optionData = new EnumOptionData(DepositAccountOnClosureType.PARTIAL_REINVEST.getValue().longValue(),
|
optionData = new EnumOptionData(DepositAccountOnClosureType.PARTIAL_REINVEST.getValue().longValue(),
|
||||||
DepositAccountOnClosureType.PARTIAL_REINVEST.getCode(), "Partial Re-Invest");
|
DepositAccountOnClosureType.PARTIAL_REINVEST.getCode(), "Partial Re-Invest");
|
||||||
break;
|
break;
|
||||||
case TRANSFER_TO_GL:
|
|
||||||
optionData = new EnumOptionData(DepositAccountOnClosureType.TRANSFER_TO_GL.getValue().longValue(),
|
|
||||||
DepositAccountOnClosureType.TRANSFER_TO_GL.getCode(), "Transfer to GL Account");
|
|
||||||
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;
|
||||||
|
|
||||||
|
|||||||
+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) {
|
||||||
|
|||||||
-12
@@ -28,22 +28,10 @@ public interface CurrentAccountReadPlatformService {
|
|||||||
|
|
||||||
Map<String, Object> retrieveOneTransaction(Long accountId, Long transactionId);
|
Map<String, Object> retrieveOneTransaction(Long accountId, Long transactionId);
|
||||||
|
|
||||||
/** The account's charges from m_current_account_charge; chargeStatus is all, active or inactive. */
|
|
||||||
List<Map<String, Object>> retrieveCharges(Long accountId, String chargeStatus);
|
|
||||||
|
|
||||||
Map<String, Object> retrieveCharge(Long accountId, Long accountChargeId);
|
|
||||||
|
|
||||||
/** Template for adding a charge: the active current-account charges in the account's currency. */
|
|
||||||
Map<String, Object> retrieveChargeTemplate(Long accountId);
|
|
||||||
|
|
||||||
List<Map<String, Object>> retrieveAll();
|
List<Map<String, Object>> retrieveAll();
|
||||||
|
|
||||||
List<Map<String, Object>> retrieveAll(Integer offset, Integer limit, String accountNo, String clientName);
|
List<Map<String, Object>> retrieveAll(Integer offset, Integer limit, String accountNo, String clientName);
|
||||||
|
|
||||||
List<Map<String, Object>> retrieveAll(Integer offset, Integer limit, String accountNo, String clientName,
|
List<Map<String, Object>> retrieveAll(Integer offset, Integer limit, String accountNo, String clientName,
|
||||||
String status, LocalDate fromDate, LocalDate toDate, LocalDate asOnDate);
|
String status, LocalDate fromDate, LocalDate toDate, LocalDate asOnDate);
|
||||||
|
|
||||||
/** As above; with {@code overdraftOnly}, only the Cash Credit accounts - those that allow an overdraft. */
|
|
||||||
List<Map<String, Object>> retrieveAll(Integer offset, Integer limit, String accountNo, String clientName,
|
|
||||||
String status, LocalDate fromDate, LocalDate toDate, LocalDate asOnDate, boolean overdraftOnly);
|
|
||||||
}
|
}
|
||||||
|
|||||||
-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; }
|
|
||||||
}
|
}
|
||||||
|
|||||||
-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;
|
||||||
|
|||||||
-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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-8
@@ -268,9 +268,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 +393,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-41
@@ -18,29 +18,20 @@
|
|||||||
*/
|
*/
|
||||||
package org.apache.fineract.infrastructure.documentmanagement.service;
|
package org.apache.fineract.infrastructure.documentmanagement.service;
|
||||||
|
|
||||||
import java.io.FileNotFoundException;
|
|
||||||
import java.nio.file.NoSuchFileException;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.apache.fineract.infrastructure.contentstore.service.ContentStoreService;
|
import org.apache.fineract.infrastructure.contentstore.service.ContentStoreService;
|
||||||
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
|
|
||||||
import org.apache.fineract.infrastructure.documentmanagement.data.DocumentContent;
|
import org.apache.fineract.infrastructure.documentmanagement.data.DocumentContent;
|
||||||
import org.apache.fineract.infrastructure.documentmanagement.data.DocumentData;
|
import org.apache.fineract.infrastructure.documentmanagement.data.DocumentData;
|
||||||
import org.apache.fineract.infrastructure.documentmanagement.domain.DocumentRepository;
|
import org.apache.fineract.infrastructure.documentmanagement.domain.DocumentRepository;
|
||||||
import org.apache.fineract.infrastructure.documentmanagement.exception.DocumentInvalidRequestException;
|
|
||||||
import org.apache.fineract.infrastructure.documentmanagement.exception.DocumentNotFoundException;
|
import org.apache.fineract.infrastructure.documentmanagement.exception.DocumentNotFoundException;
|
||||||
import org.apache.fineract.infrastructure.documentmanagement.mapping.DocumentMapper;
|
import org.apache.fineract.infrastructure.documentmanagement.mapping.DocumentMapper;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class DocumentReadPlatformServiceImpl implements DocumentReadPlatformService {
|
public class DocumentReadPlatformServiceImpl implements DocumentReadPlatformService {
|
||||||
|
|
||||||
/** Bound on the cause walk below, so a self-referencing cause chain cannot spin. */
|
|
||||||
private static final int MAX_CAUSE_DEPTH = 10;
|
|
||||||
|
|
||||||
private final ContentStoreService storeService;
|
private final ContentStoreService storeService;
|
||||||
private final DocumentRepository documentRepository;
|
private final DocumentRepository documentRepository;
|
||||||
private final DocumentMapper documentMapper;
|
private final DocumentMapper documentMapper;
|
||||||
@@ -61,39 +52,9 @@ public class DocumentReadPlatformServiceImpl implements DocumentReadPlatformServ
|
|||||||
public DocumentContent retrieveDocumentContent(final String entityType, final Long entityId, final Long documentId) {
|
public DocumentContent retrieveDocumentContent(final String entityType, final Long entityId, final Long documentId) {
|
||||||
final var doc = documentRepository.findByIdAndParentEntityTypeAndParentEntityId(documentId, entityType, entityId)
|
final var doc = documentRepository.findByIdAndParentEntityTypeAndParentEntityId(documentId, entityType, entityId)
|
||||||
.orElseThrow(() -> new DocumentNotFoundException(entityType, entityId, documentId));
|
.orElseThrow(() -> new DocumentNotFoundException(entityType, entityId, documentId));
|
||||||
try {
|
final var is = storeService.download(doc.getLocation());
|
||||||
final var is = storeService.download(doc.getLocation());
|
|
||||||
return documentMapper.map(doc, is);
|
|
||||||
} catch (final AbstractPlatformDomainRuleException e) {
|
|
||||||
throw e;
|
|
||||||
} catch (final Exception e) {
|
|
||||||
if (isMissingFromContentStore(e)) {
|
|
||||||
// The m_document row survives but the file it points at does not — the usual cause is
|
|
||||||
// a database copied between environments without its file store (see the identical
|
|
||||||
// guard in ImageReadPlatformServiceImpl). For the caller that is simply "this document
|
|
||||||
// has no content", not a server fault, so report it the same way as a missing document
|
|
||||||
// instead of letting a raw ContentStoreException surface as a 500.
|
|
||||||
log.warn("Document {} for {} {} is recorded but missing from the content store; treating it as not found", documentId,
|
|
||||||
entityType, entityId);
|
|
||||||
throw new DocumentNotFoundException(entityType, entityId, documentId);
|
|
||||||
}
|
|
||||||
throw new DocumentInvalidRequestException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
return documentMapper.map(doc, is);
|
||||||
* Whether a failure bottoms out in "the stored file is not there", however it was wrapped on the
|
|
||||||
* way up (the file store rethrows as ContentStoreException).
|
|
||||||
*/
|
|
||||||
private boolean isMissingFromContentStore(final Throwable failure) {
|
|
||||||
Throwable cause = failure;
|
|
||||||
for (int depth = 0; cause != null && depth < MAX_CAUSE_DEPTH; depth++) {
|
|
||||||
if (cause instanceof NoSuchFileException || cause instanceof FileNotFoundException) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
cause = cause.getCause();
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
-189
@@ -1,189 +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.loanaccount.domain.transactionprocessor.impl;
|
|
||||||
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
|
|
||||||
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
|
|
||||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
|
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
|
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping;
|
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor;
|
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
|
|
||||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator;
|
|
||||||
import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@link LoanRepaymentScheduleTransactionProcessor} that pays off components in order of fees, penalties, interest,
|
|
||||||
* then principal.
|
|
||||||
*
|
|
||||||
* Other formulas exist on fineract where you can choose 'Declining-Balance Interest Recalculation' which simply means,
|
|
||||||
* recalculate the interest component based on the how much principal is outstanding at a point in time; but this isnt
|
|
||||||
* trying to model that option only the basic one for now.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
public class FeesPenaltyInterestPrincipalOrderLoanRepaymentScheduleTransactionProcessor extends AbstractLoanRepaymentScheduleTransactionProcessor {
|
|
||||||
|
|
||||||
public static final String STRATEGY_CODE = "fees-penalties-interest-principal-order-strategy";
|
|
||||||
|
|
||||||
public static final String STRATEGY_NAME = "Fees, Penalties, Interest, Principal Order";
|
|
||||||
|
|
||||||
public FeesPenaltyInterestPrincipalOrderLoanRepaymentScheduleTransactionProcessor(final ExternalIdFactory externalIdFactory,
|
|
||||||
final LoanChargeValidator loanChargeValidator, final LoanBalanceService loanBalanceService) {
|
|
||||||
super(externalIdFactory, loanChargeValidator, loanBalanceService);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getCode() {
|
|
||||||
return STRATEGY_CODE;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getName() {
|
|
||||||
return STRATEGY_NAME;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* For early/'in advance' repayments, pay off in the same way as on-time payments, interest first then principal.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected Money handleTransactionThatIsPaymentInAdvanceOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
|
|
||||||
final List<LoanRepaymentScheduleInstallment> installments, final LoanTransaction loanTransaction, final Money paymentInAdvance,
|
|
||||||
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Set<LoanCharge> charges) {
|
|
||||||
|
|
||||||
return handleTransactionThatIsOnTimePaymentOfInstallment(currentInstallment, loanTransaction, paymentInAdvance, transactionMappings,
|
|
||||||
charges);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* For late repayments, pay off in the same way as on-time payments, interest first then principal.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected Money handleTransactionThatIsALateRepaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
|
|
||||||
final List<LoanRepaymentScheduleInstallment> installments, final LoanTransaction loanTransaction,
|
|
||||||
final Money transactionAmountUnprocessed, List<LoanTransactionToRepaymentScheduleMapping> transactionMappings,
|
|
||||||
Set<LoanCharge> charges) {
|
|
||||||
|
|
||||||
return handleTransactionThatIsOnTimePaymentOfInstallment(currentInstallment, loanTransaction, transactionAmountUnprocessed,
|
|
||||||
transactionMappings, charges);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* For normal on-time repayments, pays off interest first, then principal.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected Money handleTransactionThatIsOnTimePaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
|
|
||||||
final LoanTransaction loanTransaction, final Money transactionAmountUnprocessed,
|
|
||||||
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Set<LoanCharge> charges) {
|
|
||||||
|
|
||||||
final LocalDate transactionDate = loanTransaction.getTransactionDate();
|
|
||||||
final MonetaryCurrency currency = transactionAmountUnprocessed.getCurrency();
|
|
||||||
Money transactionAmountRemaining = transactionAmountUnprocessed;
|
|
||||||
Money principalPortion = Money.zero(transactionAmountRemaining.getCurrency());
|
|
||||||
Money interestPortion = Money.zero(transactionAmountRemaining.getCurrency());
|
|
||||||
Money feeChargesPortion = Money.zero(transactionAmountRemaining.getCurrency());
|
|
||||||
Money penaltyChargesPortion = Money.zero(transactionAmountRemaining.getCurrency());
|
|
||||||
|
|
||||||
if (loanTransaction.isChargesWaiver()) {
|
|
||||||
penaltyChargesPortion = currentInstallment.waivePenaltyChargesComponent(transactionDate,
|
|
||||||
loanTransaction.getPenaltyChargesPortion(currency));
|
|
||||||
transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion);
|
|
||||||
|
|
||||||
feeChargesPortion = currentInstallment.waiveFeeChargesComponent(transactionDate,
|
|
||||||
loanTransaction.getFeeChargesPortion(currency));
|
|
||||||
transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion);
|
|
||||||
|
|
||||||
} else if (loanTransaction.isInterestWaiver()) {
|
|
||||||
interestPortion = currentInstallment.waiveInterestComponent(transactionDate, transactionAmountRemaining);
|
|
||||||
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
|
|
||||||
|
|
||||||
loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion);
|
|
||||||
} else if (loanTransaction.isChargePayment()) {
|
|
||||||
if (loanTransaction.isPenaltyPayment()) {
|
|
||||||
penaltyChargesPortion = currentInstallment.payPenaltyChargesComponent(transactionDate, transactionAmountRemaining);
|
|
||||||
transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion);
|
|
||||||
} else {
|
|
||||||
feeChargesPortion = currentInstallment.payFeeChargesComponent(transactionDate, transactionAmountRemaining);
|
|
||||||
transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion);
|
|
||||||
}
|
|
||||||
loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion);
|
|
||||||
} else {
|
|
||||||
feeChargesPortion = currentInstallment.payFeeChargesComponent(transactionDate, transactionAmountRemaining);
|
|
||||||
transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion);
|
|
||||||
|
|
||||||
penaltyChargesPortion = currentInstallment.payPenaltyChargesComponent(transactionDate, transactionAmountRemaining);
|
|
||||||
transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion);
|
|
||||||
|
|
||||||
interestPortion = currentInstallment.payInterestComponent(transactionDate, transactionAmountRemaining);
|
|
||||||
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
|
|
||||||
|
|
||||||
principalPortion = currentInstallment.payPrincipalComponent(transactionDate, transactionAmountRemaining);
|
|
||||||
transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion);
|
|
||||||
|
|
||||||
loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion);
|
|
||||||
}
|
|
||||||
if (principalPortion.plus(interestPortion).plus(feeChargesPortion).plus(penaltyChargesPortion).isGreaterThanZero()) {
|
|
||||||
transactionMappings.add(LoanTransactionToRepaymentScheduleMapping.createFrom(loanTransaction, currentInstallment,
|
|
||||||
principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion));
|
|
||||||
}
|
|
||||||
return transactionAmountRemaining;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Money handleRefundTransactionPaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
|
|
||||||
final LoanTransaction loanTransaction, final Money transactionAmountUnprocessed,
|
|
||||||
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings) {
|
|
||||||
|
|
||||||
final LocalDate transactionDate = loanTransaction.getTransactionDate();
|
|
||||||
final MonetaryCurrency currency = transactionAmountUnprocessed.getCurrency();
|
|
||||||
Money transactionAmountRemaining = transactionAmountUnprocessed;
|
|
||||||
Money principalPortion = Money.zero(currency);
|
|
||||||
Money interestPortion = Money.zero(currency);
|
|
||||||
Money feeChargesPortion = Money.zero(currency);
|
|
||||||
Money penaltyChargesPortion = Money.zero(currency);
|
|
||||||
|
|
||||||
principalPortion = currentInstallment.unpayPrincipalComponent(transactionDate, transactionAmountRemaining);
|
|
||||||
transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion);
|
|
||||||
|
|
||||||
if (transactionAmountRemaining.isGreaterThanZero()) {
|
|
||||||
interestPortion = currentInstallment.unpayInterestComponent(transactionDate, transactionAmountRemaining);
|
|
||||||
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (transactionAmountRemaining.isGreaterThanZero()) {
|
|
||||||
penaltyChargesPortion = currentInstallment.unpayPenaltyChargesComponent(transactionDate, transactionAmountRemaining);
|
|
||||||
transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (transactionAmountRemaining.isGreaterThanZero()) {
|
|
||||||
feeChargesPortion = currentInstallment.unpayFeeChargesComponent(transactionDate, transactionAmountRemaining);
|
|
||||||
transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion);
|
|
||||||
}
|
|
||||||
loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion);
|
|
||||||
if (principalPortion.plus(interestPortion).plus(feeChargesPortion).plus(penaltyChargesPortion).isGreaterThanZero()) {
|
|
||||||
transactionMappings.add(LoanTransactionToRepaymentScheduleMapping.createFrom(loanTransaction, currentInstallment,
|
|
||||||
principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion));
|
|
||||||
}
|
|
||||||
return transactionAmountRemaining;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-14
@@ -225,12 +225,6 @@ public class LoanProductData implements Serializable {
|
|||||||
private final LoanProductGuaranteeData productGuaranteeData;
|
private final LoanProductGuaranteeData productGuaranteeData;
|
||||||
private final Boolean accountMovesOutOfNPAOnlyOnArrearsCompletion;
|
private final Boolean accountMovesOutOfNPAOnlyOnArrearsCompletion;
|
||||||
private LoanProductConfigurableAttributes allowAttributeOverrides;
|
private LoanProductConfigurableAttributes allowAttributeOverrides;
|
||||||
|
|
||||||
/**
|
|
||||||
* "Advances A/C" satellite fields (security, lien, renewal, npa, cheque book), flat by their form field
|
|
||||||
* name. Set after construction (like {@link #allowAttributeOverrides}) so the edit form can repopulate.
|
|
||||||
*/
|
|
||||||
private Map<String, Object> advanceExtensions;
|
|
||||||
private final boolean syncExpectedWithDisbursementDate;
|
private final boolean syncExpectedWithDisbursementDate;
|
||||||
private final boolean isEqualAmortization;
|
private final boolean isEqualAmortization;
|
||||||
private final BigDecimal fixedPrincipalPercentagePerInstallment;
|
private final BigDecimal fixedPrincipalPercentagePerInstallment;
|
||||||
@@ -1472,14 +1466,6 @@ public class LoanProductData implements Serializable {
|
|||||||
this.allowAttributeOverrides = loanProductConfigurableAttributes;
|
this.allowAttributeOverrides = loanProductConfigurableAttributes;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setAdvanceExtensions(final Map<String, Object> advanceExtensions) {
|
|
||||||
this.advanceExtensions = advanceExtensions;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Map<String, Object> getAdvanceExtensions() {
|
|
||||||
return this.advanceExtensions;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isIsLinkedToFloatingInterestRates() {
|
public boolean isIsLinkedToFloatingInterestRates() {
|
||||||
return this.isLinkedToFloatingInterestRates;
|
return this.isLinkedToFloatingInterestRates;
|
||||||
}
|
}
|
||||||
|
|||||||
-471
@@ -1,471 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright since 2025 Mifos Initiative
|
|
||||||
*
|
|
||||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
||||||
*/
|
|
||||||
package org.apache.fineract.portfolio.loanproduct.domain;
|
|
||||||
|
|
||||||
import jakarta.persistence.Column;
|
|
||||||
import jakarta.persistence.Entity;
|
|
||||||
import jakarta.persistence.FetchType;
|
|
||||||
import jakarta.persistence.Id;
|
|
||||||
import jakarta.persistence.JoinColumn;
|
|
||||||
import jakarta.persistence.OneToOne;
|
|
||||||
import jakarta.persistence.Table;
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
import lombok.Setter;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* "Advances A/C" product configuration that Fineract's loan-product schema has no home for
|
|
||||||
* (security & margin, lien, deposit-linked interest, penal/NPA handling, renewal, monitoring,
|
|
||||||
* cheque book). It is a standalone satellite keyed by {@code loan_product_id} rather than a widening
|
|
||||||
* of {@code m_product_loan} or a new relation on {@link LoanProduct}: the product entity graph is
|
|
||||||
* loaded on the hot loan-origination path, so it is deliberately left untouched.
|
|
||||||
*
|
|
||||||
* Typed columns (not a JSON blob) so reports can filter on them and GL ids get a real FK.
|
|
||||||
*/
|
|
||||||
@Entity
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
@NoArgsConstructor
|
|
||||||
@Table(name = "m_product_loan_advance_ext")
|
|
||||||
public class LoanProductAdvanceExt {
|
|
||||||
|
|
||||||
@Id
|
|
||||||
@Column(name = "loan_product_id")
|
|
||||||
private Long loanProductId;
|
|
||||||
|
|
||||||
/** Shared primary key, mapped read-only; {@link #loanProductId} is the writable side. */
|
|
||||||
@OneToOne(fetch = FetchType.LAZY)
|
|
||||||
@JoinColumn(name = "loan_product_id", insertable = false, updatable = false)
|
|
||||||
private LoanProduct loanProduct;
|
|
||||||
|
|
||||||
// ---- classification -------------------------------------------------
|
|
||||||
@Column(name = "product_type_group")
|
|
||||||
private String productTypeGroup;
|
|
||||||
|
|
||||||
@Column(name = "product_type_sub_group")
|
|
||||||
private String productTypeSubGroup;
|
|
||||||
|
|
||||||
@Column(name = "gl_account_id")
|
|
||||||
private Long glAccountId;
|
|
||||||
|
|
||||||
@Column(name = "loan_product_category")
|
|
||||||
private String loanProductCategory;
|
|
||||||
|
|
||||||
@Column(name = "priority_sector")
|
|
||||||
private String prioritySector;
|
|
||||||
|
|
||||||
@Column(name = "cibil_account_type")
|
|
||||||
private String cibilAccountType;
|
|
||||||
|
|
||||||
@Column(name = "kfs_enabled")
|
|
||||||
private Boolean kfsEnabled;
|
|
||||||
|
|
||||||
@Column(name = "apr_enabled")
|
|
||||||
private Boolean aprEnabled;
|
|
||||||
|
|
||||||
// ---- facility -------------------------------------------------------
|
|
||||||
@Column(name = "facility_type")
|
|
||||||
private String facilityType;
|
|
||||||
|
|
||||||
// ---- security & margin ----------------------------------------------
|
|
||||||
@Column(name = "security_type")
|
|
||||||
private String securityType;
|
|
||||||
|
|
||||||
@Column(name = "charge_type")
|
|
||||||
private String chargeType;
|
|
||||||
|
|
||||||
@Column(name = "eligible_amount_basis")
|
|
||||||
private String eligibleAmountBasis;
|
|
||||||
|
|
||||||
@Column(name = "margin_pct")
|
|
||||||
private BigDecimal marginPct;
|
|
||||||
|
|
||||||
@Column(name = "ltv_pct")
|
|
||||||
private BigDecimal ltvPct;
|
|
||||||
|
|
||||||
@Column(name = "dp_method")
|
|
||||||
private String dpMethod;
|
|
||||||
|
|
||||||
// ---- lien -----------------------------------------------------------
|
|
||||||
@Column(name = "lien_required")
|
|
||||||
private Boolean lienRequired;
|
|
||||||
|
|
||||||
@Column(name = "lien_marking_stage")
|
|
||||||
private String lienMarkingStage;
|
|
||||||
|
|
||||||
@Column(name = "lien_certificate_required")
|
|
||||||
private Boolean lienCertificateRequired;
|
|
||||||
|
|
||||||
@Column(name = "lien_certificate_prefix")
|
|
||||||
private String lienCertificatePrefix;
|
|
||||||
|
|
||||||
@Column(name = "print_certificate_on_disbursement")
|
|
||||||
private Boolean printCertificateOnDisbursement;
|
|
||||||
|
|
||||||
// ---- interest (extras; rate bounds live on the core product) --------
|
|
||||||
@Column(name = "interest_basis")
|
|
||||||
private String interestBasis;
|
|
||||||
|
|
||||||
@Column(name = "spread_pct")
|
|
||||||
private BigDecimal spreadPct;
|
|
||||||
|
|
||||||
@Column(name = "interest_charged_on")
|
|
||||||
private String interestChargedOn;
|
|
||||||
|
|
||||||
@Column(name = "interest_posting_frequency")
|
|
||||||
private String interestPostingFrequency;
|
|
||||||
|
|
||||||
// ---- penal & NPA (extras; npaDays/grace/tolerance live on core) ------
|
|
||||||
@Column(name = "penal_applicable")
|
|
||||||
private Boolean penalApplicable;
|
|
||||||
|
|
||||||
@Column(name = "penal_rate_pct")
|
|
||||||
private BigDecimal penalRatePct;
|
|
||||||
|
|
||||||
@Column(name = "penal_applied_on")
|
|
||||||
private String penalAppliedOn;
|
|
||||||
|
|
||||||
@Column(name = "penal_grace_days")
|
|
||||||
private Integer penalGraceDays;
|
|
||||||
|
|
||||||
@Column(name = "npa_trigger")
|
|
||||||
private String npaTrigger;
|
|
||||||
|
|
||||||
@Column(name = "interest_after_npa")
|
|
||||||
private String interestAfterNpa;
|
|
||||||
|
|
||||||
@Column(name = "undebited_interest_gl_id")
|
|
||||||
private Long undebitedInterestGlId;
|
|
||||||
|
|
||||||
// ---- renewal & closure ----------------------------------------------
|
|
||||||
@Column(name = "renewal_allowed")
|
|
||||||
private Boolean renewalAllowed;
|
|
||||||
|
|
||||||
@Column(name = "renewal_period_months")
|
|
||||||
private Integer renewalPeriodMonths;
|
|
||||||
|
|
||||||
@Column(name = "renewal_grace_days")
|
|
||||||
private Integer renewalGraceDays;
|
|
||||||
|
|
||||||
@Column(name = "preclosure_allowed")
|
|
||||||
private String preclosureAllowed;
|
|
||||||
|
|
||||||
@Column(name = "lock_in_months")
|
|
||||||
private Integer lockInMonths;
|
|
||||||
|
|
||||||
@Column(name = "interest_on_preclosure")
|
|
||||||
private String interestOnPreclosure;
|
|
||||||
|
|
||||||
// ---- monitoring -----------------------------------------------------
|
|
||||||
@Column(name = "revaluation_freq_months")
|
|
||||||
private Integer revaluationFreqMonths;
|
|
||||||
|
|
||||||
@Column(name = "insurance_required")
|
|
||||||
private Boolean insuranceRequired;
|
|
||||||
|
|
||||||
@Column(name = "stock_statement_freq_months")
|
|
||||||
private Integer stockStatementFreqMonths;
|
|
||||||
|
|
||||||
// ---- cheque book ----------------------------------------------------
|
|
||||||
@Column(name = "cheque_book_enabled")
|
|
||||||
private Boolean chequeBookEnabled;
|
|
||||||
|
|
||||||
@Column(name = "cheque_type")
|
|
||||||
private String chequeType;
|
|
||||||
|
|
||||||
@Column(name = "default_cheque_book_size")
|
|
||||||
private Integer defaultChequeBookSize;
|
|
||||||
|
|
||||||
@Column(name = "pps_threshold")
|
|
||||||
private BigDecimal ppsThreshold;
|
|
||||||
|
|
||||||
@Column(name = "pps_mandatory_above")
|
|
||||||
private BigDecimal ppsMandatoryAbove;
|
|
||||||
|
|
||||||
// ---- share (co-op) --------------------------------------------------
|
|
||||||
@Column(name = "share_capital_gl_id")
|
|
||||||
private Long shareCapitalGlId;
|
|
||||||
|
|
||||||
// ---- security & lien extras -----------------------------------------
|
|
||||||
/** Pledgeable deposit product names, joined with "|". */
|
|
||||||
@Column(name = "eligible_deposit_products")
|
|
||||||
private String eligibleDepositProducts;
|
|
||||||
|
|
||||||
@Column(name = "lien_tenure_not_exceed_maturity")
|
|
||||||
private Boolean lienTenureNotExceedMaturity;
|
|
||||||
|
|
||||||
@Column(name = "lien_auto_release_on_closure")
|
|
||||||
private Boolean lienAutoReleaseOnClosure;
|
|
||||||
|
|
||||||
@Column(name = "lien_allow_multiple_deposits")
|
|
||||||
private Boolean lienAllowMultipleDeposits;
|
|
||||||
|
|
||||||
@Column(name = "lien_adjust_from_maturity")
|
|
||||||
private Boolean lienAdjustFromMaturity;
|
|
||||||
|
|
||||||
@Column(name = "lien_allow_third_party")
|
|
||||||
private Boolean lienAllowThirdParty;
|
|
||||||
|
|
||||||
@Column(name = "lien_allow_renewal_under_lien")
|
|
||||||
private Boolean lienAllowRenewalUnderLien;
|
|
||||||
|
|
||||||
// ---- limit & tenure extras ------------------------------------------
|
|
||||||
@Column(name = "repayment_type")
|
|
||||||
private String repaymentType;
|
|
||||||
|
|
||||||
@Column(name = "appropriation_order")
|
|
||||||
private String appropriationOrder;
|
|
||||||
|
|
||||||
@Column(name = "debit_balance_allow")
|
|
||||||
private Boolean debitBalanceAllow;
|
|
||||||
|
|
||||||
@Column(name = "allow_debit_after_due_date")
|
|
||||||
private Boolean allowDebitAfterDueDate;
|
|
||||||
|
|
||||||
@Column(name = "branch_level_account_type")
|
|
||||||
private Boolean branchLevelAccountType;
|
|
||||||
|
|
||||||
// ---- interest extras ------------------------------------------------
|
|
||||||
@Column(name = "interest_calc_method")
|
|
||||||
private String interestCalcMethod;
|
|
||||||
|
|
||||||
@Column(name = "interest_on_credit_balance")
|
|
||||||
private Boolean interestOnCreditBalance;
|
|
||||||
|
|
||||||
@Column(name = "interest_posting_day")
|
|
||||||
private String interestPostingDay;
|
|
||||||
|
|
||||||
// ---- NPA upgrade rules ----------------------------------------------
|
|
||||||
@Column(name = "npa_upgrade_only_all_arrears")
|
|
||||||
private Boolean npaUpgradeOnlyAllArrears;
|
|
||||||
|
|
||||||
@Column(name = "npa_keep_standard_if_deposit_covers")
|
|
||||||
private Boolean npaKeepStandardIfDepositCovers;
|
|
||||||
|
|
||||||
// ---- cheque book full config ----------------------------------------
|
|
||||||
/** Allowed book sizes (leaves), joined with "|". */
|
|
||||||
@Column(name = "cheque_book_sizes")
|
|
||||||
private String chequeBookSizes;
|
|
||||||
|
|
||||||
@Column(name = "cheque_max_books_outstanding")
|
|
||||||
private Integer chequeMaxBooksOutstanding;
|
|
||||||
|
|
||||||
@Column(name = "cheque_reissue_when_leaves")
|
|
||||||
private Integer chequeReissueWhenLeaves;
|
|
||||||
|
|
||||||
@Column(name = "cheque_first_book_free")
|
|
||||||
private Boolean chequeFirstBookFree;
|
|
||||||
|
|
||||||
@Column(name = "cheque_free_books_per_year")
|
|
||||||
private Integer chequeFreeBooksPerYear;
|
|
||||||
|
|
||||||
@Column(name = "cheque_name_printed")
|
|
||||||
private String chequeNamePrinted;
|
|
||||||
|
|
||||||
@Column(name = "cheque_print_description")
|
|
||||||
private String chequePrintDescription;
|
|
||||||
|
|
||||||
@Column(name = "cheque_stop_payment_allowed")
|
|
||||||
private Boolean chequeStopPaymentAllowed;
|
|
||||||
|
|
||||||
@Column(name = "cheque_maintain_stock_register")
|
|
||||||
private Boolean chequeMaintainStockRegister;
|
|
||||||
|
|
||||||
@Column(name = "cheque_allow_withdrawal_slip")
|
|
||||||
private Boolean chequeAllowWithdrawalSlip;
|
|
||||||
|
|
||||||
@Column(name = "cheque_block_npa")
|
|
||||||
private Boolean chequeBlockNpa;
|
|
||||||
|
|
||||||
@Column(name = "cheque_block_limit_expired")
|
|
||||||
private Boolean chequeBlockLimitExpired;
|
|
||||||
|
|
||||||
@Column(name = "cheque_block_over_dp")
|
|
||||||
private Boolean chequeBlockOverDp;
|
|
||||||
|
|
||||||
@Column(name = "cheque_block_frozen")
|
|
||||||
private Boolean chequeBlockFrozen;
|
|
||||||
|
|
||||||
@Column(name = "cheque_block_lien_released")
|
|
||||||
private Boolean chequeBlockLienReleased;
|
|
||||||
|
|
||||||
// ---- Loans A/C (EMI) wizard fields -------------------------------------
|
|
||||||
@Column(name = "loan_security_mandatory")
|
|
||||||
private String securityMandatory;
|
|
||||||
|
|
||||||
@Column(name = "loan_guarantors_required")
|
|
||||||
private Integer guarantorsRequired;
|
|
||||||
|
|
||||||
@Column(name = "loan_co_applicant")
|
|
||||||
private String coApplicant;
|
|
||||||
|
|
||||||
@Column(name = "loan_third_party_security")
|
|
||||||
private String thirdPartySecurity;
|
|
||||||
|
|
||||||
@Column(name = "loan_legal_opinion_required")
|
|
||||||
private Boolean legalOpinionRequired;
|
|
||||||
|
|
||||||
@Column(name = "loan_property_insurance_required")
|
|
||||||
private Boolean propertyInsuranceRequired;
|
|
||||||
|
|
||||||
@Column(name = "loan_register_charge")
|
|
||||||
private Boolean registerCharge;
|
|
||||||
|
|
||||||
@Column(name = "loan_release_security_on_closure")
|
|
||||||
private Boolean releaseSecurityOnClosure;
|
|
||||||
|
|
||||||
@Column(name = "loan_allow_multiple_securities")
|
|
||||||
private Boolean allowMultipleSecurities;
|
|
||||||
|
|
||||||
@Column(name = "loan_allow_third_party_deposit")
|
|
||||||
private Boolean allowThirdPartyDeposit;
|
|
||||||
|
|
||||||
@Column(name = "loan_min_age")
|
|
||||||
private Integer minAge;
|
|
||||||
|
|
||||||
@Column(name = "loan_max_age_at_maturity")
|
|
||||||
private Integer maxAgeAtMaturity;
|
|
||||||
|
|
||||||
@Column(name = "loan_min_cibil")
|
|
||||||
private Integer minCibil;
|
|
||||||
|
|
||||||
@Column(name = "loan_min_monthly_income")
|
|
||||||
private BigDecimal minMonthlyIncome;
|
|
||||||
|
|
||||||
@Column(name = "loan_max_foir")
|
|
||||||
private BigDecimal maxFoir;
|
|
||||||
|
|
||||||
@Column(name = "loan_max_loans_per_customer")
|
|
||||||
private Integer maxLoansPerCustomer;
|
|
||||||
|
|
||||||
@Column(name = "loan_disbursement_type")
|
|
||||||
private String disbursementType;
|
|
||||||
|
|
||||||
@Column(name = "loan_disbursement_mode")
|
|
||||||
private String disbursementMode;
|
|
||||||
|
|
||||||
@Column(name = "loan_undisbursed_interest")
|
|
||||||
private String undisbursedInterest;
|
|
||||||
|
|
||||||
@Column(name = "loan_pre_emi_interest")
|
|
||||||
private Boolean preEmiInterest;
|
|
||||||
|
|
||||||
@Column(name = "loan_emi_due_day")
|
|
||||||
private String emiDueDay;
|
|
||||||
|
|
||||||
@Column(name = "loan_holiday_rule")
|
|
||||||
private String holidayRule;
|
|
||||||
|
|
||||||
@Column(name = "loan_moratorium_months")
|
|
||||||
private Integer moratoriumMonths;
|
|
||||||
|
|
||||||
@Column(name = "loan_moratorium_interest")
|
|
||||||
private String moratoriumInterest;
|
|
||||||
|
|
||||||
@Column(name = "loan_variable_installments")
|
|
||||||
private Boolean variableInstallments;
|
|
||||||
|
|
||||||
@Column(name = "loan_repayment_mode")
|
|
||||||
private String repaymentMode;
|
|
||||||
|
|
||||||
@Column(name = "loan_linked_account_type")
|
|
||||||
private String linkedAccountType;
|
|
||||||
|
|
||||||
@Column(name = "loan_pdc_collection")
|
|
||||||
private String pdcCollection;
|
|
||||||
|
|
||||||
@Column(name = "loan_mandate_required")
|
|
||||||
private Boolean mandateRequired;
|
|
||||||
|
|
||||||
@Column(name = "loan_advance_emi")
|
|
||||||
private String advanceEmi;
|
|
||||||
|
|
||||||
@Column(name = "loan_rate_type")
|
|
||||||
private String rateType;
|
|
||||||
|
|
||||||
@Column(name = "loan_register_particular")
|
|
||||||
private String registerParticular;
|
|
||||||
|
|
||||||
@Column(name = "loan_apply_cibil_adjustment")
|
|
||||||
private Boolean applyCibilAdjustment;
|
|
||||||
|
|
||||||
@Column(name = "loan_rate_resolved_at")
|
|
||||||
private String rateResolvedAt;
|
|
||||||
|
|
||||||
@Column(name = "loan_calc_period")
|
|
||||||
private String calcPeriod;
|
|
||||||
|
|
||||||
@Column(name = "loan_interest_starts_from")
|
|
||||||
private String interestStartsFrom;
|
|
||||||
|
|
||||||
@Column(name = "loan_compounding")
|
|
||||||
private String compounding;
|
|
||||||
|
|
||||||
@Column(name = "loan_benchmark")
|
|
||||||
private String benchmark;
|
|
||||||
|
|
||||||
@Column(name = "loan_reset_frequency")
|
|
||||||
private String resetFrequency;
|
|
||||||
|
|
||||||
@Column(name = "loan_reset_change")
|
|
||||||
private String resetChange;
|
|
||||||
|
|
||||||
@Column(name = "loan_part_prepayment_allowed")
|
|
||||||
private Boolean partPrepaymentAllowed;
|
|
||||||
|
|
||||||
@Column(name = "loan_min_part_payment")
|
|
||||||
private BigDecimal minPartPayment;
|
|
||||||
|
|
||||||
@Column(name = "loan_allowed_on")
|
|
||||||
private String allowedOn;
|
|
||||||
|
|
||||||
@Column(name = "loan_part_payments_per_year")
|
|
||||||
private String partPaymentsPerYear;
|
|
||||||
|
|
||||||
@Column(name = "loan_part_prepayment_mode")
|
|
||||||
private String partPrepaymentMode;
|
|
||||||
|
|
||||||
@Column(name = "loan_foreclosure_floating_individual")
|
|
||||||
private BigDecimal foreclosureFloatingIndividual;
|
|
||||||
|
|
||||||
@Column(name = "loan_foreclosure_fixed_individual")
|
|
||||||
private BigDecimal foreclosureFixedIndividual;
|
|
||||||
|
|
||||||
@Column(name = "loan_foreclosure_non_individual")
|
|
||||||
private BigDecimal foreclosureNonIndividual;
|
|
||||||
|
|
||||||
@Column(name = "loan_foreclosure_takeover")
|
|
||||||
private BigDecimal foreclosureTakeover;
|
|
||||||
|
|
||||||
@Column(name = "loan_foreclosure_quote_valid_days")
|
|
||||||
private Integer foreclosureQuoteValidDays;
|
|
||||||
|
|
||||||
@Column(name = "loan_auto_close_on_zero_balance")
|
|
||||||
private Boolean autoCloseOnZeroBalance;
|
|
||||||
|
|
||||||
@Column(name = "loan_noc_letter")
|
|
||||||
private String nocLetter;
|
|
||||||
|
|
||||||
@Column(name = "loan_restructuring")
|
|
||||||
private String restructuring;
|
|
||||||
|
|
||||||
@Column(name = "loan_release_collateral_on_closure")
|
|
||||||
private Boolean releaseCollateralOnClosure;
|
|
||||||
|
|
||||||
@Column(name = "loan_top_up_allowed")
|
|
||||||
private Boolean topUpAllowed;
|
|
||||||
|
|
||||||
@Column(name = "loan_balance_transfer_out_allowed")
|
|
||||||
private Boolean balanceTransferOutAllowed;
|
|
||||||
|
|
||||||
public LoanProductAdvanceExt(final LoanProduct loanProduct) {
|
|
||||||
this.loanProduct = loanProduct;
|
|
||||||
this.loanProductId = loanProduct.getId();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-16
@@ -1,16 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright since 2025 Mifos Initiative
|
|
||||||
*
|
|
||||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
||||||
*/
|
|
||||||
package org.apache.fineract.portfolio.loanproduct.domain;
|
|
||||||
|
|
||||||
import java.util.Optional;
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
|
||||||
|
|
||||||
public interface LoanProductAdvanceExtRepository extends JpaRepository<LoanProductAdvanceExt, Long> {
|
|
||||||
|
|
||||||
Optional<LoanProductAdvanceExt> findByLoanProductId(Long loanProductId);
|
|
||||||
}
|
|
||||||
-76
@@ -1,76 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright since 2025 Mifos Initiative
|
|
||||||
*
|
|
||||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
||||||
*/
|
|
||||||
package org.apache.fineract.portfolio.loanproduct.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;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
import lombok.Setter;
|
|
||||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* One CIBIL/credit-score band of a rate register version — e.g. {601..650} tier "Fair" adds +1.00%.
|
|
||||||
* Applied on top of the chart cell at loan sanction. Typically 5 rows per version; ordered on reads
|
|
||||||
* by {@code displayOrder}.
|
|
||||||
*/
|
|
||||||
@Entity
|
|
||||||
@Table(name = "m_loan_rate_register_cibil_slab")
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
@NoArgsConstructor
|
|
||||||
public class LoanRateRegisterCibilSlab {
|
|
||||||
|
|
||||||
@Id
|
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
|
||||||
private Long id;
|
|
||||||
|
|
||||||
@Column(name = "effective_id", nullable = false)
|
|
||||||
private Long effectiveId;
|
|
||||||
|
|
||||||
@Column(name = "score_from", nullable = false)
|
|
||||||
private Integer scoreFrom;
|
|
||||||
|
|
||||||
@Column(name = "score_to", nullable = false)
|
|
||||||
private Integer scoreTo;
|
|
||||||
|
|
||||||
@Column(name = "tier", length = 30, nullable = false)
|
|
||||||
private String tier;
|
|
||||||
|
|
||||||
@Column(name = "slab_type", length = 20, nullable = false)
|
|
||||||
private String slabType;
|
|
||||||
|
|
||||||
@Column(name = "adjustment_pct", nullable = false)
|
|
||||||
private BigDecimal adjustmentPct;
|
|
||||||
|
|
||||||
@Column(name = "display_order", nullable = false)
|
|
||||||
private Integer displayOrder;
|
|
||||||
|
|
||||||
@Column(name = "created_date")
|
|
||||||
private LocalDateTime createdDate;
|
|
||||||
|
|
||||||
public static LoanRateRegisterCibilSlab create(final Long effectiveId, final Integer scoreFrom, final Integer scoreTo, final String tier,
|
|
||||||
final String slabType, final BigDecimal adjustmentPct, final Integer displayOrder) {
|
|
||||||
final LoanRateRegisterCibilSlab s = new LoanRateRegisterCibilSlab();
|
|
||||||
s.effectiveId = effectiveId;
|
|
||||||
s.scoreFrom = scoreFrom;
|
|
||||||
s.scoreTo = scoreTo;
|
|
||||||
s.tier = tier;
|
|
||||||
s.slabType = slabType != null ? slabType : "Flat";
|
|
||||||
s.adjustmentPct = adjustmentPct;
|
|
||||||
s.displayOrder = displayOrder;
|
|
||||||
s.createdDate = LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant());
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-17
@@ -1,17 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright since 2025 Mifos Initiative
|
|
||||||
*
|
|
||||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
||||||
*/
|
|
||||||
package org.apache.fineract.portfolio.loanproduct.domain;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
|
||||||
|
|
||||||
public interface LoanRateRegisterCibilSlabRepository extends JpaRepository<LoanRateRegisterCibilSlab, Long> {
|
|
||||||
|
|
||||||
/** CIBIL bands for a version, ordered as the UI shows them (ascending score bands). */
|
|
||||||
List<LoanRateRegisterCibilSlab> findByEffectiveIdOrderByDisplayOrderAsc(Long effectiveId);
|
|
||||||
}
|
|
||||||
-94
@@ -1,94 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright since 2025 Mifos Initiative
|
|
||||||
*
|
|
||||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
||||||
*/
|
|
||||||
package org.apache.fineract.portfolio.loanproduct.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.time.LocalDate;
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
import lombok.Setter;
|
|
||||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* One effective (versioned) copy of the central Loan Interest Rate Register. Mirrors the
|
|
||||||
* {@code FdBaseInterestRateEffective} versioning model: each approved save creates a new row with
|
|
||||||
* {@code isActive=true} and deactivates the previous active row ({@code isActive=false,
|
|
||||||
* effectiveToDate=today}). Reads for "current active" filter on {@code isActive=true}.
|
|
||||||
*
|
|
||||||
* The 34-product × 10-slab rate matrix for this version lives in {@link LoanRateRegisterSlabHistory};
|
|
||||||
* the CIBIL/credit-score bands live in {@link LoanRateRegisterCibilSlab}, both keyed by this row's
|
|
||||||
* id and cascade-deleted with it.
|
|
||||||
*/
|
|
||||||
@Entity
|
|
||||||
@Table(name = "m_loan_rate_register_effective")
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
@NoArgsConstructor
|
|
||||||
public class LoanRateRegisterEffective {
|
|
||||||
|
|
||||||
@Id
|
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
|
||||||
private Long id;
|
|
||||||
|
|
||||||
@Column(name = "name", length = 200, nullable = false)
|
|
||||||
private String name;
|
|
||||||
|
|
||||||
@Column(name = "effective_from_date", nullable = false)
|
|
||||||
private LocalDate effectiveFromDate;
|
|
||||||
|
|
||||||
@Column(name = "effective_to_date")
|
|
||||||
private LocalDate effectiveToDate;
|
|
||||||
|
|
||||||
@Column(name = "currency_code", length = 3, nullable = false)
|
|
||||||
private String currencyCode;
|
|
||||||
|
|
||||||
@Column(name = "is_active", nullable = false)
|
|
||||||
private Boolean isActive;
|
|
||||||
|
|
||||||
@Column(name = "fallback_no_cibil_slab", nullable = false)
|
|
||||||
private Integer fallbackNoCibilSlab;
|
|
||||||
|
|
||||||
@Column(name = "fallback_below_min", length = 20, nullable = false)
|
|
||||||
private String fallbackBelowMin;
|
|
||||||
|
|
||||||
@Column(name = "note", length = 500)
|
|
||||||
private String note;
|
|
||||||
|
|
||||||
@Column(name = "created_by_id")
|
|
||||||
private Long createdById;
|
|
||||||
|
|
||||||
@Column(name = "created_date")
|
|
||||||
private LocalDateTime createdDate;
|
|
||||||
|
|
||||||
public static LoanRateRegisterEffective create(final String name, final LocalDate effectiveFromDate, final String currencyCode,
|
|
||||||
final Integer fallbackNoCibilSlab, final String fallbackBelowMin, final String note, final Long createdById) {
|
|
||||||
final LoanRateRegisterEffective e = new LoanRateRegisterEffective();
|
|
||||||
e.name = name;
|
|
||||||
e.effectiveFromDate = effectiveFromDate;
|
|
||||||
e.currencyCode = currencyCode != null ? currencyCode : "INR";
|
|
||||||
e.isActive = true;
|
|
||||||
e.fallbackNoCibilSlab = fallbackNoCibilSlab != null ? fallbackNoCibilSlab : 2;
|
|
||||||
e.fallbackBelowMin = fallbackBelowMin != null ? fallbackBelowMin : "block";
|
|
||||||
e.note = note;
|
|
||||||
e.createdById = createdById;
|
|
||||||
e.createdDate = LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant());
|
|
||||||
return e;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Flip this version to inactive and close its effective window. Called when a newer version is approved. */
|
|
||||||
public void deactivate(final LocalDate toDate) {
|
|
||||||
this.isActive = false;
|
|
||||||
this.effectiveToDate = toDate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-24
@@ -1,24 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright since 2025 Mifos Initiative
|
|
||||||
*
|
|
||||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
||||||
*/
|
|
||||||
package org.apache.fineract.portfolio.loanproduct.domain;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
|
||||||
|
|
||||||
public interface LoanRateRegisterEffectiveRepository extends JpaRepository<LoanRateRegisterEffective, Long> {
|
|
||||||
|
|
||||||
/** All active versions, newest-effective first. At most one row in a healthy system. */
|
|
||||||
List<LoanRateRegisterEffective> findByIsActiveTrueOrderByEffectiveFromDateDesc();
|
|
||||||
|
|
||||||
/** The current active version, if any. */
|
|
||||||
Optional<LoanRateRegisterEffective> findFirstByIsActiveTrueOrderByEffectiveFromDateDesc();
|
|
||||||
|
|
||||||
/** Version history list — all versions, newest first. */
|
|
||||||
List<LoanRateRegisterEffective> findAllByOrderByEffectiveFromDateDesc();
|
|
||||||
}
|
|
||||||
-52
@@ -1,52 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright since 2025 Mifos Initiative
|
|
||||||
*
|
|
||||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
||||||
*/
|
|
||||||
package org.apache.fineract.portfolio.loanproduct.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.time.LocalDateTime;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
import lombok.Setter;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A "particular" (loan type) row of the Loan Interest Rate Register chart. The GL account is a
|
|
||||||
* property of the particular, so it lives here rather than on each rate-register version. Kept as a
|
|
||||||
* plain {@code gl_account_id} column (no relation) to avoid pulling the accounting module's entity
|
|
||||||
* graph into the loan module.
|
|
||||||
*/
|
|
||||||
@Entity
|
|
||||||
@Table(name = "m_loan_rate_register_product")
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
@NoArgsConstructor
|
|
||||||
public class LoanRateRegisterProduct {
|
|
||||||
|
|
||||||
@Id
|
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
|
||||||
private Long id;
|
|
||||||
|
|
||||||
@Column(name = "sr_no", nullable = false)
|
|
||||||
private Integer srNo;
|
|
||||||
|
|
||||||
@Column(name = "name", length = 200, nullable = false)
|
|
||||||
private String name;
|
|
||||||
|
|
||||||
@Column(name = "gl_account_id")
|
|
||||||
private Long glAccountId;
|
|
||||||
|
|
||||||
@Column(name = "is_active", nullable = false)
|
|
||||||
private Boolean isActive;
|
|
||||||
|
|
||||||
@Column(name = "created_date")
|
|
||||||
private LocalDateTime createdDate;
|
|
||||||
}
|
|
||||||
-17
@@ -1,17 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright since 2025 Mifos Initiative
|
|
||||||
*
|
|
||||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
||||||
*/
|
|
||||||
package org.apache.fineract.portfolio.loanproduct.domain;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
|
||||||
|
|
||||||
public interface LoanRateRegisterProductRepository extends JpaRepository<LoanRateRegisterProduct, Long> {
|
|
||||||
|
|
||||||
/** Active particulars in chart order. */
|
|
||||||
List<LoanRateRegisterProduct> findByIsActiveTrueOrderBySrNoAsc();
|
|
||||||
}
|
|
||||||
-98
@@ -1,98 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright since 2025 Mifos Initiative
|
|
||||||
*
|
|
||||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
||||||
*/
|
|
||||||
package org.apache.fineract.portfolio.loanproduct.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;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
import lombok.Setter;
|
|
||||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A single cell of a rate register version — one row per {product × amount-slab}. Product and slab
|
|
||||||
* metadata are denormalised so one query can rebuild the full matrix without a join. The cell holds
|
|
||||||
* either a plain numeric rate, a formula string ({@code "1% + FD"}) resolved at sanction, or
|
|
||||||
* N.A. (both value and formula null, {@code isNa=true}) to mean "slab not offered".
|
|
||||||
*/
|
|
||||||
@Entity
|
|
||||||
@Table(name = "m_loan_rate_register_slab_history")
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
@NoArgsConstructor
|
|
||||||
public class LoanRateRegisterSlabHistory {
|
|
||||||
|
|
||||||
@Id
|
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
|
||||||
private Long id;
|
|
||||||
|
|
||||||
@Column(name = "effective_id", nullable = false)
|
|
||||||
private Long effectiveId;
|
|
||||||
|
|
||||||
// ---- product dimension ----------------------------------------------
|
|
||||||
/** Stable link to the master particular (and its GL). Null only for rows written before the master existed. */
|
|
||||||
@Column(name = "product_id")
|
|
||||||
private Long productId;
|
|
||||||
|
|
||||||
@Column(name = "product_sr_no", nullable = false)
|
|
||||||
private Integer productSrNo;
|
|
||||||
|
|
||||||
@Column(name = "product_name", length = 200, nullable = false)
|
|
||||||
private String productName;
|
|
||||||
|
|
||||||
// ---- amount-slab dimension ------------------------------------------
|
|
||||||
@Column(name = "slab_order", nullable = false)
|
|
||||||
private Integer slabOrder;
|
|
||||||
|
|
||||||
@Column(name = "slab_label", length = 60, nullable = false)
|
|
||||||
private String slabLabel;
|
|
||||||
|
|
||||||
@Column(name = "amount_upto")
|
|
||||||
private BigDecimal amountUpto;
|
|
||||||
|
|
||||||
// ---- cell value (exactly one of value / formula is populated; both null + isNa=true means N.A.) ----
|
|
||||||
@Column(name = "rate_value")
|
|
||||||
private BigDecimal rateValue;
|
|
||||||
|
|
||||||
@Column(name = "rate_formula", length = 50)
|
|
||||||
private String rateFormula;
|
|
||||||
|
|
||||||
@Column(name = "is_formula", nullable = false)
|
|
||||||
private Boolean isFormula;
|
|
||||||
|
|
||||||
@Column(name = "is_na", nullable = false)
|
|
||||||
private Boolean isNa;
|
|
||||||
|
|
||||||
@Column(name = "created_date")
|
|
||||||
private LocalDateTime createdDate;
|
|
||||||
|
|
||||||
public static LoanRateRegisterSlabHistory create(final Long effectiveId, final Long productId, final Integer productSrNo,
|
|
||||||
final String productName, final Integer slabOrder, final String slabLabel, final BigDecimal amountUpto,
|
|
||||||
final BigDecimal rateValue, final String rateFormula, final boolean isFormula, final boolean isNa) {
|
|
||||||
final LoanRateRegisterSlabHistory s = new LoanRateRegisterSlabHistory();
|
|
||||||
s.effectiveId = effectiveId;
|
|
||||||
s.productId = productId;
|
|
||||||
s.productSrNo = productSrNo;
|
|
||||||
s.productName = productName;
|
|
||||||
s.slabOrder = slabOrder;
|
|
||||||
s.slabLabel = slabLabel;
|
|
||||||
s.amountUpto = amountUpto;
|
|
||||||
s.rateValue = rateValue;
|
|
||||||
s.rateFormula = rateFormula;
|
|
||||||
s.isFormula = isFormula;
|
|
||||||
s.isNa = isNa;
|
|
||||||
s.createdDate = LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant());
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-17
@@ -1,17 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright since 2025 Mifos Initiative
|
|
||||||
*
|
|
||||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
||||||
*/
|
|
||||||
package org.apache.fineract.portfolio.loanproduct.domain;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
|
||||||
|
|
||||||
public interface LoanRateRegisterSlabHistoryRepository extends JpaRepository<LoanRateRegisterSlabHistory, Long> {
|
|
||||||
|
|
||||||
/** All cells of a version, ordered by product then by amount-slab. Rebuilds the full matrix in order. */
|
|
||||||
List<LoanRateRegisterSlabHistory> findByEffectiveIdOrderByProductSrNoAscSlabOrderAsc(Long effectiveId);
|
|
||||||
}
|
|
||||||
-45
@@ -55,49 +55,4 @@ public final class GLAccountTransactionGuard {
|
|||||||
"glAccountId", glAccountId);
|
"glAccountId", glAccountId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Refuses to draw more out of a holding account than it holds.
|
|
||||||
*
|
|
||||||
* <p>
|
|
||||||
* A suspense or sundry account exists to hold money on its way somewhere else, so what is in it
|
|
||||||
* is the whole of what can come out. Drawing more leaves it with a negative balance, and a
|
|
||||||
* liability with a negative balance is not a small number - it says the bank owes less than
|
|
||||||
* nothing, which is not a state that exists. It also breaks the balance sheet by the overdraw,
|
|
||||||
* and the figure is small enough to sit unnoticed inside a head that totals positive.
|
|
||||||
*
|
|
||||||
* <p>
|
|
||||||
* Checked here rather than at each call site because the accounts this protects are reached by
|
|
||||||
* several features posting directly through JDBC, and a rule enforced in one of them is a rule
|
|
||||||
* the next one added will not have.
|
|
||||||
*
|
|
||||||
* @param glAccountId the account being drawn on.
|
|
||||||
* @param amount how much the caller wants to take out.
|
|
||||||
* @param what the account's role in the caller's own words, for the message a clerk reads.
|
|
||||||
*/
|
|
||||||
public static void assertSufficientBalance(final JdbcTemplate jdbcTemplate, final Long glAccountId,
|
|
||||||
final java.math.BigDecimal amount, final String what) {
|
|
||||||
if (glAccountId == null || amount == null || amount.signum() <= 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Credits less debits: these are liability-natured holding accounts, so the balance is what
|
|
||||||
// is still owed onward. Reversed entries and the entries they reverse both drop out - a
|
|
||||||
// reversal pair nets to nothing, and counting either side alone moves the balance by the
|
|
||||||
// whole amount.
|
|
||||||
final java.math.BigDecimal held = jdbcTemplate.queryForObject("""
|
|
||||||
SELECT COALESCE(SUM(CASE WHEN type_enum = 1 THEN amount ELSE -amount END), 0)
|
|
||||||
FROM acc_gl_journal_entry
|
|
||||||
WHERE account_id = ?
|
|
||||||
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)
|
|
||||||
""", java.math.BigDecimal.class, glAccountId);
|
|
||||||
final java.math.BigDecimal balance = held == null ? java.math.BigDecimal.ZERO : held;
|
|
||||||
if (balance.compareTo(amount) < 0) {
|
|
||||||
throw new PlatformDataIntegrityException("error.msg.glaccount.insufficient.balance",
|
|
||||||
what + " holds " + balance + " but " + amount + " was asked of it. Drawing the "
|
|
||||||
+ "difference would leave the account with a negative balance, which a "
|
|
||||||
+ "holding account cannot have.",
|
|
||||||
"glAccountId", glAccountId, amount);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
-197
@@ -1,197 +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.interbranch.api;
|
|
||||||
|
|
||||||
import com.google.gson.Gson;
|
|
||||||
import com.google.gson.GsonBuilder;
|
|
||||||
import com.google.gson.JsonArray;
|
|
||||||
import com.google.gson.JsonElement;
|
|
||||||
import com.google.gson.JsonObject;
|
|
||||||
import jakarta.ws.rs.GET;
|
|
||||||
import jakarta.ws.rs.Path;
|
|
||||||
import jakarta.ws.rs.PathParam;
|
|
||||||
import jakarta.ws.rs.Produces;
|
|
||||||
import jakarta.ws.rs.core.MediaType;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.LinkedHashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
|
||||||
import org.springframework.jdbc.core.JdbcTemplate;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tells a person looking at one ledger transaction whether it is one branch's part of a transfer between branches.
|
|
||||||
*
|
|
||||||
* <p>
|
|
||||||
* A transfer between accounts at different branches is posted as one ledger transaction per branch, joined through the
|
|
||||||
* Liability Transfer account. Looking at either part on its own shows only two ledger lines, with nothing to say it has
|
|
||||||
* another part in another branch. The bank transfer log records the ledger ids of every part, so given any one of them
|
|
||||||
* this reports the whole transfer: who it moved money from and to, in which branches, and who raised and approved it.
|
|
||||||
*/
|
|
||||||
@Path("/v1/accounting/interbranchtransfers")
|
|
||||||
@Component
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
|
||||||
public class InterBranchTransferApiResource {
|
|
||||||
|
|
||||||
private final PlatformSecurityContext context;
|
|
||||||
private final JdbcTemplate jdbcTemplate;
|
|
||||||
private final Gson gson = new GsonBuilder().serializeNulls().create();
|
|
||||||
|
|
||||||
@GET
|
|
||||||
@Path("{transactionId}")
|
|
||||||
public String retrieve(@PathParam("transactionId") final String transactionId) {
|
|
||||||
this.context.authenticatedUser().validateHasReadPermission("JOURNALENTRY");
|
|
||||||
|
|
||||||
// Ledger transaction ids are short alphanumeric strings; anything else cannot be one.
|
|
||||||
if (transactionId == null || !transactionId.matches("[A-Za-z0-9_-]{1,50}")) {
|
|
||||||
return notInterBranch();
|
|
||||||
}
|
|
||||||
|
|
||||||
final List<Map<String, Object>> logs = this.jdbcTemplate.queryForList(
|
|
||||||
"SELECT voucher_no, total_amount, gl_txn_id, debit_entries, credit_entries, status FROM m_bank_transfer_log "
|
|
||||||
+ "WHERE (',' || gl_txn_id || ',') LIKE ? ORDER BY id DESC LIMIT 1",
|
|
||||||
"%," + transactionId + ",%");
|
|
||||||
if (logs.isEmpty()) {
|
|
||||||
return notInterBranch();
|
|
||||||
}
|
|
||||||
final Map<String, Object> log = logs.get(0);
|
|
||||||
|
|
||||||
final List<Map<String, Object>> from = legsOf(log.get("debit_entries"));
|
|
||||||
final List<Map<String, Object>> to = legsOf(log.get("credit_entries"));
|
|
||||||
|
|
||||||
final Set<Object> branchIds = new LinkedHashSet<>();
|
|
||||||
for (final Map<String, Object> leg : from) {
|
|
||||||
branchIds.add(leg.get("branchId"));
|
|
||||||
}
|
|
||||||
for (final Map<String, Object> leg : to) {
|
|
||||||
branchIds.add(leg.get("branchId"));
|
|
||||||
}
|
|
||||||
branchIds.remove(null);
|
|
||||||
if (branchIds.size() < 2) {
|
|
||||||
return notInterBranch();
|
|
||||||
}
|
|
||||||
|
|
||||||
final Map<String, Object> out = new LinkedHashMap<>();
|
|
||||||
out.put("interBranch", true);
|
|
||||||
out.put("voucherNo", log.get("voucher_no"));
|
|
||||||
out.put("amount", log.get("total_amount"));
|
|
||||||
out.put("status", log.get("status"));
|
|
||||||
out.put("thisTransactionId", transactionId);
|
|
||||||
out.put("from", from);
|
|
||||||
out.put("to", to);
|
|
||||||
out.put("parts", partsOf(String.valueOf(log.get("gl_txn_id"))));
|
|
||||||
|
|
||||||
// Raised and approved by: the clerk's request, when the transfer went through approval.
|
|
||||||
final List<Map<String, Object>> requests = this.jdbcTemplate.queryForList(
|
|
||||||
"SELECT clerk_name, manager_name, submitted_at, actioned_at FROM m_clerk_disbursement_request WHERE voucher_no = ? "
|
|
||||||
+ "ORDER BY id DESC LIMIT 1",
|
|
||||||
log.get("voucher_no"));
|
|
||||||
if (!requests.isEmpty()) {
|
|
||||||
final Map<String, Object> request = requests.get(0);
|
|
||||||
out.put("raisedBy", request.get("clerk_name"));
|
|
||||||
out.put("raisedAt", isoOf(request.get("submitted_at")));
|
|
||||||
out.put("approvedBy", request.get("manager_name"));
|
|
||||||
out.put("approvedAt", isoOf(request.get("actioned_at")));
|
|
||||||
}
|
|
||||||
return this.gson.toJson(out);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String notInterBranch() {
|
|
||||||
return "{\"interBranch\":false}";
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Each account on one side of the transfer, with the branch its books are in. */
|
|
||||||
private List<Map<String, Object>> legsOf(final Object storedJson) {
|
|
||||||
final List<Map<String, Object>> legs = new ArrayList<>();
|
|
||||||
if (storedJson == null) {
|
|
||||||
return legs;
|
|
||||||
}
|
|
||||||
final JsonArray entries = this.gson.fromJson(String.valueOf(storedJson), JsonArray.class);
|
|
||||||
for (final JsonElement element : entries) {
|
|
||||||
final JsonObject entry = element.getAsJsonObject();
|
|
||||||
final Map<String, Object> leg = new LinkedHashMap<>();
|
|
||||||
leg.put("accountNo", text(entry, "accountNo"));
|
|
||||||
leg.put("accountName", text(entry, "accountName"));
|
|
||||||
leg.put("amount", text(entry, "amount"));
|
|
||||||
final Long accountId = entry.has("savingsAccountId") && !entry.get("savingsAccountId").isJsonNull()
|
|
||||||
? entry.get("savingsAccountId").getAsLong()
|
|
||||||
: null;
|
|
||||||
final Map<String, Object> branch = accountId == null ? null : branchOfAccount(text(entry, "accountType"), accountId);
|
|
||||||
leg.put("branchId", branch == null ? null : ((Number) branch.get("id")).longValue());
|
|
||||||
leg.put("branchName", branch == null ? null : branch.get("name"));
|
|
||||||
legs.add(leg);
|
|
||||||
}
|
|
||||||
return legs;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** One entry per ledger transaction of the transfer, with the branch it was posted in. */
|
|
||||||
private List<Map<String, Object>> partsOf(final String glTxnIds) {
|
|
||||||
final List<Map<String, Object>> parts = new ArrayList<>();
|
|
||||||
for (final String id : glTxnIds.split(",")) {
|
|
||||||
final String ledgerId = id.trim();
|
|
||||||
if (ledgerId.isEmpty()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
final List<Map<String, Object>> offices = this.jdbcTemplate.queryForList(
|
|
||||||
"SELECT DISTINCT o.id, o.name FROM acc_gl_journal_entry je JOIN m_office o ON o.id = je.office_id "
|
|
||||||
+ "WHERE je.transaction_id = ?",
|
|
||||||
ledgerId);
|
|
||||||
final Map<String, Object> part = new LinkedHashMap<>();
|
|
||||||
part.put("transactionId", ledgerId);
|
|
||||||
part.put("branchId", offices.isEmpty() ? null : ((Number) offices.get(0).get("id")).longValue());
|
|
||||||
part.put("branchName", offices.isEmpty() ? null : offices.get(0).get("name"));
|
|
||||||
parts.add(part);
|
|
||||||
}
|
|
||||||
return parts;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The branch an account's books are in: its own office when it was opened at another branch than the client's,
|
|
||||||
* otherwise the client's (or group's). A current account is its own table with its own ids.
|
|
||||||
*/
|
|
||||||
private Map<String, Object> branchOfAccount(final String accountType, final Long accountId) {
|
|
||||||
final String sql = "CURRENT".equalsIgnoreCase(accountType)
|
|
||||||
? "SELECT o.id, o.name FROM m_current_account ca LEFT JOIN m_client c ON c.id = ca.client_id "
|
|
||||||
+ "JOIN m_office o ON o.id = COALESCE(ca.office_id, c.office_id) WHERE ca.id = ?"
|
|
||||||
: "SELECT o.id, o.name FROM m_savings_account sa LEFT JOIN m_client c ON c.id = sa.client_id "
|
|
||||||
+ "LEFT JOIN m_group g ON g.id = sa.group_id "
|
|
||||||
+ "JOIN m_office o ON o.id = COALESCE(sa.office_id, c.office_id, g.office_id) WHERE sa.id = ?";
|
|
||||||
return this.jdbcTemplate.queryForList(sql, accountId).stream().findFirst().orElse(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String text(final JsonObject object, final String name) {
|
|
||||||
final JsonElement element = object.get(name);
|
|
||||||
return element == null || element.isJsonNull() ? null : element.getAsString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String isoOf(final Object value) {
|
|
||||||
if (value == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (value instanceof java.sql.Timestamp) {
|
|
||||||
return ((java.sql.Timestamp) value).toLocalDateTime().toString();
|
|
||||||
}
|
|
||||||
return String.valueOf(value).replace(' ', 'T');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-162
@@ -1,162 +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.interbranch.service;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
import java.time.ZoneOffset;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.apache.fineract.accounting.glaccount.validation.GLAccountTransactionGuard;
|
|
||||||
import org.apache.fineract.accounting.journalentry.service.LedgerTransactionId;
|
|
||||||
import org.apache.fineract.accounting.journalentry.service.LedgerTxnPrefix;
|
|
||||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
|
||||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
|
||||||
import org.apache.fineract.useradministration.domain.AppUser;
|
|
||||||
import org.springframework.jdbc.core.JdbcTemplate;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Keeps a branch's cash ledger true when a cashier takes or pays cash for an account held at another branch.
|
|
||||||
*
|
|
||||||
* <p>
|
|
||||||
* A savings deposit or withdrawal always posts its ledger entries in the branch the account is held at, because that is
|
|
||||||
* where the account's books are. The cash itself, though, is in the drawer of whichever cashier handled it. When that
|
|
||||||
* cashier sits at a different branch, the account's branch would show cash it never received (or paid), and the
|
|
||||||
* cashier's branch would hold cash its ledger does not know about.
|
|
||||||
*
|
|
||||||
* <p>
|
|
||||||
* This moves the cash leg to the cashier's branch, through the Liability Transfer account, exactly as a transfer
|
|
||||||
* between two branches' accounts is booked: the account's branch reverses the cash line against Liability Transfer, and
|
|
||||||
* the cashier's branch books the same cash line against it. Each branch's books balance on their own and the bank's
|
|
||||||
* total does not change. A cashier at the account's own branch, or a user who is not a cashier, changes nothing.
|
|
||||||
*/
|
|
||||||
@Service
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
@Slf4j
|
|
||||||
public class InterBranchCashService {
|
|
||||||
|
|
||||||
private static final int FINANCIAL_ACTIVITY_LIABILITY_TRANSFER = 200;
|
|
||||||
private static final int CREDIT = 1;
|
|
||||||
private static final int DEBIT = 2;
|
|
||||||
|
|
||||||
private final PlatformSecurityContext context;
|
|
||||||
private final JdbcTemplate jdbcTemplate;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param savingsTransactionId
|
|
||||||
* the deposit or withdrawal just posted
|
|
||||||
* @param accountOfficeId
|
|
||||||
* the branch the account is held at, where that posting was booked
|
|
||||||
* @param deposit
|
|
||||||
* true for cash received, false for cash paid out
|
|
||||||
*/
|
|
||||||
public void moveCashLegToCashierBranch(final Long savingsTransactionId, final Long accountOfficeId, final String accountNo,
|
|
||||||
final BigDecimal amount, final LocalDate transactionDate, final boolean deposit) {
|
|
||||||
final AppUser user = this.context.authenticatedUser();
|
|
||||||
if (user.getOffice() == null || accountOfficeId == null || savingsTransactionId == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!isCashier(user.getId())) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
moveCashLegToBranch(savingsTransactionId, accountOfficeId, user.getOffice().getId(), accountNo, amount, transactionDate, deposit);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Same as {@link #moveCashLegToCashierBranch}, for a posting made by someone other than the cashier who handled the
|
|
||||||
* cash, such as a manager approving a deposit a cashier took earlier. The branch the cash is in is given.
|
|
||||||
*/
|
|
||||||
public void moveCashLegToBranch(final Long savingsTransactionId, final Long accountOfficeId, final Long cashierOfficeId,
|
|
||||||
final String accountNo, final BigDecimal amount, final LocalDate transactionDate, final boolean deposit) {
|
|
||||||
final AppUser user = this.context.authenticatedUser();
|
|
||||||
if (cashierOfficeId == null || accountOfficeId == null || savingsTransactionId == null
|
|
||||||
|| cashierOfficeId.equals(accountOfficeId)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The cash line of the posting just made: the debit of a deposit, the credit of a withdrawal.
|
|
||||||
final List<Map<String, Object>> cashLines = this.jdbcTemplate.queryForList(
|
|
||||||
"SELECT account_id, currency_code FROM acc_gl_journal_entry WHERE savings_transaction_id = ? AND type_enum = ? "
|
|
||||||
+ "AND amount = ? AND reversed = false ORDER BY id DESC",
|
|
||||||
savingsTransactionId, deposit ? DEBIT : CREDIT, amount);
|
|
||||||
if (cashLines.isEmpty()) {
|
|
||||||
log.warn("No cash line found for savings transaction {}; its cash was not moved to the cashier's branch",
|
|
||||||
savingsTransactionId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final Long cashGlId = ((Number) cashLines.get(0).get("account_id")).longValue();
|
|
||||||
final String currencyCode = String.valueOf(cashLines.get(0).get("currency_code"));
|
|
||||||
final Long suspenseGlId = liabilityTransferGlId();
|
|
||||||
|
|
||||||
final String note = (deposit ? "Inter-branch cash deposit" : "Inter-branch cash withdrawal") + " | A/C " + accountNo
|
|
||||||
+ " | savings txn " + savingsTransactionId + " | cashier " + user.getUsername() + " | via Liability Transfer";
|
|
||||||
|
|
||||||
// Account's branch: take the cash line back out. A deposit's was Dr cash, so Cr cash; a withdrawal's was Cr cash.
|
|
||||||
post(accountOfficeId, cashGlId, deposit ? CREDIT : DEBIT, suspenseGlId, deposit ? DEBIT : CREDIT, amount, currencyCode,
|
|
||||||
transactionDate, user.getId(), note);
|
|
||||||
// Cashier's branch: the cash really moved here.
|
|
||||||
post(cashierOfficeId, cashGlId, deposit ? DEBIT : CREDIT, suspenseGlId, deposit ? CREDIT : DEBIT, amount, currencyCode,
|
|
||||||
transactionDate, user.getId(), note);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isCashier(final Long userId) {
|
|
||||||
final Integer count = this.jdbcTemplate.queryForObject("SELECT COUNT(*) FROM m_cashiers mc JOIN m_staff ms ON ms.id = mc.staff_id "
|
|
||||||
+ "JOIN m_appuser au ON au.staff_id = ms.id WHERE au.id = ?", Integer.class, userId);
|
|
||||||
return count != null && count > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Long liabilityTransferGlId() {
|
|
||||||
final List<Long> ids = this.jdbcTemplate.queryForList(
|
|
||||||
"SELECT gl_account_id FROM acc_gl_financial_activity_account WHERE financial_activity_type = ?", Long.class,
|
|
||||||
FINANCIAL_ACTIVITY_LIABILITY_TRANSFER);
|
|
||||||
if (ids.isEmpty()) {
|
|
||||||
throw new IllegalStateException("A cash deposit or withdrawal for another branch's account needs the Liability Transfer "
|
|
||||||
+ "account mapped under Accounting > Financial Activity Mappings.");
|
|
||||||
}
|
|
||||||
return ids.get(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** One balanced entry in one branch: the cash line and its Liability Transfer counterpart. */
|
|
||||||
private void post(final Long officeId, final Long cashGlId, final int cashType, final Long suspenseGlId, final int suspenseType,
|
|
||||||
final BigDecimal amount, final String currencyCode, final LocalDate entryDate, final long userId, final String note) {
|
|
||||||
final String txnId = LedgerTransactionId.next(this.jdbcTemplate, LedgerTxnPrefix.INTER_BRANCH_CASH);
|
|
||||||
final LocalDateTime now = LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant());
|
|
||||||
insert(officeId, cashGlId, cashType, amount, txnId, entryDate, userId, now, currencyCode, note);
|
|
||||||
insert(officeId, suspenseGlId, suspenseType, amount, txnId, entryDate, userId, now, currencyCode, note);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void insert(final Long officeId, final Long glAccountId, final int typeEnum, final BigDecimal amount, final String txnId,
|
|
||||||
final LocalDate entryDate, final long userId, final LocalDateTime now, final String currencyCode, final String description) {
|
|
||||||
GLAccountTransactionGuard.assertPostable(this.jdbcTemplate, glAccountId);
|
|
||||||
final java.time.OffsetDateTime utc = now.atZone(DateUtils.getDateTimeZoneOfTenant()).toOffsetDateTime()
|
|
||||||
.withOffsetSameInstant(ZoneOffset.UTC);
|
|
||||||
this.jdbcTemplate.update(
|
|
||||||
"INSERT INTO acc_gl_journal_entry (account_id, office_id, currency_code, transaction_id, reversed, manual_entry, "
|
|
||||||
+ " entry_date, type_enum, amount, description, created_by, last_modified_by, created_date, lastmodified_date, "
|
|
||||||
+ " is_running_balance_calculated, office_running_balance, organization_running_balance, submitted_on_date, "
|
|
||||||
+ " transaction_date, created_on_utc, last_modified_on_utc) "
|
|
||||||
+ "VALUES (?,?,?,?,false,true,?,?,?,?,?,?,?,?,false,0,0,?,?,?,?)",
|
|
||||||
glAccountId, officeId, currencyCode, txnId, java.sql.Date.valueOf(entryDate), typeEnum, amount, description, userId, userId,
|
|
||||||
now, now, java.sql.Date.valueOf(entryDate), java.sql.Date.valueOf(entryDate), utc, utc);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-6
@@ -60,7 +60,6 @@ import org.apache.fineract.infrastructure.core.api.DateParam;
|
|||||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||||
import org.apache.fineract.infrastructure.core.data.DateFormat;
|
import org.apache.fineract.infrastructure.core.data.DateFormat;
|
||||||
import org.apache.fineract.infrastructure.core.data.UploadRequest;
|
import org.apache.fineract.infrastructure.core.data.UploadRequest;
|
||||||
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
|
|
||||||
import org.apache.fineract.infrastructure.core.exception.UnrecognizedQueryParamException;
|
import org.apache.fineract.infrastructure.core.exception.UnrecognizedQueryParamException;
|
||||||
import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings;
|
import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings;
|
||||||
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
|
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
|
||||||
@@ -230,11 +229,6 @@ public class JournalEntriesApiResource {
|
|||||||
@PathParam("transactionId") @Parameter(description = "transactionId") final String transactionId,
|
@PathParam("transactionId") @Parameter(description = "transactionId") final String transactionId,
|
||||||
@QueryParam("command") @Parameter(description = "command") final String commandParam) {
|
@QueryParam("command") @Parameter(description = "command") final String commandParam) {
|
||||||
CommandProcessingResult result;
|
CommandProcessingResult result;
|
||||||
if (is(commandParam, "reverse")) {
|
|
||||||
throw new PlatformDataIntegrityException("error.msg.journalentry.reverse.not.allowed",
|
|
||||||
"Reversing a journal entry is not allowed.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (is(commandParam, "reverse")) {
|
if (is(commandParam, "reverse")) {
|
||||||
final CommandWrapper commandRequest = new CommandWrapperBuilder().reverseJournalEntry(transactionId).withJson(jsonRequestBody)
|
final CommandWrapper commandRequest = new CommandWrapperBuilder().reverseJournalEntry(transactionId).withJson(jsonRequestBody)
|
||||||
.build();
|
.build();
|
||||||
|
|||||||
+44
-197
@@ -27,7 +27,6 @@ import java.util.Map;
|
|||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.apache.fineract.accounting.closure.domain.GLClosure;
|
import org.apache.fineract.accounting.closure.domain.GLClosure;
|
||||||
import org.apache.fineract.accounting.closure.domain.GLClosureRepository;
|
import org.apache.fineract.accounting.closure.domain.GLClosureRepository;
|
||||||
@@ -43,7 +42,6 @@ import org.apache.fineract.accounting.financialactivityaccount.domain.FinancialA
|
|||||||
import org.apache.fineract.accounting.financialactivityaccount.domain.FinancialActivityAccountRepositoryWrapper;
|
import org.apache.fineract.accounting.financialactivityaccount.domain.FinancialActivityAccountRepositoryWrapper;
|
||||||
import org.apache.fineract.accounting.glaccount.domain.GLAccount;
|
import org.apache.fineract.accounting.glaccount.domain.GLAccount;
|
||||||
import org.apache.fineract.accounting.glaccount.domain.GLAccountRepository;
|
import org.apache.fineract.accounting.glaccount.domain.GLAccountRepository;
|
||||||
import org.apache.fineract.accounting.glaccount.domain.GLAccountType;
|
|
||||||
import org.apache.fineract.accounting.journalentry.data.ChargePaymentDTO;
|
import org.apache.fineract.accounting.journalentry.data.ChargePaymentDTO;
|
||||||
import org.apache.fineract.accounting.journalentry.data.ClientChargePaymentDTO;
|
import org.apache.fineract.accounting.journalentry.data.ClientChargePaymentDTO;
|
||||||
import org.apache.fineract.accounting.journalentry.data.ClientTransactionDTO;
|
import org.apache.fineract.accounting.journalentry.data.ClientTransactionDTO;
|
||||||
@@ -70,7 +68,6 @@ import org.apache.fineract.infrastructure.event.business.domain.journalentry.Loa
|
|||||||
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
|
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
|
||||||
import org.apache.fineract.organisation.office.domain.Office;
|
import org.apache.fineract.organisation.office.domain.Office;
|
||||||
import org.apache.fineract.organisation.office.domain.OfficeRepository;
|
import org.apache.fineract.organisation.office.domain.OfficeRepository;
|
||||||
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
|
|
||||||
import org.apache.fineract.portfolio.PortfolioProductType;
|
import org.apache.fineract.portfolio.PortfolioProductType;
|
||||||
import org.apache.fineract.portfolio.account.PortfolioAccountType;
|
import org.apache.fineract.portfolio.account.PortfolioAccountType;
|
||||||
import org.apache.fineract.portfolio.account.service.AccountTransfersReadPlatformService;
|
import org.apache.fineract.portfolio.account.service.AccountTransfersReadPlatformService;
|
||||||
@@ -84,7 +81,6 @@ import org.apache.fineract.portfolio.shareaccounts.data.ShareAccountTransactionE
|
|||||||
import org.springframework.dao.DataAccessException;
|
import org.springframework.dao.DataAccessException;
|
||||||
import org.springframework.jdbc.core.JdbcTemplate;
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class AccountingProcessorHelper {
|
public class AccountingProcessorHelper {
|
||||||
|
|
||||||
@@ -650,12 +646,10 @@ public class AccountingProcessorHelper {
|
|||||||
*/
|
*/
|
||||||
public void createInterestPostingJournalEntriesForSavings(final Office office, final String currencyCode,
|
public void createInterestPostingJournalEntriesForSavings(final Office office, final String currencyCode,
|
||||||
final Long savingsProductId, final Long paymentTypeId, final Long savingsId, final String transactionId,
|
final Long savingsProductId, final Long paymentTypeId, final Long savingsId, final String transactionId,
|
||||||
final LocalDate transactionDate, final BigDecimal amount, final boolean isReversal, final boolean payableCarriesAccrual) {
|
final LocalDate transactionDate, final BigDecimal amount, final boolean isReversal) {
|
||||||
|
|
||||||
final GLAccount controlAccount = getLinkedGLAccountForSavingsProduct(savingsProductId,
|
final GLAccount controlAccount = getLinkedGLAccountForSavingsProduct(savingsProductId,
|
||||||
CashAccountsForSavings.SAVINGS_CONTROL.getValue(), paymentTypeId);
|
CashAccountsForSavings.SAVINGS_CONTROL.getValue(), paymentTypeId);
|
||||||
final GLAccount interestOnSavingsAccount = getLinkedGLAccountForSavingsProduct(savingsProductId,
|
|
||||||
CashAccountsForSavings.INTEREST_ON_SAVINGS.getValue(), paymentTypeId);
|
|
||||||
|
|
||||||
// The payable comes from the Interest Payable row on the product's Accounting tab, which is
|
// The payable comes from the Interest Payable row on the product's Accounting tab, which is
|
||||||
// where a bank actually picks it. The reserved name in the product's other GL mappings is
|
// where a bank actually picks it. The reserved name in the product's other GL mappings is
|
||||||
@@ -664,103 +658,30 @@ public class AccountingProcessorHelper {
|
|||||||
final GLAccount payableAccount = Optional
|
final GLAccount payableAccount = Optional
|
||||||
.ofNullable(optionalLinkedGLAccountForSavingsProduct(savingsProductId,
|
.ofNullable(optionalLinkedGLAccountForSavingsProduct(savingsProductId,
|
||||||
AccrualAccountsForSavings.INTEREST_PAYABLE.getValue(), paymentTypeId))
|
AccrualAccountsForSavings.INTEREST_PAYABLE.getValue(), paymentTypeId))
|
||||||
.or(() -> Optional.ofNullable(namedSavingsGlAccount(savingsProductId,
|
.or(() -> Optional.ofNullable(namedSavingsGlAccount(savingsProductId, SAVINGS_INTEREST_PAYABLE_MAPPING)))
|
||||||
SAVINGS_INTEREST_PAYABLE_MAPPING, "Fixed Deposit Interest Payable", "Recurring Deposit Interest Payable",
|
|
||||||
"Interest Payable", "Interest Payable Account")))
|
|
||||||
.orElse(controlAccount);
|
.orElse(controlAccount);
|
||||||
// The RD product screen saves its paid account as "Recurring Deposit Interest Paid"; without that name here an
|
final GLAccount paidAccount = Optional.ofNullable(namedSavingsGlAccount(savingsProductId, SAVINGS_INTEREST_PAID_MAPPING))
|
||||||
// RD's interest skipped the paid leg even when the product had one mapped.
|
|
||||||
final GLAccount paidAccount = Optional.ofNullable(namedSavingsGlAccount(savingsProductId,
|
|
||||||
SAVINGS_INTEREST_PAID_MAPPING, "Fixed Deposit Interest Paid", "Recurring Deposit Interest Paid", "Interest Paid",
|
|
||||||
"Interest Paid Account"))
|
|
||||||
.orElse(payableAccount);
|
.orElse(payableAccount);
|
||||||
|
|
||||||
log.info("createInterestPostingJournalEntriesForSavings: savingsProductId={}, payableAccount=[{} - {}], paidAccount=[{} - {}], controlAccount=[{} - {}], interestExpense=[{} - {}], amount={}, isReversal={}",
|
// The interest expense is not raised here. It is recognised as the interest accrues, day by
|
||||||
savingsProductId, payableAccount.getId(), payableAccount.getName(), paidAccount.getId(), paidAccount.getName(),
|
// day, and this posting settles that recognised debt - raising the expense again at payout
|
||||||
controlAccount.getId(), controlAccount.getName(), interestOnSavingsAccount.getId(), interestOnSavingsAccount.getName(),
|
// would put the same interest on the P&L twice. This mirrors the pooled posting service,
|
||||||
amount, isReversal);
|
// which pays a product's whole membership the same way; a closure reaching this path must
|
||||||
|
// produce the same entries as the nightly run, or the two disagree on what one payment did.
|
||||||
// A product that names no payable falls back to Savings Control above. The chain below would
|
|
||||||
// then open with Dr Savings Control / Cr Interest On Savings and close with its exact
|
|
||||||
// reverse, the two cancelling: the expense never charged and the member never credited. Such
|
|
||||||
// a product has no liability to draw down, so it keeps the single pair it always posted.
|
|
||||||
if (payableAccount.getId().equals(controlAccount.getId())) {
|
|
||||||
log.warn("createInterestPostingJournalEntriesForSavings: payableAccount == controlAccount [id={}], posting standard 2-leg entry",
|
|
||||||
payableAccount.getId());
|
|
||||||
postSavingsLeg(office, currencyCode, interestOnSavingsAccount, controlAccount, savingsId, transactionId, transactionDate,
|
|
||||||
amount, isReversal);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The interest is charged to expense and raised as a liability first, for whatever part of
|
|
||||||
// it no accrual has already covered.
|
|
||||||
//
|
//
|
||||||
// The chain below draws the payable DOWN, so it assumes an accrual already credited it for
|
// Stage 1: the liability moves from the payable to the account it is paid out of. Where the
|
||||||
// the period. Where none ran - a product posting on cash accounting, or a period the accrual
|
// product names no paid account both resolve to the payable and this says nothing, so it is
|
||||||
// job never covered - the payable went negative by the interest and the expense netted to
|
// skipped and the payout comes straight out of the payable.
|
||||||
// nothing across the next two legs, leaving the charge off the P&L entirely. A liability
|
final boolean payableIsPaid = payableAccount.getId().equals(paidAccount.getId());
|
||||||
// account cannot hold a negative balance: the bank does not owe less than nothing.
|
if (!payableIsPaid) {
|
||||||
//
|
|
||||||
// So the shortfall is raised here rather than assumed away. Where the accrual DID run, the
|
|
||||||
// payable already carries the interest and this leg writes nothing - the balance below is
|
|
||||||
// enough, and raising it again would double the liability.
|
|
||||||
//
|
|
||||||
// Only a liability payable is topped up. A bank that maps an expense GL as the payable books
|
|
||||||
// the cost there directly: payableBalance reads credit minus debit, so an expense's normal
|
|
||||||
// debit balance would come back negative and every posting would raise a shortfall larger
|
|
||||||
// than the interest itself.
|
|
||||||
final boolean payableIsLiability = GLAccountType.LIABILITY.getValue().equals(payableAccount.getType());
|
|
||||||
if (!payableCarriesAccrual) {
|
|
||||||
// Cash basis: nothing ever accrues into the payable, so whatever already sits on it - an opening balance
|
|
||||||
// brought over at migration, or another product's interest sharing the GL - is not this interest. Reading
|
|
||||||
// it as "already accrued" skipped the expense and drew that balance down instead, leaving the cost off the
|
|
||||||
// P&L. The whole amount is charged and raised, and a reversal or clawback takes it back the same way.
|
|
||||||
if (payableIsLiability) {
|
|
||||||
postSavingsLeg(office, currencyCode, interestOnSavingsAccount, payableAccount, savingsId, transactionId, transactionDate,
|
|
||||||
amount, isReversal);
|
|
||||||
}
|
|
||||||
postPayableToMember(office, currencyCode, payableAccount, paidAccount, controlAccount, savingsId, transactionId,
|
|
||||||
transactionDate, amount, isReversal);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final BigDecimal alreadyAccrued = payableBalance(office, payableAccount, currencyCode);
|
|
||||||
final BigDecimal shortfall = amount.subtract(alreadyAccrued).max(BigDecimal.ZERO);
|
|
||||||
log.info("createInterestPostingJournalEntriesForSavings: alreadyAccrued={}, shortfall={}, payableIsLiability={}, isReversal={}",
|
|
||||||
alreadyAccrued, shortfall, payableIsLiability, isReversal);
|
|
||||||
if (!isReversal && payableIsLiability && shortfall.compareTo(BigDecimal.ZERO) > 0) {
|
|
||||||
log.info("createInterestPostingJournalEntriesForSavings: posting Stage 1 [Dr {} / Cr {} = {}]",
|
|
||||||
interestOnSavingsAccount.getName(), payableAccount.getName(), shortfall);
|
|
||||||
// Stage 1: Dr Interest On Savings (the expense) / Cr Payable - the accrual that should have run.
|
|
||||||
postSavingsLeg(office, currencyCode, interestOnSavingsAccount, payableAccount, savingsId, transactionId, transactionDate,
|
|
||||||
shortfall, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
postPayableToMember(office, currencyCode, payableAccount, paidAccount, controlAccount, savingsId, transactionId, transactionDate,
|
|
||||||
amount, isReversal);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Stages 2 and 3: out of the payable, through the paid account when the product names one, into the member. */
|
|
||||||
private void postPayableToMember(final Office office, final String currencyCode, final GLAccount payableAccount,
|
|
||||||
final GLAccount paidAccount, final GLAccount controlAccount, final Long savingsId, final String transactionId,
|
|
||||||
final LocalDate transactionDate, final BigDecimal amount, final boolean isReversal) {
|
|
||||||
final boolean hasSeparatePaidAccount = !payableAccount.getId().equals(paidAccount.getId());
|
|
||||||
if (hasSeparatePaidAccount) {
|
|
||||||
log.info("createInterestPostingJournalEntriesForSavings: posting Stage 2 [Dr {} / Cr {} = {}] and Stage 3 [Dr {} / Cr {} = {}]",
|
|
||||||
payableAccount.getName(), paidAccount.getName(), amount, paidAccount.getName(), controlAccount.getName(), amount);
|
|
||||||
// Stage 2: Out of the payable, into the interest-paid transit/clearing account
|
|
||||||
postSavingsLeg(office, currencyCode, payableAccount, paidAccount, savingsId, transactionId, transactionDate, amount,
|
postSavingsLeg(office, currencyCode, payableAccount, paidAccount, savingsId, transactionId, transactionDate, amount,
|
||||||
isReversal);
|
isReversal);
|
||||||
|
|
||||||
// Stage 3: Out of the interest-paid account, into the member's control account
|
|
||||||
postSavingsLeg(office, currencyCode, paidAccount, controlAccount, savingsId, transactionId, transactionDate, amount,
|
|
||||||
isReversal);
|
|
||||||
} else {
|
|
||||||
log.info("createInterestPostingJournalEntriesForSavings: no separate paidAccount, posting direct payout [Dr {} / Cr {} = {}]",
|
|
||||||
payableAccount.getName(), controlAccount.getName(), amount);
|
|
||||||
// Payout directly from payable into member's control account
|
|
||||||
postSavingsLeg(office, currencyCode, payableAccount, controlAccount, savingsId, transactionId, transactionDate, amount,
|
|
||||||
isReversal);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stage 2: into the member's own balance.
|
||||||
|
final GLAccount payoutFromAccount = payableIsPaid ? payableAccount : paidAccount;
|
||||||
|
postSavingsLeg(office, currencyCode, payoutFromAccount, controlAccount, savingsId, transactionId, transactionDate, amount,
|
||||||
|
isReversal);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -802,69 +723,34 @@ public class AccountingProcessorHelper {
|
|||||||
*
|
*
|
||||||
* @return the account, or null where the product does not configure this name
|
* @return the account, or null where the product does not configure this name
|
||||||
*/
|
*/
|
||||||
/**
|
private GLAccount namedSavingsGlAccount(final Long savingsProductId, final String accountName) {
|
||||||
* What the interest payable account currently holds for this office, on its own liability side.
|
if (savingsProductId == null) {
|
||||||
*
|
|
||||||
* <p>
|
|
||||||
* Read so an interest posting can tell an accrual that already ran from one that did not. The
|
|
||||||
* posting chain draws the payable down; where nothing had raised it, the account went negative
|
|
||||||
* and the expense netted to nothing, so the charge never reached the P&L at all. The caller
|
|
||||||
* raises only the shortfall, which is nil when the accrual did run.
|
|
||||||
*
|
|
||||||
* <p>
|
|
||||||
* Credits less debits, because a payable is a liability: the balance is what the bank still
|
|
||||||
* owes. Floored at zero by the caller rather than here - a negative balance is a real state to
|
|
||||||
* report, and the caller's arithmetic needs the true figure, not a clamped one.
|
|
||||||
*/
|
|
||||||
private BigDecimal payableBalance(final Office office, final GLAccount payableAccount, final String currencyCode) {
|
|
||||||
if (payableAccount == null || payableAccount.getId() == null) {
|
|
||||||
return BigDecimal.ZERO;
|
|
||||||
}
|
|
||||||
// Reversed entries and the entries they reverse are both excluded: a reversal pair nets to
|
|
||||||
// nothing, and counting either side alone would move the balance by the whole amount.
|
|
||||||
final BigDecimal balance = jdbcTemplate.queryForObject("""
|
|
||||||
SELECT COALESCE(SUM(CASE WHEN type_enum = 1 THEN amount ELSE -amount END), 0)
|
|
||||||
FROM acc_gl_journal_entry
|
|
||||||
WHERE account_id = ?
|
|
||||||
AND office_id = ?
|
|
||||||
AND currency_code = ?
|
|
||||||
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)
|
|
||||||
""", BigDecimal.class, payableAccount.getId(), office.getId(), currencyCode);
|
|
||||||
return balance == null ? BigDecimal.ZERO : balance;
|
|
||||||
}
|
|
||||||
|
|
||||||
private GLAccount namedSavingsGlAccount(final Long savingsProductId, final String... accountNames) {
|
|
||||||
if (savingsProductId == null || accountNames == null || accountNames.length == 0) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
for (final String accountName : accountNames) {
|
// The column holds an empty string on a product saved with no named mappings, and casting
|
||||||
if (accountName == null) {
|
// that to jsonb is an error rather than an empty document - so the cast is done inside a
|
||||||
continue;
|
// subquery that has already filtered those rows out. Postgres is free to evaluate a WHERE
|
||||||
}
|
// clause in any order, so guarding at the same query level as the cast would only work by
|
||||||
final List<Long> found = jdbcTemplate.queryForList("""
|
// luck.
|
||||||
SELECT gl.id
|
final List<Long> found = jdbcTemplate.queryForList("""
|
||||||
FROM (SELECT other_gl_mappings::jsonb AS doc
|
SELECT gl.id
|
||||||
FROM m_savings_product
|
FROM (SELECT other_gl_mappings::jsonb AS doc
|
||||||
WHERE id = ?
|
FROM m_savings_product
|
||||||
AND other_gl_mappings IS NOT NULL
|
WHERE id = ?
|
||||||
AND other_gl_mappings <> ''
|
AND other_gl_mappings IS NOT NULL
|
||||||
AND other_gl_mappings LIKE '{%') sp
|
AND other_gl_mappings <> ''
|
||||||
CROSS JOIN LATERAL jsonb_array_elements(
|
AND other_gl_mappings LIKE '{%') sp
|
||||||
COALESCE(sp.doc -> 'assets', '[]'::jsonb)
|
CROSS JOIN LATERAL jsonb_array_elements(
|
||||||
|| COALESCE(sp.doc -> 'liabilities', '[]'::jsonb)
|
COALESCE(sp.doc -> 'assets', '[]'::jsonb)
|
||||||
|| COALESCE(sp.doc -> 'expenses', '[]'::jsonb)
|
|| COALESCE(sp.doc -> 'liabilities', '[]'::jsonb)
|
||||||
|| COALESCE(sp.doc -> 'income', '[]'::jsonb)
|
|| COALESCE(sp.doc -> 'expenses', '[]'::jsonb)
|
||||||
) AS m(entry)
|
|| COALESCE(sp.doc -> 'income', '[]'::jsonb)
|
||||||
JOIN acc_gl_account gl ON gl.gl_code = m.entry ->> 'glCode'
|
) AS m(entry)
|
||||||
WHERE lower(trim(m.entry ->> 'accountName')) = lower(trim(?))
|
JOIN acc_gl_account gl ON gl.gl_code = m.entry ->> 'glCode'
|
||||||
LIMIT 1
|
WHERE m.entry ->> 'accountName' = ?
|
||||||
""", Long.class, savingsProductId, accountName);
|
LIMIT 1
|
||||||
if (!found.isEmpty()) {
|
""", Long.class, savingsProductId, accountName);
|
||||||
return glAccountRepository.findById(found.get(0)).orElse(null);
|
return found.isEmpty() ? null : glAccountRepository.findById(found.get(0)).orElse(null);
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -888,45 +774,6 @@ public class AccountingProcessorHelper {
|
|||||||
createCreditJournalEntryForSavings(office, currencyCode, creditAccount, creditEntityId, transactionId, transactionDate, amount);
|
createCreditJournalEntryForSavings(office, currencyCode, creditAccount, creditEntityId, transactionId, transactionDate, amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* The same direct transfer where either side may be a current account. A current product maps its GLs under
|
|
||||||
* {@link PortfolioProductType#CURRENT}, so each side's control GL is read under its own product type -- reading a
|
|
||||||
* current product under SAVING picked up the GLs of the savings product that happens to share its id.
|
|
||||||
*/
|
|
||||||
public void createDirectTransferJournalEntries(final Office office, final String currencyCode, final Long debitProductId,
|
|
||||||
final boolean debitIsCurrent, final Long debitEntityId, final Long creditProductId, final boolean creditIsCurrent,
|
|
||||||
final Long creditEntityId, final String transactionId, final LocalDate transactionDate, final BigDecimal amount) {
|
|
||||||
final GLAccount debitAccount = controlGlAccount(debitProductId, debitIsCurrent);
|
|
||||||
final GLAccount creditAccount = controlGlAccount(creditProductId, creditIsCurrent);
|
|
||||||
createDebitJournalEntryForSavings(office, currencyCode, debitAccount, debitEntityId, transactionId, transactionDate, amount);
|
|
||||||
createCreditJournalEntryForSavings(office, currencyCode, creditAccount, creditEntityId, transactionId, transactionDate, amount);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Dr a current product's Current Control, Cr the GL mapped to {@code financialActivity}. */
|
|
||||||
public void createCurrentControlToFinancialActivityJournalEntries(final Office office, final String currencyCode,
|
|
||||||
final Long currentProductId, final int financialActivity, final Long currentAccountId, final String transactionId,
|
|
||||||
final LocalDate transactionDate, final BigDecimal amount) {
|
|
||||||
final GLAccount controlAccount = controlGlAccount(currentProductId, true);
|
|
||||||
final GLAccount activityAccount = this.financialActivityAccountRepository
|
|
||||||
.findByFinancialActivityTypeWithNotFoundDetection(financialActivity).getGlAccount();
|
|
||||||
createDebitJournalEntryForSavings(office, currencyCode, controlAccount, currentAccountId, transactionId, transactionDate, amount);
|
|
||||||
createCreditJournalEntryForSavings(office, currencyCode, activityAccount, currentAccountId, transactionId, transactionDate, amount);
|
|
||||||
}
|
|
||||||
|
|
||||||
private GLAccount controlGlAccount(final Long productId, final boolean isCurrent) {
|
|
||||||
if (!isCurrent) {
|
|
||||||
return getLinkedGLAccountForSavingsProduct(productId, CashAccountsForSavings.SAVINGS_CONTROL.getValue(), null);
|
|
||||||
}
|
|
||||||
final ProductToGLAccountMapping mapping = this.accountMappingRepository.findCoreProductToFinAccountMapping(productId,
|
|
||||||
PortfolioProductType.CURRENT.getValue(), CashAccountsForSavings.SAVINGS_CONTROL.getValue());
|
|
||||||
if (mapping == null) {
|
|
||||||
throw new GeneralPlatformDomainRuleException("error.msg.current.product.control.gl.not.mapped",
|
|
||||||
"Current product " + productId + " has no Current Control GL mapped; map it on the product's Accounting tab.",
|
|
||||||
productId);
|
|
||||||
}
|
|
||||||
return mapping.getGlAccount();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convenience method that creates a pair of related Debits and Credits for Cash Based accounting.
|
* Convenience method that creates a pair of related Debits and Credits for Cash Based accounting.
|
||||||
*
|
*
|
||||||
|
|||||||
+6
-22
@@ -173,28 +173,12 @@ public class AccrualBasedAccountingProcessorForSavings implements AccountingProc
|
|||||||
}
|
}
|
||||||
|
|
||||||
else if (savingsTransactionDTO.getTransactionType().isInterestPosting()) {
|
else if (savingsTransactionDTO.getTransactionType().isInterestPosting()) {
|
||||||
// A premature FD closure that reprices interest downward (see
|
// Post journal entry if earned interest amount is greater than
|
||||||
// FixedDepositAccount#postPreMaturityInterest) posts the difference as a NEGATIVE
|
// zero
|
||||||
// interest-posting transaction -- a clawback of interest already recognised in earlier
|
if (savingsTransactionDTO.getAmount().compareTo(BigDecimal.ZERO) > 0) {
|
||||||
// periods, not "zero interest, nothing to post". Skipping it here (the original
|
this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode,
|
||||||
// "amount > 0" guard) left the FD subledger showing the clawback while the GL never
|
AccrualAccountsForSavings.INTEREST_PAYABLE.getValue(), AccrualAccountsForSavings.SAVINGS_CONTROL.getValue(),
|
||||||
// recorded it, silently going out of sync. Treating a negative amount as a mirror-image
|
savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal);
|
||||||
// reversal -- positive magnitude, debit/credit legs swapped, same mechanism
|
|
||||||
// createInterestPostingJournalEntriesForSavings already uses for isReversal -- keeps the two
|
|
||||||
// in sync instead. See CashBasedAccountingProcessorForSavings for the same fix on the
|
|
||||||
// cash-based path.
|
|
||||||
final int amountSign = savingsTransactionDTO.getAmount().compareTo(BigDecimal.ZERO);
|
|
||||||
if (amountSign != 0) {
|
|
||||||
final boolean isClawback = amountSign < 0;
|
|
||||||
final BigDecimal postingAmount = isClawback ? amount.abs() : amount;
|
|
||||||
final boolean effectiveIsReversal = isReversal ^ isClawback;
|
|
||||||
// Routed through the same helper the cash processor uses, so a posting states
|
|
||||||
// the whole chain the product is configured for -- out of the payable, through
|
|
||||||
// the interest-paid account, into the member's balance -- rather than the single
|
|
||||||
// payable-to-control pair that used to be here, which skipped interest-paid
|
|
||||||
// entirely and left the expense off the P&L.
|
|
||||||
this.helper.createInterestPostingJournalEntriesForSavings(office, currencyCode, savingsProductId, paymentTypeId,
|
|
||||||
savingsId, transactionId, transactionDate, postingAmount, effectiveIsReversal, true);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+4
-15
@@ -167,26 +167,15 @@ public class CashBasedAccountingProcessorForSavings implements AccountingProcess
|
|||||||
}
|
}
|
||||||
|
|
||||||
else if (savingsTransactionDTO.getTransactionType().isInterestPosting()) {
|
else if (savingsTransactionDTO.getTransactionType().isInterestPosting()) {
|
||||||
// A premature FD closure that reprices interest downward (see
|
// Post journal entry if earned interest amount is greater than
|
||||||
// FixedDepositAccount#postPreMaturityInterest) posts the difference as a NEGATIVE
|
// zero
|
||||||
// interest-posting transaction -- a clawback of interest already recognised in earlier
|
if (savingsTransactionDTO.getAmount().compareTo(BigDecimal.ZERO) > 0) {
|
||||||
// periods, not "zero interest, nothing to post". Skipping it here (the original
|
|
||||||
// "amount > 0" guard) left the FD subledger showing the clawback while the GL never
|
|
||||||
// recorded it, silently going out of sync. Treating a negative amount as a mirror-image
|
|
||||||
// reversal -- positive magnitude, debit/credit legs swapped, same mechanism
|
|
||||||
// createInterestPostingJournalEntriesForSavings already uses for isReversal -- keeps the two
|
|
||||||
// in sync instead.
|
|
||||||
final int amountSign = savingsTransactionDTO.getAmount().compareTo(BigDecimal.ZERO);
|
|
||||||
if (amountSign != 0) {
|
|
||||||
final boolean isClawback = amountSign < 0;
|
|
||||||
final BigDecimal postingAmount = isClawback ? amount.abs() : amount;
|
|
||||||
final boolean effectiveIsReversal = isReversal ^ isClawback;
|
|
||||||
// Routed through the payable and paid accounts the product names, so the society
|
// Routed through the payable and paid accounts the product names, so the society
|
||||||
// can read interest declared but not yet credited as its own balance. Collapses
|
// can read interest declared but not yet credited as its own balance. Collapses
|
||||||
// to the plain Dr Interest On Savings / Cr Savings Control pair for a product
|
// to the plain Dr Interest On Savings / Cr Savings Control pair for a product
|
||||||
// that names neither.
|
// that names neither.
|
||||||
this.helper.createInterestPostingJournalEntriesForSavings(office, currencyCode, savingsProductId, paymentTypeId,
|
this.helper.createInterestPostingJournalEntriesForSavings(office, currencyCode, savingsProductId, paymentTypeId,
|
||||||
savingsId, transactionId, transactionDate, postingAmount, effectiveIsReversal, false);
|
savingsId, transactionId, transactionDate, amount, isReversal);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-17
@@ -105,23 +105,7 @@ public class JournalEntryReadPlatformServiceImpl implements JournalEntryReadPlat
|
|||||||
.append(" journalEntry.currency_code as currencyCode, curr.name as currencyName, curr.internationalized_name_code as currencyNameCode, ")
|
.append(" journalEntry.currency_code as currencyCode, curr.name as currencyName, curr.internationalized_name_code as currencyNameCode, ")
|
||||||
.append(" curr.display_symbol as currencyDisplaySymbol, curr.decimal_places as currencyDigits, curr.currency_multiplesof as inMultiplesOf, ")
|
.append(" curr.display_symbol as currencyDisplaySymbol, curr.decimal_places as currencyDigits, curr.currency_multiplesof as inMultiplesOf, ")
|
||||||
.append(" eao.external_id as externalAssetOwner, ")
|
.append(" eao.external_id as externalAssetOwner, ")
|
||||||
// entity_type_enum=6 (PortfolioProductType.CURRENT) unambiguously names a row in
|
|
||||||
// m_current_account -- newer write paths tag current-account entries with it
|
|
||||||
// instead of reusing SAVING's type, specifically so this case needs no guessing.
|
|
||||||
//
|
|
||||||
// entity_type_enum=2 (PortfolioProductType.SAVING) is still written for BOTH savings
|
|
||||||
// and current-account entries by older write paths not yet updated to the above --
|
|
||||||
// current products used to deliberately share SAVING's GL type space (see
|
|
||||||
// PortfolioProductType.CURRENT's own comment) -- and m_savings_account/
|
|
||||||
// m_current_account each have their own id sequence, so an entity_id can be a real
|
|
||||||
// row in both, making a plain COALESCE(ca.account_no, sa.account_no) pick the current
|
|
||||||
// account's number even for an entry that is genuinely a savings one.
|
|
||||||
// savings_transaction_id is the one unambiguous signal available for those older
|
|
||||||
// entries -- only a real savings transaction sets it -- so it decides first; only
|
|
||||||
// when it's absent does the current-account guess apply.
|
|
||||||
.append(" CASE ")
|
.append(" CASE ")
|
||||||
.append(" WHEN journalEntry.entity_type_enum = 6 THEN ca.account_no ")
|
|
||||||
.append(" WHEN journalEntry.entity_type_enum = 2 AND journalEntry.savings_transaction_id IS NOT NULL THEN sa.account_no ")
|
|
||||||
.append(" WHEN journalEntry.entity_type_enum = 2 THEN COALESCE(ca.account_no, sa.account_no) ")
|
.append(" WHEN journalEntry.entity_type_enum = 2 THEN COALESCE(ca.account_no, sa.account_no) ")
|
||||||
.append(" ELSE NULL ")
|
.append(" ELSE NULL ")
|
||||||
.append(" END as savingsAccountNo ");
|
.append(" END as savingsAccountNo ");
|
||||||
@@ -146,7 +130,7 @@ public class JournalEntryReadPlatformServiceImpl implements JournalEntryReadPlat
|
|||||||
.append(" left join m_external_asset_owner_journal_entry_mapping eajem on eajem.journal_entry_id = journalEntry.id ")
|
.append(" left join m_external_asset_owner_journal_entry_mapping eajem on eajem.journal_entry_id = journalEntry.id ")
|
||||||
.append(" left join m_external_asset_owner eao on eao.id = eajem.owner_id ")
|
.append(" left join m_external_asset_owner eao on eao.id = eajem.owner_id ")
|
||||||
.append(" left join m_savings_account sa on sa.id = journalEntry.entity_id AND journalEntry.entity_type_enum = 2 ")
|
.append(" left join m_savings_account sa on sa.id = journalEntry.entity_id AND journalEntry.entity_type_enum = 2 ")
|
||||||
.append(" left join m_current_account ca on ca.id = journalEntry.entity_id AND journalEntry.entity_type_enum IN (2, 6) ");
|
.append(" left join m_current_account ca on ca.id = journalEntry.entity_id AND journalEntry.entity_type_enum = 2 ");
|
||||||
if (associationParametersData.isTransactionDetailsRequired()) {
|
if (associationParametersData.isTransactionDetailsRequired()) {
|
||||||
sb.append(" left join m_loan_transaction as lt on journalEntry.loan_transaction_id = lt.id ")
|
sb.append(" left join m_loan_transaction as lt on journalEntry.loan_transaction_id = lt.id ")
|
||||||
.append(" left join m_savings_account_transaction as st on journalEntry.savings_transaction_id = st.id ")
|
.append(" left join m_savings_account_transaction as st on journalEntry.savings_transaction_id = st.id ")
|
||||||
|
|||||||
+1
-31
@@ -687,16 +687,6 @@ public class JournalEntryWritePlatformServiceJpaRepositoryImpl implements Journa
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void validateBusinessRulesForJournalEntries(final JournalEntryCommand command) {
|
private void validateBusinessRulesForJournalEntries(final JournalEntryCommand command) {
|
||||||
validateBusinessRulesForJournalEntries(command, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param requireBalanced
|
|
||||||
* false for opening balances: every leg is posted against the contra account, so the books always
|
|
||||||
* balance, and the submitted legs legitimately differ by the locked (denomination-managed) cash
|
|
||||||
* account's opening balance that is already posted and can never be part of this request.
|
|
||||||
*/
|
|
||||||
private void validateBusinessRulesForJournalEntries(final JournalEntryCommand command, final boolean requireBalanced) {
|
|
||||||
// check if date of Journal entry is valid
|
// check if date of Journal entry is valid
|
||||||
final LocalDate transactionDate = command.getTransactionDate();
|
final LocalDate transactionDate = command.getTransactionDate();
|
||||||
// shouldn't be in the future
|
// shouldn't be in the future
|
||||||
@@ -716,26 +706,6 @@ public class JournalEntryWritePlatformServiceJpaRepositoryImpl implements Journa
|
|||||||
final SingleDebitOrCreditEntryCommand[] credits = command.getCredits();
|
final SingleDebitOrCreditEntryCommand[] credits = command.getCredits();
|
||||||
final SingleDebitOrCreditEntryCommand[] debits = command.getDebits();
|
final SingleDebitOrCreditEntryCommand[] debits = command.getDebits();
|
||||||
|
|
||||||
if (!requireBalanced) {
|
|
||||||
final List<SingleDebitOrCreditEntryCommand> entries = new ArrayList<>();
|
|
||||||
if (credits != null) {
|
|
||||||
entries.addAll(Arrays.asList(credits));
|
|
||||||
}
|
|
||||||
if (debits != null) {
|
|
||||||
entries.addAll(Arrays.asList(debits));
|
|
||||||
}
|
|
||||||
if (entries.isEmpty()) {
|
|
||||||
throw new JournalEntryInvalidException(GlJournalEntryInvalidReason.NO_DEBITS_OR_CREDITS, null, null, null);
|
|
||||||
}
|
|
||||||
for (final SingleDebitOrCreditEntryCommand entry : entries) {
|
|
||||||
if (entry.getAmount() == null || entry.getGlAccountId() == null) {
|
|
||||||
throw new JournalEntryInvalidException(GlJournalEntryInvalidReason.DEBIT_CREDIT_ACCOUNT_OR_AMOUNT_EMPTY, null, null,
|
|
||||||
null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// atleast one debit or credit must be present
|
// atleast one debit or credit must be present
|
||||||
if (credits == null || credits.length == 0 || debits == null || debits.length == 0) {
|
if (credits == null || credits.length == 0 || debits == null || debits.length == 0) {
|
||||||
throw new JournalEntryInvalidException(GlJournalEntryInvalidReason.NO_DEBITS_OR_CREDITS, null, null, null);
|
throw new JournalEntryInvalidException(GlJournalEntryInvalidReason.NO_DEBITS_OR_CREDITS, null, null, null);
|
||||||
@@ -813,7 +783,7 @@ public class JournalEntryWritePlatformServiceJpaRepositoryImpl implements Journa
|
|||||||
final Office office = this.officeRepositoryWrapper.findOneWithNotFoundDetection(officeId);
|
final Office office = this.officeRepositoryWrapper.findOneWithNotFoundDetection(officeId);
|
||||||
final String currencyCode = command.stringValueOfParameterNamed(JournalEntryJsonInputParams.CURRENCY_CODE.getValue());
|
final String currencyCode = command.stringValueOfParameterNamed(JournalEntryJsonInputParams.CURRENCY_CODE.getValue());
|
||||||
|
|
||||||
validateBusinessRulesForJournalEntries(journalEntryCommand, false);
|
validateBusinessRulesForJournalEntries(journalEntryCommand);
|
||||||
validateNoDenominationManagedCashAccounts(journalEntryCommand);
|
validateNoDenominationManagedCashAccounts(journalEntryCommand);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
-8
@@ -74,22 +74,14 @@ public final class LedgerTxnPrefix {
|
|||||||
public static final String CASH_MIGRATION = "CSHMIG";
|
public static final String CASH_MIGRATION = "CSHMIG";
|
||||||
/** Rounding difference on a clerk's day closure. */
|
/** Rounding difference on a clerk's day closure. */
|
||||||
public static final String CLOSURE_ROUNDING = "CLSRND";
|
public static final String CLOSURE_ROUNDING = "CLSRND";
|
||||||
/** Cash line of a deposit or withdrawal moved to the cashier's branch, when the account is held at another. */
|
|
||||||
public static final String INTER_BRANCH_CASH = "IBCASH";
|
|
||||||
|
|
||||||
// ── Year end ──────────────────────────────────────────────────────────────
|
// ── Year end ──────────────────────────────────────────────────────────────
|
||||||
/** Year-end transfer of income into the profit account. */
|
/** Year-end transfer of income into the profit account. */
|
||||||
public static final String YEAR_END_PROFIT = "YEPFT";
|
public static final String YEAR_END_PROFIT = "YEPFT";
|
||||||
/** Year-end closure of an income or expense account. */
|
/** Year-end closure of an income or expense account. */
|
||||||
public static final String YEAR_END_CLOSE = "YECLS";
|
public static final String YEAR_END_CLOSE = "YECLS";
|
||||||
/** Appropriation of a closed year's profit into the reserve funds. */
|
|
||||||
public static final String PROFIT_APPROPRIATION = "PRAPP";
|
|
||||||
|
|
||||||
// ── Savings ───────────────────────────────────────────────────────────────
|
// ── Savings ───────────────────────────────────────────────────────────────
|
||||||
/** Pooled savings interest posting for one product and date. */
|
/** Pooled savings interest posting for one product and date. */
|
||||||
public static final String SAVINGS_INTEREST_POOLED = "SAVINT";
|
public static final String SAVINGS_INTEREST_POOLED = "SAVINT";
|
||||||
|
|
||||||
// ── Fixed deposits ────────────────────────────────────────────────────────
|
|
||||||
/** Approved fixed deposit interest posting report, for one product, branch and date. */
|
|
||||||
public static final String FD_INTEREST_POSTED = "FDINT";
|
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-19
@@ -294,14 +294,8 @@ public class ProductToGLAccountMappingWritePlatformServiceImpl implements Produc
|
|||||||
SavingProductAccountingParams.TRANSFERS_SUSPENSE.getValue(), savingProductId,
|
SavingProductAccountingParams.TRANSFERS_SUSPENSE.getValue(), savingProductId,
|
||||||
CashAccountsForSavings.TRANSFERS_SUSPENSE.getValue());
|
CashAccountsForSavings.TRANSFERS_SUSPENSE.getValue());
|
||||||
|
|
||||||
// FD and RD products switch dormancy on with "dormancyAlert" (DepositProductAssembler derives the product's
|
|
||||||
// tracking flag from it) and never send isDormancyTrackingActive, so reading only that dropped the escheat
|
|
||||||
// account the screen had asked for - the product then tracked dormancy with no escheat account mapped.
|
|
||||||
final Boolean isDormancyTrackingActive = this.fromApiJsonHelper.extractBooleanNamed(isDormancyTrackingActiveParamName, element);
|
final Boolean isDormancyTrackingActive = this.fromApiJsonHelper.extractBooleanNamed(isDormancyTrackingActiveParamName, element);
|
||||||
final Boolean dormancyAlert = this.fromApiJsonHelper.parameterExists("dormancyAlert", element)
|
if (null != isDormancyTrackingActive && isDormancyTrackingActive) {
|
||||||
? this.fromApiJsonHelper.extractBooleanNamed("dormancyAlert", element)
|
|
||||||
: null;
|
|
||||||
if (Boolean.TRUE.equals(isDormancyTrackingActive) || Boolean.TRUE.equals(dormancyAlert)) {
|
|
||||||
this.savingsProductToGLAccountMappingHelper.saveSavingsToLiabilityAccountMapping(element,
|
this.savingsProductToGLAccountMappingHelper.saveSavingsToLiabilityAccountMapping(element,
|
||||||
SavingProductAccountingParams.ESCHEAT_LIABILITY.getValue(), savingProductId,
|
SavingProductAccountingParams.ESCHEAT_LIABILITY.getValue(), savingProductId,
|
||||||
CashAccountsForSavings.ESCHEAT_LIABILITY.getValue());
|
CashAccountsForSavings.ESCHEAT_LIABILITY.getValue());
|
||||||
@@ -335,18 +329,6 @@ public class ProductToGLAccountMappingWritePlatformServiceImpl implements Produc
|
|||||||
break;
|
break;
|
||||||
case CASH_BASED:
|
case CASH_BASED:
|
||||||
saveSavingsBaseAccountMapping(savingProductId, accountType, command, element);
|
saveSavingsBaseAccountMapping(savingProductId, accountType, command, element);
|
||||||
// Interest Payable is saved on cash too, and optionally: interest posting routes
|
|
||||||
// through it on either basis (see AccountingProcessorHelper
|
|
||||||
// .createInterestPostingJournalEntriesForSavings), so that interest declared but not
|
|
||||||
// yet credited to the member reads as the bank's own liability rather than appearing
|
|
||||||
// only at the moment it is paid. Mapped only when the screen supplies it - a product
|
|
||||||
// naming none still posts straight to Savings Control, as it always did.
|
|
||||||
if (this.fromApiJsonHelper.parameterExists(SavingProductAccountingParams.INTEREST_PAYABLE.getValue(),
|
|
||||||
element)) {
|
|
||||||
this.savingsProductToGLAccountMappingHelper.saveSavingsToLiabilityAccountMapping(element,
|
|
||||||
SavingProductAccountingParams.INTEREST_PAYABLE.getValue(), savingProductId,
|
|
||||||
AccrualAccountsForSavings.INTEREST_PAYABLE.getValue());
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case ACCRUAL_PERIODIC:
|
case ACCRUAL_PERIODIC:
|
||||||
|
|||||||
+2
-3
@@ -366,7 +366,7 @@ public class IncomeProfitTransferService {
|
|||||||
return jdbcTemplate.queryForObject("SELECT id FROM m_office ORDER BY hierarchy LIMIT 1", Long.class);
|
return jdbcTemplate.queryForObject("SELECT id FROM m_office ORDER BY hierarchy LIMIT 1", Long.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String resolveCurrency(final Long glAccountId) {
|
private String resolveCurrency(final Long glAccountId) {
|
||||||
try {
|
try {
|
||||||
final String code = jdbcTemplate.queryForObject(
|
final String code = jdbcTemplate.queryForObject(
|
||||||
"SELECT currency_code FROM acc_gl_journal_entry WHERE account_id = ? ORDER BY id DESC LIMIT 1", String.class,
|
"SELECT currency_code FROM acc_gl_journal_entry WHERE account_id = ? ORDER BY id DESC LIMIT 1", String.class,
|
||||||
@@ -377,8 +377,7 @@ public class IncomeProfitTransferService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Writes one journal entry leg. Shared with the profit appropriation posting so both year-end steps post alike. */
|
private void insertEntry(final Long officeId, final Long glAccountId, final int typeEnum, final BigDecimal amount, final String txnId,
|
||||||
public void insertEntry(final Long officeId, final Long glAccountId, final int typeEnum, final BigDecimal amount, final String txnId,
|
|
||||||
final LocalDate entryDate, final Long userId, final LocalDateTime now, final String currencyCode, final String description) {
|
final LocalDate entryDate, final Long userId, final LocalDateTime now, final String currencyCode, final String description) {
|
||||||
GLAccountTransactionGuard.assertPostable(jdbcTemplate, glAccountId);
|
GLAccountTransactionGuard.assertPostable(jdbcTemplate, glAccountId);
|
||||||
jdbcTemplate.update(
|
jdbcTemplate.update(
|
||||||
|
|||||||
-3
@@ -35,7 +35,6 @@ import org.apache.fineract.cob.data.OldestCOBProcessedLoanDTO;
|
|||||||
import org.apache.fineract.cob.service.LoanCOBCatchUpService;
|
import org.apache.fineract.cob.service.LoanCOBCatchUpService;
|
||||||
import org.apache.fineract.infrastructure.core.exception.JobIsNotFoundOrNotEnabledException;
|
import org.apache.fineract.infrastructure.core.exception.JobIsNotFoundOrNotEnabledException;
|
||||||
import org.apache.fineract.infrastructure.jobs.service.JobName;
|
import org.apache.fineract.infrastructure.jobs.service.JobName;
|
||||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
@Path("/v1/loans")
|
@Path("/v1/loans")
|
||||||
@@ -45,7 +44,6 @@ import org.springframework.stereotype.Component;
|
|||||||
public class LoanCOBCatchUpApiResource {
|
public class LoanCOBCatchUpApiResource {
|
||||||
|
|
||||||
private final Optional<LoanCOBCatchUpService> loanCOBCatchUpServiceOp;
|
private final Optional<LoanCOBCatchUpService> loanCOBCatchUpServiceOp;
|
||||||
private final PlatformSecurityContext context;
|
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("oldest-cob-closed")
|
@Path("oldest-cob-closed")
|
||||||
@@ -66,7 +64,6 @@ public class LoanCOBCatchUpApiResource {
|
|||||||
@ApiResponse(responseCode = "202", description = "Catch Up has been started")
|
@ApiResponse(responseCode = "202", description = "Catch Up has been started")
|
||||||
@ApiResponse(responseCode = "400", description = "Catch Up is already running")
|
@ApiResponse(responseCode = "400", description = "Catch Up is already running")
|
||||||
public Response executeLoanCOBCatchUp() {
|
public Response executeLoanCOBCatchUp() {
|
||||||
this.context.authenticatedUser().validateHasPermissionTo("EXECUTE_LOANCOBCATCHUP");
|
|
||||||
return loanCOBCatchUpServiceOp.map(loanCOBCatchUpService -> {
|
return loanCOBCatchUpServiceOp.map(loanCOBCatchUpService -> {
|
||||||
if (loanCOBCatchUpService.isCatchUpRunning().isCatchUpRunning()) {
|
if (loanCOBCatchUpService.isCatchUpRunning().isCatchUpRunning()) {
|
||||||
return Response.status(Response.Status.BAD_REQUEST).build();
|
return Response.status(Response.Status.BAD_REQUEST).build();
|
||||||
|
|||||||
-10
@@ -191,16 +191,6 @@ public class ConfigurationDomainServiceJpa implements ConfigurationDomainService
|
|||||||
return property.isEnabled();
|
return property.isEnabled();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Read on every run of the interest posting job and of the day close, so the two paths can never
|
|
||||||
* both pay the same deposit: with this on the job leaves fixed deposits alone and they are paid
|
|
||||||
* from an approved report, with it off the job pays them directly as it always did.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public boolean isFixedDepositInterestPostingApprovalEnabled() {
|
|
||||||
return getGlobalConfigurationPropertyData(GlobalConfigurationConstants.FD_INTEREST_POSTING_APPROVAL).isEnabled();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Integer retrieveFinancialYearBeginningMonth() {
|
public Integer retrieveFinancialYearBeginningMonth() {
|
||||||
final GlobalConfigurationPropertyData property = getGlobalConfigurationPropertyData(
|
final GlobalConfigurationPropertyData property = getGlobalConfigurationPropertyData(
|
||||||
|
|||||||
+8
-60
@@ -165,11 +165,9 @@ public class SecurityConfig {
|
|||||||
// envelope's RSA signature/encryption (NpciCryptoUtil) inside the handler.
|
// envelope's RSA signature/encryption (NpciCryptoUtil) inside the handler.
|
||||||
.requestMatchers(API_MATCHER.matcher(HttpMethod.POST, "/api/*/npci/**")).permitAll()
|
.requestMatchers(API_MATCHER.matcher(HttpMethod.POST, "/api/*/npci/**")).permitAll()
|
||||||
// Client account summary, third-party callers — a partner integration calls this
|
// Client account summary, third-party callers — a partner integration calls this
|
||||||
// with an X-Api-Key header instead of a Fineract username/password. The key itself
|
// with an X-Api-Key header instead of a Fineract username/password (issued via
|
||||||
// is issued by inserting directly into m_api_client in the tenant-store database
|
// POST /v1/apiclients, super-user only). Same posture as IMPS/NPCI above: the
|
||||||
// (see ApiClientAdminApiResource — its POST /v1/apiclients is deliberately disabled,
|
// outer gate stays open for this one narrow GET path, and
|
||||||
// since any bank's own super user reaching it could mint a platform-wide key). Same
|
|
||||||
// posture as IMPS/NPCI above: the outer gate stays open for this one narrow GET path, and
|
|
||||||
// ClientAllAccountsApiResource#retrieveByClientNo is what actually authenticates
|
// ClientAllAccountsApiResource#retrieveByClientNo is what actually authenticates
|
||||||
// the caller (ApiClientCredentialService checks the key's hash, active flag and
|
// the caller (ApiClientCredentialService checks the key's hash, active flag and
|
||||||
// IP allowlist) — a request with no key and no Fineract login still gets nothing,
|
// IP allowlist) — a request with no key and no Fineract login still gets nothing,
|
||||||
@@ -224,10 +222,7 @@ public class SecurityConfig {
|
|||||||
// currency
|
// currency
|
||||||
.requestMatchers(API_MATCHER.matcher(HttpMethod.GET, "/api/*/currencies"))
|
.requestMatchers(API_MATCHER.matcher(HttpMethod.GET, "/api/*/currencies"))
|
||||||
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_READ, "READ_CURRENCY")
|
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_READ, "READ_CURRENCY")
|
||||||
// the actual currency-update endpoint is PUT (CurrenciesApiResource has no @POST method) -
|
.requestMatchers(API_MATCHER.matcher(HttpMethod.POST, "/api/*/currencies"))
|
||||||
// this matcher previously targeted POST, which meant PUT fell through to the generic
|
|
||||||
// fullyAuthenticated() catch-all with no permission check at all.
|
|
||||||
.requestMatchers(API_MATCHER.matcher(HttpMethod.PUT, "/api/*/currencies"))
|
|
||||||
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "UPDATE_CURRENCY")
|
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "UPDATE_CURRENCY")
|
||||||
// notes: read
|
// notes: read
|
||||||
.requestMatchers(API_MATCHER.matcher(HttpMethod.GET, "/api/*/clients/*/notes"))
|
.requestMatchers(API_MATCHER.matcher(HttpMethod.GET, "/api/*/clients/*/notes"))
|
||||||
@@ -238,11 +233,6 @@ public class SecurityConfig {
|
|||||||
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_READ, "READ_LOANTRANSACTIONNOTE")
|
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_READ, "READ_LOANTRANSACTIONNOTE")
|
||||||
.requestMatchers(API_MATCHER.matcher(HttpMethod.GET, "/api/*/savings/*/notes"))
|
.requestMatchers(API_MATCHER.matcher(HttpMethod.GET, "/api/*/savings/*/notes"))
|
||||||
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_READ, "READ_SAVINGNOTE")
|
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_READ, "READ_SAVINGNOTE")
|
||||||
// current accounts are deposit accounts: their notes use the savings-note permissions
|
|
||||||
.requestMatchers(API_MATCHER.matcher(HttpMethod.GET, "/api/*/currentaccounts/*/notes"))
|
|
||||||
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_READ, "READ_SAVINGNOTE")
|
|
||||||
.requestMatchers(API_MATCHER.matcher(HttpMethod.GET, "/api/*/currentaccounts/*/notes/*"))
|
|
||||||
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_READ, "READ_SAVINGNOTE")
|
|
||||||
.requestMatchers(API_MATCHER.matcher(HttpMethod.GET, "/api/*/groups/*/notes"))
|
.requestMatchers(API_MATCHER.matcher(HttpMethod.GET, "/api/*/groups/*/notes"))
|
||||||
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_READ, "READ_GROUPNOTE")
|
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_READ, "READ_GROUPNOTE")
|
||||||
// notes: create
|
// notes: create
|
||||||
@@ -254,8 +244,6 @@ public class SecurityConfig {
|
|||||||
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "CREATE_LOANTRANSACTIONNOTE")
|
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "CREATE_LOANTRANSACTIONNOTE")
|
||||||
.requestMatchers(API_MATCHER.matcher(HttpMethod.POST, "/api/*/savings/*/notes"))
|
.requestMatchers(API_MATCHER.matcher(HttpMethod.POST, "/api/*/savings/*/notes"))
|
||||||
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "CREATE_SAVINGNOTE")
|
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "CREATE_SAVINGNOTE")
|
||||||
.requestMatchers(API_MATCHER.matcher(HttpMethod.POST, "/api/*/currentaccounts/*/notes"))
|
|
||||||
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "CREATE_SAVINGNOTE")
|
|
||||||
.requestMatchers(API_MATCHER.matcher(HttpMethod.POST, "/api/*/groups/*/notes"))
|
.requestMatchers(API_MATCHER.matcher(HttpMethod.POST, "/api/*/groups/*/notes"))
|
||||||
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "CREATE_GROUPNOTE")
|
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "CREATE_GROUPNOTE")
|
||||||
// notes: update
|
// notes: update
|
||||||
@@ -267,8 +255,6 @@ public class SecurityConfig {
|
|||||||
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "UPDATE_LOANTRANSACTIONNOTE")
|
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "UPDATE_LOANTRANSACTIONNOTE")
|
||||||
.requestMatchers(API_MATCHER.matcher(HttpMethod.PUT, "/api/*/savings/*/notes"))
|
.requestMatchers(API_MATCHER.matcher(HttpMethod.PUT, "/api/*/savings/*/notes"))
|
||||||
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "UPDATE_SAVINGNOTE")
|
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "UPDATE_SAVINGNOTE")
|
||||||
.requestMatchers(API_MATCHER.matcher(HttpMethod.PUT, "/api/*/currentaccounts/*/notes/*"))
|
|
||||||
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "UPDATE_SAVINGNOTE")
|
|
||||||
.requestMatchers(API_MATCHER.matcher(HttpMethod.PUT, "/api/*/groups/*/notes"))
|
.requestMatchers(API_MATCHER.matcher(HttpMethod.PUT, "/api/*/groups/*/notes"))
|
||||||
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "UPDATE_GROUPNOTE")
|
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "UPDATE_GROUPNOTE")
|
||||||
// notes: delete
|
// notes: delete
|
||||||
@@ -280,8 +266,6 @@ public class SecurityConfig {
|
|||||||
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "DELETE_LOANTRANSACTIONNOTE")
|
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "DELETE_LOANTRANSACTIONNOTE")
|
||||||
.requestMatchers(API_MATCHER.matcher(HttpMethod.DELETE, "/api/*/savings/*/notes"))
|
.requestMatchers(API_MATCHER.matcher(HttpMethod.DELETE, "/api/*/savings/*/notes"))
|
||||||
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "DELETE_SAVINGNOTE")
|
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "DELETE_SAVINGNOTE")
|
||||||
.requestMatchers(API_MATCHER.matcher(HttpMethod.DELETE, "/api/*/currentaccounts/*/notes/*"))
|
|
||||||
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "DELETE_SAVINGNOTE")
|
|
||||||
.requestMatchers(API_MATCHER.matcher(HttpMethod.DELETE, "/api/*/groups/*/notes"))
|
.requestMatchers(API_MATCHER.matcher(HttpMethod.DELETE, "/api/*/groups/*/notes"))
|
||||||
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "DELETE_GROUPNOTE")
|
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "DELETE_GROUPNOTE")
|
||||||
// document: clients
|
// document: clients
|
||||||
@@ -371,18 +355,11 @@ public class SecurityConfig {
|
|||||||
.requestMatchers(API_MATCHER.matcher(HttpMethod.GET, "/api/*/import/*"))
|
.requestMatchers(API_MATCHER.matcher(HttpMethod.GET, "/api/*/import/*"))
|
||||||
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_READ, "READ_IMPORT")
|
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_READ, "READ_IMPORT")
|
||||||
// payment type
|
// payment type
|
||||||
// NOTE: POST previously required "UPDATE_PAYMENTTYPE" (wrong authority for create), with a
|
|
||||||
// second, unreachable matcher for the same POST pattern requiring "DELETE_PAYMENTTYPE" - and
|
|
||||||
// no PUT/DELETE matcher existed at all, so those requests fell through to the generic
|
|
||||||
// fullyAuthenticated() catch-all below with no permission check. Corrected to one matcher
|
|
||||||
// per method, each requiring its own correct permission code.
|
|
||||||
.requestMatchers(API_MATCHER.matcher(HttpMethod.GET, "/api/*/paymenttypes/*"))
|
.requestMatchers(API_MATCHER.matcher(HttpMethod.GET, "/api/*/paymenttypes/*"))
|
||||||
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_READ, "READ_PAYMENTTYPE")
|
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_READ, "READ_PAYMENTTYPE")
|
||||||
.requestMatchers(API_MATCHER.matcher(HttpMethod.POST, "/api/*/paymenttypes"))
|
.requestMatchers(API_MATCHER.matcher(HttpMethod.POST, "/api/*/paymenttypes"))
|
||||||
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "CREATE_PAYMENTTYPE")
|
|
||||||
.requestMatchers(API_MATCHER.matcher(HttpMethod.PUT, "/api/*/paymenttypes/*"))
|
|
||||||
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "UPDATE_PAYMENTTYPE")
|
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "UPDATE_PAYMENTTYPE")
|
||||||
.requestMatchers(API_MATCHER.matcher(HttpMethod.DELETE, "/api/*/paymenttypes/*"))
|
.requestMatchers(API_MATCHER.matcher(HttpMethod.POST, "/api/*/paymenttypes"))
|
||||||
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "DELETE_PAYMENTTYPE")
|
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "DELETE_PAYMENTTYPE")
|
||||||
// mix: taxonomy
|
// mix: taxonomy
|
||||||
.requestMatchers(API_MATCHER.matcher(HttpMethod.GET, "/api/*/mixtaxonomy/*"))
|
.requestMatchers(API_MATCHER.matcher(HttpMethod.GET, "/api/*/mixtaxonomy/*"))
|
||||||
@@ -413,41 +390,12 @@ public class SecurityConfig {
|
|||||||
.requestMatchers(API_MATCHER.matcher(HttpMethod.DELETE, "/api/*/mixreport/*"))
|
.requestMatchers(API_MATCHER.matcher(HttpMethod.DELETE, "/api/*/mixreport/*"))
|
||||||
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "DELETE_MIX_REPORT")
|
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "DELETE_MIX_REPORT")
|
||||||
// working days
|
// working days
|
||||||
// NOTE: previously "READ_WORKING_DAYS"/"UPDATE_WORKING_DAYS" (with an underscore between
|
|
||||||
// WORKING and DAYS), which did not match the actual seeded permission codes
|
|
||||||
// (READ_WORKINGDAYS / UPDATE_WORKINGDAYS) - corrected to the real codes.
|
|
||||||
.requestMatchers(API_MATCHER.matcher(HttpMethod.GET, "/api/*/workingdays"))
|
.requestMatchers(API_MATCHER.matcher(HttpMethod.GET, "/api/*/workingdays"))
|
||||||
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_READ, "READ_WORKINGDAYS")
|
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_READ, "READ_WORKING_DAYS")
|
||||||
.requestMatchers(API_MATCHER.matcher(HttpMethod.GET, "/api/*/workingdays/template"))
|
.requestMatchers(API_MATCHER.matcher(HttpMethod.GET, "/api/*/workingdays/template"))
|
||||||
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_READ, "READ_WORKINGDAYS")
|
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_READ, "READ_WORKING_DAYS")
|
||||||
.requestMatchers(API_MATCHER.matcher(HttpMethod.PUT, "/api/*/workingdays"))
|
.requestMatchers(API_MATCHER.matcher(HttpMethod.PUT, "/api/*/workingdays"))
|
||||||
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "UPDATE_WORKINGDAYS")
|
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "UPDATE_WORKING_DAYS")
|
||||||
// sponsor banks
|
|
||||||
// NOTE: no matcher previously existed at all for this path - POST/PUT both fell
|
|
||||||
// through to the generic fullyAuthenticated() catch-all below with no permission
|
|
||||||
// check whatsoever.
|
|
||||||
.requestMatchers(API_MATCHER.matcher(HttpMethod.POST, "/api/*/sponsorbanks"))
|
|
||||||
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "CREATE_SPONSORBANK")
|
|
||||||
.requestMatchers(API_MATCHER.matcher(HttpMethod.PUT, "/api/*/sponsorbanks/*"))
|
|
||||||
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "UPDATE_SPONSORBANK")
|
|
||||||
// sponsor DD GST/GL settings
|
|
||||||
// NOTE: no matcher previously existed at all for this path either. gl-mapping is a
|
|
||||||
// genuine upsert (one call both creates and updates the single settings row), so it is
|
|
||||||
// covered by one UPDATE_SPONSORDDGLMAPPING matcher rather than a separate CREATE one.
|
|
||||||
.requestMatchers(API_MATCHER.matcher(HttpMethod.POST, "/api/*/sponsordd/gl-mapping"))
|
|
||||||
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "UPDATE_SPONSORDDGLMAPPING")
|
|
||||||
// bank profile (institution profile)
|
|
||||||
// NOTE: no matcher previously existed at all for this path - the PUT relied solely on
|
|
||||||
// the in-resource validateHasPermissionTo() check, which this change replaces with the
|
|
||||||
// command-source pipeline. Added here for the same defense-in-depth already applied to
|
|
||||||
// Currency/Payment Type/Working Days.
|
|
||||||
.requestMatchers(API_MATCHER.matcher(HttpMethod.PUT, "/api/*/institutionprofile"))
|
|
||||||
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "UPDATE_INSTITUTIONPROFILE")
|
|
||||||
// branch profile
|
|
||||||
// NOTE: no matcher previously existed at all for this path either, same rationale as
|
|
||||||
// institution profile above.
|
|
||||||
.requestMatchers(API_MATCHER.matcher(HttpMethod.PUT, "/api/*/branchprofile/*"))
|
|
||||||
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "UPDATE_BRANCHPROFILE")
|
|
||||||
|
|
||||||
.requestMatchers(API_MATCHER.matcher(HttpMethod.POST, "/api/*/twofactor/validate")).fullyAuthenticated()
|
.requestMatchers(API_MATCHER.matcher(HttpMethod.POST, "/api/*/twofactor/validate")).fullyAuthenticated()
|
||||||
.requestMatchers(API_MATCHER.matcher("/api/*/twofactor")).fullyAuthenticated()
|
.requestMatchers(API_MATCHER.matcher("/api/*/twofactor")).fullyAuthenticated()
|
||||||
|
|||||||
-6
@@ -68,7 +68,6 @@ public class TenantDatabaseUpgradeService implements InitializingBean {
|
|||||||
private final ExtendedSpringLiquibaseFactory liquibaseFactory;
|
private final ExtendedSpringLiquibaseFactory liquibaseFactory;
|
||||||
private final TenantDataSourceFactory tenantDataSourceFactory;
|
private final TenantDataSourceFactory tenantDataSourceFactory;
|
||||||
private final Environment environment;
|
private final Environment environment;
|
||||||
private final TenantInitialUserProvisioner initialUserProvisioner;
|
|
||||||
|
|
||||||
// DO NOT REMOVE! Required for liquibase custom task initialization
|
// DO NOT REMOVE! Required for liquibase custom task initialization
|
||||||
private final List<CustomTaskChange> customTaskChangesForDependencyInjection;
|
private final List<CustomTaskChange> customTaskChangesForDependencyInjection;
|
||||||
@@ -196,15 +195,10 @@ public class TenantDatabaseUpgradeService implements InitializingBean {
|
|||||||
// configuration
|
// configuration
|
||||||
// settings, and we should not use them to control the script order
|
// settings, and we should not use them to control the script order
|
||||||
if (databaseStateVerifier.isFirstLiquibaseMigration(tenantDataSource)) {
|
if (databaseStateVerifier.isFirstLiquibaseMigration(tenantDataSource)) {
|
||||||
// a Flyway-era database already has its real users; only a brand-new tenant gets the seeded ones
|
|
||||||
final boolean brandNewTenant = !databaseStateVerifier.isFlywayPresent(tenantDataSource);
|
|
||||||
ExtendedSpringLiquibase liquibase = liquibaseFactory.create(tenantDataSource, TENANT_DB_CONTEXT,
|
ExtendedSpringLiquibase liquibase = liquibaseFactory.create(tenantDataSource, TENANT_DB_CONTEXT,
|
||||||
CUSTOM_CHANGELOG_CONTEXT, INITIAL_SWITCH_CONTEXT, tenant.getTenantIdentifier());
|
CUSTOM_CHANGELOG_CONTEXT, INITIAL_SWITCH_CONTEXT, tenant.getTenantIdentifier());
|
||||||
applyInitialLiquibase(tenantDataSource, liquibase, tenant.getTenantIdentifier(),
|
applyInitialLiquibase(tenantDataSource, liquibase, tenant.getTenantIdentifier(),
|
||||||
(ds) -> !databaseStateVerifier.isTenantOnLatestUpgradableVersion(ds));
|
(ds) -> !databaseStateVerifier.isTenantOnLatestUpgradableVersion(ds));
|
||||||
if (brandNewTenant) {
|
|
||||||
initialUserProvisioner.provision(tenantDataSource, tenant.getTenantIdentifier());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
SpringLiquibase tenantLiquibase = liquibaseFactory.create(tenantDataSource, TENANT_DB_CONTEXT, CUSTOM_CHANGELOG_CONTEXT,
|
SpringLiquibase tenantLiquibase = liquibaseFactory.create(tenantDataSource, TENANT_DB_CONTEXT, CUSTOM_CHANGELOG_CONTEXT,
|
||||||
tenant.getTenantIdentifier());
|
tenant.getTenantIdentifier());
|
||||||
|
|||||||
-90
@@ -1,90 +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.infrastructure.core.service.migration;
|
|
||||||
|
|
||||||
import java.util.Locale;
|
|
||||||
import javax.sql.DataSource;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.apache.fineract.useradministration.service.AppUserConstants;
|
|
||||||
import org.springframework.core.env.Environment;
|
|
||||||
import org.springframework.jdbc.core.JdbcTemplate;
|
|
||||||
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
|
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Personalises the three users that the initial Liquibase data seeds into every brand-new tenant database (ids 1-3,
|
|
||||||
* which otherwise ship with the well-known {@code mifos} / {@code system} / {@code interopUser} usernames and the
|
|
||||||
* public default password).
|
|
||||||
*
|
|
||||||
* Runs only right after the very first migration of a tenant that was never on Flyway, so existing tenants are never
|
|
||||||
* touched. The seeded changeSet itself is deliberately left alone: editing it would change its Liquibase checksum and
|
|
||||||
* break validation on every tenant that already ran it.
|
|
||||||
*
|
|
||||||
* Usernames become {@code <tenantIdentifier>_admin} and {@code <tenantIdentifier>_interop}. The system user keeps the
|
|
||||||
* {@link AppUserConstants#SYSTEM_USER_NAME} username because scheduler jobs, COB and other background processes look it
|
|
||||||
* up by that name; only its password is changed. All three share one password: the value of
|
|
||||||
* {@code fineract.tenant.initial-user-password} (env {@code FINERACT_TENANT_INITIAL_USER_PASSWORD}) when set,
|
|
||||||
* otherwise {@link #DEFAULT_PASSWORD}.
|
|
||||||
*/
|
|
||||||
@Component
|
|
||||||
@Slf4j
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class TenantInitialUserProvisioner {
|
|
||||||
|
|
||||||
static final String PASSWORD_PROPERTY = "fineract.tenant.initial-user-password";
|
|
||||||
|
|
||||||
private static final long ADMIN_ID = 1L;
|
|
||||||
private static final long SYSTEM_ID = 2L;
|
|
||||||
private static final long INTEROP_ID = 3L;
|
|
||||||
// Meets the "strong" password policy (upper, lower, digit, special, no space, under 50 chars).
|
|
||||||
static final String DEFAULT_PASSWORD = "Cbs@Nexora#2026!Secure";
|
|
||||||
|
|
||||||
private final Environment environment;
|
|
||||||
|
|
||||||
public void provision(final DataSource dataSource, final String tenantIdentifier) {
|
|
||||||
final JdbcTemplate jdbc = new JdbcTemplate(dataSource);
|
|
||||||
final String prefix = usernamePrefix(tenantIdentifier);
|
|
||||||
|
|
||||||
final String configured = environment.getProperty(PASSWORD_PROPERTY);
|
|
||||||
final boolean overridden = configured != null && !configured.isBlank();
|
|
||||||
final String password = overridden ? configured : DEFAULT_PASSWORD;
|
|
||||||
final PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
|
|
||||||
final String encoded = encoder.encode(password);
|
|
||||||
|
|
||||||
update(jdbc, ADMIN_ID, prefix + "_admin", encoded);
|
|
||||||
update(jdbc, SYSTEM_ID, AppUserConstants.SYSTEM_USER_NAME, encoded);
|
|
||||||
update(jdbc, INTEROP_ID, prefix + "_interop", encoded);
|
|
||||||
|
|
||||||
log.warn("Tenant '{}' initial users created: {}_admin, {}, {}_interop (password: {}). Change it after first login.",
|
|
||||||
tenantIdentifier, prefix, AppUserConstants.SYSTEM_USER_NAME, prefix, overridden ? "from " + PASSWORD_PROPERTY : "built-in default");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void update(final JdbcTemplate jdbc, final long id, final String username, final String encodedPassword) {
|
|
||||||
jdbc.update("UPDATE m_appuser SET username = ?, password = ?, last_time_password_updated = CURRENT_DATE WHERE id = ?", username,
|
|
||||||
encodedPassword, id);
|
|
||||||
}
|
|
||||||
|
|
||||||
static String usernamePrefix(final String tenantIdentifier) {
|
|
||||||
final String cleaned = tenantIdentifier.toLowerCase(Locale.ROOT).replaceAll("[^a-z0-9]+", "_").replaceAll("^_+|_+$", "");
|
|
||||||
return cleaned.isEmpty() ? "tenant" : cleaned;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
+14
-20
@@ -20,6 +20,7 @@ package org.apache.fineract.infrastructure.security.apiclient;
|
|||||||
|
|
||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
import com.google.gson.GsonBuilder;
|
import com.google.gson.GsonBuilder;
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import jakarta.ws.rs.Consumes;
|
import jakarta.ws.rs.Consumes;
|
||||||
import jakarta.ws.rs.DELETE;
|
import jakarta.ws.rs.DELETE;
|
||||||
@@ -34,26 +35,17 @@ import jakarta.ws.rs.core.Response;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
||||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||||
import org.apache.fineract.useradministration.domain.AppUser;
|
import org.apache.fineract.useradministration.domain.AppUser;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Admin-only management of the third-party API keys {@link ApiClientCredentialService} authenticates.
|
* Admin-only management of the third-party API keys {@link ApiClientCredentialService} authenticates.
|
||||||
* List/deactivate/reactivate still require a normal Fineract login restricted to a super user
|
* This resource itself requires a normal Fineract login, same as everything else -- ordinary
|
||||||
* (ALL_FUNCTIONS) -- but minting a NEW key ({@link #create}) is disabled here entirely, on purpose.
|
* username/password, restricted to a super user (ALL_FUNCTIONS). Only the endpoints that use a minted
|
||||||
*
|
* key to skip login (e.g. GET /v1/clientaccounts) are the ones exempted in SecurityConfig; issuing that
|
||||||
* {@code ALL_FUNCTIONS} is a per-TENANT role grant -- and {@code TenantMenuAccessApiResource}'s whole
|
* key in the first place is not something to hand to just anyone.
|
||||||
* point is that a bank's own super user (or a role simply misconfigured to carry ALL_FUNCTIONS, as the
|
|
||||||
* "Clerk" role was found to in two tenants this session) must never be able to lift its own menu-access
|
|
||||||
* ceiling. If this endpoint minted keys for any ALL_FUNCTIONS holder, that same bank super user could
|
|
||||||
* call {@code POST /v1/apiclients} and hand themselves a brand-new platform-operator credential -- one
|
|
||||||
* that authenticates against every tenant, not just their own -- completely bypassing the ceiling this
|
|
||||||
* key mechanism exists to enforce. A super user being able to read/deactivate the client list is fine
|
|
||||||
* (visibility and revocation are not privilege escalation); being able to mint a fresh one is exactly
|
|
||||||
* the escalation path being closed. New keys are issued the same way this fix issued the first one: a
|
|
||||||
* direct insert into {@code m_api_client} (see {@link ApiClientCredentialService#createClient}), by
|
|
||||||
* whoever already has direct database access -- never over HTTP.
|
|
||||||
*/
|
*/
|
||||||
@Path("/v1/apiclients")
|
@Path("/v1/apiclients")
|
||||||
@Component
|
@Component
|
||||||
@@ -63,18 +55,20 @@ public class ApiClientAdminApiResource {
|
|||||||
|
|
||||||
private final PlatformSecurityContext context;
|
private final PlatformSecurityContext context;
|
||||||
private final ApiClientCredentialService credentialService;
|
private final ApiClientCredentialService credentialService;
|
||||||
|
private final FromJsonHelper fromJsonHelper;
|
||||||
private final Gson gson = new GsonBuilder().serializeNulls().create();
|
private final Gson gson = new GsonBuilder().serializeNulls().create();
|
||||||
|
|
||||||
/** Disabled -- see the class comment. Minting a key is a direct-database operation only. */
|
|
||||||
@POST
|
@POST
|
||||||
@Consumes(MediaType.APPLICATION_JSON)
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
public String create(final String body) {
|
public String create(final String body) {
|
||||||
requireSuperUser();
|
final AppUser user = requireSuperUser();
|
||||||
throw new WebApplicationException(Response.status(403)
|
final JsonElement root = fromJsonHelper.parse(body);
|
||||||
.entity("{\"message\":\"Minting a new platform API key over HTTP is disabled. "
|
final String clientName = fromJsonHelper.extractStringNamed("clientName", root);
|
||||||
+ "Insert directly into m_api_client in the tenant-store database instead.\"}")
|
final String allowedIps = fromJsonHelper.extractStringNamed("allowedIps", root);
|
||||||
.type(MediaType.APPLICATION_JSON).build());
|
|
||||||
|
final Map<String, Object> result = credentialService.createClient(clientName, allowedIps, user.getId());
|
||||||
|
return gson.toJson(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
|
|||||||
+1
-1
@@ -92,7 +92,7 @@ public class TenantMenuAccessApiResource {
|
|||||||
"MENU_CASHIER", "MENU_CLERK", "MENU_BRANCH_OPS", "MENU_CLEARING", "MENU_DEPOSITS", "MENU_ADVANCES",
|
"MENU_CASHIER", "MENU_CLERK", "MENU_BRANCH_OPS", "MENU_CLEARING", "MENU_DEPOSITS", "MENU_ADVANCES",
|
||||||
"MENU_LOAN_ORIGINATION", "MENU_SHARE", "MENU_NPA", "MENU_GENERAL_LEDGER", "MENU_SUB_LEDGER", "MENU_ACCOUNTING",
|
"MENU_LOAN_ORIGINATION", "MENU_SHARE", "MENU_NPA", "MENU_GENERAL_LEDGER", "MENU_SUB_LEDGER", "MENU_ACCOUNTING",
|
||||||
"MENU_CKYC_MGMT", "MENU_INSURANCE", "MENU_DIGITAL", "MENU_HRM", "MENU_ADMIN", "MENU_RBI", "MENU_INVESTMENT",
|
"MENU_CKYC_MGMT", "MENU_INSURANCE", "MENU_DIGITAL", "MENU_HRM", "MENU_ADMIN", "MENU_RBI", "MENU_INVESTMENT",
|
||||||
"MENU_REPORTS", "MENU_OTHERS", "MENU_CONFIG_WIZARD", "MENU_CHEQUEBOOK", "MENU_LETTER_MGMT", "MENU_CREDIT_BUREAU");
|
"MENU_REPORTS", "MENU_OTHERS", "MENU_CONFIG_WIZARD", "MENU_CHEQUEBOOK", "MENU_LETTER_MGMT");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This tenant's own menu-access list, for the tenant named in {@code Fineract-Platform-TenantId}.
|
* This tenant's own menu-access list, for the tenant named in {@code Fineract-Platform-TenantId}.
|
||||||
|
|||||||
+1
-6
@@ -130,12 +130,7 @@ public class TellerManagementReadPlatformServiceImpl implements TellerManagement
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Collection<TellerData> getTellers(Long officeId) {
|
public Collection<TellerData> getTellers(Long officeId) {
|
||||||
if (officeId == null) {
|
return retrieveAllTellers(false);
|
||||||
return retrieveAllTellers(false);
|
|
||||||
}
|
|
||||||
// The tellers of that one branch. The officeId used to be ignored, so the head office, whose
|
|
||||||
// hierarchy covers every branch, counted each branch's vault and cashiers as its own at day close.
|
|
||||||
return retrieveAllTellers(false).stream().filter(t -> officeId.equals(t.getOfficeId())).toList();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
+4
-6
@@ -70,6 +70,10 @@ public class TellerWritePlatformServiceJpaImpl implements TellerWritePlatformSer
|
|||||||
try {
|
try {
|
||||||
this.context.authenticatedUser();
|
this.context.authenticatedUser();
|
||||||
|
|
||||||
|
if (this.tellerRepositoryWrapper.existsAny()) {
|
||||||
|
throw new TellerAlreadyExistsException();
|
||||||
|
}
|
||||||
|
|
||||||
final Long officeId = command.longValueOfParameterNamed("officeId");
|
final Long officeId = command.longValueOfParameterNamed("officeId");
|
||||||
|
|
||||||
this.fromApiJsonDeserializer.validateForCreateAndUpdateTeller(command.json());
|
this.fromApiJsonDeserializer.validateForCreateAndUpdateTeller(command.json());
|
||||||
@@ -77,12 +81,6 @@ public class TellerWritePlatformServiceJpaImpl implements TellerWritePlatformSer
|
|||||||
// final Office parent =
|
// final Office parent =
|
||||||
// validateUserPriviledgeOnOfficeAndRetrieve(currentUser, officeId);
|
// validateUserPriviledgeOnOfficeAndRetrieve(currentUser, officeId);
|
||||||
final Office tellerOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(officeId);
|
final Office tellerOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(officeId);
|
||||||
|
|
||||||
// One teller (vault) per branch, not one for the whole bank: each branch holds and settles
|
|
||||||
// its own cash, so a second branch needs a vault of its own.
|
|
||||||
if (this.tellerRepositoryWrapper.existsInOffice(officeId)) {
|
|
||||||
throw new TellerAlreadyExistsException(tellerOffice.getName());
|
|
||||||
}
|
|
||||||
final Teller teller = Teller.fromJson(tellerOffice, command);
|
final Teller teller = Teller.fromJson(tellerOffice, command);
|
||||||
|
|
||||||
// pre save to generate id for use in office hierarchy
|
// pre save to generate id for use in office hierarchy
|
||||||
|
|||||||
+15
-12
@@ -27,15 +27,15 @@ 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.time.Instant;
|
||||||
|
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.workingdays.command.WorkingDaysUpdateCommand;
|
||||||
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.workingdays.data.WorkingDaysData;
|
import org.apache.fineract.organisation.workingdays.data.WorkingDaysData;
|
||||||
import org.apache.fineract.organisation.workingdays.data.WorkingDaysUpdateRequest;
|
import org.apache.fineract.organisation.workingdays.data.WorkingDaysUpdateRequest;
|
||||||
import org.apache.fineract.organisation.workingdays.data.WorkingDaysUpdateRequestValidator;
|
import org.apache.fineract.organisation.workingdays.data.WorkingDaysUpdateRequestValidator;
|
||||||
|
import org.apache.fineract.organisation.workingdays.data.WorkingDaysUpdateResponse;
|
||||||
import org.apache.fineract.organisation.workingdays.service.WorkingDaysReadPlatformService;
|
import org.apache.fineract.organisation.workingdays.service.WorkingDaysReadPlatformService;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
@@ -50,8 +50,7 @@ public class WorkingDaysApiResource {
|
|||||||
|
|
||||||
private final WorkingDaysReadPlatformService workingDaysReadPlatformService;
|
private final WorkingDaysReadPlatformService workingDaysReadPlatformService;
|
||||||
private final WorkingDaysUpdateRequestValidator workingDaysUpdateRequestValidator;
|
private final WorkingDaysUpdateRequestValidator workingDaysUpdateRequestValidator;
|
||||||
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
|
private final CommandPipeline commandPipeline;
|
||||||
private final DefaultToApiJsonSerializer<WorkingDaysUpdateRequest> toApiJsonSerializer;
|
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Consumes({ MediaType.APPLICATION_JSON })
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
@@ -66,13 +65,17 @@ public class WorkingDaysApiResource {
|
|||||||
@Produces({ MediaType.APPLICATION_JSON })
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
@Operation(summary = "Update a Working Day", description = "Mandatory Fields\n"
|
@Operation(summary = "Update a Working Day", description = "Mandatory Fields\n"
|
||||||
+ "recurrence,repaymentRescheduleType,extendTermForDailyRepayments,locale")
|
+ "recurrence,repaymentRescheduleType,extendTermForDailyRepayments,locale")
|
||||||
public String update(@Valid final WorkingDaysUpdateRequest request) {
|
public WorkingDaysUpdateResponse update(@Valid WorkingDaysUpdateRequest request) {
|
||||||
final CommandWrapper commandRequest = new CommandWrapperBuilder().updateWorkingDays()
|
|
||||||
.withJson(toApiJsonSerializer.serialize(request)).build();
|
|
||||||
|
|
||||||
final CommandProcessingResult result = commandsSourceWritePlatformService.logCommandSource(commandRequest);
|
final var command = new WorkingDaysUpdateCommand();
|
||||||
|
|
||||||
return toApiJsonSerializer.serialize(result);
|
command.setCommandId(System.currentTimeMillis());
|
||||||
|
command.setCreatedAt(Instant.now());
|
||||||
|
command.setPayload(request);
|
||||||
|
|
||||||
|
final Supplier<WorkingDaysUpdateResponse> response = commandPipeline.send(command);
|
||||||
|
|
||||||
|
return response.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
|
|||||||
+21
-29
@@ -18,46 +18,38 @@
|
|||||||
*/
|
*/
|
||||||
package org.apache.fineract.organisation.workingdays.handler;
|
package org.apache.fineract.organisation.workingdays.handler;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import org.apache.fineract.commands.annotation.CommandType;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
import org.apache.fineract.command.core.Command;
|
||||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
import org.apache.fineract.command.core.CommandHandler;
|
||||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
|
|
||||||
import org.apache.fineract.organisation.workingdays.data.WorkingDaysUpdateRequest;
|
import org.apache.fineract.organisation.workingdays.data.WorkingDaysUpdateRequest;
|
||||||
|
import org.apache.fineract.organisation.workingdays.data.WorkingDaysUpdateResponse;
|
||||||
import org.apache.fineract.organisation.workingdays.service.WorkingDaysWritePlatformService;
|
import org.apache.fineract.organisation.workingdays.service.WorkingDaysWritePlatformService;
|
||||||
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 = "WORKINGDAYS", action = "UPDATE")
|
@Component
|
||||||
public class UpdateWorkingDaysCommandHandler implements NewCommandSourceHandler {
|
@RequiredArgsConstructor
|
||||||
|
public class UpdateWorkingDaysCommandHandler implements CommandHandler<WorkingDaysUpdateRequest, WorkingDaysUpdateResponse> {
|
||||||
|
|
||||||
private final WorkingDaysWritePlatformService workingDaysWritePlatformService;
|
private final WorkingDaysWritePlatformService workingDaysWritePlatformService;
|
||||||
|
|
||||||
@Autowired
|
|
||||||
public UpdateWorkingDaysCommandHandler(final WorkingDaysWritePlatformService workingDaysWritePlatformService) {
|
|
||||||
this.workingDaysWritePlatformService = workingDaysWritePlatformService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
@Override
|
@Override
|
||||||
public CommandProcessingResult processCommand(final JsonCommand command) {
|
public WorkingDaysUpdateResponse handle(Command<WorkingDaysUpdateRequest> command) {
|
||||||
|
WorkingDaysUpdateRequest request = command.getPayload();
|
||||||
|
Map<String, Object> changes = this.workingDaysWritePlatformService.updateWorkingDays(request);
|
||||||
|
if (changes == null) {
|
||||||
|
changes = Collections.emptyMap();
|
||||||
|
}
|
||||||
|
|
||||||
final WorkingDaysUpdateRequest request = WorkingDaysUpdateRequest.builder()
|
return WorkingDaysUpdateResponse.builder().resourceId((Long) changes.get("resourceId")).changes(changes)
|
||||||
.recurrence(command.stringValueOfParameterNamed("recurrence"))
|
.recurrence(request.getRecurrence()).repaymentRescheduleType(request.getRepaymentRescheduleType())
|
||||||
.repaymentRescheduleType(command.integerValueSansLocaleOfParameterNamed("repaymentRescheduleType"))
|
.extendTermForDailyRepayments(request.getExtendTermForDailyRepayments())
|
||||||
.extendTermForDailyRepayments(command.booleanObjectValueOfParameterNamed("extendTermForDailyRepayments"))
|
.extendTermForRepaymentsOnHolidays(request.getExtendTermForRepaymentsOnHolidays()).build();
|
||||||
.extendTermForRepaymentsOnHolidays(command.booleanObjectValueOfParameterNamed("extendTermForRepaymentsOnHolidays"))
|
|
||||||
.build();
|
|
||||||
|
|
||||||
final Map<String, Object> changes = this.workingDaysWritePlatformService.updateWorkingDays(request);
|
|
||||||
|
|
||||||
return new CommandProcessingResultBuilder() //
|
|
||||||
.withCommandId(command.commandId()) //
|
|
||||||
.with(changes) //
|
|
||||||
.build();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
-152
@@ -1,152 +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.account.service;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.sql.PreparedStatement;
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.time.OffsetDateTime;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.UUID;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
|
|
||||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
|
||||||
import org.apache.fineract.portfolio.client.service.KycRestrictionGuard;
|
|
||||||
import org.springframework.jdbc.core.JdbcTemplate;
|
|
||||||
import org.springframework.jdbc.support.GeneratedKeyHolder;
|
|
||||||
import org.springframework.jdbc.support.KeyHolder;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A book debit: money leaves a savings or current account by book entry, not as cash.
|
|
||||||
*
|
|
||||||
* <p>
|
|
||||||
* Posts NO journal entry. The caller posts the one entry that describes the real movement (a DD, a collection...).
|
|
||||||
* Debiting through a cash withdrawal instead made the withdrawal post its own Dr control / Cr Cash-at-Vault entry --
|
|
||||||
* cash that never moved -- which callers then hid by flagging it reversed, leaving an entry in the ledger that no
|
|
||||||
* report shows.
|
|
||||||
*
|
|
||||||
* <p>
|
|
||||||
* It keeps the rules a withdrawal applies: the account must be active, the amount must fit in the available balance
|
|
||||||
* (balance less funds on hold), and a dormant account or a lapsed re-KYC stops the debit. Joins the caller's
|
|
||||||
* transaction, so a later failure rolls the debit back with everything else.
|
|
||||||
*/
|
|
||||||
@Service
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class AccountBookDebitService {
|
|
||||||
|
|
||||||
private final JdbcTemplate jdbcTemplate;
|
|
||||||
private final KycRestrictionGuard kycRestrictionGuard;
|
|
||||||
|
|
||||||
/** Debits a savings account; returns the m_savings_account_transaction id. */
|
|
||||||
public long debitSavings(final Long accountId, final BigDecimal amount, final Long officeId, final LocalDate txnDate,
|
|
||||||
final Long userId) {
|
|
||||||
final BigDecimal balance = checkedBalance("m_savings_account", "Savings", accountId, amount);
|
|
||||||
kycRestrictionGuard.check(accountId, KycRestrictionGuard.Operation.DEBIT);
|
|
||||||
|
|
||||||
final java.sql.Date sqlDate = java.sql.Date.valueOf(txnDate);
|
|
||||||
final OffsetDateTime now = OffsetDateTime.now(DateUtils.getDateTimeZoneOfTenant());
|
|
||||||
final KeyHolder kh = new GeneratedKeyHolder();
|
|
||||||
jdbcTemplate.update(con -> {
|
|
||||||
final PreparedStatement ps = con.prepareStatement("INSERT INTO m_savings_account_transaction "
|
|
||||||
+ "(savings_account_id, office_id, payment_detail_id, transaction_type_enum, is_reversed, transaction_date, amount, "
|
|
||||||
+ "submitted_on_date, ref_no, running_balance_derived, is_lien_transaction, created_by, last_modified_by, "
|
|
||||||
+ "created_on_utc, last_modified_on_utc) VALUES (?, ?, NULL, 2, false, ?, ?, ?, ?, ?, false, ?, ?, ?, ?)",
|
|
||||||
new String[] { "id" });
|
|
||||||
ps.setLong(1, accountId);
|
|
||||||
ps.setLong(2, officeId);
|
|
||||||
ps.setDate(3, sqlDate);
|
|
||||||
ps.setBigDecimal(4, amount);
|
|
||||||
ps.setDate(5, sqlDate);
|
|
||||||
ps.setString(6, refNo());
|
|
||||||
ps.setBigDecimal(7, balance.subtract(amount));
|
|
||||||
ps.setLong(8, userId);
|
|
||||||
ps.setLong(9, userId);
|
|
||||||
ps.setObject(10, now);
|
|
||||||
ps.setObject(11, now);
|
|
||||||
return ps;
|
|
||||||
}, kh);
|
|
||||||
jdbcTemplate.update("UPDATE m_savings_account SET account_balance_derived = account_balance_derived - ?, "
|
|
||||||
+ "total_withdrawals_derived = COALESCE(total_withdrawals_derived, 0) + ? WHERE id = ?", amount, amount, accountId);
|
|
||||||
return Objects.requireNonNull(kh.getKey()).longValue();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Debits a current account; returns the m_current_account_transaction id. */
|
|
||||||
public long debitCurrent(final Long accountId, final BigDecimal amount, final Long officeId, final LocalDate txnDate,
|
|
||||||
final Long userId, final String note) {
|
|
||||||
final BigDecimal balance = checkedBalance("m_current_account", "Current", accountId, amount);
|
|
||||||
kycRestrictionGuard.checkCurrentAccount(accountId, KycRestrictionGuard.Operation.DEBIT);
|
|
||||||
|
|
||||||
final java.sql.Date sqlDate = java.sql.Date.valueOf(txnDate);
|
|
||||||
final String remarks = note == null || note.isBlank() ? "Book debit" : (note.length() > 490 ? note.substring(0, 490) : note);
|
|
||||||
final KeyHolder kh = new GeneratedKeyHolder();
|
|
||||||
jdbcTemplate.update(con -> {
|
|
||||||
final PreparedStatement ps = con.prepareStatement("INSERT INTO m_current_account_transaction "
|
|
||||||
+ "(current_account_id, office_id, transaction_type_enum, transaction_date, amount, is_reversed, is_reversal, "
|
|
||||||
+ "is_manual, is_lien_transaction, ref_no, submitted_on_date, running_balance_derived, remarks, created_date, created_by) "
|
|
||||||
+ "VALUES (?, ?, 2, ?, ?, false, false, false, false, ?, ?, ?, ?, CURRENT_TIMESTAMP, ?)", new String[] { "id" });
|
|
||||||
ps.setLong(1, accountId);
|
|
||||||
ps.setLong(2, officeId);
|
|
||||||
ps.setDate(3, sqlDate);
|
|
||||||
ps.setBigDecimal(4, amount);
|
|
||||||
ps.setString(5, refNo());
|
|
||||||
ps.setDate(6, sqlDate);
|
|
||||||
ps.setBigDecimal(7, balance.subtract(amount));
|
|
||||||
ps.setString(8, remarks);
|
|
||||||
ps.setLong(9, userId);
|
|
||||||
return ps;
|
|
||||||
}, kh);
|
|
||||||
jdbcTemplate.update("UPDATE m_current_account SET account_balance_derived = account_balance_derived - ?, "
|
|
||||||
+ "total_withdrawals_derived = COALESCE(total_withdrawals_derived, 0) + ? WHERE id = ?", amount, amount, accountId);
|
|
||||||
return Objects.requireNonNull(kh.getKey()).longValue();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The account's balance, after checking it is active and can cover {@code amount} from what is not on hold. */
|
|
||||||
private BigDecimal checkedBalance(final String table, final String kind, final Long accountId, final BigDecimal amount) {
|
|
||||||
final List<Map<String, Object>> rows = jdbcTemplate.queryForList(
|
|
||||||
"SELECT status_enum, account_balance_derived, on_hold_funds_derived FROM " + table + " WHERE id = ?", accountId);
|
|
||||||
if (rows.isEmpty()) {
|
|
||||||
throw new GeneralPlatformDomainRuleException("error.msg.account.not.found", kind + " account " + accountId + " not found.",
|
|
||||||
accountId);
|
|
||||||
}
|
|
||||||
final int status = ((Number) rows.get(0).get("status_enum")).intValue();
|
|
||||||
if (status != 300) {
|
|
||||||
throw new GeneralPlatformDomainRuleException("error.msg.account.not.active",
|
|
||||||
kind + " account " + accountId + " is not active.", accountId);
|
|
||||||
}
|
|
||||||
final BigDecimal balance = rows.get(0).get("account_balance_derived") == null ? BigDecimal.ZERO
|
|
||||||
: (BigDecimal) rows.get(0).get("account_balance_derived");
|
|
||||||
final BigDecimal onHold = rows.get(0).get("on_hold_funds_derived") == null ? BigDecimal.ZERO
|
|
||||||
: (BigDecimal) rows.get(0).get("on_hold_funds_derived");
|
|
||||||
final BigDecimal available = balance.subtract(onHold);
|
|
||||||
if (available.compareTo(amount) < 0) {
|
|
||||||
throw new GeneralPlatformDomainRuleException("error.msg.account.insufficient.balance",
|
|
||||||
"Insufficient available balance in " + kind.toLowerCase() + " account " + accountId + ". Available: " + available
|
|
||||||
+ ", Requested: " + amount,
|
|
||||||
accountId);
|
|
||||||
}
|
|
||||||
return balance;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String refNo() {
|
|
||||||
return UUID.randomUUID().toString().replace("-", "").substring(0, 20);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+21
-60
@@ -27,7 +27,6 @@ import static org.apache.fineract.portfolio.account.api.AccountTransfersApiConst
|
|||||||
|
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.math.MathContext;
|
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -45,7 +44,6 @@ import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
|||||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
|
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
|
||||||
import org.apache.fineract.infrastructure.core.domain.ExternalId;
|
import org.apache.fineract.infrastructure.core.domain.ExternalId;
|
||||||
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
|
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
|
||||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
|
||||||
import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
|
import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
|
||||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||||
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
|
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
|
||||||
@@ -59,7 +57,6 @@ import org.apache.fineract.portfolio.account.domain.AccountTransferRepository;
|
|||||||
import org.apache.fineract.portfolio.account.domain.AccountTransferTransaction;
|
import org.apache.fineract.portfolio.account.domain.AccountTransferTransaction;
|
||||||
import org.apache.fineract.portfolio.account.domain.AccountTransferType;
|
import org.apache.fineract.portfolio.account.domain.AccountTransferType;
|
||||||
import org.apache.fineract.portfolio.account.exception.DifferentCurrenciesException;
|
import org.apache.fineract.portfolio.account.exception.DifferentCurrenciesException;
|
||||||
import org.apache.fineract.portfolio.client.service.KycRestrictionGuard;
|
|
||||||
import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccount;
|
import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccount;
|
||||||
import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountRepository;
|
import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountRepository;
|
||||||
import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountTransaction;
|
import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountTransaction;
|
||||||
@@ -76,13 +73,11 @@ import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail;
|
|||||||
import org.apache.fineract.portfolio.savings.SavingsTransactionBooleanValues;
|
import org.apache.fineract.portfolio.savings.SavingsTransactionBooleanValues;
|
||||||
import org.apache.fineract.portfolio.savings.domain.GSIMRepositoy;
|
import org.apache.fineract.portfolio.savings.domain.GSIMRepositoy;
|
||||||
import org.apache.fineract.portfolio.savings.domain.GroupSavingsIndividualMonitoring;
|
import org.apache.fineract.portfolio.savings.domain.GroupSavingsIndividualMonitoring;
|
||||||
import org.apache.fineract.portfolio.savings.domain.RecurringDepositAccount;
|
|
||||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
|
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
|
||||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccountAssembler;
|
import org.apache.fineract.portfolio.savings.domain.SavingsAccountAssembler;
|
||||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccountTransaction;
|
import org.apache.fineract.portfolio.savings.domain.SavingsAccountTransaction;
|
||||||
import org.apache.fineract.portfolio.savings.service.SavingsAccountDomainService;
|
import org.apache.fineract.portfolio.savings.service.SavingsAccountDomainService;
|
||||||
import org.apache.fineract.portfolio.savings.service.SavingsAccountWritePlatformService;
|
import org.apache.fineract.portfolio.savings.service.SavingsAccountWritePlatformService;
|
||||||
import org.apache.fineract.portfolio.tds.service.SavingsInterestTdsService;
|
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@@ -105,8 +100,6 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
|
|||||||
private final CurrentAccountRepository currentAccountRepository;
|
private final CurrentAccountRepository currentAccountRepository;
|
||||||
private final CurrentAccountTransactionRepository currentAccountTransactionRepository;
|
private final CurrentAccountTransactionRepository currentAccountTransactionRepository;
|
||||||
private final AccountingProcessorHelper accountingProcessorHelper;
|
private final AccountingProcessorHelper accountingProcessorHelper;
|
||||||
private final KycRestrictionGuard kycRestrictionGuard;
|
|
||||||
private final SavingsInterestTdsService savingsInterestTdsService;
|
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
@Override
|
@Override
|
||||||
@@ -152,7 +145,6 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
|
|||||||
|
|
||||||
final SavingsAccountTransaction deposit = this.savingsAccountDomainService.handleDeposit(toSavingsAccount, fmt, transactionDate,
|
final SavingsAccountTransaction deposit = this.savingsAccountDomainService.handleDeposit(toSavingsAccount, fmt, transactionDate,
|
||||||
transactionAmount, paymentDetail, isAccountTransfer, isRegularTransaction, backdatedTxnsAllowedTill);
|
transactionAmount, paymentDetail, isAccountTransfer, isRegularTransaction, backdatedTxnsAllowedTill);
|
||||||
applyToRecurringDepositSchedule(toSavingsAccount, deposit);
|
|
||||||
|
|
||||||
if (!fromSavingsAccount.getCurrency().getCode().equals(toSavingsAccount.getCurrency().getCode())) {
|
if (!fromSavingsAccount.getCurrency().getCode().equals(toSavingsAccount.getCurrency().getCode())) {
|
||||||
throw new DifferentCurrenciesException(fromSavingsAccount.getCurrency().getCode(),
|
throw new DifferentCurrenciesException(fromSavingsAccount.getCurrency().getCode(),
|
||||||
@@ -208,7 +200,6 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
|
|||||||
|
|
||||||
final SavingsAccountTransaction deposit = this.savingsAccountDomainService.handleDeposit(toSavingsAccount, fmt, transactionDate,
|
final SavingsAccountTransaction deposit = this.savingsAccountDomainService.handleDeposit(toSavingsAccount, fmt, transactionDate,
|
||||||
transactionAmount, paymentDetail, isAccountTransfer, isRegularTransaction, backdatedTxnsAllowedTill);
|
transactionAmount, paymentDetail, isAccountTransfer, isRegularTransaction, backdatedTxnsAllowedTill);
|
||||||
applyToRecurringDepositSchedule(toSavingsAccount, deposit);
|
|
||||||
|
|
||||||
final AccountTransferDetails accountTransferDetails = this.accountTransferAssembler.assembleLoanToSavingsTransfer(command,
|
final AccountTransferDetails accountTransferDetails = this.accountTransferAssembler.assembleLoanToSavingsTransfer(command,
|
||||||
fromLoanAccount, toSavingsAccount, deposit, loanRefundTransaction);
|
fromLoanAccount, toSavingsAccount, deposit, loanRefundTransaction);
|
||||||
@@ -414,15 +405,11 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
|
|||||||
final SavingsAccountTransaction deposit = this.savingsAccountDomainService.handleDeposit(toSavingsAccount,
|
final SavingsAccountTransaction deposit = this.savingsAccountDomainService.handleDeposit(toSavingsAccount,
|
||||||
accountTransferDTO.getFmt(), transactionDate, accountTransferDTO.getTransactionAmount(),
|
accountTransferDTO.getFmt(), transactionDate, accountTransferDTO.getTransactionAmount(),
|
||||||
accountTransferDTO.getPaymentDetail(), isAccountTransfer, isRegularTransaction, backdatedTxnsAllowedTill);
|
accountTransferDTO.getPaymentDetail(), isAccountTransfer, isRegularTransaction, backdatedTxnsAllowedTill);
|
||||||
applyToRecurringDepositSchedule(toSavingsAccount, deposit);
|
|
||||||
|
|
||||||
accountTransferDetails = this.accountTransferAssembler.assembleSavingsToSavingsTransfer(accountTransferDTO, fromSavingsAccount,
|
accountTransferDetails = this.accountTransferAssembler.assembleSavingsToSavingsTransfer(accountTransferDTO, fromSavingsAccount,
|
||||||
toSavingsAccount, withdrawal, deposit);
|
toSavingsAccount, withdrawal, deposit);
|
||||||
this.accountTransferDetailRepository.saveAndFlush(accountTransferDetails);
|
this.accountTransferDetailRepository.saveAndFlush(accountTransferDetails);
|
||||||
transferTransactionId = accountTransferDetails.getId();
|
transferTransactionId = accountTransferDetails.getId();
|
||||||
// FD/RD interest reaching a savings account is where TDS is tracked and, once due, deducted.
|
|
||||||
this.savingsInterestTdsService.onTransfer(fromSavingsAccount, toSavingsAccount, deposit,
|
|
||||||
AccountTransferType.fromInt(accountTransferDTO.getTransferType()));
|
|
||||||
|
|
||||||
} else if (isLoanToSavingsAccountTransfer(accountTransferDTO.getFromAccountType(), accountTransferDTO.getToAccountType())) {
|
} else if (isLoanToSavingsAccountTransfer(accountTransferDTO.getFromAccountType(), accountTransferDTO.getToAccountType())) {
|
||||||
|
|
||||||
@@ -460,7 +447,6 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
|
|||||||
final SavingsAccountTransaction deposit = this.savingsAccountDomainService.handleDeposit(toSavingsAccount,
|
final SavingsAccountTransaction deposit = this.savingsAccountDomainService.handleDeposit(toSavingsAccount,
|
||||||
accountTransferDTO.getFmt(), accountTransferDTO.getTransactionDate(), accountTransferDTO.getTransactionAmount(),
|
accountTransferDTO.getFmt(), accountTransferDTO.getTransactionDate(), accountTransferDTO.getTransactionAmount(),
|
||||||
accountTransferDTO.getPaymentDetail(), isAccountTransfer, isRegularTransaction, backdatedTxnsAllowedTill);
|
accountTransferDTO.getPaymentDetail(), isAccountTransfer, isRegularTransaction, backdatedTxnsAllowedTill);
|
||||||
applyToRecurringDepositSchedule(toSavingsAccount, deposit);
|
|
||||||
accountTransferDetails = this.accountTransferAssembler.assembleLoanToSavingsTransfer(accountTransferDTO, fromLoanAccount,
|
accountTransferDetails = this.accountTransferAssembler.assembleLoanToSavingsTransfer(accountTransferDTO, fromLoanAccount,
|
||||||
toSavingsAccount, deposit, loanTransaction);
|
toSavingsAccount, deposit, loanTransaction);
|
||||||
this.accountTransferDetailRepository.saveAndFlush(accountTransferDetails);
|
this.accountTransferDetailRepository.saveAndFlush(accountTransferDetails);
|
||||||
@@ -486,7 +472,7 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
|
|||||||
|
|
||||||
final CurrentAccountTransaction withdrawal = performCurrentWithdrawal(fromCurrentAccount,
|
final CurrentAccountTransaction withdrawal = performCurrentWithdrawal(fromCurrentAccount,
|
||||||
accountTransferDTO.getTransactionAmount(), accountTransferDTO.getTransactionDate(),
|
accountTransferDTO.getTransactionAmount(), accountTransferDTO.getTransactionDate(),
|
||||||
toSavingsAccount.productId(), false, toSavingsAccount.getId());
|
toSavingsAccount.productId(), toSavingsAccount.getId());
|
||||||
|
|
||||||
// skipAccountingPostings=true: current account withdrawal leg (C-txn) already posts
|
// skipAccountingPostings=true: current account withdrawal leg (C-txn) already posts
|
||||||
// DR CURRENT ACCOUNT / CR SAVING INDIVIDUAL directly — each with its own account number.
|
// DR CURRENT ACCOUNT / CR SAVING INDIVIDUAL directly — each with its own account number.
|
||||||
@@ -495,7 +481,6 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
|
|||||||
accountTransferDTO.getFmt(), accountTransferDTO.getTransactionDate(),
|
accountTransferDTO.getFmt(), accountTransferDTO.getTransactionDate(),
|
||||||
accountTransferDTO.getTransactionAmount(), accountTransferDTO.getPaymentDetail(),
|
accountTransferDTO.getTransactionAmount(), accountTransferDTO.getPaymentDetail(),
|
||||||
isAccountTransfer, isRegularTransaction, backdatedTxnsAllowedTill, true);
|
isAccountTransfer, isRegularTransaction, backdatedTxnsAllowedTill, true);
|
||||||
applyToRecurringDepositSchedule(toSavingsAccount, deposit);
|
|
||||||
|
|
||||||
if (accountTransferDetails == null) {
|
if (accountTransferDetails == null) {
|
||||||
accountTransferDetails = AccountTransferDetails.currentToSavingsTransfer(
|
accountTransferDetails = AccountTransferDetails.currentToSavingsTransfer(
|
||||||
@@ -532,7 +517,7 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
|
|||||||
|
|
||||||
performCurrentDeposit(toCurrentAccount, accountTransferDTO.getTransactionAmount(),
|
performCurrentDeposit(toCurrentAccount, accountTransferDTO.getTransactionAmount(),
|
||||||
accountTransferDTO.getTransactionDate(),
|
accountTransferDTO.getTransactionDate(),
|
||||||
fromSavingsAccount.productId(), false, fromSavingsAccount.getId());
|
fromSavingsAccount.productId(), fromSavingsAccount.getId());
|
||||||
|
|
||||||
if (accountTransferDetails == null) {
|
if (accountTransferDetails == null) {
|
||||||
accountTransferDetails = AccountTransferDetails.savingsToCurrentTransfer(
|
accountTransferDetails = AccountTransferDetails.savingsToCurrentTransfer(
|
||||||
@@ -560,10 +545,10 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
|
|||||||
|
|
||||||
performCurrentWithdrawal(fromCurrentAccount, accountTransferDTO.getTransactionAmount(),
|
performCurrentWithdrawal(fromCurrentAccount, accountTransferDTO.getTransactionAmount(),
|
||||||
accountTransferDTO.getTransactionDate(),
|
accountTransferDTO.getTransactionDate(),
|
||||||
toCurrentAccount.getProductId(), true, toCurrentAccount.getId());
|
toCurrentAccount.getProductId(), toCurrentAccount.getId());
|
||||||
performCurrentDeposit(toCurrentAccount, accountTransferDTO.getTransactionAmount(),
|
performCurrentDeposit(toCurrentAccount, accountTransferDTO.getTransactionAmount(),
|
||||||
accountTransferDTO.getTransactionDate(),
|
accountTransferDTO.getTransactionDate(),
|
||||||
fromCurrentAccount.getProductId(), true, fromCurrentAccount.getId());
|
fromCurrentAccount.getProductId(), fromCurrentAccount.getId());
|
||||||
|
|
||||||
if (accountTransferDetails == null) {
|
if (accountTransferDetails == null) {
|
||||||
accountTransferDetails = AccountTransferDetails.currentToCurrentTransfer(
|
accountTransferDetails = AccountTransferDetails.currentToCurrentTransfer(
|
||||||
@@ -657,26 +642,6 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
|
|||||||
return accountTransferDetails;
|
return accountTransferDetails;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Money transferred into a recurring deposit is an installment payment, exactly like a counter deposit through
|
|
||||||
* DepositAccountDomainServiceJpa#handleRDDeposit. The generic deposit above only credits the balance, so without
|
|
||||||
* this the deposit's schedule kept showing the installment as unpaid and overdue, and its projected maturity
|
|
||||||
* amount never moved - for manual transfers, standing instructions and RD Auto Debit alike. Any other destination
|
|
||||||
* is left untouched.
|
|
||||||
*/
|
|
||||||
private void applyToRecurringDepositSchedule(final SavingsAccount toSavingsAccount, final SavingsAccountTransaction deposit) {
|
|
||||||
if (!(toSavingsAccount instanceof RecurringDepositAccount recurringDeposit)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final boolean isPreMatureClosure = false;
|
|
||||||
recurringDeposit.updateDepositAmount(deposit.getAmount());
|
|
||||||
recurringDeposit.handleScheduleInstallments(deposit);
|
|
||||||
recurringDeposit.updateMaturityDateAndAmount(MathContext.DECIMAL64, isPreMatureClosure,
|
|
||||||
this.configurationDomainService.isSavingsInterestPostingAtCurrentPeriodEnd(),
|
|
||||||
this.configurationDomainService.retrieveFinancialYearBeginningMonth());
|
|
||||||
recurringDeposit.updateOverduePayments(DateUtils.getBusinessLocalDate());
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isLoanToSavingsAccountTransfer(final PortfolioAccountType fromAccountType, final PortfolioAccountType toAccountType) {
|
private boolean isLoanToSavingsAccountTransfer(final PortfolioAccountType fromAccountType, final PortfolioAccountType toAccountType) {
|
||||||
return fromAccountType.isLoanAccount() && toAccountType.isSavingsAccount();
|
return fromAccountType.isLoanAccount() && toAccountType.isSavingsAccount();
|
||||||
}
|
}
|
||||||
@@ -716,8 +681,6 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
|
|||||||
*/
|
*/
|
||||||
private CurrentAccountTransaction performCurrentWithdrawalForLoan(final CurrentAccount account, final BigDecimal amount,
|
private CurrentAccountTransaction performCurrentWithdrawalForLoan(final CurrentAccount account, final BigDecimal amount,
|
||||||
final LocalDate txnDate) {
|
final LocalDate txnDate) {
|
||||||
// Money leaving a current account by transfer is refused on a dormant one, as a withdrawal is.
|
|
||||||
kycRestrictionGuard.checkCurrentAccount(account.getId(), KycRestrictionGuard.Operation.TRANSFER);
|
|
||||||
final BigDecimal balance = account.getAccountBalanceDerived() != null ? account.getAccountBalanceDerived() : BigDecimal.ZERO;
|
final BigDecimal balance = account.getAccountBalanceDerived() != null ? account.getAccountBalanceDerived() : BigDecimal.ZERO;
|
||||||
final BigDecimal onHold = account.getOnHoldFundsDerived() != null ? account.getOnHoldFundsDerived() : BigDecimal.ZERO;
|
final BigDecimal onHold = account.getOnHoldFundsDerived() != null ? account.getOnHoldFundsDerived() : BigDecimal.ZERO;
|
||||||
final BigDecimal available = balance.subtract(onHold);
|
final BigDecimal available = balance.subtract(onHold);
|
||||||
@@ -736,9 +699,10 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
|
|||||||
currentAccountRepository.save(account);
|
currentAccountRepository.save(account);
|
||||||
if (account.getProductId() != null && txn.getId() != null) {
|
if (account.getProductId() != null && txn.getId() != null) {
|
||||||
final String currentTxnId = "CA" + txn.getId();
|
final String currentTxnId = "CA" + txn.getId();
|
||||||
accountingProcessorHelper.createCurrentControlToFinancialActivityJournalEntries(account.getOffice(),
|
accountingProcessorHelper.createCashBasedJournalEntriesAndReversalsForSavings(account.getOffice(),
|
||||||
account.getCurrencyCode(), account.getProductId(), FinancialActivity.LIABILITY_TRANSFER.getValue(),
|
account.getCurrencyCode(), CashAccountsForSavings.SAVINGS_CONTROL.getValue(),
|
||||||
account.getId(), currentTxnId, txnDate, amount);
|
FinancialActivity.LIABILITY_TRANSFER.getValue(), account.getProductId(), null,
|
||||||
|
account.getId(), currentTxnId, txnDate, amount, false);
|
||||||
}
|
}
|
||||||
return txn;
|
return txn;
|
||||||
}
|
}
|
||||||
@@ -748,9 +712,7 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
|
|||||||
* Throws {@link GeneralPlatformDomainRuleException} if balance is insufficient.
|
* Throws {@link GeneralPlatformDomainRuleException} if balance is insufficient.
|
||||||
*/
|
*/
|
||||||
private CurrentAccountTransaction performCurrentWithdrawal(final CurrentAccount account, final BigDecimal amount,
|
private CurrentAccountTransaction performCurrentWithdrawal(final CurrentAccount account, final BigDecimal amount,
|
||||||
final LocalDate txnDate, final Long counterpartProductId, final boolean counterpartIsCurrent,
|
final LocalDate txnDate, final Long counterpartProductId, final Long counterpartEntityId) {
|
||||||
final Long counterpartEntityId) {
|
|
||||||
kycRestrictionGuard.checkCurrentAccount(account.getId(), KycRestrictionGuard.Operation.TRANSFER);
|
|
||||||
final BigDecimal balance = account.getAccountBalanceDerived() != null ? account.getAccountBalanceDerived() : BigDecimal.ZERO;
|
final BigDecimal balance = account.getAccountBalanceDerived() != null ? account.getAccountBalanceDerived() : BigDecimal.ZERO;
|
||||||
final BigDecimal onHold = account.getOnHoldFundsDerived() != null ? account.getOnHoldFundsDerived() : BigDecimal.ZERO;
|
final BigDecimal onHold = account.getOnHoldFundsDerived() != null ? account.getOnHoldFundsDerived() : BigDecimal.ZERO;
|
||||||
final BigDecimal available = balance.subtract(onHold);
|
final BigDecimal available = balance.subtract(onHold);
|
||||||
@@ -768,8 +730,7 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
|
|||||||
(account.getTotalWithdrawalsDerived() != null ? account.getTotalWithdrawalsDerived() : BigDecimal.ZERO).add(amount));
|
(account.getTotalWithdrawalsDerived() != null ? account.getTotalWithdrawalsDerived() : BigDecimal.ZERO).add(amount));
|
||||||
currentAccountRepository.save(account);
|
currentAccountRepository.save(account);
|
||||||
// Post journal entries: DR current account GL, CR counterpart GL — each with correct entity/account number
|
// Post journal entries: DR current account GL, CR counterpart GL — each with correct entity/account number
|
||||||
postCurrentAccountTransferJournalEntries(account, txn, amount, txnDate, true, counterpartProductId, counterpartIsCurrent,
|
postCurrentAccountTransferJournalEntries(account, txn, amount, txnDate, true, counterpartProductId, counterpartEntityId);
|
||||||
counterpartEntityId);
|
|
||||||
return txn;
|
return txn;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -777,9 +738,7 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
|
|||||||
* Deposit amount into a current account and save the transaction.
|
* Deposit amount into a current account and save the transaction.
|
||||||
*/
|
*/
|
||||||
private CurrentAccountTransaction performCurrentDeposit(final CurrentAccount account, final BigDecimal amount,
|
private CurrentAccountTransaction performCurrentDeposit(final CurrentAccount account, final BigDecimal amount,
|
||||||
final LocalDate txnDate, final Long counterpartProductId, final boolean counterpartIsCurrent,
|
final LocalDate txnDate, final Long counterpartProductId, final Long counterpartEntityId) {
|
||||||
final Long counterpartEntityId) {
|
|
||||||
kycRestrictionGuard.checkCurrentAccount(account.getId(), KycRestrictionGuard.Operation.CREDIT);
|
|
||||||
final BigDecimal balance = account.getAccountBalanceDerived() != null ? account.getAccountBalanceDerived() : BigDecimal.ZERO;
|
final BigDecimal balance = account.getAccountBalanceDerived() != null ? account.getAccountBalanceDerived() : BigDecimal.ZERO;
|
||||||
final CurrentAccountTransaction txn = CurrentAccountTransaction.deposit(account, account.getOffice(), amount, txnDate, null);
|
final CurrentAccountTransaction txn = CurrentAccountTransaction.deposit(account, account.getOffice(), amount, txnDate, null);
|
||||||
final BigDecimal newBalance = balance.add(amount);
|
final BigDecimal newBalance = balance.add(amount);
|
||||||
@@ -790,8 +749,7 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
|
|||||||
(account.getTotalDepositsDerived() != null ? account.getTotalDepositsDerived() : BigDecimal.ZERO).add(amount));
|
(account.getTotalDepositsDerived() != null ? account.getTotalDepositsDerived() : BigDecimal.ZERO).add(amount));
|
||||||
currentAccountRepository.save(account);
|
currentAccountRepository.save(account);
|
||||||
// Post journal entries: DR counterpart GL, CR current account GL — each with correct entity/account number
|
// Post journal entries: DR counterpart GL, CR current account GL — each with correct entity/account number
|
||||||
postCurrentAccountTransferJournalEntries(account, txn, amount, txnDate, false, counterpartProductId, counterpartIsCurrent,
|
postCurrentAccountTransferJournalEntries(account, txn, amount, txnDate, false, counterpartProductId, counterpartEntityId);
|
||||||
counterpartEntityId);
|
|
||||||
return txn;
|
return txn;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -825,7 +783,7 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
|
|||||||
*/
|
*/
|
||||||
private void postCurrentAccountTransferJournalEntries(final CurrentAccount account, final CurrentAccountTransaction txn,
|
private void postCurrentAccountTransferJournalEntries(final CurrentAccount account, final CurrentAccountTransaction txn,
|
||||||
final BigDecimal amount, final LocalDate txnDate, final boolean isWithdrawal,
|
final BigDecimal amount, final LocalDate txnDate, final boolean isWithdrawal,
|
||||||
final Long counterpartProductId, final boolean counterpartIsCurrent, final Long counterpartEntityId) {
|
final Long counterpartProductId, final Long counterpartEntityId) {
|
||||||
if (account.getProductId() == null || txn.getId() == null) {
|
if (account.getProductId() == null || txn.getId() == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -836,13 +794,17 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
|
|||||||
final String currentTxnId = "CA" + txn.getId();
|
final String currentTxnId = "CA" + txn.getId();
|
||||||
if (isWithdrawal) {
|
if (isWithdrawal) {
|
||||||
// DR: current account GL CR: counterpart (savings/current) GL
|
// DR: current account GL CR: counterpart (savings/current) GL
|
||||||
accountingProcessorHelper.createDirectTransferJournalEntries(account.getOffice(), account.getCurrencyCode(),
|
accountingProcessorHelper.createDirectTransferJournalEntriesForSavings(
|
||||||
account.getProductId(), true, account.getId(), counterpartProductId, counterpartIsCurrent, counterpartEntityId,
|
account.getOffice(), account.getCurrencyCode(),
|
||||||
|
account.getProductId(), account.getId(),
|
||||||
|
counterpartProductId, counterpartEntityId,
|
||||||
currentTxnId, txnDate, amount);
|
currentTxnId, txnDate, amount);
|
||||||
} else {
|
} else {
|
||||||
// DR: counterpart (savings/current) GL CR: current account GL
|
// DR: counterpart (savings/current) GL CR: current account GL
|
||||||
accountingProcessorHelper.createDirectTransferJournalEntries(account.getOffice(), account.getCurrencyCode(),
|
accountingProcessorHelper.createDirectTransferJournalEntriesForSavings(
|
||||||
counterpartProductId, counterpartIsCurrent, counterpartEntityId, account.getProductId(), true, account.getId(),
|
account.getOffice(), account.getCurrencyCode(),
|
||||||
|
counterpartProductId, counterpartEntityId,
|
||||||
|
account.getProductId(), account.getId(),
|
||||||
currentTxnId, txnDate, amount);
|
currentTxnId, txnDate, amount);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -885,7 +847,6 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
|
|||||||
|
|
||||||
final SavingsAccountTransaction deposit = this.savingsAccountDomainService.handleDeposit(toSavingsAccount, fmt, transactionDate,
|
final SavingsAccountTransaction deposit = this.savingsAccountDomainService.handleDeposit(toSavingsAccount, fmt, transactionDate,
|
||||||
transactionAmount, paymentDetail, true, true, backdatedTxnsAllowedTill);
|
transactionAmount, paymentDetail, true, true, backdatedTxnsAllowedTill);
|
||||||
applyToRecurringDepositSchedule(toSavingsAccount, deposit);
|
|
||||||
|
|
||||||
final AccountTransferDetails accountTransferDetails = this.accountTransferAssembler.assembleLoanToSavingsTransfer(command,
|
final AccountTransferDetails accountTransferDetails = this.accountTransferAssembler.assembleLoanToSavingsTransfer(command,
|
||||||
fromLoanAccount, toSavingsAccount, deposit, loanRefundTransaction);
|
fromLoanAccount, toSavingsAccount, deposit, loanRefundTransaction);
|
||||||
|
|||||||
+2
-6
@@ -37,7 +37,6 @@ import org.apache.fineract.portfolio.account.domain.AccountTransferRepository;
|
|||||||
import org.apache.fineract.portfolio.account.domain.StandingInstructionAssembler;
|
import org.apache.fineract.portfolio.account.domain.StandingInstructionAssembler;
|
||||||
import org.apache.fineract.portfolio.account.domain.StandingInstructionRepository;
|
import org.apache.fineract.portfolio.account.domain.StandingInstructionRepository;
|
||||||
import org.apache.fineract.portfolio.account.mapper.AccountTransfersMapper;
|
import org.apache.fineract.portfolio.account.mapper.AccountTransfersMapper;
|
||||||
import org.apache.fineract.portfolio.client.service.KycRestrictionGuard;
|
|
||||||
import org.apache.fineract.portfolio.account.service.AccountAssociationsReadPlatformService;
|
import org.apache.fineract.portfolio.account.service.AccountAssociationsReadPlatformService;
|
||||||
import org.apache.fineract.portfolio.account.service.AccountAssociationsReadPlatformServiceImpl;
|
import org.apache.fineract.portfolio.account.service.AccountAssociationsReadPlatformServiceImpl;
|
||||||
import org.apache.fineract.portfolio.account.service.AccountTransfersReadPlatformService;
|
import org.apache.fineract.portfolio.account.service.AccountTransfersReadPlatformService;
|
||||||
@@ -61,7 +60,6 @@ import org.apache.fineract.portfolio.savings.domain.GSIMRepositoy;
|
|||||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccountAssembler;
|
import org.apache.fineract.portfolio.savings.domain.SavingsAccountAssembler;
|
||||||
import org.apache.fineract.portfolio.savings.service.SavingsAccountDomainService;
|
import org.apache.fineract.portfolio.savings.service.SavingsAccountDomainService;
|
||||||
import org.apache.fineract.portfolio.savings.service.SavingsAccountWritePlatformService;
|
import org.apache.fineract.portfolio.savings.service.SavingsAccountWritePlatformService;
|
||||||
import org.apache.fineract.portfolio.tds.service.SavingsInterestTdsService;
|
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
@@ -98,14 +96,12 @@ public class AccountConfiguration {
|
|||||||
GSIMRepositoy gsimRepository, ConfigurationDomainService configurationDomainService, ExternalIdFactory externalIdFactory,
|
GSIMRepositoy gsimRepository, ConfigurationDomainService configurationDomainService, ExternalIdFactory externalIdFactory,
|
||||||
FineractProperties fineractProperties, CurrentAccountRepository currentAccountRepository,
|
FineractProperties fineractProperties, CurrentAccountRepository currentAccountRepository,
|
||||||
CurrentAccountTransactionRepository currentAccountTransactionRepository,
|
CurrentAccountTransactionRepository currentAccountTransactionRepository,
|
||||||
AccountingProcessorHelper accountingProcessorHelper, KycRestrictionGuard kycRestrictionGuard,
|
AccountingProcessorHelper accountingProcessorHelper) {
|
||||||
SavingsInterestTdsService savingsInterestTdsService) {
|
|
||||||
return new AccountTransfersWritePlatformServiceImpl(accountTransfersDataValidator, accountTransferAssembler,
|
return new AccountTransfersWritePlatformServiceImpl(accountTransfersDataValidator, accountTransferAssembler,
|
||||||
accountTransferRepository, savingsAccountAssembler, savingsAccountDomainService, loanAccountAssembler,
|
accountTransferRepository, savingsAccountAssembler, savingsAccountDomainService, loanAccountAssembler,
|
||||||
loanAccountDomainService, savingsAccountWritePlatformService, accountTransferDetailRepository, loanReadPlatformService,
|
loanAccountDomainService, savingsAccountWritePlatformService, accountTransferDetailRepository, loanReadPlatformService,
|
||||||
gsimRepository, configurationDomainService, externalIdFactory, fineractProperties,
|
gsimRepository, configurationDomainService, externalIdFactory, fineractProperties,
|
||||||
currentAccountRepository, currentAccountTransactionRepository, accountingProcessorHelper, kycRestrictionGuard,
|
currentAccountRepository, currentAccountTransactionRepository, accountingProcessorHelper);
|
||||||
savingsInterestTdsService);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
|
|||||||
+4
-20
@@ -361,15 +361,9 @@ public class AccountDetailsReadPlatformServiceJpaRepositoryImpl implements Accou
|
|||||||
// Renewal-chain identity for Fixed Deposits: the stable customer-facing FD number and
|
// Renewal-chain identity for Fixed Deposits: the stable customer-facing FD number and
|
||||||
// this row's cycle within the chain, so a client's account list can collapse a renewed
|
// this row's cycle within the chain, so a client's account list can collapse a renewed
|
||||||
// FD into one entry rather than listing every term as a separate account.
|
// FD into one entry rather than listing every term as a separate account.
|
||||||
accountsSummary.append("dtp.fd_account_number as fdAccountNumber, dtp.renewal_cycle_no as renewalCycleNo, ");
|
accountsSummary.append("dtp.fd_account_number as fdAccountNumber, dtp.renewal_cycle_no as renewalCycleNo ");
|
||||||
// The branch the account is held at: its own office when opened at another branch, else the client's or group's.
|
|
||||||
accountsSummary.append("acct_office.id as accountOfficeId, acct_office.name as accountOfficeName ");
|
|
||||||
accountsSummary.append("from m_savings_account sa ");
|
accountsSummary.append("from m_savings_account sa ");
|
||||||
accountsSummary.append("join m_savings_product as p on p.id = sa.product_id ");
|
accountsSummary.append("join m_savings_product as p on p.id = sa.product_id ");
|
||||||
accountsSummary.append("left join m_client acct_client on acct_client.id = sa.client_id ");
|
|
||||||
accountsSummary.append("left join m_group acct_group on acct_group.id = sa.group_id ");
|
|
||||||
accountsSummary.append(
|
|
||||||
"left join m_office acct_office on acct_office.id = coalesce(sa.office_id, acct_client.office_id, acct_group.office_id) ");
|
|
||||||
accountsSummary.append("left join m_deposit_account_term_and_preclosure dtp on dtp.savings_account_id = sa.id ");
|
accountsSummary.append("left join m_deposit_account_term_and_preclosure dtp on dtp.savings_account_id = sa.id ");
|
||||||
accountsSummary.append("join m_currency curr on curr.code = sa.currency_code ");
|
accountsSummary.append("join m_currency curr on curr.code = sa.currency_code ");
|
||||||
accountsSummary.append("left join m_appuser sbu on sbu.id = sa.submittedon_userid ");
|
accountsSummary.append("left join m_appuser sbu on sbu.id = sa.submittedon_userid ");
|
||||||
@@ -470,12 +464,9 @@ public class AccountDetailsReadPlatformServiceJpaRepositoryImpl implements Accou
|
|||||||
final String fdAccountNumber = rs.getString("fdAccountNumber");
|
final String fdAccountNumber = rs.getString("fdAccountNumber");
|
||||||
final Integer renewalCycleNo = JdbcSupport.getInteger(rs, "renewalCycleNo");
|
final Integer renewalCycleNo = JdbcSupport.getInteger(rs, "renewalCycleNo");
|
||||||
|
|
||||||
final Long accountOfficeId = JdbcSupport.getLong(rs, "accountOfficeId");
|
|
||||||
final String accountOfficeName = rs.getString("accountOfficeName");
|
|
||||||
|
|
||||||
return new SavingsAccountSummaryData(id, accountNo, externalId, productId, productName, shortProductName, status, currency,
|
return new SavingsAccountSummaryData(id, accountNo, externalId, productId, productName, shortProductName, status, currency,
|
||||||
accountBalance, onHoldFunds, onHoldAmount, availableBalance, accountTypeData, timeline, depositTypeData, subStatus,
|
accountBalance, onHoldFunds, onHoldAmount, availableBalance, accountTypeData, timeline, depositTypeData, subStatus,
|
||||||
lastActiveTransactionDate, fdAccountNumber, renewalCycleNo, accountOfficeId, accountOfficeName);
|
lastActiveTransactionDate, fdAccountNumber, renewalCycleNo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -505,13 +496,9 @@ public class AccountDetailsReadPlatformServiceJpaRepositoryImpl implements Accou
|
|||||||
sb.append("ca.currency_code as currencyCode, ca.currency_digits as currencyDigits, ca.currency_multiplesof as inMultiplesOf, ");
|
sb.append("ca.currency_code as currencyCode, ca.currency_digits as currencyDigits, ca.currency_multiplesof as inMultiplesOf, ");
|
||||||
sb.append("curr.name as currencyName, curr.internationalized_name_code as currencyNameCode, ");
|
sb.append("curr.name as currencyName, curr.internationalized_name_code as currencyNameCode, ");
|
||||||
sb.append("curr.display_symbol as currencyDisplaySymbol, ");
|
sb.append("curr.display_symbol as currencyDisplaySymbol, ");
|
||||||
sb.append("ca.product_id as productId, cp.name as productName, cp.short_name as shortProductName, ");
|
sb.append("ca.product_id as productId, cp.name as productName, cp.short_name as shortProductName ");
|
||||||
// The branch the account is held at: its own office, else the client's.
|
|
||||||
sb.append("acct_office.id as accountOfficeId, acct_office.name as accountOfficeName ");
|
|
||||||
sb.append("from m_current_account ca ");
|
sb.append("from m_current_account ca ");
|
||||||
sb.append("join m_current_product cp on cp.id = ca.product_id ");
|
sb.append("join m_current_product cp on cp.id = ca.product_id ");
|
||||||
sb.append("left join m_client acct_client on acct_client.id = ca.client_id ");
|
|
||||||
sb.append("left join m_office acct_office on acct_office.id = coalesce(ca.office_id, acct_client.office_id) ");
|
|
||||||
sb.append("join m_currency curr on curr.code = ca.currency_code ");
|
sb.append("join m_currency curr on curr.code = ca.currency_code ");
|
||||||
sb.append("left join m_appuser sbu on sbu.id = ca.submittedon_userid ");
|
sb.append("left join m_appuser sbu on sbu.id = ca.submittedon_userid ");
|
||||||
sb.append("left join m_appuser rbu on rbu.id = ca.rejectedon_userid ");
|
sb.append("left join m_appuser rbu on rbu.id = ca.rejectedon_userid ");
|
||||||
@@ -591,13 +578,10 @@ public class AccountDetailsReadPlatformServiceJpaRepositoryImpl implements Accou
|
|||||||
|
|
||||||
final EnumOptionData depositTypeData = SavingsEnumerations.depositType(400);
|
final EnumOptionData depositTypeData = SavingsEnumerations.depositType(400);
|
||||||
|
|
||||||
final Long accountOfficeId = JdbcSupport.getLong(rs, "accountOfficeId");
|
|
||||||
final String accountOfficeName = rs.getString("accountOfficeName");
|
|
||||||
|
|
||||||
// Current accounts have no deposit term, so no FD renewal chain to report.
|
// Current accounts have no deposit term, so no FD renewal chain to report.
|
||||||
return new SavingsAccountSummaryData(id, accountNo, externalId, productId, productName, shortProductName, status, currency,
|
return new SavingsAccountSummaryData(id, accountNo, externalId, productId, productName, shortProductName, status, currency,
|
||||||
accountBalance, onHoldFunds, null, accountBalance, accountTypeData, timeline, depositTypeData, subStatus, activatedOnDate,
|
accountBalance, onHoldFunds, null, accountBalance, accountTypeData, timeline, depositTypeData, subStatus, activatedOnDate,
|
||||||
null, null, accountOfficeId, accountOfficeName);
|
null, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
-672
@@ -1,672 +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.almreturn.api;
|
|
||||||
|
|
||||||
import com.google.gson.Gson;
|
|
||||||
import jakarta.ws.rs.BadRequestException;
|
|
||||||
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.sql.Date;
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.time.temporal.ChronoUnit;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.regex.Matcher;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
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;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The fixed-term part of the Structural Liquidity Statement: what the bank's customers' Fixed
|
|
||||||
* Deposits, Recurring Deposits and term loans add up to in each residual-maturity bucket, counted
|
|
||||||
* from an as-on date.
|
|
||||||
*
|
|
||||||
* <h3>How each figure is slotted</h3>
|
|
||||||
*
|
|
||||||
* <ul>
|
|
||||||
* <li><b>Fixed and Recurring Deposits</b> (outflow): the account balance, on the deposit's maturity
|
|
||||||
* date. RBI slots term deposits by residual maturity; accrued interest is a separate line of the
|
|
||||||
* statement ("Interest Payable"), so it is not added here. A deposit already at or past its
|
|
||||||
* maturity is an overdue liability and goes in the first bucket.</li>
|
|
||||||
* <li><b>Term loans</b> (inflow): each unpaid instalment's principal, on that instalment's due date -
|
|
||||||
* RBI asks for the interim cash flows, not the whole loan in one bucket. An instalment already due
|
|
||||||
* is overdue: one that fell due less than a month ago goes in the "recent" bucket and an older one
|
|
||||||
* in the "old" bucket. Both bucket positions are parameters, because they are the bank's
|
|
||||||
* Overdue Receivables rule and not something this code should decide.</li>
|
|
||||||
* <li><b>Not slotted by date</b>, reported on their own so they are not silently lost: NPA loans (RBI
|
|
||||||
* slots them by classification, not by instalment) and cash-credit / overdraft / demand loans (their
|
|
||||||
* stored schedule is a synthetic equal-instalment one, not a real repayment pattern).</li>
|
|
||||||
* </ul>
|
|
||||||
*
|
|
||||||
* <h3>What this does not cover</h3>
|
|
||||||
*
|
|
||||||
* The bank's own placements and borrowings - FDs with other banks, investments, borrowings - have no
|
|
||||||
* register in the system, only ledger balances, so there is nothing to slot by date. The response
|
|
||||||
* says so rather than leaving them out silently.
|
|
||||||
*
|
|
||||||
* <h3>As-on date</h3>
|
|
||||||
*
|
|
||||||
* Accounts are read in their state today. The as-on date only decides where a maturity falls
|
|
||||||
* relative to it; it does not rebuild what the books looked like on an earlier day.
|
|
||||||
*
|
|
||||||
* <h3>Buckets</h3>
|
|
||||||
*
|
|
||||||
* The bucket edges arrive from the caller, since the bank edits them. {@code buckets=14d,28d,3m,6m,1y,3y,5y}
|
|
||||||
* means the first bucket ends 14 days out, the second 28 days out, the third 3 calendar months out
|
|
||||||
* and so on, with a last bucket that has no end. Months and years are calendar ones.
|
|
||||||
*/
|
|
||||||
@Path("/v1/almreturn/maturity-breakdown")
|
|
||||||
@Component
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
@Produces({ MediaType.APPLICATION_JSON })
|
|
||||||
public class AlmMaturityBreakdownApiResource {
|
|
||||||
|
|
||||||
/** Same audience as the other RBI returns, so no separate permission is introduced for it. */
|
|
||||||
private static final String RESOURCE_NAME_FOR_PERMISSION = "OSSRETURN";
|
|
||||||
|
|
||||||
/** The bucket edges used when the caller names none: RBI's non-scheduled UCB structure. */
|
|
||||||
private static final String DEFAULT_BUCKETS = "14d,28d,3m,6m,1y,3y,5y";
|
|
||||||
|
|
||||||
private static final int FIXED_DEPOSIT = 200;
|
|
||||||
private static final int RECURRING_DEPOSIT = 300;
|
|
||||||
|
|
||||||
/** SavingsAccountStatusType ACTIVE and MATURED: a matured deposit is still a liability until closed or renewed. */
|
|
||||||
private static final int SAVINGS_ACTIVE = 300;
|
|
||||||
private static final int SAVINGS_MATURED = 800;
|
|
||||||
|
|
||||||
/** LoanStatus.ACTIVE. */
|
|
||||||
private static final int LOAN_ACTIVE = 300;
|
|
||||||
|
|
||||||
private static final Pattern EDGE = Pattern.compile("^(\\d{1,4})([dmy])$");
|
|
||||||
|
|
||||||
private final JdbcTemplate jdbcTemplate;
|
|
||||||
private final PlatformSecurityContext context;
|
|
||||||
private final Gson gson = new Gson();
|
|
||||||
|
|
||||||
@GET
|
|
||||||
public String retrieve(@QueryParam("asOnDate") final String asOnDateParam, @QueryParam("officeId") final Long officeId,
|
|
||||||
@QueryParam("buckets") final String bucketsParam, @QueryParam("overdueRecentBucket") final Integer overdueRecentBucket,
|
|
||||||
@QueryParam("overdueOldBucket") final Integer overdueOldBucket) {
|
|
||||||
this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSION);
|
|
||||||
|
|
||||||
final LocalDate asOn = parseDate(asOnDateParam);
|
|
||||||
final List<LocalDate> limits = parseLimits(asOn, bucketsParam == null || bucketsParam.isBlank() ? DEFAULT_BUCKETS : bucketsParam);
|
|
||||||
final int bucketCount = limits.size() + 1;
|
|
||||||
final int recent = clampBucket(overdueRecentBucket, 0, bucketCount);
|
|
||||||
final int old = clampBucket(overdueOldBucket, 2, bucketCount);
|
|
||||||
final String hierarchy = scopeHierarchy(officeId) + "%";
|
|
||||||
|
|
||||||
final List<Map<String, Object>> groups = new ArrayList<>();
|
|
||||||
groups.add(depositGroup("FD", "Fixed Deposits", FIXED_DEPOSIT, asOn, limits, hierarchy));
|
|
||||||
groups.add(depositGroup("RD", "Recurring Deposits", RECURRING_DEPOSIT, asOn, limits, hierarchy));
|
|
||||||
groups.add(termLoanGroup(asOn, limits, hierarchy, recent, old));
|
|
||||||
|
|
||||||
final Map<String, Object> response = new LinkedHashMap<>();
|
|
||||||
response.put("asOnDate", asOn.toString());
|
|
||||||
response.put("officeId", officeId);
|
|
||||||
response.put("bucketCount", bucketCount);
|
|
||||||
response.put("bucketEnds", limits.stream().map(LocalDate::toString).toList());
|
|
||||||
response.put("groups", groups);
|
|
||||||
response.put("notSlotted", List.of(npaLoans(hierarchy), revolvingLoans(hierarchy)));
|
|
||||||
response.put("notes", List.of(
|
|
||||||
"Deposits are slotted by their balance on the maturity date; accrued interest belongs to the Interest Payable line.",
|
|
||||||
"Term loans are slotted instalment by instalment (principal only); accrued interest belongs to the Interest Receivable line.",
|
|
||||||
"Accounts are read in their state today; the as-on date only decides where a maturity falls.",
|
|
||||||
"The bank's own FDs with other banks, investments and borrowings have no register, so they are not part of this breakdown."));
|
|
||||||
return this.gson.toJson(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─────────────────────────── Deposits ───────────────────────────
|
|
||||||
|
|
||||||
private Map<String, Object> depositGroup(final String key, final String label, final int depositType, final LocalDate asOn,
|
|
||||||
final List<LocalDate> limits, final String hierarchy) {
|
|
||||||
final List<Map<String, Object>> rows = this.jdbcTemplate.queryForList("""
|
|
||||||
SELECT datp.maturity_date AS "d",
|
|
||||||
sa.status_enum AS "st",
|
|
||||||
COUNT(*) AS "n",
|
|
||||||
COALESCE(SUM(sa.account_balance_derived), 0) AS "amt"
|
|
||||||
FROM m_savings_account sa
|
|
||||||
JOIN m_deposit_account_term_and_preclosure datp ON datp.savings_account_id = sa.id
|
|
||||||
LEFT JOIN m_client c ON c.id = sa.client_id
|
|
||||||
LEFT JOIN m_group g ON g.id = sa.group_id
|
|
||||||
JOIN m_office o ON o.id = COALESCE(sa.office_id, c.office_id, g.office_id)
|
|
||||||
WHERE sa.deposit_type_enum = ?
|
|
||||||
AND sa.status_enum IN (?, ?)
|
|
||||||
AND o.hierarchy LIKE ?
|
|
||||||
GROUP BY datp.maturity_date, sa.status_enum
|
|
||||||
""", depositType, SAVINGS_ACTIVE, SAVINGS_MATURED, hierarchy);
|
|
||||||
|
|
||||||
final BigDecimal[] buckets = zeros(limits.size() + 1);
|
|
||||||
long accounts = 0;
|
|
||||||
for (final Map<String, Object> row : rows) {
|
|
||||||
final long n = ((Number) row.get("n")).longValue();
|
|
||||||
final BigDecimal amount = (BigDecimal) row.get("amt");
|
|
||||||
accounts += n;
|
|
||||||
final Date maturity = (Date) row.get("d");
|
|
||||||
// A MATURED deposit is already past its date and still owed: the first bucket, whatever the date says.
|
|
||||||
final boolean matured = ((Number) row.get("st")).intValue() == SAVINGS_MATURED;
|
|
||||||
final int index = matured || maturity == null ? 0 : bucketIndex(asOn, maturity.toLocalDate(), limits);
|
|
||||||
buckets[index] = buckets[index].add(amount);
|
|
||||||
}
|
|
||||||
return group(key, label, "liability", accounts, buckets);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─────────────────────────── Loans ───────────────────────────
|
|
||||||
|
|
||||||
private Map<String, Object> termLoanGroup(final LocalDate asOn, final List<LocalDate> limits, final String hierarchy,
|
|
||||||
final int recentOverdueBucket, final int oldOverdueBucket) {
|
|
||||||
final List<Map<String, Object>> rows = this.jdbcTemplate.queryForList("""
|
|
||||||
SELECT rs.duedate AS "d",
|
|
||||||
COALESCE(SUM(GREATEST(rs.principal_amount
|
|
||||||
- COALESCE(rs.principal_completed_derived, 0)
|
|
||||||
- COALESCE(rs.principal_writtenoff_derived, 0), 0)), 0) AS "amt"
|
|
||||||
FROM m_loan_repayment_schedule rs
|
|
||||||
JOIN m_loan l ON l.id = rs.loan_id
|
|
||||||
LEFT JOIN m_client c ON c.id = l.client_id
|
|
||||||
LEFT JOIN m_group g ON g.id = l.group_id
|
|
||||||
JOIN m_office o ON o.id = COALESCE(c.office_id, g.office_id)
|
|
||||||
WHERE l.loan_status_id = ?
|
|
||||||
AND COALESCE(l.is_npa, false) = false
|
|
||||||
AND COALESCE(rs.completed_derived, false) = false
|
|
||||||
AND NOT EXISTS (SELECT 1 FROM m_product_loan_advance_ext ext
|
|
||||||
WHERE ext.loan_product_id = l.product_id
|
|
||||||
AND (LOWER(ext.product_type_sub_group) IN ('cc', 'overdraft', 'kcc')
|
|
||||||
OR LOWER(ext.facility_type) IN ('demand', 'overdraft')))
|
|
||||||
AND o.hierarchy LIKE ?
|
|
||||||
GROUP BY rs.duedate
|
|
||||||
""", LOAN_ACTIVE, hierarchy);
|
|
||||||
|
|
||||||
final BigDecimal[] buckets = zeros(limits.size() + 1);
|
|
||||||
final LocalDate recentCutoff = asOn.minusMonths(1);
|
|
||||||
for (final Map<String, Object> row : rows) {
|
|
||||||
final LocalDate due = ((Date) row.get("d")).toLocalDate();
|
|
||||||
final BigDecimal amount = (BigDecimal) row.get("amt");
|
|
||||||
final int index;
|
|
||||||
if (due.isAfter(asOn)) {
|
|
||||||
index = bucketIndex(asOn, due, limits);
|
|
||||||
} else {
|
|
||||||
// Already due and unpaid. Under a month overdue is still expected shortly; older is not.
|
|
||||||
index = due.isAfter(recentCutoff) ? recentOverdueBucket : oldOverdueBucket;
|
|
||||||
}
|
|
||||||
buckets[index] = buckets[index].add(amount);
|
|
||||||
}
|
|
||||||
|
|
||||||
final Long loans = this.jdbcTemplate.queryForObject("""
|
|
||||||
SELECT COUNT(*)
|
|
||||||
FROM m_loan l
|
|
||||||
LEFT JOIN m_client c ON c.id = l.client_id
|
|
||||||
LEFT JOIN m_group g ON g.id = l.group_id
|
|
||||||
JOIN m_office o ON o.id = COALESCE(c.office_id, g.office_id)
|
|
||||||
WHERE l.loan_status_id = ?
|
|
||||||
AND COALESCE(l.is_npa, false) = false
|
|
||||||
AND NOT EXISTS (SELECT 1 FROM m_product_loan_advance_ext ext
|
|
||||||
WHERE ext.loan_product_id = l.product_id
|
|
||||||
AND (LOWER(ext.product_type_sub_group) IN ('cc', 'overdraft', 'kcc')
|
|
||||||
OR LOWER(ext.facility_type) IN ('demand', 'overdraft')))
|
|
||||||
AND o.hierarchy LIKE ?
|
|
||||||
""", Long.class, LOAN_ACTIVE, hierarchy);
|
|
||||||
return group("LOAN", "Term Loans (principal instalments)", "asset", loans == null ? 0 : loans, buckets);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** NPA loans: RBI slots them by classification (sub-standard / doubtful / loss), not by instalment date. */
|
|
||||||
private Map<String, Object> npaLoans(final String hierarchy) {
|
|
||||||
return outstanding("NPA", "NPA loans", """
|
|
||||||
SELECT COUNT(*) AS "n", COALESCE(SUM(l.principal_outstanding_derived), 0) AS "amt"
|
|
||||||
FROM m_loan l
|
|
||||||
LEFT JOIN m_client c ON c.id = l.client_id
|
|
||||||
LEFT JOIN m_group g ON g.id = l.group_id
|
|
||||||
JOIN m_office o ON o.id = COALESCE(c.office_id, g.office_id)
|
|
||||||
WHERE l.loan_status_id = ? AND COALESCE(l.is_npa, false) = true AND o.hierarchy LIKE ?
|
|
||||||
""", "Slotted by classification (sub-standard, doubtful, loss), net of provision, not by instalment date.",
|
|
||||||
hierarchy);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** CC / OD / demand loans: the stored schedule is synthetic, so the volatile/core split rule applies instead. */
|
|
||||||
private Map<String, Object> revolvingLoans(final String hierarchy) {
|
|
||||||
return outstanding("REVOLVING", "CC / OD / demand loans", """
|
|
||||||
SELECT COUNT(*) AS "n", COALESCE(SUM(l.principal_outstanding_derived), 0) AS "amt"
|
|
||||||
FROM m_loan l
|
|
||||||
LEFT JOIN m_client c ON c.id = l.client_id
|
|
||||||
LEFT JOIN m_group g ON g.id = l.group_id
|
|
||||||
JOIN m_office o ON o.id = COALESCE(c.office_id, g.office_id)
|
|
||||||
WHERE l.loan_status_id = ?
|
|
||||||
AND COALESCE(l.is_npa, false) = false
|
|
||||||
AND EXISTS (SELECT 1 FROM m_product_loan_advance_ext ext
|
|
||||||
WHERE ext.loan_product_id = l.product_id
|
|
||||||
AND (LOWER(ext.product_type_sub_group) IN ('cc', 'overdraft', 'kcc')
|
|
||||||
OR LOWER(ext.facility_type) IN ('demand', 'overdraft')))
|
|
||||||
AND o.hierarchy LIKE ?
|
|
||||||
""", "Split into a volatile and a core share by the bank's own rule; the stored instalment schedule is not a real repayment pattern.",
|
|
||||||
hierarchy);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** One count-and-total line for loans that are reported apart from the date buckets. */
|
|
||||||
private Map<String, Object> outstanding(final String key, final String label, final String sql, final String why,
|
|
||||||
final String hierarchy) {
|
|
||||||
final Map<String, Object> row = this.jdbcTemplate.queryForMap(sql, LOAN_ACTIVE, hierarchy);
|
|
||||||
final Map<String, Object> line = new LinkedHashMap<>();
|
|
||||||
line.put("key", key);
|
|
||||||
line.put("label", label);
|
|
||||||
line.put("side", "asset");
|
|
||||||
line.put("accounts", ((Number) row.get("n")).longValue());
|
|
||||||
line.put("outstanding", row.get("amt"));
|
|
||||||
line.put("why", why);
|
|
||||||
return line;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─────────────────────────── Picking accounts one by one ───────────────────────────
|
|
||||||
|
|
||||||
/** How many matches one search returns. */
|
|
||||||
private static final int SEARCH_LIMIT = 25;
|
|
||||||
|
|
||||||
/** How many accounts one report may cover. */
|
|
||||||
private static final int SELECTED_LIMIT = 200;
|
|
||||||
|
|
||||||
/** A loan product whose stored schedule is not a real repayment pattern: cash credit, overdraft, demand. */
|
|
||||||
private static final String REVOLVING_PRODUCT = "EXISTS (SELECT 1 FROM m_product_loan_advance_ext ext"
|
|
||||||
+ " WHERE ext.loan_product_id = l.product_id"
|
|
||||||
+ " AND (LOWER(ext.product_type_sub_group) IN ('cc', 'overdraft', 'kcc') OR LOWER(ext.facility_type) IN ('demand', 'overdraft')))";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find fixed deposits, recurring deposits and loans by account number or customer name, so a few of
|
|
||||||
* them can be picked and broken down by days.
|
|
||||||
*
|
|
||||||
* @param kind
|
|
||||||
* FD, RD or LOAN to look in one kind only; empty to look in all three
|
|
||||||
*/
|
|
||||||
@GET
|
|
||||||
@Path("search")
|
|
||||||
public String search(@QueryParam("q") final String q, @QueryParam("kind") final String kind,
|
|
||||||
@QueryParam("officeId") final Long officeId) {
|
|
||||||
this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSION);
|
|
||||||
|
|
||||||
final String text = q == null ? "" : q.trim().toLowerCase();
|
|
||||||
final List<Map<String, Object>> results = new ArrayList<>();
|
|
||||||
if (text.length() >= 2) {
|
|
||||||
final String like = "%" + text + "%";
|
|
||||||
final String hierarchy = scopeHierarchy(officeId) + "%";
|
|
||||||
final String wanted = kind == null ? "" : kind.trim().toUpperCase();
|
|
||||||
if (wanted.isEmpty() || "FD".equals(wanted)) {
|
|
||||||
results.addAll(searchDeposits(FIXED_DEPOSIT, "FD", like, hierarchy));
|
|
||||||
}
|
|
||||||
if (wanted.isEmpty() || "RD".equals(wanted)) {
|
|
||||||
results.addAll(searchDeposits(RECURRING_DEPOSIT, "RD", like, hierarchy));
|
|
||||||
}
|
|
||||||
if (wanted.isEmpty() || "LOAN".equals(wanted)) {
|
|
||||||
results.addAll(searchLoans(like, hierarchy));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
final Map<String, Object> response = new LinkedHashMap<>();
|
|
||||||
response.put("results", results);
|
|
||||||
return this.gson.toJson(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The day-wise breakdown of the accounts that were picked: each deposit as one row on its maturity
|
|
||||||
* date, each loan as one row per unpaid instalment on its due date, every row with the days to that
|
|
||||||
* date from the as-on date and the bucket it falls in.
|
|
||||||
*
|
|
||||||
* @param accountsParam
|
|
||||||
* the picked accounts as KIND:id pairs, for example FD:12,RD:5,LOAN:33
|
|
||||||
*/
|
|
||||||
@GET
|
|
||||||
@Path("selected")
|
|
||||||
public String selected(@QueryParam("accounts") final String accountsParam, @QueryParam("asOnDate") final String asOnDateParam,
|
|
||||||
@QueryParam("officeId") final Long officeId, @QueryParam("buckets") final String bucketsParam,
|
|
||||||
@QueryParam("overdueRecentBucket") final Integer overdueRecentBucket,
|
|
||||||
@QueryParam("overdueOldBucket") final Integer overdueOldBucket) {
|
|
||||||
this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSION);
|
|
||||||
|
|
||||||
if (accountsParam == null || accountsParam.isBlank()) {
|
|
||||||
throw new BadRequestException("accounts is required, like FD:12,RD:5,LOAN:33");
|
|
||||||
}
|
|
||||||
final LocalDate asOn = parseDate(asOnDateParam);
|
|
||||||
final List<LocalDate> limits = parseLimits(asOn, bucketsParam == null || bucketsParam.isBlank() ? DEFAULT_BUCKETS : bucketsParam);
|
|
||||||
final int bucketCount = limits.size() + 1;
|
|
||||||
final String hierarchy = scopeHierarchy(officeId) + "%";
|
|
||||||
|
|
||||||
final List<Long> fixed = new ArrayList<>();
|
|
||||||
final List<Long> recurring = new ArrayList<>();
|
|
||||||
final List<Long> loans = new ArrayList<>();
|
|
||||||
for (final String token : accountsParam.split(",")) {
|
|
||||||
final String[] parts = token.trim().split(":");
|
|
||||||
if (parts.length != 2) {
|
|
||||||
throw new BadRequestException("accounts must look like FD:12,RD:5,LOAN:33");
|
|
||||||
}
|
|
||||||
final long id;
|
|
||||||
try {
|
|
||||||
id = Long.parseLong(parts[1].trim());
|
|
||||||
} catch (final NumberFormatException e) {
|
|
||||||
throw new BadRequestException("Account id '" + parts[1] + "' is not a number");
|
|
||||||
}
|
|
||||||
switch (parts[0].trim().toUpperCase()) {
|
|
||||||
case "FD" -> fixed.add(id);
|
|
||||||
case "RD" -> recurring.add(id);
|
|
||||||
case "LOAN" -> loans.add(id);
|
|
||||||
default -> throw new BadRequestException("Account kind '" + parts[0] + "' must be FD, RD or LOAN");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (fixed.size() + recurring.size() + loans.size() > SELECTED_LIMIT) {
|
|
||||||
throw new BadRequestException("Pick at most " + SELECTED_LIMIT + " accounts at a time");
|
|
||||||
}
|
|
||||||
|
|
||||||
final List<Map<String, Object>> rows = new ArrayList<>();
|
|
||||||
if (!fixed.isEmpty()) {
|
|
||||||
rows.addAll(depositRows(FIXED_DEPOSIT, "FD", fixed, asOn, limits, hierarchy));
|
|
||||||
}
|
|
||||||
if (!recurring.isEmpty()) {
|
|
||||||
rows.addAll(depositRows(RECURRING_DEPOSIT, "RD", recurring, asOn, limits, hierarchy));
|
|
||||||
}
|
|
||||||
if (!loans.isEmpty()) {
|
|
||||||
rows.addAll(instalmentRows(loans, asOn, limits, hierarchy, clampBucket(overdueRecentBucket, 0, bucketCount),
|
|
||||||
clampBucket(overdueOldBucket, 2, bucketCount)));
|
|
||||||
}
|
|
||||||
// By date, undated rows last, then by account.
|
|
||||||
rows.sort(Comparator.comparing((Map<String, Object> r) -> String.valueOf(r.get("date")))
|
|
||||||
.thenComparing(r -> String.valueOf(r.get("accountNo"))));
|
|
||||||
|
|
||||||
BigDecimal total = BigDecimal.ZERO;
|
|
||||||
for (final Map<String, Object> row : rows) {
|
|
||||||
total = total.add((BigDecimal) row.get("amount"));
|
|
||||||
}
|
|
||||||
final Map<String, Object> response = new LinkedHashMap<>();
|
|
||||||
response.put("asOnDate", asOn.toString());
|
|
||||||
response.put("bucketCount", bucketCount);
|
|
||||||
response.put("count", rows.size());
|
|
||||||
response.put("total", total);
|
|
||||||
response.put("rows", rows);
|
|
||||||
return this.gson.toJson(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<Map<String, Object>> searchDeposits(final int depositType, final String kind, final String like,
|
|
||||||
final String hierarchy) {
|
|
||||||
final List<Map<String, Object>> found = this.jdbcTemplate.queryForList("""
|
|
||||||
SELECT sa.id AS "id",
|
|
||||||
sa.account_no AS "accountNo",
|
|
||||||
COALESCE(c.display_name, g.display_name) AS "customer",
|
|
||||||
sp.name AS "product",
|
|
||||||
datp.maturity_date AS "d",
|
|
||||||
COALESCE(sa.account_balance_derived, 0) AS "amt"
|
|
||||||
FROM m_savings_account sa
|
|
||||||
JOIN m_deposit_account_term_and_preclosure datp ON datp.savings_account_id = sa.id
|
|
||||||
JOIN m_savings_product sp ON sp.id = sa.product_id
|
|
||||||
LEFT JOIN m_client c ON c.id = sa.client_id
|
|
||||||
LEFT JOIN m_group g ON g.id = sa.group_id
|
|
||||||
JOIN m_office o ON o.id = COALESCE(sa.office_id, c.office_id, g.office_id)
|
|
||||||
WHERE sa.deposit_type_enum = ?
|
|
||||||
AND sa.status_enum IN (?, ?)
|
|
||||||
AND o.hierarchy LIKE ?
|
|
||||||
AND (LOWER(sa.account_no) LIKE ? OR LOWER(COALESCE(c.display_name, g.display_name, '')) LIKE ?)
|
|
||||||
ORDER BY sa.account_no
|
|
||||||
LIMIT ?
|
|
||||||
""", depositType, SAVINGS_ACTIVE, SAVINGS_MATURED, hierarchy, like, like, SEARCH_LIMIT);
|
|
||||||
return hits(found, kind);
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<Map<String, Object>> searchLoans(final String like, final String hierarchy) {
|
|
||||||
final List<Map<String, Object>> found = this.jdbcTemplate.queryForList("""
|
|
||||||
SELECT l.id AS "id",
|
|
||||||
l.account_no AS "accountNo",
|
|
||||||
COALESCE(c.display_name, g.display_name) AS "customer",
|
|
||||||
lp.name AS "product",
|
|
||||||
COALESCE(l.principal_outstanding_derived, 0) AS "amt"
|
|
||||||
FROM m_loan l
|
|
||||||
JOIN m_product_loan lp ON lp.id = l.product_id
|
|
||||||
LEFT JOIN m_client c ON c.id = l.client_id
|
|
||||||
LEFT JOIN m_group g ON g.id = l.group_id
|
|
||||||
JOIN m_office o ON o.id = COALESCE(c.office_id, g.office_id)
|
|
||||||
WHERE l.loan_status_id = ?
|
|
||||||
AND o.hierarchy LIKE ?
|
|
||||||
AND (LOWER(l.account_no) LIKE ? OR LOWER(COALESCE(c.display_name, g.display_name, '')) LIKE ?)
|
|
||||||
ORDER BY l.account_no
|
|
||||||
LIMIT ?
|
|
||||||
""", LOAN_ACTIVE, hierarchy, like, like, SEARCH_LIMIT);
|
|
||||||
return hits(found, "LOAN");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<Map<String, Object>> hits(final List<Map<String, Object>> found, final String kind) {
|
|
||||||
final List<Map<String, Object>> rows = new ArrayList<>();
|
|
||||||
for (final Map<String, Object> r : found) {
|
|
||||||
final Date date = (Date) r.get("d");
|
|
||||||
final Map<String, Object> row = new LinkedHashMap<>();
|
|
||||||
row.put("kind", kind);
|
|
||||||
row.put("id", r.get("id"));
|
|
||||||
row.put("accountNo", r.get("accountNo"));
|
|
||||||
row.put("customer", r.get("customer"));
|
|
||||||
row.put("product", r.get("product"));
|
|
||||||
row.put("date", date == null ? null : date.toLocalDate().toString());
|
|
||||||
row.put("amount", r.get("amt"));
|
|
||||||
rows.add(row);
|
|
||||||
}
|
|
||||||
return rows;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String placeholders(final int count) {
|
|
||||||
return String.join(",", Collections.nCopies(count, "?"));
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<Map<String, Object>> depositRows(final int depositType, final String label, final List<Long> ids,
|
|
||||||
final LocalDate asOn, final List<LocalDate> limits, final String hierarchy) {
|
|
||||||
final String sql = """
|
|
||||||
SELECT sa.id AS "id",
|
|
||||||
sa.account_no AS "accountNo",
|
|
||||||
COALESCE(c.display_name, g.display_name) AS "customer",
|
|
||||||
sp.name AS "product",
|
|
||||||
datp.maturity_date AS "d",
|
|
||||||
sa.status_enum AS "st",
|
|
||||||
COALESCE(sa.account_balance_derived, 0) AS "amt"
|
|
||||||
FROM m_savings_account sa
|
|
||||||
JOIN m_deposit_account_term_and_preclosure datp ON datp.savings_account_id = sa.id
|
|
||||||
JOIN m_savings_product sp ON sp.id = sa.product_id
|
|
||||||
LEFT JOIN m_client c ON c.id = sa.client_id
|
|
||||||
LEFT JOIN m_group g ON g.id = sa.group_id
|
|
||||||
JOIN m_office o ON o.id = COALESCE(sa.office_id, c.office_id, g.office_id)
|
|
||||||
WHERE sa.deposit_type_enum = ?
|
|
||||||
AND sa.status_enum IN (?, ?)
|
|
||||||
AND o.hierarchy LIKE ?
|
|
||||||
AND sa.id IN (%s)
|
|
||||||
ORDER BY datp.maturity_date, sa.account_no
|
|
||||||
""".formatted(placeholders(ids.size()));
|
|
||||||
final List<Object> args = new ArrayList<>(List.of(depositType, SAVINGS_ACTIVE, SAVINGS_MATURED, hierarchy));
|
|
||||||
args.addAll(ids);
|
|
||||||
|
|
||||||
final List<Map<String, Object>> rows = new ArrayList<>();
|
|
||||||
for (final Map<String, Object> r : this.jdbcTemplate.queryForList(sql, args.toArray())) {
|
|
||||||
final Date maturity = (Date) r.get("d");
|
|
||||||
final boolean matured = ((Number) r.get("st")).intValue() == SAVINGS_MATURED;
|
|
||||||
final Map<String, Object> row = new LinkedHashMap<>();
|
|
||||||
row.put("id", r.get("id"));
|
|
||||||
row.put("accountNo", r.get("accountNo"));
|
|
||||||
row.put("customer", r.get("customer"));
|
|
||||||
row.put("product", r.get("product"));
|
|
||||||
row.put("kind", label);
|
|
||||||
row.put("date", maturity == null ? null : maturity.toLocalDate().toString());
|
|
||||||
row.put("days", maturity == null ? null : ChronoUnit.DAYS.between(asOn, maturity.toLocalDate()));
|
|
||||||
row.put("status", matured ? "MATURED" : "ACTIVE");
|
|
||||||
// Same rule as the totals: a matured deposit is overdue and sits in the first bucket.
|
|
||||||
row.put("bucket", matured || maturity == null ? 0 : bucketIndex(asOn, maturity.toLocalDate(), limits));
|
|
||||||
row.put("amount", r.get("amt"));
|
|
||||||
rows.add(row);
|
|
||||||
}
|
|
||||||
return rows;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The unpaid instalments of the picked loans. A picked loan is shown by its schedule whatever it is,
|
|
||||||
* with a note where the schedule is not the whole story (an NPA, or a cash credit / overdraft).
|
|
||||||
*/
|
|
||||||
private List<Map<String, Object>> instalmentRows(final List<Long> ids, final LocalDate asOn, final List<LocalDate> limits,
|
|
||||||
final String hierarchy, final int recentOverdueBucket, final int oldOverdueBucket) {
|
|
||||||
final String sql = """
|
|
||||||
SELECT l.id AS "id",
|
|
||||||
l.account_no AS "accountNo",
|
|
||||||
COALESCE(c.display_name, g.display_name) AS "customer",
|
|
||||||
lp.name AS "product",
|
|
||||||
rs.installment AS "installment",
|
|
||||||
rs.duedate AS "d",
|
|
||||||
GREATEST(rs.principal_amount
|
|
||||||
- COALESCE(rs.principal_completed_derived, 0)
|
|
||||||
- COALESCE(rs.principal_writtenoff_derived, 0), 0) AS "amt",
|
|
||||||
COALESCE(l.is_npa, false) AS "npa",
|
|
||||||
(%s) AS "revolving"
|
|
||||||
FROM m_loan_repayment_schedule rs
|
|
||||||
JOIN m_loan l ON l.id = rs.loan_id
|
|
||||||
JOIN m_product_loan lp ON lp.id = l.product_id
|
|
||||||
LEFT JOIN m_client c ON c.id = l.client_id
|
|
||||||
LEFT JOIN m_group g ON g.id = l.group_id
|
|
||||||
JOIN m_office o ON o.id = COALESCE(c.office_id, g.office_id)
|
|
||||||
WHERE l.loan_status_id = ?
|
|
||||||
AND COALESCE(rs.completed_derived, false) = false
|
|
||||||
AND GREATEST(rs.principal_amount
|
|
||||||
- COALESCE(rs.principal_completed_derived, 0)
|
|
||||||
- COALESCE(rs.principal_writtenoff_derived, 0), 0) > 0
|
|
||||||
AND o.hierarchy LIKE ?
|
|
||||||
AND l.id IN (%s)
|
|
||||||
ORDER BY rs.duedate, l.account_no
|
|
||||||
""".formatted(REVOLVING_PRODUCT, placeholders(ids.size()));
|
|
||||||
final List<Object> args = new ArrayList<>(List.of(LOAN_ACTIVE, hierarchy));
|
|
||||||
args.addAll(ids);
|
|
||||||
|
|
||||||
final LocalDate recentCutoff = asOn.minusMonths(1);
|
|
||||||
final List<Map<String, Object>> rows = new ArrayList<>();
|
|
||||||
for (final Map<String, Object> r : this.jdbcTemplate.queryForList(sql, args.toArray())) {
|
|
||||||
final LocalDate due = ((Date) r.get("d")).toLocalDate();
|
|
||||||
final int bucket;
|
|
||||||
if (due.isAfter(asOn)) {
|
|
||||||
bucket = bucketIndex(asOn, due, limits);
|
|
||||||
} else {
|
|
||||||
bucket = due.isAfter(recentCutoff) ? recentOverdueBucket : oldOverdueBucket;
|
|
||||||
}
|
|
||||||
final boolean npa = Boolean.TRUE.equals(r.get("npa"));
|
|
||||||
final boolean revolving = Boolean.TRUE.equals(r.get("revolving"));
|
|
||||||
final Map<String, Object> row = new LinkedHashMap<>();
|
|
||||||
row.put("id", r.get("id"));
|
|
||||||
row.put("accountNo", r.get("accountNo"));
|
|
||||||
row.put("customer", r.get("customer"));
|
|
||||||
row.put("product", r.get("product"));
|
|
||||||
row.put("kind", "LOAN");
|
|
||||||
row.put("installment", r.get("installment"));
|
|
||||||
row.put("date", due.toString());
|
|
||||||
row.put("days", ChronoUnit.DAYS.between(asOn, due));
|
|
||||||
row.put("status", due.isAfter(asOn) ? "DUE LATER" : "OVERDUE");
|
|
||||||
row.put("bucket", bucket);
|
|
||||||
row.put("amount", r.get("amt"));
|
|
||||||
row.put("note", npa ? "NPA loan: RBI slots it by classification, not by instalment date"
|
|
||||||
: revolving ? "CC / OD loan: the stored schedule is not a real repayment pattern" : null);
|
|
||||||
rows.add(row);
|
|
||||||
}
|
|
||||||
return rows;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─────────────────────────── Helpers ───────────────────────────
|
|
||||||
|
|
||||||
private static Map<String, Object> group(final String key, final String label, final String side, final long accounts,
|
|
||||||
final BigDecimal[] buckets) {
|
|
||||||
BigDecimal total = BigDecimal.ZERO;
|
|
||||||
for (final BigDecimal b : buckets) {
|
|
||||||
total = total.add(b);
|
|
||||||
}
|
|
||||||
final Map<String, Object> group = new LinkedHashMap<>();
|
|
||||||
group.put("key", key);
|
|
||||||
group.put("label", label);
|
|
||||||
group.put("side", side);
|
|
||||||
group.put("accounts", accounts);
|
|
||||||
group.put("total", total);
|
|
||||||
group.put("buckets", List.of(buckets));
|
|
||||||
return group;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static BigDecimal[] zeros(final int size) {
|
|
||||||
final BigDecimal[] values = new BigDecimal[size];
|
|
||||||
java.util.Arrays.fill(values, BigDecimal.ZERO);
|
|
||||||
return values;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Bucket of a maturity: due or overdue goes in the first; otherwise the first bucket whose end it does not pass. */
|
|
||||||
static int bucketIndex(final LocalDate asOn, final LocalDate maturity, final List<LocalDate> limits) {
|
|
||||||
if (!maturity.isAfter(asOn)) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
for (int i = 0; i < limits.size(); i++) {
|
|
||||||
if (!maturity.isAfter(limits.get(i))) {
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return limits.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** "14d,28d,3m" to the dates those edges reach from the as-on date. Calendar months and years; edges must climb. */
|
|
||||||
private static List<LocalDate> parseLimits(final LocalDate asOn, final String spec) {
|
|
||||||
final List<LocalDate> limits = new ArrayList<>();
|
|
||||||
for (final String token : spec.split(",")) {
|
|
||||||
final Matcher m = EDGE.matcher(token.trim().toLowerCase());
|
|
||||||
if (!m.matches() || Integer.parseInt(m.group(1)) < 1) {
|
|
||||||
throw new BadRequestException("Bucket edge '" + token + "' is not like 14d, 3m or 1y");
|
|
||||||
}
|
|
||||||
final int n = Integer.parseInt(m.group(1));
|
|
||||||
final LocalDate limit = switch (m.group(2)) {
|
|
||||||
case "d" -> asOn.plusDays(n);
|
|
||||||
case "m" -> asOn.plusMonths(n);
|
|
||||||
default -> asOn.plusYears(n);
|
|
||||||
};
|
|
||||||
if (!limits.isEmpty() && !limit.isAfter(limits.get(limits.size() - 1))) {
|
|
||||||
throw new BadRequestException("Bucket edges must run in increasing order: " + spec);
|
|
||||||
}
|
|
||||||
limits.add(limit);
|
|
||||||
}
|
|
||||||
return limits;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int clampBucket(final Integer requested, final int fallback, final int bucketCount) {
|
|
||||||
final int value = requested == null ? fallback : requested;
|
|
||||||
return Math.max(0, Math.min(value, bucketCount - 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static LocalDate parseDate(final String raw) {
|
|
||||||
if (raw == null || raw.isBlank()) {
|
|
||||||
return DateUtils.getBusinessLocalDate();
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return LocalDate.parse(raw);
|
|
||||||
} catch (final java.time.format.DateTimeParseException e) {
|
|
||||||
throw new BadRequestException("asOnDate must be yyyy-MM-dd");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The office tree a caller may read: their own, or a branch under it if they asked for one. */
|
|
||||||
private String scopeHierarchy(final Long officeId) {
|
|
||||||
final String own = this.context.authenticatedUser().getOffice().getHierarchy();
|
|
||||||
if (officeId == null) {
|
|
||||||
return own;
|
|
||||||
}
|
|
||||||
final List<String> asked = this.jdbcTemplate.queryForList("SELECT hierarchy FROM m_office WHERE id = ?", String.class, officeId);
|
|
||||||
return !asked.isEmpty() && asked.get(0).startsWith(own) ? asked.get(0) : own;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user