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"), // INVALID(0, "accountType.invalid"), //
LOAN(1, "accountType.loan"), // LOAN(1, "accountType.loan"), //
SAVINGS(2, "accountType.savings"), // SAVINGS(2, "accountType.savings"), //
CURRENT(3, "accountType.current"), //
; ;
private final Integer value; private final Integer value;
@@ -52,6 +53,9 @@ public enum PortfolioAccountType {
case 2: case 2:
enumType = SAVINGS; enumType = SAVINGS;
break; break;
case 3:
enumType = CURRENT;
break;
default: default:
enumType = INVALID; enumType = INVALID;
} }
@@ -68,4 +72,8 @@ public enum PortfolioAccountType {
public boolean isLoanAccount() { public boolean isLoanAccount() {
return this.equals(LOAN); return this.equals(LOAN);
} }
public boolean isCurrentAccount() {
return this.equals(CURRENT);
}
} }
@@ -207,10 +207,10 @@ public class StandingInstructionApiResource {
if (associationParameters.contains("template")) { if (associationParameters.contains("template")) {
final StandingInstructionData templateData = standingInstructionReadPlatformService.retrieveTemplate( final StandingInstructionData templateData = standingInstructionReadPlatformService.retrieveTemplate(
standingInstructionData.getFromClient().getOfficeId(), standingInstructionData.getFromClient().getId(), 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.getToClient().getOfficeId(), standingInstructionData.getToClient().getId(),
standingInstructionData.getToAccount().getId(), standingInstructionData.getToAccountType().getValue(), standingInstructionData.getToAccount().getId(), standingInstructionData.toAccountTypeEnum().getValue(),
standingInstructionData.getTransferType().getValue()); standingInstructionData.transferTypeEnum().getValue());
standingInstructionData = StandingInstructionData.withTemplateData(standingInstructionData, templateData); standingInstructionData = StandingInstructionData.withTemplateData(standingInstructionData, templateData);
} }
} }
@@ -59,7 +59,7 @@ public class AccountTransfersDetailDataValidator {
final Integer fromAccountType = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(fromAccountTypeParamName, element); final Integer fromAccountType = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(fromAccountTypeParamName, element);
baseDataValidator.reset().parameter(fromAccountTypeParamName).value(fromAccountType).notNull() 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); final Long toOfficeId = this.fromApiJsonHelper.extractLongNamed(toOfficeIdParamName, element);
baseDataValidator.reset().parameter(toOfficeIdParamName).value(toOfficeId).notNull().integerGreaterThanZero(); baseDataValidator.reset().parameter(toOfficeIdParamName).value(toOfficeId).notNull().integerGreaterThanZero();
@@ -72,7 +72,7 @@ public class AccountTransfersDetailDataValidator {
final Integer toAccountType = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(toAccountTypeParamName, element); final Integer toAccountType = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(toAccountTypeParamName, element);
baseDataValidator.reset().parameter(toAccountTypeParamName).value(toAccountType).notNull().isOneOfTheseValues(Integer.valueOf(1), 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; private final Long accountDetailId;
@Getter @Getter
private final String name; private final String name;
@Getter
private final OfficeData fromOffice; private final OfficeData fromOffice;
@Getter @Getter
private final ClientData fromClient; private final ClientData fromClient;
@Getter
private final EnumOptionData fromAccountType; private final EnumOptionData fromAccountType;
@Getter @Getter
private final PortfolioAccountData fromAccount; private final PortfolioAccountData fromAccount;
@Getter
private final OfficeData toOffice; private final OfficeData toOffice;
@Getter @Getter
private final ClientData toClient; private final ClientData toClient;
@Getter
private final EnumOptionData toAccountType; private final EnumOptionData toAccountType;
@Getter @Getter
private final PortfolioAccountData toAccount; private final PortfolioAccountData toAccount;
@Getter
private final EnumOptionData transferType; private final EnumOptionData transferType;
@Getter
private final EnumOptionData priority; private final EnumOptionData priority;
@Getter
private final EnumOptionData instructionType; private final EnumOptionData instructionType;
@Getter @Getter
private final EnumOptionData status; private final EnumOptionData status;
@@ -69,27 +76,46 @@ public final class StandingInstructionData {
private final BigDecimal amount; private final BigDecimal amount;
@Getter @Getter
private final LocalDate validFrom; private final LocalDate validFrom;
@Getter
private final LocalDate validTill; private final LocalDate validTill;
@Getter
private final EnumOptionData recurrenceType; private final EnumOptionData recurrenceType;
@Getter
private final EnumOptionData recurrenceFrequency; private final EnumOptionData recurrenceFrequency;
@Getter @Getter
private final Integer recurrenceInterval; private final Integer recurrenceInterval;
@Getter
private final MonthDay recurrenceOnMonthDay; private final MonthDay recurrenceOnMonthDay;
@Getter
private final Page<AccountTransferData> transactions; private final Page<AccountTransferData> transactions;
@Getter
private final Collection<OfficeData> fromOfficeOptions; private final Collection<OfficeData> fromOfficeOptions;
@Getter
private final Collection<ClientData> fromClientOptions; private final Collection<ClientData> fromClientOptions;
@Getter
private final Collection<EnumOptionData> fromAccountTypeOptions; private final Collection<EnumOptionData> fromAccountTypeOptions;
@Getter
private final Collection<PortfolioAccountData> fromAccountOptions; private final Collection<PortfolioAccountData> fromAccountOptions;
@Getter
private final Collection<OfficeData> toOfficeOptions; private final Collection<OfficeData> toOfficeOptions;
@Getter
private final Collection<ClientData> toClientOptions; private final Collection<ClientData> toClientOptions;
@Getter
private final Collection<EnumOptionData> toAccountTypeOptions; private final Collection<EnumOptionData> toAccountTypeOptions;
@Getter
private final Collection<PortfolioAccountData> toAccountOptions; private final Collection<PortfolioAccountData> toAccountOptions;
@Getter
private final Collection<EnumOptionData> transferTypeOptions; private final Collection<EnumOptionData> transferTypeOptions;
@Getter
private final Collection<EnumOptionData> statusOptions; private final Collection<EnumOptionData> statusOptions;
@Getter
private final Collection<EnumOptionData> instructionTypeOptions; private final Collection<EnumOptionData> instructionTypeOptions;
@Getter
private final Collection<EnumOptionData> priorityOptions; private final Collection<EnumOptionData> priorityOptions;
@Getter
private final Collection<EnumOptionData> recurrenceTypeOptions; private final Collection<EnumOptionData> recurrenceTypeOptions;
@Getter
private final Collection<EnumOptionData> recurrenceFrequencyOptions; private final Collection<EnumOptionData> recurrenceFrequencyOptions;
public static StandingInstructionData template(final Collection<OfficeData> fromOfficeOptions, public static StandingInstructionData template(final Collection<OfficeData> fromOfficeOptions,
@@ -282,28 +308,32 @@ public final class StandingInstructionData {
instructionData.recurrenceTypeOptions, instructionData.recurrenceFrequencyOptions); 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); 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); 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); 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); 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); 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); 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() { public Integer toTransferType() {
Integer transferType = null; Integer transferType = null;
AccountTransferType accountTransferType = getTransferType(); AccountTransferType accountTransferType = transferTypeEnum();
if (accountTransferType.isChargePayment()) { if (accountTransferType != null && accountTransferType.isChargePayment()) {
transferType = LoanTransactionType.CHARGE_PAYMENT.getValue(); transferType = LoanTransactionType.CHARGE_PAYMENT.getValue();
} else if (accountTransferType.isLoanRepayment()) { } else if (accountTransferType != null && accountTransferType.isLoanRepayment()) {
transferType = LoanTransactionType.REPAYMENT.getValue(); transferType = LoanTransactionType.REPAYMENT.getValue();
} }
return transferType; return transferType;
@@ -171,7 +171,8 @@ public class StandingInstructionDataValidator {
baseDataValidator.reset().parameter(StandingInstructionApiConstants.nameParamName).value(name).notNull(); baseDataValidator.reset().parameter(StandingInstructionApiConstants.nameParamName).value(name).notNull();
final Integer toAccountType = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(toAccountTypeParamName, element); 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) baseDataValidator.reset().parameter(StandingInstructionApiConstants.instructionTypeParamName).value(standingInstructionType)
.notNull().inMinMaxRange(1, 1); .notNull().inMinMaxRange(1, 1);
baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceTypeParamName).value(recurrenceType).notNull() baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceTypeParamName).value(recurrenceType).notNull()
@@ -192,7 +193,8 @@ public class StandingInstructionDataValidator {
&& (fromPortfolioAccountType.isLoanAccount() || toPortfolioAccountType.isLoanAccount())) { && (fromPortfolioAccountType.isLoanAccount() || toPortfolioAccountType.isLoanAccount())) {
errorCode = "not.account.transfer"; errorCode = "not.account.transfer";
} else if (accountTransferType.isLoanRepayment() } else if (accountTransferType.isLoanRepayment()
&& (fromPortfolioAccountType.isLoanAccount() || toPortfolioAccountType.isSavingsAccount())) { && (fromPortfolioAccountType.isLoanAccount()
|| (toPortfolioAccountType.isSavingsAccount() || toPortfolioAccountType.isCurrentAccount()))) {
errorCode = "not.loan.repayment"; errorCode = "not.loan.repayment";
} }
if (errorCode != null) { 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.organisation.office.domain.OfficeRepositoryWrapper;
import org.apache.fineract.portfolio.client.domain.Client; import org.apache.fineract.portfolio.client.domain.Client;
import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper; 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.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler; import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler;
import org.apache.fineract.portfolio.savings.domain.SavingsAccount; import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
@@ -49,16 +51,19 @@ public class AccountTransferDetailAssembler {
private final SavingsAccountAssembler savingsAccountAssembler; private final SavingsAccountAssembler savingsAccountAssembler;
private final FromJsonHelper fromApiJsonHelper; private final FromJsonHelper fromApiJsonHelper;
private final LoanAssembler loanAccountAssembler; private final LoanAssembler loanAccountAssembler;
private final CurrentAccountRepository currentAccountRepository;
@Autowired @Autowired
public AccountTransferDetailAssembler(final ClientRepositoryWrapper clientRepository, public AccountTransferDetailAssembler(final ClientRepositoryWrapper clientRepository,
final OfficeRepositoryWrapper officeRepositoryWrapper, final SavingsAccountAssembler savingsAccountAssembler, 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.clientRepository = clientRepository;
this.officeRepositoryWrapper = officeRepositoryWrapper; this.officeRepositoryWrapper = officeRepositoryWrapper;
this.savingsAccountAssembler = savingsAccountAssembler; this.savingsAccountAssembler = savingsAccountAssembler;
this.fromApiJsonHelper = fromApiJsonHelper; this.fromApiJsonHelper = fromApiJsonHelper;
this.loanAccountAssembler = loanAccountAssembler; this.loanAccountAssembler = loanAccountAssembler;
this.currentAccountRepository = currentAccountRepository;
} }
public AccountTransferDetails assembleSavingsToSavingsTransfer(final JsonCommand command) { public AccountTransferDetails assembleSavingsToSavingsTransfer(final JsonCommand command) {
@@ -211,4 +216,99 @@ public class AccountTransferDetailAssembler {
return AccountTransferDetails.loanToLoanTransfer(fromOffice, fromClient, fromLoanAccount, toOffice, toClient, toLoanAccount, return AccountTransferDetails.loanToLoanTransfer(fromOffice, fromClient, fromLoanAccount, toOffice, toClient, toLoanAccount,
transferType); 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.infrastructure.core.domain.AbstractPersistableCustom;
import org.apache.fineract.organisation.office.domain.Office; import org.apache.fineract.organisation.office.domain.Office;
import org.apache.fineract.portfolio.client.domain.Client; 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.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.savings.domain.SavingsAccount; 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) @JoinColumn(name = "from_loan_account_id", nullable = true)
private Loan fromLoanAccount; 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") @Column(name = "transfer_type")
private Integer transferType; 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, final SavingsAccount fromSavingsAccount, final Office toOffice, final Client toClient, final SavingsAccount toSavingsAccount,
Integer transferType) { Integer transferType) {
return new AccountTransferDetails(fromOffice, fromClient, fromSavingsAccount, null, toOffice, toClient, toSavingsAccount, null, return new AccountTransferDetails(fromOffice, fromClient, fromSavingsAccount, null, null, toOffice, toClient, toSavingsAccount,
transferType, null); null, null, transferType, null);
} }
public static AccountTransferDetails savingsToLoanTransfer(final Office fromOffice, final Client fromClient, public static AccountTransferDetails savingsToLoanTransfer(final Office fromOffice, final Client fromClient,
final SavingsAccount fromSavingsAccount, final Office toOffice, final Client toClient, final Loan toLoanAccount, final SavingsAccount fromSavingsAccount, final Office toOffice, final Client toClient, final Loan toLoanAccount,
Integer transferType) { Integer transferType) {
return new AccountTransferDetails(fromOffice, fromClient, fromSavingsAccount, null, toOffice, toClient, null, toLoanAccount, return new AccountTransferDetails(fromOffice, fromClient, fromSavingsAccount, null, null, toOffice, toClient, null, toLoanAccount,
transferType, null); null, transferType, null);
} }
public static AccountTransferDetails loanTosavingsTransfer(final Office fromOffice, final Client fromClient, final Loan fromLoanAccount, public static AccountTransferDetails loanTosavingsTransfer(final Office fromOffice, final Client fromClient, final Loan fromLoanAccount,
final Office toOffice, final Client toClient, final SavingsAccount toSavingsAccount, Integer transferType) { final Office toOffice, final Client toClient, final SavingsAccount toSavingsAccount, Integer transferType) {
return new AccountTransferDetails(fromOffice, fromClient, null, fromLoanAccount, toOffice, toClient, toSavingsAccount, null, return new AccountTransferDetails(fromOffice, fromClient, null, fromLoanAccount, null, toOffice, toClient, toSavingsAccount, null,
transferType, null); null, transferType, null);
} }
protected AccountTransferDetails() { protected AccountTransferDetails() {
@@ -106,17 +115,21 @@ public class AccountTransferDetails extends AbstractPersistableCustom<Long> {
} }
private AccountTransferDetails(final Office fromOffice, final Client fromClient, final SavingsAccount fromSavingsAccount, 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 fromLoanAccount, final CurrentAccount fromCurrentAccount,
final Loan toLoanAccount, final Integer transferType, final Office toOffice, final Client toClient, final SavingsAccount toSavingsAccount,
final Loan toLoanAccount, final CurrentAccount toCurrentAccount,
final Integer transferType,
final AccountTransferStandingInstruction accountTransferStandingInstruction) { final AccountTransferStandingInstruction accountTransferStandingInstruction) {
this.fromOffice = fromOffice; this.fromOffice = fromOffice;
this.fromClient = fromClient; this.fromClient = fromClient;
this.fromSavingsAccount = fromSavingsAccount; this.fromSavingsAccount = fromSavingsAccount;
this.fromLoanAccount = fromLoanAccount; this.fromLoanAccount = fromLoanAccount;
this.fromCurrentAccount = fromCurrentAccount;
this.toOffice = toOffice; this.toOffice = toOffice;
this.toClient = toClient; this.toClient = toClient;
this.toSavingsAccount = toSavingsAccount; this.toSavingsAccount = toSavingsAccount;
this.toLoanAccount = toLoanAccount; this.toLoanAccount = toLoanAccount;
this.toCurrentAccount = toCurrentAccount;
this.transferType = transferType; this.transferType = transferType;
this.accountTransferStandingInstruction = accountTransferStandingInstruction; this.accountTransferStandingInstruction = accountTransferStandingInstruction;
} }
@@ -145,6 +158,14 @@ public class AccountTransferDetails extends AbstractPersistableCustom<Long> {
return this.fromLoanAccount; return this.fromLoanAccount;
} }
public CurrentAccount fromCurrentAccount() {
return this.fromCurrentAccount;
}
public CurrentAccount toCurrentAccount() {
return this.toCurrentAccount;
}
public AccountTransferStandingInstruction accountTransferStandingInstruction() { public AccountTransferStandingInstruction accountTransferStandingInstruction() {
return this.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, public static AccountTransferDetails loanToLoanTransfer(Office fromOffice, Client fromClient, Loan fromLoanAccount, Office toOffice,
Client toClient, Loan toLoanAccount, Integer transferType) { Client toClient, Loan toLoanAccount, Integer transferType) {
return new AccountTransferDetails(fromOffice, fromClient, null, fromLoanAccount, toOffice, toClient, null, toLoanAccount, return new AccountTransferDetails(fromOffice, fromClient, null, fromLoanAccount, null, toOffice, toClient, null, toLoanAccount,
transferType, null); 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() { 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(instructionTypeParamName).value(this.instructionType).notNull().inMinMaxRange(1, 1);
baseDataValidator.reset().parameter(recurrenceTypeParamName).value(this.recurrenceType).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(); 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; String errorCode = null;
if (this.accountTransferDetails.transferType().isAccountTransfer() if (this.accountTransferDetails.transferType().isAccountTransfer()
&& (this.accountTransferDetails.fromSavingsAccount() == null || this.accountTransferDetails.toSavingsAccount() == null)) { && !(fromAccountValid && toAccountValid)) {
errorCode = "not.account.transfer"; errorCode = "not.account.transfer";
} else if (this.accountTransferDetails.transferType().isLoanRepayment() } else if (this.accountTransferDetails.transferType().isLoanRepayment()
&& (this.accountTransferDetails.fromSavingsAccount() == null || this.accountTransferDetails.toLoanAccount() == null)) { && !(fromAccountValidForLoan && this.accountTransferDetails.toLoanAccount() != null)) {
errorCode = "not.loan.repayment"; errorCode = "not.loan.repayment";
} }
if (errorCode != null) { if (errorCode != null) {
@@ -35,6 +35,9 @@ import java.time.LocalDate;
import java.time.MonthDay; import java.time.MonthDay;
import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.organisation.monetary.domain.Money; import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccount;
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -61,8 +64,13 @@ public class StandingInstructionAssembler {
BigDecimal amount = null; BigDecimal amount = null;
final BigDecimal transferAmount = command.bigDecimalValueOfParameterNamed(amountParamName); final BigDecimal transferAmount = command.bigDecimalValueOfParameterNamed(amountParamName);
if (transferAmount != null) { if (transferAmount != null) {
final Money monetaryAmount = Money.of(accountTransferDetails.fromSavingsAccount().getCurrency(), transferAmount); final MonetaryCurrency currency = resolveCurrency(accountTransferDetails);
amount = monetaryAmount.getAmount(); if (currency != null) {
final Money monetaryAmount = Money.of(currency, transferAmount);
amount = monetaryAmount.getAmount();
} else {
amount = transferAmount;
}
} }
final Integer status = command.integerValueOfParameterNamed(statusParamName); final Integer status = command.integerValueOfParameterNamed(statusParamName);
final Integer priority = command.integerValueOfParameterNamed(priorityParamName); final Integer priority = command.integerValueOfParameterNamed(priorityParamName);
@@ -78,6 +86,22 @@ public class StandingInstructionAssembler {
accountTransferDetails.updateAccountTransferStandingInstruction(accountTransferStandingInstruction); 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) { public AccountTransferDetails assembleSavingsToLoanTransfer(final JsonCommand command) {
final AccountTransferDetails accountTransferDetails = this.accountTransferDetailAssembler.assembleSavingsToLoanTransfer(command); final AccountTransferDetails accountTransferDetails = this.accountTransferDetailAssembler.assembleSavingsToLoanTransfer(command);
assembleStandingInstruction(command, accountTransferDetails); assembleStandingInstruction(command, accountTransferDetails);
@@ -90,4 +114,30 @@ public class StandingInstructionAssembler {
return accountTransferDetails; 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<>(); List<Throwable> errors = new ArrayList<>();
for (StandingInstructionData data : instructionData) { for (StandingInstructionData data : instructionData) {
boolean isDueForTransfer = false; boolean isDueForTransfer = false;
AccountTransferRecurrenceType recurrenceType = data.getRecurrenceType(); AccountTransferRecurrenceType recurrenceType = data.recurrenceTypeEnum();
StandingInstructionType instructionType = data.getInstructionType(); StandingInstructionType instructionType = data.instructionTypeEnum();
LocalDate transactionDate = DateUtils.getBusinessLocalDate(); LocalDate transactionDate = DateUtils.getBusinessLocalDate();
if (recurrenceType.isPeriodicRecurrence()) { if (recurrenceType.isPeriodicRecurrence()) {
final ScheduledDateGenerator scheduledDateGenerator = new DefaultScheduledDateGenerator(); final ScheduledDateGenerator scheduledDateGenerator = new DefaultScheduledDateGenerator();
PeriodFrequencyType frequencyType = data.getRecurrenceFrequency(); PeriodFrequencyType frequencyType = data.recurrenceFrequencyEnum();
LocalDate startDate = data.getValidFrom(); LocalDate startDate = data.getValidFrom();
if (frequencyType.isMonthly()) { if (frequencyType.isMonthly()) {
startDate = startDate.withDayOfMonth(data.getRecurrenceOnDay()); startDate = startDate.withDayOfMonth(data.getRecurrenceOnDay());
@@ -89,11 +89,11 @@ public class ExecuteStandingInstructionsTasklet implements Tasklet {
} }
BigDecimal transactionAmount = data.getAmount(); BigDecimal transactionAmount = data.getAmount();
if (data.getToAccountType().isLoanAccount() if (data.toAccountTypeEnum() != null && data.toAccountTypeEnum().isLoanAccount()
&& (recurrenceType.isDuesRecurrence() || (isDueForTransfer && instructionType.isDuesAmoutTransfer()))) { && (recurrenceType.isDuesRecurrence() || (isDueForTransfer && instructionType.isDuesAmoutTransfer()))) {
StandingInstructionDuesData standingInstructionDuesData = standingInstructionReadPlatformService StandingInstructionDuesData standingInstructionDuesData = standingInstructionReadPlatformService
.retriveLoanDuesData(data.getToAccount().getId()); .retriveLoanDuesData(data.getToAccount().getId());
if (data.getInstructionType().isDuesAmoutTransfer()) { if (data.instructionTypeEnum().isDuesAmoutTransfer()) {
transactionAmount = standingInstructionDuesData.totalDueAmount(); transactionAmount = standingInstructionDuesData.totalDueAmount();
} }
if (recurrenceType.isDuesRecurrence()) { if (recurrenceType.isDuesRecurrence()) {
@@ -106,9 +106,9 @@ public class ExecuteStandingInstructionsTasklet implements Tasklet {
final boolean isRegularTransaction = true; final boolean isRegularTransaction = true;
final boolean isExceptionForBalanceCheck = false; final boolean isExceptionForBalanceCheck = false;
AccountTransferDTO accountTransferDTO = new AccountTransferDTO(transactionDate, transactionAmount, 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.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); isRegularTransaction, isExceptionForBalanceCheck);
final boolean transferCompleted = transferAmount(errors, accountTransferDTO, data.getId()); final boolean transferCompleted = transferAmount(errors, accountTransferDTO, data.getId());
@@ -52,6 +52,10 @@ public final class AccountTransferEnumerations {
optionData = new EnumOptionData(PortfolioAccountType.SAVINGS.getValue().longValue(), optionData = new EnumOptionData(PortfolioAccountType.SAVINGS.getValue().longValue(),
PortfolioAccountType.SAVINGS.getCode(), "Savings Account"); PortfolioAccountType.SAVINGS.getCode(), "Savings Account");
break; 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.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.ExternalIdFactory; 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.PortfolioAccountType;
import org.apache.fineract.portfolio.account.data.AccountTransferDTO; import org.apache.fineract.portfolio.account.data.AccountTransferDTO;
import org.apache.fineract.portfolio.account.data.AccountTransfersDataValidator; 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.AccountTransferTransaction;
import org.apache.fineract.portfolio.account.domain.AccountTransferType; import org.apache.fineract.portfolio.account.domain.AccountTransferType;
import org.apache.fineract.portfolio.account.exception.DifferentCurrenciesException; import org.apache.fineract.portfolio.account.exception.DifferentCurrenciesException;
import org.apache.fineract.portfolio.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.data.HolidayDetailDTO;
import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanAccountDomainService; import org.apache.fineract.portfolio.loanaccount.domain.LoanAccountDomainService;
@@ -88,6 +94,8 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
private final ConfigurationDomainService configurationDomainService; private final ConfigurationDomainService configurationDomainService;
private final ExternalIdFactory externalIdFactory; private final ExternalIdFactory externalIdFactory;
private final FineractProperties fineractProperties; private final FineractProperties fineractProperties;
private final CurrentAccountRepository currentAccountRepository;
private final CurrentAccountTransactionRepository currentAccountTransactionRepository;
@Transactional @Transactional
@Override @Override
@@ -448,6 +456,128 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
gsim.setParentDeposit(newBalance); gsim.setParentDeposit(newBalance);
gsimRepository.save(gsim); 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 { } else {
throw new GeneralPlatformDomainRuleException("error.msg.accounttransfer.loan.to.loan.not.supported", throw new GeneralPlatformDomainRuleException("error.msg.accounttransfer.loan.to.loan.not.supported",
"Account transfer from loan to another loan is 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(); 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 @Override
@Transactional @Transactional
public CommandProcessingResult refundByTransfer(JsonCommand command) { public CommandProcessingResult refundByTransfer(JsonCommand command) {
@@ -43,12 +43,14 @@ public class PortfolioAccountReadPlatformServiceImpl implements PortfolioAccount
private final PortfolioSavingsAccountMapper savingsAccountMapper; private final PortfolioSavingsAccountMapper savingsAccountMapper;
private final PortfolioLoanAccountMapper loanAccountMapper; private final PortfolioLoanAccountMapper loanAccountMapper;
private final PortfolioLoanAccountRefundByTransferMapper accountRefundByTransferMapper; private final PortfolioLoanAccountRefundByTransferMapper accountRefundByTransferMapper;
private final PortfolioCurrentAccountMapper currentAccountMapper;
public PortfolioAccountReadPlatformServiceImpl(final JdbcTemplate jdbcTemplate, DatabaseSpecificSQLGenerator sqlGenerator) { public PortfolioAccountReadPlatformServiceImpl(final JdbcTemplate jdbcTemplate, DatabaseSpecificSQLGenerator sqlGenerator) {
this.jdbcTemplate = jdbcTemplate; this.jdbcTemplate = jdbcTemplate;
this.savingsAccountMapper = new PortfolioSavingsAccountMapper(); this.savingsAccountMapper = new PortfolioSavingsAccountMapper();
this.loanAccountMapper = new PortfolioLoanAccountMapper(); this.loanAccountMapper = new PortfolioLoanAccountMapper();
this.accountRefundByTransferMapper = new PortfolioLoanAccountRefundByTransferMapper(sqlGenerator); this.accountRefundByTransferMapper = new PortfolioLoanAccountRefundByTransferMapper(sqlGenerator);
this.currentAccountMapper = new PortfolioCurrentAccountMapper();
} }
@Override @Override
@@ -86,6 +88,15 @@ public class PortfolioAccountReadPlatformServiceImpl implements PortfolioAccount
accountData = this.jdbcTemplate.queryForObject(sql, this.savingsAccountMapper, sqlParams); accountData = this.jdbcTemplate.queryForObject(sql, this.savingsAccountMapper, sqlParams);
break; 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) { } catch (final EmptyResultDataAccessException e) {
throw new AccountTransferNotFoundException(accountId, 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 accounts = this.jdbcTemplate.query(sql, this.savingsAccountMapper, sqlParams.toArray()); // NOSONAR
break; 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; return accounts;
@@ -393,4 +421,62 @@ public class PortfolioAccountReadPlatformServiceImpl implements PortfolioAccount
return accountData; 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 loanAccountType = accountType(PortfolioAccountType.LOAN);
final EnumOptionData savingsAccountType = accountType(PortfolioAccountType.SAVINGS); final EnumOptionData savingsAccountType = accountType(PortfolioAccountType.SAVINGS);
final EnumOptionData currentAccountType = accountType(PortfolioAccountType.CURRENT);
final Integer mostRelevantFromAccountType = fromAccountType; final Integer mostRelevantFromAccountType = fromAccountType;
Collection<EnumOptionData> fromAccountTypeOptions = null; Collection<EnumOptionData> fromAccountTypeOptions = null;
Collection<EnumOptionData> toAccountTypeOptions = null; Collection<EnumOptionData> toAccountTypeOptions = null;
if (accountTransferType.isAccountTransfer()) { if (accountTransferType.isAccountTransfer()) {
fromAccountTypeOptions = Arrays.asList(savingsAccountType); fromAccountTypeOptions = Arrays.asList(savingsAccountType, currentAccountType);
toAccountTypeOptions = Arrays.asList(savingsAccountType); toAccountTypeOptions = Arrays.asList(savingsAccountType, currentAccountType);
} else if (accountTransferType.isLoanRepayment()) { } else if (accountTransferType.isLoanRepayment()) {
fromAccountTypeOptions = Arrays.asList(savingsAccountType); fromAccountTypeOptions = Arrays.asList(savingsAccountType, currentAccountType);
toAccountTypeOptions = Arrays.asList(loanAccountType); toAccountTypeOptions = Arrays.asList(loanAccountType);
} else { } else {
fromAccountTypeOptions = Arrays.asList(savingsAccountType, loanAccountType); fromAccountTypeOptions = Arrays.asList(savingsAccountType, currentAccountType, loanAccountType);
toAccountTypeOptions = Arrays.asList(loanAccountType, savingsAccountType); toAccountTypeOptions = Arrays.asList(loanAccountType, savingsAccountType, currentAccountType);
} }
final Integer mostRelevantToAccountType = toAccountType; final Integer mostRelevantToAccountType = toAccountType;
@@ -152,6 +153,8 @@ public class StandingInstructionReadPlatformServiceImpl implements StandingInstr
Integer accountType; Integer accountType;
if (mostRelevantFromAccountType == 1) { if (mostRelevantFromAccountType == 1) {
accountType = PortfolioAccountType.LOAN.getValue(); accountType = PortfolioAccountType.LOAN.getValue();
} else if (mostRelevantFromAccountType == 3) {
accountType = PortfolioAccountType.CURRENT.getValue();
} else { } else {
accountType = PortfolioAccountType.SAVINGS.getValue(); accountType = PortfolioAccountType.SAVINGS.getValue();
} }
@@ -301,6 +304,9 @@ public class StandingInstructionReadPlatformServiceImpl implements StandingInstr
} else if (accountType.isLoanAccount()) { } else if (accountType.isLoanAccount()) {
sqlBuilder.append(" fromloanacc.id=? "); sqlBuilder.append(" fromloanacc.id=? ");
paramObj.add(standingInstructionDTO.fromAccount()); paramObj.add(standingInstructionDTO.fromAccount());
} else if (accountType.isCurrentAccount()) {
sqlBuilder.append(" fromcuracc.id=? ");
paramObj.add(standingInstructionDTO.fromAccount());
} }
addAndCaluse = true; addAndCaluse = true;
} }
@@ -387,7 +393,11 @@ public class StandingInstructionReadPlatformServiceImpl implements StandingInstr
sqlBuilder.append("tosavacc.id as toSavingsAccountId, tosavacc.account_no as toSavingsAccountNo,"); sqlBuilder.append("tosavacc.id as toSavingsAccountId, tosavacc.account_no as toSavingsAccountNo,");
sqlBuilder.append("tosp.id as toProductId, tosp.name as toProductName, "); sqlBuilder.append("tosp.id as toProductId, tosp.name as toProductName, ");
sqlBuilder.append("toloanacc.id as toLoanAccountId, toloanacc.account_no as toLoanAccountNo, "); 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(" 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_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 "); 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_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_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_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(); this.schemaSql = sqlBuilder.toString();
} }
@@ -489,6 +503,16 @@ public class StandingInstructionReadPlatformServiceImpl implements StandingInstr
fromAccount = new PortfolioAccountData(fromLoanAccountId, fromLoanAccountNo, null, null, null, null, null, fromAccount = new PortfolioAccountData(fromLoanAccountId, fromLoanAccountNo, null, null, null, null, null,
fromLoanProductId, fromLoanProductName, null, null, null); fromLoanProductId, fromLoanProductName, null, null, null);
fromAccountType = accountType(PortfolioAccountType.LOAN); 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; PortfolioAccountData toAccount = null;
@@ -510,6 +534,16 @@ public class StandingInstructionReadPlatformServiceImpl implements StandingInstr
toAccount = new PortfolioAccountData(toLoanAccountId, toLoanAccountNo, null, null, null, null, null, toLoanProductId, toAccount = new PortfolioAccountData(toLoanAccountId, toLoanAccountNo, null, null, null, null, null, toLoanProductId,
toLoanProductName, null, null, null); toLoanProductName, null, null, null);
toAccountType = accountType(PortfolioAccountType.LOAN); 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, return StandingInstructionData.instance(id, accountDetailId, name, fromOffice, toOffice, fromClient, toClient, fromAccountType,
@@ -82,11 +82,25 @@ public class StandingInstructionWritePlatformServiceImpl implements StandingInst
this.accountTransferDetailRepository.saveAndFlush(standingInstruction); this.accountTransferDetailRepository.saveAndFlush(standingInstruction);
standingInstructionId = standingInstruction.accountTransferStandingInstruction().getId(); standingInstructionId = standingInstruction.accountTransferStandingInstruction().getId();
} else if (isLoanToSavingsAccountTransfer(fromAccountType, toAccountType)) { } else if (isLoanToSavingsAccountTransfer(fromAccountType, toAccountType)) {
final AccountTransferDetails standingInstruction = this.standingInstructionAssembler.assembleLoanToSavingsTransfer(command); final AccountTransferDetails standingInstruction = this.standingInstructionAssembler.assembleLoanToSavingsTransfer(command);
this.accountTransferDetailRepository.saveAndFlush(standingInstruction); this.accountTransferDetailRepository.saveAndFlush(standingInstruction);
standingInstructionId = standingInstruction.accountTransferStandingInstruction().getId(); 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) { } catch (final JpaSystemException | DataIntegrityViolationException dve) {
final Throwable throwable = dve.getMostSpecificCause(); final Throwable throwable = dve.getMostSpecificCause();
@@ -122,6 +136,26 @@ public class StandingInstructionWritePlatformServiceImpl implements StandingInst
return fromAccountType.isSavingsAccount() && toAccountType.isSavingsAccount(); 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 @Override
public CommandProcessingResult update(final Long id, final JsonCommand command) { public CommandProcessingResult update(final Long id, final JsonCommand command) {
this.standingInstructionDataValidator.validateForUpdate(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.service.SqlValidator;
import org.apache.fineract.infrastructure.security.utils.ColumnValidator; import org.apache.fineract.infrastructure.security.utils.ColumnValidator;
import org.apache.fineract.organisation.office.service.OfficeReadPlatformService; 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.AccountTransfersDataValidator;
import org.apache.fineract.portfolio.account.data.StandingInstructionDataValidator; import org.apache.fineract.portfolio.account.data.StandingInstructionDataValidator;
import org.apache.fineract.portfolio.account.domain.AccountTransferAssembler; import org.apache.fineract.portfolio.account.domain.AccountTransferAssembler;
@@ -91,11 +93,13 @@ public class AccountConfiguration {
LoanAccountDomainService loanAccountDomainService, SavingsAccountWritePlatformService savingsAccountWritePlatformService, LoanAccountDomainService loanAccountDomainService, SavingsAccountWritePlatformService savingsAccountWritePlatformService,
AccountTransferDetailRepository accountTransferDetailRepository, LoanReadPlatformService loanReadPlatformService, AccountTransferDetailRepository accountTransferDetailRepository, LoanReadPlatformService loanReadPlatformService,
GSIMRepositoy gsimRepository, ConfigurationDomainService configurationDomainService, ExternalIdFactory externalIdFactory, GSIMRepositoy gsimRepository, ConfigurationDomainService configurationDomainService, ExternalIdFactory externalIdFactory,
FineractProperties fineractProperties) { FineractProperties fineractProperties, CurrentAccountRepository currentAccountRepository,
CurrentAccountTransactionRepository currentAccountTransactionRepository) {
return new AccountTransfersWritePlatformServiceImpl(accountTransfersDataValidator, accountTransferAssembler, return new AccountTransfersWritePlatformServiceImpl(accountTransfersDataValidator, accountTransferAssembler,
accountTransferRepository, savingsAccountAssembler, savingsAccountDomainService, loanAccountAssembler, accountTransferRepository, savingsAccountAssembler, savingsAccountDomainService, loanAccountAssembler,
loanAccountDomainService, savingsAccountWritePlatformService, accountTransferDetailRepository, loanReadPlatformService, loanAccountDomainService, savingsAccountWritePlatformService, accountTransferDetailRepository, loanReadPlatformService,
gsimRepository, configurationDomainService, externalIdFactory, fineractProperties); gsimRepository, configurationDomainService, externalIdFactory, fineractProperties,
currentAccountRepository, currentAccountTransactionRepository);
} }
@Bean @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.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:root} 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}
@@ -317,4 +317,5 @@
<include file="parts/0298_unique_external_id_on_current_account.xml" relativeToChangelogFile="true"/> <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/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/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> </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>