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 552c2fc59e1..db779445726 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 @@ -117,7 +117,10 @@ public void validateForCreate(final JsonCommand command) { final BigDecimal transferAmount = this.fromApiJsonHelper .extractBigDecimalWithLocaleNamed(StandingInstructionApiConstants.amountParamName, element); - baseDataValidator.reset().parameter(StandingInstructionApiConstants.amountParamName).value(transferAmount).positiveAmount(); + + if (transferAmount != null) { + baseDataValidator.reset().parameter(StandingInstructionApiConstants.amountParamName).value(transferAmount).positiveAmount(); + } final Integer transferType = this.fromApiJsonHelper.extractIntegerNamed(transferTypeParamName, element, Locale.getDefault()); baseDataValidator.reset().parameter(transferTypeParamName).value(transferType).notNull().inMinMaxRange(1, 3); @@ -132,10 +135,18 @@ public void validateForCreate(final JsonCommand command) { baseDataValidator.reset().parameter(StandingInstructionApiConstants.instructionTypeParamName).value(standingInstructionType) .notNull().inMinMaxRange(1, 2); + if (isAmountRequired(standingInstructionType)) { + baseDataValidator.reset().parameter(StandingInstructionApiConstants.amountParamName).value(transferAmount).notNull(); + } + final Integer recurrenceType = this.fromApiJsonHelper.extractIntegerNamed(StandingInstructionApiConstants.recurrenceTypeParamName, element, Locale.getDefault()); - baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceTypeParamName).value(recurrenceType).notNull() - .inMinMaxRange(1, 2); + + if (isRecurrenceRequired(standingInstructionType)) { + baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceTypeParamName).value(recurrenceType).notNull() + .inMinMaxRange(1, 2); + } + boolean isPeriodic = false; if (recurrenceType != null) { isPeriodic = AccountTransferRecurrenceType.fromInt(recurrenceType).isPeriodicRecurrence(); @@ -143,29 +154,37 @@ public void validateForCreate(final JsonCommand command) { final Integer recurrenceFrequency = this.fromApiJsonHelper .extractIntegerNamed(StandingInstructionApiConstants.recurrenceFrequencyParamName, element, Locale.getDefault()); - baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceFrequencyParamName).value(recurrenceFrequency) - .inMinMaxRange(0, 3); + + if (isRecurrenceRequired(standingInstructionType)) { + baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceFrequencyParamName).value(recurrenceFrequency) + .inMinMaxRange(1, 3); + } if (recurrenceFrequency != null) { PeriodFrequencyType frequencyType = PeriodFrequencyType.fromInt(recurrenceFrequency); - if (frequencyType.isMonthly() || frequencyType.isYearly()) { + if (frequencyType != null && (frequencyType.isMonthly() || frequencyType.isYearly())) { final MonthDay monthDay = this.fromApiJsonHelper .extractMonthDayNamed(StandingInstructionApiConstants.recurrenceOnMonthDayParamName, element); - baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceOnMonthDayParamName).value(monthDay) - .notNull(); + + if (isRecurrenceRequired(standingInstructionType)) { + baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceOnMonthDayParamName).value(monthDay) + .notNull(); + } } } 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(); + if (isRecurrenceRequired(standingInstructionType)) { + baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceIntervalParamName).value(recurrenceInterval) + .notNull(); + baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceFrequencyParamName).value(recurrenceFrequency) + .notNull(); + baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceIntervalParamName).value(recurrenceInterval) + .integerGreaterThanZero(); + } } - baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceIntervalParamName).value(recurrenceInterval) - .integerGreaterThanZero(); final String name = this.fromApiJsonHelper.extractStringNamed(StandingInstructionApiConstants.nameParamName, element); baseDataValidator.reset().parameter(StandingInstructionApiConstants.nameParamName).value(name).notNull(); @@ -178,7 +197,7 @@ public void validateForCreate(final JsonCommand command) { .inMinMaxRange(1, 1); } - if (standingInstructionType != null && StandingInstructionType.fromInt(standingInstructionType).isFixedAmoutTransfer()) { + if (isAmountRequired(standingInstructionType)) { baseDataValidator.reset().parameter(StandingInstructionApiConstants.amountParamName).value(transferAmount).notNull(); } @@ -230,10 +249,22 @@ public void validateForUpdate(final JsonCommand command) { baseDataValidator.reset().parameter(StandingInstructionApiConstants.validTillParamName).value(validTill).notNull(); } + Integer standingInstructionType = null; + if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.instructionTypeParamName, element)) { + standingInstructionType = this.fromApiJsonHelper.extractIntegerNamed(StandingInstructionApiConstants.instructionTypeParamName, + element, Locale.getDefault()); + baseDataValidator.reset().parameter(StandingInstructionApiConstants.instructionTypeParamName).value(standingInstructionType) + .notNull().inMinMaxRange(1, 2); + } + if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.amountParamName, element)) { final BigDecimal transferAmount = this.fromApiJsonHelper .extractBigDecimalWithLocaleNamed(StandingInstructionApiConstants.amountParamName, element); baseDataValidator.reset().parameter(StandingInstructionApiConstants.amountParamName).value(transferAmount).positiveAmount(); + + if (isAmountRequired(standingInstructionType)) { + baseDataValidator.reset().parameter(StandingInstructionApiConstants.amountParamName).value(transferAmount).notNull(); + } } if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.statusParamName, element)) { @@ -250,13 +281,6 @@ public void validateForUpdate(final JsonCommand command) { .inMinMaxRange(1, 4); } - 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); - } - if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.recurrenceTypeParamName, element)) { final Integer recurrenceType = this.fromApiJsonHelper .extractIntegerNamed(StandingInstructionApiConstants.recurrenceTypeParamName, element, Locale.getDefault()); @@ -274,8 +298,11 @@ public void validateForUpdate(final JsonCommand command) { 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(); + + if (isRecurrenceRequired(standingInstructionType)) { + baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceIntervalParamName).value(recurrenceInterval) + .integerGreaterThanZero(); + } } if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.nameParamName, element)) { @@ -291,4 +318,40 @@ private void throwExceptionIfValidationWarningsExist(final List validator.validateForCreate(command)); + } + + @Test + void nullAmount_shouldFail() { + setupInstructionType(1); + setupAmount(null); + + PlatformApiDataValidationException ex = assertThrows(PlatformApiDataValidationException.class, + () -> validator.validateForCreate(command)); + assertHasError(ex, StandingInstructionApiConstants.amountParamName); + } + + @Test + void missingRecurrence_shouldFail() { + setupInstructionType(1); + setupAmount(BigDecimal.valueOf(1000)); + setupRecurrenceMissing(); + + PlatformApiDataValidationException ex = assertThrows(PlatformApiDataValidationException.class, + () -> validator.validateForCreate(command)); + assertHasError(ex, StandingInstructionApiConstants.recurrenceIntervalParamName); + } + + @ParameterizedTest + @ValueSource(ints = { 0, -1, 99 }) + void fixedInvalidRecurrenceType_shouldFail(int invalidType) { + setupInstructionType(1); + setupAmount(BigDecimal.valueOf(1000)); + + when(fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.recurrenceTypeParamName, element)).thenReturn(true); + + when(fromApiJsonHelper.extractIntegerNamed(eq(StandingInstructionApiConstants.recurrenceTypeParamName), eq(element), + any(Locale.class))).thenReturn(invalidType); + + assertThrows(PlatformApiDataValidationException.class, () -> validator.validateForCreate(command)); + } + + @Test + void negativeAmount_shouldFail() { + setupInstructionType(1); + setupAmount(BigDecimal.valueOf(-100)); + + PlatformApiDataValidationException ex = assertThrows(PlatformApiDataValidationException.class, + () -> validator.validateForCreate(command)); + assertHasError(ex, StandingInstructionApiConstants.amountParamName); + } + + @Test + void zeroAmount_shouldFail() { + setupInstructionType(1); + setupAmount(BigDecimal.ZERO); + + PlatformApiDataValidationException ex = assertThrows(PlatformApiDataValidationException.class, + () -> validator.validateForCreate(command)); + assertHasError(ex, StandingInstructionApiConstants.amountParamName); + } + + @Test + void zeroInterval_shouldFail() { + setupInstructionType(1); + setupAmount(BigDecimal.valueOf(1000)); + setupRecurrenceInterval(0); + + PlatformApiDataValidationException ex = assertThrows(PlatformApiDataValidationException.class, + () -> validator.validateForCreate(command)); + assertHasError(ex, StandingInstructionApiConstants.recurrenceIntervalParamName); + } + } + + @Nested + class DuesAmountTransferTypeTests { + + @Test + void allNull_shouldPass() { + setupInstructionType(2); + setupAmount(null); + setupRecurrenceMissing(); + + assertDoesNotThrow(() -> validator.validateForCreate(command)); + } + + @Test + void amountOnly_shouldPass() { + setupInstructionType(2); + setupAmount(BigDecimal.valueOf(500)); + setupRecurrenceMissing(); + + assertDoesNotThrow(() -> validator.validateForCreate(command)); + } + + @Test + void recurrenceOnly_shouldPass() { + setupInstructionType(2); + setupAmount(null); + setupRecurrence(1, 30, 1, "10-15"); + + assertDoesNotThrow(() -> validator.validateForCreate(command)); + } + + @Test + void allFieldsProvided_shouldPass() { + setupInstructionType(2); + setupAmount(BigDecimal.valueOf(1000)); + setupRecurrence(1, 30, 1, "10-15"); + + assertDoesNotThrow(() -> validator.validateForCreate(command)); + } + + @Test + void negativeAmount_shouldFail() { + setupInstructionType(2); + setupAmount(BigDecimal.valueOf(-100)); + + PlatformApiDataValidationException ex = assertThrows(PlatformApiDataValidationException.class, + () -> validator.validateForCreate(command)); + assertHasError(ex, StandingInstructionApiConstants.amountParamName); + } + + @Test + void zeroInterval_shouldFail() { + setupInstructionType(2); + setupAmount(BigDecimal.valueOf(1000)); + setupRecurrenceInterval(0); + + PlatformApiDataValidationException ex = assertThrows(PlatformApiDataValidationException.class, + () -> validator.validateForCreate(command)); + assertHasError(ex, StandingInstructionApiConstants.recurrenceIntervalParamName); + } + + @Test + void missingAmountParam_shouldPass() { + setupInstructionType(2); + when(fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.amountParamName, element)).thenReturn(false); + + assertDoesNotThrow(() -> validator.validateForCreate(command)); + } + + @Test + void explicitNullAmount_shouldPass() { + setupInstructionType(2); + setupAmount(null); + + assertDoesNotThrow(() -> validator.validateForCreate(command)); + } + } + + @Nested + class InstructionTypeEdgeCases { + + @Test + void nullType_shouldFail() { + when(fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.instructionTypeParamName, element)) + .thenReturn(true); + when(fromApiJsonHelper.extractIntegerNamed(eq(StandingInstructionApiConstants.instructionTypeParamName), + eq(element), any(Locale.class))).thenReturn(null); + + assertThrows(PlatformApiDataValidationException.class, + () -> validator.validateForCreate(command)); + } + + @ParameterizedTest + @ValueSource(ints = { 0, 3, 999, -1 }) + void invalidTypeValues_shouldFail(int invalidType) { + setupInstructionType(invalidType); + assertThrows(PlatformApiDataValidationException.class, () -> validator.validateForCreate(command)); + } + + @Test + void missingTypeParam_shouldFail() { + when(fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.instructionTypeParamName, element)) + .thenReturn(false); + + PlatformApiDataValidationException ex = assertThrows( + PlatformApiDataValidationException.class, + () -> validator.validateForCreate(command)); + assertHasError(ex, StandingInstructionApiConstants.instructionTypeParamName); + } + } + + @Nested + class PartialDataTests { + + @Test + void fixedPartialRecurrence_shouldFail() { + setupInstructionType(1); + setupAmount(BigDecimal.valueOf(1000)); + when(fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.recurrenceIntervalParamName, element)).thenReturn(true); + when(fromApiJsonHelper.extractIntegerNamed(eq(StandingInstructionApiConstants.recurrenceIntervalParamName), eq(element), + any(Locale.class))).thenReturn(30); + + PlatformApiDataValidationException ex = assertThrows(PlatformApiDataValidationException.class, + () -> validator.validateForCreate(command)); + assertTrue(ex.getErrors().size() >= 1); + } + + @Test + void duesPartialInput_shouldPass() { + setupInstructionType(2); + setupAmount(BigDecimal.valueOf(1000)); + setupRecurrenceInterval(30); + + assertDoesNotThrow(() -> validator.validateForCreate(command)); + } + + @Test + void fixedOnlyRecurrence_shouldFail() { + setupInstructionType(1); + setupAmount(null); + setupRecurrence(1, 30, 1, "10-15"); + + PlatformApiDataValidationException ex = assertThrows(PlatformApiDataValidationException.class, + () -> validator.validateForCreate(command)); + assertHasError(ex, StandingInstructionApiConstants.amountParamName); + } + + @Test + void emptyJson_shouldFail() { + when(fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.instructionTypeParamName, element)) + .thenReturn(false); + + assertThrows(PlatformApiDataValidationException.class, + () -> validator.validateForCreate(command)); + } + } + + @Nested + class ErrorHandlingTests { + + @Test + void fixedMultipleErrors_shouldAccumulate() { + setupInstructionType(1); + setupAmount(null); + setupRecurrenceMissing(); + + PlatformApiDataValidationException ex = assertThrows(PlatformApiDataValidationException.class, + () -> validator.validateForCreate(command)); + Set errors = ex.getErrors(); + assertTrue(errors.size() > 1); + } + + @Test + void duesValidAmountInvalidRecurrence_shouldFail() { + setupInstructionType(2); + setupAmount(BigDecimal.valueOf(1000)); + setupRecurrenceInterval(0); + + PlatformApiDataValidationException ex = assertThrows(PlatformApiDataValidationException.class, + () -> validator.validateForCreate(command)); + assertHasError(ex, StandingInstructionApiConstants.recurrenceIntervalParamName); + } + } + + @Nested + class UpdateValidationTests { + + @Test + void fixedNullAmount_shouldFail() { + setupInstructionType(1); + setupAmount(null); + + PlatformApiDataValidationException ex = assertThrows(PlatformApiDataValidationException.class, + () -> validator.validateForUpdate(command)); + assertHasError(ex, StandingInstructionApiConstants.amountParamName); + } + + @Test + void duesNullAmount_shouldPass() { + setupInstructionType(2); + setupAmount(null); + + assertDoesNotThrow(() -> validator.validateForUpdate(command)); + } + + @Test + void duesPartialUpdate_shouldPass() { + setupInstructionType(2); + setupAmount(BigDecimal.valueOf(500)); + + assertDoesNotThrow(() -> validator.validateForUpdate(command)); + } + } + + private void setupInstructionType(int type) { + when(fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.instructionTypeParamName, element)) + .thenReturn(true); + when(fromApiJsonHelper.extractIntegerNamed(eq(StandingInstructionApiConstants.instructionTypeParamName), + eq(element), any(Locale.class))).thenReturn(type); + } + + private void setupAmount(BigDecimal amount) { + when(fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.amountParamName, element)) + .thenReturn(true); + when(fromApiJsonHelper.extractBigDecimalWithLocaleNamed(eq(StandingInstructionApiConstants.amountParamName), + eq(element), any(Locale.class))).thenReturn(amount); + } + + private void setupRecurrence(int recurrenceType, int interval, int frequency, String monthDay) { + when(fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.recurrenceTypeParamName, element)) + .thenReturn(true); + when(fromApiJsonHelper.extractIntegerNamed(eq(StandingInstructionApiConstants.recurrenceTypeParamName), + eq(element), any(Locale.class))).thenReturn(recurrenceType); + + when(fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.recurrenceIntervalParamName, element)) + .thenReturn(true); + when(fromApiJsonHelper.extractIntegerNamed(eq(StandingInstructionApiConstants.recurrenceIntervalParamName), + eq(element), any(Locale.class))).thenReturn(interval); + + when(fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.recurrenceFrequencyParamName, element)) + .thenReturn(true); + when(fromApiJsonHelper.extractIntegerNamed(eq(StandingInstructionApiConstants.recurrenceFrequencyParamName), + eq(element), any(Locale.class))).thenReturn(frequency); + + when(fromApiJsonHelper.extractMonthDayNamed(eq(StandingInstructionApiConstants.recurrenceOnMonthDayParamName), + eq(element))).thenReturn(monthDay != null ? MonthDay.fromString(monthDay) : null); + } + + private void setupRecurrenceInterval(int interval) { + when(fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.recurrenceIntervalParamName, element)) + .thenReturn(true); + when(fromApiJsonHelper.extractIntegerNamed(eq(StandingInstructionApiConstants.recurrenceIntervalParamName), + eq(element), any(Locale.class))).thenReturn(interval); + } + + private void setupRecurrenceMissing() { + when(fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.recurrenceTypeParamName, element)).thenReturn(false); + when(fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.recurrenceIntervalParamName, element)).thenReturn(false); + when(fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.recurrenceFrequencyParamName, element)).thenReturn(false); + } + + private void assertHasError(PlatformApiDataValidationException ex, String paramName) { + boolean found = ex.getErrors().stream().anyMatch(e -> e.getParameter().equals(paramName)); + assertTrue(found, "Expected validation error for parameter: " + paramName); + } +}