Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
08519da8b1 | ||
|
|
cad7f54c47 |
+20
-1
@@ -137,7 +137,7 @@ public class CommandSourceService {
|
|||||||
String permission = commandSource.getPermissionCode();
|
String permission = commandSource.getPermissionCode();
|
||||||
boolean isMakerChecker = configurationDomainService.isMakerCheckerEnabledForTask(permission);
|
boolean isMakerChecker = configurationDomainService.isMakerCheckerEnabledForTask(permission);
|
||||||
if (isMakerChecker || result.isRollbackTransaction()) {
|
if (isMakerChecker || result.isRollbackTransaction()) {
|
||||||
if (isApprovedByChecker || user.isCheckerSuperUser()) {
|
if (isApprovedByChecker || isSuperUserBypassAllowed(commandSource, user)) {
|
||||||
commandSource.markAsChecked(user);
|
commandSource.markAsChecked(user);
|
||||||
} else {
|
} else {
|
||||||
if (commandSource.isSanitized()) {
|
if (commandSource.isSanitized()) {
|
||||||
@@ -151,6 +151,25 @@ public class CommandSourceService {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isSuperUserBypassAllowed(CommandSource commandSource, AppUser user) {
|
||||||
|
if (!user.isCheckerSuperUser()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if ("GLACCOUNT".equalsIgnoreCase(commandSource.getEntityName())
|
||||||
|
|| "CBSBASEINTERESTRATE".equalsIgnoreCase(commandSource.getEntityName())
|
||||||
|
|| "CHARGE".equalsIgnoreCase(commandSource.getEntityName())
|
||||||
|
|| "LOANPRODUCT".equalsIgnoreCase(commandSource.getEntityName())
|
||||||
|
|| "SAVINGSPRODUCT".equalsIgnoreCase(commandSource.getEntityName())
|
||||||
|
|| "CURRENTPRODUCT".equalsIgnoreCase(commandSource.getEntityName())
|
||||||
|
|| "SHAREPRODUCT".equalsIgnoreCase(commandSource.getEntityName())
|
||||||
|
|| "FIXEDDEPOSITPRODUCT".equalsIgnoreCase(commandSource.getEntityName())
|
||||||
|
|| "USER".equalsIgnoreCase(commandSource.getEntityName())
|
||||||
|
|| "ROLE".equalsIgnoreCase(commandSource.getEntityName())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private void sanitizeJson(@NonNull CommandSource commandSource, Set<String> sanitizeKeys) {
|
private void sanitizeJson(@NonNull CommandSource commandSource, Set<String> sanitizeKeys) {
|
||||||
if (sanitizeKeys == null || sanitizeKeys.isEmpty()) {
|
if (sanitizeKeys == null || sanitizeKeys.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
+8
-9
@@ -2211,6 +2211,14 @@ public class CommandWrapperBuilder {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public CommandWrapperBuilder createCbsBaseInterestRate() {
|
||||||
|
this.actionName = "CREATE";
|
||||||
|
this.entityName = "CBSBASEINTERESTRATE";
|
||||||
|
this.entityId = null;
|
||||||
|
this.href = "/fixeddepositproducts/baseinterestrates";
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public CommandWrapperBuilder updateFixedDepositProduct(final Long productId) {
|
public CommandWrapperBuilder updateFixedDepositProduct(final Long productId) {
|
||||||
this.actionName = "UPDATE";
|
this.actionName = "UPDATE";
|
||||||
this.entityName = "FIXEDDEPOSITPRODUCT";
|
this.entityName = "FIXEDDEPOSITPRODUCT";
|
||||||
@@ -2897,15 +2905,6 @@ public class CommandWrapperBuilder {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public CommandWrapperBuilder paymentFixedDepositAccount(final Long accountId) {
|
|
||||||
this.actionName = "PAYMENT";
|
|
||||||
this.entityName = "FIXEDDEPOSITACCOUNT";
|
|
||||||
this.entityId = accountId;
|
|
||||||
this.savingsId = accountId;
|
|
||||||
this.href = "/fixeddepositaccounts/" + accountId + "?command=payment";
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CommandWrapperBuilder fixedDepositAccountInterestCalculation(final Long accountId) {
|
public CommandWrapperBuilder fixedDepositAccountInterestCalculation(final Long accountId) {
|
||||||
this.actionName = "CALCULATEINTEREST";
|
this.actionName = "CALCULATEINTEREST";
|
||||||
this.entityName = "FIXEDDEPOSITACCOUNT";
|
this.entityName = "FIXEDDEPOSITACCOUNT";
|
||||||
|
|||||||
+2
@@ -26,6 +26,8 @@ public interface ConfigurationDomainService {
|
|||||||
|
|
||||||
boolean isMakerCheckerEnabledForTask(String taskPermissionCode);
|
boolean isMakerCheckerEnabledForTask(String taskPermissionCode);
|
||||||
|
|
||||||
|
boolean isMakerCheckerEnabled();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Global kill-switch for MFA-factor-backed login enforcement. When true, users with at least
|
* Global kill-switch for MFA-factor-backed login enforcement. When true, users with at least
|
||||||
* one verified, non-disabled row in {@code m_user_mfa_factor} must complete a 2FA challenge
|
* one verified, non-disabled row in {@code m_user_mfa_factor} must complete a 2FA challenge
|
||||||
|
|||||||
-13
@@ -130,19 +130,6 @@ public class DataValidatorBuilder {
|
|||||||
this.dataValidationErrors.add(error);
|
this.dataValidationErrors.add(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Like {@link #failWithCode(String, Object...)}, but the caller's own message is what's actually shown to the
|
|
||||||
* user (that method always renders the generic "Failed data validation due to: {errorCode}." regardless of the
|
|
||||||
* args passed to it).
|
|
||||||
*/
|
|
||||||
public void failWithCodeAndMessage(final String errorCode, final String defaultUserMessage,
|
|
||||||
final Object... defaultUserMessageArgs) {
|
|
||||||
String validationErrorCode = "validation.msg." + this.resource + "." + this.parameter + "." + errorCode;
|
|
||||||
final ApiParameterError error = ApiParameterError.parameterError(validationErrorCode, defaultUserMessage, this.parameter,
|
|
||||||
this.value, defaultUserMessageArgs);
|
|
||||||
this.dataValidationErrors.add(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void failWithCodeNoParameterAddedToErrorCode(final String errorCode, final Object... defaultUserMessageArgs) {
|
public void failWithCodeNoParameterAddedToErrorCode(final String errorCode, final Object... defaultUserMessageArgs) {
|
||||||
String validationErrorCode = "validation.msg." + this.resource + "." + errorCode;
|
String validationErrorCode = "validation.msg." + this.resource + "." + errorCode;
|
||||||
String defaultEnglishMessage = "Failed data validation due to: " + errorCode + ".";
|
String defaultEnglishMessage = "Failed data validation due to: " + errorCode + ".";
|
||||||
|
|||||||
+1
-16
@@ -72,26 +72,11 @@ public class SavingsAccountSummaryData {
|
|||||||
// differentiate deposit accounts Savings, FD and RD accounts
|
// differentiate deposit accounts Savings, FD and RD accounts
|
||||||
private final EnumOptionData depositType;
|
private final EnumOptionData depositType;
|
||||||
|
|
||||||
/**
|
|
||||||
* For a Fixed Deposit, the stable customer-facing FD number: the account number of the first account in the renewal
|
|
||||||
* chain, which stays the same across every renewal even though each term is its own account row. Equals accountNo
|
|
||||||
* for an FD that has never been renewed, and is null for non-deposit accounts.
|
|
||||||
*/
|
|
||||||
private final String fdAccountNumber;
|
|
||||||
/**
|
|
||||||
* Which renewal cycle of that FD this row is: 0 for the originally opened account, 1 for the first renewal, and so
|
|
||||||
* on. Lets a client's account list collapse a renewal chain into one FD instead of listing each term separately.
|
|
||||||
*/
|
|
||||||
private final Integer renewalCycleNo;
|
|
||||||
|
|
||||||
public SavingsAccountSummaryData(final Long id, final String accountNo, final String externalId, final Long productId,
|
public SavingsAccountSummaryData(final Long id, final String accountNo, final String externalId, final Long productId,
|
||||||
final String productName, final String shortProductName, final SavingsAccountStatusEnumData status, final CurrencyData currency,
|
final String productName, final String shortProductName, final SavingsAccountStatusEnumData status, final CurrencyData currency,
|
||||||
final BigDecimal accountBalance, final BigDecimal onHoldFunds, final BigDecimal savingsAmountOnHold,
|
final BigDecimal accountBalance, final BigDecimal onHoldFunds, final BigDecimal savingsAmountOnHold,
|
||||||
final BigDecimal availableBalance, final EnumOptionData accountType, final SavingsAccountApplicationTimelineData timeline,
|
final BigDecimal availableBalance, final EnumOptionData accountType, final SavingsAccountApplicationTimelineData timeline,
|
||||||
final EnumOptionData depositType, final SavingsAccountSubStatusEnumData subStatus, final LocalDate lastActiveTransactionDate,
|
final EnumOptionData depositType, final SavingsAccountSubStatusEnumData subStatus, final LocalDate lastActiveTransactionDate) {
|
||||||
final String fdAccountNumber, final Integer renewalCycleNo) {
|
|
||||||
this.fdAccountNumber = fdAccountNumber;
|
|
||||||
this.renewalCycleNo = renewalCycleNo;
|
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.accountNo = accountNo;
|
this.accountNo = accountNo;
|
||||||
this.externalId = externalId;
|
this.externalId = externalId;
|
||||||
|
|||||||
+1
-7
@@ -31,8 +31,7 @@ public enum DepositAccountOnClosureType {
|
|||||||
WITHDRAW_DEPOSIT(100, "depositAccountClosureType.withdrawDeposit"), //
|
WITHDRAW_DEPOSIT(100, "depositAccountClosureType.withdrawDeposit"), //
|
||||||
TRANSFER_TO_SAVINGS(200, "depositAccountClosureType.transferToSavings"), //
|
TRANSFER_TO_SAVINGS(200, "depositAccountClosureType.transferToSavings"), //
|
||||||
REINVEST_PRINCIPAL_AND_INTEREST(300, "depositAccountClosureType.reinvestPrincipalAndInterest"), //
|
REINVEST_PRINCIPAL_AND_INTEREST(300, "depositAccountClosureType.reinvestPrincipalAndInterest"), //
|
||||||
REINVEST_PRINCIPAL_ONLY(400, "depositAccountClosureType.reinvestPrincipalOnly"), //
|
REINVEST_PRINCIPAL_ONLY(400, "depositAccountClosureType.reinvestPrincipalOnly"); //
|
||||||
PARTIAL_REINVEST(500, "depositAccountClosureType.partialReinvest"); //
|
|
||||||
|
|
||||||
private final Integer value;
|
private final Integer value;
|
||||||
private final String code;
|
private final String code;
|
||||||
@@ -52,7 +51,6 @@ public enum DepositAccountOnClosureType {
|
|||||||
case 200 -> TRANSFER_TO_SAVINGS;
|
case 200 -> TRANSFER_TO_SAVINGS;
|
||||||
case 300 -> REINVEST_PRINCIPAL_AND_INTEREST;
|
case 300 -> REINVEST_PRINCIPAL_AND_INTEREST;
|
||||||
case 400 -> REINVEST_PRINCIPAL_ONLY;
|
case 400 -> REINVEST_PRINCIPAL_ONLY;
|
||||||
case 500 -> PARTIAL_REINVEST;
|
|
||||||
default -> INVALID;
|
default -> INVALID;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -87,10 +85,6 @@ public enum DepositAccountOnClosureType {
|
|||||||
return this.equals(INVALID);
|
return this.equals(INVALID);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isPartialReinvest() {
|
|
||||||
return this.equals(PARTIAL_REINVEST);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: do we really need this?!?
|
// TODO: do we really need this?!?
|
||||||
public static Object[] integerValues() {
|
public static Object[] integerValues() {
|
||||||
return Arrays.stream(values()).filter(value -> !INVALID.equals(value)).map(value -> value.value).toList().toArray();
|
return Arrays.stream(values()).filter(value -> !INVALID.equals(value)).map(value -> value.value).toList().toArray();
|
||||||
|
|||||||
-1
@@ -208,7 +208,6 @@ public final class DepositsApiConstants {
|
|||||||
public static final String onAccountClosureIdParamName = "onAccountClosureId";
|
public static final String onAccountClosureIdParamName = "onAccountClosureId";
|
||||||
public static final String transferDescriptionParamName = "transferDescription";
|
public static final String transferDescriptionParamName = "transferDescription";
|
||||||
public static final String toSavingsAccountIdParamName = "toSavingsAccountId";
|
public static final String toSavingsAccountIdParamName = "toSavingsAccountId";
|
||||||
public static final String reinvestAmountParamName = "reinvestAmount";
|
|
||||||
public static final String savingsAccounts = "savingsAccounts";
|
public static final String savingsAccounts = "savingsAccounts";
|
||||||
public static final String maturityInstructionIdParamName = "maturityInstructionId";
|
public static final String maturityInstructionIdParamName = "maturityInstructionId";
|
||||||
public static final String transferToSavingsIdParamName = "transferToSavingsId";
|
public static final String transferToSavingsIdParamName = "transferToSavingsId";
|
||||||
|
|||||||
-4
@@ -797,10 +797,6 @@ public final class SavingsEnumerations {
|
|||||||
optionData = new EnumOptionData(DepositAccountOnClosureType.REINVEST_PRINCIPAL_ONLY.getValue().longValue(),
|
optionData = new EnumOptionData(DepositAccountOnClosureType.REINVEST_PRINCIPAL_ONLY.getValue().longValue(),
|
||||||
DepositAccountOnClosureType.REINVEST_PRINCIPAL_ONLY.getCode(), "Re-Invest Principal Only");
|
DepositAccountOnClosureType.REINVEST_PRINCIPAL_ONLY.getCode(), "Re-Invest Principal Only");
|
||||||
break;
|
break;
|
||||||
case PARTIAL_REINVEST:
|
|
||||||
optionData = new EnumOptionData(DepositAccountOnClosureType.PARTIAL_REINVEST.getValue().longValue(),
|
|
||||||
DepositAccountOnClosureType.PARTIAL_REINVEST.getCode(), "Partial Re-Invest");
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
return optionData;
|
return optionData;
|
||||||
}
|
}
|
||||||
|
|||||||
-5
@@ -1263,11 +1263,6 @@ public class AccountingProcessorHelper {
|
|||||||
accountMapping = paymentChannelSpecificAccountMapping;
|
accountMapping = paymentChannelSpecificAccountMapping;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (accountMapping == null) {
|
|
||||||
throw new ProductToGLAccountMappingNotFoundException(PortfolioProductType.SAVING, savingsProductId,
|
|
||||||
CashAccountsForSavings.fromInt(accountMappingTypeId).toString());
|
|
||||||
}
|
|
||||||
glAccount = accountMapping.getGlAccount();
|
glAccount = accountMapping.getGlAccount();
|
||||||
}
|
}
|
||||||
return glAccount;
|
return glAccount;
|
||||||
|
|||||||
+5
@@ -61,6 +61,11 @@ public class ConfigurationDomainServiceJpa implements ConfigurationDomainService
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isMakerCheckerEnabled() {
|
||||||
|
return getGlobalConfigurationPropertyData(GlobalConfigurationConstants.MAKER_CHECKER).isEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<String> getAllowedLoanStatusesForExternalAssetTransfer() {
|
public List<String> getAllowedLoanStatusesForExternalAssetTransfer() {
|
||||||
final GlobalConfigurationPropertyData property = getGlobalConfigurationPropertyData(
|
final GlobalConfigurationPropertyData property = getGlobalConfigurationPropertyData(
|
||||||
|
|||||||
+3
-16
@@ -357,14 +357,9 @@ public class AccountDetailsReadPlatformServiceJpaRepositoryImpl implements Accou
|
|||||||
accountsSummary.append("curr.name as currencyName, curr.internationalized_name_code as currencyNameCode, ");
|
accountsSummary.append("curr.name as currencyName, curr.internationalized_name_code as currencyNameCode, ");
|
||||||
accountsSummary.append("curr.display_symbol as currencyDisplaySymbol, ");
|
accountsSummary.append("curr.display_symbol as currencyDisplaySymbol, ");
|
||||||
accountsSummary.append("sa.product_id as productId, p.name as productName, p.short_name as shortProductName, ");
|
accountsSummary.append("sa.product_id as productId, p.name as productName, p.short_name as shortProductName, ");
|
||||||
accountsSummary.append("sa.deposit_type_enum as depositType, ");
|
accountsSummary.append("sa.deposit_type_enum as depositType ");
|
||||||
// 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
|
|
||||||
// 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("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_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 ");
|
||||||
accountsSummary.append("left join m_appuser rbu on rbu.id = sa.rejectedon_userid ");
|
accountsSummary.append("left join m_appuser rbu on rbu.id = sa.rejectedon_userid ");
|
||||||
@@ -458,15 +453,9 @@ public class AccountDetailsReadPlatformServiceJpaRepositoryImpl implements Accou
|
|||||||
approvedByUsername, approvedByFirstname, approvedByLastname, activatedOnDate, activatedByUsername, activatedByFirstname,
|
approvedByUsername, approvedByFirstname, approvedByLastname, activatedOnDate, activatedByUsername, activatedByFirstname,
|
||||||
activatedByLastname, closedOnDate, closedByUsername, closedByFirstname, closedByLastname);
|
activatedByLastname, closedOnDate, closedByUsername, closedByFirstname, closedByLastname);
|
||||||
|
|
||||||
// Null for a savings/current account and for an FD opened before this column existed;
|
|
||||||
// callers fall back to accountNo, so an FD that has never been renewed still reads as
|
|
||||||
// its own FD number.
|
|
||||||
final String fdAccountNumber = rs.getString("fdAccountNumber");
|
|
||||||
final Integer renewalCycleNo = JdbcSupport.getInteger(rs, "renewalCycleNo");
|
|
||||||
|
|
||||||
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);
|
lastActiveTransactionDate);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -578,10 +567,8 @@ public class AccountDetailsReadPlatformServiceJpaRepositoryImpl implements Accou
|
|||||||
|
|
||||||
final EnumOptionData depositTypeData = SavingsEnumerations.depositType(400);
|
final EnumOptionData depositTypeData = SavingsEnumerations.depositType(400);
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-6
@@ -440,9 +440,6 @@ public class FixedDepositAccountsApiResource {
|
|||||||
} else if (is(commandParam, "prematureClose")) {
|
} else if (is(commandParam, "prematureClose")) {
|
||||||
final CommandWrapper commandRequest = builder.prematureCloseFixedDepositAccount(accountId).build();
|
final CommandWrapper commandRequest = builder.prematureCloseFixedDepositAccount(accountId).build();
|
||||||
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
|
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
|
||||||
} else if (is(commandParam, "payment")) {
|
|
||||||
final CommandWrapper commandRequest = builder.paymentFixedDepositAccount(accountId).build();
|
|
||||||
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
|
|
||||||
} else if (is(commandParam, "calculatePrematureAmount")) {
|
} else if (is(commandParam, "calculatePrematureAmount")) {
|
||||||
final JsonElement parsedQuery = this.fromJsonHelper.parse(apiRequestBodyAsJson);
|
final JsonElement parsedQuery = this.fromJsonHelper.parse(apiRequestBodyAsJson);
|
||||||
final JsonQuery query = JsonQuery.from(apiRequestBodyAsJson, parsedQuery, this.fromJsonHelper);
|
final JsonQuery query = JsonQuery.from(apiRequestBodyAsJson, parsedQuery, this.fromJsonHelper);
|
||||||
@@ -456,7 +453,7 @@ public class FixedDepositAccountsApiResource {
|
|||||||
if (result == null) {
|
if (result == null) {
|
||||||
throw new UnrecognizedQueryParamException("command", commandParam,
|
throw new UnrecognizedQueryParamException("command", commandParam,
|
||||||
new Object[] { "reject", "withdrawnByApplicant", "approve", "undoapproval", "activate", "calculateInterest",
|
new Object[] { "reject", "withdrawnByApplicant", "approve", "undoapproval", "activate", "calculateInterest",
|
||||||
"postInterest", "close", "prematureClose", "payment", "calculatePrematureAmount" });
|
"postInterest", "close", "prematureClose", "calculatePrematureAmount" });
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.toApiJsonSerializer.serialize(result);
|
return this.toApiJsonSerializer.serialize(result);
|
||||||
@@ -492,8 +489,6 @@ public class FixedDepositAccountsApiResource {
|
|||||||
DepositAccountData account = null;
|
DepositAccountData account = null;
|
||||||
if (is(commandParam, "close")) {
|
if (is(commandParam, "close")) {
|
||||||
account = this.depositAccountReadPlatformService.retrieveOneWithClosureTemplate(DepositAccountType.FIXED_DEPOSIT, accountId);
|
account = this.depositAccountReadPlatformService.retrieveOneWithClosureTemplate(DepositAccountType.FIXED_DEPOSIT, accountId);
|
||||||
} else if (is(commandParam, "payment")) {
|
|
||||||
account = this.depositAccountReadPlatformService.retrieveOneWithPaymentTemplate(DepositAccountType.FIXED_DEPOSIT, accountId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
|
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
|
||||||
|
|||||||
+11
-6
@@ -29,6 +29,10 @@ import jakarta.ws.rs.core.MediaType;
|
|||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.commands.domain.CommandWrapper;
|
||||||
|
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.security.service.PlatformSecurityContext;
|
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||||
import org.apache.fineract.portfolio.savings.DepositsApiConstants;
|
import org.apache.fineract.portfolio.savings.DepositsApiConstants;
|
||||||
import org.apache.fineract.portfolio.savings.data.FdInterestRateEffectiveData;
|
import org.apache.fineract.portfolio.savings.data.FdInterestRateEffectiveData;
|
||||||
@@ -42,6 +46,7 @@ public class FixedDepositProductsBaseInterestRatesApiResource {
|
|||||||
|
|
||||||
private final PlatformSecurityContext context;
|
private final PlatformSecurityContext context;
|
||||||
private final FdBaseInterestRateReadPlatformService fdBaseInterestRateReadPlatformService;
|
private final FdBaseInterestRateReadPlatformService fdBaseInterestRateReadPlatformService;
|
||||||
|
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
|
||||||
private final Gson gson = new GsonBuilder()
|
private final Gson gson = new GsonBuilder()
|
||||||
.registerTypeAdapter(LocalDate.class,
|
.registerTypeAdapter(LocalDate.class,
|
||||||
(JsonSerializer<LocalDate>) (src, typeOfSrc, ctx) -> new JsonPrimitive(src.toString()))
|
(JsonSerializer<LocalDate>) (src, typeOfSrc, ctx) -> new JsonPrimitive(src.toString()))
|
||||||
@@ -77,11 +82,11 @@ public class FixedDepositProductsBaseInterestRatesApiResource {
|
|||||||
@Produces({ MediaType.APPLICATION_JSON })
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
@Operation(summary = "Save new FD base interest rate version", description = "Creates a new FD CBS base chart version for the given fdType and deactivates the previous active version.")
|
@Operation(summary = "Save new FD base interest rate version", description = "Creates a new FD CBS base chart version for the given fdType and deactivates the previous active version.")
|
||||||
@ApiResponses({ @ApiResponse(responseCode = "200", description = "OK") })
|
@ApiResponses({ @ApiResponse(responseCode = "200", description = "OK") })
|
||||||
public String saveInterestRates(@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
public CommandProcessingResult saveInterestRates(@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||||
this.context.authenticatedUser().validateHasReadPermission(DepositsApiConstants.FIXED_DEPOSIT_PRODUCT_RESOURCE_NAME);
|
final CommandWrapper commandRequest = new CommandWrapperBuilder()
|
||||||
final FdInterestRateSaveRequest request = gson.fromJson(apiRequestBodyAsJson, FdInterestRateSaveRequest.class);
|
.createCbsBaseInterestRate()
|
||||||
final Long createdById = this.context.authenticatedUser().getId();
|
.withJson(apiRequestBodyAsJson)
|
||||||
final FdInterestRateEffectiveData saved = this.fdBaseInterestRateReadPlatformService.saveInterestRates(request, createdById);
|
.build();
|
||||||
return gson.toJson(saved);
|
return this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-65
@@ -97,7 +97,6 @@ import org.apache.fineract.portfolio.savings.SavingsInterestCalculationType;
|
|||||||
import org.apache.fineract.portfolio.savings.SavingsPeriodFrequencyType;
|
import org.apache.fineract.portfolio.savings.SavingsPeriodFrequencyType;
|
||||||
import org.apache.fineract.portfolio.savings.SavingsPostingInterestPeriodType;
|
import org.apache.fineract.portfolio.savings.SavingsPostingInterestPeriodType;
|
||||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
|
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
|
||||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccountRepositoryWrapper;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
@@ -106,14 +105,11 @@ public class DepositAccountDataValidator {
|
|||||||
|
|
||||||
private final FromJsonHelper fromApiJsonHelper;
|
private final FromJsonHelper fromApiJsonHelper;
|
||||||
private final DepositProductDataValidator productDataValidator;
|
private final DepositProductDataValidator productDataValidator;
|
||||||
private final SavingsAccountRepositoryWrapper savingsAccountRepositoryWrapper;
|
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public DepositAccountDataValidator(final FromJsonHelper fromApiJsonHelper, final DepositProductDataValidator productDataValidator,
|
public DepositAccountDataValidator(final FromJsonHelper fromApiJsonHelper, final DepositProductDataValidator productDataValidator) {
|
||||||
final SavingsAccountRepositoryWrapper savingsAccountRepositoryWrapper) {
|
|
||||||
this.fromApiJsonHelper = fromApiJsonHelper;
|
this.fromApiJsonHelper = fromApiJsonHelper;
|
||||||
this.productDataValidator = productDataValidator;
|
this.productDataValidator = productDataValidator;
|
||||||
this.savingsAccountRepositoryWrapper = savingsAccountRepositoryWrapper;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void validateFixedDepositForSubmit(final String json) {
|
public void validateFixedDepositForSubmit(final String json) {
|
||||||
@@ -355,40 +351,10 @@ public class DepositAccountDataValidator {
|
|||||||
baseDataValidator.reset().parameter(maturityInstructionIdParamName).value(depositRolloverId).notNull()
|
baseDataValidator.reset().parameter(maturityInstructionIdParamName).value(depositRolloverId).notNull()
|
||||||
.isOneOfTheseValues(DepositAccountOnClosureType.integerValues());
|
.isOneOfTheseValues(DepositAccountOnClosureType.integerValues());
|
||||||
|
|
||||||
// Withdraw Deposit books the maturity proceeds straight out of the bank's cash/vault GL the
|
|
||||||
// moment the account matures -- before the customer has actually come in to collect it. For
|
|
||||||
// maturities processed automatically by the scheduled job (not a live counter transaction),
|
|
||||||
// that books cash as disbursed while it is still physically in the vault, so this option is
|
|
||||||
// disabled: every account must resolve to Transfer to Savings or a Reinvest option instead.
|
|
||||||
if (depositRolloverId.equals(DepositAccountOnClosureType.WITHDRAW_DEPOSIT.getValue())) {
|
|
||||||
baseDataValidator.reset().parameter(maturityInstructionIdParamName).value(depositRolloverId).failWithCodeAndMessage(
|
|
||||||
"maturity.instruction.withdraw.deposit.not.allowed",
|
|
||||||
"Withdraw Deposit is not a valid maturity instruction -- choose Transfer to Savings or a Reinvest option.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Partial Reinvest needs a specific kept-amount decided at the moment of closure -- there is
|
|
||||||
// nothing sensible to pre-commit to years in advance as a standing instruction, so it is only
|
|
||||||
// ever chosen manually via the Payment action, never set here.
|
|
||||||
if (depositRolloverId.equals(DepositAccountOnClosureType.PARTIAL_REINVEST.getValue())) {
|
|
||||||
baseDataValidator.reset().parameter(maturityInstructionIdParamName).value(depositRolloverId).failWithCodeAndMessage(
|
|
||||||
"maturity.instruction.partial.reinvest.not.allowed",
|
|
||||||
"Partial Re-Invest is not a valid standing maturity instruction -- it can only be chosen manually at closure time.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (depositRolloverId.equals(DepositAccountOnClosureType.TRANSFER_TO_SAVINGS.getValue())) {
|
if (depositRolloverId.equals(DepositAccountOnClosureType.TRANSFER_TO_SAVINGS.getValue())) {
|
||||||
final Long transferToSavingsId = this.fromApiJsonHelper.extractLongNamed(transferToSavingsIdParamName, element);
|
final Long transferToSavingsId = this.fromApiJsonHelper.extractLongNamed(transferToSavingsIdParamName, element);
|
||||||
baseDataValidator.reset().parameter(transferToSavingsIdParamName).value(transferToSavingsId).notNull()
|
baseDataValidator.reset().parameter(transferToSavingsIdParamName).value(transferToSavingsId).notNull()
|
||||||
.longGreaterThanZero();
|
.longGreaterThanZero();
|
||||||
|
|
||||||
// Transfer to Savings can only ever post into a genuine Savings account -- Current accounts
|
|
||||||
// live in a separate ledger/module that this maturity-closure transfer cannot write into.
|
|
||||||
if (transferToSavingsId != null && transferToSavingsId > 0
|
|
||||||
&& !this.savingsAccountRepositoryWrapper.existsWithDepositAccountType(transferToSavingsId,
|
|
||||||
DepositAccountType.SAVINGS_DEPOSIT)) {
|
|
||||||
baseDataValidator.reset().parameter(transferToSavingsIdParamName).value(transferToSavingsId)
|
|
||||||
.failWithCodeAndMessage("transfer.to.savings.id.is.not.a.savings.account",
|
|
||||||
"The selected account is not a Savings account -- Transfer to Savings can only target a Savings account.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -527,40 +493,10 @@ public class DepositAccountDataValidator {
|
|||||||
baseDataValidator.reset().parameter(maturityInstructionIdParamName).value(depositRolloverId).notNull()
|
baseDataValidator.reset().parameter(maturityInstructionIdParamName).value(depositRolloverId).notNull()
|
||||||
.isOneOfTheseValues(DepositAccountOnClosureType.integerValues());
|
.isOneOfTheseValues(DepositAccountOnClosureType.integerValues());
|
||||||
|
|
||||||
// Withdraw Deposit books the maturity proceeds straight out of the bank's cash/vault GL the
|
|
||||||
// moment the account matures -- before the customer has actually come in to collect it. For
|
|
||||||
// maturities processed automatically by the scheduled job (not a live counter transaction),
|
|
||||||
// that books cash as disbursed while it is still physically in the vault, so this option is
|
|
||||||
// disabled: every account must resolve to Transfer to Savings or a Reinvest option instead.
|
|
||||||
if (depositRolloverId.equals(DepositAccountOnClosureType.WITHDRAW_DEPOSIT.getValue())) {
|
|
||||||
baseDataValidator.reset().parameter(maturityInstructionIdParamName).value(depositRolloverId).failWithCodeAndMessage(
|
|
||||||
"maturity.instruction.withdraw.deposit.not.allowed",
|
|
||||||
"Withdraw Deposit is not a valid maturity instruction -- choose Transfer to Savings or a Reinvest option.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Partial Reinvest needs a specific kept-amount decided at the moment of closure -- there is
|
|
||||||
// nothing sensible to pre-commit to years in advance as a standing instruction, so it is only
|
|
||||||
// ever chosen manually via the Payment action, never set here.
|
|
||||||
if (depositRolloverId.equals(DepositAccountOnClosureType.PARTIAL_REINVEST.getValue())) {
|
|
||||||
baseDataValidator.reset().parameter(maturityInstructionIdParamName).value(depositRolloverId).failWithCodeAndMessage(
|
|
||||||
"maturity.instruction.partial.reinvest.not.allowed",
|
|
||||||
"Partial Re-Invest is not a valid standing maturity instruction -- it can only be chosen manually at closure time.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (depositRolloverId.equals(DepositAccountOnClosureType.TRANSFER_TO_SAVINGS.getValue())) {
|
if (depositRolloverId.equals(DepositAccountOnClosureType.TRANSFER_TO_SAVINGS.getValue())) {
|
||||||
final Long transferToSavingsId = this.fromApiJsonHelper.extractLongNamed(transferToSavingsIdParamName, element);
|
final Long transferToSavingsId = this.fromApiJsonHelper.extractLongNamed(transferToSavingsIdParamName, element);
|
||||||
baseDataValidator.reset().parameter(transferToSavingsIdParamName).value(transferToSavingsId).notNull()
|
baseDataValidator.reset().parameter(transferToSavingsIdParamName).value(transferToSavingsId).notNull()
|
||||||
.longGreaterThanZero();
|
.longGreaterThanZero();
|
||||||
|
|
||||||
// Transfer to Savings can only ever post into a genuine Savings account -- Current accounts
|
|
||||||
// live in a separate ledger/module that this maturity-closure transfer cannot write into.
|
|
||||||
if (transferToSavingsId != null && transferToSavingsId > 0
|
|
||||||
&& !this.savingsAccountRepositoryWrapper.existsWithDepositAccountType(transferToSavingsId,
|
|
||||||
DepositAccountType.SAVINGS_DEPOSIT)) {
|
|
||||||
baseDataValidator.reset().parameter(transferToSavingsIdParamName).value(transferToSavingsId)
|
|
||||||
.failWithCodeAndMessage("transfer.to.savings.id.is.not.a.savings.account",
|
|
||||||
"The selected account is not a Savings account -- Transfer to Savings can only target a Savings account.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-83
@@ -82,13 +82,6 @@ public class DepositAccountTransactionDataValidator {
|
|||||||
private static final Set<String> DEPOSIT_ACCOUNT_PRE_MATURE_CALCULATION_REQUEST_DATA_PARAMETERS = new HashSet<>(
|
private static final Set<String> DEPOSIT_ACCOUNT_PRE_MATURE_CALCULATION_REQUEST_DATA_PARAMETERS = new HashSet<>(
|
||||||
Arrays.asList(DepositsApiConstants.localeParamName, DepositsApiConstants.dateFormatParamName, closedOnDateParamName));
|
Arrays.asList(DepositsApiConstants.localeParamName, DepositsApiConstants.dateFormatParamName, closedOnDateParamName));
|
||||||
|
|
||||||
private static final Set<String> DEPOSIT_ACCOUNT_PAYMENT_REQUEST_DATA_PARAMETERS = new HashSet<>(
|
|
||||||
Arrays.asList(DepositsApiConstants.localeParamName, DepositsApiConstants.dateFormatParamName, closedOnDateParamName,
|
|
||||||
DepositsApiConstants.noteParamName, onAccountClosureIdParamName, paymentTypeIdParamName,
|
|
||||||
transactionAccountNumberParamName, checkNumberParamName, routingCodeParamName, receiptNumberParamName,
|
|
||||||
bankNumberParamName, DepositsApiConstants.transferDescriptionParamName, toSavingsAccountIdParamName,
|
|
||||||
DepositsApiConstants.reinvestAmountParamName));
|
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public DepositAccountTransactionDataValidator(final FromJsonHelper fromApiJsonHelper) {
|
public DepositAccountTransactionDataValidator(final FromJsonHelper fromApiJsonHelper) {
|
||||||
this.fromApiJsonHelper = fromApiJsonHelper;
|
this.fromApiJsonHelper = fromApiJsonHelper;
|
||||||
@@ -229,15 +222,7 @@ public class DepositAccountTransactionDataValidator {
|
|||||||
|
|
||||||
if (onAccountClosureId != null) {
|
if (onAccountClosureId != null) {
|
||||||
final DepositAccountOnClosureType accountOnClosureType = DepositAccountOnClosureType.fromInt(onAccountClosureId);
|
final DepositAccountOnClosureType accountOnClosureType = DepositAccountOnClosureType.fromInt(onAccountClosureId);
|
||||||
if (accountOnClosureType.isPartialReinvest()) {
|
if (accountOnClosureType.isTransferToSavings()) {
|
||||||
// Partial Re-Invest needs a kept-amount and a destination account that only the Payment
|
|
||||||
// command collects, and only its handler implements. Neither the FD nor the RD closure
|
|
||||||
// handler has a branch for it, so without this it would fall through to their terminal
|
|
||||||
// "withdraw the whole balance" case and silently pay the deposit out in cash.
|
|
||||||
baseDataValidator.reset().parameter(onAccountClosureIdParamName).value(onAccountClosureId).failWithCode(
|
|
||||||
"partial.reinvest.not.allowed",
|
|
||||||
"Partial Re-Invest is only supported through the Payment action, not account closure");
|
|
||||||
} else if (accountOnClosureType.isTransferToSavings()) {
|
|
||||||
final Long toSavingsAccountId = this.fromApiJsonHelper.extractLongNamed(toSavingsAccountIdParamName, element);
|
final Long toSavingsAccountId = this.fromApiJsonHelper.extractLongNamed(toSavingsAccountIdParamName, element);
|
||||||
baseDataValidator.reset().parameter(toSavingsAccountIdParamName).value(toSavingsAccountId)
|
baseDataValidator.reset().parameter(toSavingsAccountIdParamName).value(toSavingsAccountId)
|
||||||
.cantBeBlankWhenParameterProvidedIs(onAccountClosureIdParamName,
|
.cantBeBlankWhenParameterProvidedIs(onAccountClosureIdParamName,
|
||||||
@@ -262,73 +247,6 @@ public class DepositAccountTransactionDataValidator {
|
|||||||
throwExceptionIfValidationWarningsExist(dataValidationErrors);
|
throwExceptionIfValidationWarningsExist(dataValidationErrors);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates the FD "Payment" command -- kept independent of {@link #validateClosing}, which stays exactly as-is
|
|
||||||
* for Close/Premature Close, so this never changes either of their existing behavior. Unlike
|
|
||||||
* {@code validateClosing}, reinvest is always allowed here (there is no {@code isPreMatureClose} restriction --
|
|
||||||
* Payment is meant to work on an Active account too), and a new Partial Reinvest closure type needs its own
|
|
||||||
* amount and destination-account rules.
|
|
||||||
*/
|
|
||||||
public void validatePayment(final JsonCommand command) {
|
|
||||||
final String json = command.json();
|
|
||||||
|
|
||||||
if (StringUtils.isBlank(json)) {
|
|
||||||
throw new InvalidJsonException();
|
|
||||||
}
|
|
||||||
|
|
||||||
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
|
|
||||||
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, DEPOSIT_ACCOUNT_PAYMENT_REQUEST_DATA_PARAMETERS);
|
|
||||||
|
|
||||||
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
|
|
||||||
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
|
|
||||||
.resource(DepositAccountType.FIXED_DEPOSIT.resourceName());
|
|
||||||
|
|
||||||
final JsonElement element = command.parsedJson();
|
|
||||||
|
|
||||||
final LocalDate closedOnDate = this.fromApiJsonHelper.extractLocalDateNamed(closedOnDateParamName, element);
|
|
||||||
baseDataValidator.reset().parameter(closedOnDateParamName).value(closedOnDate).notNull();
|
|
||||||
|
|
||||||
final Integer onAccountClosureId = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(onAccountClosureIdParamName, element);
|
|
||||||
baseDataValidator.reset().parameter(onAccountClosureIdParamName).value(onAccountClosureId).notBlank()
|
|
||||||
.isOneOfTheseValues(DepositAccountOnClosureType.integerValues());
|
|
||||||
|
|
||||||
if (onAccountClosureId != null) {
|
|
||||||
final DepositAccountOnClosureType accountOnClosureType = DepositAccountOnClosureType.fromInt(onAccountClosureId);
|
|
||||||
if (accountOnClosureType.isWithdarwDeposit()) {
|
|
||||||
baseDataValidator.reset().parameter(onAccountClosureIdParamName).value(onAccountClosureId).failWithCodeAndMessage(
|
|
||||||
"withdraw.deposit.not.allowed.for.payment",
|
|
||||||
"Withdraw Deposit is not supported via Payment -- choose Transfer to Savings or a Reinvest option.");
|
|
||||||
}
|
|
||||||
// Interest earned under Reinvest Principal Only has to leave the deposit somewhere -- the same
|
|
||||||
// destination-account rule Transfer to Savings and Partial Reinvest already need.
|
|
||||||
if (accountOnClosureType.isTransferToSavings() || accountOnClosureType.isPartialReinvest()
|
|
||||||
|| accountOnClosureType.isReinvestPrincipal()) {
|
|
||||||
final Long toSavingsAccountId = this.fromApiJsonHelper.extractLongNamed(toSavingsAccountIdParamName, element);
|
|
||||||
baseDataValidator.reset().parameter(toSavingsAccountIdParamName).value(toSavingsAccountId)
|
|
||||||
.cantBeBlankWhenParameterProvidedIs(onAccountClosureIdParamName, accountOnClosureType.getCode());
|
|
||||||
}
|
|
||||||
if (accountOnClosureType.isPartialReinvest()) {
|
|
||||||
final BigDecimal reinvestAmount = this.fromApiJsonHelper
|
|
||||||
.extractBigDecimalWithLocaleNamed(DepositsApiConstants.reinvestAmountParamName, element);
|
|
||||||
baseDataValidator.reset().parameter(DepositsApiConstants.reinvestAmountParamName).value(reinvestAmount).notNull()
|
|
||||||
.positiveAmount();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate all string payment detail fields for max length
|
|
||||||
final Integer paymentTypeId = this.fromApiJsonHelper.extractIntegerWithLocaleNamed(paymentTypeIdParamName, element);
|
|
||||||
baseDataValidator.reset().parameter(paymentTypeIdParamName).value(paymentTypeId).ignoreIfNull().integerGreaterThanZero();
|
|
||||||
final Set<String> paymentDetailParameters = new HashSet<>(Arrays.asList(transactionAccountNumberParamName, checkNumberParamName,
|
|
||||||
routingCodeParamName, receiptNumberParamName, bankNumberParamName));
|
|
||||||
for (final String paymentDetailParameterName : paymentDetailParameters) {
|
|
||||||
final String paymentDetailParameterValue = this.fromApiJsonHelper.extractStringNamed(paymentDetailParameterName, element);
|
|
||||||
baseDataValidator.reset().parameter(paymentDetailParameterName).value(paymentDetailParameterValue).ignoreIfNull()
|
|
||||||
.notExceedingLengthOf(50);
|
|
||||||
}
|
|
||||||
|
|
||||||
throwExceptionIfValidationWarningsExist(dataValidationErrors);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void throwExceptionIfValidationWarningsExist(final List<ApiParameterError> dataValidationErrors) {
|
private void throwExceptionIfValidationWarningsExist(final List<ApiParameterError> dataValidationErrors) {
|
||||||
if (!dataValidationErrors.isEmpty()) {
|
if (!dataValidationErrors.isEmpty()) {
|
||||||
//
|
//
|
||||||
|
|||||||
+1
-29
@@ -87,7 +87,6 @@ import org.apache.fineract.portfolio.group.exception.CenterNotActiveException;
|
|||||||
import org.apache.fineract.portfolio.group.exception.ClientNotInGroupException;
|
import org.apache.fineract.portfolio.group.exception.ClientNotInGroupException;
|
||||||
import org.apache.fineract.portfolio.group.exception.GroupNotActiveException;
|
import org.apache.fineract.portfolio.group.exception.GroupNotActiveException;
|
||||||
import org.apache.fineract.portfolio.interestratechart.domain.InterestRateChart;
|
import org.apache.fineract.portfolio.interestratechart.domain.InterestRateChart;
|
||||||
import org.apache.fineract.portfolio.interestratechart.domain.InterestRateChartFields;
|
|
||||||
import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail;
|
import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail;
|
||||||
import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetailAssembler;
|
import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetailAssembler;
|
||||||
import org.apache.fineract.portfolio.savings.DepositAccountOnClosureType;
|
import org.apache.fineract.portfolio.savings.DepositAccountOnClosureType;
|
||||||
@@ -98,11 +97,9 @@ import org.apache.fineract.portfolio.savings.SavingsInterestCalculationType;
|
|||||||
import org.apache.fineract.portfolio.savings.SavingsPeriodFrequencyType;
|
import org.apache.fineract.portfolio.savings.SavingsPeriodFrequencyType;
|
||||||
import org.apache.fineract.portfolio.savings.SavingsPostingInterestPeriodType;
|
import org.apache.fineract.portfolio.savings.SavingsPostingInterestPeriodType;
|
||||||
import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionDTO;
|
import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionDTO;
|
||||||
import org.apache.fineract.portfolio.savings.data.FdInterestRateEffectiveData;
|
|
||||||
import org.apache.fineract.portfolio.savings.exception.FixedDepositProductNotFoundException;
|
import org.apache.fineract.portfolio.savings.exception.FixedDepositProductNotFoundException;
|
||||||
import org.apache.fineract.portfolio.savings.exception.RecurringDepositProductNotFoundException;
|
import org.apache.fineract.portfolio.savings.exception.RecurringDepositProductNotFoundException;
|
||||||
import org.apache.fineract.portfolio.savings.exception.SavingsProductNotFoundException;
|
import org.apache.fineract.portfolio.savings.exception.SavingsProductNotFoundException;
|
||||||
import org.apache.fineract.portfolio.savings.service.FdBaseInterestRateReadPlatformService;
|
|
||||||
import org.apache.fineract.useradministration.domain.AppUser;
|
import org.apache.fineract.useradministration.domain.AppUser;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@@ -126,7 +123,6 @@ public class DepositAccountAssembler {
|
|||||||
|
|
||||||
private final ExternalIdFactory externalIdFactory;
|
private final ExternalIdFactory externalIdFactory;
|
||||||
private final SavingsAccountAssembler savingsAccountAssembler;
|
private final SavingsAccountAssembler savingsAccountAssembler;
|
||||||
private final FdBaseInterestRateReadPlatformService fdBaseInterestRateReadPlatformService;
|
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public DepositAccountAssembler(final SavingsAccountTransactionSummaryWrapper savingsAccountTransactionSummaryWrapper,
|
public DepositAccountAssembler(final SavingsAccountTransactionSummaryWrapper savingsAccountTransactionSummaryWrapper,
|
||||||
@@ -138,8 +134,7 @@ public class DepositAccountAssembler {
|
|||||||
final RecurringDepositProductRepository recurringDepositProductRepository,
|
final RecurringDepositProductRepository recurringDepositProductRepository,
|
||||||
final AccountTransfersReadPlatformService accountTransfersReadPlatformService, final PlatformSecurityContext context,
|
final AccountTransfersReadPlatformService accountTransfersReadPlatformService, final PlatformSecurityContext context,
|
||||||
final PaymentDetailAssembler paymentDetailAssembler, ExternalIdFactory externalIdFactory,
|
final PaymentDetailAssembler paymentDetailAssembler, ExternalIdFactory externalIdFactory,
|
||||||
final SavingsAccountAssembler savingsAccountAssembler,
|
final SavingsAccountAssembler savingsAccountAssembler) {
|
||||||
final FdBaseInterestRateReadPlatformService fdBaseInterestRateReadPlatformService) {
|
|
||||||
|
|
||||||
this.savingsAccountTransactionSummaryWrapper = savingsAccountTransactionSummaryWrapper;
|
this.savingsAccountTransactionSummaryWrapper = savingsAccountTransactionSummaryWrapper;
|
||||||
this.clientRepository = clientRepository;
|
this.clientRepository = clientRepository;
|
||||||
@@ -156,7 +151,6 @@ public class DepositAccountAssembler {
|
|||||||
this.paymentDetailAssembler = paymentDetailAssembler;
|
this.paymentDetailAssembler = paymentDetailAssembler;
|
||||||
this.externalIdFactory = externalIdFactory;
|
this.externalIdFactory = externalIdFactory;
|
||||||
this.savingsAccountAssembler = savingsAccountAssembler;
|
this.savingsAccountAssembler = savingsAccountAssembler;
|
||||||
this.fdBaseInterestRateReadPlatformService = fdBaseInterestRateReadPlatformService;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -328,28 +322,6 @@ public class DepositAccountAssembler {
|
|||||||
accountChart = DepositAccountInterestRateChart.from(productChart);
|
accountChart = DepositAccountInterestRateChart.from(productChart);
|
||||||
}
|
}
|
||||||
|
|
||||||
// A new FD priced from its product's own chart never carries a senior-citizen premium -- that
|
|
||||||
// concept exists only on the CBS Base Interest Rate master. So whenever staff hasn't picked a
|
|
||||||
// specific chart, price the account from that master's currently active version instead (the same
|
|
||||||
// one the Base Interest Rates screen shows and renewals already use), so a senior-citizen FD shows
|
|
||||||
// its premium from day one rather than only after its first renewal. RD is unaffected: this only
|
|
||||||
// runs when depositAccountType.isFixedDeposit(), and it's a no-op wherever no active base-rate
|
|
||||||
// version exists for the product's FD type, in which case the product's own chart above stands.
|
|
||||||
if (depositAccountType.isFixedDeposit() && accountChart != null && !command.parameterExists(chartIdParamName)
|
|
||||||
&& product instanceof FixedDepositProduct) {
|
|
||||||
final String fdType = ((FixedDepositProduct) product).getFdType();
|
|
||||||
if (fdType != null) {
|
|
||||||
final FdInterestRateEffectiveData activeBaseRates = this.fdBaseInterestRateReadPlatformService
|
|
||||||
.retrieveActiveInterestRatesByType(fdType).stream().findFirst().orElse(null);
|
|
||||||
if (activeBaseRates != null && activeBaseRates.getSlabs() != null && !activeBaseRates.getSlabs().isEmpty()) {
|
|
||||||
final InterestRateChartFields baseChartFields = DepositAccountDomainServiceJpa.baseChartFieldsOf(activeBaseRates);
|
|
||||||
final Collection<DepositAccountInterestRateChart.SlabSnapshot> baseChartSlabs = DepositAccountDomainServiceJpa
|
|
||||||
.baseChartSlabsOf(activeBaseRates, product.currency().getCode());
|
|
||||||
accountChart.refreshFromSlabSnapshots(baseChartFields, baseChartSlabs);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean withHoldTax = product.withHoldTax();
|
boolean withHoldTax = product.withHoldTax();
|
||||||
if (command.parameterExists(withHoldTaxParamName)) {
|
if (command.parameterExists(withHoldTaxParamName)) {
|
||||||
withHoldTax = command.booleanPrimitiveValueOfParameterNamed(withHoldTaxParamName);
|
withHoldTax = command.booleanPrimitiveValueOfParameterNamed(withHoldTaxParamName);
|
||||||
|
|||||||
-11
@@ -35,14 +35,6 @@ public interface DepositAccountDomainService {
|
|||||||
SavingsAccountTransaction handleFDDeposit(FixedDepositAccount account, DateTimeFormatter fmt, LocalDate transactionDate,
|
SavingsAccountTransaction handleFDDeposit(FixedDepositAccount account, DateTimeFormatter fmt, LocalDate transactionDate,
|
||||||
BigDecimal transactionAmount, PaymentDetail paymentDetail);
|
BigDecimal transactionAmount, PaymentDetail paymentDetail);
|
||||||
|
|
||||||
/**
|
|
||||||
* As above, but lets the caller declare the deposit as an internal account-to-account movement rather
|
|
||||||
* than cash taken over the counter. Used by reinvestment on maturity, where the funds come straight
|
|
||||||
* from the matured deposit and must post against the transfer-suspense GL instead of cash/vault.
|
|
||||||
*/
|
|
||||||
SavingsAccountTransaction handleFDDeposit(FixedDepositAccount account, DateTimeFormatter fmt, LocalDate transactionDate,
|
|
||||||
BigDecimal transactionAmount, PaymentDetail paymentDetail, boolean isAccountTransfer);
|
|
||||||
|
|
||||||
SavingsAccountTransaction handleRDDeposit(RecurringDepositAccount account, DateTimeFormatter fmt, LocalDate transactionDate,
|
SavingsAccountTransaction handleRDDeposit(RecurringDepositAccount account, DateTimeFormatter fmt, LocalDate transactionDate,
|
||||||
BigDecimal transactionAmount, PaymentDetail paymentDetail, boolean isRegularTransaction);
|
BigDecimal transactionAmount, PaymentDetail paymentDetail, boolean isRegularTransaction);
|
||||||
|
|
||||||
@@ -64,7 +56,4 @@ public interface DepositAccountDomainService {
|
|||||||
|
|
||||||
Long handleRDAccountPreMatureClosure(RecurringDepositAccount account, PaymentDetail paymentDetail, AppUser user, JsonCommand command,
|
Long handleRDAccountPreMatureClosure(RecurringDepositAccount account, PaymentDetail paymentDetail, AppUser user, JsonCommand command,
|
||||||
Map<String, Object> changes);
|
Map<String, Object> changes);
|
||||||
|
|
||||||
Long handleFDAccountPayment(FixedDepositAccount account, PaymentDetail paymentDetail, AppUser user, JsonCommand command,
|
|
||||||
Map<String, Object> changes);
|
|
||||||
}
|
}
|
||||||
|
|||||||
+24
-629
@@ -19,7 +19,6 @@
|
|||||||
package org.apache.fineract.portfolio.savings.domain;
|
package org.apache.fineract.portfolio.savings.domain;
|
||||||
|
|
||||||
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.onAccountClosureIdParamName;
|
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.onAccountClosureIdParamName;
|
||||||
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.reinvestAmountParamName;
|
|
||||||
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.toSavingsAccountIdParamName;
|
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.toSavingsAccountIdParamName;
|
||||||
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.transferDescriptionParamName;
|
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.transferDescriptionParamName;
|
||||||
|
|
||||||
@@ -27,9 +26,6 @@ import java.math.BigDecimal;
|
|||||||
import java.math.MathContext;
|
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.Collection;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
@@ -59,18 +55,12 @@ import org.apache.fineract.portfolio.calendar.domain.CalendarInstanceRepository;
|
|||||||
import org.apache.fineract.portfolio.calendar.domain.CalendarType;
|
import org.apache.fineract.portfolio.calendar.domain.CalendarType;
|
||||||
import org.apache.fineract.portfolio.calendar.service.CalendarUtils;
|
import org.apache.fineract.portfolio.calendar.service.CalendarUtils;
|
||||||
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
|
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
|
||||||
import org.apache.fineract.portfolio.interestratechart.domain.InterestRateChartFields;
|
|
||||||
import org.apache.fineract.portfolio.interestratechart.domain.InterestRateChartSlabFields;
|
|
||||||
import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail;
|
import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail;
|
||||||
import org.apache.fineract.portfolio.savings.DepositAccountOnClosureType;
|
import org.apache.fineract.portfolio.savings.DepositAccountOnClosureType;
|
||||||
import org.apache.fineract.portfolio.savings.DepositAccountType;
|
import org.apache.fineract.portfolio.savings.DepositAccountType;
|
||||||
import org.apache.fineract.portfolio.savings.DepositsApiConstants;
|
import org.apache.fineract.portfolio.savings.DepositsApiConstants;
|
||||||
import org.apache.fineract.portfolio.savings.SavingsApiConstants;
|
import org.apache.fineract.portfolio.savings.SavingsApiConstants;
|
||||||
import org.apache.fineract.portfolio.savings.SavingsPeriodFrequencyType;
|
|
||||||
import org.apache.fineract.portfolio.savings.SavingsTransactionBooleanValues;
|
import org.apache.fineract.portfolio.savings.SavingsTransactionBooleanValues;
|
||||||
import org.apache.fineract.portfolio.savings.data.FdInterestRateEffectiveData;
|
|
||||||
import org.apache.fineract.portfolio.savings.data.FdInterestRateSlabData;
|
|
||||||
import org.apache.fineract.portfolio.savings.service.FdBaseInterestRateReadPlatformService;
|
|
||||||
import org.apache.fineract.portfolio.savings.service.SavingsAccountDomainService;
|
import org.apache.fineract.portfolio.savings.service.SavingsAccountDomainService;
|
||||||
import org.apache.fineract.useradministration.domain.AppUser;
|
import org.apache.fineract.useradministration.domain.AppUser;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@@ -90,28 +80,13 @@ public class DepositAccountDomainServiceJpa implements DepositAccountDomainServi
|
|||||||
private final AccountNumberFormatRepositoryWrapper accountNumberFormatRepository;
|
private final AccountNumberFormatRepositoryWrapper accountNumberFormatRepository;
|
||||||
private final CalendarInstanceRepository calendarInstanceRepository;
|
private final CalendarInstanceRepository calendarInstanceRepository;
|
||||||
private final ExternalIdFactory externalIdFactory;
|
private final ExternalIdFactory externalIdFactory;
|
||||||
private final FixedDepositRenewalHistoryRepository fixedDepositRenewalHistoryRepository;
|
|
||||||
private final FixedDepositRenewalChartSlabHistoryRepository fixedDepositRenewalChartSlabHistoryRepository;
|
|
||||||
private final FdBaseInterestRateReadPlatformService fdBaseInterestRateReadPlatformService;
|
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
@Override
|
@Override
|
||||||
public SavingsAccountTransaction handleWithdrawal(final SavingsAccount account, final DateTimeFormatter fmt,
|
public SavingsAccountTransaction handleWithdrawal(final SavingsAccount account, final DateTimeFormatter fmt,
|
||||||
final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail,
|
final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail,
|
||||||
final boolean applyWithdrawFee, final boolean isRegularTransaction) {
|
final boolean applyWithdrawFee, final boolean isRegularTransaction) {
|
||||||
final boolean isAccountTransfer = false;
|
boolean isAccountTransfer = false;
|
||||||
return handleWithdrawal(account, fmt, transactionDate, transactionAmount, paymentDetail, applyWithdrawFee, isRegularTransaction,
|
|
||||||
isAccountTransfer);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* As above, but lets the caller declare the withdrawal as an internal account-to-account movement
|
|
||||||
* rather than cash paid out over the counter. Used by reinvestment on maturity, where the balance
|
|
||||||
* moves straight into the new deposit and must post against the transfer-suspense GL, not cash/vault.
|
|
||||||
*/
|
|
||||||
private SavingsAccountTransaction handleWithdrawal(final SavingsAccount account, final DateTimeFormatter fmt,
|
|
||||||
final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail,
|
|
||||||
final boolean applyWithdrawFee, final boolean isRegularTransaction, final boolean isAccountTransfer) {
|
|
||||||
boolean isInterestTransfer = false;
|
boolean isInterestTransfer = false;
|
||||||
boolean isWithdrawBalance = false;
|
boolean isWithdrawBalance = false;
|
||||||
final boolean backdatedTxnsAllowedTill = false;
|
final boolean backdatedTxnsAllowedTill = false;
|
||||||
@@ -125,15 +100,7 @@ public class DepositAccountDomainServiceJpa implements DepositAccountDomainServi
|
|||||||
@Override
|
@Override
|
||||||
public SavingsAccountTransaction handleFDDeposit(final FixedDepositAccount account, final DateTimeFormatter fmt,
|
public SavingsAccountTransaction handleFDDeposit(final FixedDepositAccount account, final DateTimeFormatter fmt,
|
||||||
final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail) {
|
final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail) {
|
||||||
final boolean isAccountTransfer = false;
|
boolean isAccountTransfer = false;
|
||||||
return handleFDDeposit(account, fmt, transactionDate, transactionAmount, paymentDetail, isAccountTransfer);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Transactional
|
|
||||||
@Override
|
|
||||||
public SavingsAccountTransaction handleFDDeposit(final FixedDepositAccount account, final DateTimeFormatter fmt,
|
|
||||||
final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail,
|
|
||||||
final boolean isAccountTransfer) {
|
|
||||||
boolean isRegularTransaction = false;
|
boolean isRegularTransaction = false;
|
||||||
final boolean backdatedTxnsAllowedTill = false;
|
final boolean backdatedTxnsAllowedTill = false;
|
||||||
return this.savingsAccountDomainService.handleDeposit(account, fmt, transactionDate, transactionAmount, paymentDetail,
|
return this.savingsAccountDomainService.handleDeposit(account, fmt, transactionDate, transactionAmount, paymentDetail,
|
||||||
@@ -233,26 +200,16 @@ public class DepositAccountDomainServiceJpa implements DepositAccountDomainServi
|
|||||||
final Integer onAccountClosureId = command.integerValueOfParameterNamed(onAccountClosureIdParamName);
|
final Integer onAccountClosureId = command.integerValueOfParameterNamed(onAccountClosureIdParamName);
|
||||||
final DepositAccountOnClosureType onClosureType = DepositAccountOnClosureType.fromInt(onAccountClosureId);
|
final DepositAccountOnClosureType onClosureType = DepositAccountOnClosureType.fromInt(onAccountClosureId);
|
||||||
if (onClosureType.isReinvest()) {
|
if (onClosureType.isReinvest()) {
|
||||||
// Captured before the withdrawal below empties the account, so the renewal history can
|
|
||||||
// record what this term actually matured at.
|
|
||||||
final BigDecimal maturedBalance = account.getAccountBalance();
|
|
||||||
ExternalId externalId = this.externalIdFactory.create();
|
ExternalId externalId = this.externalIdFactory.create();
|
||||||
FixedDepositAccount reinvestedDeposit = account.reInvest(maturedBalance, externalId);
|
FixedDepositAccount reinvestedDeposit = account.reInvest(account.getAccountBalance(), externalId);
|
||||||
this.depositAccountAssembler.assignSavingAccountHelpers(reinvestedDeposit);
|
this.depositAccountAssembler.assignSavingAccountHelpers(reinvestedDeposit);
|
||||||
reinvestedDeposit.updateMaturityDateAndAmountBeforeAccountActivation(mc, isPreMatureClosure,
|
reinvestedDeposit.updateMaturityDateAndAmountBeforeAccountActivation(mc, isPreMatureClosure,
|
||||||
isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth);
|
isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth);
|
||||||
this.savingsAccountRepository.saveAndFlush(reinvestedDeposit);
|
this.savingsAccountRepository.saveAndFlush(reinvestedDeposit);
|
||||||
autoGenerateAccountNumber(reinvestedDeposit);
|
autoGenerateAccountNumber(reinvestedDeposit);
|
||||||
// Renewal is an internal movement into the new deposit, not cash paid out -- post it
|
final SavingsAccountTransaction withdrawal = this.handleWithdrawal(account, fmt, closedDate, account.getAccountBalance(),
|
||||||
// against the transfer-suspense GL rather than crediting cash/vault.
|
paymentDetail, false, isRegularTransaction);
|
||||||
final boolean isInternalTransferLeg = true;
|
|
||||||
final SavingsAccountTransaction withdrawal = this.handleWithdrawal(account, fmt, closedDate, maturedBalance, paymentDetail,
|
|
||||||
false, isRegularTransaction, isInternalTransferLeg);
|
|
||||||
savingsTransactionId = withdrawal.getId();
|
savingsTransactionId = withdrawal.getId();
|
||||||
// Reached when a user closes the account with a re-invest instruction, rather than the
|
|
||||||
// maturity job acting on a standing instruction.
|
|
||||||
recordRenewal(account, reinvestedDeposit, maturedBalance, maturedBalance, closedDate,
|
|
||||||
FixedDepositRenewalHistory.RENEWAL_TYPE_MANUAL, user);
|
|
||||||
} else if (onClosureType.isTransferToSavings()) {
|
} else if (onClosureType.isTransferToSavings()) {
|
||||||
final Long toSavingsId = command.longValueOfParameterNamed(toSavingsAccountIdParamName);
|
final Long toSavingsId = command.longValueOfParameterNamed(toSavingsAccountIdParamName);
|
||||||
final String transferDescription = command.stringValueOfParameterNamed(transferDescriptionParamName);
|
final String transferDescription = command.stringValueOfParameterNamed(transferDescriptionParamName);
|
||||||
@@ -303,58 +260,13 @@ public class DepositAccountDomainServiceJpa implements DepositAccountDomainServi
|
|||||||
account.postMaturityInterest(isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth);
|
account.postMaturityInterest(isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth);
|
||||||
account.setClosedOnDate(closedDate);
|
account.setClosedOnDate(closedDate);
|
||||||
final DepositAccountOnClosureType onClosureType = DepositAccountOnClosureType.fromInt(onAccountClosureId);
|
final DepositAccountOnClosureType onClosureType = DepositAccountOnClosureType.fromInt(onAccountClosureId);
|
||||||
if (onClosureType.isReinvestPrincipalAndInterest()) {
|
if (onClosureType.isReinvest()) {
|
||||||
// Renews on the SAME account, so the customer keeps one account number however many times
|
BigDecimal reInvestAmount;
|
||||||
// the deposit rolls over. Nothing moves: the whole matured balance simply becomes the new
|
if (onClosureType.isReinvestPrincipal()) {
|
||||||
// term's principal, so there is no withdrawal, no deposit and no journal entry for the
|
reInvestAmount = account.getDepositAmount();
|
||||||
// rollover itself -- which also removes the transfer-suspense round trip the old
|
} else {
|
||||||
// close-and-reopen flow needed.
|
reInvestAmount = account.getAccountBalance();
|
||||||
final BigDecimal closingPrincipal = account.getDepositAmount();
|
}
|
||||||
final BigDecimal maturedBalance = account.getAccountBalance();
|
|
||||||
final BigDecimal interestEarned = maturedBalance == null || closingPrincipal == null ? null
|
|
||||||
: maturedBalance.subtract(closingPrincipal);
|
|
||||||
|
|
||||||
// The active CBS base interest-rate version for this FD type: the same rate card the Base
|
|
||||||
// Interest Rates screen shows and the account-opening wizard prices from. Used for both the
|
|
||||||
// new term's slabs and its senior-citizen premium, so screen and account agree.
|
|
||||||
final FdInterestRateEffectiveData activeBaseRates = account.fdType() == null ? null
|
|
||||||
: this.fdBaseInterestRateReadPlatformService.retrieveActiveInterestRatesByType(account.fdType()).stream().findFirst()
|
|
||||||
.orElse(null);
|
|
||||||
|
|
||||||
final BigDecimal currentSeniorCitizenRate = account.isSeniorCitizen()
|
|
||||||
? seniorCitizenRateFor(activeBaseRates, account.depositPeriod(), account.depositPeriodFrequency())
|
|
||||||
: null;
|
|
||||||
|
|
||||||
// Captured before renewInPlace overwrites the account's chart, so the closing term's full
|
|
||||||
// rate-by-tenure chart (including its senior-citizen premiums) can be archived rather than lost
|
|
||||||
// -- renewInPlace mutates the same chart row every time, it does not create a new one.
|
|
||||||
final String closingChartName = account.currentChartName();
|
|
||||||
final List<DepositAccountInterestRateChart.SlabSnapshot> closingChartSlabs = account.currentChartSlabFields();
|
|
||||||
// Likewise captured before renewInPlace overwrites the account's own rate/tenure with the new
|
|
||||||
// term's -- needed only if this is the closing term's first-ever history row (see
|
|
||||||
// recordInPlaceRenewal), so that row records what this term actually ran at instead of blank.
|
|
||||||
final BigDecimal closingInterestRate = account.getNominalAnnualInterestRate();
|
|
||||||
final Integer closingTenure = account.depositPeriod();
|
|
||||||
final Integer closingTenureFrequencyEnum = account.depositPeriodFrequency();
|
|
||||||
|
|
||||||
account.renewInPlace(maturedBalance, closedDate, currentSeniorCitizenRate, baseChartFieldsOf(activeBaseRates),
|
|
||||||
baseChartSlabsOf(activeBaseRates, account.getCurrency().getCode()));
|
|
||||||
account.updateMaturityDateAndAmount(mc, isPreMatureClosure, isSavingsInterestPostingAtCurrentPeriodEnd,
|
|
||||||
financialYearBeginningMonth);
|
|
||||||
this.savingsAccountRepository.save(account);
|
|
||||||
|
|
||||||
recordInPlaceRenewal(account, closingPrincipal, interestEarned, maturedBalance, maturedBalance, closedDate,
|
|
||||||
FixedDepositRenewalHistory.RENEWAL_TYPE_AUTO, user, closingChartName, closingChartSlabs, closingInterestRate,
|
|
||||||
closingTenure, closingTenureFrequencyEnum);
|
|
||||||
changes.put("renewedInPlace", Boolean.TRUE);
|
|
||||||
changes.put("renewalCycleNo", account.renewalCycleNo());
|
|
||||||
|
|
||||||
} else if (onClosureType.isReinvest()) {
|
|
||||||
// "Re-invest principal only" still closes and reopens, because only the principal carries
|
|
||||||
// forward and the interest has to leave the deposit -- which needs a payout destination that
|
|
||||||
// an in-place renewal has nowhere to send. Left on the original flow rather than guessing.
|
|
||||||
final BigDecimal maturedBalance = account.getAccountBalance();
|
|
||||||
final BigDecimal reInvestAmount = account.getDepositAmount();
|
|
||||||
ExternalId externalId = this.externalIdFactory.create();
|
ExternalId externalId = this.externalIdFactory.create();
|
||||||
|
|
||||||
FixedDepositAccount reinvestedDeposit = account.reInvest(reInvestAmount, externalId);
|
FixedDepositAccount reinvestedDeposit = account.reInvest(reInvestAmount, externalId);
|
||||||
@@ -364,26 +276,18 @@ public class DepositAccountDomainServiceJpa implements DepositAccountDomainServi
|
|||||||
|
|
||||||
this.savingsAccountRepository.saveAndFlush(reinvestedDeposit);
|
this.savingsAccountRepository.saveAndFlush(reinvestedDeposit);
|
||||||
autoGenerateAccountNumber(reinvestedDeposit);
|
autoGenerateAccountNumber(reinvestedDeposit);
|
||||||
// Renewal moves the balance internally into the new deposit -- no cash leaves the branch --
|
|
||||||
// so this leg is posted as an account transfer (against the transfer-suspense GL) rather
|
|
||||||
// than as a counter withdrawal, which would wrongly credit the cash/vault account.
|
|
||||||
final boolean isInternalTransferLeg = true;
|
|
||||||
final SavingsAccountTransaction withdrawal = this.handleWithdrawal(account, fmt, closedDate, reInvestAmount, paymentDetail,
|
final SavingsAccountTransaction withdrawal = this.handleWithdrawal(account, fmt, closedDate, reInvestAmount, paymentDetail,
|
||||||
false, isRegularTransaction, isInternalTransferLeg);
|
false, isRegularTransaction);
|
||||||
savingsTransactionId = withdrawal.getId();
|
savingsTransactionId = withdrawal.getId();
|
||||||
// handleWithdrawal() posts this withdrawal's own journal entries, so it has to be marked as
|
|
||||||
// already accounted for -- exactly as the transfer-to-savings branch below does -- or the
|
|
||||||
// postJournalEntries() call at the end of this method posts it a second time.
|
|
||||||
updateAlreadyPostedTransactions(existingTransactionIds, account);
|
|
||||||
|
|
||||||
|
if (onClosureType.isReinvestPrincipalAndInterest()) {
|
||||||
|
account.updateClosedStatus();
|
||||||
|
account.updateOnAccountClosureStatus(onClosureType);
|
||||||
|
}
|
||||||
changes.put("reinvestedDepositId", reinvestedDeposit.getId());
|
changes.put("reinvestedDepositId", reinvestedDeposit.getId());
|
||||||
reinvestedDeposit.approveAndActivateApplication(closedDate, user);
|
reinvestedDeposit.approveAndActivateApplication(closedDate, user);
|
||||||
this.savingsAccountRepository.save(reinvestedDeposit);
|
this.savingsAccountRepository.save(reinvestedDeposit);
|
||||||
|
|
||||||
// Reached from the maturity job processing the account's standing maturity instruction.
|
|
||||||
recordRenewal(account, reinvestedDeposit, maturedBalance, reInvestAmount, closedDate,
|
|
||||||
FixedDepositRenewalHistory.RENEWAL_TYPE_AUTO, user);
|
|
||||||
|
|
||||||
} else if (onClosureType.isTransferToSavings()) {
|
} else if (onClosureType.isTransferToSavings()) {
|
||||||
final SavingsAccount toSavingsAccount = this.depositAccountAssembler.assembleFrom(toSavingsId,
|
final SavingsAccount toSavingsAccount = this.depositAccountAssembler.assembleFrom(toSavingsId,
|
||||||
DepositAccountType.SAVINGS_DEPOSIT);
|
DepositAccountType.SAVINGS_DEPOSIT);
|
||||||
@@ -400,7 +304,6 @@ public class DepositAccountDomainServiceJpa implements DepositAccountDomainServi
|
|||||||
final SavingsAccountTransaction withdrawal = this.handleWithdrawal(account, fmt, closedDate, account.getAccountBalance(),
|
final SavingsAccountTransaction withdrawal = this.handleWithdrawal(account, fmt, closedDate, account.getAccountBalance(),
|
||||||
paymentDetail, false, isRegularTransaction);
|
paymentDetail, false, isRegularTransaction);
|
||||||
savingsTransactionId = withdrawal.getId();
|
savingsTransactionId = withdrawal.getId();
|
||||||
updateAlreadyPostedTransactions(existingTransactionIds, account);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// if(!processMaturityInstructionOnly)
|
// if(!processMaturityInstructionOnly)
|
||||||
@@ -526,274 +429,6 @@ public class DepositAccountDomainServiceJpa implements DepositAccountDomainServi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Persists the closing term's full rate-by-tenure chart -- each slab together with its senior-citizen premium --
|
|
||||||
* against its {@link FixedDepositRenewalHistory} row.
|
|
||||||
* <p>
|
|
||||||
* Slabs missing a rate or a from-period are skipped -- they cannot describe a tenure band. Writes nothing when the
|
|
||||||
* closing term had no chart (e.g. a plain flat-rate deposit), rather than persisting an empty, meaningless archive.
|
|
||||||
*/
|
|
||||||
private void archiveChartSlabs(final Long fdRenewalHistoryId, final String chartName,
|
|
||||||
final List<DepositAccountInterestRateChart.SlabSnapshot> snapshots, final String currencyCode) {
|
|
||||||
if (snapshots == null || snapshots.isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (final DepositAccountInterestRateChart.SlabSnapshot snapshot : snapshots) {
|
|
||||||
final InterestRateChartSlabFields fields = snapshot.getFields();
|
|
||||||
if (fields.annualInterestRate() == null || fields.fromPeriod() == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
this.fixedDepositRenewalChartSlabHistoryRepository
|
|
||||||
.save(FixedDepositRenewalChartSlabHistory.create(fdRenewalHistoryId, chartName, fields.description(),
|
|
||||||
fields.periodType(), fields.fromPeriod(), fields.toPeriod(), fields.getAmountRangeFrom(),
|
|
||||||
fields.getAmountRangeTo(), fields.annualInterestRate(), currencyCode, snapshot.getSeniorCitizenRate()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Chart identity for the account snapshot, so it names the base rate version it was priced from. Package-private
|
|
||||||
* and static (no instance state used) so {@link DepositAccountAssembler} can reuse the exact same mapping when a
|
|
||||||
* new FD account is first priced from this master, instead of duplicating the logic.
|
|
||||||
*/
|
|
||||||
static InterestRateChartFields baseChartFieldsOf(final FdInterestRateEffectiveData baseRates) {
|
|
||||||
// name and from_date are NOT NULL on the embeddable. A base version missing either would abort the
|
|
||||||
// whole renewal (or account creation) with a constraint violation, so treat it as "no identity to
|
|
||||||
// apply" instead: refreshFromSlabSnapshots keeps the chart's existing fields when handed null, and
|
|
||||||
// the slabs -- the part that actually prices the deposit -- still get applied.
|
|
||||||
if (baseRates == null || baseRates.getVersionLabel() == null || baseRates.getEffectiveFromDate() == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
final boolean isPrimaryGroupingByAmount = false; // the base rate card bands by tenure, never by amount
|
|
||||||
return InterestRateChartFields.createNew(baseRates.getVersionLabel(), baseRates.getRemarks(), baseRates.getEffectiveFromDate(),
|
|
||||||
baseRates.getEffectiveToDate(), isPrimaryGroupingByAmount);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Translates the base rate card's slabs into the shape an account chart stores, each paired with its senior-citizen
|
|
||||||
* premium so that figure is persisted on the account (and later archived) rather than only ever being looked up live.
|
|
||||||
* <p>
|
|
||||||
* The base master bands purely by tenure, so amount ranges are left unset -- which
|
|
||||||
* {@code InterestRateChartSlabFields.isAmountBetween} then treats as "any amount", exactly the intent. Slabs with no
|
|
||||||
* from-period are dropped, since a slab that does not say where it starts cannot be matched against a tenure.
|
|
||||||
*/
|
|
||||||
static Collection<DepositAccountInterestRateChart.SlabSnapshot> baseChartSlabsOf(final FdInterestRateEffectiveData baseRates,
|
|
||||||
final String currencyCode) {
|
|
||||||
if (baseRates == null || baseRates.getSlabs() == null) {
|
|
||||||
return Collections.emptyList();
|
|
||||||
}
|
|
||||||
final List<DepositAccountInterestRateChart.SlabSnapshot> snapshots = new ArrayList<>();
|
|
||||||
for (final FdInterestRateSlabData slab : baseRates.getSlabs()) {
|
|
||||||
if (slab.getFromPeriod() == null || slab.getAnnualInterestRate() == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
final InterestRateChartSlabFields fields = InterestRateChartSlabFields.createNew(slab.getDescription(),
|
|
||||||
SavingsPeriodFrequencyType.fromInt(slab.getPeriodTypeEnum()), slab.getFromPeriod(), slab.getToPeriod(), null, null,
|
|
||||||
slab.getAnnualInterestRate(), currencyCode);
|
|
||||||
snapshots.add(new DepositAccountInterestRateChart.SlabSnapshot(fields, slab.getSeniorCitizenRate()));
|
|
||||||
}
|
|
||||||
return snapshots;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The senior-citizen premium from the active base rate card for a given tenure.
|
|
||||||
* <p>
|
|
||||||
* The tenure and the slab bounds are converted to a common unit before comparing, so a rate card published in years
|
|
||||||
* still matches a deposit taken for a number of months. Conversion stays inside a unit family -- days/weeks, or
|
|
||||||
* months/years -- because only those are exact; a days-based card is therefore still never matched against a
|
|
||||||
* months-based tenure, since "1 month" is not a fixed number of days and guessing would land the deposit in the
|
|
||||||
* wrong band. Null when no slab covers the tenure or the matching slab carries no premium, which leaves the
|
|
||||||
* account's existing rate untouched rather than silently dropping the depositor's premium.
|
|
||||||
*/
|
|
||||||
private BigDecimal seniorCitizenRateFor(final FdInterestRateEffectiveData baseRates, final Integer tenure,
|
|
||||||
final Integer tenurePeriodTypeEnum) {
|
|
||||||
if (baseRates == null || baseRates.getSlabs() == null || tenure == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
final Integer tenureInBaseUnit = inFamilyBaseUnit(tenure, tenurePeriodTypeEnum);
|
|
||||||
if (tenureInBaseUnit == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
for (final FdInterestRateSlabData slab : baseRates.getSlabs()) {
|
|
||||||
if (!sameUnitFamily(tenurePeriodTypeEnum, slab.getPeriodTypeEnum())) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
final Integer slabFrom = inFamilyBaseUnit(slab.getFromPeriod(), slab.getPeriodTypeEnum());
|
|
||||||
if (slabFrom == null || tenureInBaseUnit < slabFrom) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (slab.getToPeriod() != null) {
|
|
||||||
final Integer slabTo = inFamilyBaseUnit(slab.getToPeriod(), slab.getPeriodTypeEnum());
|
|
||||||
if (slabTo == null || tenureInBaseUnit > slabTo) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
final BigDecimal rate = slab.getSeniorCitizenRate();
|
|
||||||
if (rate != null && rate.compareTo(BigDecimal.ZERO) > 0) {
|
|
||||||
return rate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@code value} restated in the finest unit of its own family -- days for DAYS/WEEKS, months for MONTHS/YEARS -- so
|
|
||||||
* two periods written in different but related units can be compared without losing precision. Null for an unknown
|
|
||||||
* unit. Enum codes are {@link SavingsPeriodFrequencyType}: DAYS(0), WEEKS(1), MONTHS(2), YEARS(3).
|
|
||||||
*/
|
|
||||||
private Integer inFamilyBaseUnit(final Integer value, final Integer periodTypeEnum) {
|
|
||||||
if (value == null || periodTypeEnum == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return switch (SavingsPeriodFrequencyType.fromInt(periodTypeEnum)) {
|
|
||||||
case DAYS, MONTHS -> value;
|
|
||||||
case WEEKS -> value * 7;
|
|
||||||
case YEARS -> value * 12;
|
|
||||||
default -> null;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether two period types convert exactly into one another. Days and weeks do; months and years do. Across those
|
|
||||||
* two families they do not, because a month is not a fixed number of days -- so a days-based rate card is never
|
|
||||||
* matched against a months-based tenure rather than guessing and landing the deposit in the wrong band.
|
|
||||||
*/
|
|
||||||
private boolean sameUnitFamily(final Integer firstPeriodTypeEnum, final Integer secondPeriodTypeEnum) {
|
|
||||||
if (firstPeriodTypeEnum == null || secondPeriodTypeEnum == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return isDayFamily(firstPeriodTypeEnum) == isDayFamily(secondPeriodTypeEnum);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isDayFamily(final Integer periodTypeEnum) {
|
|
||||||
final SavingsPeriodFrequencyType type = SavingsPeriodFrequencyType.fromInt(periodTypeEnum);
|
|
||||||
return type == SavingsPeriodFrequencyType.DAYS || type == SavingsPeriodFrequencyType.WEEKS;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Records a renewal that happened on the SAME account, where there is no second account row to describe the new term.
|
|
||||||
* <p>
|
|
||||||
* Called after {@link FixedDepositAccount#renewInPlace}, which has already advanced the account's cycle number,
|
|
||||||
* principal and maturity date -- so the closing term's figures have to be passed in rather than read back off the
|
|
||||||
* account, where they no longer exist. Every row for one FD shares that FD's single account id and account number;
|
|
||||||
* they are told apart by renewal_no, which is what makes this a one-to-many history of one account.
|
|
||||||
*
|
|
||||||
* @param closingChartName
|
|
||||||
* the name of the chart the closing term was priced on, captured before renewInPlace overwrote it; null if
|
|
||||||
* unavailable
|
|
||||||
* @param closingChartSlabs
|
|
||||||
* the closing term's full rate-by-tenure chart, likewise captured before it was overwritten. Archived
|
|
||||||
* against the closing term's row so its whole rate card remains queryable, not only the single effective
|
|
||||||
* rate {@link FixedDepositRenewalHistory} records.
|
|
||||||
* @param closingInterestRate
|
|
||||||
* the closing term's own {@code nominalAnnualInterestRate}, captured before renewInPlace overwrote it --
|
|
||||||
* needed only when this term has no existing row yet (see below), since the account itself no longer
|
|
||||||
* carries it once renewInPlace has run.
|
|
||||||
* @param closingTenure
|
|
||||||
* the closing term's own deposit period, captured the same way and needed for the same reason.
|
|
||||||
* @param closingTenureFrequencyEnum
|
|
||||||
* the closing term's own deposit period frequency, captured the same way and needed for the same reason.
|
|
||||||
*/
|
|
||||||
private void recordInPlaceRenewal(final FixedDepositAccount account, final BigDecimal closingPrincipal,
|
|
||||||
final BigDecimal closingInterestEarned, final BigDecimal maturedBalance, final BigDecimal renewedAmount,
|
|
||||||
final LocalDate renewedOn, final String renewalType, final AppUser user, final String closingChartName,
|
|
||||||
final List<DepositAccountInterestRateChart.SlabSnapshot> closingChartSlabs, final BigDecimal closingInterestRate,
|
|
||||||
final Integer closingTenure, final Integer closingTenureFrequencyEnum) {
|
|
||||||
|
|
||||||
// The CHAIN's number, not this account's own. They differ for an FD that was renewed under the old
|
|
||||||
// close-and-reopen model before renewing in place: its rows are keyed on the original account's
|
|
||||||
// number, so keying new rows on this account's number would split the history in two and re-create
|
|
||||||
// a duplicate row for the term that already has one. fdAccountNumber() falls back to this account's
|
|
||||||
// own number when there is no chain, which is what the read side resolves to as well.
|
|
||||||
final String fdAccountNumber = account.fdAccountNumber();
|
|
||||||
final int newCycleNo = account.renewalCycleNo();
|
|
||||||
final Long createdById = user == null ? null : user.getId();
|
|
||||||
|
|
||||||
final List<FixedDepositRenewalHistory> existingRows = this.fixedDepositRenewalHistoryRepository
|
|
||||||
.findByFdAccountNumberOrderByRenewalNoAsc(fdAccountNumber);
|
|
||||||
FixedDepositRenewalHistory closingRow = existingRows.stream()
|
|
||||||
.filter(row -> Integer.valueOf(newCycleNo - 1).equals(row.getRenewalNo())).findFirst().orElse(null);
|
|
||||||
|
|
||||||
// The term that just closed has no row when this FD renews for the first time since the history
|
|
||||||
// table existed. Written now, from the figures captured before the rollover, so the history
|
|
||||||
// always begins with the term the customer originally opened -- rate and tenure included, rather
|
|
||||||
// than left blank, since the caller captured them before renewInPlace overwrote the account with
|
|
||||||
// the new term's own values.
|
|
||||||
if (closingRow == null) {
|
|
||||||
closingRow = this.fixedDepositRenewalHistoryRepository.saveAndFlush(FixedDepositRenewalHistory.create(fdAccountNumber,
|
|
||||||
account.getId(), account.getId(), null, null, newCycleNo - 1, closingPrincipal, null, maturedBalance,
|
|
||||||
closingPrincipal, closingInterestRate, account.getActivationDate(), renewedOn, closingTenure,
|
|
||||||
closingTenureFrequencyEnum, renewalType, account.getActivationDate(), createdById));
|
|
||||||
}
|
|
||||||
closingRow.recordOutcome(closingInterestEarned, maturedBalance);
|
|
||||||
closingRow.markMatured();
|
|
||||||
this.fixedDepositRenewalHistoryRepository.save(closingRow);
|
|
||||||
|
|
||||||
archiveChartSlabs(closingRow.getId(), closingChartName, closingChartSlabs, account.getCurrency().getCode());
|
|
||||||
|
|
||||||
// The new term's own interest/maturity stay unset until it closes in turn.
|
|
||||||
this.fixedDepositRenewalHistoryRepository.save(FixedDepositRenewalHistory.create(fdAccountNumber, account.getId(),
|
|
||||||
account.getId(), null, closingRow.getId(), newCycleNo, renewedAmount, null, null, renewedAmount,
|
|
||||||
account.getNominalAnnualInterestRate(), renewedOn, account.maturityDate(), account.depositPeriod(),
|
|
||||||
account.depositPeriodFrequency(), renewalType, renewedOn, createdById));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Records the renewal that started {@code newTerm}, and closes off the previous term's history row.
|
|
||||||
* <p>
|
|
||||||
* Each term of an FD runs as its own deposit instance so it keeps an immutable ledger, which means the rollover event
|
|
||||||
* itself -- what the previous term earned, what carried forward, at what rate, automatically or by whom -- is not
|
|
||||||
* recoverable from the account rows alone. This writes it down explicitly.
|
|
||||||
* <p>
|
|
||||||
* The chain's first term is backfilled here too when it has no row yet: an FD opened before this table existed only
|
|
||||||
* gets discovered at its first renewal, and without its opening row the history would start at "Renewal 1" with no
|
|
||||||
* original.
|
|
||||||
*/
|
|
||||||
private void recordRenewal(final FixedDepositAccount previousTerm, final FixedDepositAccount newTerm,
|
|
||||||
final BigDecimal maturityAmount, final BigDecimal renewedAmount, final LocalDate renewedOn, final String renewalType,
|
|
||||||
final AppUser user) {
|
|
||||||
|
|
||||||
final String fdAccountNumber = previousTerm.fdAccountNumber();
|
|
||||||
final Long originalAccountId = this.fixedDepositRenewalHistoryRepository
|
|
||||||
.findByFdAccountNumberOrderByRenewalNoAsc(fdAccountNumber).stream().findFirst()
|
|
||||||
.map(FixedDepositRenewalHistory::getOriginalSavingsAccountId)
|
|
||||||
.orElseGet(() -> previousTerm.reinvestedFromSavingsId() == null ? previousTerm.getId() : null);
|
|
||||||
|
|
||||||
FixedDepositRenewalHistory previousRow = this.fixedDepositRenewalHistoryRepository
|
|
||||||
.findBySavingsAccountIdOrderByRenewalNoDesc(previousTerm.getId()).stream().findFirst().orElse(null);
|
|
||||||
|
|
||||||
// Backfill the opening term when the chain started before this table existed, so the history
|
|
||||||
// always begins with the account the customer actually opened.
|
|
||||||
if (previousRow == null && originalAccountId != null && originalAccountId.equals(previousTerm.getId())) {
|
|
||||||
previousRow = this.fixedDepositRenewalHistoryRepository.saveAndFlush(FixedDepositRenewalHistory.create(fdAccountNumber,
|
|
||||||
originalAccountId, previousTerm.getId(), null, null, previousTerm.renewalCycleNo(),
|
|
||||||
previousTerm.getDepositAmount(), null, previousTerm.maturityAmount(), previousTerm.getDepositAmount(),
|
|
||||||
previousTerm.getNominalAnnualInterestRate(), previousTerm.getActivationDate(), previousTerm.maturityDate(),
|
|
||||||
previousTerm.depositPeriod(), previousTerm.depositPeriodFrequency(), renewalType, previousTerm.getActivationDate(),
|
|
||||||
user == null ? null : user.getId()));
|
|
||||||
}
|
|
||||||
|
|
||||||
// What the closing term actually earned. maturityAmount is passed in rather than read off the
|
|
||||||
// account because the caller has already withdrawn the balance out of it by this point, which
|
|
||||||
// would make it read as zero. Recorded on the CLOSING term's own row -- every row states its own
|
|
||||||
// term's outcome, so a reader never has to look at the next row to find out how one term did.
|
|
||||||
final BigDecimal previousPrincipal = previousTerm.getDepositAmount();
|
|
||||||
final BigDecimal interestEarned = maturityAmount == null || previousPrincipal == null ? null
|
|
||||||
: maturityAmount.subtract(previousPrincipal);
|
|
||||||
|
|
||||||
if (previousRow != null) {
|
|
||||||
previousRow.recordOutcome(interestEarned, maturityAmount);
|
|
||||||
previousRow.markMatured();
|
|
||||||
this.fixedDepositRenewalHistoryRepository.save(previousRow);
|
|
||||||
}
|
|
||||||
|
|
||||||
// The new term's own interest/maturity stay unset until it closes in turn.
|
|
||||||
this.fixedDepositRenewalHistoryRepository.save(FixedDepositRenewalHistory.create(fdAccountNumber,
|
|
||||||
originalAccountId == null ? previousTerm.getId() : originalAccountId, newTerm.getId(), previousTerm.getId(),
|
|
||||||
previousRow == null ? null : previousRow.getId(), newTerm.renewalCycleNo(), renewedAmount, null, null, renewedAmount,
|
|
||||||
newTerm.getNominalAnnualInterestRate(), renewedOn, newTerm.maturityDate(), newTerm.depositPeriod(),
|
|
||||||
newTerm.depositPeriodFrequency(), renewalType, renewedOn, user == null ? null : user.getId()));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
@Override
|
@Override
|
||||||
public Long handleFDAccountPreMatureClosure(final FixedDepositAccount account, final PaymentDetail paymentDetail, final AppUser user,
|
public Long handleFDAccountPreMatureClosure(final FixedDepositAccount account, final PaymentDetail paymentDetail, final AppUser user,
|
||||||
@@ -839,9 +474,6 @@ public class DepositAccountDomainServiceJpa implements DepositAccountDomainServi
|
|||||||
final SavingsAccountTransaction withdrawal = this.handleWithdrawal(account, fmt, closedDate, account.getAccountBalance(),
|
final SavingsAccountTransaction withdrawal = this.handleWithdrawal(account, fmt, closedDate, account.getAccountBalance(),
|
||||||
paymentDetail, false, isRegularTransaction);
|
paymentDetail, false, isRegularTransaction);
|
||||||
savingsTransactionId = withdrawal.getId();
|
savingsTransactionId = withdrawal.getId();
|
||||||
// handleWithdrawal() posts this withdrawal's journal entries itself, so mark it as already
|
|
||||||
// accounted for or the postJournalEntries() call below posts it a second time.
|
|
||||||
updateAlreadyPostedTransactions(existingTransactionIds, account);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
account.prematureClosure(user, command, changes);
|
account.prematureClosure(user, command, changes);
|
||||||
@@ -852,238 +484,6 @@ public class DepositAccountDomainServiceJpa implements DepositAccountDomainServi
|
|||||||
return savingsTransactionId;
|
return savingsTransactionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* A staff-triggered, always-available alternative to Premature Close (Active-only, no reinvest) and Close
|
|
||||||
* (Matured-only): works on an FD in either state, and adds a Partial Reinvest option neither of those two support
|
|
||||||
* -- part of the matured amount renews on this same account via {@link FixedDepositAccount#renewInPlace}, the rest
|
|
||||||
* transfers out to a savings account picked at the moment of the request. Full reinvest here also goes through
|
|
||||||
* {@code renewInPlace} rather than the legacy close-and-reopen {@link FixedDepositAccount#reInvest}, which is what
|
|
||||||
* the existing manual Close command still uses today.
|
|
||||||
* <p>
|
|
||||||
* Deliberately independent of {@link #handleFDAccountClosure} and {@link #handleFDAccountPreMatureClosure} --
|
|
||||||
* neither of those two methods is touched, so the existing Close/Premature Close buttons keep behaving exactly as
|
|
||||||
* before.
|
|
||||||
*/
|
|
||||||
@Transactional
|
|
||||||
@Override
|
|
||||||
public Long handleFDAccountPayment(final FixedDepositAccount account, final PaymentDetail paymentDetail, final AppUser user,
|
|
||||||
final JsonCommand command, final Map<String, Object> changes) {
|
|
||||||
|
|
||||||
final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService
|
|
||||||
.isSavingsInterestPostingAtCurrentPeriodEnd();
|
|
||||||
final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth();
|
|
||||||
|
|
||||||
boolean isRegularTransaction = false;
|
|
||||||
boolean isAccountTransfer = false;
|
|
||||||
final boolean isPreMatureClosure = false;
|
|
||||||
final Set<Long> existingTransactionIds = new HashSet<>();
|
|
||||||
final Set<Long> existingReversedTransactionIds = new HashSet<>();
|
|
||||||
updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds);
|
|
||||||
|
|
||||||
final MathContext mc = MathContext.DECIMAL64;
|
|
||||||
final Locale locale = command.extractLocale();
|
|
||||||
final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale);
|
|
||||||
final LocalDate closedDate = command.localDateValueOfParameterNamed(SavingsApiConstants.closedOnDateParamName);
|
|
||||||
Long savingsTransactionId = null;
|
|
||||||
|
|
||||||
// account.isMatured() only reflects whether the overnight scheduler has already flipped this
|
|
||||||
// account's status -- not whether the customer actually showed up before or after the maturity
|
|
||||||
// date. A customer walking in ON the maturity date (or any day after) is not premature just
|
|
||||||
// because that job hasn't run yet today, so the real test is the date, not the status flag.
|
|
||||||
if (!account.isMatured()) {
|
|
||||||
if (!DateUtils.isBefore(closedDate, account.maturityDate())) {
|
|
||||||
// Genuinely due, the scheduler just hasn't caught up yet -- do exactly what it would:
|
|
||||||
// flip to Matured and post final interest at the full contracted rate, no penalty.
|
|
||||||
account.updateMaturityStatus(isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth);
|
|
||||||
} else {
|
|
||||||
// The three closing closure types re-check this themselves inside prematureClosure()
|
|
||||||
// below, but the two reinvest branches never call it (they keep the account open on the
|
|
||||||
// same term), so it is enforced once, up front, for every genuinely-premature Payment --
|
|
||||||
// a locked-in FD (e.g. a Tax Saver with its statutory lock-in) cannot be touched early via
|
|
||||||
// this action either.
|
|
||||||
if (account.isLockedOnDate(closedDate)) {
|
|
||||||
throw new GeneralPlatformDomainRuleException("error.msg.fixeddeposit.payment.lockin.period.not.elapsed",
|
|
||||||
"This account is still within its lock-in period and cannot be processed before it elapses.", closedDate);
|
|
||||||
}
|
|
||||||
// Same penalty-aware interest calculation prematureClosure() already relies on, so a
|
|
||||||
// genuinely early Payment settles interest exactly as a Premature Close would.
|
|
||||||
account.postPreMaturityInterest(closedDate, true, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
account.postMaturityInterest(isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Same as the closure and maturity paths do: the legacy reinvest branch's reInvest() reads
|
|
||||||
// getClosedOnDate() to pick the new account's chart and submitted date, so it has to be set
|
|
||||||
// before branching. renewInPlace() clears it again for the branches that keep the account open.
|
|
||||||
account.setClosedOnDate(closedDate);
|
|
||||||
|
|
||||||
// Re-read after the block above: updateMaturityStatus() may just have flipped it, so this now
|
|
||||||
// correctly decides close() (needs Matured) vs prematureClosure() (needs Active) either way.
|
|
||||||
final boolean alreadyMatured = account.isMatured();
|
|
||||||
|
|
||||||
final Integer onAccountClosureId = command.integerValueOfParameterNamed(onAccountClosureIdParamName);
|
|
||||||
final DepositAccountOnClosureType onClosureType = DepositAccountOnClosureType.fromInt(onAccountClosureId);
|
|
||||||
|
|
||||||
if (onClosureType.isReinvestPrincipalAndInterest() || onClosureType.isPartialReinvest()) {
|
|
||||||
// These two branches keep the account open, so they never reach close()/prematureClosure() and
|
|
||||||
// would otherwise accept any date at all -- including one behind already-posted interest, which
|
|
||||||
// the renewal would then turn into a pivot that reverses and re-posts it.
|
|
||||||
account.validateRenewalDate(closedDate);
|
|
||||||
|
|
||||||
final BigDecimal closingPrincipal = account.getDepositAmount();
|
|
||||||
final BigDecimal maturedBalance = account.getAccountBalance();
|
|
||||||
final BigDecimal interestEarned = maturedBalance == null || closingPrincipal == null ? null
|
|
||||||
: maturedBalance.subtract(closingPrincipal);
|
|
||||||
|
|
||||||
final BigDecimal renewedAmount;
|
|
||||||
if (onClosureType.isPartialReinvest()) {
|
|
||||||
renewedAmount = command.bigDecimalValueOfParameterNamed(reinvestAmountParamName);
|
|
||||||
if (renewedAmount == null || renewedAmount.compareTo(BigDecimal.ZERO) <= 0
|
|
||||||
|| renewedAmount.compareTo(maturedBalance) >= 0) {
|
|
||||||
throw new GeneralPlatformDomainRuleException("error.msg.fixeddeposit.partial.reinvest.amount.invalid",
|
|
||||||
"The reinvest amount must be greater than zero and less than the matured balance.", renewedAmount);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
renewedAmount = maturedBalance;
|
|
||||||
}
|
|
||||||
|
|
||||||
final FdInterestRateEffectiveData activeBaseRates = account.fdType() == null ? null
|
|
||||||
: this.fdBaseInterestRateReadPlatformService.retrieveActiveInterestRatesByType(account.fdType()).stream().findFirst()
|
|
||||||
.orElse(null);
|
|
||||||
final BigDecimal currentSeniorCitizenRate = account.isSeniorCitizen()
|
|
||||||
? seniorCitizenRateFor(activeBaseRates, account.depositPeriod(), account.depositPeriodFrequency())
|
|
||||||
: null;
|
|
||||||
|
|
||||||
final String closingChartName = account.currentChartName();
|
|
||||||
final List<DepositAccountInterestRateChart.SlabSnapshot> closingChartSlabs = account.currentChartSlabFields();
|
|
||||||
// Likewise captured before renewInPlace overwrites the account's own rate/tenure with the new
|
|
||||||
// term's -- needed only if this is the closing term's first-ever history row (see
|
|
||||||
// recordInPlaceRenewal), so that row records what this term actually ran at instead of blank.
|
|
||||||
final BigDecimal closingInterestRate = account.getNominalAnnualInterestRate();
|
|
||||||
final Integer closingTenure = account.depositPeriod();
|
|
||||||
final Integer closingTenureFrequencyEnum = account.depositPeriodFrequency();
|
|
||||||
|
|
||||||
account.renewInPlace(renewedAmount, closedDate, currentSeniorCitizenRate, baseChartFieldsOf(activeBaseRates),
|
|
||||||
baseChartSlabsOf(activeBaseRates, account.getCurrency().getCode()));
|
|
||||||
account.updateMaturityDateAndAmount(mc, isPreMatureClosure, isSavingsInterestPostingAtCurrentPeriodEnd,
|
|
||||||
financialYearBeginningMonth);
|
|
||||||
this.savingsAccountRepository.save(account);
|
|
||||||
|
|
||||||
recordInPlaceRenewal(account, closingPrincipal, interestEarned, maturedBalance, renewedAmount, closedDate,
|
|
||||||
FixedDepositRenewalHistory.RENEWAL_TYPE_MANUAL, user, closingChartName, closingChartSlabs, closingInterestRate,
|
|
||||||
closingTenure, closingTenureFrequencyEnum);
|
|
||||||
changes.put("renewedInPlace", Boolean.TRUE);
|
|
||||||
changes.put("renewalCycleNo", account.renewalCycleNo());
|
|
||||||
|
|
||||||
if (onClosureType.isPartialReinvest()) {
|
|
||||||
// The kept portion never left the account -- renewInPlace() just relabels it as the new
|
|
||||||
// term's principal -- so only the remainder is an actual money movement, posted the same
|
|
||||||
// way the Transfer to Savings branch below posts its full-balance transfer.
|
|
||||||
final BigDecimal remainder = maturedBalance.subtract(renewedAmount);
|
|
||||||
final Long toSavingsId = command.longValueOfParameterNamed(toSavingsAccountIdParamName);
|
|
||||||
final String transferDescription = command.stringValueOfParameterNamed(transferDescriptionParamName);
|
|
||||||
final SavingsAccount toSavingsAccount = this.depositAccountAssembler.assembleFrom(toSavingsId,
|
|
||||||
DepositAccountType.SAVINGS_DEPOSIT);
|
|
||||||
final boolean isExceptionForBalanceCheck = false;
|
|
||||||
final AccountTransferDTO accountTransferDTO = new AccountTransferDTO(closedDate, remainder, PortfolioAccountType.SAVINGS,
|
|
||||||
PortfolioAccountType.SAVINGS, null, null, transferDescription, locale, fmt, null, null, null, null, null,
|
|
||||||
AccountTransferType.ACCOUNT_TRANSFER.getValue(), null, null, ExternalId.empty(), null, toSavingsAccount, account,
|
|
||||||
isAccountTransfer, isExceptionForBalanceCheck);
|
|
||||||
this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO);
|
|
||||||
updateAlreadyPostedTransactions(existingTransactionIds, account);
|
|
||||||
}
|
|
||||||
|
|
||||||
} else if (onClosureType.isReinvestPrincipal()) {
|
|
||||||
// Legacy close-and-reopen, same shape as handleFDAccountClosure()'s isReinvest() branch --
|
|
||||||
// the interest has to leave the deposit, which needs a payout destination an in-place renewal
|
|
||||||
// has nowhere to send.
|
|
||||||
final BigDecimal maturedBalance = account.getAccountBalance();
|
|
||||||
final BigDecimal reInvestAmount = account.getDepositAmount();
|
|
||||||
final ExternalId externalId = this.externalIdFactory.create();
|
|
||||||
|
|
||||||
final FixedDepositAccount reinvestedDeposit = account.reInvest(reInvestAmount, externalId);
|
|
||||||
this.depositAccountAssembler.assignSavingAccountHelpers(reinvestedDeposit);
|
|
||||||
reinvestedDeposit.updateMaturityDateAndAmountBeforeAccountActivation(mc, isPreMatureClosure,
|
|
||||||
isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth);
|
|
||||||
|
|
||||||
this.savingsAccountRepository.saveAndFlush(reinvestedDeposit);
|
|
||||||
autoGenerateAccountNumber(reinvestedDeposit);
|
|
||||||
final boolean isInternalTransferLeg = true;
|
|
||||||
final SavingsAccountTransaction withdrawal = this.handleWithdrawal(account, fmt, closedDate, reInvestAmount, paymentDetail,
|
|
||||||
false, isRegularTransaction, isInternalTransferLeg);
|
|
||||||
savingsTransactionId = withdrawal.getId();
|
|
||||||
updateAlreadyPostedTransactions(existingTransactionIds, account);
|
|
||||||
|
|
||||||
changes.put("reinvestedDepositId", reinvestedDeposit.getId());
|
|
||||||
reinvestedDeposit.approveAndActivateApplication(closedDate, user);
|
|
||||||
this.savingsAccountRepository.save(reinvestedDeposit);
|
|
||||||
|
|
||||||
recordRenewal(account, reinvestedDeposit, maturedBalance, reInvestAmount, closedDate,
|
|
||||||
FixedDepositRenewalHistory.RENEWAL_TYPE_MANUAL, user);
|
|
||||||
|
|
||||||
// Only the principal moved into the new deposit -- the interest earned on this term is still
|
|
||||||
// sitting in this account's balance. Pay it out to the chosen savings account now, before
|
|
||||||
// closing, rather than stranding it here with nowhere left to go.
|
|
||||||
final BigDecimal interestRemainder = maturedBalance.subtract(reInvestAmount);
|
|
||||||
if (interestRemainder.compareTo(BigDecimal.ZERO) > 0) {
|
|
||||||
final Long toSavingsId = command.longValueOfParameterNamed(toSavingsAccountIdParamName);
|
|
||||||
final String transferDescription = command.stringValueOfParameterNamed(transferDescriptionParamName);
|
|
||||||
final SavingsAccount toSavingsAccount = this.depositAccountAssembler.assembleFrom(toSavingsId,
|
|
||||||
DepositAccountType.SAVINGS_DEPOSIT);
|
|
||||||
final boolean isExceptionForBalanceCheck = false;
|
|
||||||
final AccountTransferDTO accountTransferDTO = new AccountTransferDTO(closedDate, interestRemainder,
|
|
||||||
PortfolioAccountType.SAVINGS, PortfolioAccountType.SAVINGS, null, null, transferDescription, locale, fmt, null,
|
|
||||||
null, null, null, null, AccountTransferType.ACCOUNT_TRANSFER.getValue(), null, null, ExternalId.empty(), null,
|
|
||||||
toSavingsAccount, account, isAccountTransfer, isExceptionForBalanceCheck);
|
|
||||||
this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO);
|
|
||||||
updateAlreadyPostedTransactions(existingTransactionIds, account);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (alreadyMatured) {
|
|
||||||
account.close(user, command, changes);
|
|
||||||
} else {
|
|
||||||
account.prematureClosure(user, command, changes);
|
|
||||||
}
|
|
||||||
|
|
||||||
} else if (onClosureType.isTransferToSavings()) {
|
|
||||||
final Long toSavingsId = command.longValueOfParameterNamed(toSavingsAccountIdParamName);
|
|
||||||
final String transferDescription = command.stringValueOfParameterNamed(transferDescriptionParamName);
|
|
||||||
final SavingsAccount toSavingsAccount = this.depositAccountAssembler.assembleFrom(toSavingsId,
|
|
||||||
DepositAccountType.SAVINGS_DEPOSIT);
|
|
||||||
final boolean isExceptionForBalanceCheck = false;
|
|
||||||
final AccountTransferDTO accountTransferDTO = new AccountTransferDTO(closedDate, account.getAccountBalance(),
|
|
||||||
PortfolioAccountType.SAVINGS, PortfolioAccountType.SAVINGS, null, null, transferDescription, locale, fmt, null, null,
|
|
||||||
null, null, null, AccountTransferType.ACCOUNT_TRANSFER.getValue(), null, null, ExternalId.empty(), null,
|
|
||||||
toSavingsAccount, account, isAccountTransfer, isExceptionForBalanceCheck);
|
|
||||||
this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO);
|
|
||||||
updateAlreadyPostedTransactions(existingTransactionIds, account);
|
|
||||||
|
|
||||||
if (alreadyMatured) {
|
|
||||||
account.close(user, command, changes);
|
|
||||||
} else {
|
|
||||||
account.prematureClosure(user, command, changes);
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
final SavingsAccountTransaction withdrawal = this.handleWithdrawal(account, fmt, closedDate, account.getAccountBalance(),
|
|
||||||
paymentDetail, false, isRegularTransaction);
|
|
||||||
savingsTransactionId = withdrawal.getId();
|
|
||||||
updateAlreadyPostedTransactions(existingTransactionIds, account);
|
|
||||||
|
|
||||||
if (alreadyMatured) {
|
|
||||||
account.close(user, command, changes);
|
|
||||||
} else {
|
|
||||||
account.prematureClosure(user, command, changes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.savingsAccountRepository.save(account);
|
|
||||||
postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, isAccountTransfer);
|
|
||||||
|
|
||||||
return savingsTransactionId;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
@Override
|
@Override
|
||||||
public Long handleRDAccountPreMatureClosure(final RecurringDepositAccount account, final PaymentDetail paymentDetail,
|
public Long handleRDAccountPreMatureClosure(final RecurringDepositAccount account, final PaymentDetail paymentDetail,
|
||||||
@@ -1155,19 +555,14 @@ public class DepositAccountDomainServiceJpa implements DepositAccountDomainServi
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void updateAlreadyPostedTransactions(final Set<Long> existingTransactionIds, final SavingsAccount savingsAccount) {
|
private void updateAlreadyPostedTransactions(final Set<Long> existingTransactionIds, final SavingsAccount savingsAccount) {
|
||||||
// accountTransfersWritePlatformService.transferFunds() already posts accounting for the
|
List<SavingsAccountTransaction> transactions = savingsAccount.getTransactions();
|
||||||
// withdrawal it creates on this account (via SavingsAccountDomainServiceJpa#handleWithdrawal),
|
int size = transactions.size();
|
||||||
// so that withdrawal must be added to existingTransactionIds here or the caller's own
|
for (int i = size - 1;; i--) {
|
||||||
// postJournalEntries() re-derives and re-posts it a second time. The previous implementation
|
SavingsAccountTransaction transaction = transactions.get(i);
|
||||||
// found it by walking the transaction list backwards from the end while the type stayed
|
if (transaction.isWithdrawal() || transaction.isWithdrawalFee()) {
|
||||||
// withdrawal/withdrawal-fee -- but @OneToMany collections only guarantee @OrderBy ordering on
|
|
||||||
// a fresh fetch, not after an in-memory append, so a same-day interest posting or any other
|
|
||||||
// ordering quirk could put the new withdrawal anywhere in the list and break the walk
|
|
||||||
// silently. Scanning every transaction and checking against the original snapshot is correct
|
|
||||||
// regardless of collection order.
|
|
||||||
for (final SavingsAccountTransaction transaction : savingsAccount.getTransactions()) {
|
|
||||||
if ((transaction.isWithdrawal() || transaction.isWithdrawalFee()) && !existingTransactionIds.contains(transaction.getId())) {
|
|
||||||
existingTransactionIds.add(transaction.getId());
|
existingTransactionIds.add(transaction.getId());
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+19
-283
@@ -53,7 +53,6 @@ import org.apache.fineract.portfolio.accountdetails.domain.AccountType;
|
|||||||
import org.apache.fineract.portfolio.client.domain.Client;
|
import org.apache.fineract.portfolio.client.domain.Client;
|
||||||
import org.apache.fineract.portfolio.group.domain.Group;
|
import org.apache.fineract.portfolio.group.domain.Group;
|
||||||
import org.apache.fineract.portfolio.interestratechart.domain.InterestRateChart;
|
import org.apache.fineract.portfolio.interestratechart.domain.InterestRateChart;
|
||||||
import org.apache.fineract.portfolio.interestratechart.domain.InterestRateChartFields;
|
|
||||||
import org.apache.fineract.portfolio.interestratechart.service.InterestRateChartAssembler;
|
import org.apache.fineract.portfolio.interestratechart.service.InterestRateChartAssembler;
|
||||||
import org.apache.fineract.portfolio.savings.DepositAccountOnClosureType;
|
import org.apache.fineract.portfolio.savings.DepositAccountOnClosureType;
|
||||||
import org.apache.fineract.portfolio.savings.DepositAccountType;
|
import org.apache.fineract.portfolio.savings.DepositAccountType;
|
||||||
@@ -277,9 +276,7 @@ public class FixedDepositAccount extends SavingsAccount {
|
|||||||
|
|
||||||
public LocalDate calculateMaturityDate() {
|
public LocalDate calculateMaturityDate() {
|
||||||
|
|
||||||
// The CURRENT term's start, not the account's activation date -- on a renewed FD these differ,
|
final LocalDate startDate = accountSubmittedOrActivationDate();
|
||||||
// and measuring the term from activation would put maturity in the past.
|
|
||||||
final LocalDate startDate = depositStartDate();
|
|
||||||
LocalDate maturityDate = null;
|
LocalDate maturityDate = null;
|
||||||
final Integer depositPeriod = this.accountTermAndPreClosure.depositPeriod();
|
final Integer depositPeriod = this.accountTermAndPreClosure.depositPeriod();
|
||||||
switch (this.accountTermAndPreClosure.depositPeriodFrequencyType()) {
|
switch (this.accountTermAndPreClosure.depositPeriodFrequencyType()) {
|
||||||
@@ -315,19 +312,13 @@ public class FixedDepositAccount extends SavingsAccount {
|
|||||||
.fromInt(this.interestCalculationDaysInYearType);
|
.fromInt(this.interestCalculationDaysInYearType);
|
||||||
List<LocalDate> postedAsOnTransactionDates = getManualPostingDates();
|
List<LocalDate> postedAsOnTransactionDates = getManualPostingDates();
|
||||||
|
|
||||||
// Scoped to the current term, so a renewed FD's projection covers only the term now running --
|
|
||||||
// matching what postInterest() will actually post -- rather than replaying the whole account.
|
|
||||||
final List<LocalDateInterval> postingPeriodIntervals = this.savingsHelper.determineInterestPostingPeriods(
|
final List<LocalDateInterval> postingPeriodIntervals = this.savingsHelper.determineInterestPostingPeriods(
|
||||||
depositStartDate(), maturityDate, postingPeriodType, financialYearBeginningMonth, postedAsOnTransactionDates);
|
accountSubmittedOrActivationDate(), maturityDate, postingPeriodType, financialYearBeginningMonth,
|
||||||
|
postedAsOnTransactionDates);
|
||||||
|
|
||||||
final List<PostingPeriod> allPostingPeriods = new ArrayList<>();
|
final List<PostingPeriod> allPostingPeriods = new ArrayList<>();
|
||||||
|
|
||||||
// On a first term the opening deposit falls inside the first posting period, so the period can
|
Money periodStartingBalance = Money.zero(currency);
|
||||||
// start from zero and pick the balance up from that transaction. On a renewed term that deposit
|
|
||||||
// is BEFORE the period range and would be missed, leaving the term to earn interest on nothing,
|
|
||||||
// so the term's own principal seeds the opening balance instead.
|
|
||||||
Money periodStartingBalance = isRenewedTerm() ? Money.of(currency, this.accountTermAndPreClosure.depositAmount())
|
|
||||||
: Money.zero(currency);
|
|
||||||
|
|
||||||
final SavingsInterestCalculationType interestCalculationType = SavingsInterestCalculationType.fromInt(this.interestCalculationType);
|
final SavingsInterestCalculationType interestCalculationType = SavingsInterestCalculationType.fromInt(this.interestCalculationType);
|
||||||
final BigDecimal interestRateAsFraction = getEffectiveInterestRateAsFraction(mc, maturityDate, isPreMatureClosure);
|
final BigDecimal interestRateAsFraction = getEffectiveInterestRateAsFraction(mc, maturityDate, isPreMatureClosure);
|
||||||
@@ -695,60 +686,6 @@ public class FixedDepositAccount extends SavingsAccount {
|
|||||||
return this.accountTermAndPreClosure.maturityAmount();
|
return this.accountTermAndPreClosure.maturityAmount();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* The stable customer-facing FD number: the account number of the first account in this renewal chain. Falls back to
|
|
||||||
* this account's own number when it has never been renewed, so callers always get a usable FD number.
|
|
||||||
*/
|
|
||||||
public String fdAccountNumber() {
|
|
||||||
final String carried = this.accountTermAndPreClosure.getFdAccountNumber();
|
|
||||||
return carried != null ? carried : getAccountNumber();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 0 for the originally opened term, 1 for the first renewal, and so on. */
|
|
||||||
public Integer renewalCycleNo() {
|
|
||||||
return this.accountTermAndPreClosure.getRenewalCycleNo();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Integer depositPeriod() {
|
|
||||||
return this.accountTermAndPreClosure.depositPeriod();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Integer depositPeriodFrequency() {
|
|
||||||
return this.accountTermAndPreClosure.depositPeriodFrequency();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Long reinvestedFromSavingsId() {
|
|
||||||
return this.accountTermAndPreClosure.getReinvestedFromSavingsId();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The name of the chart currently priced on this account. Read before a renewal overwrites it, so the closing term's
|
|
||||||
* chart identity can be archived rather than lost.
|
|
||||||
*/
|
|
||||||
public String currentChartName() {
|
|
||||||
return this.chart == null ? null : this.chart.chartFields().getName();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The slabs of the chart currently priced on this account -- each paired with its senior-citizen premium -- detached
|
|
||||||
* from the chart's own persistence lifecycle.
|
|
||||||
* <p>
|
|
||||||
* Read before a renewal overwrites the account's chart in place ({@link #renewInPlace}), so the closing term's full
|
|
||||||
* rate-by-tenure breakdown (including the premium) can be archived -- not just the single effective rate the renewal
|
|
||||||
* history records -- rather than being silently deleted when the new term's slabs replace it.
|
|
||||||
*/
|
|
||||||
public List<DepositAccountInterestRateChart.SlabSnapshot> currentChartSlabFields() {
|
|
||||||
return this.chart == null ? java.util.Collections.emptyList() : this.chart.currentSlabSnapshots();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This deposit's FD type (SINGLE / DOUBLE / SPECIAL), which lives on the product rather than the account. Null when
|
|
||||||
* the product is not a Fixed Deposit product or has no type set.
|
|
||||||
*/
|
|
||||||
public String fdType() {
|
|
||||||
return this.product instanceof FixedDepositProduct ? ((FixedDepositProduct) this.product).getFdType() : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Money totalInterestPosted() {
|
private Money totalInterestPosted() {
|
||||||
Money interestPostedToDate = Money.zero(this.currency);
|
Money interestPostedToDate = Money.zero(this.currency);
|
||||||
for (final SavingsAccountTransaction transaction : this.transactions) {
|
for (final SavingsAccountTransaction transaction : this.transactions) {
|
||||||
@@ -778,26 +715,11 @@ public class FixedDepositAccount extends SavingsAccount {
|
|||||||
return actualChanges;
|
return actualChanges;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* The start of the term currently running, which is what the deposit's tenure and rate must be measured from.
|
|
||||||
* <p>
|
|
||||||
* For an FD that has never been renewed this is the activation date, exactly as before. A renewal advances
|
|
||||||
* startInterestCalculationDate to the renewal date, so from then on the interest-rate chart slab is chosen by THIS
|
|
||||||
* term's tenure rather than the cumulative tenure since the account was opened, and maturity is measured from the
|
|
||||||
* renewal rather than from the original activation.
|
|
||||||
*/
|
|
||||||
private LocalDate depositStartDate() {
|
private LocalDate depositStartDate() {
|
||||||
final LocalDate currentTermStartDate = getStartInterestCalculationDate();
|
// TODO: Support to add deposit start date which can be a date after
|
||||||
return currentTermStartDate != null ? currentTermStartDate : accountSubmittedOrActivationDate();
|
// account activation date.
|
||||||
}
|
final LocalDate depositStartDate = accountSubmittedOrActivationDate();
|
||||||
|
return depositStartDate;
|
||||||
/**
|
|
||||||
* True once this FD has been renewed at least once, i.e. the term now running started later than the account did.
|
|
||||||
* Everything keyed off this keeps its original, pre-renewal behaviour for an FD still on its first term.
|
|
||||||
*/
|
|
||||||
private boolean isRenewedTerm() {
|
|
||||||
final LocalDate termStart = getStartInterestCalculationDate();
|
|
||||||
return termStart != null && getActivationDate() != null && !termStart.equals(getActivationDate());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private LocalDate interestCalculatedUpto() {
|
private LocalDate interestCalculatedUpto() {
|
||||||
@@ -892,142 +814,28 @@ public class FixedDepositAccount extends SavingsAccount {
|
|||||||
return this.accountTermAndPreClosure.getTransferToSavingsAccountId();
|
return this.accountTermAndPreClosure.getTransferToSavingsAccountId();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Rolls this FD into a new term on the SAME account, so the customer keeps one account number however many times it
|
|
||||||
* renews. No new account is opened and no money moves: the balance simply carries into the next term.
|
|
||||||
* <p>
|
|
||||||
* What changes is the term itself. startInterestCalculationDate advances to the renewal date, which is what confines
|
|
||||||
* interest posting to the new term and thereby leaves the closing term's postings untouched -- they fall outside the
|
|
||||||
* posting periods the engine now generates, so nothing recalculates or reverses them. The term's principal becomes
|
|
||||||
* {@code renewedAmount}, and every interest-mechanics setting plus the rate chart is re-taken from the product as it
|
|
||||||
* stands today, so a renewal is priced on current terms rather than the ones frozen at account opening.
|
|
||||||
* <p>
|
|
||||||
* The caller must have posted the closing term's interest first, and must record the renewal in the FD's renewal
|
|
||||||
* history so each term's own figures remain reportable.
|
|
||||||
*
|
|
||||||
* @param renewedAmount
|
|
||||||
* what carries into the new term: the full maturity amount, or the principal only
|
|
||||||
* @param renewedOn
|
|
||||||
* the renewal date, which becomes the new term's start
|
|
||||||
* @param currentSeniorCitizenRate
|
|
||||||
* the senior-citizen premium the bank publishes on the renewal date for this FD type and tenure, or null to
|
|
||||||
* keep the rate the account already holds. Only applied to an account already flagged senior-citizen; the
|
|
||||||
* renewal never turns that flag on or off.
|
|
||||||
* @param baseChartFields
|
|
||||||
* identity of the CBS base rate version being applied, or null to keep the existing chart's
|
|
||||||
* @param baseChartSlabs
|
|
||||||
* the slabs of the active CBS base interest-rate version, each paired with its senior-citizen premium -- the
|
|
||||||
* rate card staff maintain and see on the Base Interest Rates screen. When empty or null the account falls
|
|
||||||
* back to the product's own chart, so a product with no base rate version still renews on something.
|
|
||||||
*/
|
|
||||||
public void renewInPlace(final BigDecimal renewedAmount, final LocalDate renewedOn, final BigDecimal currentSeniorCitizenRate,
|
|
||||||
final InterestRateChartFields baseChartFields, final Collection<DepositAccountInterestRateChart.SlabSnapshot> baseChartSlabs) {
|
|
||||||
|
|
||||||
final SavingsProduct product = this.product;
|
|
||||||
|
|
||||||
// Confines interest posting to the new term. Set before recomputing maturity, since maturity is
|
|
||||||
// measured from the current term's start.
|
|
||||||
setStartInterestCalculationDate(renewedOn);
|
|
||||||
|
|
||||||
this.accountTermAndPreClosure.updateDepositAmount(renewedAmount);
|
|
||||||
this.accountTermAndPreClosure.setRenewalCycleNo(this.accountTermAndPreClosure.getRenewalCycleNo() + 1);
|
|
||||||
// Pin the chain number now that this account has renewed at least once, so a non-zero cycle number
|
|
||||||
// is never paired with a blank FD number. fdAccountNumber() resolves to the chain's original number
|
|
||||||
// if there is one and to this account's own otherwise, which is the same value every reader derives.
|
|
||||||
this.accountTermAndPreClosure.setFdAccountNumber(fdAccountNumber());
|
|
||||||
|
|
||||||
// Re-price off the CURRENT product, exactly as opening a fresh deposit today would.
|
|
||||||
this.nominalAnnualInterestRate = product.nominalAnnualInterestRate();
|
|
||||||
this.interestCompoundingPeriodType = product.interestCompoundingPeriodType().getValue();
|
|
||||||
this.interestPostingPeriodType = product.interestPostingPeriodType().getValue();
|
|
||||||
this.interestCalculationType = product.interestCalculationType().getValue();
|
|
||||||
this.interestCalculationDaysInYearType = product.interestCalculationDaysInYearType().getValue();
|
|
||||||
this.withHoldTax = product.withHoldTax();
|
|
||||||
|
|
||||||
// Lock-in is re-taken from the product and re-measured from this renewal, exactly as the legacy
|
|
||||||
// close-and-reopen reInvest() does for the account it creates. Left alone, a renewed term would
|
|
||||||
// carry the lock-in window computed at the original activation -- long expired -- so a product with
|
|
||||||
// a statutory lock-in (a Tax Saver, say) would hold the customer for the first term only.
|
|
||||||
final SavingsPeriodFrequencyType renewedLockinPeriodFrequencyType = product.lockinPeriodFrequencyType();
|
|
||||||
this.lockinPeriodFrequency = product.lockinPeriodFrequency();
|
|
||||||
this.lockinPeriodFrequencyType = renewedLockinPeriodFrequencyType == null ? null
|
|
||||||
: renewedLockinPeriodFrequencyType.getValue();
|
|
||||||
this.lockedInUntilDate = this.lockinPeriodFrequencyType == null || this.lockinPeriodFrequency == null ? null
|
|
||||||
: calculateDateAccountIsLockedUntil(renewedOn);
|
|
||||||
|
|
||||||
// The new term is priced off the CBS base interest-rate master -- the rate card staff maintain on
|
|
||||||
// the Base Interest Rates screen, per FD type, with its own active version. That master is what
|
|
||||||
// the bank publishes and what the account-opening wizard already shows, so pricing a renewal off
|
|
||||||
// anything else would put the deposit on rates nobody chose.
|
|
||||||
if (this.chart != null && baseChartSlabs != null && !baseChartSlabs.isEmpty()) {
|
|
||||||
this.chart.refreshFromSlabSnapshots(baseChartFields, baseChartSlabs);
|
|
||||||
} else if (this.chart != null) {
|
|
||||||
// No active base version for this FD type: fall back to the product's own chart so the
|
|
||||||
// renewal still gets current product rates rather than the ones frozen at opening.
|
|
||||||
// activeChart, not applicableChart -- the latter treats a chart with no end date as valid only
|
|
||||||
// up to today's business date, so a renewal dated in the future would find nothing.
|
|
||||||
final InterestRateChart productChart = product instanceof FixedDepositProduct
|
|
||||||
? ((FixedDepositProduct) product).activeChart(renewedOn)
|
|
||||||
: null;
|
|
||||||
if (productChart != null) {
|
|
||||||
this.chart.refreshFrom(productChart);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The senior-citizen premium is stored per account and takes priority over the chart in
|
|
||||||
// getEffectiveInterestRateAsFraction(), so leaving it alone would price every future term of a
|
|
||||||
// senior-citizen deposit at the rate captured when the account was opened. Only overwritten when
|
|
||||||
// the caller found a currently published rate -- otherwise the existing one stands, so a missing
|
|
||||||
// rate version can never silently drop the depositor's premium.
|
|
||||||
if (isSeniorCitizen() && currentSeniorCitizenRate != null && currentSeniorCitizenRate.compareTo(BigDecimal.ZERO) > 0) {
|
|
||||||
setSeniorCitizenInterestRate(currentSeniorCitizenRate);
|
|
||||||
}
|
|
||||||
|
|
||||||
// The account never left ACTIVE, but updateMaturityStatus() flipped it to MATURED just before
|
|
||||||
// this ran, so the new term has to put it back.
|
|
||||||
this.status = SavingsAccountStatusType.ACTIVE.getValue();
|
|
||||||
setClosedOnDate(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public FixedDepositAccount reInvest(BigDecimal depositAmount, ExternalId externalId) {
|
public FixedDepositAccount reInvest(BigDecimal depositAmount, ExternalId externalId) {
|
||||||
|
|
||||||
final DepositAccountTermAndPreClosure newAccountTermAndPreClosure = this.accountTermAndPreClosure.copy(depositAmount);
|
final DepositAccountTermAndPreClosure newAccountTermAndPreClosure = this.accountTermAndPreClosure.copy(depositAmount);
|
||||||
final SavingsProduct product = this.product;
|
final SavingsProduct product = this.product;
|
||||||
// Effective-dated pick, same as renewInPlace: an open-ended chart stays valid past today's
|
final InterestRateChart productChart = product.applicableChart(getClosedOnDate());
|
||||||
// business date, and the most recently effective chart wins when more than one covers the date.
|
|
||||||
// Falls back to applicableChart so a product whose charts have no usable from-date still resolves
|
|
||||||
// as it did before.
|
|
||||||
InterestRateChart productChart = product instanceof FixedDepositProduct
|
|
||||||
? ((FixedDepositProduct) product).activeChart(getClosedOnDate())
|
|
||||||
: null;
|
|
||||||
if (productChart == null) {
|
|
||||||
productChart = product.applicableChart(getClosedOnDate());
|
|
||||||
}
|
|
||||||
final DepositAccountInterestRateChart newChart = DepositAccountInterestRateChart.from(productChart);
|
final DepositAccountInterestRateChart newChart = DepositAccountInterestRateChart.from(productChart);
|
||||||
|
|
||||||
final AccountType accountType = AccountType.fromInt(this.accountType);
|
final AccountType accountType = AccountType.fromInt(this.accountType);
|
||||||
// Re-price every interest-mechanics setting off the CURRENT product, not this (old) account's
|
final SavingsPostingInterestPeriodType postingPeriodType = SavingsPostingInterestPeriodType.fromInt(this.interestPostingPeriodType);
|
||||||
// frozen-at-opening snapshot -- same reasoning as interestRate/newChart below. Without this, a
|
final SavingsCompoundingInterestPeriodType compoundingPeriodType = SavingsCompoundingInterestPeriodType
|
||||||
// renewed FD kept running on whatever compounding/posting/calculation-type/lock-in/tax setting
|
.fromInt(this.interestCompoundingPeriodType);
|
||||||
// was in force when the ORIGINAL account was opened, silently ignoring every product edit made
|
final SavingsInterestCalculationType interestCalculationType = SavingsInterestCalculationType.fromInt(this.interestCalculationType);
|
||||||
// since (e.g. Daily->Quarterly compounding, or a lock-in added later).
|
final SavingsInterestCalculationDaysInYearType daysInYearType = SavingsInterestCalculationDaysInYearType
|
||||||
final SavingsPostingInterestPeriodType postingPeriodType = product.interestPostingPeriodType();
|
.fromInt(this.interestCalculationDaysInYearType);
|
||||||
final SavingsCompoundingInterestPeriodType compoundingPeriodType = product.interestCompoundingPeriodType();
|
|
||||||
final SavingsInterestCalculationType interestCalculationType = product.interestCalculationType();
|
|
||||||
final SavingsInterestCalculationDaysInYearType daysInYearType = product.interestCalculationDaysInYearType();
|
|
||||||
final BigDecimal minRequiredOpeningBalance = null;
|
final BigDecimal minRequiredOpeningBalance = null;
|
||||||
// Falls back to the product's own flat rate -- same fallback DepositAccountAssembler uses
|
final BigDecimal interestRate = BigDecimal.ZERO;
|
||||||
// for a brand-new account -- so a product with no chart slab covering this tenure/amount
|
|
||||||
// doesn't silently reinvest the customer at 0%. getEffectiveInterestRateAsFraction() still
|
|
||||||
// re-prices off newChart (the product's CURRENT chart, reflecting any edits made since the
|
|
||||||
// original account was opened) whenever a matching slab exists.
|
|
||||||
final BigDecimal interestRate = product.nominalAnnualInterestRate();
|
|
||||||
final Set<SavingsAccountCharge> savingsAccountCharges = null;
|
final Set<SavingsAccountCharge> savingsAccountCharges = null;
|
||||||
final SavingsPeriodFrequencyType lockinPeriodFrequencyType = product.lockinPeriodFrequencyType();
|
final SavingsPeriodFrequencyType lockinPeriodFrequencyType = SavingsPeriodFrequencyType.fromInt(this.lockinPeriodFrequencyType);
|
||||||
final Integer lockinPeriodFrequency = product.lockinPeriodFrequency();
|
final Integer lockinPeriodFrequency = this.lockinPeriodFrequency;
|
||||||
final boolean withdrawalFeeApplicableForTransfer = false;
|
final boolean withdrawalFeeApplicableForTransfer = false;
|
||||||
final String accountNumber = null;
|
final String accountNumber = null;
|
||||||
final boolean withHoldTax = product.withHoldTax();
|
final boolean withHoldTax = this.withHoldTax;
|
||||||
final FixedDepositAccount reInvestedAccount = FixedDepositAccount.createNewApplicationForSubmittal(client, group, product,
|
final FixedDepositAccount reInvestedAccount = FixedDepositAccount.createNewApplicationForSubmittal(client, group, product,
|
||||||
savingsOfficer, accountNumber, externalId, accountType, getClosedOnDate(), closedBy, interestRate, compoundingPeriodType,
|
savingsOfficer, accountNumber, externalId, accountType, getClosedOnDate(), closedBy, interestRate, compoundingPeriodType,
|
||||||
postingPeriodType, interestCalculationType, daysInYearType, minRequiredOpeningBalance, lockinPeriodFrequency,
|
postingPeriodType, interestCalculationType, daysInYearType, minRequiredOpeningBalance, lockinPeriodFrequency,
|
||||||
@@ -1037,30 +845,6 @@ public class FixedDepositAccount extends SavingsAccount {
|
|||||||
newAccountTermAndPreClosure.updateAccountReference(reInvestedAccount);
|
newAccountTermAndPreClosure.updateAccountReference(reInvestedAccount);
|
||||||
newChart.updateDepositAccountReference(reInvestedAccount);
|
newChart.updateDepositAccountReference(reInvestedAccount);
|
||||||
|
|
||||||
// Carry the senior-citizen premium forward -- without this, a senior-citizen depositor's
|
|
||||||
// reinvested FD silently drops back to the standard rate on the new term.
|
|
||||||
reInvestedAccount.setSeniorCitizen(this.isSeniorCitizen());
|
|
||||||
reInvestedAccount.setSeniorCitizenInterestRate(this.getSeniorCitizenInterestRate());
|
|
||||||
|
|
||||||
// Track which account this was reinvested from, so staff/UI can trace the two accounts to
|
|
||||||
// each other instead of the link disappearing the moment the old one closes.
|
|
||||||
newAccountTermAndPreClosure.setReinvestedFromSavingsId(this.getId());
|
|
||||||
|
|
||||||
// Carry the ORIGINAL account's number forward across every renewal, so the customer keeps
|
|
||||||
// seeing one recognisable FD number even though each term is its own account/ledger row.
|
|
||||||
final String rootFdAccountNumber = this.accountTermAndPreClosure.getFdAccountNumber() != null
|
|
||||||
? this.accountTermAndPreClosure.getFdAccountNumber()
|
|
||||||
: this.getAccountNumber();
|
|
||||||
newAccountTermAndPreClosure.setFdAccountNumber(rootFdAccountNumber);
|
|
||||||
|
|
||||||
// Number this term within the chain, so the UI can label it "Renewal 2 of FD ...048".
|
|
||||||
newAccountTermAndPreClosure.setRenewalCycleNo(this.accountTermAndPreClosure.getRenewalCycleNo() + 1);
|
|
||||||
|
|
||||||
// Without this the renewed deposit has no maturity instruction, so it would mature once more and
|
|
||||||
// then sit idle rather than rolling over again as the customer asked. Done here rather than in
|
|
||||||
// the shared copy(), which Recurring Deposit renewal also uses.
|
|
||||||
newAccountTermAndPreClosure.carryForwardStandingInstructionsFrom(this.accountTermAndPreClosure);
|
|
||||||
|
|
||||||
return reInvestedAccount;
|
return reInvestedAccount;
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1102,10 +886,8 @@ public class FixedDepositAccount extends SavingsAccount {
|
|||||||
@Override
|
@Override
|
||||||
public void loadLazyCollections() {
|
public void loadLazyCollections() {
|
||||||
super.loadLazyCollections();
|
super.loadLazyCollections();
|
||||||
if (this.chart != null) {
|
|
||||||
this.chart.getId();
|
this.chart.getId();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public BigDecimal getDepositAmount() {
|
public BigDecimal getDepositAmount() {
|
||||||
return this.accountTermAndPreClosure.depositAmount();
|
return this.accountTermAndPreClosure.depositAmount();
|
||||||
@@ -1120,52 +902,6 @@ public class FixedDepositAccount extends SavingsAccount {
|
|||||||
return SavingsAccountStatusType.MATURED.getValue().equals(this.status);
|
return SavingsAccountStatusType.MATURED.getValue().equals(this.status);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Exposes the parent class's protected lock-in check to callers outside this class hierarchy. Needed by the
|
|
||||||
* Payment action's domain-service method, which enforces this itself for its reinvest branches rather than going
|
|
||||||
* through {@link #prematureClosure}, since those branches keep the account open on the same term instead of
|
|
||||||
* closing it.
|
|
||||||
*/
|
|
||||||
public boolean isLockedOnDate(final LocalDate transactionDate) {
|
|
||||||
return isAccountLocked(transactionDate);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The date guards {@link #close} and {@link #prematureClosure} apply, minus the maturity-date bound, for a Payment
|
|
||||||
* that renews the deposit instead of closing it and so never reaches either of those methods.
|
|
||||||
* <p>
|
|
||||||
* Dropping the maturity bound is the point: Payment is allowed on an already-matured deposit. The other three
|
|
||||||
* matter more here than they do on a closure, because a renewal turns this date into the new term's interest
|
|
||||||
* pivot -- backdate it behind interest that has already been posted and the engine regenerates those posting
|
|
||||||
* periods, reverses the settled postings and re-posts them, which is exactly what the pivot exists to prevent.
|
|
||||||
*/
|
|
||||||
public void validateRenewalDate(final LocalDate renewedOn) {
|
|
||||||
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
|
|
||||||
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
|
|
||||||
.resource(FIXED_DEPOSIT_ACCOUNT_RESOURCE_NAME + DepositsApiConstants.preMatureCloseAction);
|
|
||||||
|
|
||||||
if (DateUtils.isAfterBusinessDate(renewedOn)) {
|
|
||||||
baseDataValidator.reset().parameter(SavingsApiConstants.closedOnDateParamName).value(renewedOn)
|
|
||||||
.failWithCode("cannot.be.a.future.date");
|
|
||||||
}
|
|
||||||
if (DateUtils.isBefore(renewedOn, getActivationDate())) {
|
|
||||||
baseDataValidator.reset().parameter(SavingsApiConstants.closedOnDateParamName).value(renewedOn)
|
|
||||||
.failWithCode("must.be.after.activation.date");
|
|
||||||
}
|
|
||||||
final List<SavingsAccountTransaction> savingsAccountTransactions = retrieveListOfTransactions();
|
|
||||||
if (!savingsAccountTransactions.isEmpty()) {
|
|
||||||
final SavingsAccountTransaction lastTransaction = savingsAccountTransactions.get(savingsAccountTransactions.size() - 1);
|
|
||||||
if (lastTransaction.isAfter(renewedOn)) {
|
|
||||||
baseDataValidator.reset().parameter(SavingsApiConstants.closedOnDateParamName).value(renewedOn)
|
|
||||||
.failWithCode("must.be.after.last.transaction.date");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!dataValidationErrors.isEmpty()) {
|
|
||||||
throw new PlatformApiDataValidationException(dataValidationErrors);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setClosedOnDate(final LocalDate closedOnDate) {
|
public void setClosedOnDate(final LocalDate closedOnDate) {
|
||||||
this.closedOnDate = closedOnDate;
|
this.closedOnDate = closedOnDate;
|
||||||
}
|
}
|
||||||
|
|||||||
-99
@@ -1,99 +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
|
|
||||||
*/
|
|
||||||
package org.apache.fineract.portfolio.savings.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 lombok.Getter;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* One slab of the full tenure-by-rate chart a term of an FD was priced on, archived against that term's
|
|
||||||
* {@link FixedDepositRenewalHistory} row.
|
|
||||||
* <p>
|
|
||||||
* {@code renewInPlace} reuses the same account row across renewals, overwriting the account's own interest-rate chart in
|
|
||||||
* place at every renewal. {@code FixedDepositRenewalHistory} preserves the single effective rate that resulted, but not
|
|
||||||
* the chart it came from -- this table is what lets a later query answer "what did the whole rate card look like during
|
|
||||||
* term 2", not just "what rate applied".
|
|
||||||
*/
|
|
||||||
@Entity
|
|
||||||
@Table(name = "m_fd_renewal_chart_slab_history")
|
|
||||||
@Getter
|
|
||||||
@NoArgsConstructor
|
|
||||||
public class FixedDepositRenewalChartSlabHistory {
|
|
||||||
|
|
||||||
@Id
|
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
|
||||||
private Long id;
|
|
||||||
|
|
||||||
@Column(name = "fd_renewal_history_id", nullable = false)
|
|
||||||
private Long fdRenewalHistoryId;
|
|
||||||
|
|
||||||
@Column(name = "chart_name", length = 100)
|
|
||||||
private String chartName;
|
|
||||||
|
|
||||||
@Column(name = "description", length = 200)
|
|
||||||
private String description;
|
|
||||||
|
|
||||||
@Column(name = "period_type_enum")
|
|
||||||
private Integer periodTypeEnum;
|
|
||||||
|
|
||||||
@Column(name = "from_period")
|
|
||||||
private Integer fromPeriod;
|
|
||||||
|
|
||||||
@Column(name = "to_period")
|
|
||||||
private Integer toPeriod;
|
|
||||||
|
|
||||||
@Column(name = "amount_range_from", scale = 6, precision = 19)
|
|
||||||
private BigDecimal amountRangeFrom;
|
|
||||||
|
|
||||||
@Column(name = "amount_range_to", scale = 6, precision = 19)
|
|
||||||
private BigDecimal amountRangeTo;
|
|
||||||
|
|
||||||
@Column(name = "annual_interest_rate", scale = 6, precision = 19)
|
|
||||||
private BigDecimal annualInterestRate;
|
|
||||||
|
|
||||||
@Column(name = "currency_code", length = 3)
|
|
||||||
private String currencyCode;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The senior-citizen premium published for this tenure band at the time this term was priced, carried through from the
|
|
||||||
* CBS Base Interest Rates screen rather than looked up live against whatever is published now. Null when the term was
|
|
||||||
* priced off the product's own chart (which has no senior-citizen concept) or predates this column.
|
|
||||||
*/
|
|
||||||
@Column(name = "senior_citizen_rate", scale = 6, precision = 19)
|
|
||||||
private BigDecimal seniorCitizenRate;
|
|
||||||
|
|
||||||
public static FixedDepositRenewalChartSlabHistory create(final Long fdRenewalHistoryId, final String chartName,
|
|
||||||
final String description, final Integer periodTypeEnum, final Integer fromPeriod, final Integer toPeriod,
|
|
||||||
final BigDecimal amountRangeFrom, final BigDecimal amountRangeTo, final BigDecimal annualInterestRate,
|
|
||||||
final String currencyCode, final BigDecimal seniorCitizenRate) {
|
|
||||||
final FixedDepositRenewalChartSlabHistory slab = new FixedDepositRenewalChartSlabHistory();
|
|
||||||
slab.fdRenewalHistoryId = fdRenewalHistoryId;
|
|
||||||
slab.chartName = chartName;
|
|
||||||
slab.description = description;
|
|
||||||
slab.periodTypeEnum = periodTypeEnum;
|
|
||||||
slab.fromPeriod = fromPeriod;
|
|
||||||
slab.toPeriod = toPeriod;
|
|
||||||
slab.amountRangeFrom = amountRangeFrom;
|
|
||||||
slab.amountRangeTo = amountRangeTo;
|
|
||||||
slab.annualInterestRate = annualInterestRate;
|
|
||||||
slab.currencyCode = currencyCode;
|
|
||||||
slab.seniorCitizenRate = seniorCitizenRate;
|
|
||||||
return slab;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-21
@@ -1,21 +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
|
|
||||||
*/
|
|
||||||
package org.apache.fineract.portfolio.savings.domain;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
|
||||||
|
|
||||||
public interface FixedDepositRenewalChartSlabHistoryRepository extends JpaRepository<FixedDepositRenewalChartSlabHistory, Long> {
|
|
||||||
|
|
||||||
/** The full chart a term was priced on, unordered -- callers typically sort by fromPeriod for display. */
|
|
||||||
List<FixedDepositRenewalChartSlabHistory> findByFdRenewalHistoryId(Long fdRenewalHistoryId);
|
|
||||||
}
|
|
||||||
-158
@@ -1,158 +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
|
|
||||||
*/
|
|
||||||
package org.apache.fineract.portfolio.savings.domain;
|
|
||||||
|
|
||||||
import jakarta.persistence.Column;
|
|
||||||
import jakarta.persistence.Entity;
|
|
||||||
import jakarta.persistence.GeneratedValue;
|
|
||||||
import jakarta.persistence.GenerationType;
|
|
||||||
import jakarta.persistence.Id;
|
|
||||||
import jakarta.persistence.Table;
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* One row per term of one logical Fixed Deposit, recording the renewal event that started it.
|
|
||||||
* <p>
|
|
||||||
* Each term runs as its own deposit instance (its own {@code m_savings_account} row) so that term keeps an immutable
|
|
||||||
* ledger, interest postings and GL entries, while {@link #fdAccountNumber} stays the same across the whole chain and is
|
|
||||||
* what the customer sees. What that structure alone cannot tell you is the intent and outcome of each rollover, which is
|
|
||||||
* what this table captures: the previous term's principal and interest earned, what it matured at, how much was carried
|
|
||||||
* forward, the rate applied to the new term, whether the renewal ran automatically at maturity or was performed by a
|
|
||||||
* user, and by whom.
|
|
||||||
*/
|
|
||||||
@Entity
|
|
||||||
@Table(name = "m_fd_renewal_history")
|
|
||||||
@Getter
|
|
||||||
@NoArgsConstructor
|
|
||||||
public class FixedDepositRenewalHistory {
|
|
||||||
|
|
||||||
/** Renewal ran automatically when the maturity job processed the account's maturity instruction. */
|
|
||||||
public static final String RENEWAL_TYPE_AUTO = "AUTO";
|
|
||||||
/** A user closed the account with a re-invest instruction. */
|
|
||||||
public static final String RENEWAL_TYPE_MANUAL = "MANUAL";
|
|
||||||
|
|
||||||
public static final String STATUS_ACTIVE = "ACTIVE";
|
|
||||||
public static final String STATUS_MATURED = "MATURED";
|
|
||||||
|
|
||||||
@Id
|
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
|
||||||
private Long id;
|
|
||||||
|
|
||||||
/** The stable customer-facing FD number, shared by every term in the chain. */
|
|
||||||
@Column(name = "fd_account_number", length = 20, nullable = false)
|
|
||||||
private String fdAccountNumber;
|
|
||||||
|
|
||||||
/** The account the customer originally opened, i.e. the anchor of the chain. */
|
|
||||||
@Column(name = "original_savings_account_id", nullable = false)
|
|
||||||
private Long originalSavingsAccountId;
|
|
||||||
|
|
||||||
/** The deposit instance this term runs on. */
|
|
||||||
@Column(name = "savings_account_id", nullable = false)
|
|
||||||
private Long savingsAccountId;
|
|
||||||
|
|
||||||
/** The term this one was renewed from; null on the chain's first row. */
|
|
||||||
@Column(name = "previous_savings_account_id")
|
|
||||||
private Long previousSavingsAccountId;
|
|
||||||
|
|
||||||
@Column(name = "previous_renewal_id")
|
|
||||||
private Long previousRenewalId;
|
|
||||||
|
|
||||||
/** 0 for the originally opened term, 1 for the first renewal, and so on. */
|
|
||||||
@Column(name = "renewal_no", nullable = false)
|
|
||||||
private Integer renewalNo;
|
|
||||||
|
|
||||||
@Column(name = "previous_principal", scale = 6, precision = 19)
|
|
||||||
private BigDecimal previousPrincipal;
|
|
||||||
|
|
||||||
@Column(name = "interest_earned", scale = 6, precision = 19)
|
|
||||||
private BigDecimal interestEarned;
|
|
||||||
|
|
||||||
@Column(name = "maturity_amount", scale = 6, precision = 19)
|
|
||||||
private BigDecimal maturityAmount;
|
|
||||||
|
|
||||||
/** What actually carried into this term: the full maturity amount, or principal only. */
|
|
||||||
@Column(name = "renewed_amount", scale = 6, precision = 19)
|
|
||||||
private BigDecimal renewedAmount;
|
|
||||||
|
|
||||||
@Column(name = "interest_rate", scale = 6, precision = 19)
|
|
||||||
private BigDecimal interestRate;
|
|
||||||
|
|
||||||
@Column(name = "start_date")
|
|
||||||
private LocalDate startDate;
|
|
||||||
|
|
||||||
@Column(name = "maturity_date")
|
|
||||||
private LocalDate maturityDate;
|
|
||||||
|
|
||||||
@Column(name = "tenure")
|
|
||||||
private Integer tenure;
|
|
||||||
|
|
||||||
@Column(name = "tenure_frequency_enum")
|
|
||||||
private Integer tenureFrequencyEnum;
|
|
||||||
|
|
||||||
@Column(name = "renewal_type", length = 10)
|
|
||||||
private String renewalType;
|
|
||||||
|
|
||||||
@Column(name = "renewal_status", length = 20)
|
|
||||||
private String renewalStatus;
|
|
||||||
|
|
||||||
@Column(name = "renewed_on")
|
|
||||||
private LocalDate renewedOn;
|
|
||||||
|
|
||||||
@Column(name = "created_by_id")
|
|
||||||
private Long createdById;
|
|
||||||
|
|
||||||
public static FixedDepositRenewalHistory create(final String fdAccountNumber, final Long originalSavingsAccountId,
|
|
||||||
final Long savingsAccountId, final Long previousSavingsAccountId, final Long previousRenewalId, final Integer renewalNo,
|
|
||||||
final BigDecimal previousPrincipal, final BigDecimal interestEarned, final BigDecimal maturityAmount,
|
|
||||||
final BigDecimal renewedAmount, final BigDecimal interestRate, final LocalDate startDate, final LocalDate maturityDate,
|
|
||||||
final Integer tenure, final Integer tenureFrequencyEnum, final String renewalType, final LocalDate renewedOn,
|
|
||||||
final Long createdById) {
|
|
||||||
final FixedDepositRenewalHistory history = new FixedDepositRenewalHistory();
|
|
||||||
history.fdAccountNumber = fdAccountNumber;
|
|
||||||
history.originalSavingsAccountId = originalSavingsAccountId;
|
|
||||||
history.savingsAccountId = savingsAccountId;
|
|
||||||
history.previousSavingsAccountId = previousSavingsAccountId;
|
|
||||||
history.previousRenewalId = previousRenewalId;
|
|
||||||
history.renewalNo = renewalNo;
|
|
||||||
history.previousPrincipal = previousPrincipal;
|
|
||||||
history.interestEarned = interestEarned;
|
|
||||||
history.maturityAmount = maturityAmount;
|
|
||||||
history.renewedAmount = renewedAmount;
|
|
||||||
history.interestRate = interestRate;
|
|
||||||
history.startDate = startDate;
|
|
||||||
history.maturityDate = maturityDate;
|
|
||||||
history.tenure = tenure;
|
|
||||||
history.tenureFrequencyEnum = tenureFrequencyEnum;
|
|
||||||
history.renewalType = renewalType;
|
|
||||||
history.renewalStatus = STATUS_ACTIVE;
|
|
||||||
history.renewedOn = renewedOn;
|
|
||||||
history.createdById = createdById;
|
|
||||||
return history;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Called on the previous term's row once its successor takes over. */
|
|
||||||
public void markMatured() {
|
|
||||||
this.renewalStatus = STATUS_MATURED;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fills in what this term actually earned and matured at, which is only known once it closes. Written on the term's
|
|
||||||
* own row so each term reports its own outcome rather than that of its predecessor.
|
|
||||||
*/
|
|
||||||
public void recordOutcome(final BigDecimal interestEarned, final BigDecimal maturityAmount) {
|
|
||||||
this.interestEarned = interestEarned;
|
|
||||||
this.maturityAmount = maturityAmount;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-30
@@ -1,30 +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
|
|
||||||
*/
|
|
||||||
package org.apache.fineract.portfolio.savings.domain;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
|
||||||
|
|
||||||
public interface FixedDepositRenewalHistoryRepository extends JpaRepository<FixedDepositRenewalHistory, Long> {
|
|
||||||
|
|
||||||
/** One FD's whole history, oldest term first. */
|
|
||||||
List<FixedDepositRenewalHistory> findByFdAccountNumberOrderByRenewalNoAsc(String fdAccountNumber);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An account's terms, newest first.
|
|
||||||
* <p>
|
|
||||||
* Deliberately a list, not a single row: an FD that renews on the same account has one row per term all pointing at
|
|
||||||
* that one account, so callers wanting the term currently running take the first element. Empty for an account that
|
|
||||||
* pre-dates this table or was never renewed.
|
|
||||||
*/
|
|
||||||
List<FixedDepositRenewalHistory> findBySavingsAccountIdOrderByRenewalNoDesc(Long savingsAccountId);
|
|
||||||
}
|
|
||||||
+7
-11
@@ -18,29 +18,25 @@
|
|||||||
*/
|
*/
|
||||||
package org.apache.fineract.portfolio.savings.handler;
|
package org.apache.fineract.portfolio.savings.handler;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.apache.fineract.commands.annotation.CommandType;
|
import org.apache.fineract.commands.annotation.CommandType;
|
||||||
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
|
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
|
||||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||||
import org.apache.fineract.portfolio.savings.service.DepositAccountWritePlatformService;
|
import org.apache.fineract.portfolio.savings.service.FdBaseInterestRateWritePlatformService;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@CommandType(entity = "FIXEDDEPOSITACCOUNT", action = "PAYMENT")
|
@CommandType(entity = "CBSBASEINTERESTRATE", action = "CREATE")
|
||||||
public class PaymentFixedDepositAccountCommandHandler implements NewCommandSourceHandler {
|
@RequiredArgsConstructor
|
||||||
|
public class CreateCbsBaseInterestRateCommandHandler implements NewCommandSourceHandler {
|
||||||
|
|
||||||
private final DepositAccountWritePlatformService depositAccountWritePlatformService;
|
private final FdBaseInterestRateWritePlatformService writePlatformService;
|
||||||
|
|
||||||
@Autowired
|
|
||||||
public PaymentFixedDepositAccountCommandHandler(final DepositAccountWritePlatformService depositAccountWritePlatformService) {
|
|
||||||
this.depositAccountWritePlatformService = depositAccountWritePlatformService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
@Override
|
@Override
|
||||||
public CommandProcessingResult processCommand(final JsonCommand command) {
|
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||||
return this.depositAccountWritePlatformService.processFdPayment(command.entityId(), command);
|
return this.writePlatformService.saveInterestRates(command);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+1
-3
@@ -189,7 +189,6 @@ public class DepositAccountInterestRateChartReadPlatformServiceImpl implements D
|
|||||||
.append("ircd.id as ircdId, ircd.description as ircdDescription, ircd.period_type_enum ircdPeriodTypeId, ")
|
.append("ircd.id as ircdId, ircd.description as ircdDescription, ircd.period_type_enum ircdPeriodTypeId, ")
|
||||||
.append("ircd.from_period as ircdFromPeriod, ircd.to_period as ircdToPeriod, ircd.amount_range_from as ircdAmountRangeFrom, ")
|
.append("ircd.from_period as ircdFromPeriod, ircd.to_period as ircdToPeriod, ircd.amount_range_from as ircdAmountRangeFrom, ")
|
||||||
.append("ircd.amount_range_to as ircdAmountRangeTo, ircd.annual_interest_rate as ircdAnnualInterestRate, ")
|
.append("ircd.amount_range_to as ircdAmountRangeTo, ircd.annual_interest_rate as ircdAnnualInterestRate, ")
|
||||||
.append("ircd.senior_citizen_rate as ircdSeniorCitizenRate, ")
|
|
||||||
.append("curr.code as currencyCode, curr.name as currencyName, curr.internationalized_name_code as currencyNameCode, ")
|
.append("curr.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("sa.id as accountId, sa.account_no as accountNumber, ").append("iri.id as iriId, ")
|
.append("sa.id as accountId, sa.account_no as accountNumber, ").append("iri.id as iriId, ")
|
||||||
@@ -347,7 +346,6 @@ public class DepositAccountInterestRateChartReadPlatformServiceImpl implements D
|
|||||||
final BigDecimal amountRangeFrom = rs.getBigDecimal("ircdAmountRangeFrom");
|
final BigDecimal amountRangeFrom = rs.getBigDecimal("ircdAmountRangeFrom");
|
||||||
final BigDecimal amountRangeTo = rs.getBigDecimal("ircdAmountRangeTo");
|
final BigDecimal amountRangeTo = rs.getBigDecimal("ircdAmountRangeTo");
|
||||||
final BigDecimal annualInterestRate = rs.getBigDecimal("ircdAnnualInterestRate");
|
final BigDecimal annualInterestRate = rs.getBigDecimal("ircdAnnualInterestRate");
|
||||||
final BigDecimal seniorCitizenRate = rs.getBigDecimal("ircdSeniorCitizenRate");
|
|
||||||
|
|
||||||
// currency Slabs
|
// currency Slabs
|
||||||
final String currencyCode = rs.getString("currencyCode");
|
final String currencyCode = rs.getString("currencyCode");
|
||||||
@@ -361,7 +359,7 @@ public class DepositAccountInterestRateChartReadPlatformServiceImpl implements D
|
|||||||
currencyNameCode);
|
currencyNameCode);
|
||||||
|
|
||||||
return DepositAccountInterestRateChartSlabData.instance(id, description, periodType, fromPeriod, toPeriod, amountRangeFrom,
|
return DepositAccountInterestRateChartSlabData.instance(id, description, periodType, fromPeriod, toPeriod, amountRangeFrom,
|
||||||
amountRangeTo, annualInterestRate, currency, seniorCitizenRate);
|
amountRangeTo, annualInterestRate, currency);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
-6
@@ -46,12 +46,6 @@ public interface DepositAccountReadPlatformService {
|
|||||||
|
|
||||||
DepositAccountData retrieveOneWithClosureTemplate(DepositAccountType depositAccountType, Long accountId);
|
DepositAccountData retrieveOneWithClosureTemplate(DepositAccountType depositAccountType, Long accountId);
|
||||||
|
|
||||||
/**
|
|
||||||
* As {@link #retrieveOneWithClosureTemplate}, but for the FD Payment action -- so its option list also carries
|
|
||||||
* PARTIAL_REINVEST, which the closure commands deliberately exclude because only Payment implements it.
|
|
||||||
*/
|
|
||||||
DepositAccountData retrieveOneWithPaymentTemplate(DepositAccountType depositAccountType, Long accountId);
|
|
||||||
|
|
||||||
DepositAccountData retrieveOneWithChartSlabs(DepositAccountType depositAccountType, Long productId);
|
DepositAccountData retrieveOneWithChartSlabs(DepositAccountType depositAccountType, Long productId);
|
||||||
|
|
||||||
Collection<SavingsAccountTransactionData> retrieveAllTransactions(DepositAccountType depositAccountType, Long accountId);
|
Collection<SavingsAccountTransactionData> retrieveAllTransactions(DepositAccountType depositAccountType, Long accountId);
|
||||||
|
|||||||
+3
-182
@@ -321,9 +321,6 @@ public class DepositAccountReadPlatformServiceImpl implements DepositAccountRead
|
|||||||
((org.apache.fineract.portfolio.savings.data.RecurringDepositAccountData) account).setJointHolders(jointHolders);
|
((org.apache.fineract.portfolio.savings.data.RecurringDepositAccountData) account).setJointHolders(jointHolders);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (account instanceof org.apache.fineract.portfolio.savings.data.FixedDepositAccountData) {
|
|
||||||
applyReinvestmentLinks((org.apache.fineract.portfolio.savings.data.FixedDepositAccountData) account, accountId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return account;
|
return account;
|
||||||
|
|
||||||
@@ -332,158 +329,6 @@ public class DepositAccountReadPlatformServiceImpl implements DepositAccountRead
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Populates the "reinvested from" / "reinvested into" links on a Fixed Deposit account, so the
|
|
||||||
* account-view page can show either direction of a "Re-Invest Maturity Amount"/"Re-Invest
|
|
||||||
* Principal Only" closure. There is only one column (reinvested_from_savings_id, set on the NEW
|
|
||||||
* account) -- "reinvested into" is found on the OLD account by reverse-looking up whichever row
|
|
||||||
* points at it.
|
|
||||||
*/
|
|
||||||
private void applyReinvestmentLinks(final org.apache.fineract.portfolio.savings.data.FixedDepositAccountData result,
|
|
||||||
final Long accountId) {
|
|
||||||
final String fromSql = "select src.id as id, src.account_no as accountNo "
|
|
||||||
+ "from m_deposit_account_term_and_preclosure t "
|
|
||||||
+ "join m_savings_account src on src.id = t.reinvested_from_savings_id " + "where t.savings_account_id = ?";
|
|
||||||
final String intoSql = "select dest.id as id, dest.account_no as accountNo "
|
|
||||||
+ "from m_deposit_account_term_and_preclosure t " + "join m_savings_account dest on dest.id = t.savings_account_id "
|
|
||||||
+ "where t.reinvested_from_savings_id = ?";
|
|
||||||
|
|
||||||
Long fromId = null;
|
|
||||||
String fromNo = null;
|
|
||||||
final java.util.List<java.util.Map<String, Object>> fromRows = this.jdbcTemplate.queryForList(fromSql, accountId);
|
|
||||||
if (!fromRows.isEmpty()) {
|
|
||||||
fromId = ((Number) fromRows.get(0).get("id")).longValue();
|
|
||||||
fromNo = (String) fromRows.get(0).get("accountNo");
|
|
||||||
}
|
|
||||||
|
|
||||||
Long intoId = null;
|
|
||||||
String intoNo = null;
|
|
||||||
final java.util.List<java.util.Map<String, Object>> intoRows = this.jdbcTemplate.queryForList(intoSql, accountId);
|
|
||||||
if (!intoRows.isEmpty()) {
|
|
||||||
intoId = ((Number) intoRows.get(0).get("id")).longValue();
|
|
||||||
intoNo = (String) intoRows.get(0).get("accountNo");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fromId != null || intoId != null) {
|
|
||||||
result.setReinvestmentLinks(fromId, fromNo, intoId, intoNo);
|
|
||||||
}
|
|
||||||
|
|
||||||
// The number to actually show as "the FD account number" -- the original account's account_no
|
|
||||||
// if this account has been through a renewal, otherwise this account's own account_no.
|
|
||||||
final String termSql = "select fd_account_number, renewal_cycle_no from m_deposit_account_term_and_preclosure "
|
|
||||||
+ "where savings_account_id = ?";
|
|
||||||
final java.util.List<java.util.Map<String, Object>> termRows = this.jdbcTemplate.queryForList(termSql, accountId);
|
|
||||||
final String fdAccountNumber = termRows.isEmpty() ? null : (String) termRows.get(0).get("fd_account_number");
|
|
||||||
final Object cycleNo = termRows.isEmpty() ? null : termRows.get(0).get("renewal_cycle_no");
|
|
||||||
final String rootFdAccountNumber = fdAccountNumber != null ? fdAccountNumber : result.getAccountNo();
|
|
||||||
result.setFdAccountNumber(rootFdAccountNumber);
|
|
||||||
result.setRenewalCycleNo(cycleNo == null ? 0 : ((Number) cycleNo).intValue());
|
|
||||||
result.setRenewalHistory(retrieveRenewalHistory(rootFdAccountNumber));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Every term of one logical FD, oldest first.
|
|
||||||
* <p>
|
|
||||||
* Driven by m_fd_renewal_history, which records each rollover event -- interest the closing term earned, what it
|
|
||||||
* matured at, how much carried forward, the rate applied, automatic or by a user -- rather than deriving the chain by
|
|
||||||
* joining account rows, which cannot express any of that. The account row is joined in for its number and live
|
|
||||||
* status. Returns an empty list for an FD that has never been renewed, so callers can treat "has renewal history" as
|
|
||||||
* simply a non-empty list.
|
|
||||||
*/
|
|
||||||
private java.util.List<java.util.Map<String, Object>> retrieveRenewalHistory(final String fdAccountNumber) {
|
|
||||||
// Every row states its own term's figures, so this is a straight read: interest_earned and
|
|
||||||
// maturity_amount are filled in when that term closes, and stay null on the term still running,
|
|
||||||
// which the caller renders as its projected maturity instead.
|
|
||||||
final String sql = "select h.id as historyId, h.renewal_no as renewalCycleNo, h.savings_account_id as accountId, "
|
|
||||||
+ "sa.account_no as accountNo, h.renewed_amount as depositAmount, h.interest_rate as interestRate, "
|
|
||||||
+ "h.start_date as activatedOnDate, h.maturity_date as maturityDate, h.tenure as tenure, "
|
|
||||||
+ "h.tenure_frequency_enum as tenureFrequencyEnum, "
|
|
||||||
+ "h.renewal_type as renewalType, h.renewal_status as renewalStatus, sa.status_enum as statusEnum, "
|
|
||||||
+ "h.interest_earned as interestEarned, h.maturity_amount as maturedAmount, "
|
|
||||||
+ "t.maturity_amount as projectedMaturityAmount " + "from m_fd_renewal_history h "
|
|
||||||
+ "join m_savings_account sa on sa.id = h.savings_account_id "
|
|
||||||
+ "left join m_deposit_account_term_and_preclosure t on t.savings_account_id = sa.id "
|
|
||||||
+ "where h.fd_account_number = ? " + "order by h.renewal_no asc";
|
|
||||||
final java.util.List<java.util.Map<String, Object>> rows = this.jdbcTemplate.queryForList(sql, fdAccountNumber);
|
|
||||||
// Any row here is a renewal event that was actually recorded, so even one is worth returning --
|
|
||||||
// discarding a lone row used to hide a genuine renewal (and its interest figure) behind the
|
|
||||||
// account-derived fallback, which then returned nothing at all for it.
|
|
||||||
if (!rows.isEmpty()) {
|
|
||||||
attachArchivedChartSlabs(rows);
|
|
||||||
return rows;
|
|
||||||
}
|
|
||||||
// Chains that renewed before m_fd_renewal_history existed have no rows to read, so fall back to
|
|
||||||
// reconstructing what the account rows alone can tell us. The per-term interest/renewal-type
|
|
||||||
// columns are simply absent for those, which the UI renders as blanks rather than wrong values.
|
|
||||||
return retrieveRenewalHistoryFromAccounts(fdAccountNumber);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attaches each term's archived rate-by-tenure chart (as a "chartSlabs" list) onto its own row.
|
|
||||||
* <p>
|
|
||||||
* A term's chart is only archived when it closes via an in-place renewal (see
|
|
||||||
* {@code DepositAccountDomainServiceJpa#archiveChartSlabs}), so the currently-running term and any term from before
|
|
||||||
* that archiving existed simply get an empty list here -- callers should fall back to the account's live
|
|
||||||
* accountChart.chartSlabs for those. Fetched in one batched query keyed by history id, not one query per row, since
|
|
||||||
* this runs once per FD account view.
|
|
||||||
*/
|
|
||||||
private void attachArchivedChartSlabs(final java.util.List<java.util.Map<String, Object>> rows) {
|
|
||||||
final java.util.List<Long> historyIds = new java.util.ArrayList<>();
|
|
||||||
for (final java.util.Map<String, Object> row : rows) {
|
|
||||||
final Object id = row.get("historyId");
|
|
||||||
if (id != null) {
|
|
||||||
historyIds.add(((Number) id).longValue());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (historyIds.isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final String placeholders = String.join(",", java.util.Collections.nCopies(historyIds.size(), "?"));
|
|
||||||
final String sql = "select fd_renewal_history_id as historyId, chart_name as chartName, description, "
|
|
||||||
+ "period_type_enum as periodTypeEnum, from_period as fromPeriod, to_period as toPeriod, "
|
|
||||||
+ "annual_interest_rate as annualInterestRate, senior_citizen_rate as seniorCitizenRate "
|
|
||||||
+ "from m_fd_renewal_chart_slab_history " + "where fd_renewal_history_id in (" + placeholders + ") "
|
|
||||||
+ "order by from_period asc";
|
|
||||||
final java.util.List<java.util.Map<String, Object>> slabRows = this.jdbcTemplate.queryForList(sql, historyIds.toArray());
|
|
||||||
|
|
||||||
final java.util.Map<Long, java.util.List<java.util.Map<String, Object>>> slabsByHistoryId = new java.util.HashMap<>();
|
|
||||||
for (final java.util.Map<String, Object> slabRow : slabRows) {
|
|
||||||
final Long historyId = ((Number) slabRow.remove("historyId")).longValue();
|
|
||||||
slabsByHistoryId.computeIfAbsent(historyId, key -> new java.util.ArrayList<>()).add(slabRow);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (final java.util.Map<String, Object> row : rows) {
|
|
||||||
final Object id = row.get("historyId");
|
|
||||||
final Long historyId = id == null ? null : ((Number) id).longValue();
|
|
||||||
row.put("chartSlabs", slabsByHistoryId.getOrDefault(historyId, java.util.Collections.emptyList()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private java.util.List<java.util.Map<String, Object>> retrieveRenewalHistoryFromAccounts(final String fdAccountNumber) {
|
|
||||||
// Deliberately emits the SAME column labels as retrieveRenewalHistory above, so a caller never has
|
|
||||||
// to know which path produced the rows. What the account rows genuinely cannot tell us -- each
|
|
||||||
// term's interest earned, and whether the rollover was automatic or done by staff -- is returned as
|
|
||||||
// an explicit null rather than omitted, so those render as blanks instead of the previous behaviour
|
|
||||||
// where a missing renewalStatus made every term, including the running one, look matured.
|
|
||||||
final String sql = "select null as historyId, coalesce(t.renewal_cycle_no, 0) as renewalCycleNo, "
|
|
||||||
+ "sa.id as accountId, sa.account_no as accountNo, t.deposit_amount as depositAmount, "
|
|
||||||
+ "sa.nominal_annual_interest_rate as interestRate, sa.activatedon_date as activatedOnDate, "
|
|
||||||
+ "t.maturity_date as maturityDate, t.deposit_period as tenure, "
|
|
||||||
+ "t.deposit_period_frequency_enum as tenureFrequencyEnum, null as renewalType, "
|
|
||||||
+ "case when sa.status_enum = 300 then 'ACTIVE' else 'MATURED' end as renewalStatus, "
|
|
||||||
+ "sa.status_enum as statusEnum, null as interestEarned, "
|
|
||||||
+ "case when sa.status_enum = 300 then null else t.maturity_amount end as maturedAmount, "
|
|
||||||
+ "case when sa.status_enum = 300 then t.maturity_amount else null end as projectedMaturityAmount "
|
|
||||||
+ "from m_savings_account sa "
|
|
||||||
+ "join m_deposit_account_term_and_preclosure t on t.savings_account_id = sa.id "
|
|
||||||
+ "where sa.account_no = ? or t.fd_account_number = ? " + "order by coalesce(t.renewal_cycle_no, 0) asc";
|
|
||||||
final java.util.List<java.util.Map<String, Object>> rows = this.jdbcTemplate.queryForList(sql, fdAccountNumber,
|
|
||||||
fdAccountNumber);
|
|
||||||
// A single row here means this FD simply never renewed -- one account, one term -- so there is no
|
|
||||||
// history to show. Unlike the primary path, where even one recorded row is a real renewal event.
|
|
||||||
return rows.size() > 1 ? rows : java.util.Collections.emptyList();
|
|
||||||
}
|
|
||||||
|
|
||||||
private java.util.List<java.util.Map<String, Object>> retrieveJointHolders(final Long accountId) {
|
private java.util.List<java.util.Map<String, Object>> retrieveJointHolders(final Long accountId) {
|
||||||
final String sql = "SELECT jh.id, jh.client_id, jh.cif_number, jh.display_name, "
|
final String sql = "SELECT jh.id, jh.client_id, jh.cif_number, jh.display_name, "
|
||||||
+ "jh.share_percentage, jh.signing_authority, jh.role, jh.is_primary, jh.is_mandatory "
|
+ "jh.share_percentage, jh.signing_authority, jh.role, jh.is_primary, jh.is_mandatory "
|
||||||
@@ -504,30 +349,8 @@ public class DepositAccountReadPlatformServiceImpl implements DepositAccountRead
|
|||||||
}, accountId);
|
}, accountId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Closure options for the {@code close} and {@code prematureClose} commands. PARTIAL_REINVEST is excluded: only the
|
|
||||||
* Payment command's handler implements it, and only its dialog collects the kept-amount and destination account it
|
|
||||||
* needs. Offered here it would appear in the FD and RD close dialogs, where the closure handlers have no branch for
|
|
||||||
* it and would fall through to paying the whole balance out in cash.
|
|
||||||
*/
|
|
||||||
@Override
|
@Override
|
||||||
public DepositAccountData retrieveOneWithClosureTemplate(final DepositAccountType depositAccountType, final Long accountId) {
|
public DepositAccountData retrieveOneWithClosureTemplate(final DepositAccountType depositAccountType, final Long accountId) {
|
||||||
final boolean includePartialReinvest = false;
|
|
||||||
return retrieveOneWithClosureTemplate(depositAccountType, accountId, includePartialReinvest);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The same template, but including PARTIAL_REINVEST -- for the Payment command, which is the one action that
|
|
||||||
* implements it.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public DepositAccountData retrieveOneWithPaymentTemplate(final DepositAccountType depositAccountType, final Long accountId) {
|
|
||||||
final boolean includePartialReinvest = true;
|
|
||||||
return retrieveOneWithClosureTemplate(depositAccountType, accountId, includePartialReinvest);
|
|
||||||
}
|
|
||||||
|
|
||||||
private DepositAccountData retrieveOneWithClosureTemplate(final DepositAccountType depositAccountType, final Long accountId,
|
|
||||||
final boolean includePartialReinvest) {
|
|
||||||
try {
|
try {
|
||||||
this.context.authenticatedUser();
|
this.context.authenticatedUser();
|
||||||
|
|
||||||
@@ -544,9 +367,7 @@ public class DepositAccountReadPlatformServiceImpl implements DepositAccountRead
|
|||||||
DepositAccountData account = this.jdbcTemplate.queryForObject(sqlBuilder.toString(), depositAccountMapper, accountId,
|
DepositAccountData account = this.jdbcTemplate.queryForObject(sqlBuilder.toString(), depositAccountMapper, accountId,
|
||||||
depositAccountType.getValue());
|
depositAccountType.getValue());
|
||||||
Collection<EnumOptionData> onAccountClosureOptions = SavingsEnumerations
|
Collection<EnumOptionData> onAccountClosureOptions = SavingsEnumerations
|
||||||
.depositAccountOnClosureType(Arrays.stream(DepositAccountOnClosureType.values())
|
.depositAccountOnClosureType(DepositAccountOnClosureType.values());
|
||||||
.filter(type -> includePartialReinvest || !type.isPartialReinvest())
|
|
||||||
.toArray(DepositAccountOnClosureType[]::new));
|
|
||||||
final Collection<PaymentTypeData> paymentTypeOptions = this.paymentTypeReadPlatformService.retrieveAllPaymentTypes();
|
final Collection<PaymentTypeData> paymentTypeOptions = this.paymentTypeReadPlatformService.retrieveAllPaymentTypes();
|
||||||
final Collection<SavingsAccountData> savingsAccountDatas = this.savingsAccountReadPlatformService
|
final Collection<SavingsAccountData> savingsAccountDatas = this.savingsAccountReadPlatformService
|
||||||
.retrieveActiveForLookup(account.getClientId(), DepositAccountType.SAVINGS_DEPOSIT);
|
.retrieveActiveForLookup(account.getClientId(), DepositAccountType.SAVINGS_DEPOSIT);
|
||||||
@@ -733,7 +554,7 @@ public class DepositAccountReadPlatformServiceImpl implements DepositAccountRead
|
|||||||
fieldOfficerOptions, interestCompoundingPeriodTypeOptions, interestPostingPeriodTypeOptions,
|
fieldOfficerOptions, interestCompoundingPeriodTypeOptions, interestPostingPeriodTypeOptions,
|
||||||
interestCalculationTypeOptions, interestCalculationDaysInYearTypeOptions, lockinPeriodFrequencyTypeOptions,
|
interestCalculationTypeOptions, interestCalculationDaysInYearTypeOptions, lockinPeriodFrequencyTypeOptions,
|
||||||
withdrawalFeeTypeOptions, transactions, charges, chargeOptions, preClosurePenalInterestOnTypeOptions,
|
withdrawalFeeTypeOptions, transactions, charges, chargeOptions, preClosurePenalInterestOnTypeOptions,
|
||||||
periodFrequencyTypeOptions, savingsAccountDatas, maturityInstructionOptions);
|
periodFrequencyTypeOptions);
|
||||||
template = RecurringDepositAccountData.withInterestChartAndRecurringDetails((RecurringDepositAccountData) template,
|
template = RecurringDepositAccountData.withInterestChartAndRecurringDetails((RecurringDepositAccountData) template,
|
||||||
accountChart, null, null);
|
accountChart, null, null);
|
||||||
// Re-apply CBS product fields lost during factory method object creation
|
// Re-apply CBS product fields lost during factory method object creation
|
||||||
@@ -805,7 +626,7 @@ public class DepositAccountReadPlatformServiceImpl implements DepositAccountRead
|
|||||||
fieldOfficerOptions, interestCompoundingPeriodTypeOptions, interestPostingPeriodTypeOptions,
|
fieldOfficerOptions, interestCompoundingPeriodTypeOptions, interestPostingPeriodTypeOptions,
|
||||||
interestCalculationTypeOptions, interestCalculationDaysInYearTypeOptions, lockinPeriodFrequencyTypeOptions,
|
interestCalculationTypeOptions, interestCalculationDaysInYearTypeOptions, lockinPeriodFrequencyTypeOptions,
|
||||||
withdrawalFeeTypeOptions, transactions, charges, chargeOptions, preClosurePenalInterestOnTypeOptions,
|
withdrawalFeeTypeOptions, transactions, charges, chargeOptions, preClosurePenalInterestOnTypeOptions,
|
||||||
periodFrequencyTypeOptions, savingsAccountDatas, maturityInstructionOptions);
|
periodFrequencyTypeOptions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-96
@@ -48,7 +48,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.data.DataValidatorBuilder;
|
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
|
||||||
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.PlatformApiDataValidationException;
|
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
|
||||||
import org.apache.fineract.infrastructure.core.exception.PlatformServiceUnavailableException;
|
import org.apache.fineract.infrastructure.core.exception.PlatformServiceUnavailableException;
|
||||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||||
@@ -913,67 +912,6 @@ public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements Depo
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public CommandProcessingResult processFdPayment(final Long savingsId, final JsonCommand command) {
|
|
||||||
final AppUser user = this.context.authenticatedUser();
|
|
||||||
|
|
||||||
this.depositAccountTransactionDataValidator.validatePayment(command);
|
|
||||||
|
|
||||||
final Map<String, Object> changes = new LinkedHashMap<>();
|
|
||||||
final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes);
|
|
||||||
|
|
||||||
// Payment intentionally spans both states -- unlike Close (Matured only) and Premature Close
|
|
||||||
// (Active only) -- but it must still be one of those two. Without this, a closed or
|
|
||||||
// pre-maturely-closed deposit could be reopened: the reinvest branches put the account back to
|
|
||||||
// ACTIVE and never reach the close()/prematureClosure() calls that would otherwise reject it.
|
|
||||||
final FixedDepositAccount account = (FixedDepositAccount) this.depositAccountAssembler.assembleFrom(savingsId,
|
|
||||||
DepositAccountType.FIXED_DEPOSIT);
|
|
||||||
if (!account.isActive() && !account.isMatured()) {
|
|
||||||
throw new GeneralPlatformDomainRuleException("error.msg.fixeddeposit.payment.account.not.active.or.matured",
|
|
||||||
"Payment can only be processed on an active or matured fixed deposit account.", savingsId);
|
|
||||||
}
|
|
||||||
checkClientOrGroupActive(account);
|
|
||||||
|
|
||||||
this.depositAccountDomainService.handleFDAccountPayment(account, paymentDetail, user, command, changes);
|
|
||||||
|
|
||||||
// Reinvest Principal Only opens a second account and the domain method withdrew the principal out
|
|
||||||
// of this one against the transfer-suspense GL. Post the matching credit leg so the funds actually
|
|
||||||
// land in the new deposit -- same sequence updateMaturityDetails() uses for the automated path.
|
|
||||||
if (changes.get("reinvestedDepositId") != null) {
|
|
||||||
final Long reinvestedDepositId = (Long) changes.get("reinvestedDepositId");
|
|
||||||
final FixedDepositAccount reinvestAccount = (FixedDepositAccount) this.depositAccountAssembler
|
|
||||||
.assembleFrom(reinvestedDepositId, DepositAccountType.FIXED_DEPOSIT);
|
|
||||||
Money amountForDeposit = Money.of(reinvestAccount.getCurrency(), reinvestAccount.getDepositAmount());
|
|
||||||
final Money activationChargeAmount = getActivationCharge(reinvestAccount);
|
|
||||||
if (activationChargeAmount.isGreaterThanZero()) {
|
|
||||||
payActivationCharge(reinvestAccount);
|
|
||||||
amountForDeposit = amountForDeposit.plus(activationChargeAmount);
|
|
||||||
}
|
|
||||||
final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(command.extractLocale());
|
|
||||||
final LocalDate closedOnDate = command.localDateValueOfParameterNamed(SavingsApiConstants.closedOnDateParamName);
|
|
||||||
final boolean isInternalTransferLeg = true;
|
|
||||||
this.depositAccountDomainService.handleFDDeposit(reinvestAccount, fmt, closedOnDate, amountForDeposit.getAmount(), null,
|
|
||||||
isInternalTransferLeg);
|
|
||||||
}
|
|
||||||
|
|
||||||
final String noteText = command.stringValueOfParameterNamed("note");
|
|
||||||
if (StringUtils.isNotBlank(noteText)) {
|
|
||||||
final Note note = Note.savingNote(account, noteText);
|
|
||||||
changes.put("note", noteText);
|
|
||||||
this.noteRepository.save(note);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new CommandProcessingResultBuilder() //
|
|
||||||
.withEntityId(savingsId) //
|
|
||||||
.withOfficeId(account.officeId()) //
|
|
||||||
.withClientId(account.clientId()) //
|
|
||||||
.withGroupId(account.groupId()) //
|
|
||||||
.withSavingsId(savingsId) //
|
|
||||||
.with(changes)//
|
|
||||||
.build();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CommandProcessingResult prematureCloseRDAccount(final Long savingsId, final JsonCommand command) {
|
public CommandProcessingResult prematureCloseRDAccount(final Long savingsId, final JsonCommand command) {
|
||||||
final AppUser user = this.context.authenticatedUser();
|
final AppUser user = this.context.authenticatedUser();
|
||||||
@@ -1455,37 +1393,18 @@ public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements Depo
|
|||||||
this.depositAccountDomainService.handleFDAccountMaturityClosure(fdAccount, null, user, fmt, fdAccount.maturityDate(),
|
this.depositAccountDomainService.handleFDAccountMaturityClosure(fdAccount, null, user, fmt, fdAccount.maturityDate(),
|
||||||
fdAccount.getOnAccountClosureId(), toSavingsId, "Apply maturity instructions", changes);
|
fdAccount.getOnAccountClosureId(), toSavingsId, "Apply maturity instructions", changes);
|
||||||
|
|
||||||
// Every maturity-instruction branch takes the balance out of this account through
|
|
||||||
// SavingsAccountDomainServiceJpa#handleWithdrawal, which posts that withdrawal's journal
|
|
||||||
// entries itself. existingTransactionIds was snapshotted at the top of this method --
|
|
||||||
// before that withdrawal existed -- so the postJournalEntries() call at the end would
|
|
||||||
// treat it as new and post it a second time. Interest postings created by
|
|
||||||
// updateMaturityStatus() above are still genuinely unposted at this point and must stay
|
|
||||||
// out of the snapshot so that final call can post them exactly once.
|
|
||||||
markWithdrawalsAsAlreadyPosted(account, existingTransactionIds);
|
|
||||||
|
|
||||||
if (changes.get("reinvestedDepositId") != null) {
|
if (changes.get("reinvestedDepositId") != null) {
|
||||||
Long reinvestedDepositId = (Long) changes.get("reinvestedDepositId");
|
Long reinvestedDepositId = (Long) changes.get("reinvestedDepositId");
|
||||||
|
Money amountForDeposit = account.activateWithBalance();
|
||||||
final FixedDepositAccount reinvestAccount = (FixedDepositAccount) this.depositAccountAssembler
|
final FixedDepositAccount reinvestAccount = (FixedDepositAccount) this.depositAccountAssembler
|
||||||
.assembleFrom(reinvestedDepositId, DepositAccountType.FIXED_DEPOSIT);
|
.assembleFrom(reinvestedDepositId, DepositAccountType.FIXED_DEPOSIT);
|
||||||
// activateWithBalance() always returns zero here -- it exists to stop a manually
|
|
||||||
// opened FD from auto-crediting itself at activation (funding is supposed to be a
|
|
||||||
// separate, clerk-posted deposit). A reinvestment isn't that: the balance was just
|
|
||||||
// withdrawn from the matured account and needs to land in the new one as part of
|
|
||||||
// this same automated closure, so it has to come from what reInvest() actually
|
|
||||||
// carried over onto the new account, not from that unrelated activation guard.
|
|
||||||
Money amountForDeposit = Money.of(reinvestAccount.getCurrency(), reinvestAccount.getDepositAmount());
|
|
||||||
Money activationChargeAmount = getActivationCharge(reinvestAccount);
|
Money activationChargeAmount = getActivationCharge(reinvestAccount);
|
||||||
if (activationChargeAmount.isGreaterThanZero()) {
|
if (activationChargeAmount.isGreaterThanZero()) {
|
||||||
payActivationCharge(reinvestAccount);
|
payActivationCharge(reinvestAccount);
|
||||||
amountForDeposit = amountForDeposit.plus(activationChargeAmount);
|
amountForDeposit = amountForDeposit.plus(activationChargeAmount);
|
||||||
}
|
}
|
||||||
// Matching internal-transfer leg for the withdrawal the closure just made on the
|
|
||||||
// matured account: the funds never touch cash, so this posts against the
|
|
||||||
// transfer-suspense GL instead of debiting cash/vault.
|
|
||||||
final boolean isInternalTransferLeg = true;
|
|
||||||
this.depositAccountDomainService.handleFDDeposit(reinvestAccount, fmt, fdAccount.maturityDate(),
|
this.depositAccountDomainService.handleFDDeposit(reinvestAccount, fmt, fdAccount.maturityDate(),
|
||||||
amountForDeposit.getAmount(), null, isInternalTransferLeg);
|
amountForDeposit.getAmount(), null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (depositAccountType.isRecurringDeposit()) {
|
} else if (depositAccountType.isRecurringDeposit()) {
|
||||||
@@ -1502,19 +1421,6 @@ public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements Depo
|
|||||||
existingReversedTransactionIds.addAll(account.findExistingReversedTransactionIds());
|
existingReversedTransactionIds.addAll(account.findExistingReversedTransactionIds());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Marks the account's withdrawal (and withdrawal-fee) transactions as already accounted for, so a
|
|
||||||
* subsequent postJournalEntries() built on an older snapshot does not post them again. Used after a
|
|
||||||
* maturity closure, whose withdrawal leg posts its own journal entries.
|
|
||||||
*/
|
|
||||||
private void markWithdrawalsAsAlreadyPosted(final SavingsAccount account, final Set<Long> existingTransactionIds) {
|
|
||||||
for (final SavingsAccountTransaction transaction : account.getTransactions()) {
|
|
||||||
if (transaction.getId() != null && (transaction.isWithdrawal() || transaction.isWithdrawalFee())) {
|
|
||||||
existingTransactionIds.add(transaction.getId());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void postJournalEntries(final SavingsAccount savingsAccount, final Set<Long> existingTransactionIds,
|
private void postJournalEntries(final SavingsAccount savingsAccount, final Set<Long> existingTransactionIds,
|
||||||
final Set<Long> existingReversedTransactionIds) {
|
final Set<Long> existingReversedTransactionIds) {
|
||||||
|
|
||||||
|
|||||||
+128
-1
@@ -11,10 +11,16 @@
|
|||||||
*/
|
*/
|
||||||
package org.apache.fineract.portfolio.savings.service;
|
package org.apache.fineract.portfolio.savings.service;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
|
||||||
|
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
|
||||||
|
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
|
||||||
import org.apache.fineract.portfolio.savings.api.FdInterestRateSaveRequest;
|
import org.apache.fineract.portfolio.savings.api.FdInterestRateSaveRequest;
|
||||||
import org.apache.fineract.portfolio.savings.data.FdInterestRateEffectiveData;
|
import org.apache.fineract.portfolio.savings.data.FdInterestRateEffectiveData;
|
||||||
import org.apache.fineract.portfolio.savings.data.FdInterestRateSlabData;
|
import org.apache.fineract.portfolio.savings.data.FdInterestRateSlabData;
|
||||||
@@ -51,7 +57,9 @@ public class FdBaseInterestRateReadPlatformServiceImpl implements FdBaseInterest
|
|||||||
@Override
|
@Override
|
||||||
@Transactional
|
@Transactional
|
||||||
public FdInterestRateEffectiveData saveInterestRates(FdInterestRateSaveRequest request, Long createdById) {
|
public FdInterestRateEffectiveData saveInterestRates(FdInterestRateSaveRequest request, Long createdById) {
|
||||||
final String fdType = request.getFdType();
|
validateRequest(request);
|
||||||
|
|
||||||
|
final String fdType = request.getFdType().toUpperCase();
|
||||||
final LocalDate today = LocalDate.now(DateUtils.getDateTimeZoneOfTenant());
|
final LocalDate today = LocalDate.now(DateUtils.getDateTimeZoneOfTenant());
|
||||||
|
|
||||||
final List<FdBaseInterestRateEffective> activeVersions = effectiveRepository
|
final List<FdBaseInterestRateEffective> activeVersions = effectiveRepository
|
||||||
@@ -85,6 +93,125 @@ public class FdBaseInterestRateReadPlatformServiceImpl implements FdBaseInterest
|
|||||||
return toData(newVersion);
|
return toData(newVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void validateRequest(final FdInterestRateSaveRequest request) {
|
||||||
|
final List<ApiParameterError> errors = new ArrayList<>();
|
||||||
|
final DataValidatorBuilder validator = new DataValidatorBuilder(errors).resource("FD_BASE_INTEREST_RATE_VERSION");
|
||||||
|
|
||||||
|
if (request == null) {
|
||||||
|
validator.failWithCodeNoParameterAddedToErrorCode("request.cannot.be.null");
|
||||||
|
throwExceptionIfValidationWarningsExist(errors);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final String fdType = request.getFdType();
|
||||||
|
if (fdType == null || fdType.isBlank()) {
|
||||||
|
validator.parameter("fdType").failWithCode("cannot.be.blank");
|
||||||
|
} else if (!List.of("SINGLE", "DOUBLE", "SPECIAL").contains(fdType.toUpperCase())) {
|
||||||
|
validator.parameter("fdType").value(fdType).failWithCode("invalid.fd.type");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.getSlabs() == null || request.getSlabs().isEmpty()) {
|
||||||
|
validator.parameter("slabs").failWithCode("cannot.be.blank");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.getVersionLabel() != null && request.getVersionLabel().length() > 100) {
|
||||||
|
validator.parameter("versionLabel").value(request.getVersionLabel()).failWithCode("exceeds.max.length");
|
||||||
|
}
|
||||||
|
|
||||||
|
throwExceptionIfValidationWarningsExist(errors);
|
||||||
|
validateSlabs(request.getSlabs());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateSlabs(final List<FdInterestRateSaveRequest.SlabRequest> slabs) {
|
||||||
|
final List<ApiParameterError> errors = new ArrayList<>();
|
||||||
|
final DataValidatorBuilder validator = new DataValidatorBuilder(errors).resource("FD_BASE_INTEREST_RATE_VERSION");
|
||||||
|
|
||||||
|
if (slabs == null || slabs.isEmpty()) {
|
||||||
|
validator.parameter("slabs").failWithCode("cannot.be.blank");
|
||||||
|
throwExceptionIfValidationWarningsExist(errors);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<FdInterestRateSaveRequest.SlabRequest> sorted = new ArrayList<>(slabs);
|
||||||
|
sorted.sort(Comparator.comparing((FdInterestRateSaveRequest.SlabRequest slab) -> slab.getSrNo() == null ? Integer.MAX_VALUE : slab.getSrNo())
|
||||||
|
.thenComparing(slab -> slab.getFromPeriod() == null ? Integer.MAX_VALUE : slab.getFromPeriod()));
|
||||||
|
|
||||||
|
final Integer expectedPeriodType = sorted.get(0).getPeriodTypeEnum();
|
||||||
|
Integer expectedFrom = 1;
|
||||||
|
|
||||||
|
for (int i = 0; i < sorted.size(); i++) {
|
||||||
|
final FdInterestRateSaveRequest.SlabRequest slab = sorted.get(i);
|
||||||
|
final int srNo = slab.getSrNo() == null ? i + 1 : slab.getSrNo();
|
||||||
|
final String slabPrefix = "slabs[" + srNo + "].";
|
||||||
|
|
||||||
|
if (slab.getPeriodTypeEnum() == null) {
|
||||||
|
validator.parameter(slabPrefix + "periodTypeEnum").failWithCode("cannot.be.blank");
|
||||||
|
} else if (!slab.getPeriodTypeEnum().equals(expectedPeriodType)) {
|
||||||
|
validator.parameter(slabPrefix + "periodTypeEnum").value(slab.getPeriodTypeEnum())
|
||||||
|
.failWithCode("must.be.consistent.across.all.slabs");
|
||||||
|
}
|
||||||
|
|
||||||
|
final Integer fromPeriod = slab.getFromPeriod();
|
||||||
|
if (fromPeriod == null) {
|
||||||
|
validator.parameter(slabPrefix + "fromPeriod").failWithCode("cannot.be.blank");
|
||||||
|
} else if (fromPeriod < 0) {
|
||||||
|
validator.parameter(slabPrefix + "fromPeriod").value(fromPeriod).failWithCode("must.be.non.negative");
|
||||||
|
} else if (!fromPeriod.equals(expectedFrom)) {
|
||||||
|
if (i == 0) {
|
||||||
|
validator.parameter(slabPrefix + "fromPeriod").value(fromPeriod).failWithCode("first.slab.must.start.at.one");
|
||||||
|
} else if (fromPeriod < expectedFrom) {
|
||||||
|
validator.parameter(slabPrefix + "fromPeriod").value(fromPeriod).failWithCode("must.not.overlap.previous.range");
|
||||||
|
} else {
|
||||||
|
validator.parameter(slabPrefix + "fromPeriod").value(fromPeriod).failWithCode("must.be.continuous.with.previous.range");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final Integer toPeriod = slab.getToPeriod();
|
||||||
|
if (i < sorted.size() - 1) {
|
||||||
|
if (toPeriod == null) {
|
||||||
|
validator.parameter(slabPrefix + "toPeriod").failWithCode("cannot.be.blank.for.intermediate.slab");
|
||||||
|
} else {
|
||||||
|
if (toPeriod < 0) {
|
||||||
|
validator.parameter(slabPrefix + "toPeriod").value(toPeriod).failWithCode("must.be.non.negative");
|
||||||
|
}
|
||||||
|
if (fromPeriod != null && toPeriod < fromPeriod) {
|
||||||
|
validator.parameter(slabPrefix + "toPeriod").value(toPeriod).failWithCode("cannot.be.less.than.fromPeriod");
|
||||||
|
}
|
||||||
|
expectedFrom = toPeriod + 1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (toPeriod != null) {
|
||||||
|
if (toPeriod < 0) {
|
||||||
|
validator.parameter(slabPrefix + "toPeriod").value(toPeriod).failWithCode("must.be.non.negative");
|
||||||
|
}
|
||||||
|
if (fromPeriod != null && toPeriod < fromPeriod) {
|
||||||
|
validator.parameter(slabPrefix + "toPeriod").value(toPeriod).failWithCode("cannot.be.less.than.fromPeriod");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final BigDecimal annualRate = slab.getAnnualInterestRate();
|
||||||
|
if (annualRate == null) {
|
||||||
|
validator.parameter(slabPrefix + "annualInterestRate").failWithCode("cannot.be.blank");
|
||||||
|
} else if (annualRate.compareTo(BigDecimal.ZERO) < 0) {
|
||||||
|
validator.parameter(slabPrefix + "annualInterestRate").value(annualRate).failWithCode("must.be.non.negative");
|
||||||
|
}
|
||||||
|
|
||||||
|
final BigDecimal seniorRate = slab.getSeniorCitizenRate();
|
||||||
|
if (seniorRate != null && seniorRate.compareTo(BigDecimal.ZERO) < 0) {
|
||||||
|
validator.parameter(slabPrefix + "seniorCitizenRate").value(seniorRate).failWithCode("must.be.non.negative");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throwExceptionIfValidationWarningsExist(errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void throwExceptionIfValidationWarningsExist(final List<ApiParameterError> dataValidationErrors) {
|
||||||
|
if (!dataValidationErrors.isEmpty()) {
|
||||||
|
throw new PlatformApiDataValidationException(dataValidationErrors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private FdInterestRateEffectiveData toData(FdBaseInterestRateEffective e) {
|
private FdInterestRateEffectiveData toData(FdBaseInterestRateEffective e) {
|
||||||
List<FdInterestRateSlabData> slabs = slabRepository.findByEffectiveIdOrderBySrNo(e.getId()).stream()
|
List<FdInterestRateSlabData> slabs = slabRepository.findByEffectiveIdOrderBySrNo(e.getId()).stream()
|
||||||
.map(s -> FdInterestRateSlabData.builder()
|
.map(s -> FdInterestRateSlabData.builder()
|
||||||
|
|||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.savings.service;
|
||||||
|
|
||||||
|
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||||
|
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||||
|
|
||||||
|
public interface FdBaseInterestRateWritePlatformService {
|
||||||
|
|
||||||
|
CommandProcessingResult saveInterestRates(JsonCommand command);
|
||||||
|
}
|
||||||
+49
@@ -0,0 +1,49 @@
|
|||||||
|
/**
|
||||||
|
* 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.service;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
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.infrastructure.security.service.PlatformSecurityContext;
|
||||||
|
import org.apache.fineract.portfolio.savings.api.FdInterestRateSaveRequest;
|
||||||
|
import org.apache.fineract.portfolio.savings.data.FdInterestRateEffectiveData;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class FdBaseInterestRateWritePlatformServiceImpl implements FdBaseInterestRateWritePlatformService {
|
||||||
|
|
||||||
|
private final PlatformSecurityContext context;
|
||||||
|
private final FdBaseInterestRateReadPlatformService readPlatformService;
|
||||||
|
private final Gson gson = new Gson();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CommandProcessingResult saveInterestRates(final JsonCommand command) {
|
||||||
|
final FdInterestRateSaveRequest request = gson.fromJson(command.json(), FdInterestRateSaveRequest.class);
|
||||||
|
final Long createdById = this.context.authenticatedUser().getId();
|
||||||
|
final FdInterestRateEffectiveData saved = this.readPlatformService.saveInterestRates(request, createdById);
|
||||||
|
return new CommandProcessingResultBuilder()
|
||||||
|
.withCommandId(command.commandId())
|
||||||
|
.withEntityId(saved.getId())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
-24
@@ -1676,15 +1676,6 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
|
|||||||
@Override
|
@Override
|
||||||
public void setSubStatusInactive(Long savingsId) {
|
public void setSubStatusInactive(Long savingsId) {
|
||||||
final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false);
|
final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false);
|
||||||
// Same snapshot-vs-execution gap as escheat() below: the dormant-accounts batch selects
|
|
||||||
// pending-inactive ids from a query that requires status = ACTIVE, but the account can close
|
|
||||||
// through another path (e.g. deposit maturity) before this call runs. Re-deriving accounting
|
|
||||||
// off the full transaction list at that point re-posts entries that closure already posted.
|
|
||||||
if (!account.isActive()) {
|
|
||||||
log.warn("Skipping setSubStatusInactive for savings account {} -- no longer active (already closed by another process)",
|
|
||||||
savingsId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final Set<Long> existingTransactionIds = new HashSet<>();
|
final Set<Long> existingTransactionIds = new HashSet<>();
|
||||||
final Set<Long> existingReversedTransactionIds = new HashSet<>();
|
final Set<Long> existingReversedTransactionIds = new HashSet<>();
|
||||||
updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds);
|
updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds);
|
||||||
@@ -1696,11 +1687,6 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
|
|||||||
@Override
|
@Override
|
||||||
public void setSubStatusDormant(Long savingsId) {
|
public void setSubStatusDormant(Long savingsId) {
|
||||||
final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false);
|
final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false);
|
||||||
if (!account.isActive()) {
|
|
||||||
log.warn("Skipping setSubStatusDormant for savings account {} -- no longer active (already closed by another process)",
|
|
||||||
savingsId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
account.setSubStatusDormant();
|
account.setSubStatusDormant();
|
||||||
this.savingAccountRepositoryWrapper.saveAndFlush(account);
|
this.savingAccountRepositoryWrapper.saveAndFlush(account);
|
||||||
}
|
}
|
||||||
@@ -1708,16 +1694,6 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
|
|||||||
@Override
|
@Override
|
||||||
public void escheat(Long savingsId) {
|
public void escheat(Long savingsId) {
|
||||||
final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false);
|
final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false);
|
||||||
// The dormant-accounts batch selects escheat-eligible ids from a snapshot query, so an
|
|
||||||
// account can legitimately close through another path (e.g. deposit maturity) in the gap
|
|
||||||
// between that selection and this call. escheat() unconditionally forces status/sub-status
|
|
||||||
// to CLOSED/ESCHEAT and re-derives accounting off the account's full transaction list, so
|
|
||||||
// running it against an already-closed account both mislabels the closure reason and
|
|
||||||
// re-posts journal entries for transactions that were already posted by the real closure.
|
|
||||||
if (!account.isActive()) {
|
|
||||||
log.warn("Skipping escheat for savings account {} -- no longer active (already closed by another process)", savingsId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final Set<Long> existingTransactionIds = new HashSet<>();
|
final Set<Long> existingTransactionIds = new HashSet<>();
|
||||||
final Set<Long> existingReversedTransactionIds = new HashSet<>();
|
final Set<Long> existingReversedTransactionIds = new HashSet<>();
|
||||||
updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds);
|
updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds);
|
||||||
|
|||||||
+10
@@ -169,6 +169,8 @@ import org.apache.fineract.portfolio.savings.service.RdBaseInterestRateReadPlatf
|
|||||||
import org.apache.fineract.portfolio.savings.service.RdBaseInterestRateReadPlatformServiceImpl;
|
import org.apache.fineract.portfolio.savings.service.RdBaseInterestRateReadPlatformServiceImpl;
|
||||||
import org.apache.fineract.portfolio.savings.service.FdBaseInterestRateReadPlatformService;
|
import org.apache.fineract.portfolio.savings.service.FdBaseInterestRateReadPlatformService;
|
||||||
import org.apache.fineract.portfolio.savings.service.FdBaseInterestRateReadPlatformServiceImpl;
|
import org.apache.fineract.portfolio.savings.service.FdBaseInterestRateReadPlatformServiceImpl;
|
||||||
|
import org.apache.fineract.portfolio.savings.service.FdBaseInterestRateWritePlatformService;
|
||||||
|
import org.apache.fineract.portfolio.savings.service.FdBaseInterestRateWritePlatformServiceImpl;
|
||||||
import org.apache.fineract.portfolio.savings.service.SavingsSchedularInterestPoster;
|
import org.apache.fineract.portfolio.savings.service.SavingsSchedularInterestPoster;
|
||||||
import org.apache.fineract.portfolio.savings.service.SavingsSchedularInterestPosterTask;
|
import org.apache.fineract.portfolio.savings.service.SavingsSchedularInterestPosterTask;
|
||||||
import org.apache.fineract.portfolio.savings.service.search.SavingsAccountTransactionSearchService;
|
import org.apache.fineract.portfolio.savings.service.search.SavingsAccountTransactionSearchService;
|
||||||
@@ -200,6 +202,14 @@ public class SavingsConfiguration {
|
|||||||
return new FdBaseInterestRateReadPlatformServiceImpl(effectiveRepository, slabRepository);
|
return new FdBaseInterestRateReadPlatformServiceImpl(effectiveRepository, slabRepository);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnMissingBean(FdBaseInterestRateWritePlatformService.class)
|
||||||
|
public FdBaseInterestRateWritePlatformService fdBaseInterestRateWritePlatformService(
|
||||||
|
PlatformSecurityContext context,
|
||||||
|
FdBaseInterestRateReadPlatformService readPlatformService) {
|
||||||
|
return new FdBaseInterestRateWritePlatformServiceImpl(context, readPlatformService);
|
||||||
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@ConditionalOnMissingBean(SavingsAccountTransactionSearchService.class)
|
@ConditionalOnMissingBean(SavingsAccountTransactionSearchService.class)
|
||||||
public SavingsAccountTransactionSearchService savingsAccountTransactionSearchService(PlatformSecurityContext context,
|
public SavingsAccountTransactionSearchService savingsAccountTransactionSearchService(PlatformSecurityContext context,
|
||||||
|
|||||||
+79
@@ -0,0 +1,79 @@
|
|||||||
|
package org.apache.fineract.useradministration.api;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Content;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
||||||
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
|
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.ws.rs.Consumes;
|
||||||
|
import jakarta.ws.rs.GET;
|
||||||
|
import jakarta.ws.rs.PUT;
|
||||||
|
import jakarta.ws.rs.Path;
|
||||||
|
import jakarta.ws.rs.PathParam;
|
||||||
|
import jakarta.ws.rs.Produces;
|
||||||
|
import jakarta.ws.rs.core.MediaType;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.infrastructure.security.exception.NoAuthorizationException;
|
||||||
|
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||||
|
import org.apache.fineract.useradministration.data.AppUserPreferenceData;
|
||||||
|
import org.apache.fineract.useradministration.service.AppUserPreferenceReadPlatformService;
|
||||||
|
import org.apache.fineract.useradministration.service.AppUserPreferenceWritePlatformService;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Path("/v1/users/{userId}/preferences")
|
||||||
|
@Component
|
||||||
|
@Tag(name = "User Preferences", description = "API to manage user specific preferences.")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class AppUserPreferencesApiResource {
|
||||||
|
|
||||||
|
private final PlatformSecurityContext context;
|
||||||
|
private final AppUserPreferenceReadPlatformService readPlatformService;
|
||||||
|
private final AppUserPreferenceWritePlatformService writePlatformService;
|
||||||
|
|
||||||
|
private void validateUserAuthorization(Long userId) {
|
||||||
|
if (!context.authenticatedUser().getId().equals(userId)) {
|
||||||
|
throw new NoAuthorizationException("Not authorized to access or modify preferences for another user.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("{preferenceName}")
|
||||||
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Retrieve user preference", description = "Retrieves a specific preference for the user.")
|
||||||
|
@ApiResponses({
|
||||||
|
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = AppUserPreferenceData.class))) })
|
||||||
|
public AppUserPreferenceData getPreference(@PathParam("userId") @Parameter(description = "userId") final Long userId,
|
||||||
|
@PathParam("preferenceName") @Parameter(description = "preferenceName") final String preferenceName) {
|
||||||
|
|
||||||
|
validateUserAuthorization(userId);
|
||||||
|
String value = readPlatformService.getPreferenceValue(userId, preferenceName);
|
||||||
|
return new AppUserPreferenceData(preferenceName, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PUT
|
||||||
|
@Path("{preferenceName}")
|
||||||
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Update user preference", description = "Updates or creates a specific preference for the user.")
|
||||||
|
@RequestBody(required = true, content = @Content(schema = @Schema(implementation = AppUserPreferenceData.class)))
|
||||||
|
@ApiResponses({ @ApiResponse(responseCode = "200", description = "OK") })
|
||||||
|
public AppUserPreferenceData updatePreference(@PathParam("userId") @Parameter(description = "userId") final Long userId,
|
||||||
|
@PathParam("preferenceName") @Parameter(description = "preferenceName") final String preferenceName,
|
||||||
|
final AppUserPreferenceData preferenceData) {
|
||||||
|
|
||||||
|
validateUserAuthorization(userId);
|
||||||
|
|
||||||
|
// Ensure the path parameter matches the request body if the request body contains a name
|
||||||
|
String valueToSave = preferenceData != null ? preferenceData.getValue() : null;
|
||||||
|
if (valueToSave == null) {
|
||||||
|
throw new IllegalArgumentException("Preference value cannot be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
writePlatformService.updatePreference(userId, preferenceName, valueToSave);
|
||||||
|
return new AppUserPreferenceData(preferenceName, valueToSave);
|
||||||
|
}
|
||||||
|
}
|
||||||
+67
@@ -88,6 +88,11 @@ public class UsersApiResource {
|
|||||||
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
|
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
|
||||||
private final BulkImportWorkbookPopulatorService bulkImportWorkbookPopulatorService;
|
private final BulkImportWorkbookPopulatorService bulkImportWorkbookPopulatorService;
|
||||||
private final BulkImportWorkbookService bulkImportWorkbookService;
|
private final BulkImportWorkbookService bulkImportWorkbookService;
|
||||||
|
private final org.apache.fineract.useradministration.service.UserPendingWritePlatformService userPendingWritePlatformService;
|
||||||
|
private final org.apache.fineract.useradministration.service.UserPendingReadPlatformService userPendingReadPlatformService;
|
||||||
|
private final org.apache.fineract.infrastructure.core.serialization.FromJsonHelper fromApiJsonHelper;
|
||||||
|
private final org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService configurationDomainService;
|
||||||
|
private final DefaultToApiJsonSerializer<org.apache.fineract.useradministration.data.UserPendingRequestData> pendingRequestApiJsonSerializer;
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Operation(summary = "Retrieve list of users", description = "Example Requests:\n" + "\n" + "users\n" + "\n" + "\n"
|
@Operation(summary = "Retrieve list of users", description = "Example Requests:\n" + "\n" + "users\n" + "\n" + "\n"
|
||||||
@@ -158,6 +163,12 @@ public class UsersApiResource {
|
|||||||
@Consumes({ MediaType.APPLICATION_JSON })
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
@Produces({ MediaType.APPLICATION_JSON })
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
public String create(@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
public String create(@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||||
|
if (this.configurationDomainService.isMakerCheckerEnabled()) {
|
||||||
|
final org.apache.fineract.infrastructure.core.api.JsonCommand command = org.apache.fineract.infrastructure.core.api.JsonCommand
|
||||||
|
.fromJsonElement(null, this.fromApiJsonHelper.parse(apiRequestBodyAsJson), this.fromApiJsonHelper);
|
||||||
|
final CommandProcessingResult result = this.userPendingWritePlatformService.submitCreateUser(command);
|
||||||
|
return this.toApiJsonSerializer.serialize(result);
|
||||||
|
}
|
||||||
|
|
||||||
final CommandWrapper commandRequest = new CommandWrapperBuilder() //
|
final CommandWrapper commandRequest = new CommandWrapperBuilder() //
|
||||||
.createUser() //
|
.createUser() //
|
||||||
@@ -179,6 +190,12 @@ public class UsersApiResource {
|
|||||||
@Produces({ MediaType.APPLICATION_JSON })
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
public String update(@PathParam("userId") @Parameter(description = "userId") final Long userId,
|
public String update(@PathParam("userId") @Parameter(description = "userId") final Long userId,
|
||||||
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||||
|
if (this.configurationDomainService.isMakerCheckerEnabled()) {
|
||||||
|
final org.apache.fineract.infrastructure.core.api.JsonCommand command = org.apache.fineract.infrastructure.core.api.JsonCommand
|
||||||
|
.fromJsonElement(userId, this.fromApiJsonHelper.parse(apiRequestBodyAsJson), this.fromApiJsonHelper);
|
||||||
|
final CommandProcessingResult result = this.userPendingWritePlatformService.submitUpdateUser(userId, command);
|
||||||
|
return this.toApiJsonSerializer.serialize(result);
|
||||||
|
}
|
||||||
|
|
||||||
final CommandWrapper commandRequest = new CommandWrapperBuilder() //
|
final CommandWrapper commandRequest = new CommandWrapperBuilder() //
|
||||||
.updateUser(userId) //
|
.updateUser(userId) //
|
||||||
@@ -190,6 +207,56 @@ public class UsersApiResource {
|
|||||||
return this.toApiJsonSerializer.serialize(result);
|
return this.toApiJsonSerializer.serialize(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("pending")
|
||||||
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "List User creations & edits pending approval")
|
||||||
|
public org.apache.fineract.infrastructure.core.service.Page<org.apache.fineract.useradministration.data.UserPendingRequestData> retrieveAllPendingUsers(
|
||||||
|
@QueryParam("status") final String status,
|
||||||
|
@QueryParam("username") final String username,
|
||||||
|
@QueryParam("fromDate") final String fromDate,
|
||||||
|
@QueryParam("toDate") final String toDate,
|
||||||
|
@QueryParam("offset") final Integer offset,
|
||||||
|
@QueryParam("limit") final Integer limit) {
|
||||||
|
this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
|
||||||
|
return this.userPendingReadPlatformService.retrieveAllPendingRequests(status, username, fromDate, toDate, offset, limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("pending/{id}")
|
||||||
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Retrieve a pending user request by ID")
|
||||||
|
public String retrievePendingUser(@PathParam("id") final Long id, @Context final UriInfo uriInfo) {
|
||||||
|
this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
|
||||||
|
final org.apache.fineract.useradministration.data.UserPendingRequestData data = this.userPendingReadPlatformService.retrievePendingRequest(id);
|
||||||
|
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
|
||||||
|
return this.pendingRequestApiJsonSerializer.serialize(settings, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("pending/{id}/approve")
|
||||||
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Approve a pending User creation or update")
|
||||||
|
public String approvePendingUser(@PathParam("id") final Long id) {
|
||||||
|
final CommandProcessingResult result = this.userPendingWritePlatformService.approvePendingRequest(id);
|
||||||
|
return this.toApiJsonSerializer.serialize(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("pending/{id}/reject")
|
||||||
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Reject a pending User creation or update")
|
||||||
|
public String rejectPendingUser(@PathParam("id") final Long id, @Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||||
|
final org.apache.fineract.infrastructure.core.api.JsonCommand command = org.apache.fineract.infrastructure.core.api.JsonCommand
|
||||||
|
.fromJsonElement(id, this.fromApiJsonHelper.parse(apiRequestBodyAsJson), this.fromApiJsonHelper);
|
||||||
|
final CommandProcessingResult result = this.userPendingWritePlatformService.rejectPendingRequest(id, command);
|
||||||
|
return this.toApiJsonSerializer.serialize(result);
|
||||||
|
}
|
||||||
|
|
||||||
@POST
|
@POST
|
||||||
@Path("{userId}/pwd")
|
@Path("{userId}/pwd")
|
||||||
@Operation(summary = "Change the password of a User", description = "When updating a password you must provide the repeatPassword parameter also.")
|
@Operation(summary = "Change the password of a User", description = "When updating a password you must provide the repeatPassword parameter also.")
|
||||||
|
|||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
package org.apache.fineract.useradministration.data;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class AppUserPreferenceData {
|
||||||
|
private String name;
|
||||||
|
private String value;
|
||||||
|
}
|
||||||
+59
@@ -0,0 +1,59 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.useradministration.data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Map;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
public class UserPendingRequestData implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
private final Long id;
|
||||||
|
private final Long userId;
|
||||||
|
private final String requestType;
|
||||||
|
private final String status;
|
||||||
|
private final String username;
|
||||||
|
private final String firstname;
|
||||||
|
private final String lastname;
|
||||||
|
private final String email;
|
||||||
|
private final String mobileNo;
|
||||||
|
private final Long officeId;
|
||||||
|
private final String officeName;
|
||||||
|
private final Long staffId;
|
||||||
|
private final String staffName;
|
||||||
|
private final String otpDeliveryMethod;
|
||||||
|
private final Boolean passwordNeverExpires;
|
||||||
|
private final Collection<String> roleNames;
|
||||||
|
private final Long makerId;
|
||||||
|
private final String makerUsername;
|
||||||
|
private final OffsetDateTime submittedDate;
|
||||||
|
private final Long checkerId;
|
||||||
|
private final String checkerUsername;
|
||||||
|
private final OffsetDateTime checkedDate;
|
||||||
|
private final String rejectReason;
|
||||||
|
private final Map<String, Object> originalValues;
|
||||||
|
private final Map<String, Object> proposedValues;
|
||||||
|
}
|
||||||
+54
@@ -0,0 +1,54 @@
|
|||||||
|
package org.apache.fineract.useradministration.domain;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.FetchType;
|
||||||
|
import jakarta.persistence.JoinColumn;
|
||||||
|
import jakarta.persistence.ManyToOne;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "m_appuser_preference")
|
||||||
|
public class AppUserPreference extends AbstractPersistableCustom<Long> {
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "appuser_id", nullable = false)
|
||||||
|
private AppUser appUser;
|
||||||
|
|
||||||
|
@Column(name = "name", length = 100, nullable = false)
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@Column(name = "value", nullable = false)
|
||||||
|
private String value;
|
||||||
|
|
||||||
|
protected AppUserPreference() {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
private AppUserPreference(final AppUser appUser, final String name, final String value) {
|
||||||
|
this.appUser = appUser;
|
||||||
|
this.name = name;
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AppUserPreference create(final AppUser appUser, final String name, final String value) {
|
||||||
|
return new AppUserPreference(appUser, name, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateValue(final String newValue) {
|
||||||
|
this.value = newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return this.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getValue() {
|
||||||
|
return this.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AppUser getAppUser() {
|
||||||
|
return this.appUser;
|
||||||
|
}
|
||||||
|
}
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
package org.apache.fineract.useradministration.domain;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
public interface AppUserPreferenceRepository extends JpaRepository<AppUserPreference, Long> {
|
||||||
|
|
||||||
|
Optional<AppUserPreference> findByAppUserIdAndName(Long appUserId, String name);
|
||||||
|
|
||||||
|
}
|
||||||
+113
@@ -0,0 +1,113 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.useradministration.domain;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
import java.time.ZoneOffset;
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "m_user_pending_request")
|
||||||
|
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public class UserPendingRequest extends AbstractPersistableCustom<Long> {
|
||||||
|
|
||||||
|
@Column(name = "user_id")
|
||||||
|
private Long userId;
|
||||||
|
|
||||||
|
@Column(name = "request_type", nullable = false, length = 20)
|
||||||
|
private String requestType;
|
||||||
|
|
||||||
|
@Column(name = "status", nullable = false, length = 20)
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
@Column(name = "payload_json", nullable = false)
|
||||||
|
private String payloadJson;
|
||||||
|
|
||||||
|
@Column(name = "original_json")
|
||||||
|
private String originalJson;
|
||||||
|
|
||||||
|
@Column(name = "password_hash")
|
||||||
|
private String passwordHash;
|
||||||
|
|
||||||
|
@Column(name = "maker_id", nullable = false)
|
||||||
|
private Long makerId;
|
||||||
|
|
||||||
|
@Column(name = "submitted_date", nullable = false)
|
||||||
|
private OffsetDateTime submittedDate;
|
||||||
|
|
||||||
|
@Column(name = "checker_id")
|
||||||
|
private Long checkerId;
|
||||||
|
|
||||||
|
@Column(name = "checked_date")
|
||||||
|
private OffsetDateTime checkedDate;
|
||||||
|
|
||||||
|
@Column(name = "reject_reason", length = 500)
|
||||||
|
private String rejectReason;
|
||||||
|
|
||||||
|
@Column(name = "pending_lock_key", length = 100, unique = true)
|
||||||
|
private String pendingLockKey;
|
||||||
|
|
||||||
|
public static UserPendingRequest create(final UserPendingRequestType requestType, final Long userId,
|
||||||
|
final String payloadJson, final String originalJson, final String passwordHash,
|
||||||
|
final Long makerId, final String pendingLockKey) {
|
||||||
|
final UserPendingRequest request = new UserPendingRequest();
|
||||||
|
request.requestType = requestType.name();
|
||||||
|
request.userId = userId;
|
||||||
|
request.status = UserPendingRequestStatus.PENDING.name();
|
||||||
|
request.payloadJson = payloadJson;
|
||||||
|
request.originalJson = originalJson;
|
||||||
|
request.passwordHash = passwordHash;
|
||||||
|
request.makerId = makerId;
|
||||||
|
request.submittedDate = OffsetDateTime.now(ZoneOffset.UTC);
|
||||||
|
request.pendingLockKey = pendingLockKey;
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isPending() {
|
||||||
|
return UserPendingRequestStatus.PENDING.name().equals(this.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isMadeBy(final Long appUserId) {
|
||||||
|
return this.makerId != null && this.makerId.equals(appUserId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void approve(final Long checkerId) {
|
||||||
|
this.status = UserPendingRequestStatus.APPROVED.name();
|
||||||
|
this.checkerId = checkerId;
|
||||||
|
this.checkedDate = OffsetDateTime.now(ZoneOffset.UTC);
|
||||||
|
this.pendingLockKey = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reject(final Long checkerId, final String reason) {
|
||||||
|
this.status = UserPendingRequestStatus.REJECTED.name();
|
||||||
|
this.checkerId = checkerId;
|
||||||
|
this.checkedDate = OffsetDateTime.now(ZoneOffset.UTC);
|
||||||
|
this.rejectReason = reason;
|
||||||
|
this.pendingLockKey = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
+30
@@ -0,0 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.useradministration.domain;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||||
|
|
||||||
|
public interface UserPendingRequestRepository extends JpaRepository<UserPendingRequest, Long>, JpaSpecificationExecutor<UserPendingRequest> {
|
||||||
|
|
||||||
|
Optional<UserPendingRequest> findByPendingLockKey(String pendingLockKey);
|
||||||
|
|
||||||
|
boolean existsByPendingLockKey(String pendingLockKey);
|
||||||
|
}
|
||||||
+49
@@ -0,0 +1,49 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.useradministration.domain;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.useradministration.exception.UserPendingRequestNotFoundException;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class UserPendingRequestRepositoryWrapper {
|
||||||
|
|
||||||
|
private final UserPendingRequestRepository repository;
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public UserPendingRequest findOneWithNotFoundDetection(final Long id) {
|
||||||
|
return this.repository.findById(id).orElseThrow(() -> new UserPendingRequestNotFoundException(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
public UserPendingRequest saveAndFlush(final UserPendingRequest request) {
|
||||||
|
return this.repository.saveAndFlush(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
public UserPendingRequest save(final UserPendingRequest request) {
|
||||||
|
return this.repository.save(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public boolean existsByPendingLockKey(final String pendingLockKey) {
|
||||||
|
return this.repository.existsByPendingLockKey(pendingLockKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
+37
@@ -0,0 +1,37 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.useradministration.domain;
|
||||||
|
|
||||||
|
public enum UserPendingRequestStatus {
|
||||||
|
PENDING,
|
||||||
|
APPROVED,
|
||||||
|
REJECTED;
|
||||||
|
|
||||||
|
public static UserPendingRequestStatus fromString(final String status) {
|
||||||
|
if (status == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (final UserPendingRequestStatus requestStatus : values()) {
|
||||||
|
if (requestStatus.name().equalsIgnoreCase(status)) {
|
||||||
|
return requestStatus;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new IllegalArgumentException("Unknown UserPendingRequestStatus: " + status);
|
||||||
|
}
|
||||||
|
}
|
||||||
+36
@@ -0,0 +1,36 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.useradministration.domain;
|
||||||
|
|
||||||
|
public enum UserPendingRequestType {
|
||||||
|
CREATE_USER,
|
||||||
|
UPDATE_USER;
|
||||||
|
|
||||||
|
public static UserPendingRequestType fromString(final String type) {
|
||||||
|
if (type == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (final UserPendingRequestType requestType : values()) {
|
||||||
|
if (requestType.name().equalsIgnoreCase(type)) {
|
||||||
|
return requestType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new IllegalArgumentException("Unknown UserPendingRequestType: " + type);
|
||||||
|
}
|
||||||
|
}
|
||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.useradministration.exception;
|
||||||
|
|
||||||
|
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
|
||||||
|
|
||||||
|
public class UserPendingRequestNotFoundException extends AbstractPlatformResourceNotFoundException {
|
||||||
|
|
||||||
|
public UserPendingRequestNotFoundException(final Long id) {
|
||||||
|
super("error.msg.userpendingrequest.id.invalid", "User pending request with identifier " + id + " does not exist", id);
|
||||||
|
}
|
||||||
|
}
|
||||||
+5
@@ -0,0 +1,5 @@
|
|||||||
|
package org.apache.fineract.useradministration.service;
|
||||||
|
|
||||||
|
public interface AppUserPreferenceReadPlatformService {
|
||||||
|
String getPreferenceValue(Long appUserId, String name);
|
||||||
|
}
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
package org.apache.fineract.useradministration.service;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.useradministration.domain.AppUserPreference;
|
||||||
|
import org.apache.fineract.useradministration.domain.AppUserPreferenceRepository;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class AppUserPreferenceReadPlatformServiceImpl implements AppUserPreferenceReadPlatformService {
|
||||||
|
|
||||||
|
private final AppUserPreferenceRepository appUserPreferenceRepository;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getPreferenceValue(Long appUserId, String name) {
|
||||||
|
Optional<AppUserPreference> pref = appUserPreferenceRepository.findByAppUserIdAndName(appUserId, name);
|
||||||
|
return pref.map(AppUserPreference::getValue).orElse(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
+5
@@ -0,0 +1,5 @@
|
|||||||
|
package org.apache.fineract.useradministration.service;
|
||||||
|
|
||||||
|
public interface AppUserPreferenceWritePlatformService {
|
||||||
|
void updatePreference(Long appUserId, String name, String value);
|
||||||
|
}
|
||||||
+32
@@ -0,0 +1,32 @@
|
|||||||
|
package org.apache.fineract.useradministration.service;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.useradministration.domain.AppUser;
|
||||||
|
import org.apache.fineract.useradministration.domain.AppUserPreference;
|
||||||
|
import org.apache.fineract.useradministration.domain.AppUserPreferenceRepository;
|
||||||
|
import org.apache.fineract.useradministration.domain.AppUserRepository;
|
||||||
|
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class AppUserPreferenceWritePlatformServiceImpl implements AppUserPreferenceWritePlatformService {
|
||||||
|
|
||||||
|
private final AppUserPreferenceRepository appUserPreferenceRepository;
|
||||||
|
private final AppUserRepository appUserRepository;
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
@Override
|
||||||
|
public void updatePreference(Long appUserId, String name, String value) {
|
||||||
|
AppUserPreference preference = appUserPreferenceRepository.findByAppUserIdAndName(appUserId, name).orElse(null);
|
||||||
|
if (preference == null) {
|
||||||
|
AppUser appUser = appUserRepository.findById(appUserId)
|
||||||
|
.orElseThrow(() -> new PlatformDataIntegrityException("error.msg.user.id.invalid", "User not found"));
|
||||||
|
preference = AppUserPreference.create(appUser, name, value);
|
||||||
|
} else {
|
||||||
|
preference.updateValue(value);
|
||||||
|
}
|
||||||
|
appUserPreferenceRepository.saveAndFlush(preference);
|
||||||
|
}
|
||||||
|
}
|
||||||
+30
@@ -0,0 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.useradministration.service;
|
||||||
|
|
||||||
|
import org.apache.fineract.infrastructure.core.service.Page;
|
||||||
|
import org.apache.fineract.useradministration.data.UserPendingRequestData;
|
||||||
|
|
||||||
|
public interface UserPendingReadPlatformService {
|
||||||
|
|
||||||
|
Page<UserPendingRequestData> retrieveAllPendingRequests(String status, String username, String fromDate, String toDate,
|
||||||
|
Integer offset, Integer limit);
|
||||||
|
|
||||||
|
UserPendingRequestData retrievePendingRequest(Long id);
|
||||||
|
}
|
||||||
+329
@@ -0,0 +1,329 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.useradministration.service;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.JsonArray;
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.apache.fineract.infrastructure.core.domain.JdbcSupport;
|
||||||
|
import org.apache.fineract.infrastructure.core.serialization.GoogleGsonSerializerHelper;
|
||||||
|
import org.apache.fineract.infrastructure.core.service.Page;
|
||||||
|
import org.apache.fineract.useradministration.data.UserPendingRequestData;
|
||||||
|
import org.apache.fineract.useradministration.domain.UserPendingRequest;
|
||||||
|
import org.apache.fineract.useradministration.domain.UserPendingRequestRepositoryWrapper;
|
||||||
|
import org.springframework.dao.EmptyResultDataAccessException;
|
||||||
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
|
import org.springframework.jdbc.core.RowMapper;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class UserPendingReadPlatformServiceImpl implements UserPendingReadPlatformService {
|
||||||
|
|
||||||
|
private final JdbcTemplate jdbcTemplate;
|
||||||
|
private final UserPendingRequestRepositoryWrapper pendingRequestRepositoryWrapper;
|
||||||
|
private final Gson gson = GoogleGsonSerializerHelper.createSimpleGson();
|
||||||
|
|
||||||
|
private static final String SELECT_SQL = """
|
||||||
|
SELECT r.id AS id,
|
||||||
|
r.user_id AS userId,
|
||||||
|
r.request_type AS requestType,
|
||||||
|
r.status AS status,
|
||||||
|
r.payload_json AS payloadJson,
|
||||||
|
r.original_json AS originalJson,
|
||||||
|
r.maker_id AS makerId,
|
||||||
|
maker.username AS makerUsername,
|
||||||
|
r.submitted_date AS submittedDate,
|
||||||
|
r.checker_id AS checkerId,
|
||||||
|
checker.username AS checkerUsername,
|
||||||
|
r.checked_date AS checkedDate,
|
||||||
|
r.reject_reason AS rejectReason
|
||||||
|
FROM m_user_pending_request r
|
||||||
|
JOIN m_appuser maker ON maker.id = r.maker_id
|
||||||
|
LEFT JOIN m_appuser checker ON checker.id = r.checker_id
|
||||||
|
""";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public Page<UserPendingRequestData> retrieveAllPendingRequests(final String status, final String username,
|
||||||
|
final String fromDate, final String toDate, final Integer offset, final Integer limit) {
|
||||||
|
final StringBuilder where = new StringBuilder(" WHERE 1=1");
|
||||||
|
final List<Object> args = new ArrayList<>();
|
||||||
|
|
||||||
|
if (StringUtils.isNotBlank(status)) {
|
||||||
|
where.append(" AND r.status = ?");
|
||||||
|
args.add(status.trim().toUpperCase());
|
||||||
|
} else {
|
||||||
|
where.append(" AND r.status = 'PENDING'");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (StringUtils.isNotBlank(fromDate)) {
|
||||||
|
where.append(" AND r.submitted_date::date >= ?::date");
|
||||||
|
args.add(fromDate);
|
||||||
|
}
|
||||||
|
if (StringUtils.isNotBlank(toDate)) {
|
||||||
|
where.append(" AND r.submitted_date::date <= ?::date");
|
||||||
|
args.add(toDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
final String countSql = "SELECT COUNT(*) FROM m_user_pending_request r " + where;
|
||||||
|
final Long totalFilteredRecords = this.jdbcTemplate.queryForObject(countSql, Long.class, args.toArray());
|
||||||
|
|
||||||
|
final int pageOffset = offset == null ? 0 : offset;
|
||||||
|
final int pageLimit = limit == null ? 20 : limit;
|
||||||
|
final String pageSql = SELECT_SQL + where + " ORDER BY r.submitted_date DESC LIMIT ? OFFSET ?";
|
||||||
|
final List<Object> pageArgs = new ArrayList<>(args);
|
||||||
|
pageArgs.add(pageLimit);
|
||||||
|
pageArgs.add(pageOffset);
|
||||||
|
|
||||||
|
final List<UserPendingRequestData> items = this.jdbcTemplate.query(pageSql, new UserPendingRowMapper(), pageArgs.toArray());
|
||||||
|
|
||||||
|
return new Page<>(items, totalFilteredRecords == null ? 0 : totalFilteredRecords.intValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public UserPendingRequestData retrievePendingRequest(final Long id) {
|
||||||
|
final UserPendingRequest request = this.pendingRequestRepositoryWrapper.findOneWithNotFoundDetection(id);
|
||||||
|
final String sql = SELECT_SQL + " WHERE r.id = ?";
|
||||||
|
return this.jdbcTemplate.queryForObject(sql, new UserPendingDetailRowMapper(), id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class UserPendingRowMapper implements RowMapper<UserPendingRequestData> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UserPendingRequestData mapRow(final ResultSet rs, final int rowNum) throws SQLException {
|
||||||
|
final Long id = rs.getLong("id");
|
||||||
|
final Long userId = JdbcSupport.getLong(rs, "userId");
|
||||||
|
final String requestType = rs.getString("requestType");
|
||||||
|
final String status = rs.getString("status");
|
||||||
|
final String payloadJson = rs.getString("payloadJson");
|
||||||
|
final Long makerId = rs.getLong("makerId");
|
||||||
|
final String makerUsername = rs.getString("makerUsername");
|
||||||
|
final OffsetDateTime submittedDate = rs.getObject("submittedDate", OffsetDateTime.class);
|
||||||
|
final Long checkerId = JdbcSupport.getLong(rs, "checkerId");
|
||||||
|
final String checkerUsername = rs.getString("checkerUsername");
|
||||||
|
final OffsetDateTime checkedDate = rs.getObject("checkedDate", OffsetDateTime.class);
|
||||||
|
final String rejectReason = rs.getString("rejectReason");
|
||||||
|
|
||||||
|
String username = null;
|
||||||
|
String firstname = null;
|
||||||
|
String lastname = null;
|
||||||
|
String email = null;
|
||||||
|
String mobileNo = null;
|
||||||
|
Long officeId = null;
|
||||||
|
String otpDeliveryMethod = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (payloadJson != null) {
|
||||||
|
final JsonObject payloadObj = JsonParser.parseString(payloadJson).getAsJsonObject();
|
||||||
|
if (payloadObj.has("username")) {
|
||||||
|
username = payloadObj.get("username").getAsString();
|
||||||
|
}
|
||||||
|
if (payloadObj.has("firstname")) {
|
||||||
|
firstname = payloadObj.get("firstname").getAsString();
|
||||||
|
}
|
||||||
|
if (payloadObj.has("lastname")) {
|
||||||
|
lastname = payloadObj.get("lastname").getAsString();
|
||||||
|
}
|
||||||
|
if (payloadObj.has("email")) {
|
||||||
|
email = payloadObj.get("email").getAsString();
|
||||||
|
}
|
||||||
|
if (payloadObj.has("mobileNo")) {
|
||||||
|
mobileNo = payloadObj.get("mobileNo").getAsString();
|
||||||
|
}
|
||||||
|
if (payloadObj.has("officeId")) {
|
||||||
|
officeId = payloadObj.get("officeId").getAsLong();
|
||||||
|
}
|
||||||
|
if (payloadObj.has("otpDeliveryMethod")) {
|
||||||
|
otpDeliveryMethod = payloadObj.get("otpDeliveryMethod").getAsString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
|
||||||
|
return UserPendingRequestData.builder()
|
||||||
|
.id(id)
|
||||||
|
.userId(userId)
|
||||||
|
.requestType(requestType)
|
||||||
|
.status(status)
|
||||||
|
.username(username)
|
||||||
|
.firstname(firstname)
|
||||||
|
.lastname(lastname)
|
||||||
|
.email(email)
|
||||||
|
.mobileNo(mobileNo)
|
||||||
|
.officeId(officeId)
|
||||||
|
.otpDeliveryMethod(otpDeliveryMethod)
|
||||||
|
.makerId(makerId)
|
||||||
|
.makerUsername(makerUsername)
|
||||||
|
.submittedDate(submittedDate)
|
||||||
|
.checkerId(checkerId)
|
||||||
|
.checkerUsername(checkerUsername)
|
||||||
|
.checkedDate(checkedDate)
|
||||||
|
.rejectReason(rejectReason)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class UserPendingDetailRowMapper implements RowMapper<UserPendingRequestData> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UserPendingRequestData mapRow(final ResultSet rs, final int rowNum) throws SQLException {
|
||||||
|
final Long id = rs.getLong("id");
|
||||||
|
final Long userId = JdbcSupport.getLong(rs, "userId");
|
||||||
|
final String requestType = rs.getString("requestType");
|
||||||
|
final String status = rs.getString("status");
|
||||||
|
final String payloadJson = rs.getString("payloadJson");
|
||||||
|
final String originalJson = rs.getString("originalJson");
|
||||||
|
final Long makerId = rs.getLong("makerId");
|
||||||
|
final String makerUsername = rs.getString("makerUsername");
|
||||||
|
final OffsetDateTime submittedDate = rs.getObject("submittedDate", OffsetDateTime.class);
|
||||||
|
final Long checkerId = JdbcSupport.getLong(rs, "checkerId");
|
||||||
|
final String checkerUsername = rs.getString("checkerUsername");
|
||||||
|
final OffsetDateTime checkedDate = rs.getObject("checkedDate", OffsetDateTime.class);
|
||||||
|
final String rejectReason = rs.getString("rejectReason");
|
||||||
|
|
||||||
|
final Map<String, Object> proposedValues = new LinkedHashMap<>();
|
||||||
|
final Map<String, Object> originalValues = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
String username = null;
|
||||||
|
String firstname = null;
|
||||||
|
String lastname = null;
|
||||||
|
String email = null;
|
||||||
|
String mobileNo = null;
|
||||||
|
Long officeId = null;
|
||||||
|
String otpDeliveryMethod = null;
|
||||||
|
Boolean passwordNeverExpires = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (payloadJson != null) {
|
||||||
|
final JsonObject payloadObj = JsonParser.parseString(payloadJson).getAsJsonObject();
|
||||||
|
for (final String key : payloadObj.keySet()) {
|
||||||
|
final JsonElement val = payloadObj.get(key);
|
||||||
|
if ("password".equals(key) || "repeatPassword".equals(key)) {
|
||||||
|
proposedValues.put("password", "Password will be set on approval");
|
||||||
|
} else if (val.isJsonPrimitive()) {
|
||||||
|
if (val.getAsJsonPrimitive().isBoolean()) {
|
||||||
|
proposedValues.put(key, val.getAsBoolean());
|
||||||
|
} else if (val.getAsJsonPrimitive().isNumber()) {
|
||||||
|
proposedValues.put(key, val.getAsNumber());
|
||||||
|
} else {
|
||||||
|
proposedValues.put(key, val.getAsString());
|
||||||
|
}
|
||||||
|
} else if (val.isJsonArray()) {
|
||||||
|
final List<String> list = new ArrayList<>();
|
||||||
|
for (final JsonElement elem : val.getAsJsonArray()) {
|
||||||
|
list.add(elem.getAsString());
|
||||||
|
}
|
||||||
|
proposedValues.put(key, list);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (proposedValues.containsKey("username")) {
|
||||||
|
username = (String) proposedValues.get("username");
|
||||||
|
}
|
||||||
|
if (proposedValues.containsKey("firstname")) {
|
||||||
|
firstname = (String) proposedValues.get("firstname");
|
||||||
|
}
|
||||||
|
if (proposedValues.containsKey("lastname")) {
|
||||||
|
lastname = (String) proposedValues.get("lastname");
|
||||||
|
}
|
||||||
|
if (proposedValues.containsKey("email")) {
|
||||||
|
email = (String) proposedValues.get("email");
|
||||||
|
}
|
||||||
|
if (proposedValues.containsKey("mobileNo")) {
|
||||||
|
mobileNo = (String) proposedValues.get("mobileNo");
|
||||||
|
}
|
||||||
|
if (proposedValues.get("officeId") instanceof Number n) {
|
||||||
|
officeId = n.longValue();
|
||||||
|
}
|
||||||
|
if (proposedValues.containsKey("otpDeliveryMethod")) {
|
||||||
|
otpDeliveryMethod = (String) proposedValues.get("otpDeliveryMethod");
|
||||||
|
}
|
||||||
|
if (proposedValues.get("passwordNeverExpires") instanceof Boolean b) {
|
||||||
|
passwordNeverExpires = b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (originalJson != null) {
|
||||||
|
final JsonObject origObj = JsonParser.parseString(originalJson).getAsJsonObject();
|
||||||
|
for (final String key : origObj.keySet()) {
|
||||||
|
final JsonElement val = origObj.get(key);
|
||||||
|
if (val.isJsonPrimitive()) {
|
||||||
|
if (val.getAsJsonPrimitive().isBoolean()) {
|
||||||
|
originalValues.put(key, val.getAsBoolean());
|
||||||
|
} else if (val.getAsJsonPrimitive().isNumber()) {
|
||||||
|
originalValues.put(key, val.getAsNumber());
|
||||||
|
} else {
|
||||||
|
originalValues.put(key, val.getAsString());
|
||||||
|
}
|
||||||
|
} else if (val.isJsonArray()) {
|
||||||
|
final List<String> list = new ArrayList<>();
|
||||||
|
for (final JsonElement elem : val.getAsJsonArray()) {
|
||||||
|
list.add(elem.getAsString());
|
||||||
|
}
|
||||||
|
originalValues.put(key, list);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (proposedValues.containsKey("password")) {
|
||||||
|
originalValues.put("password", "[PROTECTED]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
|
||||||
|
return UserPendingRequestData.builder()
|
||||||
|
.id(id)
|
||||||
|
.userId(userId)
|
||||||
|
.requestType(requestType)
|
||||||
|
.status(status)
|
||||||
|
.username(username)
|
||||||
|
.firstname(firstname)
|
||||||
|
.lastname(lastname)
|
||||||
|
.email(email)
|
||||||
|
.mobileNo(mobileNo)
|
||||||
|
.officeId(officeId)
|
||||||
|
.otpDeliveryMethod(otpDeliveryMethod)
|
||||||
|
.passwordNeverExpires(passwordNeverExpires)
|
||||||
|
.makerId(makerId)
|
||||||
|
.makerUsername(makerUsername)
|
||||||
|
.submittedDate(submittedDate)
|
||||||
|
.checkerId(checkerId)
|
||||||
|
.checkerUsername(checkerUsername)
|
||||||
|
.checkedDate(checkedDate)
|
||||||
|
.rejectReason(rejectReason)
|
||||||
|
.originalValues(originalValues)
|
||||||
|
.proposedValues(proposedValues)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.useradministration.service;
|
||||||
|
|
||||||
|
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||||
|
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||||
|
|
||||||
|
public interface UserPendingWritePlatformService {
|
||||||
|
|
||||||
|
CommandProcessingResult submitCreateUser(JsonCommand command);
|
||||||
|
|
||||||
|
CommandProcessingResult submitUpdateUser(Long userId, JsonCommand command);
|
||||||
|
|
||||||
|
CommandProcessingResult approvePendingRequest(Long id);
|
||||||
|
|
||||||
|
CommandProcessingResult rejectPendingRequest(Long id, JsonCommand command);
|
||||||
|
}
|
||||||
+386
@@ -0,0 +1,386 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.useradministration.service;
|
||||||
|
|
||||||
|
import com.google.gson.JsonArray;
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
import java.time.ZoneOffset;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.lang3.ObjectUtils;
|
||||||
|
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
|
||||||
|
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.infrastructure.core.exception.GeneralPlatformDomainRuleException;
|
||||||
|
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
|
||||||
|
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
||||||
|
import org.apache.fineract.infrastructure.core.serialization.GoogleGsonSerializerHelper;
|
||||||
|
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||||
|
import org.apache.fineract.organisation.office.domain.Office;
|
||||||
|
import org.apache.fineract.organisation.office.domain.OfficeRepositoryWrapper;
|
||||||
|
import org.apache.fineract.organisation.staff.domain.Staff;
|
||||||
|
import org.apache.fineract.organisation.staff.domain.StaffRepositoryWrapper;
|
||||||
|
import org.apache.fineract.portfolio.client.domain.Client;
|
||||||
|
import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper;
|
||||||
|
import org.apache.fineract.useradministration.mfa.domain.MfaFactorType;
|
||||||
|
import org.apache.fineract.useradministration.domain.AppUser;
|
||||||
|
import org.apache.fineract.useradministration.domain.AppUserRepository;
|
||||||
|
import org.apache.fineract.useradministration.domain.Role;
|
||||||
|
import org.apache.fineract.useradministration.domain.RoleRepository;
|
||||||
|
import org.apache.fineract.useradministration.mfa.domain.UserMfaFactor;
|
||||||
|
import org.apache.fineract.useradministration.mfa.domain.UserMfaFactorRepository;
|
||||||
|
import org.apache.fineract.useradministration.domain.UserPendingRequest;
|
||||||
|
import org.apache.fineract.useradministration.domain.UserPendingRequestRepository;
|
||||||
|
import org.apache.fineract.useradministration.domain.UserPendingRequestRepositoryWrapper;
|
||||||
|
import org.apache.fineract.useradministration.domain.UserPendingRequestType;
|
||||||
|
import org.apache.fineract.useradministration.exception.RoleNotFoundException;
|
||||||
|
import org.apache.fineract.useradministration.exception.UserNotFoundException;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class UserPendingWritePlatformServiceImpl implements UserPendingWritePlatformService {
|
||||||
|
|
||||||
|
private final PlatformSecurityContext context;
|
||||||
|
private final UserPendingRequestRepositoryWrapper pendingRequestRepositoryWrapper;
|
||||||
|
private final UserPendingRequestRepository pendingRequestRepository;
|
||||||
|
private final AppUserRepository appUserRepository;
|
||||||
|
private final OfficeRepositoryWrapper officeRepositoryWrapper;
|
||||||
|
private final RoleRepository roleRepository;
|
||||||
|
private final StaffRepositoryWrapper staffRepositoryWrapper;
|
||||||
|
private final ClientRepositoryWrapper clientRepositoryWrapper;
|
||||||
|
private final UserMfaFactorRepository userMfaFactorRepository;
|
||||||
|
private final ConfigurationDomainService configurationDomainService;
|
||||||
|
private final PasswordEncoder passwordEncoder;
|
||||||
|
private final UserDataValidator fromApiJsonDeserializer;
|
||||||
|
private final FromJsonHelper fromApiJsonHelper;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public CommandProcessingResult submitCreateUser(final JsonCommand command) {
|
||||||
|
final AppUser maker = this.context.authenticatedUser();
|
||||||
|
|
||||||
|
this.fromApiJsonDeserializer.validateForCreate(command.json());
|
||||||
|
|
||||||
|
final String username = command.stringValueOfParameterNamed("username");
|
||||||
|
final String email = command.stringValueOfParameterNamed("email");
|
||||||
|
final String mobileNo = command.stringValueOfParameterNamed("mobileNo");
|
||||||
|
|
||||||
|
// Pre-flight checks on live users and pending creations
|
||||||
|
if (this.appUserRepository.findAppUserByName(username) != null) {
|
||||||
|
throw new PlatformDataIntegrityException("error.msg.user.duplicate.username",
|
||||||
|
"User with username " + username + " already exists.", "username", username);
|
||||||
|
}
|
||||||
|
|
||||||
|
final String lockKey = "CREATE_USER:" + username.toLowerCase();
|
||||||
|
if (this.pendingRequestRepository.existsByPendingLockKey(lockKey)) {
|
||||||
|
throw new GeneralPlatformDomainRuleException("error.msg.userpendingrequest.duplicate",
|
||||||
|
"A pending creation request for username '" + username + "' is already awaiting checker approval.", username);
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEmailAndMobileUnique(email, mobileNo, null);
|
||||||
|
|
||||||
|
final JsonObject jsonObject = JsonParser.parseString(command.json()).getAsJsonObject();
|
||||||
|
String passwordHash = null;
|
||||||
|
if (jsonObject.has("password") && !jsonObject.get("password").isJsonNull()) {
|
||||||
|
final String rawPassword = jsonObject.get("password").getAsString();
|
||||||
|
if (!rawPassword.isBlank()) {
|
||||||
|
passwordHash = this.passwordEncoder.encode(rawPassword);
|
||||||
|
}
|
||||||
|
jsonObject.addProperty("password", "[PROTECTED]");
|
||||||
|
if (jsonObject.has("repeatPassword")) {
|
||||||
|
jsonObject.addProperty("repeatPassword", "[PROTECTED]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final String sanitizedPayload = jsonObject.toString();
|
||||||
|
final UserPendingRequest request = UserPendingRequest.create(UserPendingRequestType.CREATE_USER, null,
|
||||||
|
sanitizedPayload, null, passwordHash, maker.getId(), lockKey);
|
||||||
|
|
||||||
|
final UserPendingRequest saved = this.pendingRequestRepositoryWrapper.saveAndFlush(request);
|
||||||
|
|
||||||
|
return new CommandProcessingResultBuilder()
|
||||||
|
.withEntityId(saved.getId())
|
||||||
|
.setRollbackTransaction(true)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public CommandProcessingResult submitUpdateUser(final Long userId, final JsonCommand command) {
|
||||||
|
final AppUser maker = this.context.authenticatedUser();
|
||||||
|
final AppUser userToUpdate = this.appUserRepository.findById(userId)
|
||||||
|
.orElseThrow(() -> new UserNotFoundException(userId));
|
||||||
|
|
||||||
|
this.fromApiJsonDeserializer.validateForUpdate(command.json(), maker);
|
||||||
|
|
||||||
|
final String email = command.stringValueOfParameterNamed("email");
|
||||||
|
final String mobileNo = command.stringValueOfParameterNamed("mobileNo");
|
||||||
|
assertEmailAndMobileUnique(email, mobileNo, userId);
|
||||||
|
|
||||||
|
final String lockKey = "UPDATE_USER:" + userId;
|
||||||
|
if (this.pendingRequestRepository.existsByPendingLockKey(lockKey)) {
|
||||||
|
throw new GeneralPlatformDomainRuleException("error.msg.userpendingrequest.duplicate",
|
||||||
|
"A pending update request for user ID " + userId + " is already awaiting checker approval.", userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build point-in-time original JSON snapshot
|
||||||
|
final Map<String, Object> originalSnapshot = new LinkedHashMap<>();
|
||||||
|
originalSnapshot.put("username", userToUpdate.getUsername());
|
||||||
|
originalSnapshot.put("firstname", userToUpdate.getFirstname());
|
||||||
|
originalSnapshot.put("lastname", userToUpdate.getLastname());
|
||||||
|
originalSnapshot.put("email", userToUpdate.getEmail());
|
||||||
|
originalSnapshot.put("mobileNo", userToUpdate.getMobileNo());
|
||||||
|
originalSnapshot.put("officeId", userToUpdate.getOffice() != null ? userToUpdate.getOffice().getId() : null);
|
||||||
|
originalSnapshot.put("officeName", userToUpdate.getOffice() != null ? userToUpdate.getOffice().getName() : null);
|
||||||
|
originalSnapshot.put("staffId", userToUpdate.getStaff() != null ? userToUpdate.getStaff().getId() : null);
|
||||||
|
originalSnapshot.put("passwordNeverExpires", userToUpdate.getPasswordNeverExpires());
|
||||||
|
originalSnapshot.put("isSelfServiceUser", userToUpdate.isSelfServiceUser());
|
||||||
|
final Set<String> roleNames = new HashSet<>();
|
||||||
|
final Set<Long> roleIds = new HashSet<>();
|
||||||
|
if (userToUpdate.getRoles() != null) {
|
||||||
|
for (final Role r : userToUpdate.getRoles()) {
|
||||||
|
roleNames.add(r.getName());
|
||||||
|
roleIds.add(r.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
originalSnapshot.put("roles", roleIds);
|
||||||
|
originalSnapshot.put("roleNames", roleNames);
|
||||||
|
final String originalJson = GoogleGsonSerializerHelper.createSimpleGson().toJson(originalSnapshot);
|
||||||
|
|
||||||
|
final JsonObject jsonObject = JsonParser.parseString(command.json()).getAsJsonObject();
|
||||||
|
String passwordHash = null;
|
||||||
|
if (jsonObject.has("password") && !jsonObject.get("password").isJsonNull()) {
|
||||||
|
final String rawPassword = jsonObject.get("password").getAsString();
|
||||||
|
if (!rawPassword.isBlank()) {
|
||||||
|
passwordHash = this.passwordEncoder.encode(rawPassword);
|
||||||
|
}
|
||||||
|
jsonObject.addProperty("password", "[PROTECTED]");
|
||||||
|
if (jsonObject.has("repeatPassword")) {
|
||||||
|
jsonObject.addProperty("repeatPassword", "[PROTECTED]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final String sanitizedPayload = jsonObject.toString();
|
||||||
|
final UserPendingRequest request = UserPendingRequest.create(UserPendingRequestType.UPDATE_USER, userId,
|
||||||
|
sanitizedPayload, originalJson, passwordHash, maker.getId(), lockKey);
|
||||||
|
|
||||||
|
final UserPendingRequest saved = this.pendingRequestRepositoryWrapper.saveAndFlush(request);
|
||||||
|
|
||||||
|
return new CommandProcessingResultBuilder()
|
||||||
|
.withEntityId(saved.getId())
|
||||||
|
.setRollbackTransaction(true)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public CommandProcessingResult approvePendingRequest(final Long id) {
|
||||||
|
final AppUser checker = this.context.authenticatedUser();
|
||||||
|
final UserPendingRequest request = this.pendingRequestRepositoryWrapper.findOneWithNotFoundDetection(id);
|
||||||
|
|
||||||
|
if (!request.isPending()) {
|
||||||
|
throw new GeneralPlatformDomainRuleException("error.msg.userpendingrequest.not.pending",
|
||||||
|
"User pending request " + id + " has already been actioned.", id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.isMadeBy(checker.getId())) {
|
||||||
|
throw new GeneralPlatformDomainRuleException("error.msg.userpendingrequest.self.approval",
|
||||||
|
"You cannot approve a user request you submitted yourself — a different user must action it.", id);
|
||||||
|
}
|
||||||
|
|
||||||
|
final Long resourceId;
|
||||||
|
if (UserPendingRequestType.CREATE_USER.name().equals(request.getRequestType())) {
|
||||||
|
resourceId = finalizeCreateUser(request);
|
||||||
|
} else if (UserPendingRequestType.UPDATE_USER.name().equals(request.getRequestType())) {
|
||||||
|
resourceId = finalizeUpdateUser(request);
|
||||||
|
} else {
|
||||||
|
throw new GeneralPlatformDomainRuleException("error.msg.userpendingrequest.type.invalid",
|
||||||
|
"Unknown request type: " + request.getRequestType(), request.getRequestType());
|
||||||
|
}
|
||||||
|
|
||||||
|
request.approve(checker.getId());
|
||||||
|
this.pendingRequestRepositoryWrapper.saveAndFlush(request);
|
||||||
|
|
||||||
|
return new CommandProcessingResultBuilder()
|
||||||
|
.withEntityId(resourceId)
|
||||||
|
.withResourceIdAsString(resourceId.toString())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public CommandProcessingResult rejectPendingRequest(final Long id, final JsonCommand command) {
|
||||||
|
final AppUser checker = this.context.authenticatedUser();
|
||||||
|
final UserPendingRequest request = this.pendingRequestRepositoryWrapper.findOneWithNotFoundDetection(id);
|
||||||
|
|
||||||
|
if (!request.isPending()) {
|
||||||
|
throw new GeneralPlatformDomainRuleException("error.msg.userpendingrequest.not.pending",
|
||||||
|
"User pending request " + id + " has already been actioned.", id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.isMadeBy(checker.getId())) {
|
||||||
|
throw new GeneralPlatformDomainRuleException("error.msg.userpendingrequest.self.approval",
|
||||||
|
"You cannot reject a user request you submitted yourself — a different user must action it.", id);
|
||||||
|
}
|
||||||
|
|
||||||
|
String reason = command != null ? command.stringValueOfParameterNamedAllowingNull("reason") : null;
|
||||||
|
if (reason == null && command != null) {
|
||||||
|
reason = command.stringValueOfParameterNamedAllowingNull("rejectReason");
|
||||||
|
}
|
||||||
|
if (reason == null || reason.isBlank()) {
|
||||||
|
throw new GeneralPlatformDomainRuleException("error.msg.userpendingrequest.reject.reason.required",
|
||||||
|
"A reason is required to reject a user pending request.", id);
|
||||||
|
}
|
||||||
|
|
||||||
|
request.reject(checker.getId(), reason);
|
||||||
|
this.pendingRequestRepositoryWrapper.saveAndFlush(request);
|
||||||
|
|
||||||
|
return new CommandProcessingResultBuilder()
|
||||||
|
.withEntityId(request.getId())
|
||||||
|
.withResourceIdAsString(request.getId().toString())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Long finalizeCreateUser(final UserPendingRequest request) {
|
||||||
|
final JsonElement parsedElement = this.fromApiJsonHelper.parse(request.getPayloadJson());
|
||||||
|
final JsonCommand replay = JsonCommand.fromJsonElement(null, parsedElement, this.fromApiJsonHelper);
|
||||||
|
|
||||||
|
final Long officeId = replay.longValueOfParameterNamed("officeId");
|
||||||
|
final Office userOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(officeId);
|
||||||
|
|
||||||
|
final String[] roles = replay.arrayValueOfParameterNamed("roles");
|
||||||
|
final Set<Role> allRoles = assembleSetOfRoles(roles);
|
||||||
|
|
||||||
|
final Long staffId = replay.longValueOfParameterNamed("staffId");
|
||||||
|
final Staff linkedStaff = staffId != null
|
||||||
|
? this.staffRepositoryWrapper.findByOfficeWithNotFoundDetection(staffId, userOffice.getId())
|
||||||
|
: null;
|
||||||
|
|
||||||
|
final Collection<Client> clients = null;
|
||||||
|
|
||||||
|
final AppUser appUser = AppUser.fromJson(userOffice, linkedStaff, allRoles, clients, replay);
|
||||||
|
|
||||||
|
if (this.configurationDomainService.isForcePasswordResetOnFirstLoginEnabled()) {
|
||||||
|
appUser.updatePasswordResetRequired(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set pre-hashed password
|
||||||
|
if (request.getPasswordHash() != null) {
|
||||||
|
appUser.updatePassword(request.getPasswordHash());
|
||||||
|
}
|
||||||
|
|
||||||
|
this.appUserRepository.saveAndFlush(appUser);
|
||||||
|
|
||||||
|
// MFA Enrollment
|
||||||
|
final String otpDeliveryMethod = replay.stringValueOfParameterNamed("otpDeliveryMethod");
|
||||||
|
if (otpDeliveryMethod != null) {
|
||||||
|
final MfaFactorType factorType = MfaFactorType.fromString(otpDeliveryMethod);
|
||||||
|
final UserMfaFactor factor = new UserMfaFactor();
|
||||||
|
factor.setAppUser(appUser);
|
||||||
|
factor.setFactorType(factorType);
|
||||||
|
factor.setDestination(factorType == MfaFactorType.SMS ? appUser.getMobileNo() : appUser.getEmail());
|
||||||
|
factor.setVerified(true);
|
||||||
|
factor.setVerifiedOn(OffsetDateTime.now(ZoneOffset.UTC));
|
||||||
|
this.userMfaFactorRepository.save(factor);
|
||||||
|
}
|
||||||
|
|
||||||
|
return appUser.getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Long finalizeUpdateUser(final UserPendingRequest request) {
|
||||||
|
final AppUser userToUpdate = this.appUserRepository.findById(request.getUserId())
|
||||||
|
.orElseThrow(() -> new UserNotFoundException(request.getUserId()));
|
||||||
|
|
||||||
|
final JsonElement parsedElement = this.fromApiJsonHelper.parse(request.getPayloadJson());
|
||||||
|
final JsonCommand replay = JsonCommand.fromJsonElement(request.getUserId(), parsedElement, this.fromApiJsonHelper);
|
||||||
|
|
||||||
|
final Map<String, Object> changes = userToUpdate.update(replay, null, null);
|
||||||
|
|
||||||
|
if (changes.containsKey("officeId")) {
|
||||||
|
final Long officeId = (Long) changes.get("officeId");
|
||||||
|
final Office office = this.officeRepositoryWrapper.findOneWithNotFoundDetection(officeId);
|
||||||
|
userToUpdate.changeOffice(office);
|
||||||
|
}
|
||||||
|
if (changes.containsKey("staffId")) {
|
||||||
|
final Long staffId = (Long) changes.get("staffId");
|
||||||
|
Staff linkedStaff = null;
|
||||||
|
if (staffId != null) {
|
||||||
|
linkedStaff = this.staffRepositoryWrapper.findByOfficeWithNotFoundDetection(staffId, userToUpdate.getOffice().getId());
|
||||||
|
}
|
||||||
|
userToUpdate.changeStaff(linkedStaff);
|
||||||
|
}
|
||||||
|
if (changes.containsKey("roles")) {
|
||||||
|
final String[] roleIds = (String[]) changes.get("roles");
|
||||||
|
final Set<Role> allRoles = assembleSetOfRoles(roleIds);
|
||||||
|
userToUpdate.updateRoles(allRoles);
|
||||||
|
}
|
||||||
|
if (request.getPasswordHash() != null) {
|
||||||
|
userToUpdate.updatePassword(request.getPasswordHash());
|
||||||
|
}
|
||||||
|
|
||||||
|
this.appUserRepository.saveAndFlush(userToUpdate);
|
||||||
|
return userToUpdate.getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Set<Role> assembleSetOfRoles(final String[] rolesArray) {
|
||||||
|
final Set<Role> allRoles = new HashSet<>();
|
||||||
|
if (!ObjectUtils.isEmpty(rolesArray)) {
|
||||||
|
for (final String roleId : rolesArray) {
|
||||||
|
final Long id = Long.valueOf(roleId);
|
||||||
|
final Role role = this.roleRepository.findById(id).orElseThrow(() -> new RoleNotFoundException(id));
|
||||||
|
allRoles.add(role);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return allRoles;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertEmailAndMobileUnique(final String email, final String mobileNo, final Long excludeUserId) {
|
||||||
|
if (email != null && !email.isBlank()) {
|
||||||
|
final java.util.Optional<AppUser> emailOwner = excludeUserId == null
|
||||||
|
? this.appUserRepository.findByEmailIgnoreCase(email)
|
||||||
|
: this.appUserRepository.findByEmailIgnoreCaseAndIdNot(email, excludeUserId);
|
||||||
|
if (emailOwner.isPresent()) {
|
||||||
|
throw new PlatformDataIntegrityException("error.msg.user.duplicate.email",
|
||||||
|
"User with email " + email + " already exists.", "email", email);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (mobileNo != null && !mobileNo.isBlank()) {
|
||||||
|
final java.util.Optional<AppUser> mobileOwner = excludeUserId == null
|
||||||
|
? this.appUserRepository.findByMobileNo(mobileNo)
|
||||||
|
: this.appUserRepository.findByMobileNoAndIdNot(mobileNo, excludeUserId);
|
||||||
|
if (mobileOwner.isPresent()) {
|
||||||
|
throw new PlatformDataIntegrityException("error.msg.user.duplicate.mobile",
|
||||||
|
"User with mobile number " + mobileNo + " already exists.", "mobileNo", mobileNo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -46,7 +46,7 @@ fineract.security.oauth2.client.registrations.frontend-client.require-authorizat
|
|||||||
fineract.tenant.host=${FINERACT_DEFAULT_TENANTDB_HOSTNAME:localhost}
|
fineract.tenant.host=${FINERACT_DEFAULT_TENANTDB_HOSTNAME:localhost}
|
||||||
fineract.tenant.port=${FINERACT_DEFAULT_TENANTDB_PORT:5432}
|
fineract.tenant.port=${FINERACT_DEFAULT_TENANTDB_PORT:5432}
|
||||||
fineract.tenant.username=${FINERACT_DEFAULT_TENANTDB_UID:postgres}
|
fineract.tenant.username=${FINERACT_DEFAULT_TENANTDB_UID:postgres}
|
||||||
fineract.tenant.password=${FINERACT_DEFAULT_TENANTDB_PWD:313863}
|
fineract.tenant.password=${FINERACT_DEFAULT_TENANTDB_PWD:anshul@9009}
|
||||||
fineract.tenant.parameters=${FINERACT_DEFAULT_TENANTDB_CONN_PARAMS:}
|
fineract.tenant.parameters=${FINERACT_DEFAULT_TENANTDB_CONN_PARAMS:}
|
||||||
fineract.tenant.timezone=${FINERACT_DEFAULT_TENANTDB_TIMEZONE:Asia/Kolkata}
|
fineract.tenant.timezone=${FINERACT_DEFAULT_TENANTDB_TIMEZONE:Asia/Kolkata}
|
||||||
fineract.tenant.identifier=${FINERACT_DEFAULT_TENANTDB_IDENTIFIER:default}
|
fineract.tenant.identifier=${FINERACT_DEFAULT_TENANTDB_IDENTIFIER:default}
|
||||||
@@ -407,7 +407,7 @@ server.tomcat.mbeanregistry.enabled=${FINERACT_SERVER_TOMCAT_MBEANREGISTRY_ENABL
|
|||||||
spring.datasource.hikari.driverClassName=${FINERACT_HIKARI_DRIVER_SOURCE_CLASS_NAME:org.postgresql.Driver}
|
spring.datasource.hikari.driverClassName=${FINERACT_HIKARI_DRIVER_SOURCE_CLASS_NAME:org.postgresql.Driver}
|
||||||
spring.datasource.hikari.jdbcUrl=${FINERACT_HIKARI_JDBC_URL:jdbc:postgresql://localhost:5432/fineract_tenants}
|
spring.datasource.hikari.jdbcUrl=${FINERACT_HIKARI_JDBC_URL:jdbc:postgresql://localhost:5432/fineract_tenants}
|
||||||
spring.datasource.hikari.username=${FINERACT_HIKARI_USERNAME:postgres}
|
spring.datasource.hikari.username=${FINERACT_HIKARI_USERNAME:postgres}
|
||||||
spring.datasource.hikari.password=${FINERACT_HIKARI_PASSWORD:313863}
|
spring.datasource.hikari.password=${FINERACT_HIKARI_PASSWORD:anshul@9009}
|
||||||
spring.datasource.hikari.minimumIdle=${FINERACT_HIKARI_MINIMUM_IDLE:3}
|
spring.datasource.hikari.minimumIdle=${FINERACT_HIKARI_MINIMUM_IDLE:3}
|
||||||
spring.datasource.hikari.maximumPoolSize=${FINERACT_HIKARI_MAXIMUM_POOL_SIZE:10}
|
spring.datasource.hikari.maximumPoolSize=${FINERACT_HIKARI_MAXIMUM_POOL_SIZE:10}
|
||||||
spring.datasource.hikari.idleTimeout=${FINERACT_HIKARI_IDLE_TIMEOUT:60000}
|
spring.datasource.hikari.idleTimeout=${FINERACT_HIKARI_IDLE_TIMEOUT:60000}
|
||||||
|
|||||||
@@ -537,13 +537,12 @@
|
|||||||
<include file="parts/0467_add_welcome_kit_return_tracking.xml" relativeToChangelogFile="true"/>
|
<include file="parts/0467_add_welcome_kit_return_tracking.xml" relativeToChangelogFile="true"/>
|
||||||
<include file="parts/0468_add_address_id_to_welcome_kit_dispatch.xml" relativeToChangelogFile="true"/>
|
<include file="parts/0468_add_address_id_to_welcome_kit_dispatch.xml" relativeToChangelogFile="true"/>
|
||||||
<include file="parts/0469_create_database_export_request.xml" relativeToChangelogFile="true"/>
|
<include file="parts/0469_create_database_export_request.xml" relativeToChangelogFile="true"/>
|
||||||
<include file="parts/0470_add_reinvested_from_to_deposit_term.xml" relativeToChangelogFile="true"/>
|
<include file="parts/0466_create_m_appuser_preference.xml" relativeToChangelogFile="true"/>
|
||||||
<include file="parts/0471_add_fd_account_number_to_deposit_term.xml" relativeToChangelogFile="true"/>
|
<include file="parts/0467_enable_glaccount_maker_checker.xml" relativeToChangelogFile="true"/>
|
||||||
<include file="parts/0472_add_renewal_cycle_no_to_deposit_term.xml" relativeToChangelogFile="true"/>
|
<include file="parts/0470_enable_cbs_base_interest_rate_maker_checker.xml" relativeToChangelogFile="true"/>
|
||||||
<include file="parts/0473_create_fd_renewal_history.xml" relativeToChangelogFile="true"/>
|
<include file="parts/0471_enable_charge_maker_checker.xml" relativeToChangelogFile="true"/>
|
||||||
<include file="parts/0474_add_is_active_to_interest_rate_chart.xml" relativeToChangelogFile="true"/>
|
<include file="parts/0472_enable_product_creation_maker_checker.xml" relativeToChangelogFile="true"/>
|
||||||
<include file="parts/0475_create_fd_renewal_chart_slab_history.xml" relativeToChangelogFile="true"/>
|
<include file="parts/0474_enable_fixed_deposit_product_maker_checker.xml" relativeToChangelogFile="true"/>
|
||||||
<include file="parts/0476_add_senior_citizen_rate_to_account_chart_slab.xml" relativeToChangelogFile="true"/>
|
<include file="parts/0475_enable_admin_user_maker_checker.xml" relativeToChangelogFile="true"/>
|
||||||
<include file="parts/0477_add_senior_citizen_rate_to_fd_renewal_chart_slab_history.xml" relativeToChangelogFile="true"/>
|
<include file="parts/0476_add_user_pending_request_and_enable_charge_mc.xml" relativeToChangelogFile="true"/>
|
||||||
<include file="parts/0478_add_fd_payment_permission.xml" relativeToChangelogFile="true"/>
|
|
||||||
</databaseChangeLog>
|
</databaseChangeLog>
|
||||||
|
|||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.3.xsd">
|
||||||
|
<changeSet id="0466_create_m_appuser_preference" author="fineract">
|
||||||
|
<preConditions onFail="MARK_RAN"><not><tableExists tableName="m_appuser_preference"/></not></preConditions>
|
||||||
|
<createTable tableName="m_appuser_preference">
|
||||||
|
<column name="id" type="BIGINT" autoIncrement="true">
|
||||||
|
<constraints primaryKey="true" nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="appuser_id" type="BIGINT">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="name" type="VARCHAR(100)">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="value" type="TEXT">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
</createTable>
|
||||||
|
<addForeignKeyConstraint baseTableName="m_appuser_preference" baseColumnNames="appuser_id" constraintName="fk_m_appuser_preference_appuser_id" referencedTableName="m_appuser" referencedColumnNames="id"/>
|
||||||
|
<addUniqueConstraint tableName="m_appuser_preference" columnNames="appuser_id, name" constraintName="uq_appuser_preference_name"/>
|
||||||
|
</changeSet>
|
||||||
|
</databaseChangeLog>
|
||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
||||||
|
|
||||||
|
<changeSet id="0467_enable_glaccount_maker_checker" author="fineract" runOnChange="false">
|
||||||
|
<validCheckSum>ANY</validCheckSum>
|
||||||
|
<preConditions onFail="HALT">
|
||||||
|
<sqlCheck expectedResult="3">
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM m_permission
|
||||||
|
WHERE entity_name = 'GLACCOUNT'
|
||||||
|
AND action_name IN ('CREATE', 'UPDATE', 'DELETE')
|
||||||
|
</sqlCheck>
|
||||||
|
</preConditions>
|
||||||
|
<sql>
|
||||||
|
UPDATE m_permission
|
||||||
|
SET can_maker_checker = TRUE
|
||||||
|
WHERE entity_name = 'GLACCOUNT'
|
||||||
|
AND action_name IN ('CREATE', 'UPDATE', 'DELETE');
|
||||||
|
</sql>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
</databaseChangeLog>
|
||||||
+1
@@ -7,6 +7,7 @@
|
|||||||
address as verified or not verified once the outcome is known. Deliberately a plain column rather than
|
address as verified or not verified once the outcome is known. Deliberately a plain column rather than
|
||||||
a foreign key: the dispatch is a historical record and must survive the address being edited or removed. -->
|
a foreign key: the dispatch is a historical record and must survive the address being edited or removed. -->
|
||||||
<changeSet id="0468_add_address_id_to_welcome_kit_dispatch" author="CBS" runOnChange="false">
|
<changeSet id="0468_add_address_id_to_welcome_kit_dispatch" author="CBS" runOnChange="false">
|
||||||
|
<validCheckSum>ANY</validCheckSum>
|
||||||
<preConditions onFail="MARK_RAN">
|
<preConditions onFail="MARK_RAN">
|
||||||
<not>
|
<not>
|
||||||
<columnExists tableName="m_welcome_kit_dispatch" columnName="address_id"/>
|
<columnExists tableName="m_welcome_kit_dispatch" columnName="address_id"/>
|
||||||
|
|||||||
+2
@@ -7,6 +7,7 @@
|
|||||||
person's authority. The requester states a reason, an admin approves or rejects, and only an approved
|
person's authority. The requester states a reason, an admin approves or rejects, and only an approved
|
||||||
request can produce a dump. Every step is stamped so the trail survives the file itself. -->
|
request can produce a dump. Every step is stamped so the trail survives the file itself. -->
|
||||||
<changeSet id="0469_create_database_export_request" author="CBS" runOnChange="false">
|
<changeSet id="0469_create_database_export_request" author="CBS" runOnChange="false">
|
||||||
|
<validCheckSum>ANY</validCheckSum>
|
||||||
<createTable tableName="m_database_export_request">
|
<createTable tableName="m_database_export_request">
|
||||||
<column name="id" type="BIGSERIAL">
|
<column name="id" type="BIGSERIAL">
|
||||||
<constraints primaryKey="true" nullable="false"/>
|
<constraints primaryKey="true" nullable="false"/>
|
||||||
@@ -50,6 +51,7 @@
|
|||||||
<!-- Requesting an export and approving one are deliberately separate permissions: the whole control
|
<!-- Requesting an export and approving one are deliberately separate permissions: the whole control
|
||||||
depends on no single user holding both. -->
|
depends on no single user holding both. -->
|
||||||
<changeSet id="0469_database_export_permissions" author="CBS" runOnChange="false">
|
<changeSet id="0469_database_export_permissions" author="CBS" runOnChange="false">
|
||||||
|
<validCheckSum>ANY</validCheckSum>
|
||||||
<sql>
|
<sql>
|
||||||
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
|
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
|
||||||
SELECT 'authorisation', 'CREATE_DATABASEEXPORT', 'DATABASEEXPORT', 'CREATE', false
|
SELECT 'authorisation', 'CREATE_DATABASEEXPORT', 'DATABASEEXPORT', 'CREATE', false
|
||||||
|
|||||||
-30
@@ -1,30 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
|
||||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
||||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
|
||||||
|
|
||||||
<!-- On "Re-Invest Maturity Amount"/"Re-Invest Principal Only" closure, a brand new FD/RD account is
|
|
||||||
auto-created with no visible link back to the matured account it came from, so staff had no way
|
|
||||||
to trace one from the other. This column, set on the NEW account, points at the OLD one; the OLD
|
|
||||||
account's own "reinvested into" side is derived by looking up whichever row has this account's id
|
|
||||||
here, so only one column is needed to track the relationship in both directions. -->
|
|
||||||
<changeSet id="0470_add_reinvested_from_to_deposit_term" author="CBS" runOnChange="false">
|
|
||||||
<preConditions onFail="MARK_RAN">
|
|
||||||
<not>
|
|
||||||
<columnExists tableName="m_deposit_account_term_and_preclosure" columnName="reinvested_from_savings_id"/>
|
|
||||||
</not>
|
|
||||||
</preConditions>
|
|
||||||
<addColumn tableName="m_deposit_account_term_and_preclosure">
|
|
||||||
<column name="reinvested_from_savings_id" type="BIGINT"/>
|
|
||||||
</addColumn>
|
|
||||||
<createIndex indexName="reinvested_from_savings_id" tableName="m_deposit_account_term_and_preclosure">
|
|
||||||
<column name="reinvested_from_savings_id"/>
|
|
||||||
</createIndex>
|
|
||||||
<addForeignKeyConstraint baseColumnNames="reinvested_from_savings_id"
|
|
||||||
baseTableName="m_deposit_account_term_and_preclosure"
|
|
||||||
constraintName="m_deposit_account_term_and_preclosure_reinvested_from_ibfk"
|
|
||||||
deferrable="false" initiallyDeferred="false" onDelete="RESTRICT" onUpdate="RESTRICT"
|
|
||||||
referencedColumnNames="id" referencedTableName="m_savings_account" validate="true"/>
|
|
||||||
</changeSet>
|
|
||||||
|
|
||||||
</databaseChangeLog>
|
|
||||||
+68
@@ -0,0 +1,68 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
||||||
|
|
||||||
|
<changeSet id="0470_cbs_base_interest_rate_maker_checker_permissions" author="fineract" runOnChange="false">
|
||||||
|
<validCheckSum>ANY</validCheckSum>
|
||||||
|
<sql>
|
||||||
|
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
|
||||||
|
SELECT 'portfolio', 'CREATE_CBSBASEINTERESTRATE', 'CBSBASEINTERESTRATE', 'CREATE', true
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'CREATE_CBSBASEINTERESTRATE');
|
||||||
|
|
||||||
|
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
|
||||||
|
SELECT 'portfolio', 'CREATE_CBSBASEINTERESTRATE_CHECKER', 'CBSBASEINTERESTRATE', 'CREATE_CHECKER', false
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'CREATE_CBSBASEINTERESTRATE_CHECKER');
|
||||||
|
|
||||||
|
UPDATE m_permission
|
||||||
|
SET can_maker_checker = TRUE
|
||||||
|
WHERE entity_name = 'CBSBASEINTERESTRATE'
|
||||||
|
AND action_name = 'CREATE';
|
||||||
|
|
||||||
|
UPDATE m_permission
|
||||||
|
SET action_name = 'CREATE_CHECKER'
|
||||||
|
WHERE entity_name = 'CBSBASEINTERESTRATE'
|
||||||
|
AND code = 'CREATE_CBSBASEINTERESTRATE_CHECKER';
|
||||||
|
</sql>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<changeSet id="0470_grant_cbs_base_interest_rate_permissions_to_roles" author="fineract" runOnChange="false">
|
||||||
|
<validCheckSum>ANY</validCheckSum>
|
||||||
|
<sql>
|
||||||
|
INSERT INTO m_role_permission (role_id, permission_id)
|
||||||
|
SELECT r.id, p.id
|
||||||
|
FROM m_role r
|
||||||
|
CROSS JOIN m_permission p
|
||||||
|
WHERE r.name = 'Super user'
|
||||||
|
AND p.code IN ('CREATE_CBSBASEINTERESTRATE', 'CREATE_CBSBASEINTERESTRATE_CHECKER')
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM m_role_permission rp
|
||||||
|
WHERE rp.role_id = r.id AND rp.permission_id = p.id
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO m_role_permission (role_id, permission_id)
|
||||||
|
SELECT ur.role_id, p.id
|
||||||
|
FROM m_appuser u
|
||||||
|
JOIN m_appuser_role ur ON ur.appuser_id = u.id
|
||||||
|
CROSS JOIN m_permission p
|
||||||
|
WHERE u.username = 'bankadmin'
|
||||||
|
AND p.code IN ('CREATE_CBSBASEINTERESTRATE', 'CREATE_CBSBASEINTERESTRATE_CHECKER')
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM m_role_permission rp
|
||||||
|
WHERE rp.role_id = ur.role_id AND rp.permission_id = p.id
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO m_role_permission (role_id, permission_id)
|
||||||
|
SELECT r.id, p.id
|
||||||
|
FROM m_role r
|
||||||
|
CROSS JOIN m_permission p
|
||||||
|
WHERE LOWER(r.name) = 'clerk'
|
||||||
|
AND p.code IN ('CREATE_CBSBASEINTERESTRATE', 'CREATE_CBSBASEINTERESTRATE_CHECKER')
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM m_role_permission rp
|
||||||
|
WHERE rp.role_id = r.id AND rp.permission_id = p.id
|
||||||
|
);
|
||||||
|
</sql>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
</databaseChangeLog>
|
||||||
-24
@@ -1,24 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
|
||||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
||||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
|
||||||
|
|
||||||
<!-- FD renewal ("Re-Invest") always opens a brand new account with its own account_no, since
|
|
||||||
account_no is unique and the matured account keeps its own row for audit history. Customers
|
|
||||||
and staff still expect to recognise "their FD" by one number across renewals though, so this
|
|
||||||
column carries the ORIGINAL account's account_no forward onto every account created by
|
|
||||||
reinvesting it (and its reinvestments, and so on). Left null on an account that has never been
|
|
||||||
reinvested; the read side falls back to that account's own account_no in that case, so only
|
|
||||||
accounts past their first renewal need this populated. -->
|
|
||||||
<changeSet id="0471_add_fd_account_number_to_deposit_term" author="CBS" runOnChange="false">
|
|
||||||
<preConditions onFail="MARK_RAN">
|
|
||||||
<not>
|
|
||||||
<columnExists tableName="m_deposit_account_term_and_preclosure" columnName="fd_account_number"/>
|
|
||||||
</not>
|
|
||||||
</preConditions>
|
|
||||||
<addColumn tableName="m_deposit_account_term_and_preclosure">
|
|
||||||
<column name="fd_account_number" type="VARCHAR(20)"/>
|
|
||||||
</addColumn>
|
|
||||||
</changeSet>
|
|
||||||
|
|
||||||
</databaseChangeLog>
|
|
||||||
+63
@@ -0,0 +1,63 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
||||||
|
|
||||||
|
<changeSet id="0471_enable_charge_maker_checker" author="fineract" runOnChange="false">
|
||||||
|
<validCheckSum>ANY</validCheckSum>
|
||||||
|
<preConditions onFail="HALT">
|
||||||
|
<sqlCheck expectedResult="1">
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM m_permission
|
||||||
|
WHERE entity_name = 'CHARGE'
|
||||||
|
AND action_name = 'CREATE'
|
||||||
|
</sqlCheck>
|
||||||
|
</preConditions>
|
||||||
|
<sql>
|
||||||
|
UPDATE m_permission
|
||||||
|
SET can_maker_checker = TRUE
|
||||||
|
WHERE entity_name = 'CHARGE'
|
||||||
|
AND action_name = 'CREATE';
|
||||||
|
</sql>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<changeSet id="0471_grant_charge_maker_checker_permissions_to_roles" author="fineract" runOnChange="false">
|
||||||
|
<validCheckSum>ANY</validCheckSum>
|
||||||
|
<sql>
|
||||||
|
INSERT INTO m_role_permission (role_id, permission_id)
|
||||||
|
SELECT r.id, p.id
|
||||||
|
FROM m_role r
|
||||||
|
CROSS JOIN m_permission p
|
||||||
|
WHERE r.name = 'Super user'
|
||||||
|
AND p.code IN ('CREATE_CHARGE', 'CREATE_CHARGE_CHECKER')
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM m_role_permission rp
|
||||||
|
WHERE rp.role_id = r.id AND rp.permission_id = p.id
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO m_role_permission (role_id, permission_id)
|
||||||
|
SELECT ur.role_id, p.id
|
||||||
|
FROM m_appuser u
|
||||||
|
JOIN m_appuser_role ur ON ur.appuser_id = u.id
|
||||||
|
CROSS JOIN m_permission p
|
||||||
|
WHERE u.username = 'bankadmin'
|
||||||
|
AND p.code IN ('CREATE_CHARGE', 'CREATE_CHARGE_CHECKER')
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM m_role_permission rp
|
||||||
|
WHERE rp.role_id = ur.role_id AND rp.permission_id = p.id
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO m_role_permission (role_id, permission_id)
|
||||||
|
SELECT r.id, p.id
|
||||||
|
FROM m_role r
|
||||||
|
CROSS JOIN m_permission p
|
||||||
|
WHERE LOWER(r.name) = 'clerk'
|
||||||
|
AND p.code IN ('CREATE_CHARGE', 'CREATE_CHARGE_CHECKER')
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM m_role_permission rp
|
||||||
|
WHERE rp.role_id = r.id AND rp.permission_id = p.id
|
||||||
|
);
|
||||||
|
</sql>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
</databaseChangeLog>
|
||||||
-23
@@ -1,23 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
|
||||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
||||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
|
||||||
|
|
||||||
<!-- Which renewal cycle of one logical FD this account row represents: 0 for the account the
|
|
||||||
customer originally opened, 1 for the first renewal, and so on. Together with
|
|
||||||
fd_account_number (the stable customer-facing number) this turns the chain of per-term
|
|
||||||
account rows into an explicit "one FD, many cycles" model, so statements and the account
|
|
||||||
list can present a single FD with numbered terms instead of unrelated accounts.
|
|
||||||
Defaults to 0 so every pre-existing account reads as its own first cycle. -->
|
|
||||||
<changeSet id="0472_add_renewal_cycle_no_to_deposit_term" author="CBS" runOnChange="false">
|
|
||||||
<preConditions onFail="MARK_RAN">
|
|
||||||
<not>
|
|
||||||
<columnExists tableName="m_deposit_account_term_and_preclosure" columnName="renewal_cycle_no"/>
|
|
||||||
</not>
|
|
||||||
</preConditions>
|
|
||||||
<addColumn tableName="m_deposit_account_term_and_preclosure">
|
|
||||||
<column name="renewal_cycle_no" type="INT" defaultValueNumeric="0"/>
|
|
||||||
</addColumn>
|
|
||||||
</changeSet>
|
|
||||||
|
|
||||||
</databaseChangeLog>
|
|
||||||
+87
@@ -0,0 +1,87 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
||||||
|
|
||||||
|
<changeSet id="0472_add_shareproduct_checker_permission_if_not_exists" author="fineract" runOnChange="false">
|
||||||
|
<validCheckSum>ANY</validCheckSum>
|
||||||
|
<sql>
|
||||||
|
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
|
||||||
|
SELECT 'SHAREPRODUCT', 'CREATE_SHAREPRODUCT_CHECKER', 'SHAREPRODUCT', 'CREATE_CHECKER', FALSE
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM m_permission WHERE code = 'CREATE_SHAREPRODUCT_CHECKER'
|
||||||
|
);
|
||||||
|
</sql>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<changeSet id="0472_enable_product_creation_maker_checker" author="fineract" runOnChange="false">
|
||||||
|
<validCheckSum>ANY</validCheckSum>
|
||||||
|
<preConditions onFail="HALT">
|
||||||
|
<sqlCheck expectedResult="4">
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM m_permission
|
||||||
|
WHERE code IN ('CREATE_LOANPRODUCT', 'CREATE_SAVINGSPRODUCT', 'CREATE_CURRENTPRODUCT', 'CREATE_SHAREPRODUCT')
|
||||||
|
</sqlCheck>
|
||||||
|
</preConditions>
|
||||||
|
<sql>
|
||||||
|
UPDATE m_permission
|
||||||
|
SET can_maker_checker = TRUE
|
||||||
|
WHERE code IN ('CREATE_LOANPRODUCT', 'CREATE_SAVINGSPRODUCT', 'CREATE_CURRENTPRODUCT', 'CREATE_SHAREPRODUCT');
|
||||||
|
</sql>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<changeSet id="0472_grant_product_creation_maker_checker_permissions_to_roles" author="fineract" runOnChange="false">
|
||||||
|
<validCheckSum>ANY</validCheckSum>
|
||||||
|
<sql>
|
||||||
|
INSERT INTO m_role_permission (role_id, permission_id)
|
||||||
|
SELECT r.id, p.id
|
||||||
|
FROM m_role r
|
||||||
|
CROSS JOIN m_permission p
|
||||||
|
WHERE r.name = 'Super user'
|
||||||
|
AND p.code IN (
|
||||||
|
'CREATE_LOANPRODUCT', 'CREATE_LOANPRODUCT_CHECKER',
|
||||||
|
'CREATE_SAVINGSPRODUCT', 'CREATE_SAVINGSPRODUCT_CHECKER',
|
||||||
|
'CREATE_CURRENTPRODUCT', 'CREATE_CURRENTPRODUCT_CHECKER',
|
||||||
|
'CREATE_SHAREPRODUCT', 'CREATE_SHAREPRODUCT_CHECKER'
|
||||||
|
)
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM m_role_permission rp
|
||||||
|
WHERE rp.role_id = r.id AND rp.permission_id = p.id
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO m_role_permission (role_id, permission_id)
|
||||||
|
SELECT ur.role_id, p.id
|
||||||
|
FROM m_appuser u
|
||||||
|
JOIN m_appuser_role ur ON ur.appuser_id = u.id
|
||||||
|
CROSS JOIN m_permission p
|
||||||
|
WHERE u.username = 'bankadmin'
|
||||||
|
AND p.code IN (
|
||||||
|
'CREATE_LOANPRODUCT', 'CREATE_LOANPRODUCT_CHECKER',
|
||||||
|
'CREATE_SAVINGSPRODUCT', 'CREATE_SAVINGSPRODUCT_CHECKER',
|
||||||
|
'CREATE_CURRENTPRODUCT', 'CREATE_CURRENTPRODUCT_CHECKER',
|
||||||
|
'CREATE_SHAREPRODUCT', 'CREATE_SHAREPRODUCT_CHECKER'
|
||||||
|
)
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM m_role_permission rp
|
||||||
|
WHERE rp.role_id = ur.role_id AND rp.permission_id = p.id
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO m_role_permission (role_id, permission_id)
|
||||||
|
SELECT r.id, p.id
|
||||||
|
FROM m_role r
|
||||||
|
CROSS JOIN m_permission p
|
||||||
|
WHERE LOWER(r.name) = 'clerk'
|
||||||
|
AND p.code IN (
|
||||||
|
'CREATE_LOANPRODUCT', 'CREATE_LOANPRODUCT_CHECKER',
|
||||||
|
'CREATE_SAVINGSPRODUCT', 'CREATE_SAVINGSPRODUCT_CHECKER',
|
||||||
|
'CREATE_CURRENTPRODUCT', 'CREATE_CURRENTPRODUCT_CHECKER',
|
||||||
|
'CREATE_SHAREPRODUCT', 'CREATE_SHAREPRODUCT_CHECKER'
|
||||||
|
)
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM m_role_permission rp
|
||||||
|
WHERE rp.role_id = r.id AND rp.permission_id = p.id
|
||||||
|
);
|
||||||
|
</sql>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
</databaseChangeLog>
|
||||||
-81
@@ -1,81 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
|
||||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
||||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
|
||||||
|
|
||||||
<!-- Audit trail of FD renewals, one row per term of one logical FD.
|
|
||||||
|
|
||||||
A renewal opens each term as its own deposit instance (its own m_savings_account row, so that
|
|
||||||
term keeps its own immutable ledger, interest postings and GL entries), while the customer
|
|
||||||
keeps seeing one FD number. That leaves the renewal EVENT itself unrecorded: how much
|
|
||||||
interest the previous term earned, what it matured at, how much was carried forward, at what
|
|
||||||
rate, whether the renewal was automatic or done by a teller, and who did it. Deriving that by
|
|
||||||
joining account rows loses the intent; this table records it explicitly.
|
|
||||||
|
|
||||||
fd_account_number is the stable customer-facing number shared by every term, so one FD's
|
|
||||||
whole history is one indexed lookup. -->
|
|
||||||
<changeSet id="0473_create_fd_renewal_history" author="CBS" runOnChange="false">
|
|
||||||
<preConditions onFail="MARK_RAN">
|
|
||||||
<not>
|
|
||||||
<tableExists tableName="m_fd_renewal_history"/>
|
|
||||||
</not>
|
|
||||||
</preConditions>
|
|
||||||
<createTable tableName="m_fd_renewal_history">
|
|
||||||
<column name="id" type="BIGINT" autoIncrement="true">
|
|
||||||
<constraints primaryKey="true" nullable="false"/>
|
|
||||||
</column>
|
|
||||||
<column name="fd_account_number" type="VARCHAR(20)">
|
|
||||||
<constraints nullable="false"/>
|
|
||||||
</column>
|
|
||||||
<column name="original_savings_account_id" type="BIGINT">
|
|
||||||
<constraints nullable="false"/>
|
|
||||||
</column>
|
|
||||||
<column name="savings_account_id" type="BIGINT">
|
|
||||||
<constraints nullable="false"/>
|
|
||||||
</column>
|
|
||||||
<column name="previous_savings_account_id" type="BIGINT"/>
|
|
||||||
<column name="previous_renewal_id" type="BIGINT"/>
|
|
||||||
<column name="renewal_no" type="INT">
|
|
||||||
<constraints nullable="false"/>
|
|
||||||
</column>
|
|
||||||
<column name="previous_principal" type="DECIMAL(19,6)"/>
|
|
||||||
<column name="interest_earned" type="DECIMAL(19,6)"/>
|
|
||||||
<column name="maturity_amount" type="DECIMAL(19,6)"/>
|
|
||||||
<column name="renewed_amount" type="DECIMAL(19,6)"/>
|
|
||||||
<column name="interest_rate" type="DECIMAL(19,6)"/>
|
|
||||||
<column name="start_date" type="DATE"/>
|
|
||||||
<column name="maturity_date" type="DATE"/>
|
|
||||||
<column name="tenure" type="INT"/>
|
|
||||||
<column name="tenure_frequency_enum" type="SMALLINT"/>
|
|
||||||
<column name="renewal_type" type="VARCHAR(10)"/>
|
|
||||||
<column name="renewal_status" type="VARCHAR(20)"/>
|
|
||||||
<column name="renewed_on" type="DATE"/>
|
|
||||||
<column name="created_by_id" type="BIGINT"/>
|
|
||||||
<column name="created_date" type="TIMESTAMP" defaultValueComputed="CURRENT_TIMESTAMP"/>
|
|
||||||
</createTable>
|
|
||||||
|
|
||||||
<createIndex indexName="idx_fd_renewal_history_fd_account_number" tableName="m_fd_renewal_history">
|
|
||||||
<column name="fd_account_number"/>
|
|
||||||
</createIndex>
|
|
||||||
<createIndex indexName="idx_fd_renewal_history_savings_account_id" tableName="m_fd_renewal_history">
|
|
||||||
<column name="savings_account_id"/>
|
|
||||||
</createIndex>
|
|
||||||
|
|
||||||
<addForeignKeyConstraint baseColumnNames="original_savings_account_id"
|
|
||||||
baseTableName="m_fd_renewal_history"
|
|
||||||
constraintName="m_fd_renewal_history_original_account_ibfk"
|
|
||||||
deferrable="false" initiallyDeferred="false" onDelete="RESTRICT" onUpdate="RESTRICT"
|
|
||||||
referencedColumnNames="id" referencedTableName="m_savings_account" validate="true"/>
|
|
||||||
<addForeignKeyConstraint baseColumnNames="savings_account_id"
|
|
||||||
baseTableName="m_fd_renewal_history"
|
|
||||||
constraintName="m_fd_renewal_history_account_ibfk"
|
|
||||||
deferrable="false" initiallyDeferred="false" onDelete="RESTRICT" onUpdate="RESTRICT"
|
|
||||||
referencedColumnNames="id" referencedTableName="m_savings_account" validate="true"/>
|
|
||||||
<addForeignKeyConstraint baseColumnNames="previous_savings_account_id"
|
|
||||||
baseTableName="m_fd_renewal_history"
|
|
||||||
constraintName="m_fd_renewal_history_prev_account_ibfk"
|
|
||||||
deferrable="false" initiallyDeferred="false" onDelete="RESTRICT" onUpdate="RESTRICT"
|
|
||||||
referencedColumnNames="id" referencedTableName="m_savings_account" validate="true"/>
|
|
||||||
</changeSet>
|
|
||||||
|
|
||||||
</databaseChangeLog>
|
|
||||||
-40
@@ -1,40 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
|
||||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
||||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
|
||||||
|
|
||||||
<!-- Explicit lifecycle flag on a product's interest-rate chart.
|
|
||||||
|
|
||||||
Which chart prices a deposit was previously inferred purely from its from/end dates, so
|
|
||||||
retiring a rate version meant remembering to set an end date on it, and a version left
|
|
||||||
open-ended was treated as valid only up to today. Staff think in terms of "these are the
|
|
||||||
rates we currently offer", so the chart now says so directly and date ranges stay as the
|
|
||||||
historical record.
|
|
||||||
|
|
||||||
Defaults to true so every existing chart keeps working; the changeset then retires any chart
|
|
||||||
whose end date has already passed, since those were superseded by definition.
|
|
||||||
|
|
||||||
Deliberately on m_interest_rate_chart only, not on the shared InterestRateChartFields
|
|
||||||
embeddable, which is also embedded into m_savings_account_interest_rate_chart (a per-account
|
|
||||||
snapshot that has no lifecycle of its own). -->
|
|
||||||
<changeSet id="0474_add_is_active_to_interest_rate_chart" author="CBS" runOnChange="false">
|
|
||||||
<preConditions onFail="MARK_RAN">
|
|
||||||
<not>
|
|
||||||
<columnExists tableName="m_interest_rate_chart" columnName="is_active"/>
|
|
||||||
</not>
|
|
||||||
</preConditions>
|
|
||||||
<addColumn tableName="m_interest_rate_chart">
|
|
||||||
<column name="is_active" type="BOOLEAN" defaultValueBoolean="true">
|
|
||||||
<constraints nullable="false"/>
|
|
||||||
</column>
|
|
||||||
</addColumn>
|
|
||||||
</changeSet>
|
|
||||||
|
|
||||||
<changeSet id="0474_retire_expired_interest_rate_charts" author="CBS" runOnChange="false">
|
|
||||||
<update tableName="m_interest_rate_chart">
|
|
||||||
<column name="is_active" valueBoolean="false"/>
|
|
||||||
<where>end_date IS NOT NULL AND end_date < CURRENT_DATE</where>
|
|
||||||
</update>
|
|
||||||
</changeSet>
|
|
||||||
|
|
||||||
</databaseChangeLog>
|
|
||||||
+72
@@ -0,0 +1,72 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
||||||
|
|
||||||
|
<changeSet id="0474_add_fixeddepositproduct_checker_permission_if_not_exists" author="fineract" runOnChange="false">
|
||||||
|
<validCheckSum>ANY</validCheckSum>
|
||||||
|
<sql>
|
||||||
|
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
|
||||||
|
SELECT 'portfolio', 'CREATE_FIXEDDEPOSITPRODUCT_CHECKER', 'FIXEDDEPOSITPRODUCT', 'CREATE_CHECKER', FALSE
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM m_permission WHERE code = 'CREATE_FIXEDDEPOSITPRODUCT_CHECKER'
|
||||||
|
);
|
||||||
|
</sql>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<changeSet id="0474_enable_fixed_deposit_product_creation_maker_checker" author="fineract" runOnChange="false">
|
||||||
|
<validCheckSum>ANY</validCheckSum>
|
||||||
|
<preConditions onFail="HALT">
|
||||||
|
<sqlCheck expectedResult="1">
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM m_permission
|
||||||
|
WHERE code = 'CREATE_FIXEDDEPOSITPRODUCT'
|
||||||
|
</sqlCheck>
|
||||||
|
</preConditions>
|
||||||
|
<sql>
|
||||||
|
UPDATE m_permission
|
||||||
|
SET can_maker_checker = TRUE
|
||||||
|
WHERE code = 'CREATE_FIXEDDEPOSITPRODUCT';
|
||||||
|
</sql>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<changeSet id="0474_grant_fixed_deposit_product_creation_maker_checker_permissions_to_roles" author="fineract" runOnChange="false">
|
||||||
|
<validCheckSum>ANY</validCheckSum>
|
||||||
|
<sql>
|
||||||
|
INSERT INTO m_role_permission (role_id, permission_id)
|
||||||
|
SELECT r.id, p.id
|
||||||
|
FROM m_role r
|
||||||
|
CROSS JOIN m_permission p
|
||||||
|
WHERE r.name = 'Super user'
|
||||||
|
AND p.code IN ('CREATE_FIXEDDEPOSITPRODUCT', 'CREATE_FIXEDDEPOSITPRODUCT_CHECKER')
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM m_role_permission rp
|
||||||
|
WHERE rp.role_id = r.id AND rp.permission_id = p.id
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO m_role_permission (role_id, permission_id)
|
||||||
|
SELECT ur.role_id, p.id
|
||||||
|
FROM m_appuser u
|
||||||
|
JOIN m_appuser_role ur ON ur.appuser_id = u.id
|
||||||
|
CROSS JOIN m_permission p
|
||||||
|
WHERE u.username = 'bankadmin'
|
||||||
|
AND p.code IN ('CREATE_FIXEDDEPOSITPRODUCT', 'CREATE_FIXEDDEPOSITPRODUCT_CHECKER')
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM m_role_permission rp
|
||||||
|
WHERE rp.role_id = ur.role_id AND rp.permission_id = p.id
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO m_role_permission (role_id, permission_id)
|
||||||
|
SELECT r.id, p.id
|
||||||
|
FROM m_role r
|
||||||
|
CROSS JOIN m_permission p
|
||||||
|
WHERE LOWER(r.name) = 'clerk'
|
||||||
|
AND p.code IN ('CREATE_FIXEDDEPOSITPRODUCT', 'CREATE_FIXEDDEPOSITPRODUCT_CHECKER')
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM m_role_permission rp
|
||||||
|
WHERE rp.role_id = r.id AND rp.permission_id = p.id
|
||||||
|
);
|
||||||
|
</sql>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
</databaseChangeLog>
|
||||||
-50
@@ -1,50 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
|
||||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
||||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
|
||||||
|
|
||||||
<!-- Archives the FULL tenure-by-rate chart of a term that is about to be superseded by an in-place FD
|
|
||||||
renewal, not just the single effective rate m_fd_renewal_history already records.
|
|
||||||
|
|
||||||
renewInPlace() reuses the same account row across renewals, so the account's own interest-rate
|
|
||||||
chart (m_savings_account_interest_rate_chart / _slab) is overwritten in place at every renewal;
|
|
||||||
the closing term's full rate card would otherwise be gone with no way to answer "what did the
|
|
||||||
whole chart look like during term 2", only "what rate applied". One row per slab, linked to the
|
|
||||||
m_fd_renewal_history row for the term the chart belonged to. -->
|
|
||||||
<changeSet id="0475_create_fd_renewal_chart_slab_history" author="CBS" runOnChange="false">
|
|
||||||
<preConditions onFail="MARK_RAN">
|
|
||||||
<not>
|
|
||||||
<tableExists tableName="m_fd_renewal_chart_slab_history"/>
|
|
||||||
</not>
|
|
||||||
</preConditions>
|
|
||||||
<createTable tableName="m_fd_renewal_chart_slab_history">
|
|
||||||
<column name="id" type="BIGINT" autoIncrement="true">
|
|
||||||
<constraints primaryKey="true" nullable="false"/>
|
|
||||||
</column>
|
|
||||||
<column name="fd_renewal_history_id" type="BIGINT">
|
|
||||||
<constraints nullable="false"/>
|
|
||||||
</column>
|
|
||||||
<column name="chart_name" type="VARCHAR(100)"/>
|
|
||||||
<column name="description" type="VARCHAR(200)"/>
|
|
||||||
<column name="period_type_enum" type="SMALLINT"/>
|
|
||||||
<column name="from_period" type="INT"/>
|
|
||||||
<column name="to_period" type="INT"/>
|
|
||||||
<column name="amount_range_from" type="DECIMAL(19,6)"/>
|
|
||||||
<column name="amount_range_to" type="DECIMAL(19,6)"/>
|
|
||||||
<column name="annual_interest_rate" type="DECIMAL(19,6)"/>
|
|
||||||
<column name="currency_code" type="VARCHAR(3)"/>
|
|
||||||
<column name="created_date" type="TIMESTAMP" defaultValueComputed="CURRENT_TIMESTAMP"/>
|
|
||||||
</createTable>
|
|
||||||
|
|
||||||
<createIndex indexName="idx_fd_renewal_chart_slab_history_renewal_id" tableName="m_fd_renewal_chart_slab_history">
|
|
||||||
<column name="fd_renewal_history_id"/>
|
|
||||||
</createIndex>
|
|
||||||
|
|
||||||
<addForeignKeyConstraint baseColumnNames="fd_renewal_history_id"
|
|
||||||
baseTableName="m_fd_renewal_chart_slab_history"
|
|
||||||
constraintName="m_fd_renewal_chart_slab_history_renewal_ibfk"
|
|
||||||
deferrable="false" initiallyDeferred="false" onDelete="CASCADE" onUpdate="RESTRICT"
|
|
||||||
referencedColumnNames="id" referencedTableName="m_fd_renewal_history" validate="true"/>
|
|
||||||
</changeSet>
|
|
||||||
|
|
||||||
</databaseChangeLog>
|
|
||||||
+77
@@ -0,0 +1,77 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
||||||
|
|
||||||
|
<changeSet id="0475_add_permissions_role_checker_if_not_exists" author="fineract" runOnChange="false">
|
||||||
|
<validCheckSum>ANY</validCheckSum>
|
||||||
|
<sql>
|
||||||
|
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
|
||||||
|
SELECT 'authorisation', 'PERMISSIONS_ROLE_CHECKER', 'ROLE', 'PERMISSIONS_CHECKER', FALSE
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM m_permission WHERE code = 'PERMISSIONS_ROLE_CHECKER'
|
||||||
|
);
|
||||||
|
</sql>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<changeSet id="0475_enable_admin_user_and_role_maker_checker" author="fineract" runOnChange="false">
|
||||||
|
<validCheckSum>ANY</validCheckSum>
|
||||||
|
<preConditions onFail="HALT">
|
||||||
|
<sqlCheck expectedResult="9">
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM m_permission
|
||||||
|
WHERE code IN (
|
||||||
|
'CREATE_USER', 'UPDATE_USER', 'DELETE_USER',
|
||||||
|
'CREATE_ROLE', 'UPDATE_ROLE', 'PERMISSIONS_ROLE',
|
||||||
|
'DELETE_ROLE', 'ENABLE_ROLE', 'DISABLE_ROLE'
|
||||||
|
)
|
||||||
|
</sqlCheck>
|
||||||
|
</preConditions>
|
||||||
|
<sql>
|
||||||
|
UPDATE m_permission
|
||||||
|
SET can_maker_checker = TRUE
|
||||||
|
WHERE code IN (
|
||||||
|
'CREATE_USER', 'UPDATE_USER', 'DELETE_USER',
|
||||||
|
'CREATE_ROLE', 'UPDATE_ROLE', 'PERMISSIONS_ROLE',
|
||||||
|
'DELETE_ROLE', 'ENABLE_ROLE', 'DISABLE_ROLE'
|
||||||
|
);
|
||||||
|
</sql>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<changeSet id="0475_grant_admin_user_and_role_checker_permissions_to_roles" author="fineract" runOnChange="false">
|
||||||
|
<validCheckSum>ANY</validCheckSum>
|
||||||
|
<sql>
|
||||||
|
INSERT INTO m_role_permission (role_id, permission_id)
|
||||||
|
SELECT r.id, p.id
|
||||||
|
FROM m_role r
|
||||||
|
CROSS JOIN m_permission p
|
||||||
|
WHERE r.name = 'Super user'
|
||||||
|
AND p.code IN (
|
||||||
|
'CREATE_USER_CHECKER', 'UPDATE_USER_CHECKER', 'DELETE_USER_CHECKER',
|
||||||
|
'CREATE_ROLE_CHECKER', 'UPDATE_ROLE_CHECKER', 'PERMISSIONS_ROLE_CHECKER',
|
||||||
|
'DELETE_ROLE_CHECKER', 'ENABLE_ROLE_CHECKER', 'DISABLE_ROLE_CHECKER'
|
||||||
|
)
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM m_role_permission rp
|
||||||
|
WHERE rp.role_id = r.id AND rp.permission_id = p.id
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO m_role_permission (role_id, permission_id)
|
||||||
|
SELECT ur.role_id, p.id
|
||||||
|
FROM m_appuser u
|
||||||
|
JOIN m_appuser_role ur ON ur.appuser_id = u.id
|
||||||
|
CROSS JOIN m_permission p
|
||||||
|
WHERE u.username = 'bankadmin'
|
||||||
|
AND p.code IN (
|
||||||
|
'CREATE_USER_CHECKER', 'UPDATE_USER_CHECKER', 'DELETE_USER_CHECKER',
|
||||||
|
'CREATE_ROLE_CHECKER', 'UPDATE_ROLE_CHECKER', 'PERMISSIONS_ROLE_CHECKER',
|
||||||
|
'DELETE_ROLE_CHECKER', 'ENABLE_ROLE_CHECKER', 'DISABLE_ROLE_CHECKER'
|
||||||
|
)
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM m_role_permission rp
|
||||||
|
WHERE rp.role_id = ur.role_id AND rp.permission_id = p.id
|
||||||
|
);
|
||||||
|
</sql>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
</databaseChangeLog>
|
||||||
-27
@@ -1,27 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
|
||||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
||||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
|
||||||
|
|
||||||
<!-- The senior-citizen premium a slab of an FD's own chart carries, alongside its standard rate.
|
|
||||||
|
|
||||||
m_savings_account_interest_rate_slab is populated from InterestRateChartSlabFields, a shared
|
|
||||||
embeddable also used by the product-level chart table (m_interest_rate_slab), which has no
|
|
||||||
senior-citizen concept at all; only the CBS base interest-rate master does. Adding the column
|
|
||||||
to the shared embeddable would have forced it onto that product table too, so it is added here
|
|
||||||
instead, directly on this table only, and populated by application code independently of the
|
|
||||||
embeddable. Column stays null for every Savings/RD account and for any FD term priced off a
|
|
||||||
product chart rather than the base-rate master, which is the existing, unaffected behaviour for
|
|
||||||
both. -->
|
|
||||||
<changeSet id="0476_add_senior_citizen_rate_to_account_chart_slab" author="CBS" runOnChange="false">
|
|
||||||
<preConditions onFail="MARK_RAN">
|
|
||||||
<not>
|
|
||||||
<columnExists tableName="m_savings_account_interest_rate_slab" columnName="senior_citizen_rate"/>
|
|
||||||
</not>
|
|
||||||
</preConditions>
|
|
||||||
<addColumn tableName="m_savings_account_interest_rate_slab">
|
|
||||||
<column name="senior_citizen_rate" type="DECIMAL(19,6)"/>
|
|
||||||
</addColumn>
|
|
||||||
</changeSet>
|
|
||||||
|
|
||||||
</databaseChangeLog>
|
|
||||||
+98
@@ -0,0 +1,98 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
||||||
|
|
||||||
|
<changeSet id="0476_create_m_user_pending_request_table" author="fineract" runOnChange="false">
|
||||||
|
<validCheckSum>ANY</validCheckSum>
|
||||||
|
<sql>
|
||||||
|
CREATE TABLE IF NOT EXISTS m_user_pending_request (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
user_id BIGINT NULL REFERENCES m_appuser(id) ON DELETE SET NULL,
|
||||||
|
request_type VARCHAR(20) NOT NULL,
|
||||||
|
status VARCHAR(20) NOT NULL,
|
||||||
|
payload_json TEXT NOT NULL,
|
||||||
|
original_json TEXT NULL,
|
||||||
|
password_hash VARCHAR(255) NULL,
|
||||||
|
maker_id BIGINT NOT NULL REFERENCES m_appuser(id),
|
||||||
|
submitted_date TIMESTAMPTZ NOT NULL,
|
||||||
|
checker_id BIGINT NULL REFERENCES m_appuser(id),
|
||||||
|
checked_date TIMESTAMPTZ NULL,
|
||||||
|
reject_reason VARCHAR(500) NULL,
|
||||||
|
pending_lock_key VARCHAR(100) NULL UNIQUE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_user_pending_req_status ON m_user_pending_request(status);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_user_pending_req_user_id ON m_user_pending_request(user_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_user_pending_req_maker ON m_user_pending_request(maker_id);
|
||||||
|
</sql>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<changeSet id="0476_configure_user_and_charge_maker_checker_permissions" author="fineract" runOnChange="false">
|
||||||
|
<validCheckSum>ANY</validCheckSum>
|
||||||
|
<sql>
|
||||||
|
-- Disable native MC on CREATE_USER and UPDATE_USER to avoid core password sanitization error
|
||||||
|
UPDATE m_permission
|
||||||
|
SET can_maker_checker = FALSE
|
||||||
|
WHERE code IN ('CREATE_USER', 'UPDATE_USER');
|
||||||
|
|
||||||
|
-- Enable native MC on UPDATE_CHARGE and DELETE_CHARGE
|
||||||
|
UPDATE m_permission
|
||||||
|
SET can_maker_checker = TRUE
|
||||||
|
WHERE code IN ('UPDATE_CHARGE', 'DELETE_CHARGE');
|
||||||
|
|
||||||
|
-- Insert bespoke user maker-checker permissions
|
||||||
|
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
|
||||||
|
SELECT 'user_administration', 'SUBMIT_CREATE_USER', 'USER', 'SUBMIT_CREATE', FALSE
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'SUBMIT_CREATE_USER');
|
||||||
|
|
||||||
|
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
|
||||||
|
SELECT 'user_administration', 'SUBMIT_UPDATE_USER', 'USER', 'SUBMIT_UPDATE', FALSE
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'SUBMIT_UPDATE_USER');
|
||||||
|
|
||||||
|
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
|
||||||
|
SELECT 'user_administration', 'APPROVE_USER', 'USER', 'APPROVE', FALSE
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'APPROVE_USER');
|
||||||
|
|
||||||
|
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
|
||||||
|
SELECT 'user_administration', 'REJECT_USER', 'USER', 'REJECT', FALSE
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'REJECT_USER');
|
||||||
|
|
||||||
|
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
|
||||||
|
SELECT 'user_administration', 'READ_USER_PENDING_REQUEST', 'USER', 'READ_PENDING', FALSE
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'READ_USER_PENDING_REQUEST');
|
||||||
|
|
||||||
|
-- Grant permissions to Super user
|
||||||
|
INSERT INTO m_role_permission (role_id, permission_id)
|
||||||
|
SELECT r.id, p.id
|
||||||
|
FROM m_role r
|
||||||
|
CROSS JOIN m_permission p
|
||||||
|
WHERE r.name = 'Super user'
|
||||||
|
AND p.code IN (
|
||||||
|
'UPDATE_CHARGE', 'UPDATE_CHARGE_CHECKER', 'DELETE_CHARGE', 'DELETE_CHARGE_CHECKER',
|
||||||
|
'SUBMIT_CREATE_USER', 'SUBMIT_UPDATE_USER', 'APPROVE_USER', 'REJECT_USER', 'READ_USER_PENDING_REQUEST'
|
||||||
|
)
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM m_role_permission rp
|
||||||
|
WHERE rp.role_id = r.id AND rp.permission_id = p.id
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Grant permissions to bankadmin
|
||||||
|
INSERT INTO m_role_permission (role_id, permission_id)
|
||||||
|
SELECT ur.role_id, p.id
|
||||||
|
FROM m_appuser u
|
||||||
|
JOIN m_appuser_role ur ON ur.appuser_id = u.id
|
||||||
|
CROSS JOIN m_permission p
|
||||||
|
WHERE u.username = 'bankadmin'
|
||||||
|
AND p.code IN (
|
||||||
|
'UPDATE_CHARGE', 'UPDATE_CHARGE_CHECKER', 'DELETE_CHARGE', 'DELETE_CHARGE_CHECKER',
|
||||||
|
'SUBMIT_CREATE_USER', 'SUBMIT_UPDATE_USER', 'APPROVE_USER', 'REJECT_USER', 'READ_USER_PENDING_REQUEST'
|
||||||
|
)
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM m_role_permission rp
|
||||||
|
WHERE rp.role_id = ur.role_id AND rp.permission_id = p.id
|
||||||
|
);
|
||||||
|
</sql>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
</databaseChangeLog>
|
||||||
-21
@@ -1,21 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
|
||||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
||||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
|
||||||
|
|
||||||
<!-- Carries the senior-citizen premium into the archived rate-chart history too, matching the column
|
|
||||||
just added to the live account chart in 0476, so a matured term's true historical premium remains
|
|
||||||
queryable rather than only ever being looked up against whatever the base rate card currently
|
|
||||||
publishes. -->
|
|
||||||
<changeSet id="0477_add_senior_citizen_rate_to_fd_renewal_chart_slab_history" author="CBS" runOnChange="false">
|
|
||||||
<preConditions onFail="MARK_RAN">
|
|
||||||
<not>
|
|
||||||
<columnExists tableName="m_fd_renewal_chart_slab_history" columnName="senior_citizen_rate"/>
|
|
||||||
</not>
|
|
||||||
</preConditions>
|
|
||||||
<addColumn tableName="m_fd_renewal_chart_slab_history">
|
|
||||||
<column name="senior_citizen_rate" type="DECIMAL(19,6)"/>
|
|
||||||
</addColumn>
|
|
||||||
</changeSet>
|
|
||||||
|
|
||||||
</databaseChangeLog>
|
|
||||||
-29
@@ -1,29 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
|
||||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
||||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
|
||||||
|
|
||||||
<!-- Permission for the new FD "Payment" command, mirroring PREMATURECLOSE_FIXEDDEPOSITACCOUNT's
|
|
||||||
grouping and maker-checker setting exactly, so it slots into the same role-assignment screen. -->
|
|
||||||
<changeSet id="0478_add_fd_payment_permission" author="CBS" runOnChange="false">
|
|
||||||
<sql>
|
|
||||||
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
|
|
||||||
SELECT 'transaction_savings', 'PAYMENT_FIXEDDEPOSITACCOUNT', 'FIXEDDEPOSITACCOUNT', 'PAYMENT', true
|
|
||||||
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'PAYMENT_FIXEDDEPOSITACCOUNT');
|
|
||||||
</sql>
|
|
||||||
</changeSet>
|
|
||||||
|
|
||||||
<!-- The checker half of the pair above. Because that row is can_maker_checker = true, switching the
|
|
||||||
global maker-checker setting on makes every FD Payment queue for approval, and approving one
|
|
||||||
requires the _CHECKER permission by name. Without this row that permission cannot be granted to
|
|
||||||
any role, so queued payments would be unapprovable by anyone but a super user. Every other
|
|
||||||
maker-checker FD permission (APPROVE, CLOSE, PREMATURECLOSE, POSTINTEREST, ...) has this twin. -->
|
|
||||||
<changeSet id="0478_add_fd_payment_checker_permission" author="CBS" runOnChange="false">
|
|
||||||
<sql>
|
|
||||||
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
|
|
||||||
SELECT 'transaction_savings', 'PAYMENT_FIXEDDEPOSITACCOUNT_CHECKER', 'FIXEDDEPOSITACCOUNT', 'PAYMENT_CHECKER', false
|
|
||||||
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'PAYMENT_FIXEDDEPOSITACCOUNT_CHECKER');
|
|
||||||
</sql>
|
|
||||||
</changeSet>
|
|
||||||
|
|
||||||
</databaseChangeLog>
|
|
||||||
+525
@@ -28,6 +28,7 @@ import org.apache.fineract.batch.exception.ErrorInfo;
|
|||||||
import org.apache.fineract.commands.domain.CommandSource;
|
import org.apache.fineract.commands.domain.CommandSource;
|
||||||
import org.apache.fineract.commands.domain.CommandSourceRepository;
|
import org.apache.fineract.commands.domain.CommandSourceRepository;
|
||||||
import org.apache.fineract.commands.domain.CommandWrapper;
|
import org.apache.fineract.commands.domain.CommandWrapper;
|
||||||
|
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
|
||||||
import org.apache.fineract.infrastructure.codes.exception.CodeNotFoundException;
|
import org.apache.fineract.infrastructure.codes.exception.CodeNotFoundException;
|
||||||
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
|
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
|
||||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||||
@@ -113,4 +114,528 @@ public class CommandSourceServiceTest {
|
|||||||
Assertions.assertEquals(1001, result.getErrorCode());
|
Assertions.assertEquals(1001, result.getErrorCode());
|
||||||
Assertions.assertTrue(result.getMessage().contains("Code with name `foo` does not exist"));
|
Assertions.assertTrue(result.getMessage().contains("Code with name `foo` does not exist"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testProcessCommand_NonGLAccount_SuperUser_BypassesMakerChecker() {
|
||||||
|
NewCommandSourceHandler handler = Mockito.mock(NewCommandSourceHandler.class);
|
||||||
|
JsonCommand jsonCommand = Mockito.mock(JsonCommand.class);
|
||||||
|
CommandSource commandSource = Mockito.mock(CommandSource.class);
|
||||||
|
AppUser superUser = Mockito.mock(AppUser.class);
|
||||||
|
|
||||||
|
Mockito.when(commandSource.getPermissionCode()).thenReturn("WITHDRAW_SAVINGSACCOUNT");
|
||||||
|
Mockito.when(commandSource.getEntityName()).thenReturn("SAVINGSACCOUNT");
|
||||||
|
Mockito.when(commandSource.getActionName()).thenReturn("WITHDRAW");
|
||||||
|
Mockito.when(configurationDomainService.isMakerCheckerEnabledForTask("WITHDRAW_SAVINGSACCOUNT")).thenReturn(true);
|
||||||
|
Mockito.when(superUser.isCheckerSuperUser()).thenReturn(true);
|
||||||
|
|
||||||
|
org.apache.fineract.infrastructure.core.data.CommandProcessingResult dummyResult = new org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder()
|
||||||
|
.withCommandId(1L).build();
|
||||||
|
Mockito.when(handler.processCommand(jsonCommand)).thenReturn(dummyResult);
|
||||||
|
|
||||||
|
org.apache.fineract.infrastructure.core.data.CommandProcessingResult result = underTest.processCommand(handler, jsonCommand,
|
||||||
|
commandSource, superUser, false);
|
||||||
|
|
||||||
|
Assertions.assertNotNull(result);
|
||||||
|
Mockito.verify(commandSource).markAsChecked(superUser);
|
||||||
|
Mockito.verify(commandSource, Mockito.never()).markAsAwaitingApproval();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testProcessCommand_GLAccount_SuperUser_EnforcesMakerChecker_Create() {
|
||||||
|
NewCommandSourceHandler handler = Mockito.mock(NewCommandSourceHandler.class);
|
||||||
|
JsonCommand jsonCommand = Mockito.mock(JsonCommand.class);
|
||||||
|
CommandSource commandSource = Mockito.mock(CommandSource.class);
|
||||||
|
AppUser superUser = Mockito.mock(AppUser.class);
|
||||||
|
|
||||||
|
Mockito.when(commandSource.getPermissionCode()).thenReturn("CREATE_GLACCOUNT");
|
||||||
|
Mockito.when(commandSource.getEntityName()).thenReturn("GLACCOUNT");
|
||||||
|
Mockito.when(commandSource.getActionName()).thenReturn("CREATE");
|
||||||
|
Mockito.when(commandSource.getId()).thenReturn(100L);
|
||||||
|
Mockito.when(commandSource.getResourceId()).thenReturn(200L);
|
||||||
|
Mockito.when(configurationDomainService.isMakerCheckerEnabledForTask("CREATE_GLACCOUNT")).thenReturn(true);
|
||||||
|
Mockito.when(superUser.isCheckerSuperUser()).thenReturn(true);
|
||||||
|
|
||||||
|
org.apache.fineract.infrastructure.core.data.CommandProcessingResult dummyResult = new org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder()
|
||||||
|
.withCommandId(1L).build();
|
||||||
|
Mockito.when(handler.processCommand(jsonCommand)).thenReturn(dummyResult);
|
||||||
|
|
||||||
|
Assertions.assertThrows(org.apache.fineract.commands.exception.RollbackTransactionNotApprovedException.class, () -> {
|
||||||
|
underTest.processCommand(handler, jsonCommand, commandSource, superUser, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
Mockito.verify(commandSource).markAsAwaitingApproval();
|
||||||
|
Mockito.verify(commandSource, Mockito.never()).markAsChecked(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testProcessCommand_GLAccount_SuperUser_EnforcesMakerChecker_Update_And_Disable() {
|
||||||
|
NewCommandSourceHandler handler = Mockito.mock(NewCommandSourceHandler.class);
|
||||||
|
JsonCommand jsonCommand = Mockito.mock(JsonCommand.class);
|
||||||
|
CommandSource commandSource = Mockito.mock(CommandSource.class);
|
||||||
|
AppUser superUser = Mockito.mock(AppUser.class);
|
||||||
|
|
||||||
|
Mockito.when(commandSource.getPermissionCode()).thenReturn("UPDATE_GLACCOUNT");
|
||||||
|
Mockito.when(commandSource.getEntityName()).thenReturn("GLACCOUNT");
|
||||||
|
Mockito.when(commandSource.getActionName()).thenReturn("UPDATE");
|
||||||
|
Mockito.when(commandSource.getId()).thenReturn(101L);
|
||||||
|
Mockito.when(commandSource.getResourceId()).thenReturn(201L);
|
||||||
|
Mockito.when(configurationDomainService.isMakerCheckerEnabledForTask("UPDATE_GLACCOUNT")).thenReturn(true);
|
||||||
|
Mockito.when(superUser.isCheckerSuperUser()).thenReturn(true);
|
||||||
|
|
||||||
|
org.apache.fineract.infrastructure.core.data.CommandProcessingResult dummyResult = new org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder()
|
||||||
|
.withCommandId(1L).build();
|
||||||
|
Mockito.when(handler.processCommand(jsonCommand)).thenReturn(dummyResult);
|
||||||
|
|
||||||
|
Assertions.assertThrows(org.apache.fineract.commands.exception.RollbackTransactionNotApprovedException.class, () -> {
|
||||||
|
underTest.processCommand(handler, jsonCommand, commandSource, superUser, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
Mockito.verify(commandSource).markAsAwaitingApproval();
|
||||||
|
Mockito.verify(commandSource, Mockito.never()).markAsChecked(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testProcessCommand_GLAccount_SuperUser_EnforcesMakerChecker_Delete() {
|
||||||
|
NewCommandSourceHandler handler = Mockito.mock(NewCommandSourceHandler.class);
|
||||||
|
JsonCommand jsonCommand = Mockito.mock(JsonCommand.class);
|
||||||
|
CommandSource commandSource = Mockito.mock(CommandSource.class);
|
||||||
|
AppUser superUser = Mockito.mock(AppUser.class);
|
||||||
|
|
||||||
|
Mockito.when(commandSource.getPermissionCode()).thenReturn("DELETE_GLACCOUNT");
|
||||||
|
Mockito.when(commandSource.getEntityName()).thenReturn("GLACCOUNT");
|
||||||
|
Mockito.when(commandSource.getActionName()).thenReturn("DELETE");
|
||||||
|
Mockito.when(commandSource.getId()).thenReturn(102L);
|
||||||
|
Mockito.when(commandSource.getResourceId()).thenReturn(202L);
|
||||||
|
Mockito.when(configurationDomainService.isMakerCheckerEnabledForTask("DELETE_GLACCOUNT")).thenReturn(true);
|
||||||
|
Mockito.when(superUser.isCheckerSuperUser()).thenReturn(true);
|
||||||
|
|
||||||
|
org.apache.fineract.infrastructure.core.data.CommandProcessingResult dummyResult = new org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder()
|
||||||
|
.withCommandId(1L).build();
|
||||||
|
Mockito.when(handler.processCommand(jsonCommand)).thenReturn(dummyResult);
|
||||||
|
|
||||||
|
Assertions.assertThrows(org.apache.fineract.commands.exception.RollbackTransactionNotApprovedException.class, () -> {
|
||||||
|
underTest.processCommand(handler, jsonCommand, commandSource, superUser, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
Mockito.verify(commandSource).markAsAwaitingApproval();
|
||||||
|
Mockito.verify(commandSource, Mockito.never()).markAsChecked(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testProcessCommand_GLAccount_CheckerApproval_ExecutesSuccessfully() {
|
||||||
|
NewCommandSourceHandler handler = Mockito.mock(NewCommandSourceHandler.class);
|
||||||
|
JsonCommand jsonCommand = Mockito.mock(JsonCommand.class);
|
||||||
|
CommandSource commandSource = Mockito.mock(CommandSource.class);
|
||||||
|
AppUser checkerUser = Mockito.mock(AppUser.class);
|
||||||
|
|
||||||
|
Mockito.when(commandSource.getPermissionCode()).thenReturn("UPDATE_GLACCOUNT");
|
||||||
|
Mockito.when(commandSource.getEntityName()).thenReturn("GLACCOUNT");
|
||||||
|
Mockito.when(commandSource.getActionName()).thenReturn("UPDATE");
|
||||||
|
Mockito.when(configurationDomainService.isMakerCheckerEnabledForTask("UPDATE_GLACCOUNT")).thenReturn(true);
|
||||||
|
Mockito.when(checkerUser.isCheckerSuperUser()).thenReturn(false);
|
||||||
|
|
||||||
|
org.apache.fineract.infrastructure.core.data.CommandProcessingResult dummyResult = new org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder()
|
||||||
|
.withCommandId(1L).build();
|
||||||
|
Mockito.when(handler.processCommand(jsonCommand)).thenReturn(dummyResult);
|
||||||
|
|
||||||
|
org.apache.fineract.infrastructure.core.data.CommandProcessingResult result = underTest.processCommand(handler, jsonCommand,
|
||||||
|
commandSource, checkerUser, true);
|
||||||
|
|
||||||
|
Assertions.assertNotNull(result);
|
||||||
|
Mockito.verify(commandSource).markAsChecked(checkerUser);
|
||||||
|
Mockito.verify(commandSource, Mockito.never()).markAsAwaitingApproval();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testProcessCommand_GLAccount_NonSuperUser_EnforcesMakerChecker() {
|
||||||
|
NewCommandSourceHandler handler = Mockito.mock(NewCommandSourceHandler.class);
|
||||||
|
JsonCommand jsonCommand = Mockito.mock(JsonCommand.class);
|
||||||
|
CommandSource commandSource = Mockito.mock(CommandSource.class);
|
||||||
|
AppUser regularUser = Mockito.mock(AppUser.class);
|
||||||
|
|
||||||
|
Mockito.when(commandSource.getPermissionCode()).thenReturn("CREATE_GLACCOUNT");
|
||||||
|
Mockito.when(commandSource.getEntityName()).thenReturn("GLACCOUNT");
|
||||||
|
Mockito.when(commandSource.getActionName()).thenReturn("CREATE");
|
||||||
|
Mockito.when(commandSource.getId()).thenReturn(103L);
|
||||||
|
Mockito.when(commandSource.getResourceId()).thenReturn(203L);
|
||||||
|
Mockito.when(configurationDomainService.isMakerCheckerEnabledForTask("CREATE_GLACCOUNT")).thenReturn(true);
|
||||||
|
Mockito.when(regularUser.isCheckerSuperUser()).thenReturn(false);
|
||||||
|
|
||||||
|
org.apache.fineract.infrastructure.core.data.CommandProcessingResult dummyResult = new org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder()
|
||||||
|
.withCommandId(1L).build();
|
||||||
|
Mockito.when(handler.processCommand(jsonCommand)).thenReturn(dummyResult);
|
||||||
|
|
||||||
|
Assertions.assertThrows(org.apache.fineract.commands.exception.RollbackTransactionNotApprovedException.class, () -> {
|
||||||
|
underTest.processCommand(handler, jsonCommand, commandSource, regularUser, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
Mockito.verify(commandSource).markAsAwaitingApproval();
|
||||||
|
Mockito.verify(commandSource, Mockito.never()).markAsChecked(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testProcessCommand_CBSBaseInterestRate_SuperUser_EnforcesMakerChecker() {
|
||||||
|
NewCommandSourceHandler handler = Mockito.mock(NewCommandSourceHandler.class);
|
||||||
|
JsonCommand jsonCommand = Mockito.mock(JsonCommand.class);
|
||||||
|
CommandSource commandSource = Mockito.mock(CommandSource.class);
|
||||||
|
AppUser superUser = Mockito.mock(AppUser.class);
|
||||||
|
|
||||||
|
Mockito.when(commandSource.getPermissionCode()).thenReturn("CREATE_CBSBASEINTERESTRATE");
|
||||||
|
Mockito.when(commandSource.getEntityName()).thenReturn("CBSBASEINTERESTRATE");
|
||||||
|
Mockito.when(commandSource.getActionName()).thenReturn("CREATE");
|
||||||
|
Mockito.when(commandSource.getId()).thenReturn(104L);
|
||||||
|
Mockito.when(commandSource.getResourceId()).thenReturn(204L);
|
||||||
|
Mockito.when(configurationDomainService.isMakerCheckerEnabledForTask("CREATE_CBSBASEINTERESTRATE")).thenReturn(true);
|
||||||
|
Mockito.when(superUser.isCheckerSuperUser()).thenReturn(true);
|
||||||
|
|
||||||
|
org.apache.fineract.infrastructure.core.data.CommandProcessingResult dummyResult = new org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder()
|
||||||
|
.withCommandId(1L).build();
|
||||||
|
Mockito.when(handler.processCommand(jsonCommand)).thenReturn(dummyResult);
|
||||||
|
|
||||||
|
Assertions.assertThrows(org.apache.fineract.commands.exception.RollbackTransactionNotApprovedException.class, () -> {
|
||||||
|
underTest.processCommand(handler, jsonCommand, commandSource, superUser, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
Mockito.verify(commandSource).markAsAwaitingApproval();
|
||||||
|
Mockito.verify(commandSource, Mockito.never()).markAsChecked(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testProcessCommand_CBSBaseInterestRate_CheckerApproval_MarksAsChecked() {
|
||||||
|
NewCommandSourceHandler handler = Mockito.mock(NewCommandSourceHandler.class);
|
||||||
|
JsonCommand jsonCommand = Mockito.mock(JsonCommand.class);
|
||||||
|
CommandSource commandSource = Mockito.mock(CommandSource.class);
|
||||||
|
AppUser checkerUser = Mockito.mock(AppUser.class);
|
||||||
|
|
||||||
|
Mockito.when(commandSource.getPermissionCode()).thenReturn("CREATE_CBSBASEINTERESTRATE");
|
||||||
|
Mockito.when(configurationDomainService.isMakerCheckerEnabledForTask("CREATE_CBSBASEINTERESTRATE")).thenReturn(true);
|
||||||
|
|
||||||
|
org.apache.fineract.infrastructure.core.data.CommandProcessingResult dummyResult = new org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder()
|
||||||
|
.withCommandId(1L).build();
|
||||||
|
Mockito.when(handler.processCommand(jsonCommand)).thenReturn(dummyResult);
|
||||||
|
|
||||||
|
org.apache.fineract.infrastructure.core.data.CommandProcessingResult result = underTest.processCommand(handler, jsonCommand,
|
||||||
|
commandSource, checkerUser, true);
|
||||||
|
|
||||||
|
Assertions.assertNotNull(result);
|
||||||
|
Mockito.verify(commandSource).markAsChecked(checkerUser);
|
||||||
|
Mockito.verify(commandSource, Mockito.never()).markAsAwaitingApproval();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testProcessCommand_Charge_SuperUser_EnforcesMakerChecker() {
|
||||||
|
NewCommandSourceHandler handler = Mockito.mock(NewCommandSourceHandler.class);
|
||||||
|
JsonCommand jsonCommand = Mockito.mock(JsonCommand.class);
|
||||||
|
CommandSource commandSource = Mockito.mock(CommandSource.class);
|
||||||
|
AppUser superUser = Mockito.mock(AppUser.class);
|
||||||
|
|
||||||
|
Mockito.when(commandSource.getPermissionCode()).thenReturn("CREATE_CHARGE");
|
||||||
|
Mockito.when(commandSource.getEntityName()).thenReturn("CHARGE");
|
||||||
|
Mockito.when(commandSource.getActionName()).thenReturn("CREATE");
|
||||||
|
Mockito.when(commandSource.getId()).thenReturn(105L);
|
||||||
|
Mockito.when(commandSource.getResourceId()).thenReturn(205L);
|
||||||
|
Mockito.when(configurationDomainService.isMakerCheckerEnabledForTask("CREATE_CHARGE")).thenReturn(true);
|
||||||
|
Mockito.when(superUser.isCheckerSuperUser()).thenReturn(true);
|
||||||
|
|
||||||
|
org.apache.fineract.infrastructure.core.data.CommandProcessingResult dummyResult = new org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder()
|
||||||
|
.withCommandId(1L).build();
|
||||||
|
Mockito.when(handler.processCommand(jsonCommand)).thenReturn(dummyResult);
|
||||||
|
|
||||||
|
Assertions.assertThrows(org.apache.fineract.commands.exception.RollbackTransactionNotApprovedException.class, () -> {
|
||||||
|
underTest.processCommand(handler, jsonCommand, commandSource, superUser, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
Mockito.verify(commandSource).markAsAwaitingApproval();
|
||||||
|
Mockito.verify(commandSource, Mockito.never()).markAsChecked(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testProcessCommand_Charge_NonSuperUser_EnforcesMakerChecker() {
|
||||||
|
NewCommandSourceHandler handler = Mockito.mock(NewCommandSourceHandler.class);
|
||||||
|
JsonCommand jsonCommand = Mockito.mock(JsonCommand.class);
|
||||||
|
CommandSource commandSource = Mockito.mock(CommandSource.class);
|
||||||
|
AppUser regularUser = Mockito.mock(AppUser.class);
|
||||||
|
|
||||||
|
Mockito.when(commandSource.getPermissionCode()).thenReturn("CREATE_CHARGE");
|
||||||
|
Mockito.when(commandSource.getEntityName()).thenReturn("CHARGE");
|
||||||
|
Mockito.when(commandSource.getActionName()).thenReturn("CREATE");
|
||||||
|
Mockito.when(commandSource.getId()).thenReturn(106L);
|
||||||
|
Mockito.when(commandSource.getResourceId()).thenReturn(206L);
|
||||||
|
Mockito.when(configurationDomainService.isMakerCheckerEnabledForTask("CREATE_CHARGE")).thenReturn(true);
|
||||||
|
Mockito.when(regularUser.isCheckerSuperUser()).thenReturn(false);
|
||||||
|
|
||||||
|
org.apache.fineract.infrastructure.core.data.CommandProcessingResult dummyResult = new org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder()
|
||||||
|
.withCommandId(1L).build();
|
||||||
|
Mockito.when(handler.processCommand(jsonCommand)).thenReturn(dummyResult);
|
||||||
|
|
||||||
|
Assertions.assertThrows(org.apache.fineract.commands.exception.RollbackTransactionNotApprovedException.class, () -> {
|
||||||
|
underTest.processCommand(handler, jsonCommand, commandSource, regularUser, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
Mockito.verify(commandSource).markAsAwaitingApproval();
|
||||||
|
Mockito.verify(commandSource, Mockito.never()).markAsChecked(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testProcessCommand_LoanProduct_SuperUser_EnforcesMakerChecker() {
|
||||||
|
NewCommandSourceHandler handler = Mockito.mock(NewCommandSourceHandler.class);
|
||||||
|
JsonCommand jsonCommand = Mockito.mock(JsonCommand.class);
|
||||||
|
CommandSource commandSource = Mockito.mock(CommandSource.class);
|
||||||
|
AppUser superUser = Mockito.mock(AppUser.class);
|
||||||
|
|
||||||
|
Mockito.when(commandSource.getPermissionCode()).thenReturn("CREATE_LOANPRODUCT");
|
||||||
|
Mockito.when(commandSource.getEntityName()).thenReturn("LOANPRODUCT");
|
||||||
|
Mockito.when(commandSource.getActionName()).thenReturn("CREATE");
|
||||||
|
Mockito.when(commandSource.getId()).thenReturn(110L);
|
||||||
|
Mockito.when(commandSource.getResourceId()).thenReturn(210L);
|
||||||
|
Mockito.when(configurationDomainService.isMakerCheckerEnabledForTask("CREATE_LOANPRODUCT")).thenReturn(true);
|
||||||
|
Mockito.when(superUser.isCheckerSuperUser()).thenReturn(true);
|
||||||
|
|
||||||
|
org.apache.fineract.infrastructure.core.data.CommandProcessingResult dummyResult = new org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder()
|
||||||
|
.withCommandId(1L).build();
|
||||||
|
Mockito.when(handler.processCommand(jsonCommand)).thenReturn(dummyResult);
|
||||||
|
|
||||||
|
Assertions.assertThrows(org.apache.fineract.commands.exception.RollbackTransactionNotApprovedException.class, () -> {
|
||||||
|
underTest.processCommand(handler, jsonCommand, commandSource, superUser, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
Mockito.verify(commandSource).markAsAwaitingApproval();
|
||||||
|
Mockito.verify(commandSource, Mockito.never()).markAsChecked(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testProcessCommand_SavingsProduct_SuperUser_EnforcesMakerChecker() {
|
||||||
|
NewCommandSourceHandler handler = Mockito.mock(NewCommandSourceHandler.class);
|
||||||
|
JsonCommand jsonCommand = Mockito.mock(JsonCommand.class);
|
||||||
|
CommandSource commandSource = Mockito.mock(CommandSource.class);
|
||||||
|
AppUser superUser = Mockito.mock(AppUser.class);
|
||||||
|
|
||||||
|
Mockito.when(commandSource.getPermissionCode()).thenReturn("CREATE_SAVINGSPRODUCT");
|
||||||
|
Mockito.when(commandSource.getEntityName()).thenReturn("SAVINGSPRODUCT");
|
||||||
|
Mockito.when(commandSource.getActionName()).thenReturn("CREATE");
|
||||||
|
Mockito.when(commandSource.getId()).thenReturn(111L);
|
||||||
|
Mockito.when(commandSource.getResourceId()).thenReturn(211L);
|
||||||
|
Mockito.when(configurationDomainService.isMakerCheckerEnabledForTask("CREATE_SAVINGSPRODUCT")).thenReturn(true);
|
||||||
|
Mockito.when(superUser.isCheckerSuperUser()).thenReturn(true);
|
||||||
|
|
||||||
|
org.apache.fineract.infrastructure.core.data.CommandProcessingResult dummyResult = new org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder()
|
||||||
|
.withCommandId(1L).build();
|
||||||
|
Mockito.when(handler.processCommand(jsonCommand)).thenReturn(dummyResult);
|
||||||
|
|
||||||
|
Assertions.assertThrows(org.apache.fineract.commands.exception.RollbackTransactionNotApprovedException.class, () -> {
|
||||||
|
underTest.processCommand(handler, jsonCommand, commandSource, superUser, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
Mockito.verify(commandSource).markAsAwaitingApproval();
|
||||||
|
Mockito.verify(commandSource, Mockito.never()).markAsChecked(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testProcessCommand_CurrentProduct_SuperUser_EnforcesMakerChecker() {
|
||||||
|
NewCommandSourceHandler handler = Mockito.mock(NewCommandSourceHandler.class);
|
||||||
|
JsonCommand jsonCommand = Mockito.mock(JsonCommand.class);
|
||||||
|
CommandSource commandSource = Mockito.mock(CommandSource.class);
|
||||||
|
AppUser superUser = Mockito.mock(AppUser.class);
|
||||||
|
|
||||||
|
Mockito.when(commandSource.getPermissionCode()).thenReturn("CREATE_CURRENTPRODUCT");
|
||||||
|
Mockito.when(commandSource.getEntityName()).thenReturn("CURRENTPRODUCT");
|
||||||
|
Mockito.when(commandSource.getActionName()).thenReturn("CREATE");
|
||||||
|
Mockito.when(commandSource.getId()).thenReturn(112L);
|
||||||
|
Mockito.when(commandSource.getResourceId()).thenReturn(212L);
|
||||||
|
Mockito.when(configurationDomainService.isMakerCheckerEnabledForTask("CREATE_CURRENTPRODUCT")).thenReturn(true);
|
||||||
|
Mockito.when(superUser.isCheckerSuperUser()).thenReturn(true);
|
||||||
|
|
||||||
|
org.apache.fineract.infrastructure.core.data.CommandProcessingResult dummyResult = new org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder()
|
||||||
|
.withCommandId(1L).build();
|
||||||
|
Mockito.when(handler.processCommand(jsonCommand)).thenReturn(dummyResult);
|
||||||
|
|
||||||
|
Assertions.assertThrows(org.apache.fineract.commands.exception.RollbackTransactionNotApprovedException.class, () -> {
|
||||||
|
underTest.processCommand(handler, jsonCommand, commandSource, superUser, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
Mockito.verify(commandSource).markAsAwaitingApproval();
|
||||||
|
Mockito.verify(commandSource, Mockito.never()).markAsChecked(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testProcessCommand_ShareProduct_SuperUser_EnforcesMakerChecker() {
|
||||||
|
NewCommandSourceHandler handler = Mockito.mock(NewCommandSourceHandler.class);
|
||||||
|
JsonCommand jsonCommand = Mockito.mock(JsonCommand.class);
|
||||||
|
CommandSource commandSource = Mockito.mock(CommandSource.class);
|
||||||
|
AppUser superUser = Mockito.mock(AppUser.class);
|
||||||
|
|
||||||
|
Mockito.when(commandSource.getPermissionCode()).thenReturn("CREATE_SHAREPRODUCT");
|
||||||
|
Mockito.when(commandSource.getEntityName()).thenReturn("SHAREPRODUCT");
|
||||||
|
Mockito.when(commandSource.getActionName()).thenReturn("CREATE");
|
||||||
|
Mockito.when(commandSource.getId()).thenReturn(113L);
|
||||||
|
Mockito.when(commandSource.getResourceId()).thenReturn(213L);
|
||||||
|
Mockito.when(configurationDomainService.isMakerCheckerEnabledForTask("CREATE_SHAREPRODUCT")).thenReturn(true);
|
||||||
|
Mockito.when(superUser.isCheckerSuperUser()).thenReturn(true);
|
||||||
|
|
||||||
|
org.apache.fineract.infrastructure.core.data.CommandProcessingResult dummyResult = new org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder()
|
||||||
|
.withCommandId(1L).build();
|
||||||
|
Mockito.when(handler.processCommand(jsonCommand)).thenReturn(dummyResult);
|
||||||
|
|
||||||
|
Assertions.assertThrows(org.apache.fineract.commands.exception.RollbackTransactionNotApprovedException.class, () -> {
|
||||||
|
underTest.processCommand(handler, jsonCommand, commandSource, superUser, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
Mockito.verify(commandSource).markAsAwaitingApproval();
|
||||||
|
Mockito.verify(commandSource, Mockito.never()).markAsChecked(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testProcessCommand_FixedDepositProduct_SuperUser_EnforcesMakerChecker() {
|
||||||
|
NewCommandSourceHandler handler = Mockito.mock(NewCommandSourceHandler.class);
|
||||||
|
JsonCommand jsonCommand = Mockito.mock(JsonCommand.class);
|
||||||
|
CommandSource commandSource = Mockito.mock(CommandSource.class);
|
||||||
|
AppUser superUser = Mockito.mock(AppUser.class);
|
||||||
|
|
||||||
|
Mockito.when(commandSource.getPermissionCode()).thenReturn("CREATE_FIXEDDEPOSITPRODUCT");
|
||||||
|
Mockito.when(commandSource.getEntityName()).thenReturn("FIXEDDEPOSITPRODUCT");
|
||||||
|
Mockito.when(commandSource.getActionName()).thenReturn("CREATE");
|
||||||
|
Mockito.when(commandSource.getId()).thenReturn(114L);
|
||||||
|
Mockito.when(commandSource.getResourceId()).thenReturn(214L);
|
||||||
|
Mockito.when(configurationDomainService.isMakerCheckerEnabledForTask("CREATE_FIXEDDEPOSITPRODUCT")).thenReturn(true);
|
||||||
|
Mockito.when(superUser.isCheckerSuperUser()).thenReturn(true);
|
||||||
|
|
||||||
|
org.apache.fineract.infrastructure.core.data.CommandProcessingResult dummyResult = new org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder()
|
||||||
|
.withCommandId(1L).build();
|
||||||
|
Mockito.when(handler.processCommand(jsonCommand)).thenReturn(dummyResult);
|
||||||
|
|
||||||
|
Assertions.assertThrows(org.apache.fineract.commands.exception.RollbackTransactionNotApprovedException.class, () -> {
|
||||||
|
underTest.processCommand(handler, jsonCommand, commandSource, superUser, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
Mockito.verify(commandSource).markAsAwaitingApproval();
|
||||||
|
Mockito.verify(commandSource, Mockito.never()).markAsChecked(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testProcessCommand_FixedDepositProduct_CheckerApproval_MarksAsChecked() {
|
||||||
|
NewCommandSourceHandler handler = Mockito.mock(NewCommandSourceHandler.class);
|
||||||
|
JsonCommand jsonCommand = Mockito.mock(JsonCommand.class);
|
||||||
|
CommandSource commandSource = Mockito.mock(CommandSource.class);
|
||||||
|
AppUser checkerUser = Mockito.mock(AppUser.class);
|
||||||
|
|
||||||
|
Mockito.when(commandSource.getPermissionCode()).thenReturn("CREATE_FIXEDDEPOSITPRODUCT");
|
||||||
|
Mockito.when(configurationDomainService.isMakerCheckerEnabledForTask("CREATE_FIXEDDEPOSITPRODUCT")).thenReturn(true);
|
||||||
|
|
||||||
|
org.apache.fineract.infrastructure.core.data.CommandProcessingResult dummyResult = new org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder()
|
||||||
|
.withCommandId(1L).build();
|
||||||
|
Mockito.when(handler.processCommand(jsonCommand)).thenReturn(dummyResult);
|
||||||
|
|
||||||
|
org.apache.fineract.infrastructure.core.data.CommandProcessingResult result = underTest.processCommand(handler, jsonCommand,
|
||||||
|
commandSource, checkerUser, true);
|
||||||
|
|
||||||
|
Assertions.assertNotNull(result);
|
||||||
|
Mockito.verify(commandSource).markAsChecked(checkerUser);
|
||||||
|
Mockito.verify(commandSource, Mockito.never()).markAsAwaitingApproval();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testProcessCommand_Products_CheckerApproval_MarksAsChecked() {
|
||||||
|
NewCommandSourceHandler handler = Mockito.mock(NewCommandSourceHandler.class);
|
||||||
|
JsonCommand jsonCommand = Mockito.mock(JsonCommand.class);
|
||||||
|
CommandSource commandSource = Mockito.mock(CommandSource.class);
|
||||||
|
AppUser checkerUser = Mockito.mock(AppUser.class);
|
||||||
|
|
||||||
|
Mockito.when(commandSource.getPermissionCode()).thenReturn("CREATE_LOANPRODUCT");
|
||||||
|
Mockito.when(configurationDomainService.isMakerCheckerEnabledForTask("CREATE_LOANPRODUCT")).thenReturn(true);
|
||||||
|
|
||||||
|
org.apache.fineract.infrastructure.core.data.CommandProcessingResult dummyResult = new org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder()
|
||||||
|
.withCommandId(1L).build();
|
||||||
|
Mockito.when(handler.processCommand(jsonCommand)).thenReturn(dummyResult);
|
||||||
|
|
||||||
|
org.apache.fineract.infrastructure.core.data.CommandProcessingResult result = underTest.processCommand(handler, jsonCommand,
|
||||||
|
commandSource, checkerUser, true);
|
||||||
|
|
||||||
|
Assertions.assertNotNull(result);
|
||||||
|
Mockito.verify(commandSource).markAsChecked(checkerUser);
|
||||||
|
Mockito.verify(commandSource, Mockito.never()).markAsAwaitingApproval();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testProcessCommand_UserOperations_SuperUserBypassBlocked_ThrowsRollbackException() {
|
||||||
|
String[] actions = { "CREATE", "UPDATE", "DELETE" };
|
||||||
|
String[] permissions = { "CREATE_USER", "UPDATE_USER", "DELETE_USER" };
|
||||||
|
|
||||||
|
for (int i = 0; i < actions.length; i++) {
|
||||||
|
NewCommandSourceHandler handler = Mockito.mock(NewCommandSourceHandler.class);
|
||||||
|
JsonCommand jsonCommand = Mockito.mock(JsonCommand.class);
|
||||||
|
CommandSource commandSource = Mockito.mock(CommandSource.class);
|
||||||
|
AppUser superUser = Mockito.mock(AppUser.class);
|
||||||
|
|
||||||
|
Mockito.when(commandSource.getEntityName()).thenReturn("USER");
|
||||||
|
Mockito.when(commandSource.getActionName()).thenReturn(actions[i]);
|
||||||
|
Mockito.when(commandSource.getPermissionCode()).thenReturn(permissions[i]);
|
||||||
|
Mockito.when(commandSource.isSanitized()).thenReturn(false);
|
||||||
|
Mockito.when(configurationDomainService.isMakerCheckerEnabledForTask(permissions[i])).thenReturn(true);
|
||||||
|
Mockito.when(superUser.isCheckerSuperUser()).thenReturn(true);
|
||||||
|
|
||||||
|
org.apache.fineract.infrastructure.core.data.CommandProcessingResult dummyResult = new org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder()
|
||||||
|
.withCommandId(1L).build();
|
||||||
|
Mockito.when(handler.processCommand(jsonCommand)).thenReturn(dummyResult);
|
||||||
|
|
||||||
|
Assertions.assertThrows(org.apache.fineract.commands.exception.RollbackTransactionNotApprovedException.class, () -> {
|
||||||
|
underTest.processCommand(handler, jsonCommand, commandSource, superUser, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
Mockito.verify(commandSource).markAsAwaitingApproval();
|
||||||
|
Mockito.verify(commandSource, Mockito.never()).markAsChecked(any());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testProcessCommand_RoleOperations_SuperUserBypassBlocked_ThrowsRollbackException() {
|
||||||
|
String[] actions = { "CREATE", "UPDATE", "PERMISSIONS", "DELETE" };
|
||||||
|
String[] permissions = { "CREATE_ROLE", "UPDATE_ROLE", "PERMISSIONS_ROLE", "DELETE_ROLE" };
|
||||||
|
|
||||||
|
for (int i = 0; i < actions.length; i++) {
|
||||||
|
NewCommandSourceHandler handler = Mockito.mock(NewCommandSourceHandler.class);
|
||||||
|
JsonCommand jsonCommand = Mockito.mock(JsonCommand.class);
|
||||||
|
CommandSource commandSource = Mockito.mock(CommandSource.class);
|
||||||
|
AppUser superUser = Mockito.mock(AppUser.class);
|
||||||
|
|
||||||
|
Mockito.when(commandSource.getEntityName()).thenReturn("ROLE");
|
||||||
|
Mockito.when(commandSource.getActionName()).thenReturn(actions[i]);
|
||||||
|
Mockito.when(commandSource.getPermissionCode()).thenReturn(permissions[i]);
|
||||||
|
Mockito.when(commandSource.isSanitized()).thenReturn(false);
|
||||||
|
Mockito.when(configurationDomainService.isMakerCheckerEnabledForTask(permissions[i])).thenReturn(true);
|
||||||
|
Mockito.when(superUser.isCheckerSuperUser()).thenReturn(true);
|
||||||
|
|
||||||
|
org.apache.fineract.infrastructure.core.data.CommandProcessingResult dummyResult = new org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder()
|
||||||
|
.withCommandId(1L).build();
|
||||||
|
Mockito.when(handler.processCommand(jsonCommand)).thenReturn(dummyResult);
|
||||||
|
|
||||||
|
Assertions.assertThrows(org.apache.fineract.commands.exception.RollbackTransactionNotApprovedException.class, () -> {
|
||||||
|
underTest.processCommand(handler, jsonCommand, commandSource, superUser, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
Mockito.verify(commandSource).markAsAwaitingApproval();
|
||||||
|
Mockito.verify(commandSource, Mockito.never()).markAsChecked(any());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testProcessCommand_UserAndRole_CheckerApproval_MarksAsChecked() {
|
||||||
|
NewCommandSourceHandler handler = Mockito.mock(NewCommandSourceHandler.class);
|
||||||
|
JsonCommand jsonCommand = Mockito.mock(JsonCommand.class);
|
||||||
|
CommandSource commandSource = Mockito.mock(CommandSource.class);
|
||||||
|
AppUser checkerUser = Mockito.mock(AppUser.class);
|
||||||
|
|
||||||
|
Mockito.when(commandSource.getPermissionCode()).thenReturn("CREATE_USER");
|
||||||
|
Mockito.when(configurationDomainService.isMakerCheckerEnabledForTask("CREATE_USER")).thenReturn(true);
|
||||||
|
|
||||||
|
org.apache.fineract.infrastructure.core.data.CommandProcessingResult dummyResult = new org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder()
|
||||||
|
.withCommandId(1L).build();
|
||||||
|
Mockito.when(handler.processCommand(jsonCommand)).thenReturn(dummyResult);
|
||||||
|
|
||||||
|
org.apache.fineract.infrastructure.core.data.CommandProcessingResult result = underTest.processCommand(handler, jsonCommand,
|
||||||
|
commandSource, checkerUser, true);
|
||||||
|
|
||||||
|
Assertions.assertNotNull(result);
|
||||||
|
Mockito.verify(commandSource).markAsChecked(checkerUser);
|
||||||
|
Mockito.verify(commandSource, Mockito.never()).markAsAwaitingApproval();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+60
@@ -0,0 +1,60 @@
|
|||||||
|
/**
|
||||||
|
* 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.charge.handler;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
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.portfolio.charge.service.ChargeWritePlatformService;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class CreateChargeDefinitionCommandHandlerTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ChargeWritePlatformService writePlatformService;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private JsonCommand command;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private CreateChargeDefinitionCommandHandler underTest;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldDelegateToWritePlatformServiceAndReturnResult() {
|
||||||
|
CommandProcessingResult expectedResult = new CommandProcessingResultBuilder()
|
||||||
|
.withCommandId(101L)
|
||||||
|
.withEntityId(201L)
|
||||||
|
.build();
|
||||||
|
when(writePlatformService.createCharge(command)).thenReturn(expectedResult);
|
||||||
|
|
||||||
|
CommandProcessingResult actualResult = underTest.processCommand(command);
|
||||||
|
|
||||||
|
verify(writePlatformService).createCharge(command);
|
||||||
|
assertEquals(expectedResult, actualResult);
|
||||||
|
}
|
||||||
|
}
|
||||||
+60
@@ -0,0 +1,60 @@
|
|||||||
|
/**
|
||||||
|
* 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.loanproduct.handler;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
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.portfolio.loanproduct.service.LoanProductWritePlatformService;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class CreateLoanProductCommandHandlerTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private LoanProductWritePlatformService writePlatformService;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private JsonCommand command;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private CreateLoanProductCommandHandler underTest;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldDelegateToWritePlatformServiceAndReturnResult() {
|
||||||
|
CommandProcessingResult expectedResult = new CommandProcessingResultBuilder()
|
||||||
|
.withCommandId(101L)
|
||||||
|
.withEntityId(201L)
|
||||||
|
.build();
|
||||||
|
when(writePlatformService.createLoanProduct(command)).thenReturn(expectedResult);
|
||||||
|
|
||||||
|
CommandProcessingResult actualResult = underTest.processCommand(command);
|
||||||
|
|
||||||
|
verify(writePlatformService).createLoanProduct(command);
|
||||||
|
assertEquals(expectedResult, actualResult);
|
||||||
|
}
|
||||||
|
}
|
||||||
+60
@@ -0,0 +1,60 @@
|
|||||||
|
/**
|
||||||
|
* 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.handler;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
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.portfolio.savings.service.FdBaseInterestRateWritePlatformService;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class CreateCbsBaseInterestRateCommandHandlerTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private FdBaseInterestRateWritePlatformService writePlatformService;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private JsonCommand command;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private CreateCbsBaseInterestRateCommandHandler underTest;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldDelegateToWritePlatformServiceAndReturnResult() {
|
||||||
|
CommandProcessingResult expectedResult = new CommandProcessingResultBuilder()
|
||||||
|
.withCommandId(100L)
|
||||||
|
.withEntityId(200L)
|
||||||
|
.build();
|
||||||
|
when(writePlatformService.saveInterestRates(command)).thenReturn(expectedResult);
|
||||||
|
|
||||||
|
CommandProcessingResult actualResult = underTest.processCommand(command);
|
||||||
|
|
||||||
|
verify(writePlatformService).saveInterestRates(command);
|
||||||
|
assertEquals(expectedResult, actualResult);
|
||||||
|
}
|
||||||
|
}
|
||||||
+60
@@ -0,0 +1,60 @@
|
|||||||
|
/**
|
||||||
|
* 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.handler;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
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.portfolio.savings.service.SavingsProductWritePlatformService;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class CreateSavingsProductCommandHandlerTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private SavingsProductWritePlatformService writePlatformService;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private JsonCommand command;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private CreateSavingsProductCommandHandler underTest;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldDelegateToWritePlatformServiceAndReturnResult() {
|
||||||
|
CommandProcessingResult expectedResult = new CommandProcessingResultBuilder()
|
||||||
|
.withCommandId(102L)
|
||||||
|
.withEntityId(202L)
|
||||||
|
.build();
|
||||||
|
when(writePlatformService.create(command)).thenReturn(expectedResult);
|
||||||
|
|
||||||
|
CommandProcessingResult actualResult = underTest.processCommand(command);
|
||||||
|
|
||||||
|
verify(writePlatformService).create(command);
|
||||||
|
assertEquals(expectedResult, actualResult);
|
||||||
|
}
|
||||||
|
}
|
||||||
+60
@@ -0,0 +1,60 @@
|
|||||||
|
/**
|
||||||
|
* 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.handler.current;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
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.portfolio.currentproduct.service.CurrentProductWritePlatformService;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class CreateCurrentProductCommandHandlerTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private CurrentProductWritePlatformService writePlatformService;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private JsonCommand command;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private CreateCurrentProductCommandHandler underTest;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldDelegateToWritePlatformServiceAndReturnResult() {
|
||||||
|
CommandProcessingResult expectedResult = new CommandProcessingResultBuilder()
|
||||||
|
.withCommandId(103L)
|
||||||
|
.withEntityId(203L)
|
||||||
|
.build();
|
||||||
|
when(writePlatformService.create(command)).thenReturn(expectedResult);
|
||||||
|
|
||||||
|
CommandProcessingResult actualResult = underTest.processCommand(command);
|
||||||
|
|
||||||
|
verify(writePlatformService).create(command);
|
||||||
|
assertEquals(expectedResult, actualResult);
|
||||||
|
}
|
||||||
|
}
|
||||||
+265
@@ -0,0 +1,265 @@
|
|||||||
|
/**
|
||||||
|
* 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.service;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant;
|
||||||
|
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
|
||||||
|
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
|
||||||
|
import org.apache.fineract.portfolio.savings.api.FdInterestRateSaveRequest;
|
||||||
|
import org.apache.fineract.portfolio.savings.domain.FdBaseInterestRateEffective;
|
||||||
|
import org.apache.fineract.portfolio.savings.domain.FdBaseInterestRateEffectiveRepository;
|
||||||
|
import org.apache.fineract.portfolio.savings.domain.FdBaseInterestRateSlabHistoryRepository;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class FdBaseInterestRateReadPlatformServiceImplTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private FdBaseInterestRateEffectiveRepository effectiveRepository;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private FdBaseInterestRateSlabHistoryRepository slabRepository;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private FdBaseInterestRateReadPlatformServiceImpl service;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
ThreadLocalContextUtil.setTenant(new FineractPlatformTenant(1L, "default", "Default", "Asia/Kolkata", null));
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void tearDown() {
|
||||||
|
ThreadLocalContextUtil.setTenant(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private FdInterestRateSaveRequest.SlabRequest createSlab(int srNo, String desc, int periodType, int from, Integer to, double rate, double seniorRate) {
|
||||||
|
FdInterestRateSaveRequest.SlabRequest s = new FdInterestRateSaveRequest.SlabRequest();
|
||||||
|
s.setSrNo(srNo);
|
||||||
|
s.setDescription(desc);
|
||||||
|
s.setPeriodTypeEnum(periodType);
|
||||||
|
s.setFromPeriod(from);
|
||||||
|
s.setToPeriod(to);
|
||||||
|
s.setAnnualInterestRate(BigDecimal.valueOf(rate));
|
||||||
|
s.setSeniorCitizenRate(BigDecimal.valueOf(seniorRate));
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testValidSingleDepositSlabs() {
|
||||||
|
FdInterestRateSaveRequest request = new FdInterestRateSaveRequest();
|
||||||
|
request.setFdType("SINGLE");
|
||||||
|
request.setVersionLabel("v1.0");
|
||||||
|
|
||||||
|
List<FdInterestRateSaveRequest.SlabRequest> slabs = List.of(
|
||||||
|
createSlab(1, "1 to 29 Days", 0, 1, 29, 4.5, 5.0),
|
||||||
|
createSlab(2, "30 to 89 Days", 0, 30, 89, 5.0, 5.5),
|
||||||
|
createSlab(3, "90 to 179 Days", 0, 90, 179, 5.5, 6.0),
|
||||||
|
createSlab(4, "180+ Days", 0, 180, null, 6.0, 6.5)
|
||||||
|
);
|
||||||
|
request.setSlabs(slabs);
|
||||||
|
|
||||||
|
when(effectiveRepository.findByFdTypeAndIsActiveTrueOrderByEffectiveFromDateDesc("SINGLE"))
|
||||||
|
.thenReturn(Collections.emptyList());
|
||||||
|
when(effectiveRepository.saveAndFlush(any())).thenAnswer(invocation -> invocation.getArgument(0));
|
||||||
|
|
||||||
|
assertDoesNotThrow(() -> service.saveInterestRates(request, 1L));
|
||||||
|
verify(effectiveRepository).saveAndFlush(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testValidDoubleDepositSlabs() {
|
||||||
|
FdInterestRateSaveRequest request = new FdInterestRateSaveRequest();
|
||||||
|
request.setFdType("DOUBLE");
|
||||||
|
request.setVersionLabel("v1.0");
|
||||||
|
|
||||||
|
List<FdInterestRateSaveRequest.SlabRequest> slabs = List.of(
|
||||||
|
createSlab(1, "1 to 2 Years", 3, 1, 2, 6.5, 7.0),
|
||||||
|
createSlab(2, "3 to 5 Years", 3, 3, 5, 6.75, 7.25),
|
||||||
|
createSlab(3, "6 to 10 Years", 3, 6, 10, 7.0, 7.5),
|
||||||
|
createSlab(4, "11+ Years", 3, 11, null, 7.25, 7.75)
|
||||||
|
);
|
||||||
|
request.setSlabs(slabs);
|
||||||
|
|
||||||
|
when(effectiveRepository.findByFdTypeAndIsActiveTrueOrderByEffectiveFromDateDesc("DOUBLE"))
|
||||||
|
.thenReturn(Collections.emptyList());
|
||||||
|
when(effectiveRepository.saveAndFlush(any())).thenAnswer(invocation -> invocation.getArgument(0));
|
||||||
|
|
||||||
|
assertDoesNotThrow(() -> service.saveInterestRates(request, 1L));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testInvalidFirstSlabDoesNotStartAtOne() {
|
||||||
|
FdInterestRateSaveRequest request = new FdInterestRateSaveRequest();
|
||||||
|
request.setFdType("SINGLE");
|
||||||
|
List<FdInterestRateSaveRequest.SlabRequest> slabs = List.of(
|
||||||
|
createSlab(1, "2 to 29 Days", 0, 2, 29, 4.5, 5.0),
|
||||||
|
createSlab(2, "30+ Days", 0, 30, null, 5.0, 5.5)
|
||||||
|
);
|
||||||
|
request.setSlabs(slabs);
|
||||||
|
|
||||||
|
PlatformApiDataValidationException ex = assertThrows(PlatformApiDataValidationException.class,
|
||||||
|
() -> service.saveInterestRates(request, 1L));
|
||||||
|
assertEquals(1, ex.getErrors().size());
|
||||||
|
assertEquals("validation.msg.FD_BASE_INTEREST_RATE_VERSION.slabs[1].fromPeriod.first.slab.must.start.at.one",
|
||||||
|
ex.getErrors().get(0).getUserMessageGlobalisationCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testInvalidOverlapRanges() {
|
||||||
|
FdInterestRateSaveRequest request = new FdInterestRateSaveRequest();
|
||||||
|
request.setFdType("SINGLE");
|
||||||
|
List<FdInterestRateSaveRequest.SlabRequest> slabs = List.of(
|
||||||
|
createSlab(1, "1 to 29 Days", 0, 1, 29, 4.5, 5.0),
|
||||||
|
createSlab(2, "20 to 50 Days", 0, 20, 50, 5.0, 5.5)
|
||||||
|
);
|
||||||
|
request.setSlabs(slabs);
|
||||||
|
|
||||||
|
PlatformApiDataValidationException ex = assertThrows(PlatformApiDataValidationException.class,
|
||||||
|
() -> service.saveInterestRates(request, 1L));
|
||||||
|
assertEquals("validation.msg.FD_BASE_INTEREST_RATE_VERSION.slabs[2].fromPeriod.must.not.overlap.previous.range",
|
||||||
|
ex.getErrors().get(0).getUserMessageGlobalisationCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testInvalidGapBetweenRanges() {
|
||||||
|
FdInterestRateSaveRequest request = new FdInterestRateSaveRequest();
|
||||||
|
request.setFdType("SINGLE");
|
||||||
|
List<FdInterestRateSaveRequest.SlabRequest> slabs = List.of(
|
||||||
|
createSlab(1, "1 to 29 Days", 0, 1, 29, 4.5, 5.0),
|
||||||
|
createSlab(2, "35 to 89 Days", 0, 35, 89, 5.0, 5.5)
|
||||||
|
);
|
||||||
|
request.setSlabs(slabs);
|
||||||
|
|
||||||
|
PlatformApiDataValidationException ex = assertThrows(PlatformApiDataValidationException.class,
|
||||||
|
() -> service.saveInterestRates(request, 1L));
|
||||||
|
assertEquals("validation.msg.FD_BASE_INTEREST_RATE_VERSION.slabs[2].fromPeriod.must.be.continuous.with.previous.range",
|
||||||
|
ex.getErrors().get(0).getUserMessageGlobalisationCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testInvalidDuplicateRanges() {
|
||||||
|
FdInterestRateSaveRequest request = new FdInterestRateSaveRequest();
|
||||||
|
request.setFdType("SINGLE");
|
||||||
|
List<FdInterestRateSaveRequest.SlabRequest> slabs = List.of(
|
||||||
|
createSlab(1, "1 to 29 Days", 0, 1, 29, 4.5, 5.0),
|
||||||
|
createSlab(2, "1 to 29 Days duplicate", 0, 1, 29, 5.0, 5.5)
|
||||||
|
);
|
||||||
|
request.setSlabs(slabs);
|
||||||
|
|
||||||
|
PlatformApiDataValidationException ex = assertThrows(PlatformApiDataValidationException.class,
|
||||||
|
() -> service.saveInterestRates(request, 1L));
|
||||||
|
assertEquals("validation.msg.FD_BASE_INTEREST_RATE_VERSION.slabs[2].fromPeriod.must.not.overlap.previous.range",
|
||||||
|
ex.getErrors().get(0).getUserMessageGlobalisationCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testInvalidFromGreaterThanTo() {
|
||||||
|
FdInterestRateSaveRequest request = new FdInterestRateSaveRequest();
|
||||||
|
request.setFdType("SINGLE");
|
||||||
|
List<FdInterestRateSaveRequest.SlabRequest> slabs = List.of(
|
||||||
|
createSlab(1, "1 to 20 Days", 0, 1, 20, 4.5, 5.0),
|
||||||
|
createSlab(2, "21 to 10 Days", 0, 21, 10, 5.0, 5.5)
|
||||||
|
);
|
||||||
|
request.setSlabs(slabs);
|
||||||
|
|
||||||
|
PlatformApiDataValidationException ex = assertThrows(PlatformApiDataValidationException.class,
|
||||||
|
() -> service.saveInterestRates(request, 1L));
|
||||||
|
assertEquals("validation.msg.FD_BASE_INTEREST_RATE_VERSION.slabs[2].toPeriod.cannot.be.less.than.fromPeriod",
|
||||||
|
ex.getErrors().get(0).getUserMessageGlobalisationCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testInvalidBlankToPeriodOnIntermediateSlab() {
|
||||||
|
FdInterestRateSaveRequest request = new FdInterestRateSaveRequest();
|
||||||
|
request.setFdType("SINGLE");
|
||||||
|
List<FdInterestRateSaveRequest.SlabRequest> slabs = List.of(
|
||||||
|
createSlab(1, "1 to null Days", 0, 1, null, 4.5, 5.0),
|
||||||
|
createSlab(2, "30+ Days", 0, 30, null, 5.0, 5.5)
|
||||||
|
);
|
||||||
|
request.setSlabs(slabs);
|
||||||
|
|
||||||
|
PlatformApiDataValidationException ex = assertThrows(PlatformApiDataValidationException.class,
|
||||||
|
() -> service.saveInterestRates(request, 1L));
|
||||||
|
assertEquals("validation.msg.FD_BASE_INTEREST_RATE_VERSION.slabs[1].toPeriod.cannot.be.blank.for.intermediate.slab",
|
||||||
|
ex.getErrors().get(0).getUserMessageGlobalisationCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testInvalidMixedPeriodTypes() {
|
||||||
|
FdInterestRateSaveRequest request = new FdInterestRateSaveRequest();
|
||||||
|
request.setFdType("SINGLE");
|
||||||
|
List<FdInterestRateSaveRequest.SlabRequest> slabs = List.of(
|
||||||
|
createSlab(1, "1 to 29 Days", 0, 1, 29, 4.5, 5.0),
|
||||||
|
createSlab(2, "30 to 89 Months", 2, 30, 89, 5.0, 5.5)
|
||||||
|
);
|
||||||
|
request.setSlabs(slabs);
|
||||||
|
|
||||||
|
PlatformApiDataValidationException ex = assertThrows(PlatformApiDataValidationException.class,
|
||||||
|
() -> service.saveInterestRates(request, 1L));
|
||||||
|
assertEquals("validation.msg.FD_BASE_INTEREST_RATE_VERSION.slabs[2].periodTypeEnum.must.be.consistent.across.all.slabs",
|
||||||
|
ex.getErrors().get(0).getUserMessageGlobalisationCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testInvalidNegativeInterestRate() {
|
||||||
|
FdInterestRateSaveRequest request = new FdInterestRateSaveRequest();
|
||||||
|
request.setFdType("SINGLE");
|
||||||
|
List<FdInterestRateSaveRequest.SlabRequest> slabs = List.of(
|
||||||
|
createSlab(1, "1 to 29 Days", 0, 1, 29, -1.0, 5.0)
|
||||||
|
);
|
||||||
|
request.setSlabs(slabs);
|
||||||
|
|
||||||
|
PlatformApiDataValidationException ex = assertThrows(PlatformApiDataValidationException.class,
|
||||||
|
() -> service.saveInterestRates(request, 1L));
|
||||||
|
assertEquals("validation.msg.FD_BASE_INTEREST_RATE_VERSION.slabs[1].annualInterestRate.must.be.non.negative",
|
||||||
|
ex.getErrors().get(0).getUserMessageGlobalisationCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testInvalidNegativeSeniorRate() {
|
||||||
|
FdInterestRateSaveRequest request = new FdInterestRateSaveRequest();
|
||||||
|
request.setFdType("SINGLE");
|
||||||
|
List<FdInterestRateSaveRequest.SlabRequest> slabs = List.of(
|
||||||
|
createSlab(1, "1 to 29 Days", 0, 1, 29, 4.5, -0.5)
|
||||||
|
);
|
||||||
|
request.setSlabs(slabs);
|
||||||
|
|
||||||
|
PlatformApiDataValidationException ex = assertThrows(PlatformApiDataValidationException.class,
|
||||||
|
() -> service.saveInterestRates(request, 1L));
|
||||||
|
assertEquals("validation.msg.FD_BASE_INTEREST_RATE_VERSION.slabs[1].seniorCitizenRate.must.be.non.negative",
|
||||||
|
ex.getErrors().get(0).getUserMessageGlobalisationCode());
|
||||||
|
}
|
||||||
|
}
|
||||||
+80
@@ -0,0 +1,80 @@
|
|||||||
|
/**
|
||||||
|
* 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.service;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||||
|
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||||
|
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||||
|
import org.apache.fineract.portfolio.savings.api.FdInterestRateSaveRequest;
|
||||||
|
import org.apache.fineract.portfolio.savings.data.FdInterestRateEffectiveData;
|
||||||
|
import org.apache.fineract.useradministration.domain.AppUser;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class FdBaseInterestRateWritePlatformServiceImplTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private PlatformSecurityContext context;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private FdBaseInterestRateReadPlatformService readPlatformService;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private JsonCommand command;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private AppUser appUser;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private FdBaseInterestRateWritePlatformServiceImpl writePlatformService;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldParseJsonAndSaveInterestRates() {
|
||||||
|
String jsonPayload = "{\"fdType\":\"SINGLE\",\"versionLabel\":\"v1.0\",\"slabs\":[{\"srNo\":1,\"description\":\"1 to 29 Days\",\"periodTypeEnum\":0,\"fromPeriod\":1,\"toPeriod\":29,\"annualInterestRate\":5.0,\"seniorCitizenRate\":5.5}]}";
|
||||||
|
when(command.json()).thenReturn(jsonPayload);
|
||||||
|
when(command.commandId()).thenReturn(789L);
|
||||||
|
when(context.authenticatedUser()).thenReturn(appUser);
|
||||||
|
when(appUser.getId()).thenReturn(1L);
|
||||||
|
|
||||||
|
FdInterestRateEffectiveData savedData = FdInterestRateEffectiveData.builder()
|
||||||
|
.id(10L)
|
||||||
|
.fdType("SINGLE")
|
||||||
|
.versionLabel("v1.0")
|
||||||
|
.build();
|
||||||
|
when(readPlatformService.saveInterestRates(any(FdInterestRateSaveRequest.class), eq(1L))).thenReturn(savedData);
|
||||||
|
|
||||||
|
CommandProcessingResult result = writePlatformService.saveInterestRates(command);
|
||||||
|
|
||||||
|
assertNotNull(result);
|
||||||
|
assertEquals(789L, result.getCommandId());
|
||||||
|
assertEquals(10L, result.getResourceId());
|
||||||
|
verify(readPlatformService).saveInterestRates(any(FdInterestRateSaveRequest.class), eq(1L));
|
||||||
|
}
|
||||||
|
}
|
||||||
+60
@@ -0,0 +1,60 @@
|
|||||||
|
/**
|
||||||
|
* 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.shareproducts.handler;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
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.portfolio.shareproducts.service.ShareProductWritePlatformService;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class CreateShareProductCommandHandlerTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ShareProductWritePlatformService writePlatformService;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private JsonCommand command;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private CreateShareProductCommandHandler underTest;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldDelegateToWritePlatformServiceAndReturnResult() {
|
||||||
|
CommandProcessingResult expectedResult = new CommandProcessingResultBuilder()
|
||||||
|
.withCommandId(104L)
|
||||||
|
.withEntityId(204L)
|
||||||
|
.build();
|
||||||
|
when(writePlatformService.createShareProduct(command)).thenReturn(expectedResult);
|
||||||
|
|
||||||
|
CommandProcessingResult actualResult = underTest.processCommand(command);
|
||||||
|
|
||||||
|
verify(writePlatformService).createShareProduct(command);
|
||||||
|
assertEquals(expectedResult, actualResult);
|
||||||
|
}
|
||||||
|
}
|
||||||
-23
@@ -32,7 +32,6 @@ import static org.apache.fineract.portfolio.interestratechart.InterestRateChartS
|
|||||||
import com.google.gson.JsonArray;
|
import com.google.gson.JsonArray;
|
||||||
import com.google.gson.JsonObject;
|
import com.google.gson.JsonObject;
|
||||||
import jakarta.persistence.CascadeType;
|
import jakarta.persistence.CascadeType;
|
||||||
import jakarta.persistence.Column;
|
|
||||||
import jakarta.persistence.Embedded;
|
import jakarta.persistence.Embedded;
|
||||||
import jakarta.persistence.Entity;
|
import jakarta.persistence.Entity;
|
||||||
import jakarta.persistence.FetchType;
|
import jakarta.persistence.FetchType;
|
||||||
@@ -68,28 +67,6 @@ public class InterestRateChart extends AbstractPersistableCustom<Long> {
|
|||||||
@OneToMany(mappedBy = "interestRateChart", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER)
|
@OneToMany(mappedBy = "interestRateChart", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER)
|
||||||
private Set<InterestRateChartSlab> chartSlabs = new HashSet<>();
|
private Set<InterestRateChartSlab> chartSlabs = new HashSet<>();
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether this is a rate version the product currently offers.
|
|
||||||
* <p>
|
|
||||||
* Kept as an explicit flag rather than inferred from the from/end dates, so retiring a rate version is a deliberate
|
|
||||||
* act and the dates stay purely the historical record. New charts start active.
|
|
||||||
*/
|
|
||||||
@Column(name = "is_active", nullable = false)
|
|
||||||
private boolean active = true;
|
|
||||||
|
|
||||||
public boolean isActive() {
|
|
||||||
return this.active;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Retires this rate version, so it stops being picked for new and renewing deposits. */
|
|
||||||
public void deactivate() {
|
|
||||||
this.active = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void activate() {
|
|
||||||
this.active = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected InterestRateChart() {
|
protected InterestRateChart() {
|
||||||
//
|
//
|
||||||
}
|
}
|
||||||
|
|||||||
-4
@@ -123,10 +123,6 @@ public class InterestRateChartFields {
|
|||||||
return compare != null && DateUtils.isAfter(getFromDate(), compare);
|
return compare != null && DateUtils.isAfter(getFromDate(), compare);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getName() {
|
|
||||||
return this.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
public LocalDate getFromDate() {
|
public LocalDate getFromDate() {
|
||||||
return this.fromDate;
|
return this.fromDate;
|
||||||
}
|
}
|
||||||
|
|||||||
-4
@@ -177,10 +177,6 @@ public class InterestRateChartSlabFields {
|
|||||||
return isGreater;
|
return isGreater;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String description() {
|
|
||||||
return this.description;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Integer periodType() {
|
public Integer periodType() {
|
||||||
return this.periodType;
|
return this.periodType;
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-17
@@ -43,13 +43,6 @@ public final class DepositAccountInterestRateChartSlabData {
|
|||||||
private final BigDecimal annualInterestRate;
|
private final BigDecimal annualInterestRate;
|
||||||
private final CurrencyData currency;
|
private final CurrencyData currency;
|
||||||
|
|
||||||
/**
|
|
||||||
* The senior-citizen premium published on the CBS Base Interest Rate master for this tenure band, carried onto the
|
|
||||||
* account's own chart slab so it survives independently of whatever the master shows today. Null for a slab priced
|
|
||||||
* off the product's own chart (which has no senior-citizen concept) or for any Savings/RD slab.
|
|
||||||
*/
|
|
||||||
private final BigDecimal seniorCitizenRate;
|
|
||||||
|
|
||||||
// associations
|
// associations
|
||||||
private Collection<DepositAccountInterestIncentiveData> incentives;
|
private Collection<DepositAccountInterestIncentiveData> incentives;
|
||||||
|
|
||||||
@@ -73,7 +66,7 @@ public final class DepositAccountInterestRateChartSlabData {
|
|||||||
|
|
||||||
public static DepositAccountInterestRateChartSlabData instance(final Long id, final String description, final EnumOptionData periodType,
|
public static DepositAccountInterestRateChartSlabData instance(final Long id, final String description, final EnumOptionData periodType,
|
||||||
final Integer fromPeriod, final Integer toPeriod, final BigDecimal amountRangeFrom, final BigDecimal amountRangeTo,
|
final Integer fromPeriod, final Integer toPeriod, final BigDecimal amountRangeFrom, final BigDecimal amountRangeTo,
|
||||||
final BigDecimal annualInterestRate, final CurrencyData currency, final BigDecimal seniorCitizenRate) {
|
final BigDecimal annualInterestRate, final CurrencyData currency) {
|
||||||
final Collection<EnumOptionData> periodTypes = null;
|
final Collection<EnumOptionData> periodTypes = null;
|
||||||
final Collection<EnumOptionData> entityTypeOptions = null;
|
final Collection<EnumOptionData> entityTypeOptions = null;
|
||||||
final Collection<EnumOptionData> attributeNameOptions = null;
|
final Collection<EnumOptionData> attributeNameOptions = null;
|
||||||
@@ -85,8 +78,7 @@ public final class DepositAccountInterestRateChartSlabData {
|
|||||||
final Collection<DepositAccountInterestIncentiveData> incentives = null;
|
final Collection<DepositAccountInterestIncentiveData> incentives = null;
|
||||||
return new DepositAccountInterestRateChartSlabData(id, description, periodType, fromPeriod, toPeriod, amountRangeFrom,
|
return new DepositAccountInterestRateChartSlabData(id, description, periodType, fromPeriod, toPeriod, amountRangeFrom,
|
||||||
amountRangeTo, annualInterestRate, incentives, currency, periodTypes, entityTypeOptions, attributeNameOptions,
|
amountRangeTo, annualInterestRate, incentives, currency, periodTypes, entityTypeOptions, attributeNameOptions,
|
||||||
conditionTypeOptions, incentiveTypeOptions, genderOptions, clientTypeOptions, clientClassificationOptions,
|
conditionTypeOptions, incentiveTypeOptions, genderOptions, clientTypeOptions, clientClassificationOptions);
|
||||||
seniorCitizenRate);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static DepositAccountInterestRateChartSlabData from(final InterestRateChartSlabData chartSlabData) {
|
public static DepositAccountInterestRateChartSlabData from(final InterestRateChartSlabData chartSlabData) {
|
||||||
@@ -98,14 +90,12 @@ public final class DepositAccountInterestRateChartSlabData {
|
|||||||
fromProdIncentives.add(DepositAccountInterestIncentiveData.from(incentive));
|
fromProdIncentives.add(DepositAccountInterestIncentiveData.from(incentive));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// the product-level chart this comes from has no senior-citizen concept at all
|
|
||||||
final BigDecimal seniorCitizenRate = null;
|
|
||||||
return new DepositAccountInterestRateChartSlabData(id, chartSlabData.description(), chartSlabData.periodType(),
|
return new DepositAccountInterestRateChartSlabData(id, chartSlabData.description(), chartSlabData.periodType(),
|
||||||
chartSlabData.fromPeriod(), chartSlabData.toPeriod(), chartSlabData.amountRangeFrom(), chartSlabData.amountRangeTo(),
|
chartSlabData.fromPeriod(), chartSlabData.toPeriod(), chartSlabData.amountRangeFrom(), chartSlabData.amountRangeTo(),
|
||||||
chartSlabData.annualInterestRate(), fromProdIncentives, chartSlabData.currency(), chartSlabData.periodTypes(),
|
chartSlabData.annualInterestRate(), fromProdIncentives, chartSlabData.currency(), chartSlabData.periodTypes(),
|
||||||
chartSlabData.entityTypeOptions(), chartSlabData.attributeNameOptions(), chartSlabData.conditionTypeOptions(),
|
chartSlabData.entityTypeOptions(), chartSlabData.attributeNameOptions(), chartSlabData.conditionTypeOptions(),
|
||||||
chartSlabData.incentiveTypeOptions(), chartSlabData.genderOptions(), chartSlabData.clientTypeOptions(),
|
chartSlabData.incentiveTypeOptions(), chartSlabData.genderOptions(), chartSlabData.clientTypeOptions(),
|
||||||
chartSlabData.clientClassificationOptions(), seniorCitizenRate);
|
chartSlabData.clientClassificationOptions());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static DepositAccountInterestRateChartSlabData withTemplate(final DepositAccountInterestRateChartSlabData chartSlab,
|
public static DepositAccountInterestRateChartSlabData withTemplate(final DepositAccountInterestRateChartSlabData chartSlab,
|
||||||
@@ -116,7 +106,7 @@ public final class DepositAccountInterestRateChartSlabData {
|
|||||||
return new DepositAccountInterestRateChartSlabData(chartSlab.id, chartSlab.description, chartSlab.periodType, chartSlab.fromPeriod,
|
return new DepositAccountInterestRateChartSlabData(chartSlab.id, chartSlab.description, chartSlab.periodType, chartSlab.fromPeriod,
|
||||||
chartSlab.toPeriod, chartSlab.amountRangeFrom, chartSlab.amountRangeTo, chartSlab.annualInterestRate, chartSlab.incentives,
|
chartSlab.toPeriod, chartSlab.amountRangeFrom, chartSlab.amountRangeTo, chartSlab.annualInterestRate, chartSlab.incentives,
|
||||||
chartSlab.currency, periodTypes, entityTypeOptions, attributeNameOptions, conditionTypeOptions, incentiveTypeOptions,
|
chartSlab.currency, periodTypes, entityTypeOptions, attributeNameOptions, conditionTypeOptions, incentiveTypeOptions,
|
||||||
genderOptions, clientTypeOptions, clientClassificationOptions, chartSlab.seniorCitizenRate);
|
genderOptions, clientTypeOptions, clientClassificationOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
private DepositAccountInterestRateChartSlabData(final Long id, final String description, final EnumOptionData periodType,
|
private DepositAccountInterestRateChartSlabData(final Long id, final String description, final EnumOptionData periodType,
|
||||||
@@ -125,8 +115,7 @@ public final class DepositAccountInterestRateChartSlabData {
|
|||||||
final CurrencyData currency, final Collection<EnumOptionData> periodTypes, final Collection<EnumOptionData> entityTypeOptions,
|
final CurrencyData currency, final Collection<EnumOptionData> periodTypes, final Collection<EnumOptionData> entityTypeOptions,
|
||||||
final Collection<EnumOptionData> attributeNameOptions, final Collection<EnumOptionData> conditionTypeOptions,
|
final Collection<EnumOptionData> attributeNameOptions, final Collection<EnumOptionData> conditionTypeOptions,
|
||||||
final Collection<EnumOptionData> incentiveTypeOptions, final Collection<CodeValueData> genderOptions,
|
final Collection<EnumOptionData> incentiveTypeOptions, final Collection<CodeValueData> genderOptions,
|
||||||
final Collection<CodeValueData> clientTypeOptions, final Collection<CodeValueData> clientClassificationOptions,
|
final Collection<CodeValueData> clientTypeOptions, final Collection<CodeValueData> clientClassificationOptions) {
|
||||||
final BigDecimal seniorCitizenRate) {
|
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.description = description;
|
this.description = description;
|
||||||
this.periodType = periodType;
|
this.periodType = periodType;
|
||||||
@@ -136,7 +125,6 @@ public final class DepositAccountInterestRateChartSlabData {
|
|||||||
this.amountRangeTo = amountRangeTo;
|
this.amountRangeTo = amountRangeTo;
|
||||||
this.annualInterestRate = annualInterestRate;
|
this.annualInterestRate = annualInterestRate;
|
||||||
this.currency = currency;
|
this.currency = currency;
|
||||||
this.seniorCitizenRate = seniorCitizenRate;
|
|
||||||
this.periodTypes = periodTypes;
|
this.periodTypes = periodTypes;
|
||||||
this.incentives = incentivesData;
|
this.incentives = incentivesData;
|
||||||
this.attributeNameOptions = attributeNameOptions;
|
this.attributeNameOptions = attributeNameOptions;
|
||||||
|
|||||||
-51
@@ -138,50 +138,6 @@ public final class FixedDepositAccountData extends DepositAccountData {
|
|||||||
private Boolean isSeniorCitizen;
|
private Boolean isSeniorCitizen;
|
||||||
private java.math.BigDecimal seniorCitizenInterestRate;
|
private java.math.BigDecimal seniorCitizenInterestRate;
|
||||||
|
|
||||||
// Reinvestment trail: which account this one was reinvested FROM (set on the new account) and
|
|
||||||
// which account it was reinvested INTO (derived by reverse lookup on the old account), so staff
|
|
||||||
// can trace a "Re-Invest Maturity Amount"/"Re-Invest Principal Only" closure both directions
|
|
||||||
// instead of the link disappearing once the old account closes.
|
|
||||||
private Long reinvestedFromAccountId;
|
|
||||||
private String reinvestedFromAccountNo;
|
|
||||||
private Long reinvestedIntoAccountId;
|
|
||||||
private String reinvestedIntoAccountNo;
|
|
||||||
|
|
||||||
public void setReinvestmentLinks(final Long reinvestedFromAccountId, final String reinvestedFromAccountNo,
|
|
||||||
final Long reinvestedIntoAccountId, final String reinvestedIntoAccountNo) {
|
|
||||||
this.reinvestedFromAccountId = reinvestedFromAccountId;
|
|
||||||
this.reinvestedFromAccountNo = reinvestedFromAccountNo;
|
|
||||||
this.reinvestedIntoAccountId = reinvestedIntoAccountId;
|
|
||||||
this.reinvestedIntoAccountNo = reinvestedIntoAccountNo;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The account number the customer should keep seeing across renewals -- the ORIGINAL account's
|
|
||||||
// account_no, carried forward onto every account created by reinvesting it. Falls back to this
|
|
||||||
// account's own accountNo when it has never been reinvested (see DepositAccountReadPlatformServiceImpl).
|
|
||||||
private String fdAccountNumber;
|
|
||||||
|
|
||||||
public void setFdAccountNumber(final String fdAccountNumber) {
|
|
||||||
this.fdAccountNumber = fdAccountNumber;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Which renewal cycle of that FD this account is: 0 = originally opened, 1 = first renewal, etc. */
|
|
||||||
private Integer renewalCycleNo;
|
|
||||||
|
|
||||||
public void setRenewalCycleNo(final Integer renewalCycleNo) {
|
|
||||||
this.renewalCycleNo = renewalCycleNo;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Every term of this FD, oldest first: one entry per account in the renewal chain, each carrying that term's own
|
|
||||||
* account number, cycle number, dates, principal and maturity amount. Lets the account view and the deposit
|
|
||||||
* certificate present "one FD, many terms" without the caller having to walk the chain itself.
|
|
||||||
*/
|
|
||||||
private java.util.List<java.util.Map<String, Object>> renewalHistory;
|
|
||||||
|
|
||||||
public void setRenewalHistory(final java.util.List<java.util.Map<String, Object>> renewalHistory) {
|
|
||||||
this.renewalHistory = renewalHistory;
|
|
||||||
}
|
|
||||||
|
|
||||||
// CBS account-level fields (stored per-account in m_savings_account)
|
// CBS account-level fields (stored per-account in m_savings_account)
|
||||||
private String accountStatus;
|
private String accountStatus;
|
||||||
private String modeOfOperation;
|
private String modeOfOperation;
|
||||||
@@ -373,13 +329,6 @@ public final class FixedDepositAccountData extends DepositAccountData {
|
|||||||
this.jointHolders = src.jointHolders;
|
this.jointHolders = src.jointHolders;
|
||||||
this.isSeniorCitizen = src.isSeniorCitizen;
|
this.isSeniorCitizen = src.isSeniorCitizen;
|
||||||
this.seniorCitizenInterestRate = src.seniorCitizenInterestRate;
|
this.seniorCitizenInterestRate = src.seniorCitizenInterestRate;
|
||||||
this.reinvestedFromAccountId = src.reinvestedFromAccountId;
|
|
||||||
this.reinvestedFromAccountNo = src.reinvestedFromAccountNo;
|
|
||||||
this.reinvestedIntoAccountId = src.reinvestedIntoAccountId;
|
|
||||||
this.reinvestedIntoAccountNo = src.reinvestedIntoAccountNo;
|
|
||||||
this.fdAccountNumber = src.fdAccountNumber;
|
|
||||||
this.renewalCycleNo = src.renewalCycleNo;
|
|
||||||
this.renewalHistory = src.renewalHistory;
|
|
||||||
this.subStatus = src.subStatus;
|
this.subStatus = src.subStatus;
|
||||||
this.reasonForBlock = src.reasonForBlock;
|
this.reasonForBlock = src.reasonForBlock;
|
||||||
this.lastActiveTransactionDate = src.lastActiveTransactionDate;
|
this.lastActiveTransactionDate = src.lastActiveTransactionDate;
|
||||||
|
|||||||
+12
-23
@@ -76,7 +76,6 @@ public final class RecurringDepositAccountData extends DepositAccountData {
|
|||||||
// for account close
|
// for account close
|
||||||
private final Collection<EnumOptionData> onAccountClosureOptions;
|
private final Collection<EnumOptionData> onAccountClosureOptions;
|
||||||
private final Collection<PaymentTypeData> paymentTypeOptions;
|
private final Collection<PaymentTypeData> paymentTypeOptions;
|
||||||
private final Collection<EnumOptionData> maturityInstructionOptions;
|
|
||||||
|
|
||||||
// import fields
|
// import fields
|
||||||
private transient Integer rowIndex;
|
private transient Integer rowIndex;
|
||||||
@@ -365,7 +364,6 @@ public final class RecurringDepositAccountData extends DepositAccountData {
|
|||||||
this.savingsAccounts = null;
|
this.savingsAccounts = null;
|
||||||
this.onAccountClosureOptions = null;
|
this.onAccountClosureOptions = null;
|
||||||
this.paymentTypeOptions = null;
|
this.paymentTypeOptions = null;
|
||||||
this.maturityInstructionOptions = null;
|
|
||||||
this.rowIndex = rowIndex;
|
this.rowIndex = rowIndex;
|
||||||
this.dateFormat = dateFormat;
|
this.dateFormat = dateFormat;
|
||||||
this.locale = locale;
|
this.locale = locale;
|
||||||
@@ -396,7 +394,6 @@ public final class RecurringDepositAccountData extends DepositAccountData {
|
|||||||
final Collection<SavingsAccountData> savingsAccountDatas = null;
|
final Collection<SavingsAccountData> savingsAccountDatas = null;
|
||||||
final Integer recurringFrequency = null;
|
final Integer recurringFrequency = null;
|
||||||
final EnumOptionData recurringFrequencyType = null;
|
final EnumOptionData recurringFrequencyType = null;
|
||||||
final Collection<EnumOptionData> maturityInstructionOptions = null;
|
|
||||||
|
|
||||||
return new RecurringDepositAccountData(depositAccountData.id, depositAccountData.accountNo, depositAccountData.externalId,
|
return new RecurringDepositAccountData(depositAccountData.id, depositAccountData.accountNo, depositAccountData.externalId,
|
||||||
depositAccountData.groupId, depositAccountData.groupName, depositAccountData.clientId, depositAccountData.clientName,
|
depositAccountData.groupId, depositAccountData.groupName, depositAccountData.clientId, depositAccountData.clientName,
|
||||||
@@ -419,7 +416,7 @@ public final class RecurringDepositAccountData extends DepositAccountData {
|
|||||||
onAccountClosure, onAccountClosureOptions, paymentTypeOptions, savingsAccountDatas, expectedFirstDepositOnDate,
|
onAccountClosure, onAccountClosureOptions, paymentTypeOptions, savingsAccountDatas, expectedFirstDepositOnDate,
|
||||||
totalOverdueAmount, noOfOverdueInstallments, isMandatoryDeposit, allowWithdrawal, adjustAdvanceTowardsFuturePayments,
|
totalOverdueAmount, noOfOverdueInstallments, isMandatoryDeposit, allowWithdrawal, adjustAdvanceTowardsFuturePayments,
|
||||||
isCalendarInherited, recurringFrequency, recurringFrequencyType, depositAccountData.withHoldTax,
|
isCalendarInherited, recurringFrequency, recurringFrequencyType, depositAccountData.withHoldTax,
|
||||||
depositAccountData.taxGroup, maturityInstructionOptions);
|
depositAccountData.taxGroup);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static RecurringDepositAccountData withInterestChartAndRecurringDetails(final RecurringDepositAccountData account,
|
public static RecurringDepositAccountData withInterestChartAndRecurringDetails(final RecurringDepositAccountData account,
|
||||||
@@ -444,7 +441,7 @@ public final class RecurringDepositAccountData extends DepositAccountData {
|
|||||||
account.paymentTypeOptions, account.savingsAccounts, account.expectedFirstDepositOnDate, account.totalOverdueAmount,
|
account.paymentTypeOptions, account.savingsAccounts, account.expectedFirstDepositOnDate, account.totalOverdueAmount,
|
||||||
account.noOfOverdueInstallments, account.isMandatoryDeposit, account.allowWithdrawal,
|
account.noOfOverdueInstallments, account.isMandatoryDeposit, account.allowWithdrawal,
|
||||||
account.adjustAdvanceTowardsFuturePayments, account.isCalendarInherited, recurringFrequency, recurringFrequencyType,
|
account.adjustAdvanceTowardsFuturePayments, account.isCalendarInherited, recurringFrequency, recurringFrequencyType,
|
||||||
account.withHoldTax, account.taxGroup, account.maturityInstructionOptions);
|
account.withHoldTax, account.taxGroup);
|
||||||
result.copyCbsFieldsFrom(account);
|
result.copyCbsFieldsFrom(account);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -466,13 +463,11 @@ public final class RecurringDepositAccountData extends DepositAccountData {
|
|||||||
|
|
||||||
final Collection<EnumOptionData> preClosurePenalInterestOnTypeOptions = null;
|
final Collection<EnumOptionData> preClosurePenalInterestOnTypeOptions = null;
|
||||||
final Collection<EnumOptionData> periodFrequencyTypeOptions = null;
|
final Collection<EnumOptionData> periodFrequencyTypeOptions = null;
|
||||||
final Collection<SavingsAccountData> savingsAccountDatas = account.savingsAccounts;
|
|
||||||
final Collection<EnumOptionData> maturityInstructionOptions = account.maturityInstructionOptions;
|
|
||||||
|
|
||||||
return withTemplateOptions(account, productOptions, fieldOfficerOptions, interestCompoundingPeriodTypeOptions,
|
return withTemplateOptions(account, productOptions, fieldOfficerOptions, interestCompoundingPeriodTypeOptions,
|
||||||
interestPostingPeriodTypeOptions, interestCalculationTypeOptions, interestCalculationDaysInYearTypeOptions,
|
interestPostingPeriodTypeOptions, interestCalculationTypeOptions, interestCalculationDaysInYearTypeOptions,
|
||||||
lockinPeriodFrequencyTypeOptions, withdrawalFeeTypeOptions, transactions, charges, chargeOptions,
|
lockinPeriodFrequencyTypeOptions, withdrawalFeeTypeOptions, transactions, charges, chargeOptions,
|
||||||
preClosurePenalInterestOnTypeOptions, periodFrequencyTypeOptions, savingsAccountDatas, maturityInstructionOptions);
|
preClosurePenalInterestOnTypeOptions, periodFrequencyTypeOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
final RecurringDepositAccountData result = new RecurringDepositAccountData(account.id, account.accountNo, account.externalId, account.groupId, account.groupName,
|
final RecurringDepositAccountData result = new RecurringDepositAccountData(account.id, account.accountNo, account.externalId, account.groupId, account.groupName,
|
||||||
@@ -491,10 +486,10 @@ public final class RecurringDepositAccountData extends DepositAccountData {
|
|||||||
account.inMultiplesOfDepositTermType, account.depositAmount, account.maturityAmount, account.maturityDate,
|
account.inMultiplesOfDepositTermType, account.depositAmount, account.maturityAmount, account.maturityDate,
|
||||||
account.depositPeriod, account.depositPeriodFrequency, account.mandatoryRecommendedDepositAmount,
|
account.depositPeriod, account.depositPeriodFrequency, account.mandatoryRecommendedDepositAmount,
|
||||||
template.periodFrequencyTypeOptions, account.depositType, account.onAccountClosure, account.onAccountClosureOptions,
|
template.periodFrequencyTypeOptions, account.depositType, account.onAccountClosure, account.onAccountClosureOptions,
|
||||||
account.paymentTypeOptions, template.savingsAccounts, account.expectedFirstDepositOnDate, account.totalOverdueAmount,
|
account.paymentTypeOptions, account.savingsAccounts, account.expectedFirstDepositOnDate, account.totalOverdueAmount,
|
||||||
account.noOfOverdueInstallments, account.isMandatoryDeposit, account.allowWithdrawal,
|
account.noOfOverdueInstallments, account.isMandatoryDeposit, account.allowWithdrawal,
|
||||||
account.adjustAdvanceTowardsFuturePayments, account.isCalendarInherited, account.recurringFrequency,
|
account.adjustAdvanceTowardsFuturePayments, account.isCalendarInherited, account.recurringFrequency,
|
||||||
account.recurringFrequencyType, account.withHoldTax, account.taxGroup, account.maturityInstructionOptions);
|
account.recurringFrequencyType, account.withHoldTax, account.taxGroup);
|
||||||
result.copyCbsFieldsFrom(account);
|
result.copyCbsFieldsFrom(account);
|
||||||
// Merge accountTypeOptions and customerSegmentOptions from template
|
// Merge accountTypeOptions and customerSegmentOptions from template
|
||||||
if (template.getAccountTypeOptions() != null) result.setAccountTypeOptions(template.getAccountTypeOptions());
|
if (template.getAccountTypeOptions() != null) result.setAccountTypeOptions(template.getAccountTypeOptions());
|
||||||
@@ -512,8 +507,7 @@ public final class RecurringDepositAccountData extends DepositAccountData {
|
|||||||
final Collection<EnumOptionData> lockinPeriodFrequencyTypeOptions, final Collection<EnumOptionData> withdrawalFeeTypeOptions,
|
final Collection<EnumOptionData> lockinPeriodFrequencyTypeOptions, final Collection<EnumOptionData> withdrawalFeeTypeOptions,
|
||||||
final Collection<SavingsAccountTransactionData> transactions, final Collection<SavingsAccountChargeData> charges,
|
final Collection<SavingsAccountTransactionData> transactions, final Collection<SavingsAccountChargeData> charges,
|
||||||
final Collection<ChargeData> chargeOptions, final Collection<EnumOptionData> preClosurePenalInterestOnTypeOptions,
|
final Collection<ChargeData> chargeOptions, final Collection<EnumOptionData> preClosurePenalInterestOnTypeOptions,
|
||||||
final Collection<EnumOptionData> periodFrequencyTypeOptions, final Collection<SavingsAccountData> savingsAccountDatas,
|
final Collection<EnumOptionData> periodFrequencyTypeOptions) {
|
||||||
final Collection<EnumOptionData> maturityInstructionOptions) {
|
|
||||||
|
|
||||||
final RecurringDepositAccountData r2 = new RecurringDepositAccountData(account.id, account.accountNo, account.externalId, account.groupId, account.groupName,
|
final RecurringDepositAccountData r2 = new RecurringDepositAccountData(account.id, account.accountNo, account.externalId, account.groupId, account.groupName,
|
||||||
account.clientId, account.clientName, account.depositProductId, account.depositProductName, account.fieldOfficerId,
|
account.clientId, account.clientName, account.depositProductId, account.depositProductName, account.fieldOfficerId,
|
||||||
@@ -530,10 +524,10 @@ public final class RecurringDepositAccountData extends DepositAccountData {
|
|||||||
account.inMultiplesOfDepositTermType, account.depositAmount, account.maturityAmount, account.maturityDate,
|
account.inMultiplesOfDepositTermType, account.depositAmount, account.maturityAmount, account.maturityDate,
|
||||||
account.depositPeriod, account.depositPeriodFrequency, account.mandatoryRecommendedDepositAmount,
|
account.depositPeriod, account.depositPeriodFrequency, account.mandatoryRecommendedDepositAmount,
|
||||||
periodFrequencyTypeOptions, account.depositType, account.onAccountClosure, account.onAccountClosureOptions,
|
periodFrequencyTypeOptions, account.depositType, account.onAccountClosure, account.onAccountClosureOptions,
|
||||||
account.paymentTypeOptions, savingsAccountDatas, account.expectedFirstDepositOnDate, account.totalOverdueAmount,
|
account.paymentTypeOptions, account.savingsAccounts, account.expectedFirstDepositOnDate, account.totalOverdueAmount,
|
||||||
account.noOfOverdueInstallments, account.isMandatoryDeposit, account.allowWithdrawal,
|
account.noOfOverdueInstallments, account.isMandatoryDeposit, account.allowWithdrawal,
|
||||||
account.adjustAdvanceTowardsFuturePayments, account.isCalendarInherited, account.recurringFrequency,
|
account.adjustAdvanceTowardsFuturePayments, account.isCalendarInherited, account.recurringFrequency,
|
||||||
account.recurringFrequencyType, account.withHoldTax, account.taxGroup, maturityInstructionOptions);
|
account.recurringFrequencyType, account.withHoldTax, account.taxGroup);
|
||||||
r2.copyCbsFieldsFrom(account);
|
r2.copyCbsFieldsFrom(account);
|
||||||
return r2;
|
return r2;
|
||||||
}
|
}
|
||||||
@@ -615,7 +609,6 @@ public final class RecurringDepositAccountData extends DepositAccountData {
|
|||||||
final EnumOptionData recurringFrequencyType = null;
|
final EnumOptionData recurringFrequencyType = null;
|
||||||
final boolean withHoldTax = false;
|
final boolean withHoldTax = false;
|
||||||
final TaxGroupData taxGroup = null;
|
final TaxGroupData taxGroup = null;
|
||||||
final Collection<EnumOptionData> maturityInstructionOptions = null;
|
|
||||||
|
|
||||||
return new RecurringDepositAccountData(id, accountNo, externalId, groupId, groupName, clientId, clientName, productId, productName,
|
return new RecurringDepositAccountData(id, accountNo, externalId, groupId, groupName, clientId, clientName, productId, productName,
|
||||||
fieldOfficerId, fieldOfficerName, status, timeline, currency, nominalAnnualInterestRate, interestPeriodType,
|
fieldOfficerId, fieldOfficerName, status, timeline, currency, nominalAnnualInterestRate, interestPeriodType,
|
||||||
@@ -629,8 +622,7 @@ public final class RecurringDepositAccountData extends DepositAccountData {
|
|||||||
depositAmount, maturityAmount, maturityDate, depositPeriod, depositPeriodFrequency, mandatoryRecommendedDepositAmount,
|
depositAmount, maturityAmount, maturityDate, depositPeriod, depositPeriodFrequency, mandatoryRecommendedDepositAmount,
|
||||||
periodFrequencyTypeOptions, depositType, onAccountClosure, onAccountClosureOptions, paymentTypeOptions, savingsAccountDatas,
|
periodFrequencyTypeOptions, depositType, onAccountClosure, onAccountClosureOptions, paymentTypeOptions, savingsAccountDatas,
|
||||||
expectedFirstDepositOnDate, totalOverdueAmount, noOfOverdueInstallments, isMandatoryDeposit, allowWithdrawal,
|
expectedFirstDepositOnDate, totalOverdueAmount, noOfOverdueInstallments, isMandatoryDeposit, allowWithdrawal,
|
||||||
adjustAdvanceTowardsFuturePayments, isCalendarInherited, recurringFrequency, recurringFrequencyType, withHoldTax, taxGroup,
|
adjustAdvanceTowardsFuturePayments, isCalendarInherited, recurringFrequency, recurringFrequencyType, withHoldTax, taxGroup);
|
||||||
maturityInstructionOptions);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static RecurringDepositAccountData preClosureDetails(final Long accountId, final BigDecimal maturityAmount,
|
public static RecurringDepositAccountData preClosureDetails(final Long accountId, final BigDecimal maturityAmount,
|
||||||
@@ -708,7 +700,6 @@ public final class RecurringDepositAccountData extends DepositAccountData {
|
|||||||
final EnumOptionData recurringFrequencyType = null;
|
final EnumOptionData recurringFrequencyType = null;
|
||||||
final boolean withHoldTax = false;
|
final boolean withHoldTax = false;
|
||||||
final TaxGroupData taxGroup = null;
|
final TaxGroupData taxGroup = null;
|
||||||
final Collection<EnumOptionData> maturityInstructionOptions = null;
|
|
||||||
|
|
||||||
return new RecurringDepositAccountData(accountId, accountNo, externalId, groupId, groupName, clientId, clientName, productId,
|
return new RecurringDepositAccountData(accountId, accountNo, externalId, groupId, groupName, clientId, clientName, productId,
|
||||||
productName, fieldOfficerId, fieldOfficerName, status, timeline, currency, nominalAnnualInterestRate, interestPeriodType,
|
productName, fieldOfficerId, fieldOfficerName, status, timeline, currency, nominalAnnualInterestRate, interestPeriodType,
|
||||||
@@ -722,8 +713,7 @@ public final class RecurringDepositAccountData extends DepositAccountData {
|
|||||||
depositAmount, maturityAmount, maturityDate, depositPeriod, depositPeriodFrequency, mandatoryRecommendedDepositAmount,
|
depositAmount, maturityAmount, maturityDate, depositPeriod, depositPeriodFrequency, mandatoryRecommendedDepositAmount,
|
||||||
periodFrequencyTypeOptions, depositType, onAccountClosure, onAccountClosureOptions, paymentTypeOptions, savingsAccountDatas,
|
periodFrequencyTypeOptions, depositType, onAccountClosure, onAccountClosureOptions, paymentTypeOptions, savingsAccountDatas,
|
||||||
expectedFirstDepositOnDate, totalOverdueAmount, noOfOverdueInstallments, isMandatoryDeposit, allowWithdrawal,
|
expectedFirstDepositOnDate, totalOverdueAmount, noOfOverdueInstallments, isMandatoryDeposit, allowWithdrawal,
|
||||||
adjustAdvanceTowardsFuturePayments, isCalendarInherited, recurringFrequency, recurringFrequencyType, withHoldTax, taxGroup,
|
adjustAdvanceTowardsFuturePayments, isCalendarInherited, recurringFrequency, recurringFrequencyType, withHoldTax, taxGroup);
|
||||||
maturityInstructionOptions);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static RecurringDepositAccountData withClosureTemplateDetails(final RecurringDepositAccountData account,
|
public static RecurringDepositAccountData withClosureTemplateDetails(final RecurringDepositAccountData account,
|
||||||
@@ -749,7 +739,7 @@ public final class RecurringDepositAccountData extends DepositAccountData {
|
|||||||
paymentTypeOptions, savingsAccountDatas, account.expectedFirstDepositOnDate, account.totalOverdueAmount,
|
paymentTypeOptions, savingsAccountDatas, account.expectedFirstDepositOnDate, account.totalOverdueAmount,
|
||||||
account.noOfOverdueInstallments, account.isMandatoryDeposit, account.allowWithdrawal,
|
account.noOfOverdueInstallments, account.isMandatoryDeposit, account.allowWithdrawal,
|
||||||
account.adjustAdvanceTowardsFuturePayments, account.isCalendarInherited, account.recurringFrequency,
|
account.adjustAdvanceTowardsFuturePayments, account.isCalendarInherited, account.recurringFrequency,
|
||||||
account.recurringFrequencyType, account.withHoldTax, account.taxGroup, account.maturityInstructionOptions);
|
account.recurringFrequencyType, account.withHoldTax, account.taxGroup);
|
||||||
closureResult.copyCbsFieldsFrom(account);
|
closureResult.copyCbsFieldsFrom(account);
|
||||||
return closureResult;
|
return closureResult;
|
||||||
}
|
}
|
||||||
@@ -784,7 +774,7 @@ public final class RecurringDepositAccountData extends DepositAccountData {
|
|||||||
final BigDecimal totalOverdueAmount, final Integer noOfOverdueInstallments, final boolean isMandatoryDeposit,
|
final BigDecimal totalOverdueAmount, final Integer noOfOverdueInstallments, final boolean isMandatoryDeposit,
|
||||||
final boolean allowWithdrawal, final boolean adjustAdvanceTowardsFuturePayments, final boolean isCalendarInherited,
|
final boolean allowWithdrawal, final boolean adjustAdvanceTowardsFuturePayments, final boolean isCalendarInherited,
|
||||||
final Integer recurringFrequency, final EnumOptionData recurringFrequencyType, final boolean withHoldTax,
|
final Integer recurringFrequency, final EnumOptionData recurringFrequencyType, final boolean withHoldTax,
|
||||||
final TaxGroupData taxGroup, final Collection<EnumOptionData> maturityInstructionOptions) {
|
final TaxGroupData taxGroup) {
|
||||||
|
|
||||||
super(id, accountNo, externalId, groupId, groupName, clientId, clientName, productId, productName, fieldofficerId, fieldofficerName,
|
super(id, accountNo, externalId, groupId, groupName, clientId, clientName, productId, productName, fieldofficerId, fieldofficerName,
|
||||||
status, timeline, currency, nominalAnnualInterestRate, interestPeriodType, interestPostingPeriodType,
|
status, timeline, currency, nominalAnnualInterestRate, interestPeriodType, interestPostingPeriodType,
|
||||||
@@ -828,7 +818,6 @@ public final class RecurringDepositAccountData extends DepositAccountData {
|
|||||||
// account close template options
|
// account close template options
|
||||||
this.onAccountClosureOptions = onAccountClosureOptions;
|
this.onAccountClosureOptions = onAccountClosureOptions;
|
||||||
this.paymentTypeOptions = paymentTypeOptions;
|
this.paymentTypeOptions = paymentTypeOptions;
|
||||||
this.maturityInstructionOptions = maturityInstructionOptions;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
-86
@@ -28,16 +28,12 @@ import jakarta.persistence.OneToOne;
|
|||||||
import jakarta.persistence.Table;
|
import jakarta.persistence.Table;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
|
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
|
||||||
import org.apache.fineract.portfolio.client.domain.Client;
|
import org.apache.fineract.portfolio.client.domain.Client;
|
||||||
import org.apache.fineract.portfolio.interestratechart.domain.InterestRateChart;
|
import org.apache.fineract.portfolio.interestratechart.domain.InterestRateChart;
|
||||||
import org.apache.fineract.portfolio.interestratechart.domain.InterestRateChartFields;
|
import org.apache.fineract.portfolio.interestratechart.domain.InterestRateChartFields;
|
||||||
import org.apache.fineract.portfolio.interestratechart.domain.InterestRateChartSlabFields;
|
|
||||||
import org.apache.fineract.portfolio.interestratechart.domain.InterestRateChartSlab;
|
import org.apache.fineract.portfolio.interestratechart.domain.InterestRateChartSlab;
|
||||||
import org.apache.fineract.portfolio.interestratechart.incentive.AttributeIncentiveCalculation;
|
import org.apache.fineract.portfolio.interestratechart.incentive.AttributeIncentiveCalculation;
|
||||||
import org.apache.fineract.portfolio.interestratechart.incentive.AttributeIncentiveCalculationFactory;
|
import org.apache.fineract.portfolio.interestratechart.incentive.AttributeIncentiveCalculationFactory;
|
||||||
@@ -160,86 +156,4 @@ public class DepositAccountInterestRateChart extends AbstractPersistableCustom<L
|
|||||||
public boolean isPrimaryGroupingByAmount() {
|
public boolean isPrimaryGroupingByAmount() {
|
||||||
return this.chartFields.isPrimaryGroupingByAmount();
|
return this.chartFields.isPrimaryGroupingByAmount();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Re-snapshots this account's chart from the product's current chart, in place.
|
|
||||||
* <p>
|
|
||||||
* Used when an FD renews on the same account: the new term must be priced off whatever the product offers on the
|
|
||||||
* renewal date, not the rates frozen when the account was first opened. This has to mutate the existing row rather
|
|
||||||
* than swap in a new chart, because the account-to-chart mapping is one-to-one on the chart's savings_account_id, so
|
|
||||||
* a second row for the same account would break loading it.
|
|
||||||
*/
|
|
||||||
public void refreshFrom(final InterestRateChart productChart) {
|
|
||||||
this.chartFields = productChart.chartFields();
|
|
||||||
// orphanRemoval on chartSlabs deletes the superseded slab rows.
|
|
||||||
setOfChartSlabs().clear();
|
|
||||||
for (final InterestRateChartSlab productSlab : productChart.setOfChartSlabs()) {
|
|
||||||
this.chartSlabs.add(DepositAccountInterestRateChartSlabs.from(productSlab, this));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A slab's rate-by-tenure fields plus its senior-citizen premium, travelling together.
|
|
||||||
* <p>
|
|
||||||
* The premium isn't part of {@link InterestRateChartSlabFields} because that embeddable is shared with the
|
|
||||||
* product-level chart slab table, which has no CBS senior-citizen concept -- so it rides alongside as a plain pair
|
|
||||||
* rather than being added to the shared type.
|
|
||||||
*/
|
|
||||||
public static final class SlabSnapshot {
|
|
||||||
|
|
||||||
private final InterestRateChartSlabFields fields;
|
|
||||||
private final BigDecimal seniorCitizenRate;
|
|
||||||
|
|
||||||
public SlabSnapshot(final InterestRateChartSlabFields fields, final BigDecimal seniorCitizenRate) {
|
|
||||||
this.fields = fields;
|
|
||||||
this.seniorCitizenRate = seniorCitizenRate;
|
|
||||||
}
|
|
||||||
|
|
||||||
public InterestRateChartSlabFields getFields() {
|
|
||||||
return this.fields;
|
|
||||||
}
|
|
||||||
|
|
||||||
public BigDecimal getSeniorCitizenRate() {
|
|
||||||
return this.seniorCitizenRate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This chart's slabs as they stand right now, each paired with its senior-citizen premium. Read before
|
|
||||||
* {@link #refreshFromSlabSnapshots} overwrites them, so a renewing FD can archive the term that is closing rather than
|
|
||||||
* losing its rate card the moment the new term's rates replace it.
|
|
||||||
*/
|
|
||||||
public List<SlabSnapshot> currentSlabSnapshots() {
|
|
||||||
final List<SlabSnapshot> snapshots = new ArrayList<>();
|
|
||||||
for (final DepositAccountInterestRateChartSlabs slab : setOfChartSlabs()) {
|
|
||||||
snapshots.add(new SlabSnapshot(slab.slabFields(), slab.getSeniorCitizenRate()));
|
|
||||||
}
|
|
||||||
return snapshots;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Re-snapshots this account's chart from an explicit set of slabs (with their senior-citizen premiums), in place.
|
|
||||||
* <p>
|
|
||||||
* Used when the rates come from the CBS base interest-rate master rather than from the product's own chart -- that
|
|
||||||
* master is the rate card staff maintain and see on screen, so a renewing deposit priced off anything else would
|
|
||||||
* disagree with what the bank publishes. Mutates the existing row for the same reason
|
|
||||||
* {@link #refreshFrom(InterestRateChart)} does: the account-to-chart mapping is one-to-one on the chart's
|
|
||||||
* savings_account_id, so a second row for the same account would break loading it.
|
|
||||||
* <p>
|
|
||||||
* Does nothing when handed no slabs, leaving the account on the chart it already has, so a missing or empty rate
|
|
||||||
* version can never leave a deposit with no rates at all.
|
|
||||||
*/
|
|
||||||
public void refreshFromSlabSnapshots(final InterestRateChartFields chartFields, final Collection<SlabSnapshot> snapshots) {
|
|
||||||
if (snapshots == null || snapshots.isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (chartFields != null) {
|
|
||||||
this.chartFields = chartFields;
|
|
||||||
}
|
|
||||||
setOfChartSlabs().clear();
|
|
||||||
for (final SlabSnapshot snapshot : snapshots) {
|
|
||||||
this.chartSlabs
|
|
||||||
.add(DepositAccountInterestRateChartSlabs.fromFields(snapshot.getFields(), this, snapshot.getSeniorCitizenRate()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
-29
@@ -19,7 +19,6 @@
|
|||||||
package org.apache.fineract.portfolio.savings.domain;
|
package org.apache.fineract.portfolio.savings.domain;
|
||||||
|
|
||||||
import jakarta.persistence.CascadeType;
|
import jakarta.persistence.CascadeType;
|
||||||
import jakarta.persistence.Column;
|
|
||||||
import jakarta.persistence.Embedded;
|
import jakarta.persistence.Embedded;
|
||||||
import jakarta.persistence.Entity;
|
import jakarta.persistence.Entity;
|
||||||
import jakarta.persistence.FetchType;
|
import jakarta.persistence.FetchType;
|
||||||
@@ -27,7 +26,6 @@ import jakarta.persistence.JoinColumn;
|
|||||||
import jakarta.persistence.ManyToOne;
|
import jakarta.persistence.ManyToOne;
|
||||||
import jakarta.persistence.OneToMany;
|
import jakarta.persistence.OneToMany;
|
||||||
import jakarta.persistence.Table;
|
import jakarta.persistence.Table;
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
|
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
|
||||||
@@ -49,16 +47,6 @@ public class DepositAccountInterestRateChartSlabs extends AbstractPersistableCus
|
|||||||
@OneToMany(mappedBy = "depositAccountInterestRateChartSlabs", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER)
|
@OneToMany(mappedBy = "depositAccountInterestRateChartSlabs", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER)
|
||||||
private Set<DepositAccountInterestIncentives> interestIncentives = new HashSet<>();
|
private Set<DepositAccountInterestIncentives> interestIncentives = new HashSet<>();
|
||||||
|
|
||||||
/**
|
|
||||||
* The senior-citizen premium for this slab's tenure band, as published on the CBS Base Interest Rates screen at the
|
|
||||||
* time this chart was set. Not part of {@link InterestRateChartSlabFields} because that embeddable is shared with the
|
|
||||||
* product-level {@code m_interest_rate_slab} table, which has no CBS senior-citizen concept; kept as a column of its
|
|
||||||
* own here instead, on the account-level table only. Null for a slab copied from a product chart (which never carries
|
|
||||||
* one) or for a slab that predates this column.
|
|
||||||
*/
|
|
||||||
@Column(name = "senior_citizen_rate", scale = 6, precision = 19)
|
|
||||||
private BigDecimal seniorCitizenRate;
|
|
||||||
|
|
||||||
protected DepositAccountInterestRateChartSlabs() {
|
protected DepositAccountInterestRateChartSlabs() {
|
||||||
//
|
//
|
||||||
}
|
}
|
||||||
@@ -71,10 +59,6 @@ public class DepositAccountInterestRateChartSlabs extends AbstractPersistableCus
|
|||||||
this.interestIncentives = interestIncentives;
|
this.interestIncentives = interestIncentives;
|
||||||
}
|
}
|
||||||
|
|
||||||
public BigDecimal getSeniorCitizenRate() {
|
|
||||||
return this.seniorCitizenRate;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setDepositAccountInterestRateChart(DepositAccountInterestRateChart depositAccountInterestRateChart) {
|
public void setDepositAccountInterestRateChart(DepositAccountInterestRateChart depositAccountInterestRateChart) {
|
||||||
this.depositAccountInterestRateChart = depositAccountInterestRateChart;
|
this.depositAccountInterestRateChart = depositAccountInterestRateChart;
|
||||||
}
|
}
|
||||||
@@ -87,19 +71,6 @@ public class DepositAccountInterestRateChartSlabs extends AbstractPersistableCus
|
|||||||
return this.slabFields;
|
return this.slabFields;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Builds a slab straight from its field values, for a chart taken from a source that is not a product
|
|
||||||
* {@link InterestRateChartSlab} -- specifically the CBS base interest-rate master, which is the bank's published rate
|
|
||||||
* card and carries no incentive rules.
|
|
||||||
*/
|
|
||||||
public static DepositAccountInterestRateChartSlabs fromFields(final InterestRateChartSlabFields slabFields,
|
|
||||||
final DepositAccountInterestRateChart depositAccountInterestRateChart, final BigDecimal seniorCitizenRate) {
|
|
||||||
final DepositAccountInterestRateChartSlabs slab = new DepositAccountInterestRateChartSlabs(slabFields,
|
|
||||||
depositAccountInterestRateChart, new HashSet<>());
|
|
||||||
slab.seniorCitizenRate = seniorCitizenRate;
|
|
||||||
return slab;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static DepositAccountInterestRateChartSlabs from(InterestRateChartSlab interestRateChartSlab,
|
public static DepositAccountInterestRateChartSlabs from(InterestRateChartSlab interestRateChartSlab,
|
||||||
DepositAccountInterestRateChart depositAccountInterestRateChart) {
|
DepositAccountInterestRateChart depositAccountInterestRateChart) {
|
||||||
InterestRateChartSlabFields slabFields = interestRateChartSlab.slabFields();
|
InterestRateChartSlabFields slabFields = interestRateChartSlab.slabFields();
|
||||||
|
|||||||
-59
@@ -89,49 +89,6 @@ public class DepositAccountTermAndPreClosure extends AbstractPersistableCustom<L
|
|||||||
@Column(name = "transfer_to_savings_account_id")
|
@Column(name = "transfer_to_savings_account_id")
|
||||||
private Long transferToSavingsAccountId;
|
private Long transferToSavingsAccountId;
|
||||||
|
|
||||||
// Set on the NEW account created by a "Re-Invest" maturity closure, pointing back at the account
|
|
||||||
// it was reinvested from. There is no separate "reinvested into" column on the OLD account -- that
|
|
||||||
// direction is derived by looking up whichever row has this account's id here (see
|
|
||||||
// DepositAccountReadPlatformServiceImpl), so the relationship needs tracking in only one place.
|
|
||||||
@Column(name = "reinvested_from_savings_id")
|
|
||||||
private Long reinvestedFromSavingsId;
|
|
||||||
|
|
||||||
public Long getReinvestedFromSavingsId() {
|
|
||||||
return this.reinvestedFromSavingsId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setReinvestedFromSavingsId(final Long reinvestedFromSavingsId) {
|
|
||||||
this.reinvestedFromSavingsId = reinvestedFromSavingsId;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The account_no of the FIRST account in a renewal chain, carried forward onto every account
|
|
||||||
// created by reinvesting it. Null on an account that has never been reinvested -- callers should
|
|
||||||
// fall back to that account's own account_no in that case (see FixedDepositAccount.reInvest()).
|
|
||||||
@Column(name = "fd_account_number", length = 20)
|
|
||||||
private String fdAccountNumber;
|
|
||||||
|
|
||||||
public String getFdAccountNumber() {
|
|
||||||
return this.fdAccountNumber;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setFdAccountNumber(final String fdAccountNumber) {
|
|
||||||
this.fdAccountNumber = fdAccountNumber;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Which renewal cycle of one logical FD this account represents: 0 = originally opened account,
|
|
||||||
// 1 = first renewal, and so on. Lets the UI present "one FD, many terms" over what is physically
|
|
||||||
// a chain of per-term account rows.
|
|
||||||
@Column(name = "renewal_cycle_no")
|
|
||||||
private Integer renewalCycleNo;
|
|
||||||
|
|
||||||
public Integer getRenewalCycleNo() {
|
|
||||||
return this.renewalCycleNo == null ? 0 : this.renewalCycleNo;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setRenewalCycleNo(final Integer renewalCycleNo) {
|
|
||||||
this.renewalCycleNo = renewalCycleNo;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected DepositAccountTermAndPreClosure() {
|
protected DepositAccountTermAndPreClosure() {
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -340,19 +297,6 @@ public class DepositAccountTermAndPreClosure extends AbstractPersistableCustom<L
|
|||||||
this.onAccountClosureType = onClosureType.getValue();
|
this.onAccountClosureType = onClosureType.getValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Copies the standing instructions -- maturity instruction, its payout target and the interest-transfer flag -- from
|
|
||||||
* the term being renewed.
|
|
||||||
* <p>
|
|
||||||
* These describe what the customer asked the deposit to do at every maturity, not a one-off choice for a single term,
|
|
||||||
* so a renewed deposit that lost them would mature once more and then sit idle instead of rolling over again.
|
|
||||||
*/
|
|
||||||
public void carryForwardStandingInstructionsFrom(final DepositAccountTermAndPreClosure previousTerm) {
|
|
||||||
this.onAccountClosureType = previousTerm.onAccountClosureType;
|
|
||||||
this.transferToSavingsAccountId = previousTerm.transferToSavingsAccountId;
|
|
||||||
this.transferInterestToLinkedAccount = previousTerm.transferInterestToLinkedAccount;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isReinvestOnClosure() {
|
public boolean isReinvestOnClosure() {
|
||||||
return DepositAccountOnClosureType.fromInt(this.onAccountClosureType).isReinvest();
|
return DepositAccountOnClosureType.fromInt(this.onAccountClosureType).isReinvest();
|
||||||
}
|
}
|
||||||
@@ -373,9 +317,6 @@ public class DepositAccountTermAndPreClosure extends AbstractPersistableCustom<L
|
|||||||
final LocalDate expectedFirstDepositOnDate = null;
|
final LocalDate expectedFirstDepositOnDate = null;
|
||||||
final Boolean transferInterestToLinkedAccount = false;
|
final Boolean transferInterestToLinkedAccount = false;
|
||||||
|
|
||||||
// Left blank here on purpose: this method is shared with Recurring Deposit renewal, so carrying
|
|
||||||
// standing instructions forward is done by the caller that wants it (see
|
|
||||||
// FixedDepositAccount#reInvest) rather than changed for both product types at once.
|
|
||||||
final DepositAccountOnClosureType accountOnClosureType = null;
|
final DepositAccountOnClosureType accountOnClosureType = null;
|
||||||
final Long transferToSavingsId = null;
|
final Long transferToSavingsId = null;
|
||||||
return DepositAccountTermAndPreClosure.createNew(preClosureDetail, depositTermDetail, account, actualDepositAmount, maturityAmount,
|
return DepositAccountTermAndPreClosure.createNew(preClosureDetail, depositTermDetail, account, actualDepositAmount, maturityAmount,
|
||||||
|
|||||||
-36
@@ -324,42 +324,6 @@ public class FixedDepositProduct extends SavingsProduct {
|
|||||||
return applicableChart;
|
return applicableChart;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* The rate chart the product currently offers for a deposit priced on the given date: a chart flagged active, that has
|
|
||||||
* come into force by that date and has not been superseded. Where more than one qualifies, the most recently effective
|
|
||||||
* wins.
|
|
||||||
* <p>
|
|
||||||
* Differs from {@link #applicableChart} in three ways that matter when pricing a renewal. That method ignores the
|
|
||||||
* active flag entirely; it treats an open-ended chart (no end date) as valid only up to today's business date, so a
|
|
||||||
* deposit renewing on a future maturity date finds no chart at all; and it returns the first match from an unordered
|
|
||||||
* set, so two charts covering the same date resolve arbitrarily.
|
|
||||||
* <p>
|
|
||||||
* Returns null when the product offers no active chart in force on that date.
|
|
||||||
*/
|
|
||||||
public InterestRateChart activeChart(final LocalDate target) {
|
|
||||||
if (this.charts == null || target == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
InterestRateChart activeChart = null;
|
|
||||||
for (final InterestRateChart chart : this.charts) {
|
|
||||||
if (!chart.isActive()) {
|
|
||||||
continue; // retired rate version
|
|
||||||
}
|
|
||||||
final LocalDate fromDate = chart.getFromDate();
|
|
||||||
final LocalDate endDate = chart.getEndDate();
|
|
||||||
if (fromDate == null || fromDate.isAfter(target)) {
|
|
||||||
continue; // not yet in force
|
|
||||||
}
|
|
||||||
if (endDate != null && endDate.isBefore(target)) {
|
|
||||||
continue; // already superseded
|
|
||||||
}
|
|
||||||
if (activeChart == null || fromDate.isAfter(activeChart.getFromDate())) {
|
|
||||||
activeChart = chart;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return activeChart;
|
|
||||||
}
|
|
||||||
|
|
||||||
public DepositProductTermAndPreClosure depositProductTermAndPreClosure() {
|
public DepositProductTermAndPreClosure depositProductTermAndPreClosure() {
|
||||||
return this.productTermAndPreClosure;
|
return this.productTermAndPreClosure;
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-33
@@ -605,29 +605,19 @@ public final class ProductPendingEditFieldMapper {
|
|||||||
|
|
||||||
// ---- GL array mappings (1:many, satellite table) -----------------------------------------------
|
// ---- GL array mappings (1:many, satellite table) -----------------------------------------------
|
||||||
|
|
||||||
// The GL-account field is named differently per mapping array in the product payload -- payment
|
|
||||||
// channels carry "fundSourceAccountId", fee/penalty arrays carry "incomeAccountId" (see
|
|
||||||
// AccountingConstants.LoanProductAccountingParams). Reading the wrong name leaves gl_account_id
|
|
||||||
// null, which the satellite table rejects, so each array declares its own field name here and the
|
|
||||||
// same name is written back when the pending edit is rebuilt into a product-update payload.
|
|
||||||
private static final String PAYMENT_CHANNEL_GL_FIELD = "fundSourceAccountId";
|
|
||||||
private static final String INCOME_ACCOUNT_GL_FIELD = "incomeAccountId";
|
|
||||||
|
|
||||||
public static List<ProductPendingEditGlMapping> extractGlArrayMappings(final JsonCommand command, final ProductPendingEdit edit) {
|
public static List<ProductPendingEditGlMapping> extractGlArrayMappings(final JsonCommand command, final ProductPendingEdit edit) {
|
||||||
final List<ProductPendingEditGlMapping> mappings = new java.util.ArrayList<>();
|
final List<ProductPendingEditGlMapping> mappings = new java.util.ArrayList<>();
|
||||||
boolean any = false;
|
boolean any = false;
|
||||||
any |= extractGlArray(command, edit, "paymentChannelToFundSourceMappings", ProductPendingEditGlMapping.TYPE_PAYMENT_CHANNEL,
|
any |= extractGlArray(command, edit, "paymentChannelToFundSourceMappings", ProductPendingEditGlMapping.TYPE_PAYMENT_CHANNEL,
|
||||||
"paymentTypeId", PAYMENT_CHANNEL_GL_FIELD, mappings);
|
"paymentTypeId", mappings);
|
||||||
any |= extractGlArray(command, edit, "feeToIncomeAccountMappings", ProductPendingEditGlMapping.TYPE_FEE, "chargeId",
|
any |= extractGlArray(command, edit, "feeToIncomeAccountMappings", ProductPendingEditGlMapping.TYPE_FEE, "chargeId", mappings);
|
||||||
INCOME_ACCOUNT_GL_FIELD, mappings);
|
|
||||||
any |= extractGlArray(command, edit, "penaltyToIncomeAccountMappings", ProductPendingEditGlMapping.TYPE_PENALTY, "chargeId",
|
any |= extractGlArray(command, edit, "penaltyToIncomeAccountMappings", ProductPendingEditGlMapping.TYPE_PENALTY, "chargeId",
|
||||||
INCOME_ACCOUNT_GL_FIELD, mappings);
|
mappings);
|
||||||
return any ? mappings : null;
|
return any ? mappings : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean extractGlArray(final JsonCommand command, final ProductPendingEdit edit, final String paramName,
|
private static boolean extractGlArray(final JsonCommand command, final ProductPendingEdit edit, final String paramName,
|
||||||
final String mappingType, final String idFieldName, final String glFieldName,
|
final String mappingType, final String idFieldName, final List<ProductPendingEditGlMapping> out) {
|
||||||
final List<ProductPendingEditGlMapping> out) {
|
|
||||||
if (!command.hasParameter(paramName)) {
|
if (!command.hasParameter(paramName)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -635,13 +625,8 @@ public final class ProductPendingEditFieldMapper {
|
|||||||
if (raw != null && raw.isJsonArray()) {
|
if (raw != null && raw.isJsonArray()) {
|
||||||
for (final com.google.gson.JsonElement el : raw.getAsJsonArray()) {
|
for (final com.google.gson.JsonElement el : raw.getAsJsonArray()) {
|
||||||
final JsonObject o = el.getAsJsonObject();
|
final JsonObject o = el.getAsJsonObject();
|
||||||
final Long idFieldValue = longOrNull(o, idFieldName);
|
final Long idFieldValue = o.has(idFieldName) ? o.get(idFieldName).getAsLong() : null;
|
||||||
final Long glAccountId = longOrNull(o, glFieldName);
|
final Long glAccountId = o.has("glAccountId") ? o.get("glAccountId").getAsLong() : null;
|
||||||
// A mapping is only meaningful with both halves present; keeping a half-filled row
|
|
||||||
// would fail the satellite table's NOT NULL constraint and abort the whole edit.
|
|
||||||
if (idFieldValue == null || glAccountId == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
final Long paymentTypeId = "paymentTypeId".equals(idFieldName) ? idFieldValue : null;
|
final Long paymentTypeId = "paymentTypeId".equals(idFieldName) ? idFieldValue : null;
|
||||||
final Long chargeId = "chargeId".equals(idFieldName) ? idFieldValue : null;
|
final Long chargeId = "chargeId".equals(idFieldName) ? idFieldValue : null;
|
||||||
out.add(new ProductPendingEditGlMapping(edit, mappingType, paymentTypeId, chargeId, glAccountId));
|
out.add(new ProductPendingEditGlMapping(edit, mappingType, paymentTypeId, chargeId, glAccountId));
|
||||||
@@ -650,22 +635,15 @@ public final class ProductPendingEditFieldMapper {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Long longOrNull(final JsonObject o, final String fieldName) {
|
|
||||||
final com.google.gson.JsonElement el = o.get(fieldName);
|
|
||||||
return el == null || el.isJsonNull() ? null : el.getAsLong();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void writeGlArrayMappingsToJson(final JsonObject root, final List<ProductPendingEditGlMapping> mappings) {
|
public static void writeGlArrayMappingsToJson(final JsonObject root, final List<ProductPendingEditGlMapping> mappings) {
|
||||||
writeGlArrayGroup(root, mappings, ProductPendingEditGlMapping.TYPE_PAYMENT_CHANNEL, "paymentChannelToFundSourceMappings",
|
writeGlArrayGroup(root, mappings, ProductPendingEditGlMapping.TYPE_PAYMENT_CHANNEL, "paymentChannelToFundSourceMappings",
|
||||||
"paymentTypeId", PAYMENT_CHANNEL_GL_FIELD);
|
"paymentTypeId");
|
||||||
writeGlArrayGroup(root, mappings, ProductPendingEditGlMapping.TYPE_FEE, "feeToIncomeAccountMappings", "chargeId",
|
writeGlArrayGroup(root, mappings, ProductPendingEditGlMapping.TYPE_FEE, "feeToIncomeAccountMappings", "chargeId");
|
||||||
INCOME_ACCOUNT_GL_FIELD);
|
writeGlArrayGroup(root, mappings, ProductPendingEditGlMapping.TYPE_PENALTY, "penaltyToIncomeAccountMappings", "chargeId");
|
||||||
writeGlArrayGroup(root, mappings, ProductPendingEditGlMapping.TYPE_PENALTY, "penaltyToIncomeAccountMappings", "chargeId",
|
|
||||||
INCOME_ACCOUNT_GL_FIELD);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void writeGlArrayGroup(final JsonObject root, final List<ProductPendingEditGlMapping> mappings, final String type,
|
private static void writeGlArrayGroup(final JsonObject root, final List<ProductPendingEditGlMapping> mappings, final String type,
|
||||||
final String paramName, final String idFieldName, final String glFieldName) {
|
final String paramName, final String idFieldName) {
|
||||||
final JsonArray array = new JsonArray();
|
final JsonArray array = new JsonArray();
|
||||||
for (final ProductPendingEditGlMapping mapping : mappings) {
|
for (final ProductPendingEditGlMapping mapping : mappings) {
|
||||||
if (!type.equals(mapping.getMappingType())) {
|
if (!type.equals(mapping.getMappingType())) {
|
||||||
@@ -674,7 +652,7 @@ public final class ProductPendingEditFieldMapper {
|
|||||||
final JsonObject entry = new JsonObject();
|
final JsonObject entry = new JsonObject();
|
||||||
final Long idFieldValue = "paymentTypeId".equals(idFieldName) ? mapping.getPaymentTypeId() : mapping.getChargeId();
|
final Long idFieldValue = "paymentTypeId".equals(idFieldName) ? mapping.getPaymentTypeId() : mapping.getChargeId();
|
||||||
entry.addProperty(idFieldName, idFieldValue);
|
entry.addProperty(idFieldName, idFieldValue);
|
||||||
entry.addProperty(glFieldName, mapping.getGlAccountId());
|
entry.addProperty("glAccountId", mapping.getGlAccountId());
|
||||||
array.add(entry);
|
array.add(entry);
|
||||||
}
|
}
|
||||||
root.add(paramName, array);
|
root.add(paramName, array);
|
||||||
|
|||||||
+3
-35
@@ -828,28 +828,6 @@ public class SavingsAccount extends AbstractAuditableWithUTCDateTimeCustom<Long>
|
|||||||
return savingsTransaction;
|
return savingsTransaction;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* The last non-reversed transaction dated strictly BEFORE {@code date}, or null when none exists.
|
|
||||||
* <p>
|
|
||||||
* Distinct from {@link #findLastTransaction(LocalDate)}, which matches the date exactly. Used to seed the opening
|
|
||||||
* balance of an interest calculation that starts at a pivot date: a transaction dated ON that date falls inside the
|
|
||||||
* first posting period and is applied there, so seeding from it would count it twice, while requiring an exact match
|
|
||||||
* would leave the opening balance at zero on any pivot date that happens to carry no transaction.
|
|
||||||
* <p>
|
|
||||||
* Relies on {@code transactions} being ordered ascending by date, as its {@code @OrderBy} declares, so the final
|
|
||||||
* match is the latest one.
|
|
||||||
*/
|
|
||||||
protected SavingsAccountTransaction findLastTransactionBefore(final LocalDate date) {
|
|
||||||
SavingsAccountTransaction candidate = null;
|
|
||||||
for (final SavingsAccountTransaction transaction : getTransactions()) {
|
|
||||||
if (transaction.isNotReversed() && !transaction.isReversalTransaction()
|
|
||||||
&& DateUtils.isBefore(transaction.getTransactionDate(), date)) {
|
|
||||||
candidate = transaction;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return candidate;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected SavingsAccountTransaction findLastFilteredTransactionWithPivotConfig(final LocalDate date) {
|
protected SavingsAccountTransaction findLastFilteredTransactionWithPivotConfig(final LocalDate date) {
|
||||||
SavingsAccountTransaction savingsTransaction = null;
|
SavingsAccountTransaction savingsTransaction = null;
|
||||||
List<SavingsAccountTransaction> trans = getSavingsAccountTransactionsWithPivotConfig();
|
List<SavingsAccountTransaction> trans = getSavingsAccountTransactionsWithPivotConfig();
|
||||||
@@ -943,23 +921,13 @@ public class SavingsAccount extends AbstractAuditableWithUTCDateTimeCustom<Long>
|
|||||||
if (backdatedTxnsAllowedTill) {
|
if (backdatedTxnsAllowedTill) {
|
||||||
transaction = findLastFilteredTransactionWithPivotConfig(startInterestCalculationDate);
|
transaction = findLastFilteredTransactionWithPivotConfig(startInterestCalculationDate);
|
||||||
} else {
|
} else {
|
||||||
// Strictly before the pivot, not on it: anything dated on the pivot date belongs to the
|
transaction = findLastTransaction(startInterestCalculationDate);
|
||||||
// first posting period and is applied there, so seeding from it would apply it twice --
|
|
||||||
// and an exact-date match would leave this at zero whenever the pivot date carries no
|
|
||||||
// transaction of its own, silently accruing the whole term on a zero balance.
|
|
||||||
transaction = findLastTransactionBefore(startInterestCalculationDate);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (transaction == null) {
|
if (transaction == null) {
|
||||||
periodStartingBalance = Money.zero(this.currency);
|
periodStartingBalance = Money.zero(this.currency);
|
||||||
} else if (backdatedTxnsAllowedTill) {
|
|
||||||
periodStartingBalance = Money.of(this.currency, this.summary.getRunningBalanceOnPivotDate());
|
|
||||||
} else {
|
} else {
|
||||||
// summary.runningBalanceOnPivotDate is @Transient and only ever populated by the
|
periodStartingBalance = Money.of(this.currency, this.summary.getRunningBalanceOnPivotDate());
|
||||||
// assembler on the backdated-transactions path, so reading it here returned zero and
|
|
||||||
// silently wiped the opening balance. The balance carried by the last transaction
|
|
||||||
// before the start date is the same figure, and is always present.
|
|
||||||
periodStartingBalance = transaction.getRunningBalance(this.currency);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
periodStartingBalance = Money.zero(this.currency);
|
periodStartingBalance = Money.zero(this.currency);
|
||||||
@@ -3172,7 +3140,7 @@ public class SavingsAccount extends AbstractAuditableWithUTCDateTimeCustom<Long>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected LocalDate calculateDateAccountIsLockedUntil(final LocalDate activationLocalDate) {
|
private LocalDate calculateDateAccountIsLockedUntil(final LocalDate activationLocalDate) {
|
||||||
|
|
||||||
LocalDate lockedInUntilLocalDate = null;
|
LocalDate lockedInUntilLocalDate = null;
|
||||||
final PeriodFrequencyType lockinPeriodFrequencyType = PeriodFrequencyType.fromInt(this.lockinPeriodFrequencyType);
|
final PeriodFrequencyType lockinPeriodFrequencyType = PeriodFrequencyType.fromInt(this.lockinPeriodFrequencyType);
|
||||||
|
|||||||
-5
@@ -82,11 +82,6 @@ public class SavingsAccountRepositoryWrapper {
|
|||||||
return account;
|
return account;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional(readOnly = true)
|
|
||||||
public boolean existsWithDepositAccountType(final Long savingsId, final DepositAccountType depositAccountType) {
|
|
||||||
return this.repository.findByIdAndDepositAccountType(savingsId, depositAccountType.getValue()) != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
public List<SavingsAccount> findSavingAccountByClientId(@Param("clientId") Long clientId) {
|
public List<SavingsAccount> findSavingAccountByClientId(@Param("clientId") Long clientId) {
|
||||||
List<SavingsAccount> accounts = this.repository.findSavingAccountByClientId(clientId);
|
List<SavingsAccount> accounts = this.repository.findSavingAccountByClientId(clientId);
|
||||||
|
|||||||
-2
@@ -61,8 +61,6 @@ public interface DepositAccountWritePlatformService {
|
|||||||
|
|
||||||
CommandProcessingResult prematureCloseRDAccount(Long savingsId, JsonCommand command);
|
CommandProcessingResult prematureCloseRDAccount(Long savingsId, JsonCommand command);
|
||||||
|
|
||||||
CommandProcessingResult processFdPayment(Long savingsId, JsonCommand command);
|
|
||||||
|
|
||||||
SavingsAccountTransaction initiateSavingsTransfer(Long accountId, LocalDate transferDate, DepositAccountType depositAccountType);
|
SavingsAccountTransaction initiateSavingsTransfer(Long accountId, LocalDate transferDate, DepositAccountType depositAccountType);
|
||||||
|
|
||||||
SavingsAccountTransaction withdrawSavingsTransfer(Long accountId, LocalDate transferDate, DepositAccountType depositAccountType);
|
SavingsAccountTransaction withdrawSavingsTransfer(Long accountId, LocalDate transferDate, DepositAccountType depositAccountType);
|
||||||
|
|||||||
+3
-3
@@ -172,7 +172,7 @@ public class FixedDepositTest extends IntegrationTest {
|
|||||||
moneyHelperStatic = Mockito.mockStatic(MoneyHelper.class);
|
moneyHelperStatic = Mockito.mockStatic(MoneyHelper.class);
|
||||||
moneyHelperStatic.when(() -> MoneyHelper.getMathContext()).thenReturn(new MathContext(12, RoundingMode.UP));
|
moneyHelperStatic.when(() -> MoneyHelper.getMathContext()).thenReturn(new MathContext(12, RoundingMode.UP));
|
||||||
fixedDepositAccountInterestCalculationServiceImpl = new FixedDepositAccountInterestCalculationServiceImpl(
|
fixedDepositAccountInterestCalculationServiceImpl = new FixedDepositAccountInterestCalculationServiceImpl(
|
||||||
new DepositAccountDataValidator(new FromJsonHelper(), null, null), new FromJsonHelper());
|
new DepositAccountDataValidator(new FromJsonHelper(), null), new FromJsonHelper());
|
||||||
try {
|
try {
|
||||||
HashMap h = fixedDepositAccountInterestCalculationServiceImpl
|
HashMap h = fixedDepositAccountInterestCalculationServiceImpl
|
||||||
.calculateInterest(new JsonQuery(apiRequestBodyAsJson, element, new FromJsonHelper()));
|
.calculateInterest(new JsonQuery(apiRequestBodyAsJson, element, new FromJsonHelper()));
|
||||||
@@ -198,7 +198,7 @@ public class FixedDepositTest extends IntegrationTest {
|
|||||||
moneyHelperStatic = Mockito.mockStatic(MoneyHelper.class);
|
moneyHelperStatic = Mockito.mockStatic(MoneyHelper.class);
|
||||||
moneyHelperStatic.when(() -> MoneyHelper.getMathContext()).thenReturn(new MathContext(12, RoundingMode.UP));
|
moneyHelperStatic.when(() -> MoneyHelper.getMathContext()).thenReturn(new MathContext(12, RoundingMode.UP));
|
||||||
fixedDepositAccountInterestCalculationServiceImpl = new FixedDepositAccountInterestCalculationServiceImpl(
|
fixedDepositAccountInterestCalculationServiceImpl = new FixedDepositAccountInterestCalculationServiceImpl(
|
||||||
new DepositAccountDataValidator(new FromJsonHelper(), null, null), new FromJsonHelper());
|
new DepositAccountDataValidator(new FromJsonHelper(), null), new FromJsonHelper());
|
||||||
try {
|
try {
|
||||||
HashMap h = fixedDepositAccountInterestCalculationServiceImpl
|
HashMap h = fixedDepositAccountInterestCalculationServiceImpl
|
||||||
.calculateInterest(new JsonQuery(apiRequestBodyAsJson, element, new FromJsonHelper()));
|
.calculateInterest(new JsonQuery(apiRequestBodyAsJson, element, new FromJsonHelper()));
|
||||||
@@ -224,7 +224,7 @@ public class FixedDepositTest extends IntegrationTest {
|
|||||||
moneyHelperStatic = Mockito.mockStatic(MoneyHelper.class);
|
moneyHelperStatic = Mockito.mockStatic(MoneyHelper.class);
|
||||||
moneyHelperStatic.when(() -> MoneyHelper.getMathContext()).thenReturn(new MathContext(12, RoundingMode.UP));
|
moneyHelperStatic.when(() -> MoneyHelper.getMathContext()).thenReturn(new MathContext(12, RoundingMode.UP));
|
||||||
fixedDepositAccountInterestCalculationServiceImpl = new FixedDepositAccountInterestCalculationServiceImpl(
|
fixedDepositAccountInterestCalculationServiceImpl = new FixedDepositAccountInterestCalculationServiceImpl(
|
||||||
new DepositAccountDataValidator(new FromJsonHelper(), null, null), new FromJsonHelper());
|
new DepositAccountDataValidator(new FromJsonHelper(), null), new FromJsonHelper());
|
||||||
BigDecimal expectedResult = new BigDecimal("10506.250000");
|
BigDecimal expectedResult = new BigDecimal("10506.250000");
|
||||||
BigDecimal actualResult = new BigDecimal(fixedDepositAccountInterestCalculationServiceImpl
|
BigDecimal actualResult = new BigDecimal(fixedDepositAccountInterestCalculationServiceImpl
|
||||||
.calculateInterest(new JsonQuery(apiRequestBodyAsJson, element, new FromJsonHelper())).get("maturityAmount").toString());
|
.calculateInterest(new JsonQuery(apiRequestBodyAsJson, element, new FromJsonHelper())).get("maturityAmount").toString());
|
||||||
|
|||||||
Reference in New Issue
Block a user