You are viewing a plain text version of this content. The canonical link for it is here.
Posted to issues@fineract.apache.org by "KRYSTALM7 (via GitHub)" <gi...@apache.org> on 2026/06/30 16:23:30 UTC
[PR] FINERACT-2442: Add unit tests for credit scoring factors and weights properties [fineract-loan-origination]
KRYSTALM7 opened a new pull request, #35:
URL: https://github.com/apache/fineract-loan-origination/pull/35
## Description
Adds comprehensive unit test coverage for the credit scoring factors and weights configuration introduced in #29.
### Tests Added
- **ScoringWeightsPropertiesTest** — startup validation logic, default weight values, custom weight combinations summing to 100, default threshold values.
- **IncomeLoanRatioFactorTest** — ratio tiers, zero/negative/null denominator guards (no ArithmeticException), custom threshold overrides, very small non-zero income fractions, points never exceed maxPoints.
- **DebtBurdenFactorTest** — ratio tiers, zero/negative/null income guards, null obligations treated as best case, rounding precision with small configured maxPoints, custom threshold overrides.
- **EmploymentStabilityFactorTest** — status/duration tier combinations, null/blank status and duration handling, case-insensitive status matching.
- **RepaymentHistoryFactorTest** — first-time borrower midpoint scoring, ratio tiers, negative count handling, rounding precision with small maxPoints.
- **LoanPurposeRiskFactorTest** — all purpose risk categories, null/blank/unrecognised purpose handling, case-insensitive matching.
### Coverage Focus
Tests specifically target the edge cases raised in review on #29 — division-by-zero guards, BigDecimal rounding precision, and configurable threshold behavior — to verify those fixes hold under a range of inputs, not just the happy path.
All tests are pure unit tests with no Spring context and no database; each factor is instantiated directly with a `ScoringWeightsProperties` instance, so the full suite runs in milliseconds.
---
Please make sure these boxes are checked before submitting your pull request - thanks!
- [x] Write the commit message as per [our guidelines](https://github.com/apache/fineract/blob/develop/CONTRIBUTING.md#pull-requests)
- [x] Acknowledge that we will not review PRs that are not passing the build ("green") - it is your responsibility to get a proposed PR to pass the build, not primarily the project's maintainers.
- [x] Create/update [unit or integration tests](https://fineract.apache.org/docs/current/#_testing) for verifying the changes made.
- [x] Follow our [coding conventions](https://cwiki.apache.org/confluence/display/FINERACT/Coding+Conventions).
- [ ] Add required Swagger annotation and update API documentation at fineract-provider/src/main/resources/static/legacy-docs/apiLive.htm with details of any API changes (Not Applicable)
- [x] [This PR must not be a "code dump"](https://cwiki.apache.org/confluence/display/FINERACT/Pull+Request+Size+Limit). Large changes can be made in a branch, with assistance.
Ask for help on the [developer mailing list](https://fineract.apache.org/#contribute).
Your assigned reviewer(s) will follow our [guidelines for code reviews](https://cwiki.apache.org/confluence/display/FINERACT/Code+Review+Guide).
### Related
- JIRA: https://issues.apache.org/jira/browse/FINERACT-2442
- Tests cover factors merged in #29
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: issues-unsubscribe@fineract.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] FINERACT-2442: Add unit tests for credit scoring factors and weights properties [fineract-loan-origination]
Posted by "KRYSTALM7 (via GitHub)" <gi...@apache.org>.
KRYSTALM7 commented on code in PR #35:
URL: https://github.com/apache/fineract-loan-origination/pull/35#discussion_r3506839190
##########
src/test/java/org/apache/fineract/los/scoring/factors/LoanPurposeRiskFactorTest.java:
##########
@@ -0,0 +1,123 @@
+/*
+ * 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.los.scoring.factors;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.apache.fineract.los.scoring.ScoringWeightsProperties;
+import org.apache.fineract.los.scoring.model.ApplicantScoringProfile;
+import org.apache.fineract.los.scoring.model.FactorScore;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.junit.jupiter.params.provider.NullAndEmptySource;
+
+/** Unit tests for {@link LoanPurposeRiskFactor}. */
+@DisplayName("LoanPurposeRiskFactor")
+class LoanPurposeRiskFactorTest {
+
+ private LoanPurposeRiskFactor factor;
+
+ @BeforeEach
+ void setUp() {
+ factor = new LoanPurposeRiskFactor(new ScoringWeightsProperties());
+ }
+
+ @Test
+ @DisplayName("maxPoints returns configured loan-purpose-risk weight")
+ void maxPointsReturnsConfiguredWeight() {
+ assertThat(factor.maxPoints()).isEqualTo(10);
+ }
+
+ @Test
+ @DisplayName("factorName returns stable identifier")
+ void factorNameIsStable() {
+ assertThat(factor.factorName()).isEqualTo("loan-purpose-risk");
+ }
+
+ @ParameterizedTest(name = "purpose={0} expectedPoints={1}")
+ @DisplayName("Each purpose category produces expected points")
+ @CsvSource({
+ "AGRICULTURE, 10",
+ "EDUCATION, 10",
+ "BUSINESS, 8",
+ "HOME_IMPROVEMENT, 8",
+ "CONSUMER, 5",
+ "MEDICAL, 5",
+ "SPECULATION, 3",
+ "OTHER, 3",
+ })
+ void purposeCategories(final String purpose, final int expectedPoints) {
+ final ApplicantScoringProfile profile =
+ ApplicantScoringProfile.builder().loanPurpose(purpose).build();
+
+ final FactorScore result = factor.score(profile);
+
+ assertThat(result.getPoints()).isEqualTo(expectedPoints);
+ }
+
+ @ParameterizedTest
+ @NullAndEmptySource
+ @DisplayName("Null or blank purpose scores neutral default")
+ void nullOrBlankPurposeScoresNeutral(final String purpose) {
+ final ApplicantScoringProfile profile =
+ ApplicantScoringProfile.builder().loanPurpose(purpose).build();
+
+ final FactorScore result = factor.score(profile);
+
+ assertThat(result.getPoints()).isEqualTo(5); // 50% of 10
+ assertThat(result.getExplanation()).contains("UNSPECIFIED");
+ }
+
+ @Test
+ @DisplayName("Unrecognised purpose scores neutral default")
+ void unrecognisedPurposeScoresNeutral() {
+ final ApplicantScoringProfile profile =
+ ApplicantScoringProfile.builder().loanPurpose("VACATION").build();
+
+ final FactorScore result = factor.score(profile);
+
+ assertThat(result.getPoints()).isEqualTo(5);
+ }
+
+ @Test
+ @DisplayName("Purpose comparison is case-insensitive")
+ void purposeComparisonIsCaseInsensitive() {
+ final ApplicantScoringProfile profile =
+ ApplicantScoringProfile.builder().loanPurpose("agriculture").build();
Review Comment:
We can add these AGRICULTURE, EDUCATION, BUSINESS, REAL ESTATE, GOODS & SERVICES MEDICAL, INVESTMENT, OTHER. if you can think of anything else pleaase let me know
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: issues-unsubscribe@fineract.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] FINERACT-2442: Add unit tests for credit scoring factors and weights properties [fineract-loan-origination]
Posted by "Aman-Mittal (via GitHub)" <gi...@apache.org>.
Aman-Mittal commented on code in PR #35:
URL: https://github.com/apache/fineract-loan-origination/pull/35#discussion_r3510513822
##########
src/test/java/org/apache/fineract/los/scoring/factors/LoanPurposeRiskFactorTest.java:
##########
@@ -0,0 +1,123 @@
+/*
+ * 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.los.scoring.factors;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.apache.fineract.los.scoring.ScoringWeightsProperties;
+import org.apache.fineract.los.scoring.model.ApplicantScoringProfile;
+import org.apache.fineract.los.scoring.model.FactorScore;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.junit.jupiter.params.provider.NullAndEmptySource;
+
+/** Unit tests for {@link LoanPurposeRiskFactor}. */
+@DisplayName("LoanPurposeRiskFactor")
+class LoanPurposeRiskFactorTest {
+
+ private LoanPurposeRiskFactor factor;
+
+ @BeforeEach
+ void setUp() {
+ factor = new LoanPurposeRiskFactor(new ScoringWeightsProperties());
+ }
+
+ @Test
+ @DisplayName("maxPoints returns configured loan-purpose-risk weight")
+ void maxPointsReturnsConfiguredWeight() {
+ assertThat(factor.maxPoints()).isEqualTo(10);
+ }
+
+ @Test
+ @DisplayName("factorName returns stable identifier")
+ void factorNameIsStable() {
+ assertThat(factor.factorName()).isEqualTo("loan-purpose-risk");
+ }
+
+ @ParameterizedTest(name = "purpose={0} expectedPoints={1}")
+ @DisplayName("Each purpose category produces expected points")
+ @CsvSource({
+ "AGRICULTURE, 10",
+ "EDUCATION, 10",
+ "BUSINESS, 8",
+ "HOME_IMPROVEMENT, 8",
+ "CONSUMER, 5",
+ "MEDICAL, 5",
+ "SPECULATION, 3",
+ "OTHER, 3",
+ })
+ void purposeCategories(final String purpose, final int expectedPoints) {
+ final ApplicantScoringProfile profile =
+ ApplicantScoringProfile.builder().loanPurpose(purpose).build();
+
+ final FactorScore result = factor.score(profile);
+
+ assertThat(result.getPoints()).isEqualTo(expectedPoints);
+ }
+
+ @ParameterizedTest
+ @NullAndEmptySource
+ @DisplayName("Null or blank purpose scores neutral default")
+ void nullOrBlankPurposeScoresNeutral(final String purpose) {
+ final ApplicantScoringProfile profile =
+ ApplicantScoringProfile.builder().loanPurpose(purpose).build();
+
+ final FactorScore result = factor.score(profile);
+
+ assertThat(result.getPoints()).isEqualTo(5); // 50% of 10
+ assertThat(result.getExplanation()).contains("UNSPECIFIED");
+ }
+
+ @Test
+ @DisplayName("Unrecognised purpose scores neutral default")
+ void unrecognisedPurposeScoresNeutral() {
+ final ApplicantScoringProfile profile =
+ ApplicantScoringProfile.builder().loanPurpose("VACATION").build();
+
+ final FactorScore result = factor.score(profile);
+
+ assertThat(result.getPoints()).isEqualTo(5);
+ }
+
+ @Test
+ @DisplayName("Purpose comparison is case-insensitive")
+ void purposeComparisonIsCaseInsensitive() {
+ final ApplicantScoringProfile profile =
+ ApplicantScoringProfile.builder().loanPurpose("agriculture").build();
Review Comment:
Add all that are being used for now.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: issues-unsubscribe@fineract.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] FINERACT-2442: Add unit tests for credit scoring factors and weights properties [fineract-loan-origination]
Posted by "KRYSTALM7 (via GitHub)" <gi...@apache.org>.
KRYSTALM7 commented on code in PR #35:
URL: https://github.com/apache/fineract-loan-origination/pull/35#discussion_r3506815028
##########
src/test/java/org/apache/fineract/los/scoring/factors/EmploymentStabilityFactorTest.java:
##########
@@ -0,0 +1,177 @@
+/*
+ * 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.los.scoring.factors;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.apache.fineract.los.scoring.ScoringWeightsProperties;
+import org.apache.fineract.los.scoring.model.ApplicantScoringProfile;
+import org.apache.fineract.los.scoring.model.FactorScore;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.junit.jupiter.params.provider.NullAndEmptySource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+/** Unit tests for {@link EmploymentStabilityFactor}. */
+@DisplayName("EmploymentStabilityFactor")
+class EmploymentStabilityFactorTest {
+
+ private EmploymentStabilityFactor factor;
+
+ @BeforeEach
+ void setUp() {
+ factor = new EmploymentStabilityFactor(new ScoringWeightsProperties());
+ }
+
+ @Test
+ @DisplayName("maxPoints returns configured employment-stability weight")
+ void maxPointsReturnsConfiguredWeight() {
+ assertThat(factor.maxPoints()).isEqualTo(20);
+ }
+
+ @Test
+ @DisplayName("factorName returns stable identifier")
+ void factorNameIsStable() {
+ assertThat(factor.factorName()).isEqualTo("employment-stability");
+ }
+
+ @Test
+ @DisplayName("EMPLOYED with long tenure scores maximum points")
+ void employedLongTenureScoresMaximum() {
+ final ApplicantScoringProfile profile =
+ ApplicantScoringProfile.builder()
+ .employmentStatus("EMPLOYED")
+ .employmentDurationMonths(48)
+ .build();
+
+ final FactorScore result = factor.score(profile);
+
+ assertThat(result.getPoints()).isEqualTo(20);
+ }
+
+ @Test
+ @DisplayName("UNEMPLOYED scores zero for status portion but duration still counts")
+ void unemployedStatusPortionScoresZero() {
+ final ApplicantScoringProfile profile =
+ ApplicantScoringProfile.builder()
+ .employmentStatus("UNEMPLOYED")
+ .employmentDurationMonths(60)
+ .build();
+
+ final FactorScore result = factor.score(profile);
+
+ // Status contributes 0 (UNEMPLOYED), duration still
+ // contributes 40% = 8 points regardless of status —
+ // this is the factor's intended design, not a bug.
+ assertThat(result.getPoints()).isEqualTo(8);
+ }
+
+ @ParameterizedTest
+ @NullAndEmptySource
+ @DisplayName("Null or blank employment status scores zero for status portion")
+ void nullOrBlankStatusScoresZeroStatusPortion(final String status) {
+ final ApplicantScoringProfile profile =
+ ApplicantScoringProfile.builder()
+ .employmentStatus(status)
+ .employmentDurationMonths(48)
+ .build();
+
+ final FactorScore result = factor.score(profile);
+
+ // Status contributes 0, duration still contributes 40%
+ assertThat(result.getPoints()).isEqualTo(8);
+ }
+
+ @ParameterizedTest
+ @ValueSource(ints = {-5, 0})
+ @DisplayName("Null, zero, or negative duration scores zero for duration portion")
+ void nonPositiveDurationScoresZeroDurationPortion(final int months) {
+ final ApplicantScoringProfile profile =
+ ApplicantScoringProfile.builder()
+ .employmentStatus("EMPLOYED")
+ .employmentDurationMonths(months)
+ .build();
+
+ final FactorScore result = factor.score(profile);
+
+ // Status contributes 60%, duration contributes 0
+ assertThat(result.getPoints()).isEqualTo(12);
+ }
+
+ @Test
+ @DisplayName("Null duration scores zero for duration portion")
+ void nullDurationScoresZeroDurationPortion() {
+ final ApplicantScoringProfile profile =
+ ApplicantScoringProfile.builder()
+ .employmentStatus("EMPLOYED")
+ .employmentDurationMonths(null)
+ .build();
+
+ final FactorScore result = factor.score(profile);
+
+ assertThat(result.getPoints()).isEqualTo(12);
+ }
+
+ @ParameterizedTest(name = "status={0} expectedPoints={1}")
+ @DisplayName("Each employment status tier produces expected points")
+ @CsvSource({"EMPLOYED,20", "SELF_EMPLOYED,19", "INFORMAL,14", "UNKNOWN_VALUE,14"})
+ void statusTiers(final String status, final int expectedPoints) {
+ final ApplicantScoringProfile profile =
+ ApplicantScoringProfile.builder()
+ .employmentStatus(status)
+ .employmentDurationMonths(48)
+ .build();
+
+ final FactorScore result = factor.score(profile);
+
+ assertThat(result.getPoints()).isEqualTo(expectedPoints);
+ }
+
+ @Test
+ @DisplayName("Status comparison is case-insensitive")
+ void statusComparisonIsCaseInsensitive() {
+ final ApplicantScoringProfile profile =
+ ApplicantScoringProfile.builder()
+ .employmentStatus("employed")
+ .employmentDurationMonths(48)
+ .build();
+
+ final FactorScore result = factor.score(profile);
+
+ assertThat(result.getPoints()).isEqualTo(20);
+ }
+
+ @Test
+ @DisplayName("Points never exceed configured maxPoints")
+ void pointsNeverExceedMaxPoints() {
+ final ApplicantScoringProfile profile =
+ ApplicantScoringProfile.builder()
+ .employmentStatus("EMPLOYED")
Review Comment:
Hi @Aman-Mittal are these three enough or you want me to add anything else :
EMPLOYED, SELF_EMPLOYED, UNEMPLOYED
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: issues-unsubscribe@fineract.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] FINERACT-2442: Add unit tests for credit scoring factors and weights properties [fineract-loan-origination]
Posted by "Aman-Mittal (via GitHub)" <gi...@apache.org>.
Aman-Mittal commented on code in PR #35:
URL: https://github.com/apache/fineract-loan-origination/pull/35#discussion_r3504397287
##########
src/test/java/org/apache/fineract/los/scoring/factors/EmploymentStabilityFactorTest.java:
##########
@@ -0,0 +1,177 @@
+/*
+ * 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.los.scoring.factors;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.apache.fineract.los.scoring.ScoringWeightsProperties;
+import org.apache.fineract.los.scoring.model.ApplicantScoringProfile;
+import org.apache.fineract.los.scoring.model.FactorScore;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.junit.jupiter.params.provider.NullAndEmptySource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+/** Unit tests for {@link EmploymentStabilityFactor}. */
+@DisplayName("EmploymentStabilityFactor")
+class EmploymentStabilityFactorTest {
+
+ private EmploymentStabilityFactor factor;
+
+ @BeforeEach
+ void setUp() {
+ factor = new EmploymentStabilityFactor(new ScoringWeightsProperties());
+ }
+
+ @Test
+ @DisplayName("maxPoints returns configured employment-stability weight")
+ void maxPointsReturnsConfiguredWeight() {
+ assertThat(factor.maxPoints()).isEqualTo(20);
+ }
+
+ @Test
+ @DisplayName("factorName returns stable identifier")
+ void factorNameIsStable() {
+ assertThat(factor.factorName()).isEqualTo("employment-stability");
+ }
+
+ @Test
+ @DisplayName("EMPLOYED with long tenure scores maximum points")
+ void employedLongTenureScoresMaximum() {
+ final ApplicantScoringProfile profile =
+ ApplicantScoringProfile.builder()
+ .employmentStatus("EMPLOYED")
+ .employmentDurationMonths(48)
+ .build();
+
+ final FactorScore result = factor.score(profile);
+
+ assertThat(result.getPoints()).isEqualTo(20);
+ }
+
+ @Test
+ @DisplayName("UNEMPLOYED scores zero for status portion but duration still counts")
+ void unemployedStatusPortionScoresZero() {
+ final ApplicantScoringProfile profile =
+ ApplicantScoringProfile.builder()
+ .employmentStatus("UNEMPLOYED")
+ .employmentDurationMonths(60)
+ .build();
+
+ final FactorScore result = factor.score(profile);
+
+ // Status contributes 0 (UNEMPLOYED), duration still
+ // contributes 40% = 8 points regardless of status —
+ // this is the factor's intended design, not a bug.
+ assertThat(result.getPoints()).isEqualTo(8);
+ }
+
+ @ParameterizedTest
+ @NullAndEmptySource
+ @DisplayName("Null or blank employment status scores zero for status portion")
+ void nullOrBlankStatusScoresZeroStatusPortion(final String status) {
+ final ApplicantScoringProfile profile =
+ ApplicantScoringProfile.builder()
+ .employmentStatus(status)
+ .employmentDurationMonths(48)
+ .build();
+
+ final FactorScore result = factor.score(profile);
+
+ // Status contributes 0, duration still contributes 40%
+ assertThat(result.getPoints()).isEqualTo(8);
+ }
+
+ @ParameterizedTest
+ @ValueSource(ints = {-5, 0})
+ @DisplayName("Null, zero, or negative duration scores zero for duration portion")
+ void nonPositiveDurationScoresZeroDurationPortion(final int months) {
+ final ApplicantScoringProfile profile =
+ ApplicantScoringProfile.builder()
+ .employmentStatus("EMPLOYED")
+ .employmentDurationMonths(months)
+ .build();
+
+ final FactorScore result = factor.score(profile);
+
+ // Status contributes 60%, duration contributes 0
+ assertThat(result.getPoints()).isEqualTo(12);
+ }
+
+ @Test
+ @DisplayName("Null duration scores zero for duration portion")
+ void nullDurationScoresZeroDurationPortion() {
+ final ApplicantScoringProfile profile =
+ ApplicantScoringProfile.builder()
+ .employmentStatus("EMPLOYED")
+ .employmentDurationMonths(null)
+ .build();
+
+ final FactorScore result = factor.score(profile);
+
+ assertThat(result.getPoints()).isEqualTo(12);
+ }
+
+ @ParameterizedTest(name = "status={0} expectedPoints={1}")
+ @DisplayName("Each employment status tier produces expected points")
+ @CsvSource({"EMPLOYED,20", "SELF_EMPLOYED,19", "INFORMAL,14", "UNKNOWN_VALUE,14"})
+ void statusTiers(final String status, final int expectedPoints) {
+ final ApplicantScoringProfile profile =
+ ApplicantScoringProfile.builder()
+ .employmentStatus(status)
+ .employmentDurationMonths(48)
+ .build();
+
+ final FactorScore result = factor.score(profile);
+
+ assertThat(result.getPoints()).isEqualTo(expectedPoints);
+ }
+
+ @Test
+ @DisplayName("Status comparison is case-insensitive")
+ void statusComparisonIsCaseInsensitive() {
+ final ApplicantScoringProfile profile =
+ ApplicantScoringProfile.builder()
+ .employmentStatus("employed")
+ .employmentDurationMonths(48)
+ .build();
+
+ final FactorScore result = factor.score(profile);
+
+ assertThat(result.getPoints()).isEqualTo(20);
+ }
+
+ @Test
+ @DisplayName("Points never exceed configured maxPoints")
+ void pointsNeverExceedMaxPoints() {
+ final ApplicantScoringProfile profile =
+ ApplicantScoringProfile.builder()
+ .employmentStatus("EMPLOYED")
Review Comment:
Maintain Enums for EMPLOYED
##########
src/test/java/org/apache/fineract/los/scoring/factors/LoanPurposeRiskFactorTest.java:
##########
@@ -0,0 +1,123 @@
+/*
+ * 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.los.scoring.factors;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.apache.fineract.los.scoring.ScoringWeightsProperties;
+import org.apache.fineract.los.scoring.model.ApplicantScoringProfile;
+import org.apache.fineract.los.scoring.model.FactorScore;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.junit.jupiter.params.provider.NullAndEmptySource;
+
+/** Unit tests for {@link LoanPurposeRiskFactor}. */
+@DisplayName("LoanPurposeRiskFactor")
+class LoanPurposeRiskFactorTest {
+
+ private LoanPurposeRiskFactor factor;
+
+ @BeforeEach
+ void setUp() {
+ factor = new LoanPurposeRiskFactor(new ScoringWeightsProperties());
+ }
+
+ @Test
+ @DisplayName("maxPoints returns configured loan-purpose-risk weight")
+ void maxPointsReturnsConfiguredWeight() {
+ assertThat(factor.maxPoints()).isEqualTo(10);
+ }
+
+ @Test
+ @DisplayName("factorName returns stable identifier")
+ void factorNameIsStable() {
+ assertThat(factor.factorName()).isEqualTo("loan-purpose-risk");
+ }
+
+ @ParameterizedTest(name = "purpose={0} expectedPoints={1}")
+ @DisplayName("Each purpose category produces expected points")
+ @CsvSource({
+ "AGRICULTURE, 10",
+ "EDUCATION, 10",
+ "BUSINESS, 8",
+ "HOME_IMPROVEMENT, 8",
+ "CONSUMER, 5",
+ "MEDICAL, 5",
+ "SPECULATION, 3",
+ "OTHER, 3",
+ })
+ void purposeCategories(final String purpose, final int expectedPoints) {
+ final ApplicantScoringProfile profile =
+ ApplicantScoringProfile.builder().loanPurpose(purpose).build();
+
+ final FactorScore result = factor.score(profile);
+
+ assertThat(result.getPoints()).isEqualTo(expectedPoints);
+ }
+
+ @ParameterizedTest
+ @NullAndEmptySource
+ @DisplayName("Null or blank purpose scores neutral default")
+ void nullOrBlankPurposeScoresNeutral(final String purpose) {
+ final ApplicantScoringProfile profile =
+ ApplicantScoringProfile.builder().loanPurpose(purpose).build();
+
+ final FactorScore result = factor.score(profile);
+
+ assertThat(result.getPoints()).isEqualTo(5); // 50% of 10
+ assertThat(result.getExplanation()).contains("UNSPECIFIED");
Review Comment:
Same as above
##########
src/test/java/org/apache/fineract/los/scoring/factors/LoanPurposeRiskFactorTest.java:
##########
@@ -0,0 +1,123 @@
+/*
+ * 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.los.scoring.factors;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.apache.fineract.los.scoring.ScoringWeightsProperties;
+import org.apache.fineract.los.scoring.model.ApplicantScoringProfile;
+import org.apache.fineract.los.scoring.model.FactorScore;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.junit.jupiter.params.provider.NullAndEmptySource;
+
+/** Unit tests for {@link LoanPurposeRiskFactor}. */
+@DisplayName("LoanPurposeRiskFactor")
+class LoanPurposeRiskFactorTest {
+
+ private LoanPurposeRiskFactor factor;
+
+ @BeforeEach
+ void setUp() {
+ factor = new LoanPurposeRiskFactor(new ScoringWeightsProperties());
+ }
+
+ @Test
+ @DisplayName("maxPoints returns configured loan-purpose-risk weight")
+ void maxPointsReturnsConfiguredWeight() {
+ assertThat(factor.maxPoints()).isEqualTo(10);
+ }
+
+ @Test
+ @DisplayName("factorName returns stable identifier")
+ void factorNameIsStable() {
+ assertThat(factor.factorName()).isEqualTo("loan-purpose-risk");
+ }
+
+ @ParameterizedTest(name = "purpose={0} expectedPoints={1}")
+ @DisplayName("Each purpose category produces expected points")
+ @CsvSource({
+ "AGRICULTURE, 10",
+ "EDUCATION, 10",
+ "BUSINESS, 8",
+ "HOME_IMPROVEMENT, 8",
+ "CONSUMER, 5",
+ "MEDICAL, 5",
+ "SPECULATION, 3",
+ "OTHER, 3",
+ })
+ void purposeCategories(final String purpose, final int expectedPoints) {
+ final ApplicantScoringProfile profile =
+ ApplicantScoringProfile.builder().loanPurpose(purpose).build();
+
+ final FactorScore result = factor.score(profile);
+
+ assertThat(result.getPoints()).isEqualTo(expectedPoints);
+ }
+
+ @ParameterizedTest
+ @NullAndEmptySource
+ @DisplayName("Null or blank purpose scores neutral default")
+ void nullOrBlankPurposeScoresNeutral(final String purpose) {
+ final ApplicantScoringProfile profile =
+ ApplicantScoringProfile.builder().loanPurpose(purpose).build();
+
+ final FactorScore result = factor.score(profile);
+
+ assertThat(result.getPoints()).isEqualTo(5); // 50% of 10
+ assertThat(result.getExplanation()).contains("UNSPECIFIED");
+ }
+
+ @Test
+ @DisplayName("Unrecognised purpose scores neutral default")
+ void unrecognisedPurposeScoresNeutral() {
+ final ApplicantScoringProfile profile =
+ ApplicantScoringProfile.builder().loanPurpose("VACATION").build();
+
+ final FactorScore result = factor.score(profile);
+
+ assertThat(result.getPoints()).isEqualTo(5);
+ }
+
+ @Test
+ @DisplayName("Purpose comparison is case-insensitive")
+ void purposeComparisonIsCaseInsensitive() {
+ final ApplicantScoringProfile profile =
+ ApplicantScoringProfile.builder().loanPurpose("agriculture").build();
Review Comment:
same as above
##########
src/test/java/org/apache/fineract/los/scoring/factors/LoanPurposeRiskFactorTest.java:
##########
@@ -0,0 +1,123 @@
+/*
+ * 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.los.scoring.factors;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.apache.fineract.los.scoring.ScoringWeightsProperties;
+import org.apache.fineract.los.scoring.model.ApplicantScoringProfile;
+import org.apache.fineract.los.scoring.model.FactorScore;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.junit.jupiter.params.provider.NullAndEmptySource;
+
+/** Unit tests for {@link LoanPurposeRiskFactor}. */
+@DisplayName("LoanPurposeRiskFactor")
+class LoanPurposeRiskFactorTest {
+
+ private LoanPurposeRiskFactor factor;
+
+ @BeforeEach
+ void setUp() {
+ factor = new LoanPurposeRiskFactor(new ScoringWeightsProperties());
+ }
+
+ @Test
+ @DisplayName("maxPoints returns configured loan-purpose-risk weight")
+ void maxPointsReturnsConfiguredWeight() {
+ assertThat(factor.maxPoints()).isEqualTo(10);
+ }
+
+ @Test
+ @DisplayName("factorName returns stable identifier")
+ void factorNameIsStable() {
+ assertThat(factor.factorName()).isEqualTo("loan-purpose-risk");
+ }
+
+ @ParameterizedTest(name = "purpose={0} expectedPoints={1}")
+ @DisplayName("Each purpose category produces expected points")
+ @CsvSource({
+ "AGRICULTURE, 10",
+ "EDUCATION, 10",
+ "BUSINESS, 8",
+ "HOME_IMPROVEMENT, 8",
+ "CONSUMER, 5",
+ "MEDICAL, 5",
+ "SPECULATION, 3",
+ "OTHER, 3",
+ })
+ void purposeCategories(final String purpose, final int expectedPoints) {
+ final ApplicantScoringProfile profile =
+ ApplicantScoringProfile.builder().loanPurpose(purpose).build();
+
+ final FactorScore result = factor.score(profile);
+
+ assertThat(result.getPoints()).isEqualTo(expectedPoints);
+ }
+
+ @ParameterizedTest
+ @NullAndEmptySource
+ @DisplayName("Null or blank purpose scores neutral default")
+ void nullOrBlankPurposeScoresNeutral(final String purpose) {
+ final ApplicantScoringProfile profile =
+ ApplicantScoringProfile.builder().loanPurpose(purpose).build();
+
+ final FactorScore result = factor.score(profile);
+
+ assertThat(result.getPoints()).isEqualTo(5); // 50% of 10
+ assertThat(result.getExplanation()).contains("UNSPECIFIED");
+ }
+
+ @Test
+ @DisplayName("Unrecognised purpose scores neutral default")
+ void unrecognisedPurposeScoresNeutral() {
+ final ApplicantScoringProfile profile =
+ ApplicantScoringProfile.builder().loanPurpose("VACATION").build();
+
+ final FactorScore result = factor.score(profile);
+
+ assertThat(result.getPoints()).isEqualTo(5);
+ }
+
+ @Test
+ @DisplayName("Purpose comparison is case-insensitive")
+ void purposeComparisonIsCaseInsensitive() {
+ final ApplicantScoringProfile profile =
+ ApplicantScoringProfile.builder().loanPurpose("agriculture").build();
+
+ final FactorScore result = factor.score(profile);
+
+ assertThat(result.getPoints()).isEqualTo(10);
+ }
+
+ @Test
+ @DisplayName("Points never exceed configured maxPoints")
+ void pointsNeverExceedMaxPoints() {
+ final ApplicantScoringProfile profile =
+ ApplicantScoringProfile.builder().loanPurpose("EDUCATION").build();
Review Comment:
Here too
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: issues-unsubscribe@fineract.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org