From a1e3dba5a0d73eaac01ea0435fbabf27153c5444 Mon Sep 17 00:00:00 2001 From: Francisco Cuandon Date: Tue, 26 May 2026 12:18:57 -0600 Subject: [PATCH] FINERACT-2048: Make amount and recurrence fields optional for Dues-based Standing Instructions --- .../api/StandingInstructionApiConstants.java | 11 + .../account/data/StandingInstructionData.java | 19 +- .../StandingInstructionDataValidator.java | 451 ++++++++---- .../domain/AccountTransferDetails.java | 2 + .../AccountTransferStandingInstruction.java | 65 +- ...ingInstructionReadPlatformServiceImpl.java | 18 +- ...ngInstructionWritePlatformServiceImpl.java | 25 +- .../StandingInstructionDataValidatorTest.java | 668 ++++++++++++++++++ 8 files changed, 1086 insertions(+), 173 deletions(-) create mode 100644 fineract-provider/src/test/java/org/apache/fineract/portfolio/account/data/StandingInstructionDataValidatorTest.java diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/api/StandingInstructionApiConstants.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/api/StandingInstructionApiConstants.java index 50fce06b097..6d9bdd57b4c 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/api/StandingInstructionApiConstants.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/api/StandingInstructionApiConstants.java @@ -39,4 +39,15 @@ private StandingInstructionApiConstants() { public static final String recurrenceOnMonthDayParamName = "recurrenceOnMonthDay"; public static final String monthDayFormatParamName = "monthDayFormat"; + public static final String INVALID_MONTH_DAY_FORMAT_ERROR_CODE = "invalid.month.day.format"; + public static final String BEFORE_FIRST_EXECUTION_DATE_ERROR_CODE = "must.not.be.before.first.execution.date"; + public static final String AMOUNT_NOT_ALLOWED_FOR_DUES_ERROR_CODE = "not.allowed.for.dues.instruction"; + public static final String CANNOT_TRANSFER_TO_SAME_ACCOUNT_ERROR_CODE = "transfer.to.same.account.not.allowed"; + public static final String INSTRUCTION_TYPE_DUES_NOT_ALLOWED_FOR_ACCOUNT_TRANSFER_ERROR_CODE = "dues.not.allowed.for.account.transfer"; + public static final String RECURRENCE_AS_PER_DUES_NOT_ALLOWED_FOR_SAVINGS_ERROR_CODE = "as.per.dues.not.allowed.for.account.transfer"; + public static final String ACCOUNT_TRANSFER_NOT_ALLOWED_FOR_LOAN_ERROR_CODE = "account.transfer.is.not.allowed.for.loan.accounts"; + public static final String RECURRENCE_AS_PER_DUES_NOT_ALLOWED_WITH_FIXED_INSTRUCTION_ERROR_CODE = "as.per.dues.not.allowed.with.fixed.amount"; + public static final String NOT_A_VALID_LOAN_REPAYMENT_ERROR_CODE = "is.not.a.valid.loan.repayment"; + public static final String MUST_BE_BEFORE_EXISTING_VALID_TILL_ERROR_CODE = "must.be.before.existing.valid.till"; + public static final String CANNOT_BE_BEFORE_LAST_RUN_DATE_ERROR_CODE = "cannot.be.before.last.run.date"; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/data/StandingInstructionData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/data/StandingInstructionData.java index 09b61c6b96a..b0bc1b44fcf 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/data/StandingInstructionData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/data/StandingInstructionData.java @@ -40,39 +40,29 @@ * Immutable data object representing a savings account. */ @SuppressWarnings("unused") +@Getter public final class StandingInstructionData { - @Getter private final Long id; - @Getter private final Long accountDetailId; - @Getter private final String name; private final OfficeData fromOffice; - @Getter private final ClientData fromClient; private final EnumOptionData fromAccountType; - @Getter private final PortfolioAccountData fromAccount; private final OfficeData toOffice; - @Getter private final ClientData toClient; private final EnumOptionData toAccountType; - @Getter private final PortfolioAccountData toAccount; private final EnumOptionData transferType; private final EnumOptionData priority; private final EnumOptionData instructionType; - @Getter private final EnumOptionData status; - @Getter private final BigDecimal amount; - @Getter private final LocalDate validFrom; private final LocalDate validTill; private final EnumOptionData recurrenceType; private final EnumOptionData recurrenceFrequency; - @Getter private final Integer recurrenceInterval; private final MonthDay recurrenceOnMonthDay; private final Page transactions; @@ -326,4 +316,11 @@ public Integer toTransferType() { return transferType; } + public Collection getRecurrenceFrequencyOptions() { + if (this.recurrenceFrequencyOptions == null) { + return null; + } + return this.recurrenceFrequencyOptions.stream().filter(option -> option.getId() != null && option.getId() < 4) + .collect(java.util.stream.Collectors.toList()); + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/data/StandingInstructionDataValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/data/StandingInstructionDataValidator.java index 36fbe302c18..2058b0fecc9 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/data/StandingInstructionDataValidator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/data/StandingInstructionDataValidator.java @@ -18,9 +18,30 @@ */ package org.apache.fineract.portfolio.account.data; +import static org.apache.fineract.portfolio.account.AccountDetailConstants.dateFormatParamName; +import static org.apache.fineract.portfolio.account.AccountDetailConstants.fromAccountIdParamName; import static org.apache.fineract.portfolio.account.AccountDetailConstants.fromAccountTypeParamName; +import static org.apache.fineract.portfolio.account.AccountDetailConstants.fromClientIdParamName; +import static org.apache.fineract.portfolio.account.AccountDetailConstants.fromOfficeIdParamName; +import static org.apache.fineract.portfolio.account.AccountDetailConstants.localeParamName; +import static org.apache.fineract.portfolio.account.AccountDetailConstants.toAccountIdParamName; import static org.apache.fineract.portfolio.account.AccountDetailConstants.toAccountTypeParamName; +import static org.apache.fineract.portfolio.account.AccountDetailConstants.toClientIdParamName; +import static org.apache.fineract.portfolio.account.AccountDetailConstants.toOfficeIdParamName; import static org.apache.fineract.portfolio.account.AccountDetailConstants.transferTypeParamName; +import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.STANDING_INSTRUCTION_RESOURCE_NAME; +import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.amountParamName; +import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.instructionTypeParamName; +import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.monthDayFormatParamName; +import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.nameParamName; +import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.priorityParamName; +import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.recurrenceFrequencyParamName; +import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.recurrenceIntervalParamName; +import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.recurrenceOnMonthDayParamName; +import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.recurrenceTypeParamName; +import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.statusParamName; +import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.validFromParamName; +import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.validTillParamName; import com.google.gson.JsonElement; import com.google.gson.reflect.TypeToken; @@ -42,10 +63,10 @@ import org.apache.fineract.infrastructure.core.exception.InvalidJsonException; import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; -import org.apache.fineract.portfolio.account.AccountDetailConstants; import org.apache.fineract.portfolio.account.PortfolioAccountType; import org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants; import org.apache.fineract.portfolio.account.domain.AccountTransferRecurrenceType; +import org.apache.fineract.portfolio.account.domain.AccountTransferStandingInstruction; import org.apache.fineract.portfolio.account.domain.AccountTransferType; import org.apache.fineract.portfolio.account.domain.StandingInstructionType; import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; @@ -57,26 +78,17 @@ public class StandingInstructionDataValidator { private final FromJsonHelper fromApiJsonHelper; private final AccountTransfersDetailDataValidator accountTransfersDetailDataValidator; - private static final Set CREATE_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(AccountDetailConstants.localeParamName, - AccountDetailConstants.dateFormatParamName, AccountDetailConstants.fromOfficeIdParamName, - AccountDetailConstants.fromClientIdParamName, AccountDetailConstants.fromAccountTypeParamName, - AccountDetailConstants.fromAccountIdParamName, AccountDetailConstants.toOfficeIdParamName, - AccountDetailConstants.toClientIdParamName, AccountDetailConstants.toAccountTypeParamName, - AccountDetailConstants.toAccountIdParamName, AccountDetailConstants.transferTypeParamName, - StandingInstructionApiConstants.priorityParamName, StandingInstructionApiConstants.instructionTypeParamName, - StandingInstructionApiConstants.statusParamName, StandingInstructionApiConstants.amountParamName, - StandingInstructionApiConstants.validFromParamName, StandingInstructionApiConstants.validTillParamName, - StandingInstructionApiConstants.recurrenceTypeParamName, StandingInstructionApiConstants.recurrenceFrequencyParamName, - StandingInstructionApiConstants.recurrenceIntervalParamName, StandingInstructionApiConstants.recurrenceOnMonthDayParamName, - StandingInstructionApiConstants.nameParamName, StandingInstructionApiConstants.monthDayFormatParamName)); - - private static final Set UPDATE_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(AccountDetailConstants.localeParamName, - AccountDetailConstants.dateFormatParamName, StandingInstructionApiConstants.priorityParamName, - StandingInstructionApiConstants.instructionTypeParamName, StandingInstructionApiConstants.statusParamName, - StandingInstructionApiConstants.amountParamName, StandingInstructionApiConstants.validFromParamName, - StandingInstructionApiConstants.validTillParamName, StandingInstructionApiConstants.recurrenceTypeParamName, - StandingInstructionApiConstants.recurrenceFrequencyParamName, StandingInstructionApiConstants.recurrenceIntervalParamName, - StandingInstructionApiConstants.recurrenceOnMonthDayParamName, StandingInstructionApiConstants.monthDayFormatParamName)); + private static final Set CREATE_REQUEST_DATA_PARAMETERS = new HashSet<>( + Arrays.asList(localeParamName, dateFormatParamName, fromOfficeIdParamName, fromClientIdParamName, fromAccountTypeParamName, + fromAccountIdParamName, toOfficeIdParamName, toClientIdParamName, toAccountTypeParamName, toAccountIdParamName, + transferTypeParamName, priorityParamName, instructionTypeParamName, statusParamName, amountParamName, + validFromParamName, validTillParamName, recurrenceTypeParamName, recurrenceFrequencyParamName, + recurrenceIntervalParamName, recurrenceOnMonthDayParamName, nameParamName, monthDayFormatParamName)); + + private static final Set UPDATE_REQUEST_DATA_PARAMETERS = new HashSet<>( + Arrays.asList(localeParamName, dateFormatParamName, nameParamName, priorityParamName, instructionTypeParamName, statusParamName, + amountParamName, validFromParamName, validTillParamName, recurrenceTypeParamName, recurrenceFrequencyParamName, + recurrenceIntervalParamName, recurrenceOnMonthDayParamName, monthDayFormatParamName)); @Autowired public StandingInstructionDataValidator(final FromJsonHelper fromApiJsonHelper, @@ -97,113 +109,129 @@ public void validateForCreate(final JsonCommand command) { final List dataValidationErrors = new ArrayList<>(); final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) - .resource(StandingInstructionApiConstants.STANDING_INSTRUCTION_RESOURCE_NAME); + .resource(STANDING_INSTRUCTION_RESOURCE_NAME); this.accountTransfersDetailDataValidator.validate(command, baseDataValidator); final JsonElement element = command.parsedJson(); - final Integer status = this.fromApiJsonHelper.extractIntegerNamed(StandingInstructionApiConstants.statusParamName, element, - Locale.getDefault()); - baseDataValidator.reset().parameter(StandingInstructionApiConstants.statusParamName).value(status).notNull().inMinMaxRange(1, 2); + final Integer transferType = this.fromApiJsonHelper.extractIntegerNamed(transferTypeParamName, element, Locale.getDefault()); + baseDataValidator.reset().parameter(transferTypeParamName).value(transferType).notNull().inMinMaxRange(1, 3); - final LocalDate validFrom = this.fromApiJsonHelper.extractLocalDateNamed(StandingInstructionApiConstants.validFromParamName, - element); - baseDataValidator.reset().parameter(StandingInstructionApiConstants.validFromParamName).value(validFrom).notNull(); + final String name = this.fromApiJsonHelper.extractStringNamed(nameParamName, element); + baseDataValidator.reset().parameter(nameParamName).value(name).notBlank(); - final LocalDate validTill = this.fromApiJsonHelper.extractLocalDateNamed(StandingInstructionApiConstants.validTillParamName, - element); - baseDataValidator.reset().parameter(StandingInstructionApiConstants.validTillParamName).value(validTill) - .validateDateAfter(validFrom); + final Integer priority = this.fromApiJsonHelper.extractIntegerNamed(priorityParamName, element, Locale.getDefault()); + baseDataValidator.reset().parameter(priorityParamName).value(priority).notNull().inMinMaxRange(1, 4); - final BigDecimal transferAmount = this.fromApiJsonHelper - .extractBigDecimalWithLocaleNamed(StandingInstructionApiConstants.amountParamName, element); - baseDataValidator.reset().parameter(StandingInstructionApiConstants.amountParamName).value(transferAmount).positiveAmount(); + final Integer instructionType = this.fromApiJsonHelper.extractIntegerNamed(instructionTypeParamName, element, Locale.getDefault()); + baseDataValidator.reset().parameter(instructionTypeParamName).value(instructionType).notNull().inMinMaxRange(1, 2); - final Integer transferType = this.fromApiJsonHelper.extractIntegerNamed(transferTypeParamName, element, Locale.getDefault()); - baseDataValidator.reset().parameter(transferTypeParamName).value(transferType).notNull().inMinMaxRange(1, 3); + final Integer status = this.fromApiJsonHelper.extractIntegerNamed(statusParamName, element, Locale.getDefault()); + baseDataValidator.reset().parameter(statusParamName).value(status).notNull().inMinMaxRange(1, 2); + + final LocalDate validFrom = this.fromApiJsonHelper.extractLocalDateNamed(validFromParamName, element); + baseDataValidator.reset().parameter(validFromParamName).value(validFrom).notNull(); + + final LocalDate validTill = this.fromApiJsonHelper.extractLocalDateNamed(validTillParamName, element); + baseDataValidator.reset().parameter(validTillParamName).value(validTill).validateDateAfter(validFrom); + + final Integer recurrenceType = this.fromApiJsonHelper.extractIntegerNamed(recurrenceTypeParamName, element, Locale.getDefault()); + baseDataValidator.reset().parameter(recurrenceTypeParamName).value(recurrenceType).notNull().inMinMaxRange(1, 2); - final Integer priority = this.fromApiJsonHelper.extractIntegerNamed(StandingInstructionApiConstants.priorityParamName, element, + final Integer recurrenceFrequency = this.fromApiJsonHelper.extractIntegerNamed(recurrenceFrequencyParamName, element, + Locale.getDefault()); + final Integer recurrenceInterval = this.fromApiJsonHelper.extractIntegerNamed(recurrenceIntervalParamName, element, Locale.getDefault()); - baseDataValidator.reset().parameter(StandingInstructionApiConstants.priorityParamName).value(priority).notNull().inMinMaxRange(1, - 4); - - final Integer standingInstructionType = this.fromApiJsonHelper - .extractIntegerNamed(StandingInstructionApiConstants.instructionTypeParamName, element, Locale.getDefault()); - baseDataValidator.reset().parameter(StandingInstructionApiConstants.instructionTypeParamName).value(standingInstructionType) - .notNull().inMinMaxRange(1, 2); - - final Integer recurrenceType = this.fromApiJsonHelper.extractIntegerNamed(StandingInstructionApiConstants.recurrenceTypeParamName, - element, Locale.getDefault()); - baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceTypeParamName).value(recurrenceType).notNull() - .inMinMaxRange(1, 2); - boolean isPeriodic = false; - if (recurrenceType != null) { - isPeriodic = AccountTransferRecurrenceType.fromInt(recurrenceType).isPeriodicRecurrence(); - } - final Integer recurrenceFrequency = this.fromApiJsonHelper - .extractIntegerNamed(StandingInstructionApiConstants.recurrenceFrequencyParamName, element, Locale.getDefault()); - baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceFrequencyParamName).value(recurrenceFrequency) - .inMinMaxRange(0, 3); - - if (recurrenceFrequency != null) { - PeriodFrequencyType frequencyType = PeriodFrequencyType.fromInt(recurrenceFrequency); - if (frequencyType.isMonthly() || frequencyType.isYearly()) { - final MonthDay monthDay = this.fromApiJsonHelper - .extractMonthDayNamed(StandingInstructionApiConstants.recurrenceOnMonthDayParamName, element); - baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceOnMonthDayParamName).value(monthDay) - .notNull(); + if (isPeriodicRecurrence(recurrenceType)) { + baseDataValidator.reset().parameter(recurrenceFrequencyParamName).value(recurrenceFrequency).notNull().inMinMaxRange(0, 3); + baseDataValidator.reset().parameter(recurrenceIntervalParamName).value(recurrenceInterval).notNull().integerGreaterThanZero(); + + if (recurrenceFrequency != null && recurrenceInterval != null) { + MonthDay monthDay = null; + + if (areMonthlyOrYearlyFrequency(recurrenceFrequency)) { + final String monthDayFormat = this.fromApiJsonHelper.extractStringNamed(monthDayFormatParamName, element); + baseDataValidator.reset().parameter(monthDayFormatParamName).value(monthDayFormat).notBlank(); + + final String monthDayStr = this.fromApiJsonHelper.extractStringNamed(recurrenceOnMonthDayParamName, element); + baseDataValidator.reset().parameter(recurrenceOnMonthDayParamName).value(monthDayStr).notBlank(); + + if (areNotBlankMonthDayAndMonthDayFormat(monthDayStr, monthDayFormat)) { + try { + monthDay = this.fromApiJsonHelper.extractMonthDayNamed(recurrenceOnMonthDayParamName, element); + } catch (Exception e) { + baseDataValidator.reset().parameter(recurrenceOnMonthDayParamName) + .failWithCode(StandingInstructionApiConstants.INVALID_MONTH_DAY_FORMAT_ERROR_CODE); + } + } + } + + if (areNotNullDates(validFrom, validTill)) { + LocalDate minValidTill = getMinValidTill(recurrenceFrequency, validFrom, recurrenceInterval, monthDay); + if (minValidTill != null && !validTill.isBefore(validFrom) && validTill.isBefore(minValidTill)) { + baseDataValidator.reset().parameter(validTillParamName).value(validTill) + .failWithCode(StandingInstructionApiConstants.BEFORE_FIRST_EXECUTION_DATE_ERROR_CODE); + } + } } } - final Integer recurrenceInterval = this.fromApiJsonHelper - .extractIntegerNamed(StandingInstructionApiConstants.recurrenceIntervalParamName, element, Locale.getDefault()); - if (isPeriodic) { - baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceIntervalParamName).value(recurrenceInterval) - .notNull(); - baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceFrequencyParamName).value(recurrenceFrequency) - .notNull(); - } - baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceIntervalParamName).value(recurrenceInterval) - .integerGreaterThanZero(); + final BigDecimal amount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(amountParamName, element); - final String name = this.fromApiJsonHelper.extractStringNamed(StandingInstructionApiConstants.nameParamName, element); - baseDataValidator.reset().parameter(StandingInstructionApiConstants.nameParamName).value(name).notNull(); + if (isFixedInstruction(instructionType) && isPeriodicRecurrence(recurrenceType)) { + baseDataValidator.reset().parameter(amountParamName).value(amount).notNull().positiveAmount(); + } - final Integer toAccountType = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(toAccountTypeParamName, element); - if (toAccountType != null && PortfolioAccountType.SAVINGS.equals(PortfolioAccountType.fromInt(toAccountType))) { - baseDataValidator.reset().parameter(StandingInstructionApiConstants.instructionTypeParamName).value(standingInstructionType) - .notNull().inMinMaxRange(1, 1); - baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceTypeParamName).value(recurrenceType).notNull() - .inMinMaxRange(1, 1); + if (isDuesInstruction(instructionType) && amount != null) { + baseDataValidator.reset().parameter(amountParamName) + .failWithCode(StandingInstructionApiConstants.AMOUNT_NOT_ALLOWED_FOR_DUES_ERROR_CODE); + } + if (isAccountTransfer(transferType) && isDuesInstruction(instructionType)) { + baseDataValidator.reset().parameter(instructionTypeParamName) + .failWithCode(StandingInstructionApiConstants.INSTRUCTION_TYPE_DUES_NOT_ALLOWED_FOR_ACCOUNT_TRANSFER_ERROR_CODE); } - if (standingInstructionType != null && StandingInstructionType.fromInt(standingInstructionType).isFixedAmoutTransfer()) { - baseDataValidator.reset().parameter(StandingInstructionApiConstants.amountParamName).value(transferAmount).notNull(); + if (isAccountTransfer(transferType) && isAsPerDuesRecurrence(recurrenceType)) { + baseDataValidator.reset().parameter(recurrenceTypeParamName) + .failWithCode(StandingInstructionApiConstants.RECURRENCE_AS_PER_DUES_NOT_ALLOWED_FOR_SAVINGS_ERROR_CODE); + } + if (isLoanRepayment(transferType) && isFixedInstruction(instructionType) && isAsPerDuesRecurrence(recurrenceType)) { + baseDataValidator.reset().parameter(recurrenceTypeParamName) + .failWithCode(StandingInstructionApiConstants.RECURRENCE_AS_PER_DUES_NOT_ALLOWED_WITH_FIXED_INSTRUCTION_ERROR_CODE); } - String errorCode = null; - AccountTransferType accountTransferType = AccountTransferType.fromInt(transferType); final Integer fromAccountType = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(fromAccountTypeParamName, element); + final Integer toAccountType = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(toAccountTypeParamName, element); if (fromAccountType != null && toAccountType != null) { - PortfolioAccountType fromPortfolioAccountType = PortfolioAccountType.fromInt(fromAccountType); - PortfolioAccountType toPortfolioAccountType = PortfolioAccountType.fromInt(toAccountType); - if (accountTransferType.isAccountTransfer() && (PortfolioAccountType.LOAN.equals(fromPortfolioAccountType) - || PortfolioAccountType.LOAN.equals(toPortfolioAccountType))) { - errorCode = "not.account.transfer"; - } else if (accountTransferType.isLoanRepayment() && (PortfolioAccountType.LOAN.equals(fromPortfolioAccountType) - || PortfolioAccountType.SAVINGS.equals(toPortfolioAccountType))) { - errorCode = "not.loan.repayment"; + String errorCode = null; + if (isAccountTransfer(transferType) && (isLoanAccount(fromAccountType) || isLoanAccount(toAccountType))) { + errorCode = StandingInstructionApiConstants.ACCOUNT_TRANSFER_NOT_ALLOWED_FOR_LOAN_ERROR_CODE; + } else if (isLoanRepayment(transferType) && (isLoanAccount(fromAccountType) || isSavingsAccount(toAccountType))) { + errorCode = StandingInstructionApiConstants.NOT_A_VALID_LOAN_REPAYMENT_ERROR_CODE; } + if (errorCode != null) { baseDataValidator.reset().parameter(transferTypeParamName).failWithCode(errorCode); } + + if (isAccountTransfer(transferType) && isSavingsAccount(fromAccountType) && isSavingsAccount(toAccountType)) { + final Long fromOfficeId = this.fromApiJsonHelper.extractLongNamed(fromOfficeIdParamName, element); + final Long toOfficeId = this.fromApiJsonHelper.extractLongNamed(toOfficeIdParamName, element); + final Long fromAccountId = this.fromApiJsonHelper.extractLongNamed(fromAccountIdParamName, element); + final Long toAccountId = this.fromApiJsonHelper.extractLongNamed(toAccountIdParamName, element); + + if (areEqualOfficesAndEqualAccounts(fromOfficeId, toOfficeId, fromAccountId, toAccountId)) { + baseDataValidator.reset().parameter(toAccountIdParamName) + .failWithCode(StandingInstructionApiConstants.CANNOT_TRANSFER_TO_SAME_ACCOUNT_ERROR_CODE); + } + } } throwExceptionIfValidationWarningsExist(dataValidationErrors); } - public void validateForUpdate(final JsonCommand command) { + public void validateForUpdate(final JsonCommand command, final AccountTransferStandingInstruction existingStandingInstruction) { final String json = command.json(); if (StringUtils.isBlank(json)) { @@ -215,72 +243,140 @@ public void validateForUpdate(final JsonCommand command) { final List dataValidationErrors = new ArrayList<>(); final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) - .resource(StandingInstructionApiConstants.STANDING_INSTRUCTION_RESOURCE_NAME); + .resource(STANDING_INSTRUCTION_RESOURCE_NAME); final JsonElement element = command.parsedJson(); - if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.validFromParamName, element)) { - final LocalDate validFrom = this.fromApiJsonHelper.extractLocalDateNamed(StandingInstructionApiConstants.validFromParamName, - element); - baseDataValidator.reset().parameter(StandingInstructionApiConstants.validFromParamName).value(validFrom).notNull(); + + if (this.fromApiJsonHelper.parameterExists(nameParamName, element)) { + final String name = this.fromApiJsonHelper.extractStringNamed(nameParamName, element); + baseDataValidator.reset().parameter(nameParamName).value(name).notNull(); } - if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.validTillParamName, element)) { - final LocalDate validTill = this.fromApiJsonHelper.extractLocalDateNamed(StandingInstructionApiConstants.validTillParamName, - element); - baseDataValidator.reset().parameter(StandingInstructionApiConstants.validTillParamName).value(validTill).notNull(); + if (this.fromApiJsonHelper.parameterExists(priorityParamName, element)) { + final Integer priority = this.fromApiJsonHelper.extractIntegerNamed(priorityParamName, element, Locale.getDefault()); + baseDataValidator.reset().parameter(priorityParamName).value(priority).notNull().inMinMaxRange(1, 4); } - if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.amountParamName, element)) { - final BigDecimal transferAmount = this.fromApiJsonHelper - .extractBigDecimalWithLocaleNamed(StandingInstructionApiConstants.amountParamName, element); - baseDataValidator.reset().parameter(StandingInstructionApiConstants.amountParamName).value(transferAmount).positiveAmount(); + final Integer existingTransferType = existingStandingInstruction.getAccountTransferDetails().getTransferType(); + + Integer instructionType = existingStandingInstruction.getInstructionType(); + if (this.fromApiJsonHelper.parameterExists(instructionTypeParamName, element)) { + instructionType = this.fromApiJsonHelper.extractIntegerNamed(instructionTypeParamName, element, Locale.getDefault()); + baseDataValidator.reset().parameter(instructionTypeParamName).value(instructionType).notNull().inMinMaxRange(1, 2); + if (isAccountTransfer(existingTransferType) && isDuesInstruction(instructionType)) { + baseDataValidator.reset().parameter(instructionTypeParamName) + .failWithCode(StandingInstructionApiConstants.INSTRUCTION_TYPE_DUES_NOT_ALLOWED_FOR_ACCOUNT_TRANSFER_ERROR_CODE); + } } - if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.statusParamName, element)) { - final Integer status = this.fromApiJsonHelper.extractIntegerNamed(StandingInstructionApiConstants.statusParamName, element, - Locale.getDefault()); - baseDataValidator.reset().parameter(StandingInstructionApiConstants.statusParamName).value(status).notNull().inMinMaxRange(1, - 2); + if (this.fromApiJsonHelper.parameterExists(statusParamName, element)) { + final Integer status = this.fromApiJsonHelper.extractIntegerNamed(statusParamName, element, Locale.getDefault()); + baseDataValidator.reset().parameter(statusParamName).value(status).notNull().inMinMaxRange(1, 2); } - if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.priorityParamName, element)) { - final Integer priority = this.fromApiJsonHelper.extractIntegerNamed(StandingInstructionApiConstants.priorityParamName, element, - Locale.getDefault()); - baseDataValidator.reset().parameter(StandingInstructionApiConstants.priorityParamName).value(priority).notNull() - .inMinMaxRange(1, 4); + LocalDate validFrom = existingStandingInstruction.getValidFrom(); + if (this.fromApiJsonHelper.parameterExists(validFromParamName, element)) { + validFrom = this.fromApiJsonHelper.extractLocalDateNamed(validFromParamName, element); + baseDataValidator.reset().parameter(validFromParamName).value(validFrom).notNull(); + LocalDate existingValidTill = existingStandingInstruction.getValidTill(); + if (validFrom != null && existingValidTill != null && validFrom.isAfter(existingValidTill) + && !this.fromApiJsonHelper.parameterExists(validTillParamName, element)) { + baseDataValidator.reset().parameter(validFromParamName) + .failWithCode(StandingInstructionApiConstants.MUST_BE_BEFORE_EXISTING_VALID_TILL_ERROR_CODE); + } + } + + LocalDate validTill = existingStandingInstruction.getValidTill(); + if (this.fromApiJsonHelper.parameterExists(validTillParamName, element)) { + validTill = this.fromApiJsonHelper.extractLocalDateNamed(validTillParamName, element); + baseDataValidator.reset().parameter(validTillParamName).value(validTill).notNull(); + if (areNotNullDates(validFrom, validTill)) { + baseDataValidator.reset().parameter(validTillParamName).value(validTill).validateDateAfter(validFrom); + } + if (areNotNullDates(existingStandingInstruction.getLastRunDate(), validTill) + && validTill.isBefore(existingStandingInstruction.getLastRunDate())) { + baseDataValidator.reset().parameter(validTillParamName).value(validTill) + .failWithCode(StandingInstructionApiConstants.CANNOT_BE_BEFORE_LAST_RUN_DATE_ERROR_CODE); + } + } + + Integer recurrenceType = existingStandingInstruction.getRecurrenceType(); + if (this.fromApiJsonHelper.parameterExists(recurrenceTypeParamName, element)) { + recurrenceType = this.fromApiJsonHelper.extractIntegerNamed(recurrenceTypeParamName, element, Locale.getDefault()); + baseDataValidator.reset().parameter(recurrenceTypeParamName).value(recurrenceType).notNull().inMinMaxRange(1, 2); + if (isAccountTransfer(existingTransferType) && isAsPerDuesRecurrence(recurrenceType)) { + baseDataValidator.reset().parameter(recurrenceTypeParamName) + .failWithCode(StandingInstructionApiConstants.RECURRENCE_AS_PER_DUES_NOT_ALLOWED_FOR_SAVINGS_ERROR_CODE); + } + if (isLoanRepayment(existingTransferType) && isFixedInstruction(instructionType) && isAsPerDuesRecurrence(recurrenceType)) { + baseDataValidator.reset().parameter(recurrenceTypeParamName) + .failWithCode(StandingInstructionApiConstants.RECURRENCE_AS_PER_DUES_NOT_ALLOWED_WITH_FIXED_INSTRUCTION_ERROR_CODE); + } } - if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.instructionTypeParamName, element)) { - final Integer standingInstructionType = this.fromApiJsonHelper - .extractIntegerNamed(StandingInstructionApiConstants.instructionTypeParamName, element, Locale.getDefault()); - baseDataValidator.reset().parameter(StandingInstructionApiConstants.instructionTypeParamName).value(standingInstructionType) - .notNull().inMinMaxRange(1, 2); + BigDecimal amount = null; + if (this.fromApiJsonHelper.parameterExists(amountParamName, element)) { + amount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(amountParamName, element); } - if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.recurrenceTypeParamName, element)) { - final Integer recurrenceType = this.fromApiJsonHelper - .extractIntegerNamed(StandingInstructionApiConstants.recurrenceTypeParamName, element, Locale.getDefault()); - baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceTypeParamName).value(recurrenceType).notNull() - .inMinMaxRange(1, 2); + if (isDuesInstruction(instructionType)) { + if (amount != null) { + baseDataValidator.reset().parameter(amountParamName) + .failWithCode(StandingInstructionApiConstants.AMOUNT_NOT_ALLOWED_FOR_DUES_ERROR_CODE); + } + } else if (isFixedInstruction(instructionType) && isPeriodicRecurrence(recurrenceType)) { + BigDecimal finalAmount = (amount != null) ? amount : existingStandingInstruction.getAmount(); + baseDataValidator.reset().parameter(amountParamName).value(finalAmount).positiveAmount(); } - if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.recurrenceFrequencyParamName, element)) { - final Integer recurrenceFrequency = this.fromApiJsonHelper - .extractIntegerNamed(StandingInstructionApiConstants.recurrenceFrequencyParamName, element, Locale.getDefault()); - baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceFrequencyParamName).value(recurrenceFrequency) - .inMinMaxRange(0, 3); + Integer recurrenceFrequency = existingStandingInstruction.getRecurrenceFrequency(); + if (this.fromApiJsonHelper.parameterExists(recurrenceFrequencyParamName, element)) { + recurrenceFrequency = this.fromApiJsonHelper.extractIntegerNamed(recurrenceFrequencyParamName, element, Locale.getDefault()); } - if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.recurrenceIntervalParamName, element)) { - final Integer recurrenceInterval = this.fromApiJsonHelper - .extractIntegerNamed(StandingInstructionApiConstants.recurrenceIntervalParamName, element, Locale.getDefault()); - baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceIntervalParamName).value(recurrenceInterval) - .integerGreaterThanZero(); + Integer recurrenceInterval = existingStandingInstruction.getRecurrenceInterval(); + if (this.fromApiJsonHelper.parameterExists(recurrenceIntervalParamName, element)) { + recurrenceInterval = this.fromApiJsonHelper.extractIntegerNamed(recurrenceIntervalParamName, element, Locale.getDefault()); } - if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.nameParamName, element)) { - final String name = this.fromApiJsonHelper.extractStringNamed(StandingInstructionApiConstants.nameParamName, element); - baseDataValidator.reset().parameter(StandingInstructionApiConstants.nameParamName).value(name).notNull(); + if (isPeriodicRecurrence(recurrenceType)) { + baseDataValidator.reset().parameter(recurrenceFrequencyParamName).value(recurrenceFrequency).notNull().inMinMaxRange(0, 3); + baseDataValidator.reset().parameter(recurrenceIntervalParamName).value(recurrenceInterval).notNull().integerGreaterThanZero(); + + if (recurrenceFrequency != null && recurrenceInterval != null) { + MonthDay monthDay = existingStandingInstruction.getRecurrenceOnDay() != null + ? MonthDay.of(existingStandingInstruction.getRecurrenceOnMonth(), existingStandingInstruction.getRecurrenceOnDay()) + : null; + + if (areMonthlyOrYearlyFrequency(recurrenceFrequency)) { + boolean hasMonthDay = this.fromApiJsonHelper.parameterExists(monthDayFormatParamName, element); + boolean hasMonthDayFormat = this.fromApiJsonHelper.parameterExists(recurrenceOnMonthDayParamName, element); + if (hasMonthDay || hasMonthDayFormat) { + String monthDayFormat = this.fromApiJsonHelper.extractStringNamed(monthDayFormatParamName, element); + baseDataValidator.reset().parameter(monthDayFormatParamName).value(monthDayFormat).notBlank(); + + String monthDayStr = this.fromApiJsonHelper.extractStringNamed(recurrenceOnMonthDayParamName, element); + baseDataValidator.reset().parameter(recurrenceOnMonthDayParamName).value(monthDayStr).notBlank(); + + if (areNotBlankMonthDayAndMonthDayFormat(monthDayStr, monthDayFormat)) { + try { + monthDay = this.fromApiJsonHelper.extractMonthDayNamed(recurrenceOnMonthDayParamName, element); + } catch (Exception e) { + baseDataValidator.reset().parameter(recurrenceOnMonthDayParamName) + .failWithCode(StandingInstructionApiConstants.INVALID_MONTH_DAY_FORMAT_ERROR_CODE); + } + } + } + } + + if (areNotNullDates(validFrom, validTill)) { + LocalDate minValidTill = getMinValidTill(recurrenceFrequency, validFrom, recurrenceInterval, monthDay); + if (minValidTill != null && !validTill.isBefore(validFrom) && validTill.isBefore(minValidTill)) { + baseDataValidator.reset().parameter(validTillParamName).value(validTill) + .failWithCode(StandingInstructionApiConstants.BEFORE_FIRST_EXECUTION_DATE_ERROR_CODE); + } + } + } } throwExceptionIfValidationWarningsExist(dataValidationErrors); @@ -291,4 +387,73 @@ private void throwExceptionIfValidationWarningsExist(final List { @ManyToOne diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/domain/AccountTransferStandingInstruction.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/domain/AccountTransferStandingInstruction.java index fb437308707..7ede3cb308b 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/domain/AccountTransferStandingInstruction.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/domain/AccountTransferStandingInstruction.java @@ -19,6 +19,9 @@ package org.apache.fineract.portfolio.account.domain; import static org.apache.fineract.portfolio.account.AccountDetailConstants.transferTypeParamName; +import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.ACCOUNT_TRANSFER_NOT_ALLOWED_FOR_LOAN_ERROR_CODE; +import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.AMOUNT_NOT_ALLOWED_FOR_DUES_ERROR_CODE; +import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.NOT_A_VALID_LOAN_REPAYMENT_ERROR_CODE; import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.STANDING_INSTRUCTION_RESOURCE_NAME; import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.amountParamName; import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.instructionTypeParamName; @@ -44,6 +47,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import lombok.Getter; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.ApiParameterError; import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; @@ -54,6 +58,7 @@ @Entity @Table(name = "m_account_transfer_standing_instructions", uniqueConstraints = { @UniqueConstraint(columnNames = { "name" }, name = "name") }) +@Getter public class AccountTransferStandingInstruction extends AbstractPersistableCustom { @ManyToOne @@ -97,7 +102,7 @@ public class AccountTransferStandingInstruction extends AbstractPersistableCusto private Integer recurrenceOnMonth; @Column(name = "last_run_date") - private LocalDate latsRunDate; + private LocalDate lastRunDate; protected AccountTransferStandingInstruction() { @@ -205,13 +210,13 @@ public Map update(JsonCommand command) { final MonthDay monthDay = command.extractMonthDayNamed(recurrenceOnMonthDayParamName); final String actualValueEntered = command.stringValueOfParameterNamed(recurrenceOnMonthDayParamName); final Integer dayOfMonthValue = monthDay.getDayOfMonth(); - if (!this.recurrenceOnDay.equals(dayOfMonthValue)) { + if (!java.util.Objects.equals(this.recurrenceOnDay, dayOfMonthValue)) { actualChanges.put(recurrenceOnMonthDayParamName, actualValueEntered); this.recurrenceOnDay = dayOfMonthValue; } final Integer monthOfYear = monthDay.getMonthValue(); - if (!this.recurrenceOnMonth.equals(monthOfYear)) { + if (!java.util.Objects.equals(this.recurrenceOnMonth, monthOfYear)) { actualChanges.put(recurrenceOnMonthDayParamName, actualValueEntered); this.recurrenceOnMonth = monthOfYear; } @@ -222,6 +227,46 @@ public Map update(JsonCommand command) { actualChanges.put(recurrenceIntervalParamName, newValue); this.recurrenceInterval = newValue; } + + if (StandingInstructionType.fromInt(this.instructionType).isDuesAmoutTransfer()) { + if (this.amount != null) { + actualChanges.put(amountParamName, null); + this.amount = null; + } + } + + if (AccountTransferRecurrenceType.fromInt(this.recurrenceType).isDuesRecurrence()) { + if (this.recurrenceFrequency != null) { + actualChanges.put(recurrenceFrequencyParamName, null); + this.recurrenceFrequency = null; + } + if (this.recurrenceInterval != null) { + actualChanges.put(recurrenceIntervalParamName, null); + this.recurrenceInterval = null; + } + if (this.recurrenceOnDay != null) { + actualChanges.put(recurrenceOnMonthDayParamName, null); + this.recurrenceOnDay = null; + } + if (this.recurrenceOnMonth != null) { + this.recurrenceOnMonth = null; + } + } + + if (this.recurrenceFrequency != null) { + final PeriodFrequencyType frequencyType = PeriodFrequencyType.fromInt(this.recurrenceFrequency); + + if (frequencyType.isDaily() || frequencyType.isWeekly()) { + if (this.recurrenceOnDay != null) { + actualChanges.put(recurrenceOnMonthDayParamName, null); + this.recurrenceOnDay = null; + } + if (this.recurrenceOnMonth != null) { + this.recurrenceOnMonth = null; + } + } + } + validateDependencies(baseDataValidator); if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); @@ -258,13 +303,19 @@ private void validateDependencies(final DataValidatorBuilder baseDataValidator) baseDataValidator.reset().parameter(amountParamName).value(this.amount).notNull(); } + if (StandingInstructionType.fromInt(this.instructionType).isDuesAmoutTransfer()) { + if (this.amount != null) { + baseDataValidator.reset().parameter(amountParamName).failWithCode(AMOUNT_NOT_ALLOWED_FOR_DUES_ERROR_CODE); + } + } + String errorCode = null; if (this.accountTransferDetails.transferType().isAccountTransfer() && (this.accountTransferDetails.fromSavingsAccount() == null || this.accountTransferDetails.toSavingsAccount() == null)) { - errorCode = "not.account.transfer"; + errorCode = ACCOUNT_TRANSFER_NOT_ALLOWED_FOR_LOAN_ERROR_CODE; } else if (this.accountTransferDetails.transferType().isLoanRepayment() && (this.accountTransferDetails.fromSavingsAccount() == null || this.accountTransferDetails.toLoanAccount() == null)) { - errorCode = "not.loan.repayment"; + errorCode = NOT_A_VALID_LOAN_REPAYMENT_ERROR_CODE; } if (errorCode != null) { baseDataValidator.reset().parameter(transferTypeParamName).failWithCode(errorCode); @@ -272,8 +323,8 @@ private void validateDependencies(final DataValidatorBuilder baseDataValidator) } - public void updateLatsRunDate(LocalDate latsRunDate) { - this.latsRunDate = latsRunDate; + public void updateLastRunDate(LocalDate lastRunDate) { + this.lastRunDate = lastRunDate; } public void updateStatus(Integer status) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/service/StandingInstructionReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/service/StandingInstructionReadPlatformServiceImpl.java index 9a947c029ca..bda10351ea6 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/service/StandingInstructionReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/service/StandingInstructionReadPlatformServiceImpl.java @@ -220,14 +220,24 @@ public StandingInstructionData retrieveTemplate(final Long fromOfficeId, final L */); final Collection statusOptions = Arrays.asList(standingInstructionStatus(StandingInstructionStatus.ACTIVE), standingInstructionStatus(StandingInstructionStatus.DISABLED)); - final Collection instructionTypeOptions = Arrays.asList(standingInstructionType(StandingInstructionType.FIXED), - standingInstructionType(StandingInstructionType.DUES)); final Collection priorityOptions = Arrays.asList(standingInstructionPriority(StandingInstructionPriority.URGENT), standingInstructionPriority(StandingInstructionPriority.HIGH), standingInstructionPriority(StandingInstructionPriority.MEDIUM), standingInstructionPriority(StandingInstructionPriority.LOW)); - final Collection recurrenceTypeOptions = Arrays.asList(recurrenceType(AccountTransferRecurrenceType.PERIODIC), - recurrenceType(AccountTransferRecurrenceType.AS_PER_DUES)); + + Collection instructionTypeOptions = null; + Collection recurrenceTypeOptions = null; + + if (accountTransferType.isAccountTransfer()) { + instructionTypeOptions = Arrays.asList(standingInstructionType(StandingInstructionType.FIXED)); + recurrenceTypeOptions = Arrays.asList(recurrenceType(AccountTransferRecurrenceType.PERIODIC)); + } else { + instructionTypeOptions = Arrays.asList(standingInstructionType(StandingInstructionType.FIXED), + standingInstructionType(StandingInstructionType.DUES)); + recurrenceTypeOptions = Arrays.asList(recurrenceType(AccountTransferRecurrenceType.PERIODIC), + recurrenceType(AccountTransferRecurrenceType.AS_PER_DUES)); + } + final Collection recurrenceFrequencyOptions = this.dropdownReadPlatformService.retrievePeriodFrequencyTypeOptions(); return StandingInstructionData.template(fromOffice, fromClient, fromAccountTypeData, fromAccount, transferDate, toOffice, toClient, diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/service/StandingInstructionWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/service/StandingInstructionWritePlatformServiceImpl.java index 52699095fe6..ac76d5d35f4 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/service/StandingInstructionWritePlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/service/StandingInstructionWritePlatformServiceImpl.java @@ -123,17 +123,26 @@ private boolean isSavingsToSavingsAccountTransfer(final PortfolioAccountType fro return PortfolioAccountType.SAVINGS.equals(fromAccountType) && PortfolioAccountType.SAVINGS.equals(toAccountType); } + @Transactional @Override public CommandProcessingResult update(final Long id, final JsonCommand command) { - this.standingInstructionDataValidator.validateForUpdate(command); - AccountTransferStandingInstruction standingInstructionsForUpdate = this.standingInstructionRepository.findById(id) + final AccountTransferStandingInstruction standingInstructionForUpdate = this.standingInstructionRepository.findById(id) .orElseThrow(() -> new StandingInstructionNotFoundException(id)); - final Map actualChanges = standingInstructionsForUpdate.update(command); - return new CommandProcessingResultBuilder() // - .withCommandId(command.commandId()) // - .withEntityId(id) // - .with(actualChanges) // - .build(); + + this.standingInstructionDataValidator.validateForUpdate(command, standingInstructionForUpdate); + + final Map actualChanges = standingInstructionForUpdate.update(command); + if (!actualChanges.isEmpty()) { + try { + this.standingInstructionRepository.saveAndFlush(standingInstructionForUpdate); + } catch (final JpaSystemException | DataIntegrityViolationException dve) { + final Throwable throwable = dve.getMostSpecificCause(); + handleDataIntegrityIssues(command, throwable, dve); + return CommandProcessingResult.empty(); + } + } + + return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(id).with(actualChanges).build(); } @Override diff --git a/fineract-provider/src/test/java/org/apache/fineract/portfolio/account/data/StandingInstructionDataValidatorTest.java b/fineract-provider/src/test/java/org/apache/fineract/portfolio/account/data/StandingInstructionDataValidatorTest.java new file mode 100644 index 00000000000..cdf28c646a1 --- /dev/null +++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/account/data/StandingInstructionDataValidatorTest.java @@ -0,0 +1,668 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.account.data; + +import static org.apache.fineract.portfolio.account.AccountDetailConstants.dateFormatParamName; +import static org.apache.fineract.portfolio.account.AccountDetailConstants.fromAccountIdParamName; +import static org.apache.fineract.portfolio.account.AccountDetailConstants.fromAccountTypeParamName; +import static org.apache.fineract.portfolio.account.AccountDetailConstants.fromClientIdParamName; +import static org.apache.fineract.portfolio.account.AccountDetailConstants.fromOfficeIdParamName; +import static org.apache.fineract.portfolio.account.AccountDetailConstants.localeParamName; +import static org.apache.fineract.portfolio.account.AccountDetailConstants.toAccountIdParamName; +import static org.apache.fineract.portfolio.account.AccountDetailConstants.toAccountTypeParamName; +import static org.apache.fineract.portfolio.account.AccountDetailConstants.toClientIdParamName; +import static org.apache.fineract.portfolio.account.AccountDetailConstants.toOfficeIdParamName; +import static org.apache.fineract.portfolio.account.AccountDetailConstants.transferTypeParamName; +import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.amountParamName; +import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.instructionTypeParamName; +import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.monthDayFormatParamName; +import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.nameParamName; +import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.priorityParamName; +import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.recurrenceFrequencyParamName; +import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.recurrenceIntervalParamName; +import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.recurrenceOnMonthDayParamName; +import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.recurrenceTypeParamName; +import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.statusParamName; +import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.validFromParamName; +import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.validTillParamName; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import com.google.gson.JsonElement; +import com.google.gson.JsonNull; +import com.google.gson.JsonObject; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.stream.Stream; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; +import org.apache.fineract.infrastructure.core.exception.InvalidJsonException; +import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; +import org.apache.fineract.infrastructure.core.exception.UnsupportedParameterException; +import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; +import org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants; +import org.apache.fineract.portfolio.account.domain.AccountTransferDetails; +import org.apache.fineract.portfolio.account.domain.AccountTransferStandingInstruction; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +public class StandingInstructionDataValidatorTest { + + private static final String VALIDATION_MSG_PREFIX = "validation.msg"; + private static final String MSG_SEPARATOR = "."; + private static final String CANNOT_BE_BLANK_ERROR_CODE = "cannot.be.blank"; + private static final String OUT_OF_RANGE_ERROR_CODE = "is.not.within.expected.range"; + private static final String DATE_IS_BEFORE_ERROR_CODE = "is.less.than.date"; + private static final String NOT_GREATER_THAN_ZERO_ERROR_CODE = "not.greater.than.zero"; + + private static final String invalidParamName = "invalidParam"; + private static final String invalidValue = "invalidValue"; + + @Mock + private AccountTransfersDetailDataValidator accountTransfersDetailDataValidator; + + @Mock + private AccountTransferStandingInstruction existingStandingInstruction; + + @Mock + private AccountTransferDetails accountTransferDetails; + + private static final FromJsonHelper fromApiJsonHelper = new FromJsonHelper(); + private StandingInstructionDataValidator standingInstructionDataValidator; + + private ValidationMode validationMode = ValidationMode.CREATE; + + @BeforeEach + public void setUp() { + this.standingInstructionDataValidator = new StandingInstructionDataValidator(fromApiJsonHelper, + this.accountTransfersDetailDataValidator); + } + + @Nested + class Common { + + @ParameterizedTest(name = "Mode: {0}") + @MethodSource("org.apache.fineract.portfolio.account.data.StandingInstructionDataValidatorTest#validationModes") + void shouldFailWhenRequestBodyIsNull(ValidationMode mode) { + validationMode = mode; + assertThrowsException(InvalidJsonException.class, null); + } + + @ParameterizedTest(name = "Mode: {0}") + @MethodSource("org.apache.fineract.portfolio.account.data.StandingInstructionDataValidatorTest#validationModes") + void shouldFailWhenRequestContainsUnknownParameter(ValidationMode mode) { + validationMode = mode; + + final JsonObject json = validationMode == ValidationMode.CREATE ? createAccountTransferRequest() + : commonValuesInUpdateRequest(); + + json.addProperty(invalidParamName, invalidValue); + + assertThrowsException(UnsupportedParameterException.class, json); + } + } + + @Nested + class WhenCreatingStandingInstruction { + + @BeforeEach + public void setUpCreateMode() { + validationMode = ValidationMode.CREATE; + } + + @Nested + class BaseRules { + + @Test + void shouldValidateAccountTransferDetails() { + final JsonObject json = createAccountTransferRequest(); + standingInstructionDataValidator.validateForCreate(command(json)); + + verify(accountTransfersDetailDataValidator, times(1)).validate(any(JsonCommand.class), any(DataValidatorBuilder.class)); + } + + @ParameterizedTest + @MethodSource("requiredBaseParameters") + void shouldFailWhenRequiredParameterIsMissing(String parameter) { + final JsonObject json = createAccountTransferRequest(); + json.remove(parameter); + + assertBlank(json, parameter); + } + + @ParameterizedTest + @MethodSource("parametersWithInvalidValues") + void shouldFailWhenParameterHasInvalidValue(String parameter, Integer invalidValue) { + final JsonObject json = createAccountTransferRequest(); + json.addProperty(parameter, invalidValue); + + assertRange(json, parameter); + } + + @Test + void shouldFailWhenValidTillIsBeforeValidFrom() { + final JsonObject json = createAccountTransferRequest(); + json.addProperty(validTillParamName, "15 May 2026"); + + assertValidation(json, validTillParamName, DATE_IS_BEFORE_ERROR_CODE); + } + + private static Stream requiredBaseParameters() { + return Stream.of(transferTypeParamName, nameParamName, priorityParamName, instructionTypeParamName, statusParamName, + validFromParamName, recurrenceTypeParamName); + } + + private static Stream parametersWithInvalidValues() { + return Stream.of(Arguments.of(transferTypeParamName, 4), Arguments.of(priorityParamName, 5), + Arguments.of(instructionTypeParamName, 3), Arguments.of(statusParamName, 3), + Arguments.of(recurrenceTypeParamName, 3)); + } + } + + @Nested + class PeriodicRecurrenceRules { + + @ParameterizedTest + @MethodSource("requiredParameters") + void shouldFailWhenPeriodicFieldIsMissing(String parameter) { + final JsonObject json = createAccountTransferRequest(); + json.remove(parameter); + + assertBlank(json, parameter); + } + + @Test + void shouldFailWhenRecurrenceFrequencyHasInvalidValue() { + final JsonObject json = createAccountTransferRequest(); + json.addProperty(recurrenceFrequencyParamName, 4); + + assertRange(json, recurrenceFrequencyParamName); + } + + @Test + void shouldFailWhenRecurrenceIntervalHasInvalidValue() { + final JsonObject json = createAccountTransferRequest(); + json.addProperty(recurrenceIntervalParamName, 0); + + assertValidation(json, recurrenceIntervalParamName, NOT_GREATER_THAN_ZERO_ERROR_CODE); + } + + @Test + void shouldFailWhenRecurrenceOnMonthDayHasInvalidValue() { + final JsonObject json = createAccountTransferRequest(); + json.addProperty(recurrenceOnMonthDayParamName, "08 Mayo"); + + assertValidation(json, recurrenceOnMonthDayParamName, StandingInstructionApiConstants.INVALID_MONTH_DAY_FORMAT_ERROR_CODE); + } + + @ParameterizedTest + @MethodSource("invalidExecutionDates") + void shouldFailWhenValidTillIsBeforeFirstExecution(String validTill, Integer recurrenceFrequency, Integer recurrenceInterval, + String recurrenceOnMonthDay) { + final JsonObject json = createAccountTransferRequest(); + + json.addProperty(validTillParamName, validTill); + json.addProperty(recurrenceFrequencyParamName, recurrenceFrequency); + json.addProperty(recurrenceIntervalParamName, recurrenceInterval); + + if (recurrenceOnMonthDay != null) { + json.addProperty(recurrenceOnMonthDayParamName, recurrenceOnMonthDay); + } + + assertValidation(json, validTillParamName, StandingInstructionApiConstants.BEFORE_FIRST_EXECUTION_DATE_ERROR_CODE); + } + + private static Stream requiredParameters() { + return Stream.of(recurrenceFrequencyParamName, recurrenceIntervalParamName, monthDayFormatParamName, + recurrenceOnMonthDayParamName); + } + + private static Stream invalidExecutionDates() { + return Stream.of(Arguments.of("20 May 2026", 0, 5, null), Arguments.of("29 May 2026", 1, 2, null), + Arguments.of("17 May 2026", 2, 1, "18 May")); + } + + } + + @Nested + class AmountRules { + + @Test + void shouldFailWhenAmountIsMissing() { + final JsonObject json = createAccountTransferRequest(); + json.remove(amountParamName); + + assertBlank(json, amountParamName); + } + + @Test + void shouldFailWhenAmountValueIsNotPositive() { + final JsonObject json = createAccountTransferRequest(); + json.addProperty(amountParamName, BigDecimal.valueOf(-10.00)); + + assertValidation(json, amountParamName, NOT_GREATER_THAN_ZERO_ERROR_CODE); + } + } + + @Nested + class AccountTransferRules { + + @Test + void shouldFailWithEqualAccountsAndEqualOffices() { + final JsonObject json = createAccountTransferRequest(); + json.addProperty(toAccountIdParamName, 1); + + assertValidation(json, toAccountIdParamName, StandingInstructionApiConstants.CANNOT_TRANSFER_TO_SAME_ACCOUNT_ERROR_CODE); + } + + @Test + void shouldFailWhenInstructionTypeIsDues() { + final JsonObject json = createAccountTransferRequest(); + json.addProperty(instructionTypeParamName, 2); + + assertValidation(json, instructionTypeParamName, + StandingInstructionApiConstants.INSTRUCTION_TYPE_DUES_NOT_ALLOWED_FOR_ACCOUNT_TRANSFER_ERROR_CODE); + } + + @Test + void shouldFailWhenRecurrenceTypeIsAsPerDues() { + final JsonObject json = createAccountTransferRequest(); + json.addProperty(recurrenceTypeParamName, 2); + + assertValidation(json, recurrenceTypeParamName, + StandingInstructionApiConstants.RECURRENCE_AS_PER_DUES_NOT_ALLOWED_FOR_SAVINGS_ERROR_CODE); + } + + @Test + void shouldFailWhenAccountTransferInvolvesLoanAccount() { + final JsonObject json = createAccountTransferRequest(); + json.addProperty(fromAccountTypeParamName, 1); + + assertValidation(json, transferTypeParamName, + StandingInstructionApiConstants.ACCOUNT_TRANSFER_NOT_ALLOWED_FOR_LOAN_ERROR_CODE); + } + + @Test + void shouldPassWithDailyPeriodicRecurrence() { + final JsonObject json = createAccountTransferRequest(); + + json.remove(recurrenceOnMonthDayParamName); + json.remove(monthDayFormatParamName); + + json.addProperty(recurrenceFrequencyParamName, 0); + + assertValidationSuccess(json); + } + + @Test + void shouldPassWithYearlyPeriodicRecurrence() { + final JsonObject json = createAccountTransferRequest(); + json.addProperty(recurrenceFrequencyParamName, 3); + + assertValidationSuccess(json); + } + } + + @Nested + class LoanRepaymentRules { + + @Test + void shouldFailWhenInstructionTypeIsFixedAndRecurrenceTypeIsAsPerDues() { + final JsonObject json = createLoanRepaymentRequest(); + json.addProperty(instructionTypeParamName, 1); + + assertValidation(json, recurrenceTypeParamName, + StandingInstructionApiConstants.RECURRENCE_AS_PER_DUES_NOT_ALLOWED_WITH_FIXED_INSTRUCTION_ERROR_CODE); + } + + @Test + void shouldFailWhenInstructionTypeIsDuesAndAmountIsNotNull() { + final JsonObject json = createLoanRepaymentRequest(); + json.addProperty(amountParamName, BigDecimal.TEN); + + assertValidation(json, amountParamName, StandingInstructionApiConstants.AMOUNT_NOT_ALLOWED_FOR_DUES_ERROR_CODE); + } + + @Test + void shouldFailWhenIsNotAValidLoanRepayment() { + final JsonObject json = createLoanRepaymentRequest(); + json.addProperty(toAccountTypeParamName, 2); + + assertValidation(json, transferTypeParamName, StandingInstructionApiConstants.NOT_A_VALID_LOAN_REPAYMENT_ERROR_CODE); + } + + @Test + void shouldPassWithFixedAmountAndPeriodicRecurrence() { + final JsonObject json = createLoanRepaymentRequest(); + json.addProperty(instructionTypeParamName, 1); + json.addProperty(amountParamName, BigDecimal.TEN); + json.addProperty(recurrenceTypeParamName, 1); + json.addProperty(recurrenceFrequencyParamName, 2); + json.addProperty(recurrenceIntervalParamName, 1); + json.addProperty(recurrenceOnMonthDayParamName, "15 May"); + json.addProperty(monthDayFormatParamName, "dd MMMM"); + + assertValidationSuccess(json); + } + + @Test + void shouldPassWithPeriodicRecurrence() { + final JsonObject json = createLoanRepaymentRequest(); + json.addProperty(recurrenceTypeParamName, 1); + json.addProperty(recurrenceFrequencyParamName, 1); + json.addProperty(recurrenceIntervalParamName, 20); + + assertValidationSuccess(json); + } + + @Test + void shouldPassWithTraditionalData() { + assertValidationSuccess(createLoanRepaymentRequest()); + } + } + } + + @Nested + class WhenUpdatingStandingInstruction { + + @BeforeEach + public void setUpUpdateMode() { + validationMode = ValidationMode.UPDATE; + } + + @Nested + class AccountTransfer { + + @BeforeEach + public void setUp() { + Mockito.lenient().when(existingStandingInstruction.getAccountTransferDetails()).thenReturn(accountTransferDetails); + Mockito.lenient().when(accountTransferDetails.getTransferType()).thenReturn(1); + Mockito.lenient().when(existingStandingInstruction.getRecurrenceFrequency()).thenReturn(null); + Mockito.lenient().when(existingStandingInstruction.getRecurrenceInterval()).thenReturn(null); + } + + @ParameterizedTest + @MethodSource("nullParameters") + void shouldFailWhenParameterExistsButIsNull(String parameter) { + final JsonObject json = commonValuesInUpdateRequest(); + json.add(parameter, JsonNull.INSTANCE); + + assertBlank(json, parameter); + } + + @ParameterizedTest + @MethodSource("parametersWithInvalidValues") + void shouldFailWhenParameterHasInvalidValue(String parameter, Integer invalidValue) { + final JsonObject json = commonValuesInUpdateRequest(); + json.addProperty(parameter, invalidValue); + + assertRange(json, parameter); + } + + @Test + void shouldFailWhenNewInstructionTypeIsDues() { + final JsonObject json = commonValuesInUpdateRequest(); + json.addProperty(instructionTypeParamName, 2); + + assertValidation(json, instructionTypeParamName, + StandingInstructionApiConstants.INSTRUCTION_TYPE_DUES_NOT_ALLOWED_FOR_ACCOUNT_TRANSFER_ERROR_CODE); + } + + @Test + void shouldFailWhenNewValidFromIsAfterExistingValidTill() { + final JsonObject json = commonValuesInUpdateRequest(); + json.addProperty(validFromParamName, "16 May 2026"); + + Mockito.lenient().when(existingStandingInstruction.getValidTill()).thenReturn(LocalDate.of(2026, 5, 15)); + assertValidation(json, validFromParamName, StandingInstructionApiConstants.MUST_BE_BEFORE_EXISTING_VALID_TILL_ERROR_CODE); + } + + @Test + void shouldFailWhenNewValidTillIsBeforeExistingValidFrom() { + final JsonObject json = commonValuesInUpdateRequest(); + json.addProperty(validTillParamName, "16 May 2026"); + + Mockito.lenient().when(existingStandingInstruction.getValidFrom()).thenReturn(LocalDate.of(2026, 5, 17)); + assertValidation(json, validTillParamName, DATE_IS_BEFORE_ERROR_CODE); + } + + @Test + void shouldFailWhenNewValidTillIsBeforeLastRunDate() { + final JsonObject json = commonValuesInUpdateRequest(); + json.addProperty(validTillParamName, "16 May 2026"); + + Mockito.lenient().when(existingStandingInstruction.getLastRunDate()).thenReturn(LocalDate.of(2026, 5, 17)); + assertValidation(json, validTillParamName, StandingInstructionApiConstants.CANNOT_BE_BEFORE_LAST_RUN_DATE_ERROR_CODE); + } + + @Test + void shouldFailWhenNewRecurrenceTypeIsAsPerDues() { + final JsonObject json = commonValuesInUpdateRequest(); + json.addProperty(recurrenceTypeParamName, 2); + + assertValidation(json, recurrenceTypeParamName, + StandingInstructionApiConstants.RECURRENCE_AS_PER_DUES_NOT_ALLOWED_FOR_SAVINGS_ERROR_CODE); + } + + @Test + void shouldFailWhenNewAmountIsNotPositive() { + final JsonObject json = commonValuesInUpdateRequest(); + json.addProperty(amountParamName, BigDecimal.valueOf(-10.00)); + + Mockito.lenient().when(existingStandingInstruction.getInstructionType()).thenReturn(1); + Mockito.lenient().when(existingStandingInstruction.getRecurrenceType()).thenReturn(1); + assertValidation(json, amountParamName, NOT_GREATER_THAN_ZERO_ERROR_CODE); + } + + @Test + void shouldPassWhenUpdatingFromDailyToYearlyRecurrence() { + final JsonObject json = commonValuesInUpdateRequest(); + json.addProperty(recurrenceFrequencyParamName, 3); + json.addProperty(recurrenceIntervalParamName, 1); + json.addProperty(recurrenceOnMonthDayParamName, "25 May"); + json.addProperty(monthDayFormatParamName, "dd MMMM"); + + Mockito.lenient().when(existingStandingInstruction.getRecurrenceFrequency()).thenReturn(0); + Mockito.lenient().when(existingStandingInstruction.getRecurrenceInterval()).thenReturn(1); + + Mockito.lenient().when(existingStandingInstruction.getRecurrenceOnMonth()).thenReturn(null); + Mockito.lenient().when(existingStandingInstruction.getRecurrenceOnDay()).thenReturn(null); + + assertValidationSuccess(json); + } + + private static Stream nullParameters() { + return Stream.of(nameParamName, priorityParamName, instructionTypeParamName, statusParamName, validFromParamName, + validTillParamName, recurrenceTypeParamName); + } + + private static Stream parametersWithInvalidValues() { + return Stream.of(Arguments.of(priorityParamName, 5), Arguments.of(instructionTypeParamName, 3), + Arguments.of(statusParamName, 3), Arguments.of(recurrenceTypeParamName, 3)); + } + } + + class LoanRepayment { + + @BeforeEach + public void setUp() { + Mockito.lenient().when(existingStandingInstruction.getAccountTransferDetails()).thenReturn(accountTransferDetails); + + Mockito.lenient().when(accountTransferDetails.getTransferType()).thenReturn(2); + } + + @Test + void shouldFailWhenNewRecurrenceTypeIsAsPerDues() { + final JsonObject json = commonValuesInUpdateRequest(); + json.addProperty(recurrenceTypeParamName, 2); + + Mockito.lenient().when(existingStandingInstruction.getInstructionType()).thenReturn(1); + assertValidation(json, recurrenceTypeParamName, + StandingInstructionApiConstants.RECURRENCE_AS_PER_DUES_NOT_ALLOWED_WITH_FIXED_INSTRUCTION_ERROR_CODE); + } + + @Test + void shouldFailWhenInstructionTypeIsDuesAndNewAmountIsNotNull() { + final JsonObject json = commonValuesInUpdateRequest(); + json.add(amountParamName, JsonNull.INSTANCE); + + Mockito.lenient().when(existingStandingInstruction.getInstructionType()).thenReturn(2); + assertValidation(json, amountParamName, StandingInstructionApiConstants.AMOUNT_NOT_ALLOWED_FOR_DUES_ERROR_CODE); + } + + @Test + void shouldPassWhenUpdatingFromMonthlyToWeeklyRecurrence() { + final JsonObject json = commonValuesInUpdateRequest(); + json.addProperty(recurrenceFrequencyParamName, 1); + json.addProperty(recurrenceIntervalParamName, 1); + + Mockito.lenient().when(existingStandingInstruction.getRecurrenceFrequency()).thenReturn(2); + Mockito.lenient().when(existingStandingInstruction.getRecurrenceInterval()).thenReturn(1); + + Mockito.lenient().when(existingStandingInstruction.getRecurrenceOnMonth()).thenReturn(5); + Mockito.lenient().when(existingStandingInstruction.getRecurrenceOnDay()).thenReturn(15); + + Mockito.lenient().when(existingStandingInstruction.getAccountTransferDetails()).thenReturn(accountTransferDetails); + Mockito.lenient().when(accountTransferDetails.getTransferType()).thenReturn(2); + + assertValidationSuccess(json); + } + } + } + + private JsonCommand command(JsonObject json) { + final String j = json == null ? "" : json.toString(); + final JsonElement element = fromApiJsonHelper.parse(j); + + return JsonCommand.from(j, element, fromApiJsonHelper, null, null, null, null, null, null, null, null, null, null, null, null, null, + null); + } + + private JsonObject commonValuesInCreateRequest() { + JsonObject json = new JsonObject(); + json.addProperty(localeParamName, "en"); + json.addProperty(dateFormatParamName, "dd MMMM yyyy"); + json.addProperty(fromOfficeIdParamName, 1); + json.addProperty(fromClientIdParamName, 1); + json.addProperty(fromAccountIdParamName, 1); + json.addProperty(fromAccountTypeParamName, 2); + json.addProperty(toOfficeIdParamName, 1); + json.addProperty(toClientIdParamName, 1); + json.addProperty(priorityParamName, 1); + json.addProperty(statusParamName, 1); + json.addProperty(validFromParamName, "16 May 2026"); + json.addProperty(validTillParamName, "16 May 2027"); + + return json; + } + + private JsonObject createAccountTransferRequest() { + JsonObject json = commonValuesInCreateRequest(); + json.addProperty(toAccountIdParamName, 2); + json.addProperty(toAccountTypeParamName, 2); + json.addProperty(transferTypeParamName, 1); + json.addProperty(nameParamName, "BASIC ACCOUNT TRANSFER"); + json.addProperty(instructionTypeParamName, 1); + json.addProperty(recurrenceTypeParamName, 1); + json.addProperty(amountParamName, BigDecimal.TEN); + json.addProperty(recurrenceFrequencyParamName, 2); + json.addProperty(recurrenceIntervalParamName, 1); + json.addProperty(recurrenceOnMonthDayParamName, "15 May"); + json.addProperty(monthDayFormatParamName, "dd MMMM"); + + return json; + } + + private JsonObject createLoanRepaymentRequest() { + JsonObject json = commonValuesInCreateRequest(); + json.addProperty(toAccountIdParamName, 1); + json.addProperty(toAccountTypeParamName, 1); + json.addProperty(transferTypeParamName, 2); + json.addProperty(nameParamName, "BASIC LOAN REPAYMENT"); + json.addProperty(instructionTypeParamName, 2); + json.addProperty(recurrenceTypeParamName, 2); + + return json; + } + + private JsonObject commonValuesInUpdateRequest() { + JsonObject json = new JsonObject(); + json.addProperty(localeParamName, "en"); + json.addProperty(dateFormatParamName, "dd MMMM yyyy"); + + return json; + } + + private void validate(final JsonObject json) { + if (this.validationMode == ValidationMode.CREATE) { + this.standingInstructionDataValidator.validateForCreate(command(json)); + } else { + this.standingInstructionDataValidator.validateForUpdate(command(json), existingStandingInstruction); + } + } + + private void assertThrowsException(Class exceptionClass, JsonObject json) { + assertThrows(exceptionClass, () -> validate(json)); + } + + private void assertValidation(final JsonObject json, final String parameter, final String reason) { + final String expectedCode = String.join(MSG_SEPARATOR, VALIDATION_MSG_PREFIX, + StandingInstructionApiConstants.STANDING_INSTRUCTION_RESOURCE_NAME, parameter, reason); + + PlatformApiDataValidationException ex = assertThrows(PlatformApiDataValidationException.class, () -> validate(json)); + + boolean hasError = ex.getErrors().stream().anyMatch( + error -> parameter.equals(error.getParameterName()) && expectedCode.equals(error.getUserMessageGlobalisationCode())); + + assertTrue(hasError); + } + + private void assertBlank(final JsonObject json, final String parameter) { + assertValidation(json, parameter, CANNOT_BE_BLANK_ERROR_CODE); + } + + private void assertRange(final JsonObject json, final String parameter) { + assertValidation(json, parameter, OUT_OF_RANGE_ERROR_CODE); + } + + private void assertValidationSuccess(JsonObject json) { + assertDoesNotThrow(() -> validate(json)); + } + + public enum ValidationMode { + CREATE, UPDATE + } + + private static Stream validationModes() { + return Stream.of(ValidationMode.CREATE, ValidationMode.UPDATE); + } +}