Merge remote-tracking branch 'origin/dev-latest' into dev-latest
Fineract E2E Tests / E2E Tests (Shard 5 of 10) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Canceled after 0s
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (push) Canceled after 0s
Fineract Documentation build / build (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 10 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 2 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 3 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 4 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 7 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 8 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 9 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 6 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Canceled after 0s
Fineract Liquibase Only mode - PostgreSQL / test (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with ActiveMQ (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with Kafka (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 5 of 10) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Canceled after 0s
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (push) Canceled after 0s
Fineract Documentation build / build (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 10 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 2 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 3 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 4 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 7 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 8 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 9 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 6 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Canceled after 0s
Fineract Liquibase Only mode - PostgreSQL / test (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with ActiveMQ (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with Kafka (push) Canceled after 0s
This commit is contained in:
+2
@@ -477,6 +477,8 @@ public class ProductToGLAccountMappingReadPlatformServiceImpl implements Product
|
|||||||
accountMappingDetails.put(SavingProductAccountingDataParams.SAVINGS_CONTROL.getValue(), glAccountData);
|
accountMappingDetails.put(SavingProductAccountingDataParams.SAVINGS_CONTROL.getValue(), glAccountData);
|
||||||
} else if (glAccountForSavings.equals(CashAccountsForSavings.TRANSFERS_SUSPENSE)) {
|
} else if (glAccountForSavings.equals(CashAccountsForSavings.TRANSFERS_SUSPENSE)) {
|
||||||
accountMappingDetails.put(SavingProductAccountingDataParams.TRANSFERS_SUSPENSE.getValue(), glAccountData);
|
accountMappingDetails.put(SavingProductAccountingDataParams.TRANSFERS_SUSPENSE.getValue(), glAccountData);
|
||||||
|
} else if (glAccountForSavings.equals(CashAccountsForSavings.INTEREST_PAYABLE)) {
|
||||||
|
accountMappingDetails.put(SavingProductAccountingDataParams.INTEREST_PAYABLE.getValue(), glAccountData);
|
||||||
// Income
|
// Income
|
||||||
} else if (glAccountForSavings.equals(CashAccountsForSavings.INCOME_FROM_FEES)) {
|
} else if (glAccountForSavings.equals(CashAccountsForSavings.INCOME_FROM_FEES)) {
|
||||||
accountMappingDetails.put(SavingProductAccountingDataParams.INCOME_FROM_FEES.getValue(), glAccountData);
|
accountMappingDetails.put(SavingProductAccountingDataParams.INCOME_FROM_FEES.getValue(), glAccountData);
|
||||||
|
|||||||
+1
@@ -270,6 +270,7 @@ public final class AccountingConstants {
|
|||||||
INCOME_FROM_INTEREST(12), //
|
INCOME_FROM_INTEREST(12), //
|
||||||
LOSSES_WRITTEN_OFF(13), //
|
LOSSES_WRITTEN_OFF(13), //
|
||||||
ESCHEAT_LIABILITY(14), //
|
ESCHEAT_LIABILITY(14), //
|
||||||
|
INTEREST_PAYABLE(17), //
|
||||||
/**
|
/**
|
||||||
* Where the paise dropped from a cash closure are absorbed.
|
* Where the paise dropped from a cash closure are absorbed.
|
||||||
*
|
*
|
||||||
|
|||||||
+10
-20
@@ -137,7 +137,16 @@ 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 || isSuperUserBypassAllowed(commandSource, user)) {
|
// isApprovedByChecker is only ever true here for a genuine checker approval
|
||||||
|
// (PortfolioCommandSourceWritePlatformServiceImpl.approveEntry(), which has already run
|
||||||
|
// validateMakerCheckerTransaction() and confirmed the approver is not the maker) or a user
|
||||||
|
// editing their own account details (CommandWrapper.isChangeOfOwnUserDetails() - maker-checker
|
||||||
|
// genuinely does not apply there). On the ORIGINAL submission this is always false and `user`
|
||||||
|
// here is always the maker themselves, so there is deliberately no CHECKER_SUPER_USER (or any
|
||||||
|
// other) bypass at this point: holding CHECKER_SUPER_USER lets a user approve someone ELSE's
|
||||||
|
// request without needing the specific <TASK>_CHECKER permission, it must never let a maker
|
||||||
|
// auto-approve their own submission just by holding it.
|
||||||
|
if (isApprovedByChecker) {
|
||||||
commandSource.markAsChecked(user);
|
commandSource.markAsChecked(user);
|
||||||
} else {
|
} else {
|
||||||
if (commandSource.isSanitized()) {
|
if (commandSource.isSanitized()) {
|
||||||
@@ -151,25 +160,6 @@ 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;
|
||||||
|
|||||||
+20
-8
@@ -48,6 +48,12 @@ public class PortfolioCommandSourceWritePlatformServiceImpl implements Portfolio
|
|||||||
private final FromJsonHelper fromApiJsonHelper;
|
private final FromJsonHelper fromApiJsonHelper;
|
||||||
private final CommandProcessingService processAndLogCommandService;
|
private final CommandProcessingService processAndLogCommandService;
|
||||||
private final SchedulerJobRunnerReadService schedulerJobRunnerReadService;
|
private final SchedulerJobRunnerReadService schedulerJobRunnerReadService;
|
||||||
|
// No longer consulted by validateMakerCheckerTransaction() below: the maker-identity rule (a user
|
||||||
|
// can never approve their own submission) is now unconditional and isSameMakerCheckerEnabled() no
|
||||||
|
// longer overrides it. Left injected rather than removed - enable-same-maker-checker is still a
|
||||||
|
// live, independently-configurable global switch (System -> Configurations); deleting this field,
|
||||||
|
// and by extension deciding that switch should stop existing altogether, is a separate decision
|
||||||
|
// from closing this specific self-approval loophole.
|
||||||
private final ConfigurationDomainService configurationService;
|
private final ConfigurationDomainService configurationService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -123,14 +129,20 @@ public class PortfolioCommandSourceWritePlatformServiceImpl implements Portfolio
|
|||||||
AppUser appUser = this.context.authenticatedUser();
|
AppUser appUser = this.context.authenticatedUser();
|
||||||
String permissionCode = commandSource.getPermissionCode();
|
String permissionCode = commandSource.getPermissionCode();
|
||||||
appUser.validateHasCheckerPermissionTo(permissionCode);
|
appUser.validateHasCheckerPermissionTo(permissionCode);
|
||||||
if (!configurationService.isSameMakerCheckerEnabled() && !appUser.isCheckerSuperUser()) {
|
// Maker-identity rule: the user who submitted a command may never be the one who approves
|
||||||
AppUser maker = commandSource.getMaker();
|
// (or rejects) it - this is a fixed platform rule, not overridable by CHECKER_SUPER_USER (that
|
||||||
if (maker == null) {
|
// permission still lets its holder action a DIFFERENT user's request without needing the
|
||||||
throw new UnsupportedCommandException(permissionCode, "Maker user is missing.");
|
// specific <TASK>_CHECKER permission - see validateHasCheckerPermissionTo() above - it just no
|
||||||
}
|
// longer lets them action their own) and not overridable by the enable-same-maker-checker
|
||||||
if (Objects.equals(appUser.getId(), maker.getId())) {
|
// global switch (isSameMakerCheckerEnabled() is deliberately no longer consulted here; that
|
||||||
throw new UnsupportedCommandException(permissionCode, "Can not be checked by the same user.");
|
// configuration is left in place and unused for this decision rather than removed, since
|
||||||
}
|
// deleting a still-configurable global setting is a separate decision from this one).
|
||||||
|
AppUser maker = commandSource.getMaker();
|
||||||
|
if (maker == null) {
|
||||||
|
throw new UnsupportedCommandException(permissionCode, "Maker user is missing.");
|
||||||
|
}
|
||||||
|
if (Objects.equals(appUser.getId(), maker.getId())) {
|
||||||
|
throw new UnsupportedCommandException(permissionCode, "Can not be checked by the same user.");
|
||||||
}
|
}
|
||||||
return commandSource;
|
return commandSource;
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-1
@@ -32,7 +32,8 @@ public enum DepositAccountOnClosureType {
|
|||||||
TRANSFER_TO_SAVINGS(200, "depositAccountClosureType.transferToSavings"), //
|
TRANSFER_TO_SAVINGS(200, "depositAccountClosureType.transferToSavings"), //
|
||||||
REINVEST_PRINCIPAL_AND_INTEREST(300, "depositAccountClosureType.reinvestPrincipalAndInterest"), //
|
REINVEST_PRINCIPAL_AND_INTEREST(300, "depositAccountClosureType.reinvestPrincipalAndInterest"), //
|
||||||
REINVEST_PRINCIPAL_ONLY(400, "depositAccountClosureType.reinvestPrincipalOnly"), //
|
REINVEST_PRINCIPAL_ONLY(400, "depositAccountClosureType.reinvestPrincipalOnly"), //
|
||||||
PARTIAL_REINVEST(500, "depositAccountClosureType.partialReinvest"); //
|
PARTIAL_REINVEST(500, "depositAccountClosureType.partialReinvest"), //
|
||||||
|
TRANSFER_TO_GL(600, "depositAccountClosureType.transferToGl"); //
|
||||||
|
|
||||||
private final Integer value;
|
private final Integer value;
|
||||||
private final String code;
|
private final String code;
|
||||||
@@ -53,6 +54,7 @@ public enum DepositAccountOnClosureType {
|
|||||||
case 300 -> REINVEST_PRINCIPAL_AND_INTEREST;
|
case 300 -> REINVEST_PRINCIPAL_AND_INTEREST;
|
||||||
case 400 -> REINVEST_PRINCIPAL_ONLY;
|
case 400 -> REINVEST_PRINCIPAL_ONLY;
|
||||||
case 500 -> PARTIAL_REINVEST;
|
case 500 -> PARTIAL_REINVEST;
|
||||||
|
case 600 -> TRANSFER_TO_GL;
|
||||||
default -> INVALID;
|
default -> INVALID;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -91,6 +93,17 @@ public enum DepositAccountOnClosureType {
|
|||||||
return this.equals(PARTIAL_REINVEST);
|
return this.equals(PARTIAL_REINVEST);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pays the closure proceeds into a GL account the maker picks, instead of a savings account or cash.
|
||||||
|
* Only the fixed-deposit premature-closure handler implements it -- see
|
||||||
|
* DepositAccountDomainServiceJpa#handleFDAccountPreMatureClosure; every other closure path rejects it
|
||||||
|
* in DepositAccountTransactionDataValidator#validateClosing rather than falling through to the
|
||||||
|
* terminal "pay the balance out in cash" branch.
|
||||||
|
*/
|
||||||
|
public boolean isTransferToGl() {
|
||||||
|
return this.equals(TRANSFER_TO_GL);
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: do we really need this?!?
|
// TODO: do we really need this?!?
|
||||||
public static Object[] integerValues() {
|
public static Object[] integerValues() {
|
||||||
return Arrays.stream(values()).filter(value -> !INVALID.equals(value)).map(value -> value.value).toList().toArray();
|
return Arrays.stream(values()).filter(value -> !INVALID.equals(value)).map(value -> value.value).toList().toArray();
|
||||||
|
|||||||
+8
-1
@@ -212,6 +212,9 @@ public final class DepositsApiConstants {
|
|||||||
public static final String onAccountClosureIdParamName = "onAccountClosureId";
|
public static final String onAccountClosureIdParamName = "onAccountClosureId";
|
||||||
public static final String transferDescriptionParamName = "transferDescription";
|
public static final String transferDescriptionParamName = "transferDescription";
|
||||||
public static final String toSavingsAccountIdParamName = "toSavingsAccountId";
|
public static final String toSavingsAccountIdParamName = "toSavingsAccountId";
|
||||||
|
// Destination GL account for DepositAccountOnClosureType.TRANSFER_TO_GL -- the closure proceeds are
|
||||||
|
// credited straight to this GL instead of a savings account or cash.
|
||||||
|
public static final String toGlAccountIdParamName = "toGlAccountId";
|
||||||
public static final String reinvestAmountParamName = "reinvestAmount";
|
public static final String reinvestAmountParamName = "reinvestAmount";
|
||||||
// Lets a maker waive the product's configured pre-closure penalty for one premature closure --
|
// Lets a maker waive the product's configured pre-closure penalty for one premature closure --
|
||||||
// see FixedDepositAccount#waivePreClosurePenaltyForThisRequest for why this is a transient
|
// see FixedDepositAccount#waivePreClosurePenaltyForThisRequest for why this is a transient
|
||||||
@@ -249,7 +252,7 @@ public final class DepositsApiConstants {
|
|||||||
SavingProductAccountingParams.PENALTY_INCOME_ACCOUNT_MAPPING.getValue(),
|
SavingProductAccountingParams.PENALTY_INCOME_ACCOUNT_MAPPING.getValue(),
|
||||||
SavingProductAccountingParams.INTEREST_PAYABLE.getValue(), SavingProductAccountingParams.PENALTIES_RECEIVABLE.getValue(),
|
SavingProductAccountingParams.INTEREST_PAYABLE.getValue(), SavingProductAccountingParams.PENALTIES_RECEIVABLE.getValue(),
|
||||||
SavingProductAccountingParams.FEES_RECEIVABLE.getValue(), chartsParamName, SavingsApiConstants.withHoldTaxParamName,
|
SavingProductAccountingParams.FEES_RECEIVABLE.getValue(), chartsParamName, SavingsApiConstants.withHoldTaxParamName,
|
||||||
SavingsApiConstants.taxGroupIdParamName));
|
SavingsApiConstants.taxGroupIdParamName, SavingsApiConstants.otherGlMappingsParamName));
|
||||||
|
|
||||||
private static final Set<String> PRECLOSURE_REQUEST_DATA_PARAMETERS = new HashSet<>(
|
private static final Set<String> PRECLOSURE_REQUEST_DATA_PARAMETERS = new HashSet<>(
|
||||||
Arrays.asList(preClosurePenalApplicableParamName, preClosurePenalInterestParamName, preClosurePenalInterestOnTypeIdParamName));
|
Arrays.asList(preClosurePenalApplicableParamName, preClosurePenalInterestParamName, preClosurePenalInterestOnTypeIdParamName));
|
||||||
@@ -387,6 +390,8 @@ public final class DepositsApiConstants {
|
|||||||
fixedDepositRequestData.add("paymentChannelToFundSourceMappings");
|
fixedDepositRequestData.add("paymentChannelToFundSourceMappings");
|
||||||
fixedDepositRequestData.add("feeToIncomeAccountMappings");
|
fixedDepositRequestData.add("feeToIncomeAccountMappings");
|
||||||
fixedDepositRequestData.add("penaltyToIncomeAccountMappings");
|
fixedDepositRequestData.add("penaltyToIncomeAccountMappings");
|
||||||
|
fixedDepositRequestData.add(SavingsApiConstants.otherGlMappingsParamName);
|
||||||
|
fixedDepositRequestData.add("otherGlMappingsMap");
|
||||||
return fixedDepositRequestData;
|
return fixedDepositRequestData;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -498,6 +503,8 @@ public final class DepositsApiConstants {
|
|||||||
recurringDepositRequestData.add("paymentChannelToFundSourceMappings");
|
recurringDepositRequestData.add("paymentChannelToFundSourceMappings");
|
||||||
recurringDepositRequestData.add("feeToIncomeAccountMappings");
|
recurringDepositRequestData.add("feeToIncomeAccountMappings");
|
||||||
recurringDepositRequestData.add("penaltyToIncomeAccountMappings");
|
recurringDepositRequestData.add("penaltyToIncomeAccountMappings");
|
||||||
|
recurringDepositRequestData.add(SavingsApiConstants.otherGlMappingsParamName);
|
||||||
|
recurringDepositRequestData.add("otherGlMappingsMap");
|
||||||
return recurringDepositRequestData;
|
return recurringDepositRequestData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+4
@@ -801,6 +801,10 @@ public final class SavingsEnumerations {
|
|||||||
optionData = new EnumOptionData(DepositAccountOnClosureType.PARTIAL_REINVEST.getValue().longValue(),
|
optionData = new EnumOptionData(DepositAccountOnClosureType.PARTIAL_REINVEST.getValue().longValue(),
|
||||||
DepositAccountOnClosureType.PARTIAL_REINVEST.getCode(), "Partial Re-Invest");
|
DepositAccountOnClosureType.PARTIAL_REINVEST.getCode(), "Partial Re-Invest");
|
||||||
break;
|
break;
|
||||||
|
case TRANSFER_TO_GL:
|
||||||
|
optionData = new EnumOptionData(DepositAccountOnClosureType.TRANSFER_TO_GL.getValue().longValue(),
|
||||||
|
DepositAccountOnClosureType.TRANSFER_TO_GL.getCode(), "Transfer to GL Account");
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
return optionData;
|
return optionData;
|
||||||
}
|
}
|
||||||
|
|||||||
+61
-41
@@ -27,6 +27,7 @@ import java.util.Map;
|
|||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.apache.fineract.accounting.closure.domain.GLClosure;
|
import org.apache.fineract.accounting.closure.domain.GLClosure;
|
||||||
import org.apache.fineract.accounting.closure.domain.GLClosureRepository;
|
import org.apache.fineract.accounting.closure.domain.GLClosureRepository;
|
||||||
@@ -83,6 +84,7 @@ import org.apache.fineract.portfolio.shareaccounts.data.ShareAccountTransactionE
|
|||||||
import org.springframework.dao.DataAccessException;
|
import org.springframework.dao.DataAccessException;
|
||||||
import org.springframework.jdbc.core.JdbcTemplate;
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class AccountingProcessorHelper {
|
public class AccountingProcessorHelper {
|
||||||
|
|
||||||
@@ -662,16 +664,25 @@ public class AccountingProcessorHelper {
|
|||||||
final GLAccount payableAccount = Optional
|
final GLAccount payableAccount = Optional
|
||||||
.ofNullable(optionalLinkedGLAccountForSavingsProduct(savingsProductId,
|
.ofNullable(optionalLinkedGLAccountForSavingsProduct(savingsProductId,
|
||||||
AccrualAccountsForSavings.INTEREST_PAYABLE.getValue(), paymentTypeId))
|
AccrualAccountsForSavings.INTEREST_PAYABLE.getValue(), paymentTypeId))
|
||||||
.or(() -> Optional.ofNullable(namedSavingsGlAccount(savingsProductId, SAVINGS_INTEREST_PAYABLE_MAPPING)))
|
.or(() -> Optional.ofNullable(namedSavingsGlAccount(savingsProductId,
|
||||||
|
SAVINGS_INTEREST_PAYABLE_MAPPING, "Fixed Deposit Interest Payable", "Interest Payable", "Interest Payable Account")))
|
||||||
.orElse(controlAccount);
|
.orElse(controlAccount);
|
||||||
final GLAccount paidAccount = Optional.ofNullable(namedSavingsGlAccount(savingsProductId, SAVINGS_INTEREST_PAID_MAPPING))
|
final GLAccount paidAccount = Optional.ofNullable(namedSavingsGlAccount(savingsProductId,
|
||||||
|
SAVINGS_INTEREST_PAID_MAPPING, "Fixed Deposit Interest Paid", "Interest Paid", "Interest Paid Account"))
|
||||||
.orElse(payableAccount);
|
.orElse(payableAccount);
|
||||||
|
|
||||||
|
log.info("createInterestPostingJournalEntriesForSavings: savingsProductId={}, payableAccount=[{} - {}], paidAccount=[{} - {}], controlAccount=[{} - {}], interestExpense=[{} - {}], amount={}, isReversal={}",
|
||||||
|
savingsProductId, payableAccount.getId(), payableAccount.getName(), paidAccount.getId(), paidAccount.getName(),
|
||||||
|
controlAccount.getId(), controlAccount.getName(), interestOnSavingsAccount.getId(), interestOnSavingsAccount.getName(),
|
||||||
|
amount, isReversal);
|
||||||
|
|
||||||
// A product that names no payable falls back to Savings Control above. The chain below would
|
// A product that names no payable falls back to Savings Control above. The chain below would
|
||||||
// then open with Dr Savings Control / Cr Interest On Savings and close with its exact
|
// then open with Dr Savings Control / Cr Interest On Savings and close with its exact
|
||||||
// reverse, the two cancelling: the expense never charged and the member never credited. Such
|
// reverse, the two cancelling: the expense never charged and the member never credited. Such
|
||||||
// a product has no liability to draw down, so it keeps the single pair it always posted.
|
// a product has no liability to draw down, so it keeps the single pair it always posted.
|
||||||
if (payableAccount.getId().equals(controlAccount.getId())) {
|
if (payableAccount.getId().equals(controlAccount.getId())) {
|
||||||
|
log.warn("createInterestPostingJournalEntriesForSavings: payableAccount == controlAccount [id={}], posting standard 2-leg entry",
|
||||||
|
payableAccount.getId());
|
||||||
postSavingsLeg(office, currencyCode, interestOnSavingsAccount, controlAccount, savingsId, transactionId, transactionDate,
|
postSavingsLeg(office, currencyCode, interestOnSavingsAccount, controlAccount, savingsId, transactionId, transactionDate,
|
||||||
amount, isReversal);
|
amount, isReversal);
|
||||||
return;
|
return;
|
||||||
@@ -697,28 +708,34 @@ public class AccountingProcessorHelper {
|
|||||||
final boolean payableIsLiability = GLAccountType.LIABILITY.getValue().equals(payableAccount.getType());
|
final boolean payableIsLiability = GLAccountType.LIABILITY.getValue().equals(payableAccount.getType());
|
||||||
final BigDecimal alreadyAccrued = payableBalance(office, payableAccount, currencyCode);
|
final BigDecimal alreadyAccrued = payableBalance(office, payableAccount, currencyCode);
|
||||||
final BigDecimal shortfall = amount.subtract(alreadyAccrued).max(BigDecimal.ZERO);
|
final BigDecimal shortfall = amount.subtract(alreadyAccrued).max(BigDecimal.ZERO);
|
||||||
|
log.info("createInterestPostingJournalEntriesForSavings: alreadyAccrued={}, shortfall={}, payableIsLiability={}, isReversal={}",
|
||||||
|
alreadyAccrued, shortfall, payableIsLiability, isReversal);
|
||||||
if (!isReversal && payableIsLiability && shortfall.compareTo(BigDecimal.ZERO) > 0) {
|
if (!isReversal && payableIsLiability && shortfall.compareTo(BigDecimal.ZERO) > 0) {
|
||||||
// Dr Interest On Savings (the expense) / Cr Payable - the accrual that should have run.
|
log.info("createInterestPostingJournalEntriesForSavings: posting Stage 1 [Dr {} / Cr {} = {}]",
|
||||||
|
interestOnSavingsAccount.getName(), payableAccount.getName(), shortfall);
|
||||||
|
// Stage 1: Dr Interest On Savings (the expense) / Cr Payable - the accrual that should have run.
|
||||||
postSavingsLeg(office, currencyCode, interestOnSavingsAccount, payableAccount, savingsId, transactionId, transactionDate,
|
postSavingsLeg(office, currencyCode, interestOnSavingsAccount, payableAccount, savingsId, transactionId, transactionDate,
|
||||||
shortfall, false);
|
shortfall, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Out of the payable, into the interest-paid account the product names.
|
|
||||||
postSavingsLeg(office, currencyCode, payableAccount, interestOnSavingsAccount, savingsId, transactionId, transactionDate, amount,
|
|
||||||
isReversal);
|
|
||||||
|
|
||||||
// A separate paid account, where the product names one, sits between interest-paid and the
|
|
||||||
// member; otherwise the payout leaves straight from interest-paid.
|
|
||||||
final boolean hasSeparatePaidAccount = !payableAccount.getId().equals(paidAccount.getId());
|
final boolean hasSeparatePaidAccount = !payableAccount.getId().equals(paidAccount.getId());
|
||||||
if (hasSeparatePaidAccount) {
|
if (hasSeparatePaidAccount) {
|
||||||
postSavingsLeg(office, currencyCode, interestOnSavingsAccount, paidAccount, savingsId, transactionId, transactionDate, amount,
|
log.info("createInterestPostingJournalEntriesForSavings: posting Stage 2 [Dr {} / Cr {} = {}] and Stage 3 [Dr {} / Cr {} = {}]",
|
||||||
|
payableAccount.getName(), paidAccount.getName(), amount, paidAccount.getName(), controlAccount.getName(), amount);
|
||||||
|
// Stage 2: Out of the payable, into the interest-paid transit/clearing account
|
||||||
|
postSavingsLeg(office, currencyCode, payableAccount, paidAccount, savingsId, transactionId, transactionDate, amount,
|
||||||
|
isReversal);
|
||||||
|
|
||||||
|
// Stage 3: Out of the interest-paid account, into the member's control account
|
||||||
|
postSavingsLeg(office, currencyCode, paidAccount, controlAccount, savingsId, transactionId, transactionDate, amount,
|
||||||
|
isReversal);
|
||||||
|
} else {
|
||||||
|
log.info("createInterestPostingJournalEntriesForSavings: no separate paidAccount, posting direct payout [Dr {} / Cr {} = {}]",
|
||||||
|
payableAccount.getName(), controlAccount.getName(), amount);
|
||||||
|
// Payout directly from payable into member's control account
|
||||||
|
postSavingsLeg(office, currencyCode, payableAccount, controlAccount, savingsId, transactionId, transactionDate, amount,
|
||||||
isReversal);
|
isReversal);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Into the member's own balance.
|
|
||||||
final GLAccount payoutFromAccount = hasSeparatePaidAccount ? paidAccount : interestOnSavingsAccount;
|
|
||||||
postSavingsLeg(office, currencyCode, payoutFromAccount, controlAccount, savingsId, transactionId, transactionDate, amount,
|
|
||||||
isReversal);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -792,34 +809,37 @@ public class AccountingProcessorHelper {
|
|||||||
return balance == null ? BigDecimal.ZERO : balance;
|
return balance == null ? BigDecimal.ZERO : balance;
|
||||||
}
|
}
|
||||||
|
|
||||||
private GLAccount namedSavingsGlAccount(final Long savingsProductId, final String accountName) {
|
private GLAccount namedSavingsGlAccount(final Long savingsProductId, final String... accountNames) {
|
||||||
if (savingsProductId == null) {
|
if (savingsProductId == null || accountNames == null || accountNames.length == 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
// The column holds an empty string on a product saved with no named mappings, and casting
|
for (final String accountName : accountNames) {
|
||||||
// that to jsonb is an error rather than an empty document - so the cast is done inside a
|
if (accountName == null) {
|
||||||
// subquery that has already filtered those rows out. Postgres is free to evaluate a WHERE
|
continue;
|
||||||
// clause in any order, so guarding at the same query level as the cast would only work by
|
}
|
||||||
// luck.
|
final List<Long> found = jdbcTemplate.queryForList("""
|
||||||
final List<Long> found = jdbcTemplate.queryForList("""
|
SELECT gl.id
|
||||||
SELECT gl.id
|
FROM (SELECT other_gl_mappings::jsonb AS doc
|
||||||
FROM (SELECT other_gl_mappings::jsonb AS doc
|
FROM m_savings_product
|
||||||
FROM m_savings_product
|
WHERE id = ?
|
||||||
WHERE id = ?
|
AND other_gl_mappings IS NOT NULL
|
||||||
AND other_gl_mappings IS NOT NULL
|
AND other_gl_mappings <> ''
|
||||||
AND other_gl_mappings <> ''
|
AND other_gl_mappings LIKE '{%') sp
|
||||||
AND other_gl_mappings LIKE '{%') sp
|
CROSS JOIN LATERAL jsonb_array_elements(
|
||||||
CROSS JOIN LATERAL jsonb_array_elements(
|
COALESCE(sp.doc -> 'assets', '[]'::jsonb)
|
||||||
COALESCE(sp.doc -> 'assets', '[]'::jsonb)
|
|| COALESCE(sp.doc -> 'liabilities', '[]'::jsonb)
|
||||||
|| COALESCE(sp.doc -> 'liabilities', '[]'::jsonb)
|
|| COALESCE(sp.doc -> 'expenses', '[]'::jsonb)
|
||||||
|| COALESCE(sp.doc -> 'expenses', '[]'::jsonb)
|
|| COALESCE(sp.doc -> 'income', '[]'::jsonb)
|
||||||
|| COALESCE(sp.doc -> 'income', '[]'::jsonb)
|
) AS m(entry)
|
||||||
) AS m(entry)
|
JOIN acc_gl_account gl ON gl.gl_code = m.entry ->> 'glCode'
|
||||||
JOIN acc_gl_account gl ON gl.gl_code = m.entry ->> 'glCode'
|
WHERE lower(trim(m.entry ->> 'accountName')) = lower(trim(?))
|
||||||
WHERE m.entry ->> 'accountName' = ?
|
LIMIT 1
|
||||||
LIMIT 1
|
""", Long.class, savingsProductId, accountName);
|
||||||
""", Long.class, savingsProductId, accountName);
|
if (!found.isEmpty()) {
|
||||||
return found.isEmpty() ? null : glAccountRepository.findById(found.get(0)).orElse(null);
|
return glAccountRepository.findById(found.get(0)).orElse(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+21
-1
@@ -78,7 +78,8 @@ public class DepositAccountTransactionDataValidator {
|
|||||||
DepositsApiConstants.noteParamName, onAccountClosureIdParamName, paymentTypeIdParamName,
|
DepositsApiConstants.noteParamName, onAccountClosureIdParamName, paymentTypeIdParamName,
|
||||||
transactionAccountNumberParamName, checkNumberParamName, routingCodeParamName, receiptNumberParamName,
|
transactionAccountNumberParamName, checkNumberParamName, routingCodeParamName, receiptNumberParamName,
|
||||||
bankNumberParamName, DepositsApiConstants.transferDescriptionParamName, toSavingsAccountIdParamName,
|
bankNumberParamName, DepositsApiConstants.transferDescriptionParamName, toSavingsAccountIdParamName,
|
||||||
DepositsApiConstants.waivePreClosurePenaltyParamName, DepositsApiConstants.prematureClosureReasonIdParamName));
|
DepositsApiConstants.toGlAccountIdParamName, DepositsApiConstants.waivePreClosurePenaltyParamName,
|
||||||
|
DepositsApiConstants.prematureClosureReasonIdParamName));
|
||||||
|
|
||||||
private static final Set<String> DEPOSIT_ACCOUNT_PRE_MATURE_CALCULATION_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(
|
private static final Set<String> DEPOSIT_ACCOUNT_PRE_MATURE_CALCULATION_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(
|
||||||
DepositsApiConstants.localeParamName, DepositsApiConstants.dateFormatParamName, closedOnDateParamName,
|
DepositsApiConstants.localeParamName, DepositsApiConstants.dateFormatParamName, closedOnDateParamName,
|
||||||
@@ -247,6 +248,25 @@ public class DepositAccountTransactionDataValidator {
|
|||||||
} else if (accountOnClosureType.isReinvest() && isPreMatureClose) {
|
} else if (accountOnClosureType.isReinvest() && isPreMatureClose) {
|
||||||
baseDataValidator.reset().parameter(onAccountClosureIdParamName).value(onAccountClosureId)
|
baseDataValidator.reset().parameter(onAccountClosureIdParamName).value(onAccountClosureId)
|
||||||
.failWithCode("reinvest.not.allowed", "Re-Invest is not supported for account pre mature close");
|
.failWithCode("reinvest.not.allowed", "Re-Invest is not supported for account pre mature close");
|
||||||
|
} else if (accountOnClosureType.isTransferToGl()) {
|
||||||
|
// Only the FD premature-closure handler books the GL leg. Offering it anywhere else would
|
||||||
|
// fall through to that handler's terminal "withdraw the whole balance" case and silently pay
|
||||||
|
// the deposit out in cash, which is exactly what the partial.reinvest guard above prevents.
|
||||||
|
if (!isPreMatureClose || !DepositAccountType.FIXED_DEPOSIT.equals(depositAccountType)) {
|
||||||
|
baseDataValidator.reset().parameter(onAccountClosureIdParamName).value(onAccountClosureId).failWithCode(
|
||||||
|
"transfer.to.gl.not.allowed",
|
||||||
|
"Transfer to a GL account is only supported for fixed-deposit premature closure");
|
||||||
|
}
|
||||||
|
final Long toGlAccountId = this.fromApiJsonHelper.extractLongNamed(DepositsApiConstants.toGlAccountIdParamName, element);
|
||||||
|
baseDataValidator.reset().parameter(DepositsApiConstants.toGlAccountIdParamName).value(toGlAccountId)
|
||||||
|
.cantBeBlankWhenParameterProvidedIs(onAccountClosureIdParamName, accountOnClosureType.getCode());
|
||||||
|
} else if (accountOnClosureType.isWithdarwDeposit() && isPreMatureClose) {
|
||||||
|
// Premature closure proceeds have to be traceable into a savings account, so the cash payout
|
||||||
|
// is not offered in the preview (see DepositAccountPreMatureCalculationPlatformServiceImpl)
|
||||||
|
// and is refused here as well -- otherwise a direct API call could still take the cash route.
|
||||||
|
baseDataValidator.reset().parameter(onAccountClosureIdParamName).value(onAccountClosureId).failWithCode(
|
||||||
|
"withdraw.deposit.not.allowed",
|
||||||
|
"Paying the deposit out in cash is not supported for account pre mature close -- transfer it to a savings account");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+96
@@ -25,6 +25,8 @@ import static org.apache.fineract.portfolio.savings.DepositsApiConstants.toSavin
|
|||||||
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.transferDescriptionParamName;
|
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.transferDescriptionParamName;
|
||||||
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.waivePreClosurePenaltyParamName;
|
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.waivePreClosurePenaltyParamName;
|
||||||
|
|
||||||
|
import com.google.gson.JsonArray;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.math.MathContext;
|
import java.math.MathContext;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
@@ -38,6 +40,7 @@ import java.util.Locale;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.accounting.common.AccountingConstants;
|
||||||
import org.apache.fineract.accounting.journalentry.service.JournalEntryWritePlatformService;
|
import org.apache.fineract.accounting.journalentry.service.JournalEntryWritePlatformService;
|
||||||
import org.apache.fineract.infrastructure.accountnumberformat.domain.AccountNumberFormat;
|
import org.apache.fineract.infrastructure.accountnumberformat.domain.AccountNumberFormat;
|
||||||
import org.apache.fineract.infrastructure.accountnumberformat.domain.AccountNumberFormatRepositoryWrapper;
|
import org.apache.fineract.infrastructure.accountnumberformat.domain.AccountNumberFormatRepositoryWrapper;
|
||||||
@@ -48,6 +51,7 @@ import org.apache.fineract.infrastructure.core.domain.ExternalId;
|
|||||||
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
|
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
|
||||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||||
import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
|
import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
|
||||||
|
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
||||||
import org.apache.fineract.portfolio.account.PortfolioAccountType;
|
import org.apache.fineract.portfolio.account.PortfolioAccountType;
|
||||||
import org.apache.fineract.portfolio.account.data.AccountTransferDTO;
|
import org.apache.fineract.portfolio.account.data.AccountTransferDTO;
|
||||||
import org.apache.fineract.portfolio.account.domain.AccountTransferType;
|
import org.apache.fineract.portfolio.account.domain.AccountTransferType;
|
||||||
@@ -76,6 +80,7 @@ import org.apache.fineract.portfolio.savings.data.FdInterestRateSlabData;
|
|||||||
import org.apache.fineract.portfolio.savings.service.FdBaseInterestRateReadPlatformService;
|
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.jdbc.core.JdbcTemplate;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
@@ -96,6 +101,8 @@ public class DepositAccountDomainServiceJpa implements DepositAccountDomainServi
|
|||||||
private final FixedDepositRenewalHistoryRepository fixedDepositRenewalHistoryRepository;
|
private final FixedDepositRenewalHistoryRepository fixedDepositRenewalHistoryRepository;
|
||||||
private final FixedDepositRenewalChartSlabHistoryRepository fixedDepositRenewalChartSlabHistoryRepository;
|
private final FixedDepositRenewalChartSlabHistoryRepository fixedDepositRenewalChartSlabHistoryRepository;
|
||||||
private final FdBaseInterestRateReadPlatformService fdBaseInterestRateReadPlatformService;
|
private final FdBaseInterestRateReadPlatformService fdBaseInterestRateReadPlatformService;
|
||||||
|
private final FromJsonHelper fromApiJsonHelper;
|
||||||
|
private final JdbcTemplate jdbcTemplate;
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
@Override
|
@Override
|
||||||
@@ -846,6 +853,20 @@ public class DepositAccountDomainServiceJpa implements DepositAccountDomainServi
|
|||||||
toSavingsAccount, account, isRegularTransaction, isExceptionForBalanceCheck);
|
toSavingsAccount, account, isRegularTransaction, isExceptionForBalanceCheck);
|
||||||
this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO);
|
this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO);
|
||||||
updateAlreadyPostedTransactions(existingTransactionIds, account);
|
updateAlreadyPostedTransactions(existingTransactionIds, account);
|
||||||
|
} else if (closureType.isTransferToGl()) {
|
||||||
|
final Long toGlAccountId = command.longValueOfParameterNamed(DepositsApiConstants.toGlAccountIdParamName);
|
||||||
|
final BigDecimal proceeds = account.getAccountBalance();
|
||||||
|
// Declared as an account transfer so the withdrawal's own entry credits the Liability Transfer
|
||||||
|
// account rather than cash/vault -- nothing is paid over the counter here. The offset below then
|
||||||
|
// moves it out of there into the GL the maker picked, leaving Liability Transfer at nil and the
|
||||||
|
// net posting as Dr Savings Control / Cr chosen GL.
|
||||||
|
final boolean isInternalTransfer = true;
|
||||||
|
final SavingsAccountTransaction withdrawal = this.handleWithdrawal(account, fmt, closedDate, proceeds, paymentDetail, false,
|
||||||
|
isRegularTransaction, isInternalTransfer);
|
||||||
|
savingsTransactionId = withdrawal.getId();
|
||||||
|
updateAlreadyPostedTransactions(existingTransactionIds, account);
|
||||||
|
postClosureProceedsToGlAccount(account, toGlAccountId, proceeds, closedDate,
|
||||||
|
command.stringValueOfParameterNamed(transferDescriptionParamName));
|
||||||
} else {
|
} else {
|
||||||
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);
|
||||||
@@ -863,6 +884,81 @@ public class DepositAccountDomainServiceJpa implements DepositAccountDomainServi
|
|||||||
return savingsTransactionId;
|
return savingsTransactionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Moves a premature closure's proceeds out of Liability Transfer and into the GL account the maker picked:
|
||||||
|
* Dr the Liability Transfer account / Cr that GL account.
|
||||||
|
* <p>
|
||||||
|
* The withdrawal booked immediately before this was declared an account transfer, so the accounting engine
|
||||||
|
* has already credited Liability Transfer for the same amount -- this entry clears it, leaving the pair
|
||||||
|
* netting to Dr Savings Control / Cr the chosen GL, and Liability Transfer back at nil. Posted through the real
|
||||||
|
* createJournalEntry command rather than written straight to the ledger, so it reverses, reports and
|
||||||
|
* reconciles exactly like any other manual entry.
|
||||||
|
*/
|
||||||
|
private void postClosureProceedsToGlAccount(final SavingsAccount account, final Long toGlAccountId, final BigDecimal amount,
|
||||||
|
final LocalDate closedDate, final String description) {
|
||||||
|
|
||||||
|
final Long suspenseGlAccountId = liabilityTransferGlAccountId();
|
||||||
|
|
||||||
|
final JsonObject debitEntry = new JsonObject();
|
||||||
|
debitEntry.addProperty("glAccountId", suspenseGlAccountId);
|
||||||
|
debitEntry.addProperty("amount", amount);
|
||||||
|
final JsonArray debits = new JsonArray();
|
||||||
|
debits.add(debitEntry);
|
||||||
|
|
||||||
|
final JsonObject creditEntry = new JsonObject();
|
||||||
|
creditEntry.addProperty("glAccountId", toGlAccountId);
|
||||||
|
creditEntry.addProperty("amount", amount);
|
||||||
|
final JsonArray credits = new JsonArray();
|
||||||
|
credits.add(creditEntry);
|
||||||
|
|
||||||
|
final StringBuilder comments = new StringBuilder("Premature closure of fixed deposit ").append(account.getAccountNumber());
|
||||||
|
if (description != null && !description.isBlank()) {
|
||||||
|
comments.append(" -- ").append(description);
|
||||||
|
}
|
||||||
|
|
||||||
|
final String dateFormat = "dd MMMM yyyy";
|
||||||
|
final JsonObject payload = new JsonObject();
|
||||||
|
payload.addProperty("officeId", account.officeId());
|
||||||
|
payload.addProperty("transactionDate", closedDate.format(DateTimeFormatter.ofPattern(dateFormat, Locale.ENGLISH)));
|
||||||
|
payload.addProperty("dateFormat", dateFormat);
|
||||||
|
payload.addProperty("locale", "en");
|
||||||
|
payload.addProperty("currencyCode", account.getCurrency().getCode());
|
||||||
|
payload.addProperty("comments", comments.toString());
|
||||||
|
payload.add("debits", debits);
|
||||||
|
payload.add("credits", credits);
|
||||||
|
|
||||||
|
final String json = payload.toString();
|
||||||
|
final JsonCommand journalEntryCommand = JsonCommand.from(json, this.fromApiJsonHelper.parse(json), this.fromApiJsonHelper, null,
|
||||||
|
null, null, null, null, null, null, null, null, null, null, null, null, null);
|
||||||
|
this.journalEntryWritePlatformService.createJournalEntry(journalEntryCommand);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The GL account the withdrawal above credited, and therefore the exact account this entry has to debit
|
||||||
|
* to clear it.
|
||||||
|
* <p>
|
||||||
|
* It is the organisation-wide Liability Transfer financial activity, NOT the product's own Transfers In
|
||||||
|
* Suspense mapping: {@code CashBasedAccountingProcessorForSavings} books an account-transfer withdrawal
|
||||||
|
* as Dr Savings Control / Cr {@code FinancialActivity.LIABILITY_TRANSFER}. Debiting the product mapping
|
||||||
|
* instead leaves the credit standing in Liability Transfer for ever while the two legs of this entry
|
||||||
|
* collapse onto whichever account that mapping names.
|
||||||
|
* <p>
|
||||||
|
* Refusing outright when it is unmapped is deliberate: silently picking another account would put the
|
||||||
|
* closure's money somewhere nobody chose.
|
||||||
|
*/
|
||||||
|
private Long liabilityTransferGlAccountId() {
|
||||||
|
final List<Long> glAccountIds = this.jdbcTemplate.queryForList(
|
||||||
|
"SELECT gl_account_id FROM acc_gl_financial_activity_account WHERE financial_activity_type = ? ORDER BY id DESC",
|
||||||
|
Long.class, AccountingConstants.FinancialActivity.LIABILITY_TRANSFER.getValue());
|
||||||
|
if (glAccountIds.isEmpty()) {
|
||||||
|
throw new GeneralPlatformDomainRuleException("error.msg.liability.transfer.financial.activity.not.mapped",
|
||||||
|
"No Liability Transfer account is mapped under Accounting > Financial Activity Mappings, so the closure"
|
||||||
|
+ " proceeds cannot be routed to a GL account. Map it first.",
|
||||||
|
AccountingConstants.FinancialActivity.LIABILITY_TRANSFER.getValue());
|
||||||
|
}
|
||||||
|
return glAccountIds.get(0);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A staff-triggered, always-available alternative to Premature Close (Active-only, no reinvest) and Close
|
* 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
|
* (Matured-only): works on an FD in either state, and adds a Partial Reinvest option neither of those two support
|
||||||
|
|||||||
+24
-11
@@ -829,17 +829,22 @@ public class FixedDepositAccount extends SavingsAccount {
|
|||||||
final boolean isPreMatureClosure, final boolean isSavingsInterestPostingAtCurrentPeriodEnd,
|
final boolean isPreMatureClosure, final boolean isSavingsInterestPostingAtCurrentPeriodEnd,
|
||||||
final Integer financialYearBeginningMonth) {
|
final Integer financialYearBeginningMonth) {
|
||||||
final MathContext mc = MathContext.DECIMAL64;
|
final MathContext mc = MathContext.DECIMAL64;
|
||||||
final List<PostingPeriod> postingPeriods = calculateInterestPayable(mc, preMatureDate, transactions, isPreMatureClosure,
|
this.resetAccountTransactionsEndOfDayBalances(transactions, preMatureDate);
|
||||||
isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth);
|
try {
|
||||||
|
final List<PostingPeriod> postingPeriods = calculateInterestPayable(mc, preMatureDate, transactions, isPreMatureClosure,
|
||||||
|
isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth);
|
||||||
|
|
||||||
Money interestOnMaturity = Money.zero(this.currency);
|
Money interestOnMaturity = Money.zero(this.currency);
|
||||||
|
|
||||||
for (final PostingPeriod interestPostingPeriod : postingPeriods) {
|
for (final PostingPeriod interestPostingPeriod : postingPeriods) {
|
||||||
final Money interestEarnedForPeriod = interestPostingPeriod.getInterestEarned();
|
final Money interestEarnedForPeriod = interestPostingPeriod.getInterestEarned();
|
||||||
interestOnMaturity = interestOnMaturity.plus(interestEarnedForPeriod);
|
interestOnMaturity = interestOnMaturity.plus(interestEarnedForPeriod);
|
||||||
|
}
|
||||||
|
|
||||||
|
return interestOnMaturity;
|
||||||
|
} finally {
|
||||||
|
this.resetAccountTransactionsEndOfDayBalances(transactions, DateUtils.getBusinessLocalDate());
|
||||||
}
|
}
|
||||||
|
|
||||||
return interestOnMaturity;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void postInterest(final MathContext mc, final LocalDate postingDate, boolean isInterestTransfer,
|
public void postInterest(final MathContext mc, final LocalDate postingDate, boolean isInterestTransfer,
|
||||||
@@ -946,13 +951,21 @@ public class FixedDepositAccount extends SavingsAccount {
|
|||||||
* the term restarted belongs to a term that has already been settled.
|
* the term restarted belongs to a term that has already been settled.
|
||||||
*/
|
*/
|
||||||
private Money totalInterestPosted() {
|
private Money totalInterestPosted() {
|
||||||
|
final boolean renewedTerm = isRenewedTerm();
|
||||||
final LocalDate currentTermStartDate = depositStartDate();
|
final LocalDate currentTermStartDate = depositStartDate();
|
||||||
Money interestPostedToDate = Money.zero(this.currency);
|
Money interestPostedToDate = Money.zero(this.currency);
|
||||||
for (final SavingsAccountTransaction transaction : this.transactions) {
|
for (final SavingsAccountTransaction transaction : this.transactions) {
|
||||||
if (transaction.isInterestPostingAndNotReversed()
|
if (!transaction.isInterestPostingAndNotReversed()) {
|
||||||
&& !DateUtils.isBefore(transaction.getTransactionDate(), currentTermStartDate)) {
|
continue;
|
||||||
interestPostedToDate = interestPostedToDate.plus(transaction.getAmount(currency));
|
|
||||||
}
|
}
|
||||||
|
// Strictly after, and matching transactionsForCurrentTermInterestCalculation() exactly: the
|
||||||
|
// previous term's closing interest posting is dated the SAME day the new term starts, so an
|
||||||
|
// inclusive bound still counts it here while the posting-period walk excludes it -- leaving
|
||||||
|
// the old term's entire interest subtracted from a maturity amount it was never part of.
|
||||||
|
if (renewedTerm && !DateUtils.isAfter(transaction.getTransactionDate(), currentTermStartDate)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
interestPostedToDate = interestPostedToDate.plus(transaction.getAmount(currency));
|
||||||
}
|
}
|
||||||
|
|
||||||
return interestPostedToDate;
|
return interestPostedToDate;
|
||||||
|
|||||||
+13
-8
@@ -759,17 +759,22 @@ public class RecurringDepositAccount extends SavingsAccount {
|
|||||||
final boolean isPreMatureClosure, final boolean isSavingsInterestPostingAtCurrentPeriodEnd,
|
final boolean isPreMatureClosure, final boolean isSavingsInterestPostingAtCurrentPeriodEnd,
|
||||||
final Integer financialYearBeginningMonth) {
|
final Integer financialYearBeginningMonth) {
|
||||||
final MathContext mc = MathContext.DECIMAL64;
|
final MathContext mc = MathContext.DECIMAL64;
|
||||||
final List<PostingPeriod> postingPeriods = calculateInterestPayable(mc, preMatureDate, transactions, isPreMatureClosure,
|
this.resetAccountTransactionsEndOfDayBalances(transactions, preMatureDate);
|
||||||
isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth);
|
try {
|
||||||
|
final List<PostingPeriod> postingPeriods = calculateInterestPayable(mc, preMatureDate, transactions, isPreMatureClosure,
|
||||||
|
isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth);
|
||||||
|
|
||||||
Money interestOnMaturity = Money.zero(this.currency);
|
Money interestOnMaturity = Money.zero(this.currency);
|
||||||
|
|
||||||
for (final PostingPeriod interestPostingPeriod : postingPeriods) {
|
for (final PostingPeriod interestPostingPeriod : postingPeriods) {
|
||||||
final Money interestEarnedForPeriod = interestPostingPeriod.getInterestEarned();
|
final Money interestEarnedForPeriod = interestPostingPeriod.getInterestEarned();
|
||||||
interestOnMaturity = interestOnMaturity.plus(interestEarnedForPeriod);
|
interestOnMaturity = interestOnMaturity.plus(interestEarnedForPeriod);
|
||||||
|
}
|
||||||
|
this.summary.updateFromInterestPeriodSummaries(this.currency, postingPeriods);
|
||||||
|
return interestOnMaturity;
|
||||||
|
} finally {
|
||||||
|
this.resetAccountTransactionsEndOfDayBalances(transactions, DateUtils.getBusinessLocalDate());
|
||||||
}
|
}
|
||||||
this.summary.updateFromInterestPeriodSummaries(this.currency, postingPeriods);
|
|
||||||
return interestOnMaturity;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
+10
-2
@@ -250,7 +250,11 @@ public class CurrentAccountEditWritePlatformServiceJpaRepositoryImpl implements
|
|||||||
.findByCurrentAccountIdAndStatus(currentAccountId, CurrentAccountPendingEdit.STATUS_PENDING)
|
.findByCurrentAccountIdAndStatus(currentAccountId, CurrentAccountPendingEdit.STATUS_PENDING)
|
||||||
.orElseThrow(() -> new CurrentAccountNoPendingEditException(currentAccountId));
|
.orElseThrow(() -> new CurrentAccountNoPendingEditException(currentAccountId));
|
||||||
|
|
||||||
if (pendingEdit.getMaker().getId().equals(checker.getId()) && !checker.isCheckerSuperUser()) {
|
// Maker-identity rule: the same user can never approve their own submission, even when they
|
||||||
|
// hold CHECKER_SUPER_USER - see ShareAccountEditWritePlatformServiceJpaRepositoryImpl and
|
||||||
|
// PortfolioCommandSourceWritePlatformServiceImpl.validateMakerCheckerTransaction() for the
|
||||||
|
// equivalent enforcement elsewhere.
|
||||||
|
if (pendingEdit.getMaker().getId().equals(checker.getId())) {
|
||||||
throw new CurrentAccountEditSelfApprovalNotAllowedException(currentAccountId);
|
throw new CurrentAccountEditSelfApprovalNotAllowedException(currentAccountId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -306,7 +310,11 @@ public class CurrentAccountEditWritePlatformServiceJpaRepositoryImpl implements
|
|||||||
.findByCurrentAccountIdAndStatus(currentAccountId, CurrentAccountPendingEdit.STATUS_PENDING)
|
.findByCurrentAccountIdAndStatus(currentAccountId, CurrentAccountPendingEdit.STATUS_PENDING)
|
||||||
.orElseThrow(() -> new CurrentAccountNoPendingEditException(currentAccountId));
|
.orElseThrow(() -> new CurrentAccountNoPendingEditException(currentAccountId));
|
||||||
|
|
||||||
if (pendingEdit.getMaker().getId().equals(checker.getId()) && !checker.isCheckerSuperUser()) {
|
// Maker-identity rule: the same user can never approve their own submission, even when they
|
||||||
|
// hold CHECKER_SUPER_USER - see ShareAccountEditWritePlatformServiceJpaRepositoryImpl and
|
||||||
|
// PortfolioCommandSourceWritePlatformServiceImpl.validateMakerCheckerTransaction() for the
|
||||||
|
// equivalent enforcement elsewhere.
|
||||||
|
if (pendingEdit.getMaker().getId().equals(checker.getId())) {
|
||||||
throw new CurrentAccountEditSelfApprovalNotAllowedException(currentAccountId);
|
throw new CurrentAccountEditSelfApprovalNotAllowedException(currentAccountId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+11
-3
@@ -70,9 +70,17 @@ public class DepositAccountPreMatureCalculationPlatformServiceImpl implements De
|
|||||||
final SavingsAccount account = this.depositAccountAssembler.assembleFrom(accountId, depositAccountType);
|
final SavingsAccount account = this.depositAccountAssembler.assembleFrom(accountId, depositAccountType);
|
||||||
|
|
||||||
DepositAccountData accountData = null;
|
DepositAccountData accountData = null;
|
||||||
Collection<EnumOptionData> onAccountClosureOptions = SavingsEnumerations
|
// Premature closure proceeds must stay traceable -- into the customer's savings account, or (fixed
|
||||||
.depositAccountOnClosureType(new DepositAccountOnClosureType[] { DepositAccountOnClosureType.WITHDRAW_DEPOSIT,
|
// deposits only) straight to a GL account the maker picks. Paying a broken deposit out over the
|
||||||
DepositAccountOnClosureType.TRANSFER_TO_SAVINGS });
|
// counter in cash is not offered: WITHDRAW_DEPOSIT is deliberately absent here and rejected outright
|
||||||
|
// by DepositAccountTransactionDataValidator#validateClosing, so a direct API call cannot take it
|
||||||
|
// either. TRANSFER_TO_GL is offered only for fixed deposits because only
|
||||||
|
// DepositAccountDomainServiceJpa#handleFDAccountPreMatureClosure books its GL leg.
|
||||||
|
final DepositAccountOnClosureType[] closureTypes = depositAccountType.isFixedDeposit()
|
||||||
|
? new DepositAccountOnClosureType[] { DepositAccountOnClosureType.TRANSFER_TO_SAVINGS,
|
||||||
|
DepositAccountOnClosureType.TRANSFER_TO_GL }
|
||||||
|
: new DepositAccountOnClosureType[] { DepositAccountOnClosureType.TRANSFER_TO_SAVINGS };
|
||||||
|
Collection<EnumOptionData> onAccountClosureOptions = SavingsEnumerations.depositAccountOnClosureType(closureTypes);
|
||||||
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.clientId(), DepositAccountType.SAVINGS_DEPOSIT);
|
.retrieveActiveForLookup(account.clientId(), DepositAccountType.SAVINGS_DEPOSIT);
|
||||||
|
|||||||
+5
-1
@@ -543,9 +543,13 @@ 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());
|
||||||
|
// TRANSFER_TO_GL is excluded for the same reason PARTIAL_REINVEST is filtered above: only one
|
||||||
|
// handler implements it (fixed-deposit PREMATURE closure, which builds its own option list in
|
||||||
|
// DepositAccountPreMatureCalculationPlatformServiceImpl). Offering it on this template -- the
|
||||||
|
// normal Close and Payment screens -- would only get it refused by validateClosing.
|
||||||
Collection<EnumOptionData> onAccountClosureOptions = SavingsEnumerations
|
Collection<EnumOptionData> onAccountClosureOptions = SavingsEnumerations
|
||||||
.depositAccountOnClosureType(Arrays.stream(DepositAccountOnClosureType.values())
|
.depositAccountOnClosureType(Arrays.stream(DepositAccountOnClosureType.values())
|
||||||
.filter(type -> includePartialReinvest || !type.isPartialReinvest())
|
.filter(type -> includePartialReinvest || !type.isPartialReinvest()).filter(type -> !type.isTransferToGl())
|
||||||
.toArray(DepositAccountOnClosureType[]::new));
|
.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
|
||||||
|
|||||||
+34
-1
@@ -19,10 +19,13 @@
|
|||||||
package org.apache.fineract.portfolio.savings.service;
|
package org.apache.fineract.portfolio.savings.service;
|
||||||
|
|
||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.reflect.TypeToken;
|
||||||
|
import java.lang.reflect.Type;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.sql.ResultSet;
|
import java.sql.ResultSet;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.apache.fineract.accounting.common.AccountingEnumerations;
|
import org.apache.fineract.accounting.common.AccountingEnumerations;
|
||||||
@@ -202,6 +205,31 @@ public class DepositProductReadPlatformServiceImpl implements DepositProductRead
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final Type OTHER_GL_MAPPINGS_TYPE = new TypeToken<List<Map<String, Object>>>() {}.getType();
|
||||||
|
private static final Type OTHER_GL_MAPPINGS_MAP_TYPE = new TypeToken<Map<String, List<Map<String, Object>>>>() {}.getType();
|
||||||
|
|
||||||
|
private static List<Map<String, Object>> parseOtherGlMappings(final String otherGlMappingsJson) {
|
||||||
|
if (otherGlMappingsJson == null || otherGlMappingsJson.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
com.google.gson.JsonElement element = GSON.fromJson(otherGlMappingsJson, com.google.gson.JsonElement.class);
|
||||||
|
if (!element.isJsonArray()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return GSON.fromJson(element, OTHER_GL_MAPPINGS_TYPE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<String, List<Map<String, Object>>> parseOtherGlMappingsAsMap(final String otherGlMappingsJson) {
|
||||||
|
if (otherGlMappingsJson == null || otherGlMappingsJson.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
com.google.gson.JsonElement element = GSON.fromJson(otherGlMappingsJson, com.google.gson.JsonElement.class);
|
||||||
|
if (!element.isJsonObject()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return GSON.fromJson(element, OTHER_GL_MAPPINGS_MAP_TYPE);
|
||||||
|
}
|
||||||
|
|
||||||
private abstract static class DepositProductMapper implements RowMapper<DepositProductData> {
|
private abstract static class DepositProductMapper implements RowMapper<DepositProductData> {
|
||||||
|
|
||||||
private final String schemaSql;
|
private final String schemaSql;
|
||||||
@@ -221,6 +249,7 @@ public class DepositProductReadPlatformServiceImpl implements DepositProductRead
|
|||||||
sqlBuilder.append("sp.lockin_period_frequency as lockinPeriodFrequency,");
|
sqlBuilder.append("sp.lockin_period_frequency as lockinPeriodFrequency,");
|
||||||
sqlBuilder.append("sp.lockin_period_frequency_enum as lockinPeriodFrequencyType, ");
|
sqlBuilder.append("sp.lockin_period_frequency_enum as lockinPeriodFrequencyType, ");
|
||||||
sqlBuilder.append("sp.accounting_type as accountingType, ");
|
sqlBuilder.append("sp.accounting_type as accountingType, ");
|
||||||
|
sqlBuilder.append("sp.other_gl_mappings as otherGlMappings, ");
|
||||||
sqlBuilder.append("sp.min_balance_for_interest_calculation as minBalanceForInterestCalculation, ");
|
sqlBuilder.append("sp.min_balance_for_interest_calculation as minBalanceForInterestCalculation, ");
|
||||||
sqlBuilder.append("sp.withhold_tax as withHoldTax,");
|
sqlBuilder.append("sp.withhold_tax as withHoldTax,");
|
||||||
sqlBuilder.append("tg.id as taxGroupId, tg.name as taxGroupName ");
|
sqlBuilder.append("tg.id as taxGroupId, tg.name as taxGroupName ");
|
||||||
@@ -284,10 +313,14 @@ public class DepositProductReadPlatformServiceImpl implements DepositProductRead
|
|||||||
taxGroupData = TaxGroupData.lookup(taxGroupId, taxGroupName);
|
taxGroupData = TaxGroupData.lookup(taxGroupId, taxGroupName);
|
||||||
}
|
}
|
||||||
|
|
||||||
return DepositProductData.instance(id, name, shortName, description, currency, nominalAnnualInterestRate,
|
final DepositProductData data = DepositProductData.instance(id, name, shortName, description, currency, nominalAnnualInterestRate,
|
||||||
compoundingInterestPeriodType, interestPostingPeriodType, interestCalculationType, interestCalculationDaysInYearType,
|
compoundingInterestPeriodType, interestPostingPeriodType, interestCalculationType, interestCalculationDaysInYearType,
|
||||||
lockinPeriodFrequency, lockinPeriodFrequencyType, accountingRuleType, minBalanceForInterestCalculation, withHoldTax,
|
lockinPeriodFrequency, lockinPeriodFrequencyType, accountingRuleType, minBalanceForInterestCalculation, withHoldTax,
|
||||||
taxGroupData);
|
taxGroupData);
|
||||||
|
final String otherGlMappingsJson = rs.getString("otherGlMappings");
|
||||||
|
data.setOtherGlMappings(parseOtherGlMappings(otherGlMappingsJson));
|
||||||
|
data.setOtherGlMappingsMap(parseOtherGlMappingsAsMap(otherGlMappingsJson));
|
||||||
|
return data;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+10
-2
@@ -260,7 +260,11 @@ public class FixedDepositProductWritePlatformServiceJpaRepositoryImpl implements
|
|||||||
.findByProductTypeAndProductIdAndStatus(ProductPendingEdit.TYPE_FD, productId, ProductPendingEdit.STATUS_PENDING)
|
.findByProductTypeAndProductIdAndStatus(ProductPendingEdit.TYPE_FD, productId, ProductPendingEdit.STATUS_PENDING)
|
||||||
.orElseThrow(() -> new SavingsProductNoPendingEditException(productId));
|
.orElseThrow(() -> new SavingsProductNoPendingEditException(productId));
|
||||||
|
|
||||||
if (pendingEdit.getMaker().getId().equals(checker.getId()) && !checker.isCheckerSuperUser()) {
|
// Maker-identity rule: the same user can never approve their own submission, even when they
|
||||||
|
// hold CHECKER_SUPER_USER - see
|
||||||
|
// PortfolioCommandSourceWritePlatformServiceImpl.validateMakerCheckerTransaction() for the
|
||||||
|
// equivalent enforcement in the classic command pipeline.
|
||||||
|
if (pendingEdit.getMaker().getId().equals(checker.getId())) {
|
||||||
throw new SavingsProductSelfApprovalNotAllowedException(productId);
|
throw new SavingsProductSelfApprovalNotAllowedException(productId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -350,7 +354,11 @@ public class FixedDepositProductWritePlatformServiceJpaRepositoryImpl implements
|
|||||||
.findByProductTypeAndProductIdAndStatus(ProductPendingEdit.TYPE_FD, productId, ProductPendingEdit.STATUS_PENDING)
|
.findByProductTypeAndProductIdAndStatus(ProductPendingEdit.TYPE_FD, productId, ProductPendingEdit.STATUS_PENDING)
|
||||||
.orElseThrow(() -> new SavingsProductNoPendingEditException(productId));
|
.orElseThrow(() -> new SavingsProductNoPendingEditException(productId));
|
||||||
|
|
||||||
if (pendingEdit.getMaker().getId().equals(checker.getId()) && !checker.isCheckerSuperUser()) {
|
// Maker-identity rule: the same user can never approve their own submission, even when they
|
||||||
|
// hold CHECKER_SUPER_USER - see
|
||||||
|
// PortfolioCommandSourceWritePlatformServiceImpl.validateMakerCheckerTransaction() for the
|
||||||
|
// equivalent enforcement in the classic command pipeline.
|
||||||
|
if (pendingEdit.getMaker().getId().equals(checker.getId())) {
|
||||||
throw new SavingsProductSelfApprovalNotAllowedException(productId);
|
throw new SavingsProductSelfApprovalNotAllowedException(productId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+10
-2
@@ -264,7 +264,11 @@ public class RecurringDepositProductWritePlatformServiceJpaRepositoryImpl implem
|
|||||||
.findByProductTypeAndProductIdAndStatus(ProductPendingEdit.TYPE_RD, productId, ProductPendingEdit.STATUS_PENDING)
|
.findByProductTypeAndProductIdAndStatus(ProductPendingEdit.TYPE_RD, productId, ProductPendingEdit.STATUS_PENDING)
|
||||||
.orElseThrow(() -> new SavingsProductNoPendingEditException(productId));
|
.orElseThrow(() -> new SavingsProductNoPendingEditException(productId));
|
||||||
|
|
||||||
if (pendingEdit.getMaker().getId().equals(checker.getId()) && !checker.isCheckerSuperUser()) {
|
// Maker-identity rule: the same user can never approve their own submission, even when they
|
||||||
|
// hold CHECKER_SUPER_USER - see
|
||||||
|
// PortfolioCommandSourceWritePlatformServiceImpl.validateMakerCheckerTransaction() for the
|
||||||
|
// equivalent enforcement in the classic command pipeline.
|
||||||
|
if (pendingEdit.getMaker().getId().equals(checker.getId())) {
|
||||||
throw new SavingsProductSelfApprovalNotAllowedException(productId);
|
throw new SavingsProductSelfApprovalNotAllowedException(productId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -357,7 +361,11 @@ public class RecurringDepositProductWritePlatformServiceJpaRepositoryImpl implem
|
|||||||
.findByProductTypeAndProductIdAndStatus(ProductPendingEdit.TYPE_RD, productId, ProductPendingEdit.STATUS_PENDING)
|
.findByProductTypeAndProductIdAndStatus(ProductPendingEdit.TYPE_RD, productId, ProductPendingEdit.STATUS_PENDING)
|
||||||
.orElseThrow(() -> new SavingsProductNoPendingEditException(productId));
|
.orElseThrow(() -> new SavingsProductNoPendingEditException(productId));
|
||||||
|
|
||||||
if (pendingEdit.getMaker().getId().equals(checker.getId()) && !checker.isCheckerSuperUser()) {
|
// Maker-identity rule: the same user can never approve their own submission, even when they
|
||||||
|
// hold CHECKER_SUPER_USER - see
|
||||||
|
// PortfolioCommandSourceWritePlatformServiceImpl.validateMakerCheckerTransaction() for the
|
||||||
|
// equivalent enforcement in the classic command pipeline.
|
||||||
|
if (pendingEdit.getMaker().getId().equals(checker.getId())) {
|
||||||
throw new SavingsProductSelfApprovalNotAllowedException(productId);
|
throw new SavingsProductSelfApprovalNotAllowedException(productId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+10
-2
@@ -203,7 +203,11 @@ public class SavingsAccountEditWritePlatformServiceJpaRepositoryImpl implements
|
|||||||
.findBySavingsAccountIdAndStatus(savingsId, SavingsAccountPendingEdit.STATUS_PENDING)
|
.findBySavingsAccountIdAndStatus(savingsId, SavingsAccountPendingEdit.STATUS_PENDING)
|
||||||
.orElseThrow(() -> new SavingsAccountNoPendingEditException(savingsId));
|
.orElseThrow(() -> new SavingsAccountNoPendingEditException(savingsId));
|
||||||
|
|
||||||
if (pendingEdit.getMaker().getId().equals(checker.getId()) && !checker.isCheckerSuperUser()) {
|
// Maker-identity rule: the same user can never approve their own submission, even when they
|
||||||
|
// hold CHECKER_SUPER_USER - see
|
||||||
|
// PortfolioCommandSourceWritePlatformServiceImpl.validateMakerCheckerTransaction() for the
|
||||||
|
// equivalent enforcement in the classic command pipeline.
|
||||||
|
if (pendingEdit.getMaker().getId().equals(checker.getId())) {
|
||||||
throw new SavingsAccountEditSelfApprovalNotAllowedException(savingsId);
|
throw new SavingsAccountEditSelfApprovalNotAllowedException(savingsId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -265,7 +269,11 @@ public class SavingsAccountEditWritePlatformServiceJpaRepositoryImpl implements
|
|||||||
.findBySavingsAccountIdAndStatus(savingsId, SavingsAccountPendingEdit.STATUS_PENDING)
|
.findBySavingsAccountIdAndStatus(savingsId, SavingsAccountPendingEdit.STATUS_PENDING)
|
||||||
.orElseThrow(() -> new SavingsAccountNoPendingEditException(savingsId));
|
.orElseThrow(() -> new SavingsAccountNoPendingEditException(savingsId));
|
||||||
|
|
||||||
if (pendingEdit.getMaker().getId().equals(checker.getId()) && !checker.isCheckerSuperUser()) {
|
// Maker-identity rule: the same user can never approve their own submission, even when they
|
||||||
|
// hold CHECKER_SUPER_USER - see
|
||||||
|
// PortfolioCommandSourceWritePlatformServiceImpl.validateMakerCheckerTransaction() for the
|
||||||
|
// equivalent enforcement in the classic command pipeline.
|
||||||
|
if (pendingEdit.getMaker().getId().equals(checker.getId())) {
|
||||||
throw new SavingsAccountEditSelfApprovalNotAllowedException(savingsId);
|
throw new SavingsAccountEditSelfApprovalNotAllowedException(savingsId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+29
-3
@@ -437,14 +437,38 @@ public class SavingsAccountInterestPostingServiceImpl implements SavingsAccountI
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected Collection<Long> interestTransferWithdrawalTransactionIds(final SavingsAccountData savingsAccountData) {
|
||||||
|
if (savingsAccountData.getId() == null || this.savingsHelper == null || !isTransferInterestToOtherAccount(savingsAccountData)) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
return this.savingsHelper.fetchPostInterestTransactionIds(savingsAccountData.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isTransferInterestToOtherAccount(final SavingsAccountData savingsAccountData) {
|
||||||
|
return savingsAccountData.isTransferInterestToLinkedAccount()
|
||||||
|
&& DepositAccountType.FIXED_DEPOSIT.getValue().equals(savingsAccountData.getDepositTypeId());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected List<SavingsAccountTransactionData> excludeInterestTransferWithdrawals(
|
||||||
|
final List<SavingsAccountTransactionData> transactions, final SavingsAccountData savingsAccountData) {
|
||||||
|
final Collection<Long> interestTransferWithdrawalIds = interestTransferWithdrawalTransactionIds(savingsAccountData);
|
||||||
|
if (interestTransferWithdrawalIds.isEmpty()) {
|
||||||
|
return transactions;
|
||||||
|
}
|
||||||
|
return transactions.stream().filter(transaction -> !interestTransferWithdrawalIds.contains(transaction.getId()))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
private List<SavingsAccountTransactionData> retreiveOrderedNonInterestPostingTransactions(final SavingsAccountData savingsAccountData) {
|
private List<SavingsAccountTransactionData> retreiveOrderedNonInterestPostingTransactions(final SavingsAccountData savingsAccountData) {
|
||||||
final List<SavingsAccountTransactionData> listOfTransactionsSorted = retrieveListOfTransactions(savingsAccountData);
|
final List<SavingsAccountTransactionData> listOfTransactionsSorted = retrieveListOfTransactions(savingsAccountData);
|
||||||
|
final Collection<Long> interestTransferWithdrawalIds = interestTransferWithdrawalTransactionIds(savingsAccountData);
|
||||||
|
|
||||||
final List<SavingsAccountTransactionData> orderedNonInterestPostingTransactions = new ArrayList<>();
|
final List<SavingsAccountTransactionData> orderedNonInterestPostingTransactions = new ArrayList<>();
|
||||||
|
|
||||||
for (final SavingsAccountTransactionData transaction : listOfTransactionsSorted) {
|
for (final SavingsAccountTransactionData transaction : listOfTransactionsSorted) {
|
||||||
if (!(transaction.isInterestPostingAndNotReversed() || transaction.isOverdraftInterestAndNotReversed())
|
if (!(transaction.isInterestPostingAndNotReversed() || transaction.isOverdraftInterestAndNotReversed())
|
||||||
&& transaction.isNotReversed() && !transaction.isReversalTransaction()) {
|
&& transaction.isNotReversed() && !transaction.isReversalTransaction()
|
||||||
|
&& !interestTransferWithdrawalIds.contains(transaction.getId())) {
|
||||||
orderedNonInterestPostingTransactions.add(transaction);
|
orderedNonInterestPostingTransactions.add(transaction);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -664,10 +688,12 @@ public class SavingsAccountInterestPostingServiceImpl implements SavingsAccountI
|
|||||||
|
|
||||||
protected void resetAccountTransactionsEndOfDayBalances(final List<SavingsAccountTransactionData> accountTransactionsSorted,
|
protected void resetAccountTransactionsEndOfDayBalances(final List<SavingsAccountTransactionData> accountTransactionsSorted,
|
||||||
final LocalDate interestPostingUpToDate, final SavingsAccountData savingsAccountData) {
|
final LocalDate interestPostingUpToDate, final SavingsAccountData savingsAccountData) {
|
||||||
|
final List<SavingsAccountTransactionData> filteredTransactions = excludeInterestTransferWithdrawals(accountTransactionsSorted,
|
||||||
|
savingsAccountData);
|
||||||
// loop over transactions in reverse
|
// loop over transactions in reverse
|
||||||
LocalDate endOfBalanceDate = interestPostingUpToDate;
|
LocalDate endOfBalanceDate = interestPostingUpToDate;
|
||||||
for (int i = accountTransactionsSorted.size() - 1; i >= 0; i--) {
|
for (int i = filteredTransactions.size() - 1; i >= 0; i--) {
|
||||||
final SavingsAccountTransactionData transaction = accountTransactionsSorted.get(i);
|
final SavingsAccountTransactionData transaction = filteredTransactions.get(i);
|
||||||
if (transaction.isNotReversed() && !transaction.isReversalTransaction()
|
if (transaction.isNotReversed() && !transaction.isReversalTransaction()
|
||||||
&& !(transaction.isInterestPostingAndNotReversed() || transaction.isOverdraftInterestAndNotReversed())) {
|
&& !(transaction.isInterestPostingAndNotReversed() || transaction.isOverdraftInterestAndNotReversed())) {
|
||||||
transaction.updateCumulativeBalanceAndDates(MonetaryCurrency.fromCurrencyData(savingsAccountData.getCurrency()),
|
transaction.updateCumulativeBalanceAndDates(MonetaryCurrency.fromCurrencyData(savingsAccountData.getCurrency()),
|
||||||
|
|||||||
+10
-2
@@ -420,7 +420,11 @@ public class SavingsProductWritePlatformServiceJpaRepositoryImpl implements Savi
|
|||||||
.findByProductTypeAndProductIdAndStatus(ProductPendingEdit.TYPE_SAVINGS, productId, ProductPendingEdit.STATUS_PENDING)
|
.findByProductTypeAndProductIdAndStatus(ProductPendingEdit.TYPE_SAVINGS, productId, ProductPendingEdit.STATUS_PENDING)
|
||||||
.orElseThrow(() -> new SavingsProductNoPendingEditException(productId));
|
.orElseThrow(() -> new SavingsProductNoPendingEditException(productId));
|
||||||
|
|
||||||
if (pendingEdit.getMaker().getId().equals(checker.getId()) && !checker.isCheckerSuperUser()) {
|
// Maker-identity rule: the same user can never approve their own submission, even when they
|
||||||
|
// hold CHECKER_SUPER_USER - see
|
||||||
|
// PortfolioCommandSourceWritePlatformServiceImpl.validateMakerCheckerTransaction() for the
|
||||||
|
// equivalent enforcement in the classic command pipeline.
|
||||||
|
if (pendingEdit.getMaker().getId().equals(checker.getId())) {
|
||||||
throw new SavingsProductSelfApprovalNotAllowedException(productId);
|
throw new SavingsProductSelfApprovalNotAllowedException(productId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -524,7 +528,11 @@ public class SavingsProductWritePlatformServiceJpaRepositoryImpl implements Savi
|
|||||||
.findByProductTypeAndProductIdAndStatus(ProductPendingEdit.TYPE_SAVINGS, productId, ProductPendingEdit.STATUS_PENDING)
|
.findByProductTypeAndProductIdAndStatus(ProductPendingEdit.TYPE_SAVINGS, productId, ProductPendingEdit.STATUS_PENDING)
|
||||||
.orElseThrow(() -> new SavingsProductNoPendingEditException(productId));
|
.orElseThrow(() -> new SavingsProductNoPendingEditException(productId));
|
||||||
|
|
||||||
if (pendingEdit.getMaker().getId().equals(checker.getId()) && !checker.isCheckerSuperUser()) {
|
// Maker-identity rule: the same user can never approve their own submission, even when they
|
||||||
|
// hold CHECKER_SUPER_USER - see
|
||||||
|
// PortfolioCommandSourceWritePlatformServiceImpl.validateMakerCheckerTransaction() for the
|
||||||
|
// equivalent enforcement in the classic command pipeline.
|
||||||
|
if (pendingEdit.getMaker().getId().equals(checker.getId())) {
|
||||||
throw new SavingsProductSelfApprovalNotAllowedException(productId);
|
throw new SavingsProductSelfApprovalNotAllowedException(productId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+12
-2
@@ -172,7 +172,12 @@ public class ShareAccountEditWritePlatformServiceJpaRepositoryImpl implements Sh
|
|||||||
.findByShareAccountIdAndStatus(shareAccountId, ShareAccountPendingEdit.STATUS_PENDING)
|
.findByShareAccountIdAndStatus(shareAccountId, ShareAccountPendingEdit.STATUS_PENDING)
|
||||||
.orElseThrow(() -> new ShareAccountNoPendingEditException(shareAccountId));
|
.orElseThrow(() -> new ShareAccountNoPendingEditException(shareAccountId));
|
||||||
|
|
||||||
if (pendingEdit.getMaker().getId().equals(checker.getId()) && !checker.isCheckerSuperUser()) {
|
// Maker-identity rule: the same user can never approve their own submission, even when they
|
||||||
|
// hold CHECKER_SUPER_USER - that permission still lets them approve someone ELSE's request,
|
||||||
|
// it just no longer lets them bypass the hold on their own (matches
|
||||||
|
// PortfolioCommandSourceWritePlatformServiceImpl.validateMakerCheckerTransaction(), the
|
||||||
|
// equivalent rule for the classic command pipeline).
|
||||||
|
if (pendingEdit.getMaker().getId().equals(checker.getId())) {
|
||||||
throw new ShareAccountEditSelfApprovalNotAllowedException(shareAccountId);
|
throw new ShareAccountEditSelfApprovalNotAllowedException(shareAccountId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,7 +219,12 @@ public class ShareAccountEditWritePlatformServiceJpaRepositoryImpl implements Sh
|
|||||||
.findByShareAccountIdAndStatus(shareAccountId, ShareAccountPendingEdit.STATUS_PENDING)
|
.findByShareAccountIdAndStatus(shareAccountId, ShareAccountPendingEdit.STATUS_PENDING)
|
||||||
.orElseThrow(() -> new ShareAccountNoPendingEditException(shareAccountId));
|
.orElseThrow(() -> new ShareAccountNoPendingEditException(shareAccountId));
|
||||||
|
|
||||||
if (pendingEdit.getMaker().getId().equals(checker.getId()) && !checker.isCheckerSuperUser()) {
|
// Maker-identity rule: the same user can never approve their own submission, even when they
|
||||||
|
// hold CHECKER_SUPER_USER - that permission still lets them approve someone ELSE's request,
|
||||||
|
// it just no longer lets them bypass the hold on their own (matches
|
||||||
|
// PortfolioCommandSourceWritePlatformServiceImpl.validateMakerCheckerTransaction(), the
|
||||||
|
// equivalent rule for the classic command pipeline).
|
||||||
|
if (pendingEdit.getMaker().getId().equals(checker.getId())) {
|
||||||
throw new ShareAccountEditSelfApprovalNotAllowedException(shareAccountId);
|
throw new ShareAccountEditSelfApprovalNotAllowedException(shareAccountId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+10
-2
@@ -882,7 +882,11 @@ public class ShareProductWritePlatformServiceJpaRepositoryImpl implements ShareP
|
|||||||
.findByProductTypeAndProductIdAndStatus(ProductPendingEdit.TYPE_SHARE, productId, ProductPendingEdit.STATUS_PENDING)
|
.findByProductTypeAndProductIdAndStatus(ProductPendingEdit.TYPE_SHARE, productId, ProductPendingEdit.STATUS_PENDING)
|
||||||
.orElseThrow(() -> new ShareProductNoPendingEditException(productId));
|
.orElseThrow(() -> new ShareProductNoPendingEditException(productId));
|
||||||
|
|
||||||
if (pendingEdit.getMaker().getId().equals(checker.getId()) && !checker.isCheckerSuperUser()) {
|
// Maker-identity rule: the same user can never approve their own submission, even when they
|
||||||
|
// hold CHECKER_SUPER_USER - see
|
||||||
|
// PortfolioCommandSourceWritePlatformServiceImpl.validateMakerCheckerTransaction() for the
|
||||||
|
// equivalent enforcement in the classic command pipeline.
|
||||||
|
if (pendingEdit.getMaker().getId().equals(checker.getId())) {
|
||||||
throw new ShareProductSelfApprovalNotAllowedException(productId);
|
throw new ShareProductSelfApprovalNotAllowedException(productId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -968,7 +972,11 @@ public class ShareProductWritePlatformServiceJpaRepositoryImpl implements ShareP
|
|||||||
.findByProductTypeAndProductIdAndStatus(ProductPendingEdit.TYPE_SHARE, productId, ProductPendingEdit.STATUS_PENDING)
|
.findByProductTypeAndProductIdAndStatus(ProductPendingEdit.TYPE_SHARE, productId, ProductPendingEdit.STATUS_PENDING)
|
||||||
.orElseThrow(() -> new ShareProductNoPendingEditException(productId));
|
.orElseThrow(() -> new ShareProductNoPendingEditException(productId));
|
||||||
|
|
||||||
if (pendingEdit.getMaker().getId().equals(checker.getId()) && !checker.isCheckerSuperUser()) {
|
// Maker-identity rule: the same user can never approve their own submission, even when they
|
||||||
|
// hold CHECKER_SUPER_USER - see
|
||||||
|
// PortfolioCommandSourceWritePlatformServiceImpl.validateMakerCheckerTransaction() for the
|
||||||
|
// equivalent enforcement in the classic command pipeline.
|
||||||
|
if (pendingEdit.getMaker().getId().equals(checker.getId())) {
|
||||||
throw new ShareProductSelfApprovalNotAllowedException(productId);
|
throw new ShareProductSelfApprovalNotAllowedException(productId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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:Keshav@898966}
|
fineract.tenant.password=${FINERACT_DEFAULT_TENANTDB_PWD:313863}
|
||||||
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}
|
||||||
@@ -415,7 +415,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:Keshav@898966}
|
spring.datasource.hikari.password=${FINERACT_HIKARI_PASSWORD:313863}
|
||||||
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}
|
||||||
|
|||||||
@@ -163,5 +163,6 @@
|
|||||||
<include file="parts/0588_add_premature_closure_reason.xml" relativeToChangelogFile="true"/>
|
<include file="parts/0588_add_premature_closure_reason.xml" relativeToChangelogFile="true"/>
|
||||||
<include file="parts/0589_savings_interest_posting_report.xml" relativeToChangelogFile="true"/>
|
<include file="parts/0589_savings_interest_posting_report.xml" relativeToChangelogFile="true"/>
|
||||||
<include file="parts/0590_savings_interest_posting_report_by_branch.xml" relativeToChangelogFile="true"/>
|
<include file="parts/0590_savings_interest_posting_report_by_branch.xml" relativeToChangelogFile="true"/>
|
||||||
|
<include file="parts/0588_persist_manually_enabled_maker_checker_permissions.xml" relativeToChangelogFile="true"/>
|
||||||
<include file="parts/0591_cashier_allocation_time_and_deposit_counter_office.xml" relativeToChangelogFile="true"/>
|
<include file="parts/0591_cashier_allocation_time_and_deposit_counter_office.xml" relativeToChangelogFile="true"/>
|
||||||
</databaseChangeLog>
|
</databaseChangeLog>
|
||||||
|
|||||||
+73
@@ -0,0 +1,73 @@
|
|||||||
|
<?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">
|
||||||
|
|
||||||
|
<!-- Converts manually-applied Configure Maker Checker Tasks changes into a repeatable migration.
|
||||||
|
During Admin -> Organization and Admin -> System testing, can_maker_checker was switched on
|
||||||
|
(via System -> Configure Maker Checker Tasks) for the 28 permissions below, directly in the
|
||||||
|
dev database - none of that was ever captured by a migration, so a fresh/other environment
|
||||||
|
restored from this changelog history would come up with every one of them still at its
|
||||||
|
originally-seeded false value. Every code here was independently confirmed, via read-only
|
||||||
|
inspection of both its origin changeset and the live database, to be seeded can_maker_checker
|
||||||
|
= false and currently true in the running dev DB with no existing changeset already covering
|
||||||
|
the flip (the one precedent for this pattern, DELETE_OFFICE, was flipped by changeset
|
||||||
|
0538_enable_maker_checker_for_delete_office and is deliberately left untouched here).
|
||||||
|
|
||||||
|
This changeset only ever moves can_maker_checker from false to true for these exact codes -
|
||||||
|
entity_name, action_name, grouping and every other column are left exactly as their origin
|
||||||
|
changeset defined them (including the pre-existing DELETE_ACCOUNTNUMBERFORMAT entity_name
|
||||||
|
drift, which is out of scope for this change and is not corrected here). No _CHECKER row is
|
||||||
|
touched: those already exist for every code below (added earlier by 0583/0586, or by base
|
||||||
|
Fineract for the stock codes), and can_maker_checker is not a column on a _CHECKER row.
|
||||||
|
|
||||||
|
Grouped by the screen each permission belongs to:
|
||||||
|
- Currency Configuration: UPDATE_CURRENCY
|
||||||
|
- Manage Funds: CREATE_FUND
|
||||||
|
- Working Days: UPDATE_WORKINGDAYS
|
||||||
|
- Payment Type: CREATE_PAYMENTTYPE, UPDATE_PAYMENTTYPE, DELETE_PAYMENTTYPE
|
||||||
|
- Sponsor Banks: CREATE_SPONSORBANK, UPDATE_SPONSORBANK
|
||||||
|
- Branch Profile: UPDATE_BRANCHPROFILE
|
||||||
|
- Manage Data Tables: CREATE_DATATABLE, UPDATE_DATATABLE, DELETE_DATATABLE
|
||||||
|
- Manage Codes: CREATE_CODE, UPDATE_CODE, DELETE_CODE,
|
||||||
|
CREATE_CODEVALUE, UPDATE_CODEVALUE, DELETE_CODEVALUE
|
||||||
|
- Manage Reports: CREATE_REPORT, UPDATE_REPORT, DELETE_REPORT
|
||||||
|
- Manage Hooks: CREATE_HOOK, UPDATE_HOOK, DELETE_HOOK
|
||||||
|
- Account Number Preferences: CREATE_ACCOUNTNUMBERFORMAT, DELETE_ACCOUNTNUMBERFORMAT
|
||||||
|
- Manage User Agreement: CREATE_USERAGREEMENT
|
||||||
|
- Manage Jobs (COB Workflow Step Reorder): UPDATE_BATCH_BUSINESS_STEP
|
||||||
|
|
||||||
|
Deliberately NOT included (confirmed still false, or out of scope, and left untouched):
|
||||||
|
- UPDATE_PERMISSION, UPDATE_CONFIGURATION (the Configure Maker Checker Tasks self-lockout
|
||||||
|
exclusion added by PermissionReadPlatformServiceImpl stays in effect; these remain false)
|
||||||
|
- UPDATE_SPONSORDDGLMAPPING, all Entity-to-Entity Mapping, External Services and Manage
|
||||||
|
Survey permissions (confirmed still false - never actually toggled on)
|
||||||
|
- CREATE_ROLE/UPDATE_ROLE/DELETE_ROLE/etc., CREATE_GLACCOUNT, CREATE_CHARGE,
|
||||||
|
CREATE_LOANPRODUCT, CREATE_SAVINGSPRODUCT and other permissions observed to already be
|
||||||
|
true in the live DB but belonging to Accounting/Products/Roles modules, not Admin ->
|
||||||
|
Organization or Admin -> System - out of scope for this change and left as pre-existing
|
||||||
|
drift for a separate decision. -->
|
||||||
|
<changeSet id="0588_persist_manually_enabled_maker_checker_permissions" author="cbs-anshul">
|
||||||
|
<update tableName="m_permission">
|
||||||
|
<column name="can_maker_checker" valueBoolean="true"/>
|
||||||
|
<where>code IN (
|
||||||
|
'UPDATE_CURRENCY',
|
||||||
|
'CREATE_FUND',
|
||||||
|
'UPDATE_WORKINGDAYS',
|
||||||
|
'CREATE_PAYMENTTYPE', 'UPDATE_PAYMENTTYPE', 'DELETE_PAYMENTTYPE',
|
||||||
|
'CREATE_SPONSORBANK', 'UPDATE_SPONSORBANK',
|
||||||
|
'UPDATE_BRANCHPROFILE',
|
||||||
|
'CREATE_DATATABLE', 'UPDATE_DATATABLE', 'DELETE_DATATABLE',
|
||||||
|
'CREATE_CODE', 'UPDATE_CODE', 'DELETE_CODE',
|
||||||
|
'CREATE_CODEVALUE', 'UPDATE_CODEVALUE', 'DELETE_CODEVALUE',
|
||||||
|
'CREATE_REPORT', 'UPDATE_REPORT', 'DELETE_REPORT',
|
||||||
|
'CREATE_HOOK', 'UPDATE_HOOK', 'DELETE_HOOK',
|
||||||
|
'CREATE_ACCOUNTNUMBERFORMAT', 'DELETE_ACCOUNTNUMBERFORMAT',
|
||||||
|
'CREATE_USERAGREEMENT',
|
||||||
|
'UPDATE_BATCH_BUSINESS_STEP'
|
||||||
|
)</where>
|
||||||
|
</update>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
</databaseChangeLog>
|
||||||
+12
-6
@@ -116,7 +116,11 @@ public class CommandSourceServiceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testProcessCommand_NonGLAccount_SuperUser_BypassesMakerChecker() {
|
public void testProcessCommand_NonGLAccount_SuperUser_OwnSubmission_DoesNotBypassMakerChecker() {
|
||||||
|
// A maker who happens to hold CHECKER_SUPER_USER must not auto-approve their own submission -
|
||||||
|
// CHECKER_SUPER_USER only ever lets its holder approve a DIFFERENT user's request (see
|
||||||
|
// PortfolioCommandSourceWritePlatformServiceImpl.validateMakerCheckerTransaction()). This
|
||||||
|
// replaces the old bypass this test used to assert.
|
||||||
NewCommandSourceHandler handler = Mockito.mock(NewCommandSourceHandler.class);
|
NewCommandSourceHandler handler = Mockito.mock(NewCommandSourceHandler.class);
|
||||||
JsonCommand jsonCommand = Mockito.mock(JsonCommand.class);
|
JsonCommand jsonCommand = Mockito.mock(JsonCommand.class);
|
||||||
CommandSource commandSource = Mockito.mock(CommandSource.class);
|
CommandSource commandSource = Mockito.mock(CommandSource.class);
|
||||||
@@ -125,6 +129,8 @@ public class CommandSourceServiceTest {
|
|||||||
Mockito.when(commandSource.getPermissionCode()).thenReturn("WITHDRAW_SAVINGSACCOUNT");
|
Mockito.when(commandSource.getPermissionCode()).thenReturn("WITHDRAW_SAVINGSACCOUNT");
|
||||||
Mockito.when(commandSource.getEntityName()).thenReturn("SAVINGSACCOUNT");
|
Mockito.when(commandSource.getEntityName()).thenReturn("SAVINGSACCOUNT");
|
||||||
Mockito.when(commandSource.getActionName()).thenReturn("WITHDRAW");
|
Mockito.when(commandSource.getActionName()).thenReturn("WITHDRAW");
|
||||||
|
Mockito.when(commandSource.getId()).thenReturn(300L);
|
||||||
|
Mockito.when(commandSource.getResourceId()).thenReturn(400L);
|
||||||
Mockito.when(configurationDomainService.isMakerCheckerEnabledForTask("WITHDRAW_SAVINGSACCOUNT")).thenReturn(true);
|
Mockito.when(configurationDomainService.isMakerCheckerEnabledForTask("WITHDRAW_SAVINGSACCOUNT")).thenReturn(true);
|
||||||
Mockito.when(superUser.isCheckerSuperUser()).thenReturn(true);
|
Mockito.when(superUser.isCheckerSuperUser()).thenReturn(true);
|
||||||
|
|
||||||
@@ -132,12 +138,12 @@ public class CommandSourceServiceTest {
|
|||||||
.withCommandId(1L).build();
|
.withCommandId(1L).build();
|
||||||
Mockito.when(handler.processCommand(jsonCommand)).thenReturn(dummyResult);
|
Mockito.when(handler.processCommand(jsonCommand)).thenReturn(dummyResult);
|
||||||
|
|
||||||
org.apache.fineract.infrastructure.core.data.CommandProcessingResult result = underTest.processCommand(handler, jsonCommand,
|
Assertions.assertThrows(org.apache.fineract.commands.exception.RollbackTransactionNotApprovedException.class, () -> {
|
||||||
commandSource, superUser, false);
|
underTest.processCommand(handler, jsonCommand, commandSource, superUser, false);
|
||||||
|
});
|
||||||
|
|
||||||
Assertions.assertNotNull(result);
|
Mockito.verify(commandSource).markAsAwaitingApproval();
|
||||||
Mockito.verify(commandSource).markAsChecked(superUser);
|
Mockito.verify(commandSource, Mockito.never()).markAsChecked(any());
|
||||||
Mockito.verify(commandSource, Mockito.never()).markAsAwaitingApproval();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
+19
@@ -58,6 +58,9 @@ public class DepositProductData {
|
|||||||
|
|
||||||
// accounting
|
// accounting
|
||||||
protected final EnumOptionData accountingRule;
|
protected final EnumOptionData accountingRule;
|
||||||
|
|
||||||
|
private List<Map<String, Object>> otherGlMappings;
|
||||||
|
private Map<String, List<Map<String, Object>>> otherGlMappingsMap;
|
||||||
protected final Map<String, Object> accountingMappings;
|
protected final Map<String, Object> accountingMappings;
|
||||||
protected final Collection<PaymentTypeToGLAccountMapper> paymentChannelToFundSourceMappings;
|
protected final Collection<PaymentTypeToGLAccountMapper> paymentChannelToFundSourceMappings;
|
||||||
protected final Collection<ChargeToGLAccountMapper> feeToIncomeAccountMappings;
|
protected final Collection<ChargeToGLAccountMapper> feeToIncomeAccountMappings;
|
||||||
@@ -453,4 +456,20 @@ public class DepositProductData {
|
|||||||
public EnumOptionData getInterestCompoundingPeriodType() {
|
public EnumOptionData getInterestCompoundingPeriodType() {
|
||||||
return interestCompoundingPeriodType;
|
return interestCompoundingPeriodType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<Map<String, Object>> getOtherGlMappings() {
|
||||||
|
return this.otherGlMappings;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOtherGlMappings(List<Map<String, Object>> otherGlMappings) {
|
||||||
|
this.otherGlMappings = otherGlMappings;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, List<Map<String, Object>>> getOtherGlMappingsMap() {
|
||||||
|
return this.otherGlMappingsMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOtherGlMappingsMap(Map<String, List<Map<String, Object>>> otherGlMappingsMap) {
|
||||||
|
this.otherGlMappingsMap = otherGlMappingsMap;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-1
@@ -244,7 +244,7 @@ public final class FixedDepositProductData extends DepositProductData {
|
|||||||
final Collection<EnumOptionData> periodFrequencyTypeOptions = null;
|
final Collection<EnumOptionData> periodFrequencyTypeOptions = null;
|
||||||
final Collection<TaxGroupData> taxGroupOptions = null;
|
final Collection<TaxGroupData> taxGroupOptions = null;
|
||||||
|
|
||||||
return new FixedDepositProductData(depositProductData.id, depositProductData.name, depositProductData.shortName,
|
final FixedDepositProductData result = new FixedDepositProductData(depositProductData.id, depositProductData.name, depositProductData.shortName,
|
||||||
depositProductData.description, depositProductData.currency, depositProductData.nominalAnnualInterestRate,
|
depositProductData.description, depositProductData.currency, depositProductData.nominalAnnualInterestRate,
|
||||||
depositProductData.interestCompoundingPeriodType, depositProductData.interestPostingPeriodType,
|
depositProductData.interestCompoundingPeriodType, depositProductData.interestPostingPeriodType,
|
||||||
depositProductData.interestCalculationType, depositProductData.interestCalculationDaysInYearType,
|
depositProductData.interestCalculationType, depositProductData.interestCalculationDaysInYearType,
|
||||||
@@ -258,6 +258,9 @@ public final class FixedDepositProductData extends DepositProductData {
|
|||||||
minDepositTerm, maxDepositTerm, minDepositTermType, maxDepositTermType, inMultiplesOfDepositTerm,
|
minDepositTerm, maxDepositTerm, minDepositTermType, maxDepositTermType, inMultiplesOfDepositTerm,
|
||||||
inMultiplesOfDepositTermType, minDepositAmount, depositAmount, maxDepositAmount, periodFrequencyTypeOptions,
|
inMultiplesOfDepositTermType, minDepositAmount, depositAmount, maxDepositAmount, periodFrequencyTypeOptions,
|
||||||
depositProductData.withHoldTax, depositProductData.taxGroup, taxGroupOptions);
|
depositProductData.withHoldTax, depositProductData.taxGroup, taxGroupOptions);
|
||||||
|
result.setOtherGlMappings(depositProductData.getOtherGlMappings());
|
||||||
|
result.setOtherGlMappingsMap(depositProductData.getOtherGlMappingsMap());
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static FixedDepositProductData lookup(final Long id, final String name) {
|
public static FixedDepositProductData lookup(final Long id, final String name) {
|
||||||
@@ -609,6 +612,8 @@ public final class FixedDepositProductData extends DepositProductData {
|
|||||||
this.balanceFrequency = src.balanceFrequency;
|
this.balanceFrequency = src.balanceFrequency;
|
||||||
this.withdrawalFeeForTransfers = src.withdrawalFeeForTransfers;
|
this.withdrawalFeeForTransfers = src.withdrawalFeeForTransfers;
|
||||||
this.renewalGracePeriodDays = src.renewalGracePeriodDays;
|
this.renewalGracePeriodDays = src.renewalGracePeriodDays;
|
||||||
|
this.setOtherGlMappings(src.getOtherGlMappings());
|
||||||
|
this.setOtherGlMappingsMap(src.getOtherGlMappingsMap());
|
||||||
}
|
}
|
||||||
|
|
||||||
private Boolean withdrawalFeeForTransfers;
|
private Boolean withdrawalFeeForTransfers;
|
||||||
|
|||||||
+9
@@ -52,6 +52,7 @@ import static org.apache.fineract.portfolio.savings.SavingsApiConstants.lockinPe
|
|||||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.minBalanceForInterestCalculationParamName;
|
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.minBalanceForInterestCalculationParamName;
|
||||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.nameParamName;
|
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.nameParamName;
|
||||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.nominalAnnualInterestRateParamName;
|
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.nominalAnnualInterestRateParamName;
|
||||||
|
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.otherGlMappingsParamName;
|
||||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.shortNameParamName;
|
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.shortNameParamName;
|
||||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.taxGroupIdParamName;
|
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.taxGroupIdParamName;
|
||||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.withHoldTaxParamName;
|
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.withHoldTaxParamName;
|
||||||
@@ -247,6 +248,10 @@ public class DepositProductAssembler {
|
|||||||
fixedDepositProduct.setWithdrawalFeeApplicableForTransfer(command.booleanPrimitiveValueOfParameterNamed("withdrawalFeeForTransfers"));
|
fixedDepositProduct.setWithdrawalFeeApplicableForTransfer(command.booleanPrimitiveValueOfParameterNamed("withdrawalFeeForTransfers"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (command.hasParameter(otherGlMappingsParamName)) {
|
||||||
|
fixedDepositProduct.setOtherGlMappings(command.jsonFragment(otherGlMappingsParamName));
|
||||||
|
}
|
||||||
|
|
||||||
fixedDepositProduct.validateDomainRules();
|
fixedDepositProduct.validateDomainRules();
|
||||||
|
|
||||||
return fixedDepositProduct;
|
return fixedDepositProduct;
|
||||||
@@ -402,6 +407,10 @@ public class DepositProductAssembler {
|
|||||||
recurringDepositProduct.setWithdrawalFeeApplicableForTransfer(command.booleanPrimitiveValueOfParameterNamed("withdrawalFeeForTransfers"));
|
recurringDepositProduct.setWithdrawalFeeApplicableForTransfer(command.booleanPrimitiveValueOfParameterNamed("withdrawalFeeForTransfers"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (command.hasParameter(otherGlMappingsParamName)) {
|
||||||
|
recurringDepositProduct.setOtherGlMappings(command.jsonFragment(otherGlMappingsParamName));
|
||||||
|
}
|
||||||
|
|
||||||
recurringDepositProduct.validateDomainRules();
|
recurringDepositProduct.validateDomainRules();
|
||||||
|
|
||||||
return recurringDepositProduct;
|
return recurringDepositProduct;
|
||||||
|
|||||||
+4
@@ -1123,6 +1123,10 @@ public class SavingsProduct extends AbstractPersistableCustom<Long> {
|
|||||||
return this.otherGlMappings;
|
return this.otherGlMappings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setOtherGlMappings(final String otherGlMappings) {
|
||||||
|
this.otherGlMappings = otherGlMappings;
|
||||||
|
}
|
||||||
|
|
||||||
// Enhanced Savings Product Getters and Setters
|
// Enhanced Savings Product Getters and Setters
|
||||||
public String getAccountType() {
|
public String getAccountType() {
|
||||||
return accountType;
|
return accountType;
|
||||||
|
|||||||
+137
-19
@@ -30,6 +30,7 @@ import java.util.ArrayList;
|
|||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
@@ -96,6 +97,63 @@ public class SavingsSchedularInterestPoster {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private final Map<String, Long> namedGlAccountCache = new HashMap<>();
|
||||||
|
|
||||||
|
private Long findNamedGlAccount(final Long savingsProductId, final String... accountNames) {
|
||||||
|
if (savingsProductId == null || accountNames == null || accountNames.length == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (final String accountName : accountNames) {
|
||||||
|
if (accountName == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final String cacheKey = savingsProductId + ":" + accountName.trim().toLowerCase();
|
||||||
|
if (this.namedGlAccountCache.containsKey(cacheKey)) {
|
||||||
|
final Long cached = this.namedGlAccountCache.get(cacheKey);
|
||||||
|
if (cached != null) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final List<Long> found = this.jdbcTemplate.queryForList("""
|
||||||
|
SELECT gl.id
|
||||||
|
FROM (SELECT other_gl_mappings::jsonb AS doc
|
||||||
|
FROM m_savings_product
|
||||||
|
WHERE id = ?
|
||||||
|
AND other_gl_mappings IS NOT NULL
|
||||||
|
AND other_gl_mappings <> ''
|
||||||
|
AND other_gl_mappings LIKE '{%') sp
|
||||||
|
CROSS JOIN LATERAL jsonb_array_elements(
|
||||||
|
COALESCE(sp.doc -> 'assets', '[]'::jsonb)
|
||||||
|
|| COALESCE(sp.doc -> 'liabilities', '[]'::jsonb)
|
||||||
|
|| COALESCE(sp.doc -> 'expenses', '[]'::jsonb)
|
||||||
|
|| COALESCE(sp.doc -> 'income', '[]'::jsonb)
|
||||||
|
) AS m(entry)
|
||||||
|
JOIN acc_gl_account gl ON gl.gl_code = m.entry ->> 'glCode'
|
||||||
|
WHERE lower(trim(m.entry ->> 'accountName')) = lower(trim(?))
|
||||||
|
LIMIT 1
|
||||||
|
""", Long.class, savingsProductId, accountName);
|
||||||
|
if (!found.isEmpty()) {
|
||||||
|
this.namedGlAccountCache.put(cacheKey, found.get(0));
|
||||||
|
return found.get(0);
|
||||||
|
} else {
|
||||||
|
this.namedGlAccountCache.put(cacheKey, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addJournalEntryLeg(final List<Object[]> params, final Long accountId, final Long officeId,
|
||||||
|
final String currencyCode, final String transactionId, final Long savingsTransactionId,
|
||||||
|
final LocalDate transactionDate, final JournalEntryType entryType, final BigDecimal amount,
|
||||||
|
final Long savingsAccountId, final OffsetDateTime auditDatetime, final Long userId,
|
||||||
|
final LocalDate businessDate) {
|
||||||
|
params.add(new Object[] { accountId, officeId, null, currencyCode, transactionId,
|
||||||
|
savingsTransactionId, null, false, null, false, transactionDate, entryType.getValue().longValue(),
|
||||||
|
amount, null, entryType.getValue().longValue(), savingsAccountId, auditDatetime, auditDatetime, false,
|
||||||
|
BigDecimal.ZERO, BigDecimal.ZERO, null, transactionDate, null, userId, userId, businessDate });
|
||||||
|
}
|
||||||
|
|
||||||
private void batchUpdateJournalEntries(final List<SavingsAccountData> savingsAccountDataList,
|
private void batchUpdateJournalEntries(final List<SavingsAccountData> savingsAccountDataList,
|
||||||
final HashMap<String, SavingsAccountTransactionData> savingsAccountTransactionDataHashMap)
|
final HashMap<String, SavingsAccountTransactionData> savingsAccountTransactionDataHashMap)
|
||||||
throws DataAccessException, NullPointerException {
|
throws DataAccessException, NullPointerException {
|
||||||
@@ -114,26 +172,86 @@ public class SavingsSchedularInterestPoster {
|
|||||||
savingsAccountTransactionData.setId(dataFromFetch.getId());
|
savingsAccountTransactionData.setId(dataFromFetch.getId());
|
||||||
if (savingsAccountData.getGlAccountIdForSavingsControl() != 0
|
if (savingsAccountData.getGlAccountIdForSavingsControl() != 0
|
||||||
&& savingsAccountData.getGlAccountIdForInterestOnSavings() != 0) {
|
&& savingsAccountData.getGlAccountIdForInterestOnSavings() != 0) {
|
||||||
OffsetDateTime auditDatetime = DateUtils.getAuditOffsetDateTime();
|
final OffsetDateTime auditDatetime = DateUtils.getAuditOffsetDateTime();
|
||||||
paramsForGLInsertion.add(
|
final LocalDate businessDate = DateUtils.getBusinessLocalDate();
|
||||||
new Object[] { savingsAccountTransactionData.getAccountCredit(), savingsAccountData.getOfficeId(), null,
|
final String txnIdentifier = SAVINGS_TRANSACTION_IDENTIFIER + savingsAccountTransactionData.getId().toString();
|
||||||
currencyCode, SAVINGS_TRANSACTION_IDENTIFIER + savingsAccountTransactionData.getId().toString(),
|
final Long savingsTxnId = savingsAccountTransactionData.getId();
|
||||||
savingsAccountTransactionData.getId(), null, false, null, false,
|
final LocalDate txnDate = savingsAccountTransactionData.getTransactionDate();
|
||||||
savingsAccountTransactionData.getTransactionDate(), JournalEntryType.CREDIT.getValue().longValue(),
|
final BigDecimal amount = savingsAccountTransactionData.getAmount();
|
||||||
savingsAccountTransactionData.getAmount(), null, JournalEntryType.CREDIT.getValue().longValue(),
|
final Long savingsAccountId = savingsAccountData.getId();
|
||||||
savingsAccountData.getId(), auditDatetime, auditDatetime, false, BigDecimal.ZERO, BigDecimal.ZERO,
|
final Long officeId = savingsAccountData.getOfficeId();
|
||||||
null, savingsAccountTransactionData.getTransactionDate(), null, userId, userId,
|
|
||||||
DateUtils.getBusinessLocalDate() });
|
|
||||||
|
|
||||||
paramsForGLInsertion
|
if (Boolean.TRUE.equals(isOverdraft)) {
|
||||||
.add(new Object[] { savingsAccountTransactionData.getAccountDebit(), savingsAccountData.getOfficeId(), null,
|
addJournalEntryLeg(paramsForGLInsertion, savingsAccountTransactionData.getAccountCredit(), officeId,
|
||||||
currencyCode, SAVINGS_TRANSACTION_IDENTIFIER + savingsAccountTransactionData.getId().toString(),
|
currencyCode, txnIdentifier, savingsTxnId, txnDate, JournalEntryType.CREDIT, amount,
|
||||||
savingsAccountTransactionData.getId(), null, false, null, false,
|
savingsAccountId, auditDatetime, userId, businessDate);
|
||||||
savingsAccountTransactionData.getTransactionDate(), JournalEntryType.DEBIT.getValue().longValue(),
|
addJournalEntryLeg(paramsForGLInsertion, savingsAccountTransactionData.getAccountDebit(), officeId,
|
||||||
savingsAccountTransactionData.getAmount(), null, JournalEntryType.DEBIT.getValue().longValue(),
|
currencyCode, txnIdentifier, savingsTxnId, txnDate, JournalEntryType.DEBIT, amount,
|
||||||
savingsAccountData.getId(), auditDatetime, auditDatetime, false, BigDecimal.ZERO, BigDecimal.ZERO,
|
savingsAccountId, auditDatetime, userId, businessDate);
|
||||||
null, savingsAccountTransactionData.getTransactionDate(), null, userId, userId,
|
} else {
|
||||||
DateUtils.getBusinessLocalDate() });
|
final Long expenseAccountId = savingsAccountData.getGlAccountIdForInterestOnSavings();
|
||||||
|
final Long controlAccountId = savingsAccountData.getGlAccountIdForSavingsControl();
|
||||||
|
Long payableAccountId = savingsAccountData.getGlAccountIdForInterestPayable();
|
||||||
|
final Long productId = savingsAccountData.getSavingsProductId() != null
|
||||||
|
? savingsAccountData.getSavingsProductId()
|
||||||
|
: savingsAccountData.getProductId();
|
||||||
|
|
||||||
|
if (payableAccountId == null || payableAccountId == 0) {
|
||||||
|
payableAccountId = findNamedGlAccount(productId,
|
||||||
|
"Fixed Deposit Interest Payable", "Savings Interest Payable", "Recurring Deposit Interest Payable",
|
||||||
|
"Interest Payable", "Interest Payable Account");
|
||||||
|
}
|
||||||
|
|
||||||
|
final Long paidAccountId = findNamedGlAccount(productId,
|
||||||
|
"Fixed Deposit Interest Paid", "Savings Interest Paid", "Recurring Deposit Interest Paid",
|
||||||
|
"Interest Paid", "Interest Paid Account");
|
||||||
|
|
||||||
|
final boolean hasPayable = payableAccountId != null && payableAccountId != 0 && !payableAccountId.equals(controlAccountId);
|
||||||
|
final boolean hasPaid = paidAccountId != null && paidAccountId != 0 && !paidAccountId.equals(payableAccountId);
|
||||||
|
|
||||||
|
log.info("SavingsSchedularInterestPoster: accountId={}, productId={}, expense={}, payable={}, paid={}, control={}, amount={}",
|
||||||
|
savingsAccountId, productId, expenseAccountId, payableAccountId, paidAccountId, controlAccountId, amount);
|
||||||
|
|
||||||
|
if (hasPayable && hasPaid) {
|
||||||
|
// Stage 1: Dr Interest Expense / Cr Interest Payable
|
||||||
|
addJournalEntryLeg(paramsForGLInsertion, expenseAccountId, officeId, currencyCode, txnIdentifier,
|
||||||
|
savingsTxnId, txnDate, JournalEntryType.DEBIT, amount, savingsAccountId, auditDatetime, userId, businessDate);
|
||||||
|
addJournalEntryLeg(paramsForGLInsertion, payableAccountId, officeId, currencyCode, txnIdentifier,
|
||||||
|
savingsTxnId, txnDate, JournalEntryType.CREDIT, amount, savingsAccountId, auditDatetime, userId, businessDate);
|
||||||
|
|
||||||
|
// Stage 2: Dr Interest Payable / Cr Interest Paid
|
||||||
|
addJournalEntryLeg(paramsForGLInsertion, payableAccountId, officeId, currencyCode, txnIdentifier,
|
||||||
|
savingsTxnId, txnDate, JournalEntryType.DEBIT, amount, savingsAccountId, auditDatetime, userId, businessDate);
|
||||||
|
addJournalEntryLeg(paramsForGLInsertion, paidAccountId, officeId, currencyCode, txnIdentifier,
|
||||||
|
savingsTxnId, txnDate, JournalEntryType.CREDIT, amount, savingsAccountId, auditDatetime, userId, businessDate);
|
||||||
|
|
||||||
|
// Stage 3: Dr Interest Paid / Cr Savings Control
|
||||||
|
addJournalEntryLeg(paramsForGLInsertion, paidAccountId, officeId, currencyCode, txnIdentifier,
|
||||||
|
savingsTxnId, txnDate, JournalEntryType.DEBIT, amount, savingsAccountId, auditDatetime, userId, businessDate);
|
||||||
|
addJournalEntryLeg(paramsForGLInsertion, controlAccountId, officeId, currencyCode, txnIdentifier,
|
||||||
|
savingsTxnId, txnDate, JournalEntryType.CREDIT, amount, savingsAccountId, auditDatetime, userId, businessDate);
|
||||||
|
} else if (hasPayable) {
|
||||||
|
// Stage 1: Dr Interest Expense / Cr Interest Payable
|
||||||
|
addJournalEntryLeg(paramsForGLInsertion, expenseAccountId, officeId, currencyCode, txnIdentifier,
|
||||||
|
savingsTxnId, txnDate, JournalEntryType.DEBIT, amount, savingsAccountId, auditDatetime, userId, businessDate);
|
||||||
|
addJournalEntryLeg(paramsForGLInsertion, payableAccountId, officeId, currencyCode, txnIdentifier,
|
||||||
|
savingsTxnId, txnDate, JournalEntryType.CREDIT, amount, savingsAccountId, auditDatetime, userId, businessDate);
|
||||||
|
|
||||||
|
// Stage 2: Dr Interest Payable / Cr Savings Control
|
||||||
|
addJournalEntryLeg(paramsForGLInsertion, payableAccountId, officeId, currencyCode, txnIdentifier,
|
||||||
|
savingsTxnId, txnDate, JournalEntryType.DEBIT, amount, savingsAccountId, auditDatetime, userId, businessDate);
|
||||||
|
addJournalEntryLeg(paramsForGLInsertion, controlAccountId, officeId, currencyCode, txnIdentifier,
|
||||||
|
savingsTxnId, txnDate, JournalEntryType.CREDIT, amount, savingsAccountId, auditDatetime, userId, businessDate);
|
||||||
|
} else {
|
||||||
|
// Standard 2-leg entry: Dr Interest Expense / Cr Savings Control
|
||||||
|
addJournalEntryLeg(paramsForGLInsertion, savingsAccountTransactionData.getAccountCredit(), officeId,
|
||||||
|
currencyCode, txnIdentifier, savingsTxnId, txnDate, JournalEntryType.CREDIT, amount,
|
||||||
|
savingsAccountId, auditDatetime, userId, businessDate);
|
||||||
|
addJournalEntryLeg(paramsForGLInsertion, savingsAccountTransactionData.getAccountDebit(), officeId,
|
||||||
|
currencyCode, txnIdentifier, savingsTxnId, txnDate, JournalEntryType.DEBIT, amount,
|
||||||
|
savingsAccountId, auditDatetime, userId, businessDate);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user