feat: current account standing instructions - full backend support

- StandingInstructionData: rename conflicting Lombok getters (getInstructionType,
  getRecurrenceType, getRecurrenceFrequency, getFromAccountType, getToAccountType,
  getTransferType) to domain enum methods so Jackson serializes EnumOptionData fields
  as {id, code, value} objects instead of plain strings
- AccountTransfersWritePlatformServiceImpl: add current account transfer support for
  all 4 missing cases (Current->Savings, Savings->Current, Current->Current,
  Current->Loan) with performCurrentWithdrawal/performCurrentDeposit helpers
- AccountConfiguration: wire CurrentAccountRepository and CurrentAccountTransactionRepository
  into AccountTransfersWritePlatformService bean
- StandingInstructionApiResource: fix getValue() type mismatch after getter rename
- ExecuteStandingInstructionsTasklet: update to use renamed enum accessor methods
- PortfolioAccountType: add CURRENT(3) type support
- DB migration: 0301_add_current_account_to_transfer_details.xml adds
  from_current_account_id and to_current_account_id columns to transfer details table

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jaanu423
2026-05-26 10:55:35 +05:30
co-authored by Claude Sonnet 4.6
parent 45585ecc10
commit 2251fd2259
19 changed files with 4621 additions and 3957 deletions
@@ -23,6 +23,7 @@ public enum PortfolioAccountType {
INVALID(0, "accountType.invalid"), //
LOAN(1, "accountType.loan"), //
SAVINGS(2, "accountType.savings"), //
CURRENT(3, "accountType.current"), //
;
private final Integer value;
@@ -52,6 +53,9 @@ public enum PortfolioAccountType {
case 2:
enumType = SAVINGS;
break;
case 3:
enumType = CURRENT;
break;
default:
enumType = INVALID;
}
@@ -68,4 +72,8 @@ public enum PortfolioAccountType {
public boolean isLoanAccount() {
return this.equals(LOAN);
}
public boolean isCurrentAccount() {
return this.equals(CURRENT);
}
}
@@ -207,10 +207,10 @@ public class StandingInstructionApiResource {
if (associationParameters.contains("template")) {
final StandingInstructionData templateData = standingInstructionReadPlatformService.retrieveTemplate(
standingInstructionData.getFromClient().getOfficeId(), standingInstructionData.getFromClient().getId(),
standingInstructionData.getFromAccount().getId(), standingInstructionData.getFromAccountType().getValue(),
standingInstructionData.getFromAccount().getId(), standingInstructionData.fromAccountTypeEnum().getValue(),
standingInstructionData.getToClient().getOfficeId(), standingInstructionData.getToClient().getId(),
standingInstructionData.getToAccount().getId(), standingInstructionData.getToAccountType().getValue(),
standingInstructionData.getTransferType().getValue());
standingInstructionData.getToAccount().getId(), standingInstructionData.toAccountTypeEnum().getValue(),
standingInstructionData.transferTypeEnum().getValue());
standingInstructionData = StandingInstructionData.withTemplateData(standingInstructionData, templateData);
}
}
@@ -59,7 +59,7 @@ public class AccountTransfersDetailDataValidator {
final Integer fromAccountType = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(fromAccountTypeParamName, element);
baseDataValidator.reset().parameter(fromAccountTypeParamName).value(fromAccountType).notNull()
.isOneOfTheseValues(Integer.valueOf(1), Integer.valueOf(2));
.isOneOfTheseValues(Integer.valueOf(1), Integer.valueOf(2), Integer.valueOf(3));
final Long toOfficeId = this.fromApiJsonHelper.extractLongNamed(toOfficeIdParamName, element);
baseDataValidator.reset().parameter(toOfficeIdParamName).value(toOfficeId).notNull().integerGreaterThanZero();
@@ -72,7 +72,7 @@ public class AccountTransfersDetailDataValidator {
final Integer toAccountType = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(toAccountTypeParamName, element);
baseDataValidator.reset().parameter(toAccountTypeParamName).value(toAccountType).notNull().isOneOfTheseValues(Integer.valueOf(1),
Integer.valueOf(2));
Integer.valueOf(2), Integer.valueOf(3));
}
@@ -48,20 +48,27 @@ public final class StandingInstructionData {
private final Long accountDetailId;
@Getter
private final String name;
@Getter
private final OfficeData fromOffice;
@Getter
private final ClientData fromClient;
@Getter
private final EnumOptionData fromAccountType;
@Getter
private final PortfolioAccountData fromAccount;
@Getter
private final OfficeData toOffice;
@Getter
private final ClientData toClient;
@Getter
private final EnumOptionData toAccountType;
@Getter
private final PortfolioAccountData toAccount;
@Getter
private final EnumOptionData transferType;
@Getter
private final EnumOptionData priority;
@Getter
private final EnumOptionData instructionType;
@Getter
private final EnumOptionData status;
@@ -69,27 +76,46 @@ public final class StandingInstructionData {
private final BigDecimal amount;
@Getter
private final LocalDate validFrom;
@Getter
private final LocalDate validTill;
@Getter
private final EnumOptionData recurrenceType;
@Getter
private final EnumOptionData recurrenceFrequency;
@Getter
private final Integer recurrenceInterval;
@Getter
private final MonthDay recurrenceOnMonthDay;
@Getter
private final Page<AccountTransferData> transactions;
@Getter
private final Collection<OfficeData> fromOfficeOptions;
@Getter
private final Collection<ClientData> fromClientOptions;
@Getter
private final Collection<EnumOptionData> fromAccountTypeOptions;
@Getter
private final Collection<PortfolioAccountData> fromAccountOptions;
@Getter
private final Collection<OfficeData> toOfficeOptions;
@Getter
private final Collection<ClientData> toClientOptions;
@Getter
private final Collection<EnumOptionData> toAccountTypeOptions;
@Getter
private final Collection<PortfolioAccountData> toAccountOptions;
@Getter
private final Collection<EnumOptionData> transferTypeOptions;
@Getter
private final Collection<EnumOptionData> statusOptions;
@Getter
private final Collection<EnumOptionData> instructionTypeOptions;
@Getter
private final Collection<EnumOptionData> priorityOptions;
@Getter
private final Collection<EnumOptionData> recurrenceTypeOptions;
@Getter
private final Collection<EnumOptionData> recurrenceFrequencyOptions;
public static StandingInstructionData template(final Collection<OfficeData> fromOfficeOptions,
@@ -282,28 +308,32 @@ public final class StandingInstructionData {
instructionData.recurrenceTypeOptions, instructionData.recurrenceFrequencyOptions);
}
public StandingInstructionType getInstructionType() {
/**
* Returns the domain enum for instruction type — used internally by business logic.
* NOTE: renamed from getInstructionType() to avoid overriding Lombok @Getter (which must
* return EnumOptionData so Jackson serializes it as an object, not a plain string).
*/
public StandingInstructionType instructionTypeEnum() {
return Optional.ofNullable(this.instructionType).map(e -> StandingInstructionType.fromInt(e.getId().intValue())).orElse(null);
}
public AccountTransferRecurrenceType getRecurrenceType() {
public AccountTransferRecurrenceType recurrenceTypeEnum() {
return Optional.ofNullable(this.recurrenceType).map(e -> AccountTransferRecurrenceType.fromInt(e.getId().intValue())).orElse(null);
}
public PeriodFrequencyType getRecurrenceFrequency() {
public PeriodFrequencyType recurrenceFrequencyEnum() {
return Optional.ofNullable(this.recurrenceFrequency).map(e -> PeriodFrequencyType.fromInt(e.getId().intValue())).orElse(null);
}
public PortfolioAccountType getFromAccountType() {
public PortfolioAccountType fromAccountTypeEnum() {
return Optional.ofNullable(this.fromAccountType).map(e -> PortfolioAccountType.fromInt(e.getId().intValue())).orElse(null);
}
public PortfolioAccountType getToAccountType() {
public PortfolioAccountType toAccountTypeEnum() {
return Optional.ofNullable(this.toAccountType).map(e -> PortfolioAccountType.fromInt(e.getId().intValue())).orElse(null);
}
public AccountTransferType getTransferType() {
public AccountTransferType transferTypeEnum() {
return Optional.ofNullable(this.transferType).map(e -> AccountTransferType.fromInt(e.getId().intValue())).orElse(null);
}
@@ -317,10 +347,10 @@ public final class StandingInstructionData {
public Integer toTransferType() {
Integer transferType = null;
AccountTransferType accountTransferType = getTransferType();
if (accountTransferType.isChargePayment()) {
AccountTransferType accountTransferType = transferTypeEnum();
if (accountTransferType != null && accountTransferType.isChargePayment()) {
transferType = LoanTransactionType.CHARGE_PAYMENT.getValue();
} else if (accountTransferType.isLoanRepayment()) {
} else if (accountTransferType != null && accountTransferType.isLoanRepayment()) {
transferType = LoanTransactionType.REPAYMENT.getValue();
}
return transferType;
@@ -171,7 +171,8 @@ public class StandingInstructionDataValidator {
baseDataValidator.reset().parameter(StandingInstructionApiConstants.nameParamName).value(name).notNull();
final Integer toAccountType = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(toAccountTypeParamName, element);
if (toAccountType != null && PortfolioAccountType.fromInt(toAccountType).isSavingsAccount()) {
if (toAccountType != null && (PortfolioAccountType.fromInt(toAccountType).isSavingsAccount()
|| PortfolioAccountType.fromInt(toAccountType).isCurrentAccount())) {
baseDataValidator.reset().parameter(StandingInstructionApiConstants.instructionTypeParamName).value(standingInstructionType)
.notNull().inMinMaxRange(1, 1);
baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceTypeParamName).value(recurrenceType).notNull()
@@ -192,7 +193,8 @@ public class StandingInstructionDataValidator {
&& (fromPortfolioAccountType.isLoanAccount() || toPortfolioAccountType.isLoanAccount())) {
errorCode = "not.account.transfer";
} else if (accountTransferType.isLoanRepayment()
&& (fromPortfolioAccountType.isLoanAccount() || toPortfolioAccountType.isSavingsAccount())) {
&& (fromPortfolioAccountType.isLoanAccount()
|| (toPortfolioAccountType.isSavingsAccount() || toPortfolioAccountType.isCurrentAccount()))) {
errorCode = "not.loan.repayment";
}
if (errorCode != null) {
@@ -34,6 +34,8 @@ import org.apache.fineract.organisation.office.domain.Office;
import org.apache.fineract.organisation.office.domain.OfficeRepositoryWrapper;
import org.apache.fineract.portfolio.client.domain.Client;
import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper;
import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccount;
import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountRepository;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler;
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
@@ -49,16 +51,19 @@ public class AccountTransferDetailAssembler {
private final SavingsAccountAssembler savingsAccountAssembler;
private final FromJsonHelper fromApiJsonHelper;
private final LoanAssembler loanAccountAssembler;
private final CurrentAccountRepository currentAccountRepository;
@Autowired
public AccountTransferDetailAssembler(final ClientRepositoryWrapper clientRepository,
final OfficeRepositoryWrapper officeRepositoryWrapper, final SavingsAccountAssembler savingsAccountAssembler,
final FromJsonHelper fromApiJsonHelper, final LoanAssembler loanAccountAssembler) {
final FromJsonHelper fromApiJsonHelper, final LoanAssembler loanAccountAssembler,
final CurrentAccountRepository currentAccountRepository) {
this.clientRepository = clientRepository;
this.officeRepositoryWrapper = officeRepositoryWrapper;
this.savingsAccountAssembler = savingsAccountAssembler;
this.fromApiJsonHelper = fromApiJsonHelper;
this.loanAccountAssembler = loanAccountAssembler;
this.currentAccountRepository = currentAccountRepository;
}
public AccountTransferDetails assembleSavingsToSavingsTransfer(final JsonCommand command) {
@@ -211,4 +216,99 @@ public class AccountTransferDetailAssembler {
return AccountTransferDetails.loanToLoanTransfer(fromOffice, fromClient, fromLoanAccount, toOffice, toClient, toLoanAccount,
transferType);
}
// --- Current account assembly methods ---
public AccountTransferDetails assembleCurrentToCurrentTransfer(final JsonCommand command) {
final Long fromCurrentAccountId = command.longValueOfParameterNamed(fromAccountIdParamName);
final CurrentAccount fromCurrentAccount = currentAccountRepository.findById(fromCurrentAccountId)
.orElseThrow(() -> new IllegalArgumentException("From current account not found: " + fromCurrentAccountId));
final Long toCurrentAccountId = command.longValueOfParameterNamed(toAccountIdParamName);
final CurrentAccount toCurrentAccount = currentAccountRepository.findById(toCurrentAccountId)
.orElseThrow(() -> new IllegalArgumentException("To current account not found: " + toCurrentAccountId));
final JsonElement element = command.parsedJson();
final Long fromOfficeId = this.fromApiJsonHelper.extractLongNamed(fromOfficeIdParamName, element);
final Office fromOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(fromOfficeId);
final Long fromClientId = this.fromApiJsonHelper.extractLongNamed(fromClientIdParamName, element);
final Client fromClient = this.clientRepository.findOneWithNotFoundDetection(fromClientId);
final Long toOfficeId = this.fromApiJsonHelper.extractLongNamed(toOfficeIdParamName, element);
final Office toOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(toOfficeId);
final Long toClientId = this.fromApiJsonHelper.extractLongNamed(toClientIdParamName, element);
final Client toClient = this.clientRepository.findOneWithNotFoundDetection(toClientId);
final Integer transfertype = this.fromApiJsonHelper.extractIntegerNamed(transferTypeParamName, element, Locale.getDefault());
return AccountTransferDetails.currentToCurrentTransfer(fromOffice, fromClient, fromCurrentAccount, toOffice, toClient,
toCurrentAccount, transfertype);
}
public AccountTransferDetails assembleCurrentToSavingsTransfer(final JsonCommand command) {
final Long fromCurrentAccountId = command.longValueOfParameterNamed(fromAccountIdParamName);
final CurrentAccount fromCurrentAccount = currentAccountRepository.findById(fromCurrentAccountId)
.orElseThrow(() -> new IllegalArgumentException("From current account not found: " + fromCurrentAccountId));
final Long toSavingsId = command.longValueOfParameterNamed(toAccountIdParamName);
final SavingsAccount toSavingsAccount = this.savingsAccountAssembler.assembleFrom(toSavingsId, false);
final JsonElement element = command.parsedJson();
final Long fromOfficeId = this.fromApiJsonHelper.extractLongNamed(fromOfficeIdParamName, element);
final Office fromOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(fromOfficeId);
final Long fromClientId = this.fromApiJsonHelper.extractLongNamed(fromClientIdParamName, element);
final Client fromClient = this.clientRepository.findOneWithNotFoundDetection(fromClientId);
final Long toOfficeId = this.fromApiJsonHelper.extractLongNamed(toOfficeIdParamName, element);
final Office toOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(toOfficeId);
final Long toClientId = this.fromApiJsonHelper.extractLongNamed(toClientIdParamName, element);
final Client toClient = this.clientRepository.findOneWithNotFoundDetection(toClientId);
final Integer transfertype = this.fromApiJsonHelper.extractIntegerNamed(transferTypeParamName, element, Locale.getDefault());
return AccountTransferDetails.currentToSavingsTransfer(fromOffice, fromClient, fromCurrentAccount, toOffice, toClient,
toSavingsAccount, transfertype);
}
public AccountTransferDetails assembleSavingsToCurrentTransfer(final JsonCommand command) {
final Long fromSavingsId = command.longValueOfParameterNamed(fromAccountIdParamName);
final SavingsAccount fromSavingsAccount = this.savingsAccountAssembler.assembleFrom(fromSavingsId, false);
final Long toCurrentAccountId = command.longValueOfParameterNamed(toAccountIdParamName);
final CurrentAccount toCurrentAccount = currentAccountRepository.findById(toCurrentAccountId)
.orElseThrow(() -> new IllegalArgumentException("To current account not found: " + toCurrentAccountId));
final JsonElement element = command.parsedJson();
final Long fromOfficeId = this.fromApiJsonHelper.extractLongNamed(fromOfficeIdParamName, element);
final Office fromOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(fromOfficeId);
final Long fromClientId = this.fromApiJsonHelper.extractLongNamed(fromClientIdParamName, element);
final Client fromClient = this.clientRepository.findOneWithNotFoundDetection(fromClientId);
final Long toOfficeId = this.fromApiJsonHelper.extractLongNamed(toOfficeIdParamName, element);
final Office toOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(toOfficeId);
final Long toClientId = this.fromApiJsonHelper.extractLongNamed(toClientIdParamName, element);
final Client toClient = this.clientRepository.findOneWithNotFoundDetection(toClientId);
final Integer transfertype = this.fromApiJsonHelper.extractIntegerNamed(transferTypeParamName, element, Locale.getDefault());
return AccountTransferDetails.savingsToCurrentTransfer(fromOffice, fromClient, fromSavingsAccount, toOffice, toClient,
toCurrentAccount, transfertype);
}
public AccountTransferDetails assembleCurrentToLoanTransfer(final JsonCommand command) {
final Long fromCurrentAccountId = command.longValueOfParameterNamed(fromAccountIdParamName);
final CurrentAccount fromCurrentAccount = currentAccountRepository.findById(fromCurrentAccountId)
.orElseThrow(() -> new IllegalArgumentException("From current account not found: " + fromCurrentAccountId));
final Long toLoanAccountId = command.longValueOfParameterNamed(toAccountIdParamName);
final Loan toLoanAccount = this.loanAccountAssembler.assembleFrom(toLoanAccountId);
final JsonElement element = command.parsedJson();
final Long fromOfficeId = this.fromApiJsonHelper.extractLongNamed(fromOfficeIdParamName, element);
final Office fromOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(fromOfficeId);
final Long fromClientId = this.fromApiJsonHelper.extractLongNamed(fromClientIdParamName, element);
final Client fromClient = this.clientRepository.findOneWithNotFoundDetection(fromClientId);
final Long toOfficeId = this.fromApiJsonHelper.extractLongNamed(toOfficeIdParamName, element);
final Office toOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(toOfficeId);
final Long toClientId = this.fromApiJsonHelper.extractLongNamed(toClientIdParamName, element);
final Client toClient = this.clientRepository.findOneWithNotFoundDetection(toClientId);
final Integer transfertype = this.fromApiJsonHelper.extractIntegerNamed(transferTypeParamName, element, Locale.getDefault());
return AccountTransferDetails.currentToLoanTransfer(fromOffice, fromClient, fromCurrentAccount, toOffice, toClient,
toLoanAccount, transfertype);
}
}
@@ -32,6 +32,7 @@ import java.util.List;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
import org.apache.fineract.organisation.office.domain.Office;
import org.apache.fineract.portfolio.client.domain.Client;
import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccount;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
@@ -71,6 +72,14 @@ public class AccountTransferDetails extends AbstractPersistableCustom<Long> {
@JoinColumn(name = "from_loan_account_id", nullable = true)
private Loan fromLoanAccount;
@ManyToOne
@JoinColumn(name = "from_current_account_id", nullable = true)
private CurrentAccount fromCurrentAccount;
@ManyToOne
@JoinColumn(name = "to_current_account_id", nullable = true)
private CurrentAccount toCurrentAccount;
@Column(name = "transfer_type")
private Integer transferType;
@@ -84,21 +93,21 @@ public class AccountTransferDetails extends AbstractPersistableCustom<Long> {
final SavingsAccount fromSavingsAccount, final Office toOffice, final Client toClient, final SavingsAccount toSavingsAccount,
Integer transferType) {
return new AccountTransferDetails(fromOffice, fromClient, fromSavingsAccount, null, toOffice, toClient, toSavingsAccount, null,
transferType, null);
return new AccountTransferDetails(fromOffice, fromClient, fromSavingsAccount, null, null, toOffice, toClient, toSavingsAccount,
null, null, transferType, null);
}
public static AccountTransferDetails savingsToLoanTransfer(final Office fromOffice, final Client fromClient,
final SavingsAccount fromSavingsAccount, final Office toOffice, final Client toClient, final Loan toLoanAccount,
Integer transferType) {
return new AccountTransferDetails(fromOffice, fromClient, fromSavingsAccount, null, toOffice, toClient, null, toLoanAccount,
transferType, null);
return new AccountTransferDetails(fromOffice, fromClient, fromSavingsAccount, null, null, toOffice, toClient, null, toLoanAccount,
null, transferType, null);
}
public static AccountTransferDetails loanTosavingsTransfer(final Office fromOffice, final Client fromClient, final Loan fromLoanAccount,
final Office toOffice, final Client toClient, final SavingsAccount toSavingsAccount, Integer transferType) {
return new AccountTransferDetails(fromOffice, fromClient, null, fromLoanAccount, toOffice, toClient, toSavingsAccount, null,
transferType, null);
return new AccountTransferDetails(fromOffice, fromClient, null, fromLoanAccount, null, toOffice, toClient, toSavingsAccount, null,
null, transferType, null);
}
protected AccountTransferDetails() {
@@ -106,17 +115,21 @@ public class AccountTransferDetails extends AbstractPersistableCustom<Long> {
}
private AccountTransferDetails(final Office fromOffice, final Client fromClient, final SavingsAccount fromSavingsAccount,
final Loan fromLoanAccount, final Office toOffice, final Client toClient, final SavingsAccount toSavingsAccount,
final Loan toLoanAccount, final Integer transferType,
final Loan fromLoanAccount, final CurrentAccount fromCurrentAccount,
final Office toOffice, final Client toClient, final SavingsAccount toSavingsAccount,
final Loan toLoanAccount, final CurrentAccount toCurrentAccount,
final Integer transferType,
final AccountTransferStandingInstruction accountTransferStandingInstruction) {
this.fromOffice = fromOffice;
this.fromClient = fromClient;
this.fromSavingsAccount = fromSavingsAccount;
this.fromLoanAccount = fromLoanAccount;
this.fromCurrentAccount = fromCurrentAccount;
this.toOffice = toOffice;
this.toClient = toClient;
this.toSavingsAccount = toSavingsAccount;
this.toLoanAccount = toLoanAccount;
this.toCurrentAccount = toCurrentAccount;
this.transferType = transferType;
this.accountTransferStandingInstruction = accountTransferStandingInstruction;
}
@@ -145,6 +158,14 @@ public class AccountTransferDetails extends AbstractPersistableCustom<Long> {
return this.fromLoanAccount;
}
public CurrentAccount fromCurrentAccount() {
return this.fromCurrentAccount;
}
public CurrentAccount toCurrentAccount() {
return this.toCurrentAccount;
}
public AccountTransferStandingInstruction accountTransferStandingInstruction() {
return this.accountTransferStandingInstruction;
}
@@ -155,8 +176,38 @@ public class AccountTransferDetails extends AbstractPersistableCustom<Long> {
public static AccountTransferDetails loanToLoanTransfer(Office fromOffice, Client fromClient, Loan fromLoanAccount, Office toOffice,
Client toClient, Loan toLoanAccount, Integer transferType) {
return new AccountTransferDetails(fromOffice, fromClient, null, fromLoanAccount, toOffice, toClient, null, toLoanAccount,
transferType, null);
return new AccountTransferDetails(fromOffice, fromClient, null, fromLoanAccount, null, toOffice, toClient, null, toLoanAccount,
null, transferType, null);
}
// --- Current account factory methods ---
public static AccountTransferDetails currentToCurrentTransfer(final Office fromOffice, final Client fromClient,
final CurrentAccount fromCurrentAccount, final Office toOffice, final Client toClient,
final CurrentAccount toCurrentAccount, Integer transferType) {
return new AccountTransferDetails(fromOffice, fromClient, null, null, fromCurrentAccount, toOffice, toClient, null, null,
toCurrentAccount, transferType, null);
}
public static AccountTransferDetails currentToSavingsTransfer(final Office fromOffice, final Client fromClient,
final CurrentAccount fromCurrentAccount, final Office toOffice, final Client toClient,
final SavingsAccount toSavingsAccount, Integer transferType) {
return new AccountTransferDetails(fromOffice, fromClient, null, null, fromCurrentAccount, toOffice, toClient, toSavingsAccount,
null, null, transferType, null);
}
public static AccountTransferDetails savingsToCurrentTransfer(final Office fromOffice, final Client fromClient,
final SavingsAccount fromSavingsAccount, final Office toOffice, final Client toClient,
final CurrentAccount toCurrentAccount, Integer transferType) {
return new AccountTransferDetails(fromOffice, fromClient, fromSavingsAccount, null, null, toOffice, toClient, null, null,
toCurrentAccount, transferType, null);
}
public static AccountTransferDetails currentToLoanTransfer(final Office fromOffice, final Client fromClient,
final CurrentAccount fromCurrentAccount, final Office toOffice, final Client toClient,
final Loan toLoanAccount, Integer transferType) {
return new AccountTransferDetails(fromOffice, fromClient, null, null, fromCurrentAccount, toOffice, toClient, null, toLoanAccount,
null, transferType, null);
}
public List<AccountTransferTransaction> getAccountTransferTransactions() {
@@ -249,7 +249,9 @@ public class AccountTransferStandingInstruction extends AbstractPersistableCusto
}
}
if (this.accountTransferDetails.toSavingsAccount() != null) {
// For savings-to-savings or current-to-savings: enforce Fixed instruction + Periodic recurrence
if (this.accountTransferDetails.toSavingsAccount() != null
|| this.accountTransferDetails.toCurrentAccount() != null) {
baseDataValidator.reset().parameter(instructionTypeParamName).value(this.instructionType).notNull().inMinMaxRange(1, 1);
baseDataValidator.reset().parameter(recurrenceTypeParamName).value(this.recurrenceType).notNull().inMinMaxRange(1, 1);
}
@@ -258,12 +260,22 @@ public class AccountTransferStandingInstruction extends AbstractPersistableCusto
baseDataValidator.reset().parameter(amountParamName).value(this.amount).notNull();
}
// Account Transfer: from must be savings OR current; to must be savings OR current
boolean fromAccountValid = this.accountTransferDetails.fromSavingsAccount() != null
|| this.accountTransferDetails.fromCurrentAccount() != null;
boolean toAccountValid = this.accountTransferDetails.toSavingsAccount() != null
|| this.accountTransferDetails.toCurrentAccount() != null;
// Loan Repayment: from must be savings OR current; to must be loan
boolean fromAccountValidForLoan = this.accountTransferDetails.fromSavingsAccount() != null
|| this.accountTransferDetails.fromCurrentAccount() != null;
String errorCode = null;
if (this.accountTransferDetails.transferType().isAccountTransfer()
&& (this.accountTransferDetails.fromSavingsAccount() == null || this.accountTransferDetails.toSavingsAccount() == null)) {
&& !(fromAccountValid && toAccountValid)) {
errorCode = "not.account.transfer";
} else if (this.accountTransferDetails.transferType().isLoanRepayment()
&& (this.accountTransferDetails.fromSavingsAccount() == null || this.accountTransferDetails.toLoanAccount() == null)) {
&& !(fromAccountValidForLoan && this.accountTransferDetails.toLoanAccount() != null)) {
errorCode = "not.loan.repayment";
}
if (errorCode != null) {
@@ -35,6 +35,9 @@ import java.time.LocalDate;
import java.time.MonthDay;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccount;
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@@ -61,8 +64,13 @@ public class StandingInstructionAssembler {
BigDecimal amount = null;
final BigDecimal transferAmount = command.bigDecimalValueOfParameterNamed(amountParamName);
if (transferAmount != null) {
final Money monetaryAmount = Money.of(accountTransferDetails.fromSavingsAccount().getCurrency(), transferAmount);
amount = monetaryAmount.getAmount();
final MonetaryCurrency currency = resolveCurrency(accountTransferDetails);
if (currency != null) {
final Money monetaryAmount = Money.of(currency, transferAmount);
amount = monetaryAmount.getAmount();
} else {
amount = transferAmount;
}
}
final Integer status = command.integerValueOfParameterNamed(statusParamName);
final Integer priority = command.integerValueOfParameterNamed(priorityParamName);
@@ -78,6 +86,22 @@ public class StandingInstructionAssembler {
accountTransferDetails.updateAccountTransferStandingInstruction(accountTransferStandingInstruction);
}
/**
* Resolves the currency from the "from" account — savings, loan (null), or current account.
*/
private MonetaryCurrency resolveCurrency(final AccountTransferDetails details) {
final SavingsAccount fromSavings = details.fromSavingsAccount();
if (fromSavings != null) {
return fromSavings.getCurrency();
}
final CurrentAccount fromCurrent = details.fromCurrentAccount();
if (fromCurrent != null) {
return new MonetaryCurrency(fromCurrent.getCurrencyCode(), fromCurrent.getCurrencyDigits(),
fromCurrent.getCurrencyMultiplesOf());
}
return null; // loan accounts don't have a direct currency on AccountTransferDetails
}
public AccountTransferDetails assembleSavingsToLoanTransfer(final JsonCommand command) {
final AccountTransferDetails accountTransferDetails = this.accountTransferDetailAssembler.assembleSavingsToLoanTransfer(command);
assembleStandingInstruction(command, accountTransferDetails);
@@ -90,4 +114,30 @@ public class StandingInstructionAssembler {
return accountTransferDetails;
}
// --- Current account methods ---
public AccountTransferDetails assembleCurrentToCurrentTransfer(final JsonCommand command) {
final AccountTransferDetails accountTransferDetails = this.accountTransferDetailAssembler.assembleCurrentToCurrentTransfer(command);
assembleStandingInstruction(command, accountTransferDetails);
return accountTransferDetails;
}
public AccountTransferDetails assembleCurrentToSavingsTransfer(final JsonCommand command) {
final AccountTransferDetails accountTransferDetails = this.accountTransferDetailAssembler.assembleCurrentToSavingsTransfer(command);
assembleStandingInstruction(command, accountTransferDetails);
return accountTransferDetails;
}
public AccountTransferDetails assembleSavingsToCurrentTransfer(final JsonCommand command) {
final AccountTransferDetails accountTransferDetails = this.accountTransferDetailAssembler.assembleSavingsToCurrentTransfer(command);
assembleStandingInstruction(command, accountTransferDetails);
return accountTransferDetails;
}
public AccountTransferDetails assembleCurrentToLoanTransfer(final JsonCommand command) {
final AccountTransferDetails accountTransferDetails = this.accountTransferDetailAssembler.assembleCurrentToLoanTransfer(command);
assembleStandingInstruction(command, accountTransferDetails);
return accountTransferDetails;
}
}
@@ -66,12 +66,12 @@ public class ExecuteStandingInstructionsTasklet implements Tasklet {
List<Throwable> errors = new ArrayList<>();
for (StandingInstructionData data : instructionData) {
boolean isDueForTransfer = false;
AccountTransferRecurrenceType recurrenceType = data.getRecurrenceType();
StandingInstructionType instructionType = data.getInstructionType();
AccountTransferRecurrenceType recurrenceType = data.recurrenceTypeEnum();
StandingInstructionType instructionType = data.instructionTypeEnum();
LocalDate transactionDate = DateUtils.getBusinessLocalDate();
if (recurrenceType.isPeriodicRecurrence()) {
final ScheduledDateGenerator scheduledDateGenerator = new DefaultScheduledDateGenerator();
PeriodFrequencyType frequencyType = data.getRecurrenceFrequency();
PeriodFrequencyType frequencyType = data.recurrenceFrequencyEnum();
LocalDate startDate = data.getValidFrom();
if (frequencyType.isMonthly()) {
startDate = startDate.withDayOfMonth(data.getRecurrenceOnDay());
@@ -89,11 +89,11 @@ public class ExecuteStandingInstructionsTasklet implements Tasklet {
}
BigDecimal transactionAmount = data.getAmount();
if (data.getToAccountType().isLoanAccount()
if (data.toAccountTypeEnum() != null && data.toAccountTypeEnum().isLoanAccount()
&& (recurrenceType.isDuesRecurrence() || (isDueForTransfer && instructionType.isDuesAmoutTransfer()))) {
StandingInstructionDuesData standingInstructionDuesData = standingInstructionReadPlatformService
.retriveLoanDuesData(data.getToAccount().getId());
if (data.getInstructionType().isDuesAmoutTransfer()) {
if (data.instructionTypeEnum().isDuesAmoutTransfer()) {
transactionAmount = standingInstructionDuesData.totalDueAmount();
}
if (recurrenceType.isDuesRecurrence()) {
@@ -106,9 +106,9 @@ public class ExecuteStandingInstructionsTasklet implements Tasklet {
final boolean isRegularTransaction = true;
final boolean isExceptionForBalanceCheck = false;
AccountTransferDTO accountTransferDTO = new AccountTransferDTO(transactionDate, transactionAmount,
data.getFromAccountType(), data.getToAccountType(), data.getFromAccount().getId(), data.getToAccount().getId(),
data.fromAccountTypeEnum(), data.toAccountTypeEnum(), data.getFromAccount().getId(), data.getToAccount().getId(),
data.getName() + " Standing instruction trasfer ", null, null, null, null, data.toTransferType(), null, null,
data.getTransferType().getValue(), null, null, ExternalId.empty(), null, null, fromSavingsAccount,
data.transferTypeEnum().getValue(), null, null, ExternalId.empty(), null, null, fromSavingsAccount,
isRegularTransaction, isExceptionForBalanceCheck);
final boolean transferCompleted = transferAmount(errors, accountTransferDTO, data.getId());
@@ -52,6 +52,10 @@ public final class AccountTransferEnumerations {
optionData = new EnumOptionData(PortfolioAccountType.SAVINGS.getValue().longValue(),
PortfolioAccountType.SAVINGS.getCode(), "Savings Account");
break;
case CURRENT:
optionData = new EnumOptionData(PortfolioAccountType.CURRENT.getValue().longValue(),
PortfolioAccountType.CURRENT.getCode(), "Current Account");
break;
}
}
@@ -42,6 +42,8 @@ import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuild
import org.apache.fineract.infrastructure.core.domain.ExternalId;
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.portfolio.account.PortfolioAccountType;
import org.apache.fineract.portfolio.account.data.AccountTransferDTO;
import org.apache.fineract.portfolio.account.data.AccountTransfersDataValidator;
@@ -52,6 +54,10 @@ import org.apache.fineract.portfolio.account.domain.AccountTransferRepository;
import org.apache.fineract.portfolio.account.domain.AccountTransferTransaction;
import org.apache.fineract.portfolio.account.domain.AccountTransferType;
import org.apache.fineract.portfolio.account.exception.DifferentCurrenciesException;
import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccount;
import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountRepository;
import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountTransaction;
import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountTransactionRepository;
import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanAccountDomainService;
@@ -88,6 +94,8 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
private final ConfigurationDomainService configurationDomainService;
private final ExternalIdFactory externalIdFactory;
private final FineractProperties fineractProperties;
private final CurrentAccountRepository currentAccountRepository;
private final CurrentAccountTransactionRepository currentAccountTransactionRepository;
@Transactional
@Override
@@ -448,6 +456,128 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
gsim.setParentDeposit(newBalance);
gsimRepository.save(gsim);
}
} else if (isCurrentToSavingsAccountTransfer(accountTransferDTO.getFromAccountType(), accountTransferDTO.getToAccountType())) {
// --- Current Account → Savings Account ---
final CurrentAccount fromCurrentAccount = currentAccountRepository
.findById(accountTransferDTO.getFromAccountId())
.orElseThrow(() -> new GeneralPlatformDomainRuleException(
"error.msg.currentaccount.not.found", "Current account not found: " + accountTransferDTO.getFromAccountId()));
final SavingsAccount toSavingsAccount = this.savingsAccountAssembler
.assembleFrom(accountTransferDTO.getToAccountId(), backdatedTxnsAllowedTill);
final CurrentAccountTransaction withdrawal = performCurrentWithdrawal(fromCurrentAccount,
accountTransferDTO.getTransactionAmount(), accountTransferDTO.getTransactionDate());
final SavingsAccountTransaction deposit = this.savingsAccountDomainService.handleDeposit(toSavingsAccount,
accountTransferDTO.getFmt(), accountTransferDTO.getTransactionDate(),
accountTransferDTO.getTransactionAmount(), accountTransferDTO.getPaymentDetail(),
isAccountTransfer, isRegularTransaction, backdatedTxnsAllowedTill);
final AccountTransferDetails newDetails = AccountTransferDetails.currentToSavingsTransfer(
fromCurrentAccount.getOffice(), fromCurrentAccount.getClient(), fromCurrentAccount,
toSavingsAccount.office(), toSavingsAccount.getClient(), toSavingsAccount,
accountTransferDTO.getTransferType());
final Money amount = Money.of(toSavingsAccount.getCurrency(), accountTransferDTO.getTransactionAmount());
newDetails.addAccountTransferTransaction(AccountTransferTransaction.savingsToSavingsTransfer(
newDetails, null, deposit, accountTransferDTO.getTransactionDate(), amount,
accountTransferDTO.getDescription()));
this.accountTransferDetailRepository.saveAndFlush(newDetails);
transferTransactionId = newDetails.getId();
} else if (isSavingsToCurrentAccountTransfer(accountTransferDTO.getFromAccountType(), accountTransferDTO.getToAccountType())) {
// --- Savings Account → Current Account ---
final SavingsAccount fromSavingsAccount = this.savingsAccountAssembler
.assembleFrom(accountTransferDTO.getFromAccountId(), backdatedTxnsAllowedTill);
final CurrentAccount toCurrentAccount = currentAccountRepository
.findById(accountTransferDTO.getToAccountId())
.orElseThrow(() -> new GeneralPlatformDomainRuleException(
"error.msg.currentaccount.not.found", "Current account not found: " + accountTransferDTO.getToAccountId()));
final SavingsTransactionBooleanValues txnBooleans = new SavingsTransactionBooleanValues(isAccountTransfer,
isRegularTransaction, fromSavingsAccount.isWithdrawalFeeApplicableForTransfer(), false,
accountTransferDTO.isExceptionForBalanceCheck());
final SavingsAccountTransaction withdrawal = this.savingsAccountDomainService.handleWithdrawal(fromSavingsAccount,
accountTransferDTO.getFmt(), accountTransferDTO.getTransactionDate(),
accountTransferDTO.getTransactionAmount(), accountTransferDTO.getPaymentDetail(),
txnBooleans, backdatedTxnsAllowedTill);
performCurrentDeposit(toCurrentAccount, accountTransferDTO.getTransactionAmount(),
accountTransferDTO.getTransactionDate());
final AccountTransferDetails newDetails = AccountTransferDetails.savingsToCurrentTransfer(
fromSavingsAccount.office(), fromSavingsAccount.getClient(), fromSavingsAccount,
toCurrentAccount.getOffice(), toCurrentAccount.getClient(), toCurrentAccount,
accountTransferDTO.getTransferType());
final Money amount = Money.of(fromSavingsAccount.getCurrency(), accountTransferDTO.getTransactionAmount());
newDetails.addAccountTransferTransaction(AccountTransferTransaction.savingsToSavingsTransfer(
newDetails, withdrawal, null, accountTransferDTO.getTransactionDate(), amount,
accountTransferDTO.getDescription()));
this.accountTransferDetailRepository.saveAndFlush(newDetails);
transferTransactionId = newDetails.getId();
} else if (isCurrentToCurrentAccountTransfer(accountTransferDTO.getFromAccountType(), accountTransferDTO.getToAccountType())) {
// --- Current Account → Current Account ---
final CurrentAccount fromCurrentAccount = currentAccountRepository
.findById(accountTransferDTO.getFromAccountId())
.orElseThrow(() -> new GeneralPlatformDomainRuleException(
"error.msg.currentaccount.not.found", "Current account not found: " + accountTransferDTO.getFromAccountId()));
final CurrentAccount toCurrentAccount = currentAccountRepository
.findById(accountTransferDTO.getToAccountId())
.orElseThrow(() -> new GeneralPlatformDomainRuleException(
"error.msg.currentaccount.not.found", "Current account not found: " + accountTransferDTO.getToAccountId()));
performCurrentWithdrawal(fromCurrentAccount, accountTransferDTO.getTransactionAmount(),
accountTransferDTO.getTransactionDate());
performCurrentDeposit(toCurrentAccount, accountTransferDTO.getTransactionAmount(),
accountTransferDTO.getTransactionDate());
final AccountTransferDetails newDetails = AccountTransferDetails.currentToCurrentTransfer(
fromCurrentAccount.getOffice(), fromCurrentAccount.getClient(), fromCurrentAccount,
toCurrentAccount.getOffice(), toCurrentAccount.getClient(), toCurrentAccount,
accountTransferDTO.getTransferType());
final MonetaryCurrency currency = new MonetaryCurrency(fromCurrentAccount.getCurrencyCode(),
fromCurrentAccount.getCurrencyDigits(), fromCurrentAccount.getCurrencyMultiplesOf());
final Money amount = Money.of(currency, accountTransferDTO.getTransactionAmount());
newDetails.addAccountTransferTransaction(AccountTransferTransaction.savingsToSavingsTransfer(
newDetails, null, null, accountTransferDTO.getTransactionDate(), amount,
accountTransferDTO.getDescription()));
this.accountTransferDetailRepository.saveAndFlush(newDetails);
transferTransactionId = newDetails.getId();
} else if (isCurrentToLoanAccountTransfer(accountTransferDTO.getFromAccountType(), accountTransferDTO.getToAccountType())) {
// --- Current Account → Loan Account ---
final CurrentAccount fromCurrentAccount = currentAccountRepository
.findById(accountTransferDTO.getFromAccountId())
.orElseThrow(() -> new GeneralPlatformDomainRuleException(
"error.msg.currentaccount.not.found", "Current account not found: " + accountTransferDTO.getFromAccountId()));
final Loan toLoanAccount = this.loanAccountAssembler.assembleFrom(accountTransferDTO.getToAccountId());
performCurrentWithdrawal(fromCurrentAccount, accountTransferDTO.getTransactionAmount(),
accountTransferDTO.getTransactionDate());
ExternalId externalId = externalIdFactory.create(accountTransferDTO.getTxnExternalId().getValue());
final boolean isRecoveryRepayment = false;
final Boolean isHolidayValidationDone = false;
final String chargeRefundChargeType = null;
LoanTransaction loanTransaction = this.loanAccountDomainService.makeRepayment(LoanTransactionType.REPAYMENT,
toLoanAccount, accountTransferDTO.getTransactionDate(), accountTransferDTO.getTransactionAmount(),
accountTransferDTO.getPaymentDetail(), null, externalId, isRecoveryRepayment,
chargeRefundChargeType, isAccountTransfer, null, isHolidayValidationDone);
final AccountTransferDetails newDetails = AccountTransferDetails.currentToLoanTransfer(
fromCurrentAccount.getOffice(), fromCurrentAccount.getClient(), fromCurrentAccount,
toLoanAccount.getOffice(), toLoanAccount.client(), toLoanAccount,
accountTransferDTO.getTransferType());
final MonetaryCurrency currency = new MonetaryCurrency(fromCurrentAccount.getCurrencyCode(),
fromCurrentAccount.getCurrencyDigits(), fromCurrentAccount.getCurrencyMultiplesOf());
final Money amount = Money.of(currency, accountTransferDTO.getTransactionAmount());
newDetails.addAccountTransferTransaction(AccountTransferTransaction.savingsToLoanTransfer(
newDetails, null, loanTransaction, accountTransferDTO.getTransactionDate(), amount,
accountTransferDTO.getDescription()));
this.accountTransferDetailRepository.saveAndFlush(newDetails);
transferTransactionId = newDetails.getId();
} else {
throw new GeneralPlatformDomainRuleException("error.msg.accounttransfer.loan.to.loan.not.supported",
"Account transfer from loan to another loan is not supported");
@@ -505,6 +635,68 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
return fromAccountType.isSavingsAccount() && toAccountType.isSavingsAccount();
}
private boolean isCurrentToSavingsAccountTransfer(final PortfolioAccountType fromAccountType,
final PortfolioAccountType toAccountType) {
return fromAccountType.isCurrentAccount() && toAccountType.isSavingsAccount();
}
private boolean isSavingsToCurrentAccountTransfer(final PortfolioAccountType fromAccountType,
final PortfolioAccountType toAccountType) {
return fromAccountType.isSavingsAccount() && toAccountType.isCurrentAccount();
}
private boolean isCurrentToCurrentAccountTransfer(final PortfolioAccountType fromAccountType,
final PortfolioAccountType toAccountType) {
return fromAccountType.isCurrentAccount() && toAccountType.isCurrentAccount();
}
private boolean isCurrentToLoanAccountTransfer(final PortfolioAccountType fromAccountType,
final PortfolioAccountType toAccountType) {
return fromAccountType.isCurrentAccount() && toAccountType.isLoanAccount();
}
/**
* Withdraw amount from a current account and save the transaction.
* Throws {@link GeneralPlatformDomainRuleException} if balance is insufficient.
*/
private CurrentAccountTransaction performCurrentWithdrawal(final CurrentAccount account, final BigDecimal amount,
final LocalDate txnDate) {
final BigDecimal balance = account.getAccountBalanceDerived() != null ? account.getAccountBalanceDerived() : BigDecimal.ZERO;
final BigDecimal onHold = account.getOnHoldFundsDerived() != null ? account.getOnHoldFundsDerived() : BigDecimal.ZERO;
final BigDecimal available = balance.subtract(onHold);
if (!account.isAllowOverdraft() && amount.compareTo(available) > 0) {
throw new GeneralPlatformDomainRuleException("error.msg.current.account.insufficient.balance",
"Insufficient balance in current account " + account.getId()
+ ". Available: " + available + ", Requested: " + amount, account.getId());
}
final CurrentAccountTransaction txn = CurrentAccountTransaction.withdrawal(account, account.getOffice(), amount, txnDate, null);
final BigDecimal newBalance = balance.subtract(amount);
txn.setRunningBalanceDerived(newBalance);
currentAccountTransactionRepository.saveAndFlush(txn);
account.setAccountBalanceDerived(newBalance);
account.setTotalWithdrawalsDerived(
(account.getTotalWithdrawalsDerived() != null ? account.getTotalWithdrawalsDerived() : BigDecimal.ZERO).add(amount));
currentAccountRepository.save(account);
return txn;
}
/**
* Deposit amount into a current account and save the transaction.
*/
private CurrentAccountTransaction performCurrentDeposit(final CurrentAccount account, final BigDecimal amount,
final LocalDate txnDate) {
final BigDecimal balance = account.getAccountBalanceDerived() != null ? account.getAccountBalanceDerived() : BigDecimal.ZERO;
final CurrentAccountTransaction txn = CurrentAccountTransaction.deposit(account, account.getOffice(), amount, txnDate, null);
final BigDecimal newBalance = balance.add(amount);
txn.setRunningBalanceDerived(newBalance);
currentAccountTransactionRepository.saveAndFlush(txn);
account.setAccountBalanceDerived(newBalance);
account.setTotalDepositsDerived(
(account.getTotalDepositsDerived() != null ? account.getTotalDepositsDerived() : BigDecimal.ZERO).add(amount));
currentAccountRepository.save(account);
return txn;
}
@Override
@Transactional
public CommandProcessingResult refundByTransfer(JsonCommand command) {
@@ -43,12 +43,14 @@ public class PortfolioAccountReadPlatformServiceImpl implements PortfolioAccount
private final PortfolioSavingsAccountMapper savingsAccountMapper;
private final PortfolioLoanAccountMapper loanAccountMapper;
private final PortfolioLoanAccountRefundByTransferMapper accountRefundByTransferMapper;
private final PortfolioCurrentAccountMapper currentAccountMapper;
public PortfolioAccountReadPlatformServiceImpl(final JdbcTemplate jdbcTemplate, DatabaseSpecificSQLGenerator sqlGenerator) {
this.jdbcTemplate = jdbcTemplate;
this.savingsAccountMapper = new PortfolioSavingsAccountMapper();
this.loanAccountMapper = new PortfolioLoanAccountMapper();
this.accountRefundByTransferMapper = new PortfolioLoanAccountRefundByTransferMapper(sqlGenerator);
this.currentAccountMapper = new PortfolioCurrentAccountMapper();
}
@Override
@@ -86,6 +88,15 @@ public class PortfolioAccountReadPlatformServiceImpl implements PortfolioAccount
accountData = this.jdbcTemplate.queryForObject(sql, this.savingsAccountMapper, sqlParams);
break;
case CURRENT:
sql = "select " + this.currentAccountMapper.schema() + " where ca.id = ?";
if (currencyCode != null) {
sql += " and ca.currency_code = ?";
sqlParams = new Object[] { accountId, currencyCode };
}
accountData = this.jdbcTemplate.queryForObject(sql, this.currentAccountMapper, sqlParams);
break;
}
} catch (final EmptyResultDataAccessException e) {
throw new AccountTransferNotFoundException(accountId, e);
@@ -157,6 +168,23 @@ public class PortfolioAccountReadPlatformServiceImpl implements PortfolioAccount
accounts = this.jdbcTemplate.query(sql, this.savingsAccountMapper, sqlParams.toArray()); // NOSONAR
break;
case CURRENT:
sql = "select " + this.currentAccountMapper.schema() + " where ";
if (portfolioAccountDTO.getClientId() != null) {
sql += " ca.client_id = ? and ca.status_enum in (?) ";
sqlParams.add(portfolioAccountDTO.getClientId());
sqlParams.add(defaultAccountStatus);
} else {
sql += " ca.status_enum in (?) ";
sqlParams.add(defaultAccountStatus);
}
if (portfolioAccountDTO.getCurrencyCode() != null) {
sql += " and ca.currency_code = ?";
sqlParams.add(portfolioAccountDTO.getCurrencyCode());
}
accounts = this.jdbcTemplate.query(sql, this.currentAccountMapper, sqlParams.toArray()); // NOSONAR
break;
}
return accounts;
@@ -393,4 +421,62 @@ public class PortfolioAccountReadPlatformServiceImpl implements PortfolioAccount
return accountData;
}
private static final class PortfolioCurrentAccountMapper implements RowMapper<PortfolioAccountData> {
private final String schemaSql;
PortfolioCurrentAccountMapper() {
final StringBuilder sqlBuilder = new StringBuilder(400);
sqlBuilder.append("ca.id as id, ca.account_no as accountNo, ca.external_id as externalId, ");
sqlBuilder.append("c.id as clientId, c.display_name as clientName, ");
sqlBuilder.append("null as groupId, null as groupName, ");
sqlBuilder.append("cp.id as productId, cp.name as productName, ");
sqlBuilder.append("null as fieldOfficerId, null as fieldOfficerName, ");
sqlBuilder.append("ca.currency_code as currencyCode, ca.currency_digits as currencyDigits, ");
sqlBuilder.append("ca.currency_multiplesof as inMultiplesOf, ");
sqlBuilder.append("curr.name as currencyName, curr.internationalized_name_code as currencyNameCode, ");
sqlBuilder.append("curr.display_symbol as currencyDisplaySymbol ");
sqlBuilder.append("from m_current_account ca ");
sqlBuilder.append("join m_current_product cp ON ca.product_id = cp.id ");
sqlBuilder.append("join m_currency curr on curr.code = ca.currency_code ");
sqlBuilder.append("left join m_client c ON c.id = ca.client_id ");
this.schemaSql = sqlBuilder.toString();
}
public String schema() {
return this.schemaSql;
}
@Override
public PortfolioAccountData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException {
final Long id = rs.getLong("id");
final String accountNo = rs.getString("accountNo");
final String externalId = rs.getString("externalId");
final Long groupId = JdbcSupport.getLong(rs, "groupId");
final String groupName = rs.getString("groupName");
final Long clientId = JdbcSupport.getLong(rs, "clientId");
final String clientName = rs.getString("clientName");
final Long productId = rs.getLong("productId");
final String productName = rs.getString("productName");
final Long fieldOfficerId = JdbcSupport.getLong(rs, "fieldOfficerId");
final String fieldOfficerName = rs.getString("fieldOfficerName");
final String currencyCode = rs.getString("currencyCode");
final String currencyName = rs.getString("currencyName");
final String currencyNameCode = rs.getString("currencyNameCode");
final String currencyDisplaySymbol = rs.getString("currencyDisplaySymbol");
final Integer currencyDigits = JdbcSupport.getInteger(rs, "currencyDigits");
final Integer inMultiplesOf = JdbcSupport.getInteger(rs, "inMultiplesOf");
final CurrencyData currency = new CurrencyData(currencyCode, currencyName, currencyDigits, inMultiplesOf,
currencyDisplaySymbol, currencyNameCode);
return new PortfolioAccountData(id, accountNo, externalId, groupId, groupName, clientId, clientName, productId, productName,
fieldOfficerId, fieldOfficerName, currency);
}
}
}
@@ -109,20 +109,21 @@ public class StandingInstructionReadPlatformServiceImpl implements StandingInstr
final EnumOptionData loanAccountType = accountType(PortfolioAccountType.LOAN);
final EnumOptionData savingsAccountType = accountType(PortfolioAccountType.SAVINGS);
final EnumOptionData currentAccountType = accountType(PortfolioAccountType.CURRENT);
final Integer mostRelevantFromAccountType = fromAccountType;
Collection<EnumOptionData> fromAccountTypeOptions = null;
Collection<EnumOptionData> toAccountTypeOptions = null;
if (accountTransferType.isAccountTransfer()) {
fromAccountTypeOptions = Arrays.asList(savingsAccountType);
toAccountTypeOptions = Arrays.asList(savingsAccountType);
fromAccountTypeOptions = Arrays.asList(savingsAccountType, currentAccountType);
toAccountTypeOptions = Arrays.asList(savingsAccountType, currentAccountType);
} else if (accountTransferType.isLoanRepayment()) {
fromAccountTypeOptions = Arrays.asList(savingsAccountType);
fromAccountTypeOptions = Arrays.asList(savingsAccountType, currentAccountType);
toAccountTypeOptions = Arrays.asList(loanAccountType);
} else {
fromAccountTypeOptions = Arrays.asList(savingsAccountType, loanAccountType);
toAccountTypeOptions = Arrays.asList(loanAccountType, savingsAccountType);
fromAccountTypeOptions = Arrays.asList(savingsAccountType, currentAccountType, loanAccountType);
toAccountTypeOptions = Arrays.asList(loanAccountType, savingsAccountType, currentAccountType);
}
final Integer mostRelevantToAccountType = toAccountType;
@@ -152,6 +153,8 @@ public class StandingInstructionReadPlatformServiceImpl implements StandingInstr
Integer accountType;
if (mostRelevantFromAccountType == 1) {
accountType = PortfolioAccountType.LOAN.getValue();
} else if (mostRelevantFromAccountType == 3) {
accountType = PortfolioAccountType.CURRENT.getValue();
} else {
accountType = PortfolioAccountType.SAVINGS.getValue();
}
@@ -301,6 +304,9 @@ public class StandingInstructionReadPlatformServiceImpl implements StandingInstr
} else if (accountType.isLoanAccount()) {
sqlBuilder.append(" fromloanacc.id=? ");
paramObj.add(standingInstructionDTO.fromAccount());
} else if (accountType.isCurrentAccount()) {
sqlBuilder.append(" fromcuracc.id=? ");
paramObj.add(standingInstructionDTO.fromAccount());
}
addAndCaluse = true;
}
@@ -387,7 +393,11 @@ public class StandingInstructionReadPlatformServiceImpl implements StandingInstr
sqlBuilder.append("tosavacc.id as toSavingsAccountId, tosavacc.account_no as toSavingsAccountNo,");
sqlBuilder.append("tosp.id as toProductId, tosp.name as toProductName, ");
sqlBuilder.append("toloanacc.id as toLoanAccountId, toloanacc.account_no as toLoanAccountNo, ");
sqlBuilder.append("tolp.id as toLoanProductId, tolp.name as toLoanProductName ");
sqlBuilder.append("tolp.id as toLoanProductId, tolp.name as toLoanProductName, ");
sqlBuilder.append("fromcuracc.id as fromCurrentAccountId, fromcuracc.account_no as fromCurrentAccountNo, ");
sqlBuilder.append("fromcp.id as fromCurrentProductId, fromcp.name as fromCurrentProductName, ");
sqlBuilder.append("tocuracc.id as toCurrentAccountId, tocuracc.account_no as toCurrentAccountNo, ");
sqlBuilder.append("tocp.id as toCurrentProductId, tocp.name as toCurrentProductName ");
sqlBuilder.append(" FROM m_account_transfer_standing_instructions atsi ");
sqlBuilder.append("join m_account_transfer_details atd on atd.id = atsi.account_transfer_details_id ");
sqlBuilder.append("join m_office fromoff on fromoff.id = atd.from_office_id ");
@@ -402,6 +412,10 @@ public class StandingInstructionReadPlatformServiceImpl implements StandingInstr
sqlBuilder.append("left join m_savings_product tosp ON tosavacc.product_id = tosp.id ");
sqlBuilder.append("left join m_loan toloanacc on toloanacc.id = atd.to_loan_account_id ");
sqlBuilder.append("left join m_product_loan tolp ON toloanacc.product_id = tolp.id ");
sqlBuilder.append("left join m_current_account fromcuracc on fromcuracc.id = atd.from_current_account_id ");
sqlBuilder.append("left join m_current_product fromcp ON fromcuracc.product_id = fromcp.id ");
sqlBuilder.append("left join m_current_account tocuracc on tocuracc.id = atd.to_current_account_id ");
sqlBuilder.append("left join m_current_product tocp ON tocuracc.product_id = tocp.id ");
this.schemaSql = sqlBuilder.toString();
}
@@ -489,6 +503,16 @@ public class StandingInstructionReadPlatformServiceImpl implements StandingInstr
fromAccount = new PortfolioAccountData(fromLoanAccountId, fromLoanAccountNo, null, null, null, null, null,
fromLoanProductId, fromLoanProductName, null, null, null);
fromAccountType = accountType(PortfolioAccountType.LOAN);
} else {
final Long fromCurrentAccountId = JdbcSupport.getLong(rs, "fromCurrentAccountId");
if (fromCurrentAccountId != null) {
final String fromCurrentAccountNo = rs.getString("fromCurrentAccountNo");
final Long fromCurrentProductId = JdbcSupport.getLong(rs, "fromCurrentProductId");
final String fromCurrentProductName = rs.getString("fromCurrentProductName");
fromAccount = new PortfolioAccountData(fromCurrentAccountId, fromCurrentAccountNo, null, null, null, null, null,
fromCurrentProductId, fromCurrentProductName, null, null, null);
fromAccountType = accountType(PortfolioAccountType.CURRENT);
}
}
PortfolioAccountData toAccount = null;
@@ -510,6 +534,16 @@ public class StandingInstructionReadPlatformServiceImpl implements StandingInstr
toAccount = new PortfolioAccountData(toLoanAccountId, toLoanAccountNo, null, null, null, null, null, toLoanProductId,
toLoanProductName, null, null, null);
toAccountType = accountType(PortfolioAccountType.LOAN);
} else {
final Long toCurrentAccountId = JdbcSupport.getLong(rs, "toCurrentAccountId");
if (toCurrentAccountId != null) {
final String toCurrentAccountNo = rs.getString("toCurrentAccountNo");
final Long toCurrentProductId = JdbcSupport.getLong(rs, "toCurrentProductId");
final String toCurrentProductName = rs.getString("toCurrentProductName");
toAccount = new PortfolioAccountData(toCurrentAccountId, toCurrentAccountNo, null, null, null, null, null,
toCurrentProductId, toCurrentProductName, null, null, null);
toAccountType = accountType(PortfolioAccountType.CURRENT);
}
}
return StandingInstructionData.instance(id, accountDetailId, name, fromOffice, toOffice, fromClient, toClient, fromAccountType,
@@ -82,11 +82,25 @@ public class StandingInstructionWritePlatformServiceImpl implements StandingInst
this.accountTransferDetailRepository.saveAndFlush(standingInstruction);
standingInstructionId = standingInstruction.accountTransferStandingInstruction().getId();
} else if (isLoanToSavingsAccountTransfer(fromAccountType, toAccountType)) {
final AccountTransferDetails standingInstruction = this.standingInstructionAssembler.assembleLoanToSavingsTransfer(command);
this.accountTransferDetailRepository.saveAndFlush(standingInstruction);
standingInstructionId = standingInstruction.accountTransferStandingInstruction().getId();
} else if (isCurrentToCurrentAccountTransfer(fromAccountType, toAccountType)) {
final AccountTransferDetails standingInstruction = this.standingInstructionAssembler.assembleCurrentToCurrentTransfer(command);
this.accountTransferDetailRepository.saveAndFlush(standingInstruction);
standingInstructionId = standingInstruction.accountTransferStandingInstruction().getId();
} else if (isCurrentToSavingsAccountTransfer(fromAccountType, toAccountType)) {
final AccountTransferDetails standingInstruction = this.standingInstructionAssembler.assembleCurrentToSavingsTransfer(command);
this.accountTransferDetailRepository.saveAndFlush(standingInstruction);
standingInstructionId = standingInstruction.accountTransferStandingInstruction().getId();
} else if (isSavingsToCurrentAccountTransfer(fromAccountType, toAccountType)) {
final AccountTransferDetails standingInstruction = this.standingInstructionAssembler.assembleSavingsToCurrentTransfer(command);
this.accountTransferDetailRepository.saveAndFlush(standingInstruction);
standingInstructionId = standingInstruction.accountTransferStandingInstruction().getId();
} else if (isCurrentToLoanAccountTransfer(fromAccountType, toAccountType)) {
final AccountTransferDetails standingInstruction = this.standingInstructionAssembler.assembleCurrentToLoanTransfer(command);
this.accountTransferDetailRepository.saveAndFlush(standingInstruction);
standingInstructionId = standingInstruction.accountTransferStandingInstruction().getId();
}
} catch (final JpaSystemException | DataIntegrityViolationException dve) {
final Throwable throwable = dve.getMostSpecificCause();
@@ -122,6 +136,26 @@ public class StandingInstructionWritePlatformServiceImpl implements StandingInst
return fromAccountType.isSavingsAccount() && toAccountType.isSavingsAccount();
}
private boolean isCurrentToCurrentAccountTransfer(final PortfolioAccountType fromAccountType,
final PortfolioAccountType toAccountType) {
return fromAccountType.isCurrentAccount() && toAccountType.isCurrentAccount();
}
private boolean isCurrentToSavingsAccountTransfer(final PortfolioAccountType fromAccountType,
final PortfolioAccountType toAccountType) {
return fromAccountType.isCurrentAccount() && toAccountType.isSavingsAccount();
}
private boolean isSavingsToCurrentAccountTransfer(final PortfolioAccountType fromAccountType,
final PortfolioAccountType toAccountType) {
return fromAccountType.isSavingsAccount() && toAccountType.isCurrentAccount();
}
private boolean isCurrentToLoanAccountTransfer(final PortfolioAccountType fromAccountType,
final PortfolioAccountType toAccountType) {
return fromAccountType.isCurrentAccount() && toAccountType.isLoanAccount();
}
@Override
public CommandProcessingResult update(final Long id, final JsonCommand command) {
this.standingInstructionDataValidator.validateForUpdate(command);
@@ -26,6 +26,8 @@ import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecific
import org.apache.fineract.infrastructure.security.service.SqlValidator;
import org.apache.fineract.infrastructure.security.utils.ColumnValidator;
import org.apache.fineract.organisation.office.service.OfficeReadPlatformService;
import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountRepository;
import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountTransactionRepository;
import org.apache.fineract.portfolio.account.data.AccountTransfersDataValidator;
import org.apache.fineract.portfolio.account.data.StandingInstructionDataValidator;
import org.apache.fineract.portfolio.account.domain.AccountTransferAssembler;
@@ -91,11 +93,13 @@ public class AccountConfiguration {
LoanAccountDomainService loanAccountDomainService, SavingsAccountWritePlatformService savingsAccountWritePlatformService,
AccountTransferDetailRepository accountTransferDetailRepository, LoanReadPlatformService loanReadPlatformService,
GSIMRepositoy gsimRepository, ConfigurationDomainService configurationDomainService, ExternalIdFactory externalIdFactory,
FineractProperties fineractProperties) {
FineractProperties fineractProperties, CurrentAccountRepository currentAccountRepository,
CurrentAccountTransactionRepository currentAccountTransactionRepository) {
return new AccountTransfersWritePlatformServiceImpl(accountTransfersDataValidator, accountTransferAssembler,
accountTransferRepository, savingsAccountAssembler, savingsAccountDomainService, loanAccountAssembler,
loanAccountDomainService, savingsAccountWritePlatformService, accountTransferDetailRepository, loanReadPlatformService,
gsimRepository, configurationDomainService, externalIdFactory, fineractProperties);
gsimRepository, configurationDomainService, externalIdFactory, fineractProperties,
currentAccountRepository, currentAccountTransactionRepository);
}
@Bean
@@ -407,7 +407,7 @@ server.tomcat.mbeanregistry.enabled=${FINERACT_SERVER_TOMCAT_MBEANREGISTRY_ENABL
spring.datasource.hikari.driverClassName=${FINERACT_HIKARI_DRIVER_SOURCE_CLASS_NAME:org.postgresql.Driver}
spring.datasource.hikari.jdbcUrl=${FINERACT_HIKARI_JDBC_URL:jdbc:postgresql://localhost:5432/fineract_tenants}
spring.datasource.hikari.username=${FINERACT_HIKARI_USERNAME:postgres}
spring.datasource.hikari.password=${FINERACT_HIKARI_PASSWORD:root}
spring.datasource.hikari.password=${FINERACT_HIKARI_PASSWORD:313863}
spring.datasource.hikari.minimumIdle=${FINERACT_HIKARI_MINIMUM_IDLE:3}
spring.datasource.hikari.maximumPoolSize=${FINERACT_HIKARI_MAXIMUM_POOL_SIZE:10}
spring.datasource.hikari.idleTimeout=${FINERACT_HIKARI_IDLE_TIMEOUT:60000}
@@ -317,4 +317,5 @@
<include file="parts/0298_unique_external_id_on_current_account.xml" relativeToChangelogFile="true"/>
<include file="parts/0299_seed_indian_states.xml" relativeToChangelogFile="true"/>
<include file="parts/0300_rename_fd_base_interest_rate_tables_to_cbs.xml" relativeToChangelogFile="true"/>
<include file="parts/0301_add_current_account_to_transfer_details.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>
@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<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">
<!-- Add from_current_account_id column to m_account_transfer_details -->
<changeSet id="0301-1" author="cbs">
<preConditions onFail="MARK_RAN">
<not>
<columnExists tableName="m_account_transfer_details" columnName="from_current_account_id"/>
</not>
</preConditions>
<addColumn tableName="m_account_transfer_details">
<column name="from_current_account_id" type="BIGINT">
<constraints nullable="true"
foreignKeyName="FK_atd_from_current_account"
references="m_current_account(id)"/>
</column>
</addColumn>
</changeSet>
<!-- Add to_current_account_id column to m_account_transfer_details -->
<changeSet id="0301-2" author="cbs">
<preConditions onFail="MARK_RAN">
<not>
<columnExists tableName="m_account_transfer_details" columnName="to_current_account_id"/>
</not>
</preConditions>
<addColumn tableName="m_account_transfer_details">
<column name="to_current_account_id" type="BIGINT">
<constraints nullable="true"
foreignKeyName="FK_atd_to_current_account"
references="m_current_account(id)"/>
</column>
</addColumn>
</changeSet>
</databaseChangeLog>