From 38b16f9daa3eb4278b6766ab3d91e8cd55a57f2f Mon Sep 17 00:00:00 2001 From: Risto Uibo Date: Sun, 16 Jun 2024 22:53:40 +0300 Subject: [PATCH 01/17] add few Kotlin implementations to operate side-by-side with Java, for: 1) eligibility gateway adapters, 2) ssn validation usecase in core --- server/gradle.properties | 5 +- server/loans/core/build.gradle | 18 ++++ .../usecases/ProvideLoanConfiguration.java | 31 ------- .../bank/loans/usecases/SsnValidation.java | 24 ------ .../usecases/ProvideLoanConfiguration.kt | 23 ++++++ .../kata/bank/loans/usecases/SsnValidation.kt | 27 ++++++ .../loans/usecases/SsnValidationTest.java | 82 ------------------- .../bank/loans/usecases/SsnValidationTest.kt | 78 ++++++++++++++++++ server/loans/gw-eligibility/build.gradle | 18 ++++ .../InMemoryCreditSegmentStorageAdapter.java | 33 -------- .../FirstEligiblePeriodAdapter.java | 27 ------ .../InMemoryCreditSegmentStorageAdapter.kt | 31 +++++++ .../FirstEligiblePeriodAdapter.kt | 24 ++++++ .../FirstEligiblePeriodAdapterTest.java | 50 ----------- .../FirstEligiblePeriodAdapterTest.kt | 46 +++++++++++ 15 files changed, 269 insertions(+), 248 deletions(-) delete mode 100644 server/loans/core/src/main/java/ee/rsx/kata/bank/loans/usecases/ProvideLoanConfiguration.java delete mode 100644 server/loans/core/src/main/java/ee/rsx/kata/bank/loans/usecases/SsnValidation.java create mode 100644 server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/usecases/ProvideLoanConfiguration.kt create mode 100644 server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/usecases/SsnValidation.kt delete mode 100644 server/loans/core/src/test/java/ee/rsx/kata/bank/loans/usecases/SsnValidationTest.java create mode 100644 server/loans/core/src/test/kotlin/ee/rsx/kata/bank/loans/usecases/SsnValidationTest.kt delete mode 100644 server/loans/gw-eligibility/src/main/java/ee/rsx/kata/bank/loans/adapter/eligibility/creditsegment/InMemoryCreditSegmentStorageAdapter.java delete mode 100644 server/loans/gw-eligibility/src/main/java/ee/rsx/kata/bank/loans/adapter/eligibility/eligibleperiod/FirstEligiblePeriodAdapter.java create mode 100644 server/loans/gw-eligibility/src/main/kotlin/ee/rsx/kata/bank/loans/adapter/eligibility/creditsegment/InMemoryCreditSegmentStorageAdapter.kt create mode 100644 server/loans/gw-eligibility/src/main/kotlin/ee/rsx/kata/bank/loans/adapter/eligibility/eligibleperiod/FirstEligiblePeriodAdapter.kt delete mode 100644 server/loans/gw-eligibility/src/test/java/ee/rsx/kata/bank/loans/adapter/eligibility/eligibleperiod/FirstEligiblePeriodAdapterTest.java create mode 100644 server/loans/gw-eligibility/src/test/kotlin/ee/rsx/kata/bank/loans/adapter/eligibility/eligibleperiod/FirstEligiblePeriodAdapterTest.kt diff --git a/server/gradle.properties b/server/gradle.properties index 752b5a4..7dd778c 100644 --- a/server/gradle.properties +++ b/server/gradle.properties @@ -1,7 +1,7 @@ apacheCommonsVersion=4.4 + assertjVersion=3.26.0 archUnitVersion=1.3.0 - hamcrestVersion=2.2 jakartaAnnotationVersion=3.0.0 @@ -9,9 +9,12 @@ jakartaInjectVersion=2.0.1 junitJupiterVersion=5.10.2 +kotlinVersion=2.0.0 + lombokPluginVersion=8.6 lombokVersion=1.18.32 mockitoVersion=5.12.0 + springBootVersion=3.3.0 springDependencyManagementVersion=1.1.5 diff --git a/server/loans/core/build.gradle b/server/loans/core/build.gradle index f6f2aa0..4ff190b 100644 --- a/server/loans/core/build.gradle +++ b/server/loans/core/build.gradle @@ -1,6 +1,24 @@ +import static org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 + +plugins { + id 'org.jetbrains.kotlin.jvm' version "${kotlinVersion}" +} + group = 'ee.rsx.kata.bank' version = '1.0.0-SNAPSHOT' dependencies { implementation project(":loans-api") } + +compileKotlin { + compilerOptions { + jvmTarget = JVM_17 + } +} + +compileTestKotlin { + compilerOptions { + jvmTarget = JVM_17 + } +} diff --git a/server/loans/core/src/main/java/ee/rsx/kata/bank/loans/usecases/ProvideLoanConfiguration.java b/server/loans/core/src/main/java/ee/rsx/kata/bank/loans/usecases/ProvideLoanConfiguration.java deleted file mode 100644 index afd4528..0000000 --- a/server/loans/core/src/main/java/ee/rsx/kata/bank/loans/usecases/ProvideLoanConfiguration.java +++ /dev/null @@ -1,31 +0,0 @@ -package ee.rsx.kata.bank.loans.usecases; - -import ee.rsx.kata.bank.loans.domain.limits.LoanLimitsConfig; -import ee.rsx.kata.bank.loans.domain.limits.gateway.LoanConfigGateway; -import ee.rsx.kata.bank.loans.validation.limits.LoadValidationLimits; -import ee.rsx.kata.bank.loans.validation.limits.ValidationLimitsDTO; -import jakarta.inject.Named; -import lombok.RequiredArgsConstructor; - -@Named -@RequiredArgsConstructor -class ProvideLoanConfiguration implements LoadValidationLimits { - - private static ValidationLimitsDTO toDto(LoanLimitsConfig config) { - return new ValidationLimitsDTO( - config.minimumLoanAmount(), - config.maximumLoanAmount(), - config.minimumLoanPeriodMonths(), - config.maximumLoanPeriodMonths() - ); - } - - private final LoanConfigGateway gateway; - - @Override - public ValidationLimitsDTO invoke() { - var limitsConfig = gateway.loadLimits(); - - return toDto(limitsConfig); - } -} diff --git a/server/loans/core/src/main/java/ee/rsx/kata/bank/loans/usecases/SsnValidation.java b/server/loans/core/src/main/java/ee/rsx/kata/bank/loans/usecases/SsnValidation.java deleted file mode 100644 index a0aa0f8..0000000 --- a/server/loans/core/src/main/java/ee/rsx/kata/bank/loans/usecases/SsnValidation.java +++ /dev/null @@ -1,24 +0,0 @@ -package ee.rsx.kata.bank.loans.usecases; - -import ee.rsx.kata.bank.loans.domain.ssn.SocialSecurityNumber; -import ee.rsx.kata.bank.loans.validation.ssn.SsnValidationResultDTO; -import ee.rsx.kata.bank.loans.validation.ssn.ValidateSocialSecurityNumber; -import jakarta.inject.Named; - -import java.time.format.DateTimeParseException; - -import static ee.rsx.kata.bank.loans.validation.ssn.SsnValidationResultDTO.*; - -@Named -class SsnValidation implements ValidateSocialSecurityNumber { - - @Override - public SsnValidationResultDTO on(String ssn) { - try { - var validSsn = new SocialSecurityNumber(ssn); - return okResultWith(validSsn.value()); - } catch (IllegalArgumentException | IllegalStateException | DateTimeParseException e) { - return invalidResultWith(ssn); - } - } -} diff --git a/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/usecases/ProvideLoanConfiguration.kt b/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/usecases/ProvideLoanConfiguration.kt new file mode 100644 index 0000000..4cfbeff --- /dev/null +++ b/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/usecases/ProvideLoanConfiguration.kt @@ -0,0 +1,23 @@ +package ee.rsx.kata.bank.loans.usecases + +import ee.rsx.kata.bank.loans.domain.limits.LoanLimitsConfig +import ee.rsx.kata.bank.loans.domain.limits.gateway.LoanConfigGateway +import ee.rsx.kata.bank.loans.validation.limits.LoadValidationLimits +import ee.rsx.kata.bank.loans.validation.limits.ValidationLimitsDTO +import jakarta.inject.Named + +@Named +internal class ProvideLoanConfiguration( + private val gateway: LoanConfigGateway +) : LoadValidationLimits { + + override fun invoke() = gateway.loadLimits().toDto() + + private fun LoanLimitsConfig.toDto() = + ValidationLimitsDTO( + minimumLoanAmount, + maximumLoanAmount, + minimumLoanPeriodMonths, + maximumLoanPeriodMonths + ) +} diff --git a/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/usecases/SsnValidation.kt b/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/usecases/SsnValidation.kt new file mode 100644 index 0000000..db828b8 --- /dev/null +++ b/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/usecases/SsnValidation.kt @@ -0,0 +1,27 @@ +package ee.rsx.kata.bank.loans.usecases + +import ee.rsx.kata.bank.loans.domain.ssn.SocialSecurityNumber +import ee.rsx.kata.bank.loans.validation.ssn.SsnValidationResultDTO +import ee.rsx.kata.bank.loans.validation.ssn.SsnValidationResultDTO.invalidResultWith +import ee.rsx.kata.bank.loans.validation.ssn.SsnValidationResultDTO.okResultWith +import ee.rsx.kata.bank.loans.validation.ssn.ValidateSocialSecurityNumber +import jakarta.inject.Named +import java.lang.RuntimeException +import java.time.format.DateTimeParseException + +@Named +internal class SsnValidation : ValidateSocialSecurityNumber { + + override fun on(ssn: String): SsnValidationResultDTO = + try { + val validSsn = SocialSecurityNumber(ssn) + okResultWith(validSsn.value) + } catch (e: RuntimeException) { + when (e) { + is IllegalArgumentException, + is IllegalStateException, + is DateTimeParseException -> invalidResultWith(ssn) + else -> throw e + } + } +} diff --git a/server/loans/core/src/test/java/ee/rsx/kata/bank/loans/usecases/SsnValidationTest.java b/server/loans/core/src/test/java/ee/rsx/kata/bank/loans/usecases/SsnValidationTest.java deleted file mode 100644 index dd2b59e..0000000 --- a/server/loans/core/src/test/java/ee/rsx/kata/bank/loans/usecases/SsnValidationTest.java +++ /dev/null @@ -1,82 +0,0 @@ -package ee.rsx.kata.bank.loans.usecases; - -import ee.rsx.kata.bank.loans.validation.ssn.SsnValidationResultDTO; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - -import static ee.rsx.kata.bank.loans.validation.ssn.ValidationStatus.*; -import static java.lang.String.format; -import static org.assertj.core.api.Assertions.assertThat; - -@DisplayName("SSN Validation") -class SsnValidationTest { - - private SsnValidation ssnValidation; - - @BeforeEach - void setup() { - ssnValidation = new SsnValidation(); - } - - @Test - @DisplayName("result is OK, for a given valid SSN value") - void resultIs_OK_for_givenValidSsnValue() { - var validSsnValue = "50212104262"; - - var result = ssnValidation.on(validSsnValue); - - assertThat(result).isEqualTo( - new SsnValidationResultDTO(validSsnValue, OK) - ); - } - - @Nested - @DisplayName("result is INVALID, for a given SSN value") - class ResultIsInvalidForAGivenSsnValue { - - private static SsnValidationResultDTO expectedInvalidResultFor(String invalidSsnValue) { - return new SsnValidationResultDTO(invalidSsnValue, INVALID); - } - - @Test - @DisplayName("having invalid date") - void having_invalidDate() { - var invalidDate = "021310"; - var invalidSsnValue = format("5%s4262", invalidDate); - - var result = ssnValidation.on(invalidSsnValue); - - assertThat(result).isEqualTo( - expectedInvalidResultFor(invalidSsnValue) - ); - } - - @Test - @DisplayName("having invalid checksum") - void having_invalidChecksum() { - var invalidChecksum = "3"; - var invalidSsnValue = format("5021210426%s", invalidChecksum); - - var result = ssnValidation.on(invalidSsnValue); - - assertThat(result).isEqualTo( - expectedInvalidResultFor(invalidSsnValue) - ); - } - - @Test - @DisplayName("having invalid century prefix") - void having_invalidCenturyPrefix() { - var invalidCenturyPrefix = "7"; - var invalidSsnValue = format("%s0212104262", invalidCenturyPrefix); - - var result = ssnValidation.on(invalidSsnValue); - - assertThat(result).isEqualTo( - expectedInvalidResultFor(invalidSsnValue) - ); - } - } -} diff --git a/server/loans/core/src/test/kotlin/ee/rsx/kata/bank/loans/usecases/SsnValidationTest.kt b/server/loans/core/src/test/kotlin/ee/rsx/kata/bank/loans/usecases/SsnValidationTest.kt new file mode 100644 index 0000000..d4f5e72 --- /dev/null +++ b/server/loans/core/src/test/kotlin/ee/rsx/kata/bank/loans/usecases/SsnValidationTest.kt @@ -0,0 +1,78 @@ +package ee.rsx.kata.bank.loans.usecases + +import ee.rsx.kata.bank.loans.validation.ssn.SsnValidationResultDTO +import ee.rsx.kata.bank.loans.validation.ssn.ValidationStatus.INVALID +import ee.rsx.kata.bank.loans.validation.ssn.ValidationStatus.OK +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test + +@DisplayName("SSN Validation") +internal class SsnValidationTest { + + private lateinit var ssnValidation: SsnValidation + + @BeforeEach + fun setup() { + ssnValidation = SsnValidation() + } + + @Test + fun `result is OK, for a given valid SSN value`() { + val validSsnValue = "50212104262" + + val result = ssnValidation.on(validSsnValue) + + assertThat(result).isEqualTo( + SsnValidationResultDTO(validSsnValue, OK) + ) + } + + @Nested + @DisplayName("result is INVALID, for a given SSN value") + internal inner class ResultIsInvalidForAGivenSsnValue { + + @Test + fun `having invalid date`() { + val invalidDate = "021310" + val invalidSsnValue = "5${invalidDate}4262" + + val result = ssnValidation.on(invalidSsnValue) + + assertThat(result).isEqualTo( + expectedInvalidResultFor(invalidSsnValue) + ) + } + + @Test + fun `having invalid checksum`() { + val invalidChecksum = "3" + val invalidSsnValue = "5021210426${invalidChecksum}" + + val result = ssnValidation.on(invalidSsnValue) + + assertThat(result).isEqualTo( + expectedInvalidResultFor(invalidSsnValue) + ) + } + + @Test + fun `having invalid century prefix`() { + val invalidCenturyPrefix = "7" + val invalidSsnValue = "${invalidCenturyPrefix}0212104262" + + val result = ssnValidation.on(invalidSsnValue) + + assertThat(result).isEqualTo( + expectedInvalidResultFor(invalidSsnValue) + ) + } + } + + companion object { + private fun expectedInvalidResultFor(invalidSsnValue: String) = + SsnValidationResultDTO(invalidSsnValue, INVALID) + } +} diff --git a/server/loans/gw-eligibility/build.gradle b/server/loans/gw-eligibility/build.gradle index d6e1234..31fb1e1 100644 --- a/server/loans/gw-eligibility/build.gradle +++ b/server/loans/gw-eligibility/build.gradle @@ -1,6 +1,24 @@ +import static org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 + +plugins { + id 'org.jetbrains.kotlin.jvm' version "${kotlinVersion}" +} + group = 'ee.rsx.kata.bank' version = '1.0.0-SNAPSHOT' dependencies { implementation project(":loans-core") } + +compileKotlin { + compilerOptions { + jvmTarget = JVM_17 + } +} + +compileTestKotlin { + compilerOptions { + jvmTarget = JVM_17 + } +} diff --git a/server/loans/gw-eligibility/src/main/java/ee/rsx/kata/bank/loans/adapter/eligibility/creditsegment/InMemoryCreditSegmentStorageAdapter.java b/server/loans/gw-eligibility/src/main/java/ee/rsx/kata/bank/loans/adapter/eligibility/creditsegment/InMemoryCreditSegmentStorageAdapter.java deleted file mode 100644 index f9f799e..0000000 --- a/server/loans/gw-eligibility/src/main/java/ee/rsx/kata/bank/loans/adapter/eligibility/creditsegment/InMemoryCreditSegmentStorageAdapter.java +++ /dev/null @@ -1,33 +0,0 @@ -package ee.rsx.kata.bank.loans.adapter.eligibility.creditsegment; - -import ee.rsx.kata.bank.loans.domain.segment.CreditSegment; -import ee.rsx.kata.bank.loans.domain.segment.CreditSegmentType; -import ee.rsx.kata.bank.loans.domain.segment.gateway.FindCreditSegment; -import ee.rsx.kata.bank.loans.domain.ssn.SocialSecurityNumber; -import jakarta.inject.Named; - -import java.util.Map; -import java.util.Optional; - -import static ee.rsx.kata.bank.loans.domain.segment.CreditSegmentType.*; -import static java.util.Optional.ofNullable; - -@Named -class InMemoryCreditSegmentStorageAdapter implements FindCreditSegment { - - private static final Map creditSegmentOfPerson = Map.ofEntries( - segmentFor("49002010965", DEBT, 666), - segmentFor("49002010976", SEGMENT_1, 100), - segmentFor("49002010987", SEGMENT_2, 300), - segmentFor("49002010998", SEGMENT_3, 1000) - ); - - private static Map.Entry segmentFor(String ssn, CreditSegmentType withType, int withCreditModifier) { - return Map.entry(ssn, new CreditSegment(new SocialSecurityNumber(ssn), withType, withCreditModifier)); - } - - @Override - public Optional forPerson(SocialSecurityNumber ssn) { - return ofNullable(creditSegmentOfPerson.get(ssn.value())); - } -} diff --git a/server/loans/gw-eligibility/src/main/java/ee/rsx/kata/bank/loans/adapter/eligibility/eligibleperiod/FirstEligiblePeriodAdapter.java b/server/loans/gw-eligibility/src/main/java/ee/rsx/kata/bank/loans/adapter/eligibility/eligibleperiod/FirstEligiblePeriodAdapter.java deleted file mode 100644 index 6bce53d..0000000 --- a/server/loans/gw-eligibility/src/main/java/ee/rsx/kata/bank/loans/adapter/eligibility/eligibleperiod/FirstEligiblePeriodAdapter.java +++ /dev/null @@ -1,27 +0,0 @@ -package ee.rsx.kata.bank.loans.adapter.eligibility.eligibleperiod; - -import ee.rsx.kata.bank.loans.domain.limits.gateway.DetermineEligiblePeriod; -import ee.rsx.kata.bank.loans.domain.segment.CreditSegment; -import jakarta.inject.Named; - -import java.util.Optional; - -import static java.util.Optional.*; - -@Named -class FirstEligiblePeriodAdapter implements DetermineEligiblePeriod { - - @Override - public Optional forLoan(Integer amount, CreditSegment creditSegment) { - if (creditSegment.isDebt()) { - return empty(); - } - return of(calculateFirstMinimumPeriodEligibleFor(amount, creditSegment)); - } - - private Integer calculateFirstMinimumPeriodEligibleFor(Integer amount, CreditSegment creditSegment) { - double firstPeriod = Math.floor((double) amount / creditSegment.creditModifier()); - - return Double.valueOf(firstPeriod).intValue() + 1; - } -} diff --git a/server/loans/gw-eligibility/src/main/kotlin/ee/rsx/kata/bank/loans/adapter/eligibility/creditsegment/InMemoryCreditSegmentStorageAdapter.kt b/server/loans/gw-eligibility/src/main/kotlin/ee/rsx/kata/bank/loans/adapter/eligibility/creditsegment/InMemoryCreditSegmentStorageAdapter.kt new file mode 100644 index 0000000..ce2fae1 --- /dev/null +++ b/server/loans/gw-eligibility/src/main/kotlin/ee/rsx/kata/bank/loans/adapter/eligibility/creditsegment/InMemoryCreditSegmentStorageAdapter.kt @@ -0,0 +1,31 @@ +package ee.rsx.kata.bank.loans.adapter.eligibility.creditsegment + +import ee.rsx.kata.bank.loans.domain.segment.CreditSegment +import ee.rsx.kata.bank.loans.domain.segment.CreditSegmentType +import ee.rsx.kata.bank.loans.domain.segment.CreditSegmentType.DEBT +import ee.rsx.kata.bank.loans.domain.segment.CreditSegmentType.SEGMENT_1 +import ee.rsx.kata.bank.loans.domain.segment.CreditSegmentType.SEGMENT_2 +import ee.rsx.kata.bank.loans.domain.segment.CreditSegmentType.SEGMENT_3 +import ee.rsx.kata.bank.loans.domain.segment.gateway.FindCreditSegment +import ee.rsx.kata.bank.loans.domain.ssn.SocialSecurityNumber +import jakarta.inject.Named +import java.util.* + +@Named +internal class InMemoryCreditSegmentStorageAdapter : FindCreditSegment { + + override fun forPerson(ssn: SocialSecurityNumber) = + Optional.ofNullable(creditSegmentOfPerson[ssn.value]) + + companion object { + private val creditSegmentOfPerson = mapOf( + "49002010965".let { it to segmentFor(it, DEBT, 666) }, + "49002010976".let { it to segmentFor(it, SEGMENT_1, 100) }, + "49002010987".let { it to segmentFor(it, SEGMENT_2, 300) }, + "49002010998".let { it to segmentFor(it, SEGMENT_3, 1000) } + ) + + private fun segmentFor(ssn: String, withType: CreditSegmentType, withCreditModifier: Int) = + CreditSegment(SocialSecurityNumber(ssn), withType, withCreditModifier) + } +} diff --git a/server/loans/gw-eligibility/src/main/kotlin/ee/rsx/kata/bank/loans/adapter/eligibility/eligibleperiod/FirstEligiblePeriodAdapter.kt b/server/loans/gw-eligibility/src/main/kotlin/ee/rsx/kata/bank/loans/adapter/eligibility/eligibleperiod/FirstEligiblePeriodAdapter.kt new file mode 100644 index 0000000..be714a6 --- /dev/null +++ b/server/loans/gw-eligibility/src/main/kotlin/ee/rsx/kata/bank/loans/adapter/eligibility/eligibleperiod/FirstEligiblePeriodAdapter.kt @@ -0,0 +1,24 @@ +package ee.rsx.kata.bank.loans.adapter.eligibility.eligibleperiod + +import ee.rsx.kata.bank.loans.domain.limits.gateway.DetermineEligiblePeriod +import ee.rsx.kata.bank.loans.domain.segment.CreditSegment +import jakarta.inject.Named +import java.util.* +import java.util.Optional.empty +import java.util.Optional.of +import kotlin.math.floor + +@Named +internal class FirstEligiblePeriodAdapter : DetermineEligiblePeriod { + + override fun forLoan(amount: Int, creditSegment: CreditSegment): Optional = + if (creditSegment.isDebt) + empty() + else + of(calculateFirstMinimumPeriodEligibleFor(amount, creditSegment)) + + private fun calculateFirstMinimumPeriodEligibleFor(amount: Int, creditSegment: CreditSegment): Int { + val firstPeriod = floor(amount.toDouble() / creditSegment.creditModifier()) + return firstPeriod.toInt() + 1 + } +} diff --git a/server/loans/gw-eligibility/src/test/java/ee/rsx/kata/bank/loans/adapter/eligibility/eligibleperiod/FirstEligiblePeriodAdapterTest.java b/server/loans/gw-eligibility/src/test/java/ee/rsx/kata/bank/loans/adapter/eligibility/eligibleperiod/FirstEligiblePeriodAdapterTest.java deleted file mode 100644 index 8203907..0000000 --- a/server/loans/gw-eligibility/src/test/java/ee/rsx/kata/bank/loans/adapter/eligibility/eligibleperiod/FirstEligiblePeriodAdapterTest.java +++ /dev/null @@ -1,50 +0,0 @@ -package ee.rsx.kata.bank.loans.adapter.eligibility.eligibleperiod; - -import ee.rsx.kata.bank.loans.domain.segment.CreditSegment; -import ee.rsx.kata.bank.loans.domain.ssn.SocialSecurityNumber; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -import java.util.Optional; - -import static ee.rsx.kata.bank.loans.domain.segment.CreditSegmentType.*; -import static org.assertj.core.api.Assertions.assertThat; - -@DisplayName("Eligible period calculation adapter, which returns first period that would be eligible for given amount and credit modifier") -class FirstEligiblePeriodAdapterTest { - - private FirstEligiblePeriodAdapter determineEligiblePeriod; - - @BeforeEach - void setup() { - determineEligiblePeriod = new FirstEligiblePeriodAdapter(); - } - - @Test - @DisplayName("returns the first eligible period of 51 months, based on the given loan amount and credit modifier") - void returns_firstEligiblePeriod_of_51_Months_basedOn_givenLoanAmountAndCreditModifier() { - var requestedLoan = 5000; - var creditModifier = 100; - var segment = new CreditSegment(new SocialSecurityNumber("49002010976"), SEGMENT_1, creditModifier); - - Optional eligiblePeriod = determineEligiblePeriod.forLoan(requestedLoan, segment); - - assertThat(eligiblePeriod) - .isPresent() - .contains(51); - } - - @Test - @DisplayName(("returns no eligible period, when given credit segment is a debt segment")) - void returns_noEligiblePeriod_whenGivenCreditSegmentIsADebtSegment() { - var requestedLoan = 5000; - var creditModifier = 100; - var debtSegment = new CreditSegment(new SocialSecurityNumber("49002010976"), DEBT, creditModifier); - - Optional eligiblePeriod = determineEligiblePeriod.forLoan(requestedLoan, debtSegment); - - assertThat(eligiblePeriod) - .isNotPresent(); - } -} diff --git a/server/loans/gw-eligibility/src/test/kotlin/ee/rsx/kata/bank/loans/adapter/eligibility/eligibleperiod/FirstEligiblePeriodAdapterTest.kt b/server/loans/gw-eligibility/src/test/kotlin/ee/rsx/kata/bank/loans/adapter/eligibility/eligibleperiod/FirstEligiblePeriodAdapterTest.kt new file mode 100644 index 0000000..9e90e78 --- /dev/null +++ b/server/loans/gw-eligibility/src/test/kotlin/ee/rsx/kata/bank/loans/adapter/eligibility/eligibleperiod/FirstEligiblePeriodAdapterTest.kt @@ -0,0 +1,46 @@ +package ee.rsx.kata.bank.loans.adapter.eligibility.eligibleperiod + +import ee.rsx.kata.bank.loans.domain.segment.CreditSegment +import ee.rsx.kata.bank.loans.domain.segment.CreditSegmentType.DEBT +import ee.rsx.kata.bank.loans.domain.segment.CreditSegmentType.SEGMENT_1 +import ee.rsx.kata.bank.loans.domain.ssn.SocialSecurityNumber +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +@DisplayName("Eligible period calculation adapter, which returns first period that would be eligible for given amount and credit modifier") +internal class FirstEligiblePeriodAdapterTest { + + private lateinit var determineEligiblePeriod: FirstEligiblePeriodAdapter + + @BeforeEach + fun setup() { + determineEligiblePeriod = FirstEligiblePeriodAdapter() + } + + @Test + fun `returns the first eligible period of 51 months, based on the given loan amount and credit modifier`() { + val requestedLoan = 5000 + val creditModifier = 100 + val segment = CreditSegment(SocialSecurityNumber("49002010976"), SEGMENT_1, creditModifier) + + val eligiblePeriod = determineEligiblePeriod.forLoan(requestedLoan, segment) + + assertThat(eligiblePeriod) + .isPresent() + .contains(51) + } + + @Test + fun `returns no eligible period, when given credit segment is a debt segment`() { + val requestedLoan = 5000 + val creditModifier = 100 + val debtSegment = CreditSegment(SocialSecurityNumber("49002010976"), DEBT, creditModifier) + + val eligiblePeriod = determineEligiblePeriod.forLoan(requestedLoan, debtSegment) + + assertThat(eligiblePeriod) + .isNotPresent() + } +} From ec91f138714f2d2f0c4007cd8b309b2a016a94ad Mon Sep 17 00:00:00 2001 From: Risto Uibo Date: Sun, 23 Jun 2024 16:18:02 +0300 Subject: [PATCH 02/17] server | port SocialSecurityNumber to Kotlin --- .../domain/ssn/SocialSecurityNumber.java | 104 ---------- .../loans/domain/ssn/SocialSecurityNumber.kt | 77 +++++++ .../kata/bank/loans/usecases/SsnValidation.kt | 1 - .../domain/ssn/SocialSecurityNumberTest.java | 192 ------------------ .../domain/ssn/SocialSecurityNumberTest.kt | 177 ++++++++++++++++ 5 files changed, 254 insertions(+), 297 deletions(-) delete mode 100644 server/loans/core/src/main/java/ee/rsx/kata/bank/loans/domain/ssn/SocialSecurityNumber.java create mode 100644 server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/domain/ssn/SocialSecurityNumber.kt delete mode 100644 server/loans/core/src/test/java/ee/rsx/kata/bank/loans/domain/ssn/SocialSecurityNumberTest.java create mode 100644 server/loans/core/src/test/kotlin/ee/rsx/kata/bank/loans/domain/ssn/SocialSecurityNumberTest.kt diff --git a/server/loans/core/src/main/java/ee/rsx/kata/bank/loans/domain/ssn/SocialSecurityNumber.java b/server/loans/core/src/main/java/ee/rsx/kata/bank/loans/domain/ssn/SocialSecurityNumber.java deleted file mode 100644 index 7a18c44..0000000 --- a/server/loans/core/src/main/java/ee/rsx/kata/bank/loans/domain/ssn/SocialSecurityNumber.java +++ /dev/null @@ -1,104 +0,0 @@ -package ee.rsx.kata.bank.loans.domain.ssn; - -import java.time.LocalDate; -import java.time.format.DateTimeFormatter; -import java.time.format.DateTimeParseException; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Supplier; -import java.util.regex.Pattern; -import java.util.stream.IntStream; - -import static java.lang.Integer.parseInt; -import static java.lang.String.format; -import static java.time.LocalDate.now; -import static java.time.format.DateTimeFormatter.ofPattern; -import static java.util.regex.Pattern.compile; - -public record SocialSecurityNumber(String value) { - - private static final Pattern SSN_PATTERN = compile("^[0-9]\\d{2}[0-1]\\d{7}$"); - private static final DateTimeFormatter SSN_DATE_FORMAT = ofPattern("yyyyMMdd"); - - private static final int[] DEFAULT_CHECKSUM_MULTIPLIERS = new int[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 1}; - private static final int[] RECALCULATED_CHECKSUM_MULTIPLIERS = new int[]{3, 4, 5, 6, 7, 8, 9, 1, 2, 3}; - - /** - * @throws IllegalArgumentException when provided SSN value is invalid (does not correspond to Estonian SSN rules) - * @throws IllegalStateException when century prefix of provided SSN value is invalid (not between 1...6) - * @throws DateTimeParseException when birth date part in of provided SSN does not represent a valid date - */ - public SocialSecurityNumber(String value) { - this.value = value; - validate(); - } - - private void validate() { - var isValid = - SSN_PATTERN.matcher(value).matches() && - isValidBirthDate() && - calculateChecksum() == parseChecksumAtTheEnd(); - - if (!isValid) { - throw new IllegalArgumentException(); - } - } - - private boolean isValidBirthDate() { - var birthDate = parseBirthDate(); - return birthDate.isEqual(now()) || birthDate.isBefore(now()); - } - - private LocalDate parseBirthDate() { - var date = value.substring(1, 7); - var century = value.substring(0, 1); - date = switch (century) { - case "1", "2" -> "18" + date; - case "3", "4" -> "19" + date; - case "5", "6" -> "20" + date; - default -> throw new IllegalStateException( - format("Illegal century value (%s) in social security number (%s)", century, value) - ); - }; - return LocalDate.parse(date, SSN_DATE_FORMAT); - } - - private int calculateChecksum() { - Supplier recalculation = - () -> calculateChecksumUsing(RECALCULATED_CHECKSUM_MULTIPLIERS, () -> 0); - - return calculateChecksumUsing(DEFAULT_CHECKSUM_MULTIPLIERS, recalculation); - } - - private int calculateChecksumUsing(int[] multipliers, Supplier recalculatedChecksum) { - var total = totalOfEachSsnNumberMultipliedWith(multipliers); - - var modulus = total % 11; - if (isDoubleDigit(modulus)) { - modulus = recalculatedChecksum.get(); - } - - return modulus; - } - - private int totalOfEachSsnNumberMultipliedWith(int[] multipliers) { - var total = new AtomicInteger(); - - IntStream - .range(0, value.length() - 1) - .forEach((index) -> total.addAndGet(numberAt(index) * multipliers[index])); - - return total.get(); - } - - private int numberAt(int index) { - return parseInt(value.substring(index, index + 1)); - } - - private boolean isDoubleDigit(int modulus) { - return 10 == modulus; - } - - private int parseChecksumAtTheEnd() { - return parseInt(value.substring(value.length() - 1)); - } -} diff --git a/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/domain/ssn/SocialSecurityNumber.kt b/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/domain/ssn/SocialSecurityNumber.kt new file mode 100644 index 0000000..8583dae --- /dev/null +++ b/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/domain/ssn/SocialSecurityNumber.kt @@ -0,0 +1,77 @@ +package ee.rsx.kata.bank.loans.domain.ssn + +import java.lang.IllegalStateException +import java.time.LocalDate +import java.time.LocalDate.now +import java.time.format.DateTimeFormatter.ofPattern +import java.util.concurrent.atomic.AtomicInteger +import java.util.function.Supplier +import java.util.regex.Pattern.compile +import java.util.stream.IntStream + +data class SocialSecurityNumber(val value: String) { + + /** + * @throws IllegalArgumentException when provided SSN value is invalid (does not correspond to Estonian SSN rules) + * @throws IllegalStateException when century prefix of provided SSN value is invalid (not between 1...6) + * @throws DateTimeParseException when birth date part in of provided SSN does not represent a valid date + */ + init { + validate() + } + + companion object { + private val SSN_PATTERN = compile("^[0-9]\\d{2}[0-1]\\d{7}$") + private val SSN_DATE_FORMAT = ofPattern("yyyyMMdd") + private val DEFAULT_CHECKSUM_MULTIPLIERS = intArrayOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 1) + private val RECALCULATED_CHECKSUM_MULTIPLIERS = intArrayOf(3, 4, 5, 6, 7, 8, 9, 1, 2, 3) + } + + private fun validate() = require( + SSN_PATTERN.matcher(value).matches() && + isValidBirthDate() && + calculateChecksum() == parseChecksumAtTheEnd() + ) + + private fun isValidBirthDate() = parseBirthDate().let { it.isEqual(now()) || it.isBefore(now()) } + + private fun parseBirthDate(): LocalDate { + var date = value.substring(1, 7) + val century = value.substring(0, 1) + date = when (century) { + "1", "2" -> "18$date" + "3", "4" -> "19$date" + "5", "6" -> "20$date" + else -> error("Illegal century value ($century) in social security number ($value)") + } + return LocalDate.parse(date, SSN_DATE_FORMAT) + } + + private fun calculateChecksum(): Int { + val recalculation = { calculateChecksumUsing(RECALCULATED_CHECKSUM_MULTIPLIERS) { 0 } } + return calculateChecksumUsing(DEFAULT_CHECKSUM_MULTIPLIERS, recalculation) + } + + private fun calculateChecksumUsing(multipliers: IntArray, recalculatedChecksum: Supplier): Int { + val total = totalOfEachSsnNumberMultipliedWith(multipliers) + var modulus = total % 11 + if (isDoubleDigit(modulus)) { + modulus = recalculatedChecksum.get() + } + return modulus + } + + private fun totalOfEachSsnNumberMultipliedWith(multipliers: IntArray): Int { + val total = AtomicInteger() + IntStream + .range(0, value.length - 1) + .forEach { index: Int -> total.addAndGet(numberAt(index) * multipliers[index]) } + return total.get() + } + + private fun numberAt(index: Int) = value.substring(index, index + 1).toInt() + + private fun isDoubleDigit(modulus: Int) = 10 == modulus + + private fun parseChecksumAtTheEnd() = value.substring(value.length - 1).toInt() +} diff --git a/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/usecases/SsnValidation.kt b/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/usecases/SsnValidation.kt index db828b8..c3e8903 100644 --- a/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/usecases/SsnValidation.kt +++ b/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/usecases/SsnValidation.kt @@ -6,7 +6,6 @@ import ee.rsx.kata.bank.loans.validation.ssn.SsnValidationResultDTO.invalidResul import ee.rsx.kata.bank.loans.validation.ssn.SsnValidationResultDTO.okResultWith import ee.rsx.kata.bank.loans.validation.ssn.ValidateSocialSecurityNumber import jakarta.inject.Named -import java.lang.RuntimeException import java.time.format.DateTimeParseException @Named diff --git a/server/loans/core/src/test/java/ee/rsx/kata/bank/loans/domain/ssn/SocialSecurityNumberTest.java b/server/loans/core/src/test/java/ee/rsx/kata/bank/loans/domain/ssn/SocialSecurityNumberTest.java deleted file mode 100644 index f051e73..0000000 --- a/server/loans/core/src/test/java/ee/rsx/kata/bank/loans/domain/ssn/SocialSecurityNumberTest.java +++ /dev/null @@ -1,192 +0,0 @@ -package ee.rsx.kata.bank.loans.domain.ssn; - -import org.assertj.core.api.ThrowableAssert.ThrowingCallable; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.function.Executable; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; - -import java.time.format.DateTimeParseException; -import java.util.stream.Stream; - -import static java.lang.String.format; -import static org.assertj.core.api.Assertions.*; -import static org.junit.jupiter.api.Assertions.*; - -@DisplayName("Social Security Number") -class SocialSecurityNumberTest { - - private static Stream validSocialSecurityNumbers() { - return Stream.of( - ssnArg("46301055224"), - ssnArg("37205250269"), - ssnArg("50212104262"), - ssnArg("34110140248"), - ssnArg("49002010976"), - ssnArg("49002010965"), - ssnArg("49002010998"), - ssnArg("49002010987"), - ssnArg("61504293707") - ); - } - - private static Arguments ssnArg(String ssn) { - return Arguments.of(ssn); - } - - @ParameterizedTest - @MethodSource("validSocialSecurityNumbers") - @DisplayName("Is created, when constructed with a valid ssn value of") - void isCreated_when_constructed_withValid(String ssnValue) { - assertDoesNotThrow( - () -> new SocialSecurityNumber(ssnValue) - ); - } - - @Test - @DisplayName("Has value equal to the valid provided ssn value") - void hasValue_equalTo_validProvidedSsnValue() { - String providedSsnValue = "49002010998"; - - SocialSecurityNumber validSsn = new SocialSecurityNumber(providedSsnValue); - - assertThat(validSsn.value()) - .isEqualTo(providedSsnValue); - } - - @Nested - @DisplayName("Creation fails, when SSN value") - class CreationFailsWhenSsnValue { - - private static Stream invalidDaysOfMonth() { - return Stream.of( - Arguments.of("00"), - Arguments.of("32"), - Arguments.of("99") - ); - } - - private static Stream invalidCenturyCodes() { - return Stream.of( - Arguments.of("0"), - Arguments.of("7"), - Arguments.of("8"), - Arguments.of("9") - ); - } - - @Test - @DisplayName("is blank") - void isBlank() { - String blankSsnValue = "\n \t"; - - ThrowingCallable test = () -> new SocialSecurityNumber(blankSsnValue); - - assertThatIllegalArgumentException().isThrownBy(test); - } - - @Test - @DisplayName("is too long") - void isTooLong() { - String tooLongSsnValue = "372052502690"; - - ThrowingCallable test = () -> new SocialSecurityNumber(tooLongSsnValue); - - assertThatIllegalArgumentException().isThrownBy(test); - } - - @Test - @DisplayName("is too short") - void isTooShort() { - String tooShortSsnValue = "3720525026"; - - ThrowingCallable test = () -> new SocialSecurityNumber(tooShortSsnValue); - - assertThatIllegalArgumentException().isThrownBy(test); - } - - @Test - @DisplayName("is not numeric") - void isNotNumeric() { - String nonNumericSsnValue = "3720525026A"; - - ThrowingCallable test = () -> new SocialSecurityNumber(nonNumericSsnValue); - - assertThatIllegalArgumentException().isThrownBy(test); - } - - @Test - @DisplayName("contains 31 as day of month, for a 30-day month") - void containsThirtyOneAsDayOfMonth_for_aThirtyDayMonth() { - String thirtyFirstDayOfMonth = "31"; - String monthWithThirtyDays = "04"; - String invalidSsnValue = format("372%s%s0269", monthWithThirtyDays, thirtyFirstDayOfMonth); - - ThrowingCallable test = () -> new SocialSecurityNumber(invalidSsnValue); - - assertThatIllegalArgumentException().isThrownBy(test); - } - - @Test - @DisplayName("contains birth date, which is in the future") - void containsBirthDate_which_isInTheFuture() { - String futureBirthDate = "950122"; - String invalidSsnValue = format("5%s4217", futureBirthDate); - - ThrowingCallable test = () -> new SocialSecurityNumber(invalidSsnValue); - - assertThatIllegalArgumentException().isThrownBy(test); - } - - @Test - @DisplayName("ends with invalid checksum") - void ends_with_invalidChecksum() { - String invalidChecksum = "6"; - String invalidSsnValue = format("6150429370%s", invalidChecksum); - - ThrowingCallable test = () -> new SocialSecurityNumber(invalidSsnValue); - - assertThatIllegalArgumentException().isThrownBy(test); - } - - @ParameterizedTest - @DisplayName("starts with invalid century code of") - @MethodSource("invalidCenturyCodes") - void starts_with_invalidCenturyCodeOf(String centuryCode) { - String invalidSsnValue = format("%s1504293706", centuryCode); - - ThrowingCallable test = () -> new SocialSecurityNumber(invalidSsnValue); - - assertThatIllegalStateException() - .isThrownBy(test) - .withMessage( - format("Illegal century value (%s) in social security number (%s)", centuryCode, invalidSsnValue) - ); - } - - @ParameterizedTest - @MethodSource("invalidDaysOfMonth") - @DisplayName("contains invalid day of month, as") - void containsInvalidDayOfMonth(String invalidDayOfMonth) { - String invalidSsnValue = format("37205%s0269", invalidDayOfMonth); - - Executable test = () -> new SocialSecurityNumber(invalidSsnValue); - - assertThrows(DateTimeParseException.class, test); - } - - @Test - @DisplayName("contains invalid month (13)") - void contains_invalidMonth_13() { - String invalidMonth = "13"; - String invalidSsnValue = format("615%s293707", invalidMonth); - - Executable test = () -> new SocialSecurityNumber(invalidSsnValue); - - assertThrows(DateTimeParseException.class, test); - } - } -} diff --git a/server/loans/core/src/test/kotlin/ee/rsx/kata/bank/loans/domain/ssn/SocialSecurityNumberTest.kt b/server/loans/core/src/test/kotlin/ee/rsx/kata/bank/loans/domain/ssn/SocialSecurityNumberTest.kt new file mode 100644 index 0000000..7b40636 --- /dev/null +++ b/server/loans/core/src/test/kotlin/ee/rsx/kata/bank/loans/domain/ssn/SocialSecurityNumberTest.kt @@ -0,0 +1,177 @@ +package ee.rsx.kata.bank.loans.domain.ssn + +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatIllegalArgumentException +import org.junit.jupiter.api.Assertions.assertDoesNotThrow +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import java.time.format.DateTimeParseException +import java.util.stream.Stream + +@DisplayName("Social Security Number") +internal class SocialSecurityNumberTest { + + @ParameterizedTest + @MethodSource("validSocialSecurityNumbers") + fun `Is created, when constructed with a valid ssn value of`(ssnValue: String) { + assertDoesNotThrow { + SocialSecurityNumber(ssnValue) + } + } + + @Test + fun `has value equal to the valid provided ssn value`() { + val providedSsnValue = "49002010998" + + val validSsn = SocialSecurityNumber(providedSsnValue) + + assertThat(validSsn.value).isEqualTo(providedSsnValue) + } + + @Nested + @TestInstance(PER_CLASS) + @DisplayName("Creation fails, when SSN value") + inner class CreationFailsWhenSsnValue { + + @Test + fun `is blank`() { + val blankSsnValue = "\n \t" + + val test: () -> Unit = { SocialSecurityNumber(blankSsnValue) } + + assertThatIllegalArgumentException().isThrownBy(test) + } + + @Test + fun `is too long`() { + val tooLongSsnValue = "372052502690" + + val test: () -> Unit = { SocialSecurityNumber(tooLongSsnValue) } + + assertThatIllegalArgumentException().isThrownBy(test) + } + + @Test + fun `is too short`() { + val tooShortSsnValue = "3720525026" + + val test: () -> Unit = { SocialSecurityNumber(tooShortSsnValue) } + + assertThatIllegalArgumentException().isThrownBy(test) + } + + @Test + fun `is not numeric`() { + val nonNumericSsnValue = "3720525026A" + + val test: () -> Unit = { SocialSecurityNumber(nonNumericSsnValue) } + + assertThatIllegalArgumentException().isThrownBy(test) + } + + @Test + fun `contains 31 as day of month, for a 30-day month`() { + val thirtyFirstDayOfMonth = "31" + val monthWithThirtyDays = "04" + val invalidSsnValue = "372$monthWithThirtyDays${thirtyFirstDayOfMonth}0269" + + val test: () -> Unit = { SocialSecurityNumber(invalidSsnValue) } + + assertThatIllegalArgumentException().isThrownBy(test) + } + + @Test + fun `contains birth date, which is in the future`() { + val futureBirthDate = "950122" + val invalidSsnValue = "5${futureBirthDate}4217" + + val test: () -> Unit = { SocialSecurityNumber(invalidSsnValue) } + + assertThatIllegalArgumentException().isThrownBy(test) + } + + @Test + fun `ends with invalid checksum`() { + val invalidChecksum = "6" + val invalidSsnValue = "6150429370$invalidChecksum" + + val test: () -> Unit = { SocialSecurityNumber(invalidSsnValue) } + + assertThatIllegalArgumentException().isThrownBy(test) + } + + @ParameterizedTest + @MethodSource("invalidCenturyCodes") + fun `starts with invalid century code of`(centuryCode: String) { + val invalidSsnValue = "${centuryCode}1504293706" + + val test: () -> Unit = { SocialSecurityNumber(invalidSsnValue) } + + assertThrows(test) + .apply { + assertThat(message).isEqualTo("Illegal century value ($centuryCode) in social security number ($invalidSsnValue)") + } + } + + @ParameterizedTest + @MethodSource("invalidDaysOfMonth") + fun `contains invalid day of month, as`(invalidDayOfMonth: String) { + val invalidSsnValue = "37205${invalidDayOfMonth}0269" + + val test: () -> Unit = { SocialSecurityNumber(invalidSsnValue) } + + assertThrows(DateTimeParseException::class.java, test) + } + + @Test + fun `contains invalid month (13)`() { + val invalidMonth = "13" + val invalidSsnValue = "615${invalidMonth}293707" + + val test: () -> Unit = { SocialSecurityNumber(invalidSsnValue) } + + assertThrows(DateTimeParseException::class.java, test) + } + + + private fun invalidCenturyCodes() = Stream.of( + Arguments.of("0"), + Arguments.of("7"), + Arguments.of("8"), + Arguments.of("9") + ) + + private fun invalidDaysOfMonth() = Stream.of( + Arguments.of("00"), + Arguments.of("32"), + Arguments.of("99") + ) + } + + companion object { + + @JvmStatic + private fun validSocialSecurityNumbers() = Stream.of( + ssnArg("46301055224"), + ssnArg("37205250269"), + ssnArg("50212104262"), + ssnArg("34110140248"), + ssnArg("49002010976"), + ssnArg("49002010965"), + ssnArg("49002010998"), + ssnArg("49002010987"), + ssnArg("61504293707") + ) + + + private fun ssnArg(ssn: String) = Arguments.of(ssn) + } +} From 768c177ad5aaff08b33a316d45ac15a5dbcb5b19 Mon Sep 17 00:00:00 2001 From: Risto Uibo Date: Sun, 23 Jun 2024 16:46:30 +0300 Subject: [PATCH 03/17] server | port CreditSegment to Kotlin --- .../loans/domain/segment/CreditSegment.java | 19 ---------- .../domain/segment/CreditSegmentType.java | 8 ---- .../segment/gateway/FindCreditSegment.java | 12 ------ .../usecases/LoanEligibilityCalculation.java | 4 +- .../loans/domain/segment/CreditSegment.kt | 16 ++++++++ .../loans/domain/segment/CreditSegmentType.kt | 8 ++++ .../segment/gateway/FindCreditSegment.kt | 9 +++++ .../domain/segment/CreditSegmentTest.java | 37 ------------------ .../loans/domain/segment/CreditSegmentTest.kt | 38 +++++++++++++++++++ .../FirstEligiblePeriodAdapter.kt | 2 +- 10 files changed, 74 insertions(+), 79 deletions(-) delete mode 100644 server/loans/core/src/main/java/ee/rsx/kata/bank/loans/domain/segment/CreditSegment.java delete mode 100644 server/loans/core/src/main/java/ee/rsx/kata/bank/loans/domain/segment/CreditSegmentType.java delete mode 100644 server/loans/core/src/main/java/ee/rsx/kata/bank/loans/domain/segment/gateway/FindCreditSegment.java create mode 100644 server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/domain/segment/CreditSegment.kt create mode 100644 server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/domain/segment/CreditSegmentType.kt create mode 100644 server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/domain/segment/gateway/FindCreditSegment.kt delete mode 100644 server/loans/core/src/test/java/ee/rsx/kata/bank/loans/domain/segment/CreditSegmentTest.java create mode 100644 server/loans/core/src/test/kotlin/ee/rsx/kata/bank/loans/domain/segment/CreditSegmentTest.kt diff --git a/server/loans/core/src/main/java/ee/rsx/kata/bank/loans/domain/segment/CreditSegment.java b/server/loans/core/src/main/java/ee/rsx/kata/bank/loans/domain/segment/CreditSegment.java deleted file mode 100644 index 2dfa710..0000000 --- a/server/loans/core/src/main/java/ee/rsx/kata/bank/loans/domain/segment/CreditSegment.java +++ /dev/null @@ -1,19 +0,0 @@ -package ee.rsx.kata.bank.loans.domain.segment; - -import ee.rsx.kata.bank.loans.domain.ssn.SocialSecurityNumber; - -import static ee.rsx.kata.bank.loans.domain.segment.CreditSegmentType.DEBT; - -public record CreditSegment( - SocialSecurityNumber ssn, - CreditSegmentType type, - int creditModifier -) { - public int creditModifier() { - - return isDebt() ? 0 : this.creditModifier; - } - public boolean isDebt() { - return type == DEBT; - } -} diff --git a/server/loans/core/src/main/java/ee/rsx/kata/bank/loans/domain/segment/CreditSegmentType.java b/server/loans/core/src/main/java/ee/rsx/kata/bank/loans/domain/segment/CreditSegmentType.java deleted file mode 100644 index 55b6d64..0000000 --- a/server/loans/core/src/main/java/ee/rsx/kata/bank/loans/domain/segment/CreditSegmentType.java +++ /dev/null @@ -1,8 +0,0 @@ -package ee.rsx.kata.bank.loans.domain.segment; - -public enum CreditSegmentType { - SEGMENT_1, - SEGMENT_2, - SEGMENT_3, - DEBT -} diff --git a/server/loans/core/src/main/java/ee/rsx/kata/bank/loans/domain/segment/gateway/FindCreditSegment.java b/server/loans/core/src/main/java/ee/rsx/kata/bank/loans/domain/segment/gateway/FindCreditSegment.java deleted file mode 100644 index 88c3d66..0000000 --- a/server/loans/core/src/main/java/ee/rsx/kata/bank/loans/domain/segment/gateway/FindCreditSegment.java +++ /dev/null @@ -1,12 +0,0 @@ -package ee.rsx.kata.bank.loans.domain.segment.gateway; - -import ee.rsx.kata.bank.loans.domain.segment.CreditSegment; -import ee.rsx.kata.bank.loans.domain.ssn.SocialSecurityNumber; - -import java.util.Optional; - -@FunctionalInterface -public interface FindCreditSegment { - - Optional forPerson(SocialSecurityNumber ssn); -} diff --git a/server/loans/core/src/main/java/ee/rsx/kata/bank/loans/usecases/LoanEligibilityCalculation.java b/server/loans/core/src/main/java/ee/rsx/kata/bank/loans/usecases/LoanEligibilityCalculation.java index c76fe58..52672e8 100644 --- a/server/loans/core/src/main/java/ee/rsx/kata/bank/loans/usecases/LoanEligibilityCalculation.java +++ b/server/loans/core/src/main/java/ee/rsx/kata/bank/loans/usecases/LoanEligibilityCalculation.java @@ -142,7 +142,7 @@ private LoanEligibilityStatus determineEligibilityStatusFor( LoanEligibilityRequestDTO request, CreditSegment creditSegment ) { double creditScore = - (double) creditSegment.creditModifier() / request.loanAmount() * request.loanPeriodMonths(); + (double) creditSegment.getCreditModifier() / request.loanAmount() * request.loanPeriodMonths(); return (!creditSegment.isDebt() && creditScore > 1) ? APPROVED : DENIED; } @@ -152,7 +152,7 @@ private Optional determineEligibleAmountFor( ) { int eligibleAmount = Math.min( limits.maximumLoanAmount(), - creditSegment.creditModifier() * loanPeriodMonths - 1 + creditSegment.getCreditModifier() * loanPeriodMonths - 1 ); return eligibleAmount >= limits.minimumLoanAmount() ? of(eligibleAmount) : empty(); diff --git a/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/domain/segment/CreditSegment.kt b/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/domain/segment/CreditSegment.kt new file mode 100644 index 0000000..70bfa49 --- /dev/null +++ b/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/domain/segment/CreditSegment.kt @@ -0,0 +1,16 @@ +package ee.rsx.kata.bank.loans.domain.segment + +import ee.rsx.kata.bank.loans.domain.segment.CreditSegmentType.DEBT +import ee.rsx.kata.bank.loans.domain.ssn.SocialSecurityNumber + +data class CreditSegment( + val ssn: SocialSecurityNumber, + val type: CreditSegmentType, + private val modifier: Int +) { + val isDebt + get() = type === DEBT + + val creditModifier + get() = if (isDebt) 0 else modifier +} diff --git a/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/domain/segment/CreditSegmentType.kt b/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/domain/segment/CreditSegmentType.kt new file mode 100644 index 0000000..f8ed128 --- /dev/null +++ b/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/domain/segment/CreditSegmentType.kt @@ -0,0 +1,8 @@ +package ee.rsx.kata.bank.loans.domain.segment + +enum class CreditSegmentType { + SEGMENT_1, + SEGMENT_2, + SEGMENT_3, + DEBT +} diff --git a/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/domain/segment/gateway/FindCreditSegment.kt b/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/domain/segment/gateway/FindCreditSegment.kt new file mode 100644 index 0000000..b09312f --- /dev/null +++ b/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/domain/segment/gateway/FindCreditSegment.kt @@ -0,0 +1,9 @@ +package ee.rsx.kata.bank.loans.domain.segment.gateway + +import ee.rsx.kata.bank.loans.domain.segment.CreditSegment +import ee.rsx.kata.bank.loans.domain.ssn.SocialSecurityNumber +import java.util.* + +fun interface FindCreditSegment { + fun forPerson(ssn: SocialSecurityNumber): Optional +} diff --git a/server/loans/core/src/test/java/ee/rsx/kata/bank/loans/domain/segment/CreditSegmentTest.java b/server/loans/core/src/test/java/ee/rsx/kata/bank/loans/domain/segment/CreditSegmentTest.java deleted file mode 100644 index 318c170..0000000 --- a/server/loans/core/src/test/java/ee/rsx/kata/bank/loans/domain/segment/CreditSegmentTest.java +++ /dev/null @@ -1,37 +0,0 @@ -package ee.rsx.kata.bank.loans.domain.segment; - -import ee.rsx.kata.bank.loans.domain.ssn.SocialSecurityNumber; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.EnumSource; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.params.provider.EnumSource.Mode.EXCLUDE; - -@DisplayName("Credit Segment") -class CreditSegmentTest { - - private static final SocialSecurityNumber SSN = new SocialSecurityNumber("49002010998"); - - @ParameterizedTest - @EnumSource(value = CreditSegmentType.class, mode = EXCLUDE, names = { "DEBT" }) - @DisplayName("has originally assigned credit modifier, when it's a non-debt segment of") - void hasOriginallyAssignedCreditModifier_when_itsNonDebtSegmentOf(CreditSegmentType type) { - int assignedCreditModifier = 500; - - assertThat(new CreditSegment(SSN, type, assignedCreditModifier)) - .extracting(CreditSegment::creditModifier) - .isEqualTo(assignedCreditModifier); - } - - @ParameterizedTest - @EnumSource(value = CreditSegmentType.class, names = { "DEBT" }) - @DisplayName("has a credit modifier of 0, when it's a debt segment as") - void hasCreditModifierOfZero_when_itsDebtSegmentAs(CreditSegmentType debtType) { - int assignedCreditModifier = 500; - - assertThat(new CreditSegment(SSN, debtType, assignedCreditModifier)) - .extracting(CreditSegment::creditModifier) - .isEqualTo(0); - } -} diff --git a/server/loans/core/src/test/kotlin/ee/rsx/kata/bank/loans/domain/segment/CreditSegmentTest.kt b/server/loans/core/src/test/kotlin/ee/rsx/kata/bank/loans/domain/segment/CreditSegmentTest.kt new file mode 100644 index 0000000..e5e41a2 --- /dev/null +++ b/server/loans/core/src/test/kotlin/ee/rsx/kata/bank/loans/domain/segment/CreditSegmentTest.kt @@ -0,0 +1,38 @@ +package ee.rsx.kata.bank.loans.domain.segment + +import ee.rsx.kata.bank.loans.domain.ssn.SocialSecurityNumber +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.EnumSource +import org.junit.jupiter.params.provider.EnumSource.Mode.EXCLUDE + +@DisplayName("Credit Segment") +internal class CreditSegmentTest { + + @ParameterizedTest + @EnumSource(value = CreditSegmentType::class, mode = EXCLUDE, names = ["DEBT"]) + fun `has originally assigned credit modifier, when it's a non-debt segment of`(type: CreditSegmentType) { + val assignedCreditModifier = 500 + + assertThat(CreditSegment(SSN, type, assignedCreditModifier)) + .extracting { it.creditModifier } + .isEqualTo(assignedCreditModifier) + } + + @ParameterizedTest + @EnumSource(value = CreditSegmentType::class, names = ["DEBT"]) + fun `has credit modifier of 0, when it's a debt segment as`(debtType: CreditSegmentType) { + val assignedCreditModifier = 500 + + val result = CreditSegment(SSN, debtType, assignedCreditModifier) + + assertThat(result) + .extracting { it.creditModifier } + .isEqualTo(0) + } + + companion object { + private val SSN = SocialSecurityNumber("49002010998") + } +} diff --git a/server/loans/gw-eligibility/src/main/kotlin/ee/rsx/kata/bank/loans/adapter/eligibility/eligibleperiod/FirstEligiblePeriodAdapter.kt b/server/loans/gw-eligibility/src/main/kotlin/ee/rsx/kata/bank/loans/adapter/eligibility/eligibleperiod/FirstEligiblePeriodAdapter.kt index be714a6..2aae427 100644 --- a/server/loans/gw-eligibility/src/main/kotlin/ee/rsx/kata/bank/loans/adapter/eligibility/eligibleperiod/FirstEligiblePeriodAdapter.kt +++ b/server/loans/gw-eligibility/src/main/kotlin/ee/rsx/kata/bank/loans/adapter/eligibility/eligibleperiod/FirstEligiblePeriodAdapter.kt @@ -18,7 +18,7 @@ internal class FirstEligiblePeriodAdapter : DetermineEligiblePeriod { of(calculateFirstMinimumPeriodEligibleFor(amount, creditSegment)) private fun calculateFirstMinimumPeriodEligibleFor(amount: Int, creditSegment: CreditSegment): Int { - val firstPeriod = floor(amount.toDouble() / creditSegment.creditModifier()) + val firstPeriod = floor(amount.toDouble() / creditSegment.creditModifier) return firstPeriod.toInt() + 1 } } From 5903284b65efd67eb13cd31296277d575b9e91a7 Mon Sep 17 00:00:00 2001 From: Risto Uibo Date: Sun, 23 Jun 2024 16:58:02 +0300 Subject: [PATCH 04/17] Rename .java to .kt --- .../kata/bank/loans/domain/limits/gateway/LoanConfigGateway.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename server/loans/core/src/main/{java/ee/rsx/kata/bank/loans/domain/limits/gateway/LoanConfigGateway.java => kotlin/ee/rsx/kata/bank/loans/domain/limits/gateway/LoanConfigGateway.kt} (100%) diff --git a/server/loans/core/src/main/java/ee/rsx/kata/bank/loans/domain/limits/gateway/LoanConfigGateway.java b/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/domain/limits/gateway/LoanConfigGateway.kt similarity index 100% rename from server/loans/core/src/main/java/ee/rsx/kata/bank/loans/domain/limits/gateway/LoanConfigGateway.java rename to server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/domain/limits/gateway/LoanConfigGateway.kt From bd3504169685e302cac26d7a23531fd3303dd7e2 Mon Sep 17 00:00:00 2001 From: Risto Uibo Date: Sun, 23 Jun 2024 16:58:02 +0300 Subject: [PATCH 05/17] server | port LoanLimitsConfig to Kotlin --- .../loans/domain/limits/LoanLimitsConfig.java | 8 ------ .../loans/domain/limits/LoanLimitsConfig.kt | 8 ++++++ .../limits/gateway/LoanConfigGateway.kt | 9 +++---- server/loans/gw-validation/build.gradle | 21 ++++++++++++++++ .../validation/LoanConfigurationAdapter.java | 25 ------------------- .../validation/LoanConfigurationAdapter.kt | 23 +++++++++++++++++ 6 files changed, 56 insertions(+), 38 deletions(-) delete mode 100644 server/loans/core/src/main/java/ee/rsx/kata/bank/loans/domain/limits/LoanLimitsConfig.java create mode 100644 server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/domain/limits/LoanLimitsConfig.kt delete mode 100644 server/loans/gw-validation/src/main/java/ee/rsx/kata/bank/loans/adapter/validation/LoanConfigurationAdapter.java create mode 100644 server/loans/gw-validation/src/main/kotlin/ee/rsx/kata/bank/loans/adapter/validation/LoanConfigurationAdapter.kt diff --git a/server/loans/core/src/main/java/ee/rsx/kata/bank/loans/domain/limits/LoanLimitsConfig.java b/server/loans/core/src/main/java/ee/rsx/kata/bank/loans/domain/limits/LoanLimitsConfig.java deleted file mode 100644 index 91d20e6..0000000 --- a/server/loans/core/src/main/java/ee/rsx/kata/bank/loans/domain/limits/LoanLimitsConfig.java +++ /dev/null @@ -1,8 +0,0 @@ -package ee.rsx.kata.bank.loans.domain.limits; - -public record LoanLimitsConfig( - Integer minimumLoanAmount, - Integer maximumLoanAmount, - Integer minimumLoanPeriodMonths, - Integer maximumLoanPeriodMonths -) {} diff --git a/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/domain/limits/LoanLimitsConfig.kt b/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/domain/limits/LoanLimitsConfig.kt new file mode 100644 index 0000000..ae6fe66 --- /dev/null +++ b/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/domain/limits/LoanLimitsConfig.kt @@ -0,0 +1,8 @@ +package ee.rsx.kata.bank.loans.domain.limits + +data class LoanLimitsConfig( + val minimumLoanAmount: Int, + val maximumLoanAmount: Int, + val minimumLoanPeriodMonths: Int, + val maximumLoanPeriodMonths: Int +) diff --git a/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/domain/limits/gateway/LoanConfigGateway.kt b/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/domain/limits/gateway/LoanConfigGateway.kt index 9c8ee2c..a553ee0 100644 --- a/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/domain/limits/gateway/LoanConfigGateway.kt +++ b/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/domain/limits/gateway/LoanConfigGateway.kt @@ -1,8 +1,7 @@ -package ee.rsx.kata.bank.loans.domain.limits.gateway; +package ee.rsx.kata.bank.loans.domain.limits.gateway -import ee.rsx.kata.bank.loans.domain.limits.LoanLimitsConfig; +import ee.rsx.kata.bank.loans.domain.limits.LoanLimitsConfig -public interface LoanConfigGateway { - - LoanLimitsConfig loadLimits(); +fun interface LoanConfigGateway { + fun loadLimits(): LoanLimitsConfig } diff --git a/server/loans/gw-validation/build.gradle b/server/loans/gw-validation/build.gradle index f2ef39a..31fb1e1 100644 --- a/server/loans/gw-validation/build.gradle +++ b/server/loans/gw-validation/build.gradle @@ -1,3 +1,24 @@ +import static org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 + +plugins { + id 'org.jetbrains.kotlin.jvm' version "${kotlinVersion}" +} + +group = 'ee.rsx.kata.bank' +version = '1.0.0-SNAPSHOT' + dependencies { implementation project(":loans-core") } + +compileKotlin { + compilerOptions { + jvmTarget = JVM_17 + } +} + +compileTestKotlin { + compilerOptions { + jvmTarget = JVM_17 + } +} diff --git a/server/loans/gw-validation/src/main/java/ee/rsx/kata/bank/loans/adapter/validation/LoanConfigurationAdapter.java b/server/loans/gw-validation/src/main/java/ee/rsx/kata/bank/loans/adapter/validation/LoanConfigurationAdapter.java deleted file mode 100644 index 8f41b0c..0000000 --- a/server/loans/gw-validation/src/main/java/ee/rsx/kata/bank/loans/adapter/validation/LoanConfigurationAdapter.java +++ /dev/null @@ -1,25 +0,0 @@ -package ee.rsx.kata.bank.loans.adapter.validation; - -import ee.rsx.kata.bank.loans.domain.limits.LoanLimitsConfig; -import ee.rsx.kata.bank.loans.domain.limits.gateway.LoanConfigGateway; -import jakarta.inject.Named; - -@Named -class LoanConfigurationAdapter implements LoanConfigGateway { - - private static final int MINIMUM_LOAN_AMOUNT = 2_000; - private static final int MAXIMUM_LOAN_AMOUNT = 10_000; - private static final int MINIMUM_LOAN_PERIOD_MONTHS = 12; - private static final int MAXIMUM_LOAN_PERIOD_MONTHS = 60; - - @Override - public LoanLimitsConfig loadLimits() { - - return new LoanLimitsConfig( - MINIMUM_LOAN_AMOUNT, - MAXIMUM_LOAN_AMOUNT, - MINIMUM_LOAN_PERIOD_MONTHS, - MAXIMUM_LOAN_PERIOD_MONTHS - ); - } -} diff --git a/server/loans/gw-validation/src/main/kotlin/ee/rsx/kata/bank/loans/adapter/validation/LoanConfigurationAdapter.kt b/server/loans/gw-validation/src/main/kotlin/ee/rsx/kata/bank/loans/adapter/validation/LoanConfigurationAdapter.kt new file mode 100644 index 0000000..7bf3c72 --- /dev/null +++ b/server/loans/gw-validation/src/main/kotlin/ee/rsx/kata/bank/loans/adapter/validation/LoanConfigurationAdapter.kt @@ -0,0 +1,23 @@ +package ee.rsx.kata.bank.loans.adapter.validation + +import ee.rsx.kata.bank.loans.domain.limits.LoanLimitsConfig +import ee.rsx.kata.bank.loans.domain.limits.gateway.LoanConfigGateway +import jakarta.inject.Named + +@Named +internal class LoanConfigurationAdapter : LoanConfigGateway { + + override fun loadLimits() = LoanLimitsConfig( + MINIMUM_LOAN_AMOUNT, + MAXIMUM_LOAN_AMOUNT, + MINIMUM_LOAN_PERIOD_MONTHS, + MAXIMUM_LOAN_PERIOD_MONTHS + ) + + companion object { + private const val MINIMUM_LOAN_AMOUNT = 2000 + private const val MAXIMUM_LOAN_AMOUNT = 10000 + private const val MINIMUM_LOAN_PERIOD_MONTHS = 12 + private const val MAXIMUM_LOAN_PERIOD_MONTHS = 60 + } +} From 1159352cca584c629609ba3adfa84b3fc21cc2db Mon Sep 17 00:00:00 2001 From: Risto Uibo Date: Sun, 23 Jun 2024 18:07:59 +0300 Subject: [PATCH 06/17] Rename .java to .kt --- .../bank/loans/domain/limits/gateway/DetermineEligiblePeriod.kt} | 0 .../rsx/kata/bank/loans/domain/limits/gateway/LoanEligibility.kt} | 0 .../rsx/kata/bank/loans/usecases/LoanEligibilityCalculation.kt} | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename server/loans/core/src/main/{java/ee/rsx/kata/bank/loans/domain/limits/gateway/DetermineEligiblePeriod.java => kotlin/ee/rsx/kata/bank/loans/domain/limits/gateway/DetermineEligiblePeriod.kt} (100%) rename server/loans/core/src/main/{java/ee/rsx/kata/bank/loans/domain/LoanEligibility.java => kotlin/ee/rsx/kata/bank/loans/domain/limits/gateway/LoanEligibility.kt} (100%) rename server/loans/core/src/main/{java/ee/rsx/kata/bank/loans/usecases/LoanEligibilityCalculation.java => kotlin/ee/rsx/kata/bank/loans/usecases/LoanEligibilityCalculation.kt} (100%) diff --git a/server/loans/core/src/main/java/ee/rsx/kata/bank/loans/domain/limits/gateway/DetermineEligiblePeriod.java b/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/domain/limits/gateway/DetermineEligiblePeriod.kt similarity index 100% rename from server/loans/core/src/main/java/ee/rsx/kata/bank/loans/domain/limits/gateway/DetermineEligiblePeriod.java rename to server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/domain/limits/gateway/DetermineEligiblePeriod.kt diff --git a/server/loans/core/src/main/java/ee/rsx/kata/bank/loans/domain/LoanEligibility.java b/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/domain/limits/gateway/LoanEligibility.kt similarity index 100% rename from server/loans/core/src/main/java/ee/rsx/kata/bank/loans/domain/LoanEligibility.java rename to server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/domain/limits/gateway/LoanEligibility.kt diff --git a/server/loans/core/src/main/java/ee/rsx/kata/bank/loans/usecases/LoanEligibilityCalculation.java b/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/usecases/LoanEligibilityCalculation.kt similarity index 100% rename from server/loans/core/src/main/java/ee/rsx/kata/bank/loans/usecases/LoanEligibilityCalculation.java rename to server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/usecases/LoanEligibilityCalculation.kt From edc389923241083a340a9464b8a7b714d3020dfb Mon Sep 17 00:00:00 2001 From: Risto Uibo Date: Sun, 23 Jun 2024 18:07:59 +0300 Subject: [PATCH 07/17] server | port LoanEligibilityCalculation to Kotlin --- server/gradle.properties | 1 + server/loans/core/build.gradle | 2 + .../limits/gateway/DetermineEligiblePeriod.kt | 13 +- .../domain/limits/gateway/LoanEligibility.kt | 15 +- .../usecases/LoanEligibilityCalculation.kt | 256 +++++----- .../LoanEligibilityCalculationTest.java | 457 ----------------- .../LoanEligibilityCalculationTestKotlin.kt | 459 ++++++++++++++++++ 7 files changed, 599 insertions(+), 604 deletions(-) delete mode 100644 server/loans/core/src/test/java/ee/rsx/kata/bank/loans/usecases/LoanEligibilityCalculationTest.java create mode 100644 server/loans/core/src/test/kotlin/ee/rsx/kata/bank/loans/usecases/LoanEligibilityCalculationTestKotlin.kt diff --git a/server/gradle.properties b/server/gradle.properties index 7dd778c..8f2227f 100644 --- a/server/gradle.properties +++ b/server/gradle.properties @@ -15,6 +15,7 @@ lombokPluginVersion=8.6 lombokVersion=1.18.32 mockitoVersion=5.12.0 +mockitoKotlinVersion=5.3.1 springBootVersion=3.3.0 springDependencyManagementVersion=1.1.5 diff --git a/server/loans/core/build.gradle b/server/loans/core/build.gradle index 4ff190b..97bdd4e 100644 --- a/server/loans/core/build.gradle +++ b/server/loans/core/build.gradle @@ -9,6 +9,8 @@ version = '1.0.0-SNAPSHOT' dependencies { implementation project(":loans-api") + + testImplementation "org.mockito.kotlin:mockito-kotlin:${mockitoKotlinVersion}" } compileKotlin { diff --git a/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/domain/limits/gateway/DetermineEligiblePeriod.kt b/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/domain/limits/gateway/DetermineEligiblePeriod.kt index 50761eb..bec747d 100644 --- a/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/domain/limits/gateway/DetermineEligiblePeriod.kt +++ b/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/domain/limits/gateway/DetermineEligiblePeriod.kt @@ -1,11 +1,8 @@ -package ee.rsx.kata.bank.loans.domain.limits.gateway; +package ee.rsx.kata.bank.loans.domain.limits.gateway -import ee.rsx.kata.bank.loans.domain.segment.CreditSegment; +import ee.rsx.kata.bank.loans.domain.segment.CreditSegment +import java.util.* -import java.util.Optional; - -@FunctionalInterface -public interface DetermineEligiblePeriod { - - Optional forLoan(Integer amount, CreditSegment creditSegment); +fun interface DetermineEligiblePeriod { + fun forLoan(amount: Int, creditSegment: CreditSegment): Optional } diff --git a/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/domain/limits/gateway/LoanEligibility.kt b/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/domain/limits/gateway/LoanEligibility.kt index 7e3d84f..33e114b 100644 --- a/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/domain/limits/gateway/LoanEligibility.kt +++ b/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/domain/limits/gateway/LoanEligibility.kt @@ -1,10 +1,9 @@ -package ee.rsx.kata.bank.loans.domain; +package ee.rsx.kata.bank.loans.domain.limits.gateway -import ee.rsx.kata.bank.loans.eligibility.LoanEligibilityStatus; -import jakarta.annotation.Nullable; +import ee.rsx.kata.bank.loans.eligibility.LoanEligibilityStatus -public record LoanEligibility( - LoanEligibilityStatus status, - @Nullable Integer eligibleAmount, - @Nullable Integer eligiblePeriod -) {} +data class LoanEligibility( + val status: LoanEligibilityStatus, + val eligibleAmount: Int? = null, + val eligiblePeriod: Int? = null +) diff --git a/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/usecases/LoanEligibilityCalculation.kt b/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/usecases/LoanEligibilityCalculation.kt index 52672e8..9d13a11 100644 --- a/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/usecases/LoanEligibilityCalculation.kt +++ b/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/usecases/LoanEligibilityCalculation.kt @@ -1,160 +1,154 @@ -package ee.rsx.kata.bank.loans.usecases; - -import ee.rsx.kata.bank.loans.domain.LoanEligibility; -import ee.rsx.kata.bank.loans.domain.limits.gateway.DetermineEligiblePeriod; -import ee.rsx.kata.bank.loans.domain.segment.CreditSegment; -import ee.rsx.kata.bank.loans.domain.segment.gateway.FindCreditSegment; -import ee.rsx.kata.bank.loans.domain.ssn.SocialSecurityNumber; -import ee.rsx.kata.bank.loans.eligibility.CalculateLoanEligibility; -import ee.rsx.kata.bank.loans.eligibility.LoanEligibilityRequestDTO; -import ee.rsx.kata.bank.loans.eligibility.LoanEligibilityResultDTO; -import ee.rsx.kata.bank.loans.eligibility.LoanEligibilityStatus; -import ee.rsx.kata.bank.loans.validation.limits.LoadValidationLimits; -import ee.rsx.kata.bank.loans.validation.limits.ValidationLimitsDTO; -import ee.rsx.kata.bank.loans.validation.ssn.ValidateSocialSecurityNumber; -import jakarta.annotation.Nullable; -import jakarta.inject.Named; -import lombok.RequiredArgsConstructor; - -import java.util.List; -import java.util.Optional; -import java.util.stream.Stream; - -import static ee.rsx.kata.bank.loans.eligibility.LoanEligibilityStatus.*; -import static ee.rsx.kata.bank.loans.validation.ssn.ValidationStatus.OK; -import static java.util.Optional.*; -import static org.apache.commons.collections4.CollectionUtils.isEmpty; +package ee.rsx.kata.bank.loans.usecases + +import ee.rsx.kata.bank.loans.domain.limits.gateway.DetermineEligiblePeriod +import ee.rsx.kata.bank.loans.domain.limits.gateway.LoanEligibility +import ee.rsx.kata.bank.loans.domain.segment.CreditSegment +import ee.rsx.kata.bank.loans.domain.segment.gateway.FindCreditSegment +import ee.rsx.kata.bank.loans.domain.ssn.SocialSecurityNumber +import ee.rsx.kata.bank.loans.eligibility.CalculateLoanEligibility +import ee.rsx.kata.bank.loans.eligibility.LoanEligibilityRequestDTO +import ee.rsx.kata.bank.loans.eligibility.LoanEligibilityResultDTO +import ee.rsx.kata.bank.loans.eligibility.LoanEligibilityStatus +import ee.rsx.kata.bank.loans.eligibility.LoanEligibilityStatus.APPROVED +import ee.rsx.kata.bank.loans.eligibility.LoanEligibilityStatus.DENIED +import ee.rsx.kata.bank.loans.eligibility.LoanEligibilityStatus.INVALID +import ee.rsx.kata.bank.loans.validation.limits.LoadValidationLimits +import ee.rsx.kata.bank.loans.validation.limits.ValidationLimitsDTO +import ee.rsx.kata.bank.loans.validation.ssn.ValidateSocialSecurityNumber +import ee.rsx.kata.bank.loans.validation.ssn.ValidationStatus.OK +import jakarta.annotation.Nullable +import jakarta.inject.Named +import org.apache.commons.collections4.CollectionUtils +import java.util.* +import java.util.stream.Stream +import kotlin.math.min @Named -@RequiredArgsConstructor -class LoanEligibilityCalculation implements CalculateLoanEligibility { - - private final ValidateSocialSecurityNumber validateSocialSecurityNumber; - private final LoadValidationLimits loadValidationLimits; - private final FindCreditSegment findCreditSegment; - private final DetermineEligiblePeriod determineEligiblePeriod; - - @Override - public LoanEligibilityResultDTO on(LoanEligibilityRequestDTO eligibilityRequest) { - var limits = loadValidationLimits.invoke(); - List validationErrors = validate(eligibilityRequest, limits); - - var eligibility = isEmpty(validationErrors) - ? calculateEligibility(eligibilityRequest, limits) - : new LoanEligibility(INVALID, null, null); - - return new LoanEligibilityResultDTO( - eligibility.status(), +internal class LoanEligibilityCalculation( + private val validateSocialSecurityNumber: ValidateSocialSecurityNumber, + private val loadValidationLimits: LoadValidationLimits, + private val findCreditSegment: FindCreditSegment, + private val determineEligiblePeriod: DetermineEligiblePeriod +) : CalculateLoanEligibility { + + override fun on(eligibilityRequest: LoanEligibilityRequestDTO): LoanEligibilityResultDTO { + val limits = loadValidationLimits.invoke() + val validationErrors = validate(eligibilityRequest, limits) + + val (status, eligibleAmount, eligiblePeriod) = + if (CollectionUtils.isEmpty(validationErrors)) calculateEligibility( + eligibilityRequest, + limits + ) else + LoanEligibility(INVALID, null, null) + + return LoanEligibilityResultDTO( + status, validationErrors, - eligibilityRequest.ssn(), - eligibilityRequest.loanAmount(), - eligibilityRequest.loanPeriodMonths(), - eligibility.eligibleAmount(), - eligibility.eligiblePeriod() - ); + eligibilityRequest.ssn, + eligibilityRequest.loanAmount, + eligibilityRequest.loanPeriodMonths, + eligibleAmount, + eligiblePeriod + ) } - private @Nullable List validate( - LoanEligibilityRequestDTO eligibilityRequest, - ValidationLimitsDTO limits - ) { - List errors = Stream.of( - checkForSsnErrorIn(eligibilityRequest), - checkForAmountErrorIn(eligibilityRequest, limits), - checkForPeriodErrorIn(eligibilityRequest, limits) - ) - .flatMap(Optional::stream) - .toList(); - - - return errors.isEmpty() ? null : errors; + @Nullable + private fun validate( + eligibilityRequest: LoanEligibilityRequestDTO, + limits: ValidationLimitsDTO + ): List? { + val errors = Stream.of( + checkForSsnErrorIn(eligibilityRequest), + checkForAmountErrorIn(eligibilityRequest, limits), + checkForPeriodErrorIn(eligibilityRequest, limits) + ) + .flatMap { it.stream() } + .toList() + + return if (errors.isEmpty()) null else errors } - private Optional checkForSsnErrorIn(LoanEligibilityRequestDTO request) { - var ssnValidity = validateSocialSecurityNumber.on(request.ssn()).status(); + private fun checkForSsnErrorIn(request: LoanEligibilityRequestDTO): Optional { + val ssnValidity = validateSocialSecurityNumber.on(request.ssn).status - return ssnValidity == OK ? empty() : of("SSN is not valid"); + return if (ssnValidity == OK) + Optional.empty() + else + Optional.of("SSN is not valid") } - private Optional checkForAmountErrorIn(LoanEligibilityRequestDTO request, ValidationLimitsDTO limits) { - var amount = request.loanAmount(); - if (amount < limits.minimumLoanAmount()) { - return of("Loan amount is less than minimum required"); + private fun checkForAmountErrorIn(request: LoanEligibilityRequestDTO, limits: ValidationLimitsDTO): Optional { + val amount = request.loanAmount + if (amount < limits.minimumLoanAmount) { + return Optional.of("Loan amount is less than minimum required") } - if (amount > limits.maximumLoanAmount()) { - return of("Loan amount is more than maximum allowed"); - } - return empty(); + return if (amount > limits.maximumLoanAmount) { + Optional.of("Loan amount is more than maximum allowed") + } else Optional.empty() } - private Optional checkForPeriodErrorIn(LoanEligibilityRequestDTO request, ValidationLimitsDTO limits) { - var period = request.loanPeriodMonths(); - if (period < limits.minimumLoanPeriodMonths()) { - return of("Loan period is less than minimum required"); - } - if (period > limits.maximumLoanPeriodMonths()) { - return of("Loan period is more than maximum allowed"); + private fun checkForPeriodErrorIn(request: LoanEligibilityRequestDTO, limits: ValidationLimitsDTO): Optional { + val period = request.loanPeriodMonths + if (period < limits.minimumLoanPeriodMonths) { + return Optional.of("Loan period is less than minimum required") } - return empty(); + return if (period > limits.maximumLoanPeriodMonths) { + Optional.of("Loan period is more than maximum allowed") + } else Optional.empty() } - private LoanEligibility calculateEligibility(LoanEligibilityRequestDTO request, ValidationLimitsDTO limits) { - var ssn = new SocialSecurityNumber(request.ssn()); - Optional creditSegment = findCreditSegment.forPerson(ssn); + private fun calculateEligibility(request: LoanEligibilityRequestDTO, limits: ValidationLimitsDTO): LoanEligibility { + val ssn = SocialSecurityNumber(request.ssn) + val creditSegment = findCreditSegment.forPerson(ssn) return creditSegment - .map(segment -> { - var status = determineEligibilityStatusFor(request, segment); - - return determineEligibleAmountFor(limits, segment, request.loanPeriodMonths()) - .map(eligibleAmount -> new LoanEligibility(status, eligibleAmount, null)) - .orElseGet( - () -> recalculateEligibilityFor(request, limits, segment, status) - ); - }) - .orElseGet( - () -> new LoanEligibility(DENIED, null, null) - ); + .map { + val status = determineEligibilityStatusFor(request, it) + + determineEligibleAmountFor(limits, it, request.loanPeriodMonths) + .map { eligibleAmount -> LoanEligibility(status, eligibleAmount) } + .orElseGet { recalculateEligibilityFor(request, limits, it, status) } + } + .orElseGet { LoanEligibility(status = DENIED) } } - private LoanEligibility recalculateEligibilityFor( - LoanEligibilityRequestDTO request, - ValidationLimitsDTO limits, - CreditSegment segment, - LoanEligibilityStatus status - ) { - if (segment.isDebt()) { - return new LoanEligibility(status, null, null); + private fun recalculateEligibilityFor( + request: LoanEligibilityRequestDTO, + limits: ValidationLimitsDTO, + segment: CreditSegment, + status: LoanEligibilityStatus + ): LoanEligibility { + if (segment.isDebt) { + return LoanEligibility(status) } - Optional newPeriod = - determineEligiblePeriod.forLoan(request.loanAmount(), segment) - .filter(period -> - limits.minimumLoanPeriodMonths() <= period && period <= limits.maximumLoanPeriodMonths() - ); - Optional newAmount = - newPeriod.flatMap(period -> determineEligibleAmountFor(limits, segment, period)); + val newPeriod = determineEligiblePeriod.forLoan(request.loanAmount, segment) + .filter { limits.minimumLoanPeriodMonths <= it && it <= limits.maximumLoanPeriodMonths } - return new LoanEligibility(status, newAmount.orElse(null), newPeriod.orElse(null)); - } + val newAmount = newPeriod.flatMap { determineEligibleAmountFor(limits, segment, it) } - private LoanEligibilityStatus determineEligibilityStatusFor( - LoanEligibilityRequestDTO request, CreditSegment creditSegment - ) { - double creditScore = - (double) creditSegment.getCreditModifier() / request.loanAmount() * request.loanPeriodMonths(); - - return (!creditSegment.isDebt() && creditScore > 1) ? APPROVED : DENIED; + return LoanEligibility(status, newAmount.orElse(null), newPeriod.orElse(null)) } - private Optional determineEligibleAmountFor( - ValidationLimitsDTO limits, CreditSegment creditSegment, Integer loanPeriodMonths - ) { - int eligibleAmount = Math.min( - limits.maximumLoanAmount(), - creditSegment.getCreditModifier() * loanPeriodMonths - 1 - ); + private fun determineEligibilityStatusFor( + request: LoanEligibilityRequestDTO, creditSegment: CreditSegment + ): LoanEligibilityStatus { + val creditScore = creditSegment.creditModifier.toDouble() / request.loanAmount * request.loanPeriodMonths + return if (!creditSegment.isDebt && creditScore > 1) APPROVED else DENIED + } - return eligibleAmount >= limits.minimumLoanAmount() ? of(eligibleAmount) : empty(); + private fun determineEligibleAmountFor( + limits: ValidationLimitsDTO, creditSegment: CreditSegment, loanPeriodMonths: Int + ): Optional { + val eligibleAmount = min( + limits.maximumLoanAmount.toDouble(), + (creditSegment.creditModifier * loanPeriodMonths - 1).toDouble() + ).toInt() + + return if (eligibleAmount >= limits.minimumLoanAmount) + Optional.of(eligibleAmount) + else + Optional.empty() } } diff --git a/server/loans/core/src/test/java/ee/rsx/kata/bank/loans/usecases/LoanEligibilityCalculationTest.java b/server/loans/core/src/test/java/ee/rsx/kata/bank/loans/usecases/LoanEligibilityCalculationTest.java deleted file mode 100644 index c9d8f37..0000000 --- a/server/loans/core/src/test/java/ee/rsx/kata/bank/loans/usecases/LoanEligibilityCalculationTest.java +++ /dev/null @@ -1,457 +0,0 @@ -package ee.rsx.kata.bank.loans.usecases; - -import ee.rsx.kata.bank.loans.domain.limits.gateway.DetermineEligiblePeriod; -import ee.rsx.kata.bank.loans.domain.segment.CreditSegment; -import ee.rsx.kata.bank.loans.domain.segment.CreditSegmentType; -import ee.rsx.kata.bank.loans.domain.segment.gateway.FindCreditSegment; -import ee.rsx.kata.bank.loans.domain.ssn.SocialSecurityNumber; -import ee.rsx.kata.bank.loans.eligibility.LoanEligibilityRequestDTO; -import ee.rsx.kata.bank.loans.eligibility.LoanEligibilityResultDTO; -import ee.rsx.kata.bank.loans.eligibility.LoanEligibilityStatus; -import ee.rsx.kata.bank.loans.validation.limits.LoadValidationLimits; -import ee.rsx.kata.bank.loans.validation.limits.ValidationLimitsDTO; -import ee.rsx.kata.bank.loans.validation.ssn.SsnValidationResultDTO; -import ee.rsx.kata.bank.loans.validation.ssn.ValidateSocialSecurityNumber; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.mockito.stubbing.Answer; - -import java.util.List; -import java.util.Optional; - -import static ee.rsx.kata.bank.loans.domain.segment.CreditSegmentType.*; -import static ee.rsx.kata.bank.loans.eligibility.LoanEligibilityStatus.*; -import static ee.rsx.kata.bank.loans.validation.ssn.SsnValidationResultDTO.*; -import static java.util.Arrays.stream; -import static java.util.Optional.empty; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.*; -import static org.mockito.Mockito.*; - -@DisplayName("Loan eligibility calculation") -@ExtendWith(MockitoExtension.class) -class LoanEligibilityCalculationTest { - - private static final Integer MINIMUM_REQUIRED_LOAN_AMOUNT = 2_000; - private static final Integer MAXIMUM_ALLOWED_LOAN_AMOUNT = 10_000; - - private static final Integer MINIMUM_REQUIRED_LOAN_PERIOD = 12; - private static final Integer MAXIMUM_ALLOWED_LOAN_PERIOD = 60; - - private static final ValidationLimitsDTO TEST_VALIDATION_LIMITS = - new ValidationLimitsDTO( - MINIMUM_REQUIRED_LOAN_AMOUNT, - MAXIMUM_ALLOWED_LOAN_AMOUNT, - MINIMUM_REQUIRED_LOAN_PERIOD, - MAXIMUM_ALLOWED_LOAN_PERIOD - ); - - private static Answer okResultWithProvidedSsn() { - return methodCall -> { - String providedSsn = methodCall.getArgument(0); - return okResultWith(providedSsn); - }; - } - - @Mock - private ValidateSocialSecurityNumber validateSocialSecurityNumber; - - @Mock - private LoadValidationLimits loadValidationLimits; - - @Mock - private FindCreditSegment findCreditSegment; - - @Mock - private DetermineEligiblePeriod determineEligiblePeriod; - - @InjectMocks - private LoanEligibilityCalculation calculateLoanEligibility; - - @BeforeEach - void setup() { - when(validateSocialSecurityNumber.on(anyString())) - .thenAnswer(okResultWithProvidedSsn()); - - when(loadValidationLimits.invoke()) - .thenReturn(TEST_VALIDATION_LIMITS); - - when(findCreditSegment.forPerson(any())) - .thenReturn(empty()); - } - - @Nested - @DisplayName("returns DENIED result") - class ReturnsDeniedResult { - - @Test - @DisplayName("when credit segment for given person is not found") - void when_creditSegmentForGivenPerson_isNotFound() { - var validRequest = testRequest().create(); - whenCreditSegmentNotFoundForPerson(DEFAULT_SSN); - - var result = calculateLoanEligibility.on(validRequest); - - assertThat(result) - .isEqualTo(expectedResult(DENIED).create()); - } - - @Test - @DisplayName("when credit segment found, but its credit modifier too low for requested loan") - void when_creditSegmentFound_butCreditModifierTooLowForRequestedLoan() { - var validRequest = testRequest().create(); - int tooLowCreditModifier = 100; - whenCreditSegmentFoundForPerson(DEFAULT_SSN, SEGMENT_3, tooLowCreditModifier); - - var result = calculateLoanEligibility.on(validRequest); - - assertThat(result) - .isEqualTo(expectedResult(DENIED).eligibleLoanAmount(3599).create()); - } - - @Test - @DisplayName("with no eligible amount when it would be less than minimum loan amount") - void withNoEligibleAmount_whenItWouldBe_lessThan_MinimumLoanAmount() { - var validRequest = testRequest().amount(2000).period(20).create(); - whenCreditSegmentFoundForPerson(DEFAULT_SSN, SEGMENT_3, 100); - - var result = calculateLoanEligibility.on(validRequest); - - assertThat(result) - .isEqualTo( - expectedResult(DENIED) - .amount(2000) - .period(20) - .eligibleLoanAmount(null) - .create() - ); - } - - @Test - @DisplayName("with new eligible period (received from gateway) and amount, when no amount available for given period") - void withNewEligiblePeriodAndAmount_whenNoAmountAvailableForGivenPeriod() { - int shortPeriodOneYear = 12; - var validRequest = testRequest().amount(5000).period(shortPeriodOneYear).create(); - int creditModifier = 100; - var segment = whenCreditSegmentFoundForPerson(DEFAULT_SSN, SEGMENT_3, creditModifier); - var newEligiblePeriod = whenNewEligiblePeriodDeterminedFor(validRequest, segment, 51); - - var result = calculateLoanEligibility.on(validRequest); - - int expectedNewEligibleLoanAmount = newEligiblePeriod * creditModifier - 1; - assertThat(result) - .isEqualTo( - expectedResult(DENIED) - .amount(5000) - .period(shortPeriodOneYear) - .eligibleLoanAmount(expectedNewEligibleLoanAmount) - .eligibleLoanPeriod(newEligiblePeriod) - .create() - ); - } - - @Test - @DisplayName("with no new eligible period (received from gateway) nor amount, when new determined period above maximum") - void withNoNewEligiblePeriod_norAmount_whenNewDeterminedPeriodAboveMaximum() { - int shortPeriodOneYear = 12; - var validRequest = testRequest().amount(9000).period(shortPeriodOneYear).create(); - int creditModifier = 100; - var segment = whenCreditSegmentFoundForPerson(DEFAULT_SSN, SEGMENT_3, creditModifier); - whenNewEligiblePeriodDeterminedFor(validRequest, segment, 91); - - var result = calculateLoanEligibility.on(validRequest); - - assertThat(result) - .isEqualTo( - expectedResult(DENIED) - .amount(9000) - .period(shortPeriodOneYear) - .eligibleLoanAmount(null) - .eligibleLoanPeriod(null) - .create() - ); - } - - private int whenNewEligiblePeriodDeterminedFor( - LoanEligibilityRequestDTO validRequest, CreditSegment segment, Integer newEligiblePeriod - ) { - when(determineEligiblePeriod.forLoan(validRequest.loanAmount(), segment)) - .thenReturn(Optional.of(newEligiblePeriod)); - return newEligiblePeriod; - } - } - - @Nested - @DisplayName("returns APPROVED result") - class ReturnsApprovedResult { - - @Test - @DisplayName("along with provided valid eligibility request data") - void with_providedValidEligibilityRequestData() { - var validRequest = testRequest().create(); - whenCreditSegmentFoundForPerson(DEFAULT_SSN, SEGMENT_3, 1000); - - var result = calculateLoanEligibility.on(validRequest); - - assertThat(result) - .isEqualTo(expectedResult(APPROVED).eligibleLoanAmount(10_000).create()); - } - - @Test - @DisplayName("when low credit segment found, but period is long enough (46 months)") - void when_lowCreditSegmentFound_butPeriodIsLongEnough_fortySixMonths() { - int fortySixMonths = 46; - var validRequest = testRequest().period(fortySixMonths).create(); - whenCreditSegmentFoundForPerson(DEFAULT_SSN, SEGMENT_1, 100); - - var result = calculateLoanEligibility.on(validRequest); - - assertThat(result) - .isEqualTo( - expectedResult(APPROVED) - .eligibleLoanAmount(4599) - .period(fortySixMonths) - .create() - ); - } - - @Test - @DisplayName("when low credit segment found, but amount is small enough") - void when_lowCreditSegmentFound_butAmountIsSmallEnough() { - int smallAmount = 2000; - var validRequest = testRequest().amount(smallAmount).create(); - whenCreditSegmentFoundForPerson(DEFAULT_SSN, SEGMENT_1, 60); - - var result = calculateLoanEligibility.on(validRequest); - - assertThat(result) - .isEqualTo( - expectedResult(APPROVED).eligibleLoanAmount(2159).amount(smallAmount).create() - ); - } - } - - @Nested - @DisplayName("returns INVALID result") - class ReturnsInvalidResult { - - @BeforeEach - void setup() { - reset(findCreditSegment); - } - - @Test - @DisplayName("with SSN error message, when invalid SSN provided") - void with_ssnErrorMessage_whenInvalidSsnProvided() { - var invalidSsn = "49002010966"; - whenSsnValidationFailsFor(invalidSsn); - var invalidRequest = testRequest().ssn(invalidSsn).create(); - - var result = calculateLoanEligibility.on(invalidRequest); - - assertThat(result).isEqualTo( - expectedResult(INVALID).ssn(invalidSsn).errors("SSN is not valid").create() - ); - } - - private void whenSsnValidationFailsFor(String invalidSsn) { - when(validateSocialSecurityNumber.on(invalidSsn)).thenReturn(invalidResultWith(invalidSsn)); - } - - @Test - @DisplayName("with error message on too small loan amount, when too small loan amount provided") - void with_loanAmountTooSmallMessage_whenTooSmallLoanAmountProvided() { - var tooSmallLoanAmount = MINIMUM_REQUIRED_LOAN_AMOUNT - 1; - var invalidRequest = testRequest().amount(tooSmallLoanAmount).create(); - - var result = calculateLoanEligibility.on(invalidRequest); - - assertThat(result).isEqualTo( - expectedResult(INVALID).amount(tooSmallLoanAmount).errors("Loan amount is less than minimum required").create() - ); - } - - @Test - @DisplayName("with error message on too large loan amount, when too large loan amount provided") - void with_loanAmountTooLargeMessage_whenTooLargeLoanAmountProvided() { - var tooLargeLoanAmount = MAXIMUM_ALLOWED_LOAN_AMOUNT + 1; - var invalidRequest = testRequest().amount(tooLargeLoanAmount).create(); - - var result = calculateLoanEligibility.on(invalidRequest); - - assertThat(result) - .isEqualTo( - expectedResult(INVALID).amount(tooLargeLoanAmount).errors("Loan amount is more than maximum allowed").create() - ); - } - - @Test - @DisplayName("with error message on too small loan period, when too small loan period provided") - void with_loanPeriodTooSmallMessage_whenTooSmallLoanPeriodProvided() { - var tooSmallLoanPeriod = MINIMUM_REQUIRED_LOAN_PERIOD - 1; - var invalidRequest = testRequest().period(tooSmallLoanPeriod).create(); - - var result = calculateLoanEligibility.on(invalidRequest); - - assertThat(result) - .isEqualTo( - expectedResult(INVALID).period(tooSmallLoanPeriod).errors("Loan period is less than minimum required").create() - ); - } - - @Test - @DisplayName("with error message on too large loan period, when too big loan period provided") - void with_loanPeriodTooLargeMessage_whenTooLargeLoanPeriodProvided() { - var tooLargeLoanPeriod = MAXIMUM_ALLOWED_LOAN_PERIOD + 1; - var invalidRequest = testRequest().period(tooLargeLoanPeriod).create(); - - var result = calculateLoanEligibility.on(invalidRequest); - - assertThat(result) - .isEqualTo( - expectedResult(INVALID).period(tooLargeLoanPeriod).errors("Loan period is more than maximum allowed").create() - ); - } - - @Test - @DisplayName("with several error messages, when eligibility result contains several invalid details") - void with_severalErrorMessages_whenEligibilityResult_contains_severalInvalidDetails() { - var invalidSsn = "49002010966"; - whenSsnValidationFailsFor(invalidSsn); - var tooSmallLoanAmount = MINIMUM_REQUIRED_LOAN_AMOUNT - 1; - var tooLargeLoanPeriod = MAXIMUM_ALLOWED_LOAN_PERIOD + 1; - var invalidRequest = - testRequest().ssn(invalidSsn).amount(tooSmallLoanAmount).period(tooLargeLoanPeriod).create(); - - var result = calculateLoanEligibility.on(invalidRequest); - - assertThat(result) - .isEqualTo( - expectedResult(INVALID) - .ssn(invalidSsn) - .amount(tooSmallLoanAmount) - .period(tooLargeLoanPeriod) - .errors( - "SSN is not valid", - "Loan amount is less than minimum required", - "Loan period is more than maximum allowed" - ) - .create() - ); - } - } - - private CreditSegment whenCreditSegmentFoundForPerson(String withSsn, CreditSegmentType segmentType, int creditModifier) { - var ssn = new SocialSecurityNumber(withSsn); - var foundSegment = new CreditSegment(ssn, segmentType, creditModifier); - when(findCreditSegment.forPerson(ssn)) - .thenReturn(Optional.of(foundSegment)); - return foundSegment; - } - - private void whenCreditSegmentNotFoundForPerson(String withSsn) { - var ssn = new SocialSecurityNumber(withSsn); - when(findCreditSegment.forPerson(ssn)) - .thenReturn(empty()); - } - - private static final String DEFAULT_SSN = "49002010965"; - private static final Integer DEFAULT_AMOUNT = 4500; - private static final Integer DEFAULT_PERIOD = 36; - - private static DefaultTestRequest testRequest() { - return new DefaultTestRequest(); - } - - static class DefaultTestRequest { - - private String ssn = DEFAULT_SSN; - private Integer amount = DEFAULT_AMOUNT; - private Integer period = DEFAULT_PERIOD; - - public DefaultTestRequest ssn(String value) { - this.ssn = value; - return this; - } - - public DefaultTestRequest amount(Integer value) { - this.amount = value; - return this; - } - - public DefaultTestRequest period(Integer value) { - this.period = value; - return this; - } - - public LoanEligibilityRequestDTO create() { - return new LoanEligibilityRequestDTO(this.ssn, this.amount, this.period); - } - } - - private static DefaultTestResult expectedResult() { - return new DefaultTestResult(); - } - - private static DefaultTestResult expectedResult(LoanEligibilityStatus withStatus) { - return expectedResult().status(withStatus); - } - - static class DefaultTestResult { - - private LoanEligibilityStatus status = APPROVED; - private List errors = null; - private String ssn = DEFAULT_SSN; - private Integer amount = DEFAULT_AMOUNT; - private Integer period = DEFAULT_PERIOD; - private Integer eligibleLoanAmount = null; - private Integer eligibleLoanPeriod = null; - - public DefaultTestResult status(LoanEligibilityStatus value) { - this.status = value; - return this; - } - - public DefaultTestResult errors(String... errors) { - this.errors = stream(errors).toList(); - return this; - } - - public DefaultTestResult ssn(String value) { - this.ssn = value; - return this; - } - - public DefaultTestResult amount(Integer value) { - this.amount = value; - return this; - } - - public DefaultTestResult period(Integer value) { - this.period = value; - return this; - } - - public DefaultTestResult eligibleLoanAmount(Integer value) { - this.eligibleLoanAmount = value; - return this; - } - - public DefaultTestResult eligibleLoanPeriod(Integer value) { - this.eligibleLoanPeriod = value; - return this; - } - - public LoanEligibilityResultDTO create() { - return new LoanEligibilityResultDTO( - status, errors, ssn, amount, period, eligibleLoanAmount, eligibleLoanPeriod - ); - } - } -} diff --git a/server/loans/core/src/test/kotlin/ee/rsx/kata/bank/loans/usecases/LoanEligibilityCalculationTestKotlin.kt b/server/loans/core/src/test/kotlin/ee/rsx/kata/bank/loans/usecases/LoanEligibilityCalculationTestKotlin.kt new file mode 100644 index 0000000..34bc861 --- /dev/null +++ b/server/loans/core/src/test/kotlin/ee/rsx/kata/bank/loans/usecases/LoanEligibilityCalculationTestKotlin.kt @@ -0,0 +1,459 @@ +package ee.rsx.kata.bank.loans.usecases + +import ee.rsx.kata.bank.loans.domain.limits.gateway.DetermineEligiblePeriod +import ee.rsx.kata.bank.loans.domain.segment.CreditSegment +import ee.rsx.kata.bank.loans.domain.segment.CreditSegmentType +import ee.rsx.kata.bank.loans.domain.segment.CreditSegmentType.SEGMENT_1 +import ee.rsx.kata.bank.loans.domain.segment.CreditSegmentType.SEGMENT_3 +import ee.rsx.kata.bank.loans.domain.segment.gateway.FindCreditSegment +import ee.rsx.kata.bank.loans.domain.ssn.SocialSecurityNumber +import ee.rsx.kata.bank.loans.eligibility.LoanEligibilityRequestDTO +import ee.rsx.kata.bank.loans.eligibility.LoanEligibilityResultDTO +import ee.rsx.kata.bank.loans.eligibility.LoanEligibilityStatus +import ee.rsx.kata.bank.loans.eligibility.LoanEligibilityStatus.APPROVED +import ee.rsx.kata.bank.loans.eligibility.LoanEligibilityStatus.DENIED +import ee.rsx.kata.bank.loans.eligibility.LoanEligibilityStatus.INVALID +import ee.rsx.kata.bank.loans.validation.limits.LoadValidationLimits +import ee.rsx.kata.bank.loans.validation.limits.ValidationLimitsDTO +import ee.rsx.kata.bank.loans.validation.ssn.SsnValidationResultDTO.invalidResultWith +import ee.rsx.kata.bank.loans.validation.ssn.SsnValidationResultDTO.okResultWith +import ee.rsx.kata.bank.loans.validation.ssn.ValidateSocialSecurityNumber +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.InjectMocks +import org.mockito.Mock +import org.mockito.Mockito.reset +import org.mockito.Mockito.`when` +import org.mockito.invocation.InvocationOnMock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.kotlin.any +import org.mockito.kotlin.whenever +import org.mockito.stubbing.Answer +import java.util.* + +@DisplayName("Loan eligibility calculation") +@ExtendWith(MockitoExtension::class) +internal class LoanEligibilityCalculationTestKotlin { + + @Mock + private lateinit var validateSocialSecurityNumber: ValidateSocialSecurityNumber + + @Mock + private lateinit var loadValidationLimits: LoadValidationLimits + + @Mock + private lateinit var findCreditSegment: FindCreditSegment + + @Mock + private lateinit var determineEligiblePeriod: DetermineEligiblePeriod + + @InjectMocks + private lateinit var calculateLoanEligibility: LoanEligibilityCalculation + + @BeforeEach + fun setup() { + whenever(validateSocialSecurityNumber.on(any())) + .thenAnswer(okResultWithProvidedSsn()) + whenever(loadValidationLimits.invoke()) + .thenReturn(TEST_VALIDATION_LIMITS) + whenever(findCreditSegment.forPerson(any())) + .thenReturn(Optional.empty()) + } + + @Nested + @DisplayName("returns DENIED result") + internal inner class ReturnsDeniedResult { + + @Test + fun `when credit segment for given person is not found`() { + val validRequest = testRequest().create() + whenCreditSegmentNotFoundForPerson(DEFAULT_SSN) + + val result = calculateLoanEligibility.on(validRequest) + + assertThat(result) + .isEqualTo( + expectedResult(withStatus = DENIED).create() + ) + } + + @Test + fun `when credit segment found, but its credit modifier too low for requested loan`() { + val validRequest = testRequest().create() + val tooLowCreditModifier = 100 + whenCreditSegmentFoundForPerson(DEFAULT_SSN, SEGMENT_3, tooLowCreditModifier) + + val result = calculateLoanEligibility.on(validRequest) + + assertThat(result) + .isEqualTo( + expectedResult(withStatus = DENIED) + .eligibleLoanAmount(3599) + .create() + ) + } + + @Test + fun `with no eligible amount when it would be less than minimum loan amount`() { + val validRequest = testRequest().amount(2000).period(20).create() + whenCreditSegmentFoundForPerson(DEFAULT_SSN, SEGMENT_3, 100) + + val result = calculateLoanEligibility.on(validRequest) + + assertThat(result) + .isEqualTo( + expectedResult(withStatus = DENIED) + .amount(2000) + .period(20) + .eligibleLoanAmount(null) + .create() + ) + } + + @Test + fun `with new eligible period (received from gateway) and amount, when no amount available for given period`() { + val shortPeriodOneYear = 12 + val validRequest = testRequest().amount(5000).period(shortPeriodOneYear).create() + val creditModifier = 100 + val segment = whenCreditSegmentFoundForPerson(DEFAULT_SSN, SEGMENT_3, creditModifier) + val newEligiblePeriod = whenNewEligiblePeriodDeterminedFor(validRequest, segment, 51) + + val result = calculateLoanEligibility.on(validRequest) + + val expectedNewEligibleLoanAmount = newEligiblePeriod * creditModifier - 1 + assertThat(result) + .isEqualTo( + expectedResult(withStatus = DENIED) + .amount(5000) + .period(shortPeriodOneYear) + .eligibleLoanAmount(expectedNewEligibleLoanAmount) + .eligibleLoanPeriod(newEligiblePeriod) + .create() + ) + } + + @Test + @DisplayName("with no new eligible period (received from gateway) nor amount, when new determined period above maximum") + fun `with no new eligible period (received from gateway) nor amount, when new determined period above maximum`() { + val shortPeriodOneYear = 12 + val validRequest = testRequest().amount(9000).period(shortPeriodOneYear).create() + val creditModifier = 100 + val segment = whenCreditSegmentFoundForPerson(DEFAULT_SSN, SEGMENT_3, creditModifier) + whenNewEligiblePeriodDeterminedFor(validRequest, segment, 91) + + val result = calculateLoanEligibility.on(validRequest) + + assertThat(result) + .isEqualTo( + expectedResult(withStatus = DENIED) + .amount(9000) + .period(shortPeriodOneYear) + .eligibleLoanAmount(null) + .eligibleLoanPeriod(null) + .create() + ) + } + + private fun whenNewEligiblePeriodDeterminedFor( + validRequest: LoanEligibilityRequestDTO, segment: CreditSegment, newEligiblePeriod: Int + ): Int { + `when`(determineEligiblePeriod.forLoan(validRequest.loanAmount, segment)) + .thenReturn(Optional.of(newEligiblePeriod)) + return newEligiblePeriod + } + } + + @Nested + @DisplayName("returns APPROVED result") + internal inner class ReturnsApprovedResult { + + @Test + fun `along with provided valid eligibility request data`() { + val validRequest = testRequest().create() + whenCreditSegmentFoundForPerson(DEFAULT_SSN, SEGMENT_3, 1000) + + val result = calculateLoanEligibility.on(validRequest) + + assertThat(result) + .isEqualTo( + expectedResult(withStatus = APPROVED) + .eligibleLoanAmount(10000) + .create() + ) + } + + @Test + fun `when low credit segment found, but period is long enough (46 months)`() { + val fortySixMonths = 46 + val validRequest = testRequest().period(fortySixMonths).create() + + whenCreditSegmentFoundForPerson(DEFAULT_SSN, SEGMENT_1, 100) + + val result = calculateLoanEligibility.on(validRequest) + assertThat(result) + .isEqualTo( + expectedResult(APPROVED) + .eligibleLoanAmount(4599) + .period(fortySixMonths) + .create() + ) + } + + @Test + fun `when low credit segment found, but amount is small enough`() { + val smallAmount = 2000 + val validRequest = testRequest().amount(smallAmount).create() + whenCreditSegmentFoundForPerson(DEFAULT_SSN, SEGMENT_1, 60) + + val result = calculateLoanEligibility.on(validRequest) + + assertThat(result) + .isEqualTo( + expectedResult(withStatus = APPROVED) + .eligibleLoanAmount(2159) + .amount(smallAmount) + .create() + ) + } + } + + @Nested + @DisplayName("returns INVALID result") + internal inner class ReturnsInvalidResult { + @BeforeEach + fun setup() { + reset(findCreditSegment) + } + + @Test + fun `with SSN error message, when invalid SSN provided`() { + val invalidSsn = "49002010966" + whenSsnValidationFailsFor(invalidSsn) + val invalidRequest = testRequest().ssn(invalidSsn).create() + + val result = calculateLoanEligibility.on(invalidRequest) + + assertThat(result).isEqualTo( + expectedResult(withStatus = INVALID) + .ssn(invalidSsn) + .errors("SSN is not valid") + .create() + ) + } + + private fun whenSsnValidationFailsFor(invalidSsn: String) { + `when`(validateSocialSecurityNumber.on(invalidSsn)) + .thenReturn(invalidResultWith(invalidSsn)) + } + + @Test + fun `with error message on too small loan amount, when too small loan amount provided`() { + val tooSmallLoanAmount = MINIMUM_REQUIRED_LOAN_AMOUNT - 1 + val invalidRequest = testRequest().amount(tooSmallLoanAmount).create() + + val result = calculateLoanEligibility.on(invalidRequest) + + assertThat(result).isEqualTo( + expectedResult(withStatus = INVALID).amount(tooSmallLoanAmount) + .errors("Loan amount is less than minimum required") + .create() + ) + } + + @Test + fun `with error message on too large loan amount, when too large loan amount provided`() { + val tooLargeLoanAmount = MAXIMUM_ALLOWED_LOAN_AMOUNT + 1 + val invalidRequest = testRequest().amount(tooLargeLoanAmount).create() + + val result = calculateLoanEligibility.on(invalidRequest) + + assertThat(result) + .isEqualTo( + expectedResult(withStatus = INVALID) + .amount(tooLargeLoanAmount) + .errors("Loan amount is more than maximum allowed") + .create() + ) + } + + @Test + @DisplayName("with error message on too small loan period, when too small loan period provided") + fun `with error message on too small loan period, when too small loan period provided`() { + val tooSmallLoanPeriod = MINIMUM_REQUIRED_LOAN_PERIOD - 1 + val invalidRequest = testRequest().period(tooSmallLoanPeriod).create() + + val result = calculateLoanEligibility.on(invalidRequest) + + assertThat(result) + .isEqualTo( + expectedResult(withStatus = INVALID) + .period(tooSmallLoanPeriod) + .errors("Loan period is less than minimum required") + .create() + ) + } + + @Test + fun `with error message on too large loan period, when too big loan period provided`() { + val tooLargeLoanPeriod = MAXIMUM_ALLOWED_LOAN_PERIOD + 1 + val invalidRequest = testRequest().period(tooLargeLoanPeriod).create() + + val result = calculateLoanEligibility.on(invalidRequest) + + assertThat(result) + .isEqualTo( + expectedResult(withStatus = INVALID) + .period(tooLargeLoanPeriod) + .errors("Loan period is more than maximum allowed") + .create() + ) + } + + @Test + fun `with several error messages, when eligibility result contains several invalid details`() { + val invalidSsn = "49002010966" + whenSsnValidationFailsFor(invalidSsn) + val tooSmallLoanAmount = MINIMUM_REQUIRED_LOAN_AMOUNT - 1 + val tooLargeLoanPeriod = MAXIMUM_ALLOWED_LOAN_PERIOD + 1 + val invalidRequest = testRequest().ssn(invalidSsn).amount(tooSmallLoanAmount).period(tooLargeLoanPeriod).create() + + val result = calculateLoanEligibility.on(invalidRequest) + + assertThat(result) + .isEqualTo( + expectedResult(withStatus = INVALID) + .ssn(invalidSsn) + .amount(tooSmallLoanAmount) + .period(tooLargeLoanPeriod) + .errors( + "SSN is not valid", + "Loan amount is less than minimum required", + "Loan period is more than maximum allowed" + ) + .create() + ) + } + } + + private fun whenCreditSegmentFoundForPerson(withSsn: String, segmentType: CreditSegmentType, creditModifier: Int): CreditSegment { + val ssn = SocialSecurityNumber(withSsn) + val foundSegment = CreditSegment(ssn, segmentType, creditModifier) + `when`(findCreditSegment.forPerson(ssn)) + .thenReturn(Optional.of(foundSegment)) + return foundSegment + } + + private fun whenCreditSegmentNotFoundForPerson(withSsn: String) { + val ssn = SocialSecurityNumber(withSsn) + `when`(findCreditSegment.forPerson(ssn)) + .thenReturn(Optional.empty()) + } + + internal class DefaultTestRequest { + private var ssn = DEFAULT_SSN + private var amount = DEFAULT_AMOUNT + private var period = DEFAULT_PERIOD + fun ssn(value: String): DefaultTestRequest { + ssn = value + return this + } + + fun amount(value: Int): DefaultTestRequest { + amount = value + return this + } + + fun period(value: Int): DefaultTestRequest { + period = value + return this + } + + fun create(): LoanEligibilityRequestDTO { + return LoanEligibilityRequestDTO(ssn, amount, period) + } + } + + internal class DefaultTestResult { + private var status = APPROVED + private var errors: List? = null + private var ssn = DEFAULT_SSN + private var amount = DEFAULT_AMOUNT + private var period = DEFAULT_PERIOD + private var eligibleLoanAmount: Int? = null + private var eligibleLoanPeriod: Int? = null + fun status(value: LoanEligibilityStatus): DefaultTestResult { + status = value + return this + } + + fun errors(vararg errors: String): DefaultTestResult { + this.errors = Arrays.stream(errors).toList() + return this + } + + fun ssn(value: String): DefaultTestResult { + ssn = value + return this + } + + fun amount(value: Int): DefaultTestResult { + amount = value + return this + } + + fun period(value: Int): DefaultTestResult { + period = value + return this + } + + fun eligibleLoanAmount(value: Int?): DefaultTestResult { + eligibleLoanAmount = value + return this + } + + fun eligibleLoanPeriod(value: Int?): DefaultTestResult { + eligibleLoanPeriod = value + return this + } + + fun create(): LoanEligibilityResultDTO { + return LoanEligibilityResultDTO( + status, errors, ssn, amount, period, eligibleLoanAmount, eligibleLoanPeriod + ) + } + } + + companion object { + private const val MINIMUM_REQUIRED_LOAN_AMOUNT = 2000 + private const val MAXIMUM_ALLOWED_LOAN_AMOUNT = 10000 + private const val MINIMUM_REQUIRED_LOAN_PERIOD = 12 + private const val MAXIMUM_ALLOWED_LOAN_PERIOD = 60 + + private val TEST_VALIDATION_LIMITS = ValidationLimitsDTO( + MINIMUM_REQUIRED_LOAN_AMOUNT, + MAXIMUM_ALLOWED_LOAN_AMOUNT, + MINIMUM_REQUIRED_LOAN_PERIOD, + MAXIMUM_ALLOWED_LOAN_PERIOD + ) + + private const val DEFAULT_SSN = "49002010965" + private const val DEFAULT_AMOUNT = 4500 + private const val DEFAULT_PERIOD = 36 + + private fun okResultWithProvidedSsn() = + Answer { methodCall: InvocationOnMock -> + val providedSsn = methodCall.getArgument(0) + okResultWith(providedSsn) + } + + private fun testRequest() = DefaultTestRequest() + + private fun expectedResult() = DefaultTestResult() + + private fun expectedResult(withStatus: LoanEligibilityStatus) = expectedResult().status(withStatus) + } +} From 8926a4b56f7dc6e687473b3e51a37a1ef94c6710 Mon Sep 17 00:00:00 2001 From: Risto Uibo Date: Mon, 24 Jun 2024 14:46:37 +0300 Subject: [PATCH 08/17] Rename .java to .kt --- .../rsx/kata/bank/loans/validation/ssn/SsnValidationResultDTO.kt} | 0 .../bank/loans/validation/ssn/ValidateSocialSecurityNumber.kt} | 0 .../ee/rsx/kata/bank/loans/validation/ssn/ValidationStatus.kt} | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename server/loans/api/src/main/{java/ee/rsx/kata/bank/loans/validation/ssn/SsnValidationResultDTO.java => kotlin/ee/rsx/kata/bank/loans/validation/ssn/SsnValidationResultDTO.kt} (100%) rename server/loans/api/src/main/{java/ee/rsx/kata/bank/loans/validation/ssn/ValidateSocialSecurityNumber.java => kotlin/ee/rsx/kata/bank/loans/validation/ssn/ValidateSocialSecurityNumber.kt} (100%) rename server/loans/api/src/main/{java/ee/rsx/kata/bank/loans/validation/ssn/ValidationStatus.java => kotlin/ee/rsx/kata/bank/loans/validation/ssn/ValidationStatus.kt} (100%) diff --git a/server/loans/api/src/main/java/ee/rsx/kata/bank/loans/validation/ssn/SsnValidationResultDTO.java b/server/loans/api/src/main/kotlin/ee/rsx/kata/bank/loans/validation/ssn/SsnValidationResultDTO.kt similarity index 100% rename from server/loans/api/src/main/java/ee/rsx/kata/bank/loans/validation/ssn/SsnValidationResultDTO.java rename to server/loans/api/src/main/kotlin/ee/rsx/kata/bank/loans/validation/ssn/SsnValidationResultDTO.kt diff --git a/server/loans/api/src/main/java/ee/rsx/kata/bank/loans/validation/ssn/ValidateSocialSecurityNumber.java b/server/loans/api/src/main/kotlin/ee/rsx/kata/bank/loans/validation/ssn/ValidateSocialSecurityNumber.kt similarity index 100% rename from server/loans/api/src/main/java/ee/rsx/kata/bank/loans/validation/ssn/ValidateSocialSecurityNumber.java rename to server/loans/api/src/main/kotlin/ee/rsx/kata/bank/loans/validation/ssn/ValidateSocialSecurityNumber.kt diff --git a/server/loans/api/src/main/java/ee/rsx/kata/bank/loans/validation/ssn/ValidationStatus.java b/server/loans/api/src/main/kotlin/ee/rsx/kata/bank/loans/validation/ssn/ValidationStatus.kt similarity index 100% rename from server/loans/api/src/main/java/ee/rsx/kata/bank/loans/validation/ssn/ValidationStatus.java rename to server/loans/api/src/main/kotlin/ee/rsx/kata/bank/loans/validation/ssn/ValidationStatus.kt From 8cad3e76d8e8956eeaa65613966ce50475edccea Mon Sep 17 00:00:00 2001 From: Risto Uibo Date: Mon, 24 Jun 2024 14:46:37 +0300 Subject: [PATCH 09/17] server | port Loans API to Kotlin --- .../eligibility/LoanEligibilityEndpoint.java | 2 +- server/loans/api/build.gradle | 18 ++++++++++++++ .../eligibility/CalculateLoanEligibility.java | 7 ------ .../LoanEligibilityRequestDTO.java | 7 ------ .../eligibility/LoanEligibilityResultDTO.java | 13 ---------- .../eligibility/LoanEligibilityStatus.java | 7 ------ .../limits/LoadValidationLimits.java | 7 ------ .../limits/ValidationLimitsDTO.java | 8 ------- .../eligibility/CalculateLoanEligibility.kt | 5 ++++ .../eligibility/LoanEligibilityRequestDTO.kt | 8 +++++++ .../eligibility/LoanEligibilityResultDTO.kt | 11 +++++++++ .../eligibility/LoanEligibilityStatus.kt | 7 ++++++ .../validation/limits/LoadValidationLimits.kt | 5 ++++ .../validation/limits/ValidationLimitsDTO.kt | 8 +++++++ .../validation/ssn/SsnValidationResultDTO.kt | 24 +++++++++---------- .../ssn/ValidateSocialSecurityNumber.kt | 8 +++---- .../loans/validation/ssn/ValidationStatus.kt | 4 ++-- .../kata/bank/loans/usecases/SsnValidation.kt | 4 ++-- .../LoanEligibilityCalculationTestKotlin.kt | 4 ++-- 19 files changed, 84 insertions(+), 73 deletions(-) delete mode 100644 server/loans/api/src/main/java/ee/rsx/kata/bank/loans/eligibility/CalculateLoanEligibility.java delete mode 100644 server/loans/api/src/main/java/ee/rsx/kata/bank/loans/eligibility/LoanEligibilityRequestDTO.java delete mode 100644 server/loans/api/src/main/java/ee/rsx/kata/bank/loans/eligibility/LoanEligibilityResultDTO.java delete mode 100644 server/loans/api/src/main/java/ee/rsx/kata/bank/loans/eligibility/LoanEligibilityStatus.java delete mode 100644 server/loans/api/src/main/java/ee/rsx/kata/bank/loans/validation/limits/LoadValidationLimits.java delete mode 100644 server/loans/api/src/main/java/ee/rsx/kata/bank/loans/validation/limits/ValidationLimitsDTO.java create mode 100644 server/loans/api/src/main/kotlin/ee/rsx/kata/bank/loans/eligibility/CalculateLoanEligibility.kt create mode 100644 server/loans/api/src/main/kotlin/ee/rsx/kata/bank/loans/eligibility/LoanEligibilityRequestDTO.kt create mode 100644 server/loans/api/src/main/kotlin/ee/rsx/kata/bank/loans/eligibility/LoanEligibilityResultDTO.kt create mode 100644 server/loans/api/src/main/kotlin/ee/rsx/kata/bank/loans/eligibility/LoanEligibilityStatus.kt create mode 100644 server/loans/api/src/main/kotlin/ee/rsx/kata/bank/loans/validation/limits/LoadValidationLimits.kt create mode 100644 server/loans/api/src/main/kotlin/ee/rsx/kata/bank/loans/validation/limits/ValidationLimitsDTO.kt diff --git a/server/app/src/main/java/ee/rsx/kata/bank/app/rest/loans/eligibility/LoanEligibilityEndpoint.java b/server/app/src/main/java/ee/rsx/kata/bank/app/rest/loans/eligibility/LoanEligibilityEndpoint.java index 181960a..d9f448a 100644 --- a/server/app/src/main/java/ee/rsx/kata/bank/app/rest/loans/eligibility/LoanEligibilityEndpoint.java +++ b/server/app/src/main/java/ee/rsx/kata/bank/app/rest/loans/eligibility/LoanEligibilityEndpoint.java @@ -27,7 +27,7 @@ public ResponseEntity calculateLoanEligibility( ) { var eligibility = calculateLoanEligibility.on(request); - var httpStatus = switch (eligibility.result()) { + var httpStatus = switch (eligibility.getResult()) { case APPROVED -> OK; case INVALID -> BAD_REQUEST; case DENIED -> NOT_ACCEPTABLE; diff --git a/server/loans/api/build.gradle b/server/loans/api/build.gradle index a55dad6..bef645b 100644 --- a/server/loans/api/build.gradle +++ b/server/loans/api/build.gradle @@ -1,2 +1,20 @@ +import static org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 + +plugins { + id 'org.jetbrains.kotlin.jvm' version "${kotlinVersion}" +} + group = 'ee.rsx.kata.bank' version = '1.0.0-SNAPSHOT' + +compileKotlin { + compilerOptions { + jvmTarget = JVM_17 + } +} + +compileTestKotlin { + compilerOptions { + jvmTarget = JVM_17 + } +} diff --git a/server/loans/api/src/main/java/ee/rsx/kata/bank/loans/eligibility/CalculateLoanEligibility.java b/server/loans/api/src/main/java/ee/rsx/kata/bank/loans/eligibility/CalculateLoanEligibility.java deleted file mode 100644 index 6f7c892..0000000 --- a/server/loans/api/src/main/java/ee/rsx/kata/bank/loans/eligibility/CalculateLoanEligibility.java +++ /dev/null @@ -1,7 +0,0 @@ -package ee.rsx.kata.bank.loans.eligibility; - -@FunctionalInterface -public interface CalculateLoanEligibility { - - LoanEligibilityResultDTO on(LoanEligibilityRequestDTO eligibilityRequest); -} diff --git a/server/loans/api/src/main/java/ee/rsx/kata/bank/loans/eligibility/LoanEligibilityRequestDTO.java b/server/loans/api/src/main/java/ee/rsx/kata/bank/loans/eligibility/LoanEligibilityRequestDTO.java deleted file mode 100644 index 712615d..0000000 --- a/server/loans/api/src/main/java/ee/rsx/kata/bank/loans/eligibility/LoanEligibilityRequestDTO.java +++ /dev/null @@ -1,7 +0,0 @@ -package ee.rsx.kata.bank.loans.eligibility; - -public record LoanEligibilityRequestDTO( - String ssn, - Integer loanAmount, - Integer loanPeriodMonths -) {} diff --git a/server/loans/api/src/main/java/ee/rsx/kata/bank/loans/eligibility/LoanEligibilityResultDTO.java b/server/loans/api/src/main/java/ee/rsx/kata/bank/loans/eligibility/LoanEligibilityResultDTO.java deleted file mode 100644 index 5b367cf..0000000 --- a/server/loans/api/src/main/java/ee/rsx/kata/bank/loans/eligibility/LoanEligibilityResultDTO.java +++ /dev/null @@ -1,13 +0,0 @@ -package ee.rsx.kata.bank.loans.eligibility; - -import java.util.List; - -public record LoanEligibilityResultDTO( - LoanEligibilityStatus result, - List errors, - String ssn, - Integer loanAmount, - Integer loanPeriodMonths, - Integer eligibleLoanAmount, - Integer eligibleLoanPeriod -) {} diff --git a/server/loans/api/src/main/java/ee/rsx/kata/bank/loans/eligibility/LoanEligibilityStatus.java b/server/loans/api/src/main/java/ee/rsx/kata/bank/loans/eligibility/LoanEligibilityStatus.java deleted file mode 100644 index be4ab07..0000000 --- a/server/loans/api/src/main/java/ee/rsx/kata/bank/loans/eligibility/LoanEligibilityStatus.java +++ /dev/null @@ -1,7 +0,0 @@ -package ee.rsx.kata.bank.loans.eligibility; - -public enum LoanEligibilityStatus { - APPROVED, - DENIED, - INVALID -} diff --git a/server/loans/api/src/main/java/ee/rsx/kata/bank/loans/validation/limits/LoadValidationLimits.java b/server/loans/api/src/main/java/ee/rsx/kata/bank/loans/validation/limits/LoadValidationLimits.java deleted file mode 100644 index 2296162..0000000 --- a/server/loans/api/src/main/java/ee/rsx/kata/bank/loans/validation/limits/LoadValidationLimits.java +++ /dev/null @@ -1,7 +0,0 @@ -package ee.rsx.kata.bank.loans.validation.limits; - -@FunctionalInterface -public interface LoadValidationLimits { - - ValidationLimitsDTO invoke(); -} diff --git a/server/loans/api/src/main/java/ee/rsx/kata/bank/loans/validation/limits/ValidationLimitsDTO.java b/server/loans/api/src/main/java/ee/rsx/kata/bank/loans/validation/limits/ValidationLimitsDTO.java deleted file mode 100644 index 6815ca8..0000000 --- a/server/loans/api/src/main/java/ee/rsx/kata/bank/loans/validation/limits/ValidationLimitsDTO.java +++ /dev/null @@ -1,8 +0,0 @@ -package ee.rsx.kata.bank.loans.validation.limits; - -public record ValidationLimitsDTO( - Integer minimumLoanAmount, - Integer maximumLoanAmount, - Integer minimumLoanPeriodMonths, - Integer maximumLoanPeriodMonths -) {} diff --git a/server/loans/api/src/main/kotlin/ee/rsx/kata/bank/loans/eligibility/CalculateLoanEligibility.kt b/server/loans/api/src/main/kotlin/ee/rsx/kata/bank/loans/eligibility/CalculateLoanEligibility.kt new file mode 100644 index 0000000..63004b7 --- /dev/null +++ b/server/loans/api/src/main/kotlin/ee/rsx/kata/bank/loans/eligibility/CalculateLoanEligibility.kt @@ -0,0 +1,5 @@ +package ee.rsx.kata.bank.loans.eligibility + +fun interface CalculateLoanEligibility { + fun on(eligibilityRequest: LoanEligibilityRequestDTO): LoanEligibilityResultDTO +} diff --git a/server/loans/api/src/main/kotlin/ee/rsx/kata/bank/loans/eligibility/LoanEligibilityRequestDTO.kt b/server/loans/api/src/main/kotlin/ee/rsx/kata/bank/loans/eligibility/LoanEligibilityRequestDTO.kt new file mode 100644 index 0000000..77e4370 --- /dev/null +++ b/server/loans/api/src/main/kotlin/ee/rsx/kata/bank/loans/eligibility/LoanEligibilityRequestDTO.kt @@ -0,0 +1,8 @@ +package ee.rsx.kata.bank.loans.eligibility + +@JvmRecord +data class LoanEligibilityRequestDTO( + val ssn: String, + val loanAmount: Int, + val loanPeriodMonths: Int +) diff --git a/server/loans/api/src/main/kotlin/ee/rsx/kata/bank/loans/eligibility/LoanEligibilityResultDTO.kt b/server/loans/api/src/main/kotlin/ee/rsx/kata/bank/loans/eligibility/LoanEligibilityResultDTO.kt new file mode 100644 index 0000000..69f576f --- /dev/null +++ b/server/loans/api/src/main/kotlin/ee/rsx/kata/bank/loans/eligibility/LoanEligibilityResultDTO.kt @@ -0,0 +1,11 @@ +package ee.rsx.kata.bank.loans.eligibility + +data class LoanEligibilityResultDTO( + val result: LoanEligibilityStatus, + val errors: List? = null, + val ssn: String, + val loanAmount: Int, + val loanPeriodMonths: Int, + val eligibleLoanAmount: Int? = null, + val eligibleLoanPeriod: Int? = null +) diff --git a/server/loans/api/src/main/kotlin/ee/rsx/kata/bank/loans/eligibility/LoanEligibilityStatus.kt b/server/loans/api/src/main/kotlin/ee/rsx/kata/bank/loans/eligibility/LoanEligibilityStatus.kt new file mode 100644 index 0000000..52035f3 --- /dev/null +++ b/server/loans/api/src/main/kotlin/ee/rsx/kata/bank/loans/eligibility/LoanEligibilityStatus.kt @@ -0,0 +1,7 @@ +package ee.rsx.kata.bank.loans.eligibility + +enum class LoanEligibilityStatus { + APPROVED, + DENIED, + INVALID +} diff --git a/server/loans/api/src/main/kotlin/ee/rsx/kata/bank/loans/validation/limits/LoadValidationLimits.kt b/server/loans/api/src/main/kotlin/ee/rsx/kata/bank/loans/validation/limits/LoadValidationLimits.kt new file mode 100644 index 0000000..0d2d7d0 --- /dev/null +++ b/server/loans/api/src/main/kotlin/ee/rsx/kata/bank/loans/validation/limits/LoadValidationLimits.kt @@ -0,0 +1,5 @@ +package ee.rsx.kata.bank.loans.validation.limits + +fun interface LoadValidationLimits { + operator fun invoke(): ValidationLimitsDTO +} diff --git a/server/loans/api/src/main/kotlin/ee/rsx/kata/bank/loans/validation/limits/ValidationLimitsDTO.kt b/server/loans/api/src/main/kotlin/ee/rsx/kata/bank/loans/validation/limits/ValidationLimitsDTO.kt new file mode 100644 index 0000000..df44a1f --- /dev/null +++ b/server/loans/api/src/main/kotlin/ee/rsx/kata/bank/loans/validation/limits/ValidationLimitsDTO.kt @@ -0,0 +1,8 @@ +package ee.rsx.kata.bank.loans.validation.limits + +data class ValidationLimitsDTO( + val minimumLoanAmount: Int, + val maximumLoanAmount: Int, + val minimumLoanPeriodMonths: Int, + val maximumLoanPeriodMonths: Int +) diff --git a/server/loans/api/src/main/kotlin/ee/rsx/kata/bank/loans/validation/ssn/SsnValidationResultDTO.kt b/server/loans/api/src/main/kotlin/ee/rsx/kata/bank/loans/validation/ssn/SsnValidationResultDTO.kt index 843fd2b..16a7b3c 100644 --- a/server/loans/api/src/main/kotlin/ee/rsx/kata/bank/loans/validation/ssn/SsnValidationResultDTO.kt +++ b/server/loans/api/src/main/kotlin/ee/rsx/kata/bank/loans/validation/ssn/SsnValidationResultDTO.kt @@ -1,14 +1,14 @@ -package ee.rsx.kata.bank.loans.validation.ssn; - -import static ee.rsx.kata.bank.loans.validation.ssn.ValidationStatus.INVALID; - -public record SsnValidationResultDTO(String ssn, ValidationStatus status) { - - public static SsnValidationResultDTO okResultWith(String ssn) { - return new SsnValidationResultDTO(ssn, ValidationStatus.OK); - } - - public static SsnValidationResultDTO invalidResultWith(String ssn) { - return new SsnValidationResultDTO(ssn, INVALID); +package ee.rsx.kata.bank.loans.validation.ssn + +@JvmRecord +data class SsnValidationResultDTO(val ssn: String, val status: ValidationStatus) { + companion object { + fun okResultWith(ssn: String): SsnValidationResultDTO { + return SsnValidationResultDTO(ssn, ValidationStatus.OK) + } + + fun invalidResultWith(ssn: String): SsnValidationResultDTO { + return SsnValidationResultDTO(ssn, ValidationStatus.INVALID) + } } } diff --git a/server/loans/api/src/main/kotlin/ee/rsx/kata/bank/loans/validation/ssn/ValidateSocialSecurityNumber.kt b/server/loans/api/src/main/kotlin/ee/rsx/kata/bank/loans/validation/ssn/ValidateSocialSecurityNumber.kt index 426fc35..0d8d867 100644 --- a/server/loans/api/src/main/kotlin/ee/rsx/kata/bank/loans/validation/ssn/ValidateSocialSecurityNumber.kt +++ b/server/loans/api/src/main/kotlin/ee/rsx/kata/bank/loans/validation/ssn/ValidateSocialSecurityNumber.kt @@ -1,7 +1,5 @@ -package ee.rsx.kata.bank.loans.validation.ssn; +package ee.rsx.kata.bank.loans.validation.ssn -@FunctionalInterface -public interface ValidateSocialSecurityNumber { - - SsnValidationResultDTO on(String ssn); +fun interface ValidateSocialSecurityNumber { + fun on(ssn: String): SsnValidationResultDTO } diff --git a/server/loans/api/src/main/kotlin/ee/rsx/kata/bank/loans/validation/ssn/ValidationStatus.kt b/server/loans/api/src/main/kotlin/ee/rsx/kata/bank/loans/validation/ssn/ValidationStatus.kt index ff47cf2..1b8aaac 100644 --- a/server/loans/api/src/main/kotlin/ee/rsx/kata/bank/loans/validation/ssn/ValidationStatus.kt +++ b/server/loans/api/src/main/kotlin/ee/rsx/kata/bank/loans/validation/ssn/ValidationStatus.kt @@ -1,6 +1,6 @@ -package ee.rsx.kata.bank.loans.validation.ssn; +package ee.rsx.kata.bank.loans.validation.ssn -public enum ValidationStatus { +enum class ValidationStatus { OK, INVALID } diff --git a/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/usecases/SsnValidation.kt b/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/usecases/SsnValidation.kt index c3e8903..e1fbf1b 100644 --- a/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/usecases/SsnValidation.kt +++ b/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/usecases/SsnValidation.kt @@ -2,8 +2,8 @@ package ee.rsx.kata.bank.loans.usecases import ee.rsx.kata.bank.loans.domain.ssn.SocialSecurityNumber import ee.rsx.kata.bank.loans.validation.ssn.SsnValidationResultDTO -import ee.rsx.kata.bank.loans.validation.ssn.SsnValidationResultDTO.invalidResultWith -import ee.rsx.kata.bank.loans.validation.ssn.SsnValidationResultDTO.okResultWith +import ee.rsx.kata.bank.loans.validation.ssn.SsnValidationResultDTO.Companion.invalidResultWith +import ee.rsx.kata.bank.loans.validation.ssn.SsnValidationResultDTO.Companion.okResultWith import ee.rsx.kata.bank.loans.validation.ssn.ValidateSocialSecurityNumber import jakarta.inject.Named import java.time.format.DateTimeParseException diff --git a/server/loans/core/src/test/kotlin/ee/rsx/kata/bank/loans/usecases/LoanEligibilityCalculationTestKotlin.kt b/server/loans/core/src/test/kotlin/ee/rsx/kata/bank/loans/usecases/LoanEligibilityCalculationTestKotlin.kt index 34bc861..1d7450e 100644 --- a/server/loans/core/src/test/kotlin/ee/rsx/kata/bank/loans/usecases/LoanEligibilityCalculationTestKotlin.kt +++ b/server/loans/core/src/test/kotlin/ee/rsx/kata/bank/loans/usecases/LoanEligibilityCalculationTestKotlin.kt @@ -15,8 +15,8 @@ import ee.rsx.kata.bank.loans.eligibility.LoanEligibilityStatus.DENIED import ee.rsx.kata.bank.loans.eligibility.LoanEligibilityStatus.INVALID import ee.rsx.kata.bank.loans.validation.limits.LoadValidationLimits import ee.rsx.kata.bank.loans.validation.limits.ValidationLimitsDTO -import ee.rsx.kata.bank.loans.validation.ssn.SsnValidationResultDTO.invalidResultWith -import ee.rsx.kata.bank.loans.validation.ssn.SsnValidationResultDTO.okResultWith +import ee.rsx.kata.bank.loans.validation.ssn.SsnValidationResultDTO.Companion.invalidResultWith +import ee.rsx.kata.bank.loans.validation.ssn.SsnValidationResultDTO.Companion.okResultWith import ee.rsx.kata.bank.loans.validation.ssn.ValidateSocialSecurityNumber import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.BeforeEach From e559e3d91b6dddfe059f5f3ef3a3fb24e7e01d13 Mon Sep 17 00:00:00 2001 From: Risto Uibo Date: Mon, 24 Jun 2024 15:34:07 +0300 Subject: [PATCH 10/17] Rename .java to .kt --- .../app/Server.java => kotlin/ee/rsx/kata/bank/app/Server.kt} | 0 .../bank/app/rest/loans/eligibility/LoanEligibilityEndpoint.kt} | 0 .../kata/bank/app/rest/loans/validation/ValidationEndpoints.kt} | 0 .../bank/app/architecture/DependencyRulesArchitectureTest.kt} | 0 .../loans/eligibility/LoanEligibilityIntegrationTest.kt} | 0 .../loans/validation/SsnValidationIntegrationTest.kt} | 0 .../loans/validation/ValidationLimitsIntegrationTest.kt} | 0 7 files changed, 0 insertions(+), 0 deletions(-) rename server/app/src/main/{java/ee/rsx/kata/bank/app/Server.java => kotlin/ee/rsx/kata/bank/app/Server.kt} (100%) rename server/app/src/main/{java/ee/rsx/kata/bank/app/rest/loans/eligibility/LoanEligibilityEndpoint.java => kotlin/ee/rsx/kata/bank/app/rest/loans/eligibility/LoanEligibilityEndpoint.kt} (100%) rename server/app/src/main/{java/ee/rsx/kata/bank/app/rest/loans/validation/ValidationEndpoints.java => kotlin/ee/rsx/kata/bank/app/rest/loans/validation/ValidationEndpoints.kt} (100%) rename server/app/src/test/{java/ee/rsx/kata/bank/app/architecture/DependencyRulesArchitectureTest.java => kotlin/ee/rsx/kata/bank/app/architecture/DependencyRulesArchitectureTest.kt} (100%) rename server/app/src/test/{java/ee/rsx/kata/bank/app/integrationtest/loans/eligibility/LoanEligibilityIntegrationTest.java => kotlin/ee/rsx/kata/bank/app/integrationtest/loans/eligibility/LoanEligibilityIntegrationTest.kt} (100%) rename server/app/src/test/{java/ee/rsx/kata/bank/app/integrationtest/loans/validation/SsnValidationIntegrationTest.java => kotlin/ee/rsx/kata/bank/app/integrationtest/loans/validation/SsnValidationIntegrationTest.kt} (100%) rename server/app/src/test/{java/ee/rsx/kata/bank/app/integrationtest/loans/validation/ValidationLimitsIntegrationTest.java => kotlin/ee/rsx/kata/bank/app/integrationtest/loans/validation/ValidationLimitsIntegrationTest.kt} (100%) diff --git a/server/app/src/main/java/ee/rsx/kata/bank/app/Server.java b/server/app/src/main/kotlin/ee/rsx/kata/bank/app/Server.kt similarity index 100% rename from server/app/src/main/java/ee/rsx/kata/bank/app/Server.java rename to server/app/src/main/kotlin/ee/rsx/kata/bank/app/Server.kt diff --git a/server/app/src/main/java/ee/rsx/kata/bank/app/rest/loans/eligibility/LoanEligibilityEndpoint.java b/server/app/src/main/kotlin/ee/rsx/kata/bank/app/rest/loans/eligibility/LoanEligibilityEndpoint.kt similarity index 100% rename from server/app/src/main/java/ee/rsx/kata/bank/app/rest/loans/eligibility/LoanEligibilityEndpoint.java rename to server/app/src/main/kotlin/ee/rsx/kata/bank/app/rest/loans/eligibility/LoanEligibilityEndpoint.kt diff --git a/server/app/src/main/java/ee/rsx/kata/bank/app/rest/loans/validation/ValidationEndpoints.java b/server/app/src/main/kotlin/ee/rsx/kata/bank/app/rest/loans/validation/ValidationEndpoints.kt similarity index 100% rename from server/app/src/main/java/ee/rsx/kata/bank/app/rest/loans/validation/ValidationEndpoints.java rename to server/app/src/main/kotlin/ee/rsx/kata/bank/app/rest/loans/validation/ValidationEndpoints.kt diff --git a/server/app/src/test/java/ee/rsx/kata/bank/app/architecture/DependencyRulesArchitectureTest.java b/server/app/src/test/kotlin/ee/rsx/kata/bank/app/architecture/DependencyRulesArchitectureTest.kt similarity index 100% rename from server/app/src/test/java/ee/rsx/kata/bank/app/architecture/DependencyRulesArchitectureTest.java rename to server/app/src/test/kotlin/ee/rsx/kata/bank/app/architecture/DependencyRulesArchitectureTest.kt diff --git a/server/app/src/test/java/ee/rsx/kata/bank/app/integrationtest/loans/eligibility/LoanEligibilityIntegrationTest.java b/server/app/src/test/kotlin/ee/rsx/kata/bank/app/integrationtest/loans/eligibility/LoanEligibilityIntegrationTest.kt similarity index 100% rename from server/app/src/test/java/ee/rsx/kata/bank/app/integrationtest/loans/eligibility/LoanEligibilityIntegrationTest.java rename to server/app/src/test/kotlin/ee/rsx/kata/bank/app/integrationtest/loans/eligibility/LoanEligibilityIntegrationTest.kt diff --git a/server/app/src/test/java/ee/rsx/kata/bank/app/integrationtest/loans/validation/SsnValidationIntegrationTest.java b/server/app/src/test/kotlin/ee/rsx/kata/bank/app/integrationtest/loans/validation/SsnValidationIntegrationTest.kt similarity index 100% rename from server/app/src/test/java/ee/rsx/kata/bank/app/integrationtest/loans/validation/SsnValidationIntegrationTest.java rename to server/app/src/test/kotlin/ee/rsx/kata/bank/app/integrationtest/loans/validation/SsnValidationIntegrationTest.kt diff --git a/server/app/src/test/java/ee/rsx/kata/bank/app/integrationtest/loans/validation/ValidationLimitsIntegrationTest.java b/server/app/src/test/kotlin/ee/rsx/kata/bank/app/integrationtest/loans/validation/ValidationLimitsIntegrationTest.kt similarity index 100% rename from server/app/src/test/java/ee/rsx/kata/bank/app/integrationtest/loans/validation/ValidationLimitsIntegrationTest.java rename to server/app/src/test/kotlin/ee/rsx/kata/bank/app/integrationtest/loans/validation/ValidationLimitsIntegrationTest.kt From c712b15231c1657505e893e7f70efa79bb53508a Mon Sep 17 00:00:00 2001 From: Risto Uibo Date: Mon, 24 Jun 2024 15:34:07 +0300 Subject: [PATCH 11/17] server | port app module (and application tests) to Kotlin --- server/app/build.gradle | 15 ++ .../kotlin/ee/rsx/kata/bank/app/Server.kt | 17 +- .../eligibility/LoanEligibilityEndpoint.kt | 68 +++-- .../loans/validation/ValidationEndpoints.kt | 39 ++- .../DependencyRulesArchitectureTest.kt | 83 +++--- .../LoanEligibilityIntegrationTest.kt | 246 +++++++++--------- .../SsnValidationIntegrationTest.kt | 84 +++--- .../ValidationLimitsIntegrationTest.kt | 43 +-- 8 files changed, 295 insertions(+), 300 deletions(-) diff --git a/server/app/build.gradle b/server/app/build.gradle index ca90258..f0a8e3d 100644 --- a/server/app/build.gradle +++ b/server/app/build.gradle @@ -1,7 +1,11 @@ +import static org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 + plugins { + id 'org.jetbrains.kotlin.jvm' version "${kotlinVersion}" id 'org.springframework.boot' version "${springBootVersion}" id 'io.spring.dependency-management' version "${springDependencyManagementVersion}" } + group = 'ee.rsx.kata.bank' version = '1.0.0-SNAPSHOT' @@ -14,3 +18,14 @@ dependencies { testImplementation "com.tngtech.archunit:archunit-junit5:${archUnitVersion}" } +compileKotlin { + compilerOptions { + jvmTarget = JVM_17 + } +} + +compileTestKotlin { + compilerOptions { + jvmTarget = JVM_17 + } +} diff --git a/server/app/src/main/kotlin/ee/rsx/kata/bank/app/Server.kt b/server/app/src/main/kotlin/ee/rsx/kata/bank/app/Server.kt index 30aae76..73e6230 100644 --- a/server/app/src/main/kotlin/ee/rsx/kata/bank/app/Server.kt +++ b/server/app/src/main/kotlin/ee/rsx/kata/bank/app/Server.kt @@ -1,14 +1,11 @@ -package ee.rsx.kata.bank.app; +package ee.rsx.kata.bank.app -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.context.annotation.ComponentScan; +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.runApplication -@SpringBootApplication -@ComponentScan("ee.rsx.kata.bank") -public class Server { +@SpringBootApplication(scanBasePackages = ["ee.rsx.kata.bank"]) +open class Server - public static void main(String[] args) { - SpringApplication.run(Server.class, args); - } +fun main(vararg args: String) { + runApplication(*args) } diff --git a/server/app/src/main/kotlin/ee/rsx/kata/bank/app/rest/loans/eligibility/LoanEligibilityEndpoint.kt b/server/app/src/main/kotlin/ee/rsx/kata/bank/app/rest/loans/eligibility/LoanEligibilityEndpoint.kt index d9f448a..884a65f 100644 --- a/server/app/src/main/kotlin/ee/rsx/kata/bank/app/rest/loans/eligibility/LoanEligibilityEndpoint.kt +++ b/server/app/src/main/kotlin/ee/rsx/kata/bank/app/rest/loans/eligibility/LoanEligibilityEndpoint.kt @@ -1,42 +1,38 @@ -package ee.rsx.kata.bank.app.rest.loans.eligibility; - -import ee.rsx.kata.bank.loans.eligibility.CalculateLoanEligibility; -import ee.rsx.kata.bank.loans.eligibility.LoanEligibilityRequestDTO; -import ee.rsx.kata.bank.loans.eligibility.LoanEligibilityResultDTO; -import lombok.RequiredArgsConstructor; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import static org.springframework.http.HttpStatus.*; -import static org.springframework.http.MediaType.APPLICATION_JSON; -import static org.springframework.http.ResponseEntity.status; +package ee.rsx.kata.bank.app.rest.loans.eligibility + +import ee.rsx.kata.bank.loans.eligibility.CalculateLoanEligibility +import ee.rsx.kata.bank.loans.eligibility.LoanEligibilityRequestDTO +import ee.rsx.kata.bank.loans.eligibility.LoanEligibilityResultDTO +import ee.rsx.kata.bank.loans.eligibility.LoanEligibilityStatus.APPROVED +import ee.rsx.kata.bank.loans.eligibility.LoanEligibilityStatus.DENIED +import ee.rsx.kata.bank.loans.eligibility.LoanEligibilityStatus.INVALID +import org.springframework.http.HttpStatus.BAD_REQUEST +import org.springframework.http.HttpStatus.NOT_ACCEPTABLE +import org.springframework.http.HttpStatus.OK +import org.springframework.http.MediaType.APPLICATION_JSON +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController @RestController @RequestMapping("/loans/eligibility") -@RequiredArgsConstructor -public class LoanEligibilityEndpoint { - - private final CalculateLoanEligibility calculateLoanEligibility; +class LoanEligibilityEndpoint( + private val calculateLoanEligibility: CalculateLoanEligibility +) { @PostMapping - public ResponseEntity calculateLoanEligibility( - @RequestBody LoanEligibilityRequestDTO request - ) { - var eligibility = calculateLoanEligibility.on(request); - - var httpStatus = switch (eligibility.getResult()) { - case APPROVED -> OK; - case INVALID -> BAD_REQUEST; - case DENIED -> NOT_ACCEPTABLE; - }; - - return status(httpStatus) - .contentType(APPLICATION_JSON) - .body(eligibility); - } + fun calculateLoanEligibility(@RequestBody request: LoanEligibilityRequestDTO): ResponseEntity = + with(calculateLoanEligibility.on(request)) { + val httpStatus = when (result) { + APPROVED -> OK + INVALID -> BAD_REQUEST + DENIED -> NOT_ACCEPTABLE + } + + ResponseEntity.status(httpStatus) + .contentType(APPLICATION_JSON) + .body(this) + } } - - diff --git a/server/app/src/main/kotlin/ee/rsx/kata/bank/app/rest/loans/validation/ValidationEndpoints.kt b/server/app/src/main/kotlin/ee/rsx/kata/bank/app/rest/loans/validation/ValidationEndpoints.kt index c68db10..7b1d324 100644 --- a/server/app/src/main/kotlin/ee/rsx/kata/bank/app/rest/loans/validation/ValidationEndpoints.kt +++ b/server/app/src/main/kotlin/ee/rsx/kata/bank/app/rest/loans/validation/ValidationEndpoints.kt @@ -1,31 +1,22 @@ -package ee.rsx.kata.bank.app.rest.loans.validation; +package ee.rsx.kata.bank.app.rest.loans.validation -import ee.rsx.kata.bank.loans.validation.limits.LoadValidationLimits; -import ee.rsx.kata.bank.loans.validation.limits.ValidationLimitsDTO; -import ee.rsx.kata.bank.loans.validation.ssn.SsnValidationResultDTO; -import ee.rsx.kata.bank.loans.validation.ssn.ValidateSocialSecurityNumber; -import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; +import ee.rsx.kata.bank.loans.validation.limits.LoadValidationLimits +import ee.rsx.kata.bank.loans.validation.ssn.ValidateSocialSecurityNumber +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RestController @RestController @RequestMapping("/loans/validation") -@RequiredArgsConstructor -public class ValidationEndpoints { +class ValidationEndpoints( + private val loadValidationLimits: LoadValidationLimits, + private val validateSocialSecurityNumber: ValidateSocialSecurityNumber +) { - private final LoadValidationLimits loadValidationLimits; - private final ValidateSocialSecurityNumber validateSocialSecurityNumber; + @GetMapping(value = ["/limits"]) + fun loadValidationLimits() = loadValidationLimits.invoke() - @GetMapping(value = "/limits") - public ValidationLimitsDTO loadValidationLimits() { - return loadValidationLimits.invoke(); - } - - @GetMapping(value = "/ssn") - public SsnValidationResultDTO validateSsn(@RequestParam("value") String value) { - return validateSocialSecurityNumber.on(value); - } + @GetMapping(value = ["/ssn"]) + fun validateSsn(@RequestParam("value") value: String) = validateSocialSecurityNumber.on(value) } - diff --git a/server/app/src/test/kotlin/ee/rsx/kata/bank/app/architecture/DependencyRulesArchitectureTest.kt b/server/app/src/test/kotlin/ee/rsx/kata/bank/app/architecture/DependencyRulesArchitectureTest.kt index 8145ebc..d9cb193 100644 --- a/server/app/src/test/kotlin/ee/rsx/kata/bank/app/architecture/DependencyRulesArchitectureTest.kt +++ b/server/app/src/test/kotlin/ee/rsx/kata/bank/app/architecture/DependencyRulesArchitectureTest.kt @@ -1,68 +1,67 @@ -package ee.rsx.kata.bank.app.architecture; +package ee.rsx.kata.bank.app.architecture -import com.tngtech.archunit.base.DescribedPredicate; -import com.tngtech.archunit.core.domain.JavaClass; -import com.tngtech.archunit.junit.AnalyzeClasses; -import com.tngtech.archunit.junit.ArchTest; -import com.tngtech.archunit.lang.ArchRule; +import com.tngtech.archunit.base.DescribedPredicate.not +import com.tngtech.archunit.core.domain.JavaClass.Predicates.ENUMS +import com.tngtech.archunit.core.domain.JavaClass.Predicates.resideInAPackage +import com.tngtech.archunit.core.domain.JavaClass.Predicates.resideInAnyPackage +import com.tngtech.archunit.core.domain.JavaClass.Predicates.resideOutsideOfPackage +import com.tngtech.archunit.junit.AnalyzeClasses +import com.tngtech.archunit.junit.ArchTest +import com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses +import org.junit.jupiter.api.DisplayName -import java.util.List; - -import static com.tngtech.archunit.base.DescribedPredicate.not; -import static com.tngtech.archunit.core.domain.JavaClass.Predicates.*; -import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses; - -@AnalyzeClasses(packages = {"ee.rsx.kata.bank"}) -class DependencyRulesArchitectureTest { - - public static final String BANK_ROOT_PACKAGE = "ee.rsx.kata.bank"; - - public static final String[] BANK_CORE_PACKAGES = - List.of( - BANK_ROOT_PACKAGE + ".(**).domain..", - BANK_ROOT_PACKAGE + ".(**).usecases.." - ) - .toArray(new String[0]); +@AnalyzeClasses(packages = ["ee.rsx.kata.bank"]) +@Suppress("unused") +@DisplayName("Dependency Rules of architecture") +internal class DependencyRulesArchitectureTest { @ArchTest - private final ArchRule classesOutsideOfCore_exceptGatewayAdapters_shouldNotDependOn_classesInCore = + private val `Classes outside of Core (except gateway adapters) should not depend on classes in core` = noClasses().that() - .resideInAPackage(BANK_ROOT_PACKAGE + "..") + .resideInAPackage("$BANK_ROOT_PACKAGE..") .and() - .resideOutsideOfPackages(BANK_CORE_PACKAGES) + .resideOutsideOfPackages(*BANK_CORE_PACKAGES) .and( ignoreGatewayAdapterClasses() ) .should() .dependOnClassesThat( - resideInAnyPackage(BANK_CORE_PACKAGES) - ); - - private static DescribedPredicate ignoreGatewayAdapterClasses() { - return not(resideInAPackage(BANK_ROOT_PACKAGE + ".(**).adapter..")); - } + resideInAnyPackage(*BANK_CORE_PACKAGES) + ) @ArchTest - private final ArchRule bankCoreClasses_shouldNotDependOn_springFramework = + private val `Bank Core classes should not depend on Spring Framework` = noClasses().that() - .resideInAnyPackage(BANK_CORE_PACKAGES) + .resideInAnyPackage(*BANK_CORE_PACKAGES) .should() - .dependOnClassesThat().resideInAPackage("org.springframework.."); + .dependOnClassesThat().resideInAPackage("org.springframework..") @ArchTest - private final ArchRule domainClasses_shouldNotDependOn_anyBankLoansClasses_outsideOfDomain = + private val `Domain classes should not depend on any bank loans classes outside of domain` = noClasses().that() .resideInAPackage( - BANK_ROOT_PACKAGE + ".(**).domain.." - ) + "$BANK_ROOT_PACKAGE.(**).domain.." + ) .should() .dependOnClassesThat( areBankLoansClassesOutsideOfDomain() - ); + ) - private DescribedPredicate areBankLoansClassesOutsideOfDomain() { - return resideInAPackage(BANK_ROOT_PACKAGE + "..") + private fun areBankLoansClassesOutsideOfDomain() = + resideInAPackage("$BANK_ROOT_PACKAGE..") .and(resideOutsideOfPackage("..domain..")) - .and(not(ENUMS)); + .and(not(ENUMS)) + + companion object { + const val BANK_ROOT_PACKAGE = "ee.rsx.kata.bank" + + val BANK_CORE_PACKAGES = listOf( + "$BANK_ROOT_PACKAGE.(**).domain..", + "$BANK_ROOT_PACKAGE.(**).usecases.." + ) + .toTypedArray() + + private fun ignoreGatewayAdapterClasses() = + not(resideInAPackage("$BANK_ROOT_PACKAGE.(**).adapter..")) } } diff --git a/server/app/src/test/kotlin/ee/rsx/kata/bank/app/integrationtest/loans/eligibility/LoanEligibilityIntegrationTest.kt b/server/app/src/test/kotlin/ee/rsx/kata/bank/app/integrationtest/loans/eligibility/LoanEligibilityIntegrationTest.kt index f35151f..ddfe5c8 100644 --- a/server/app/src/test/kotlin/ee/rsx/kata/bank/app/integrationtest/loans/eligibility/LoanEligibilityIntegrationTest.kt +++ b/server/app/src/test/kotlin/ee/rsx/kata/bank/app/integrationtest/loans/eligibility/LoanEligibilityIntegrationTest.kt @@ -1,199 +1,197 @@ -package ee.rsx.kata.bank.app.integrationtest.loans.eligibility; - -import ee.rsx.kata.bank.app.Server; -import jakarta.inject.Inject; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.result.MockMvcResultHandlers; - -import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; -import static org.springframework.http.MediaType.APPLICATION_JSON; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; - -@SpringBootTest(webEnvironment = RANDOM_PORT, classes = {Server.class}) +package ee.rsx.kata.bank.app.integrationtest.loans.eligibility + +import ee.rsx.kata.bank.app.Server +import jakarta.inject.Inject +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT +import org.springframework.http.MediaType.APPLICATION_JSON +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.content +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status + +@SpringBootTest(webEnvironment = RANDOM_PORT, classes = [Server::class]) @AutoConfigureMockMvc @DisplayName("Loan eligibility calculation") -public class LoanEligibilityIntegrationTest { +class LoanEligibilityIntegrationTest { - private static final String LOAN_ELIGIBILITY_URL = "/loans/eligibility"; + companion object { + private const val LOAN_ELIGIBILITY_URL = "/loans/eligibility" + } @Inject - private MockMvc mockMvc; + private lateinit var mockMvc: MockMvc @Test - @DisplayName("returns an DENIED eligibility result for the provided loan request, since person is in DEBT") - void returns_DENIED_result_forProvidedLoanRequest_sincePersonInDebt() throws Exception { - var personInDebt = "49002010965"; - var loanEligibilityRequest = """ + fun `returns an DENIED eligibility result for the provided loan request, since person is in DEBT`() { + val personInDebt = "49002010965" + val loanEligibilityRequest = """ { - "ssn": "%s", + "ssn": "$personInDebt", "loanAmount": 4500, "loanPeriodMonths": 36 } - """.formatted(personInDebt); - var postRequest = - post(LOAN_ELIGIBILITY_URL) - .contentType(APPLICATION_JSON) - .content(loanEligibilityRequest); + + """.trimIndent() + + val postRequest = post(LOAN_ELIGIBILITY_URL) + .contentType(APPLICATION_JSON) + .content(loanEligibilityRequest) - var notAcceptableStatus = status().isNotAcceptable(); + val notAcceptableStatus = status().isNotAcceptable() mockMvc.perform(postRequest) .andExpect(notAcceptableStatus) .andExpect( content().json( """ - { - "result": "DENIED", - "ssn": "%s", - "loanAmount": 4500, - "loanPeriodMonths": 36 - } - """.formatted(personInDebt) + { + "result": "DENIED", + "ssn": "$personInDebt", + "loanAmount": 4500, + "loanPeriodMonths": 36 + } + """.trimIndent() ) - ); + ) } @Test - @DisplayName("returns an APPROVED eligibility result for the provided loan request, since person is in high SEGMENT 3") - void returns_APPROVED_result_forProvidedLoanRequest_sincePersonInHighCreditSegment() throws Exception { - var highCreditPerson = "49002010998"; - var loanEligibilityRequest = """ + fun `returns an APPROVED eligibility result for the provided loan request, since person is in high SEGMENT 3`() { + val highCreditPerson = "49002010998" + val loanEligibilityRequest = """ { - "ssn": "%s", + "ssn": "$highCreditPerson", "loanAmount": 4500, "loanPeriodMonths": 36 } - """.formatted(highCreditPerson); - var postRequest = - post(LOAN_ELIGIBILITY_URL) - .contentType(APPLICATION_JSON) - .content(loanEligibilityRequest); + """.trimIndent() + + val postRequest = post(LOAN_ELIGIBILITY_URL) + .contentType(APPLICATION_JSON) + .content(loanEligibilityRequest) mockMvc.perform(postRequest) .andExpect(status().isOk()) .andExpect( content().json( """ - { - "result": "APPROVED", - "ssn": "%s", - "loanAmount": 4500, - "loanPeriodMonths": 36, - "eligibleLoanAmount": 10000 - } - """.formatted(highCreditPerson) + { + "result": "APPROVED", + "ssn": "$highCreditPerson", + "loanAmount": 4500, + "loanPeriodMonths": 36, + "eligibleLoanAmount": 10000 + } + """.trimIndent() ) - ); + ) } @Test - @DisplayName("returns an APPROVED eligibility result for the provided loan request, since person is in good SEGMENT 2") - void returns_APPROVED_result_forProvidedLoanRequest_sincePersonInGoodCreditSegment() throws Exception { - var goodCreditPerson = "49002010987"; - var loanEligibilityRequest = """ + fun `returns an APPROVED eligibility result for the provided loan request, since person is in good SEGMENT 2`() { + val goodCreditPerson = "49002010987" + val loanEligibilityRequest = """ { - "ssn": "%s", + "ssn": "$goodCreditPerson", "loanAmount": 4500, "loanPeriodMonths": 36 } - """.formatted(goodCreditPerson); - var postRequest = - post(LOAN_ELIGIBILITY_URL) - .contentType(APPLICATION_JSON) - .content(loanEligibilityRequest); + + """.trimIndent() + + val postRequest = post(LOAN_ELIGIBILITY_URL) + .contentType(APPLICATION_JSON) + .content(loanEligibilityRequest) mockMvc.perform(postRequest) .andExpect(status().isOk()) .andExpect( content().json( """ - { - "result": "APPROVED", - "ssn": "%s", - "loanAmount": 4500, - "loanPeriodMonths": 36 - } - """.formatted(goodCreditPerson) + { + "result": "APPROVED", + "ssn": "$goodCreditPerson", + "loanAmount": 4500, + "loanPeriodMonths": 36 + } + """.trimIndent() ) - ); + ) } @Test - @DisplayName("returns a DENIED eligibility result for the provided loan request, since person is in low SEGMENT 1") - void returns_DENIED_result_forProvidedLoanRequest_sincePersonInLowCreditSegment() throws Exception { - var lowCreditPerson = "49002010976"; - var loanEligibilityRequest = """ + fun `returns a DENIED eligibility result for the provided loan request, since person is in low SEGMENT 1`() { + val lowCreditPerson = "49002010976" + val loanEligibilityRequest = """ { - "ssn": "%s", + "ssn": "$lowCreditPerson", "loanAmount": 4500, "loanPeriodMonths": 36 } - """.formatted(lowCreditPerson); - var postRequest = - post(LOAN_ELIGIBILITY_URL) - .contentType(APPLICATION_JSON) - .content(loanEligibilityRequest); + + """.trimIndent() + + val postRequest = post(LOAN_ELIGIBILITY_URL) + .contentType(APPLICATION_JSON) + .content(loanEligibilityRequest) - var notAcceptableStatus = status().isNotAcceptable(); + val notAcceptableStatus = status().isNotAcceptable() mockMvc.perform(postRequest) .andExpect(notAcceptableStatus) - .andDo(MockMvcResultHandlers.print()) .andExpect( content().json( """ - { - "result": "DENIED", - "ssn": "%s", - "loanAmount": 4500, - "loanPeriodMonths": 36, - "eligibleLoanAmount": 3599 - } - """.formatted(lowCreditPerson) + { + "result": "DENIED", + "ssn": "$lowCreditPerson", + "loanAmount": 4500, + "loanPeriodMonths": 36, + "eligibleLoanAmount": 3599 + } + """.trimIndent() ) - ); + ) } @Test - @DisplayName("returns an INVALID result with validation error details, for an invalid loan request") - void returns_INVALID_result_with_validationErrorDetails_forInvalidLoanRequest() throws Exception { - var ssnWithInvalidChecksum = "49002010968"; - var tooSmallLoanAmount = 1500; - var tooLargeLoanPeriod = 61; - var loanEligibilityRequest = """ + fun `returns an INVALID result with validation error details, for an invalid loan request`() { + val ssnWithInvalidChecksum = "49002010968" + val tooSmallLoanAmount = 1500 + val tooLargeLoanPeriod = 61 + val loanEligibilityRequest = """ { - "ssn": "%s", - "loanAmount": %s, - "loanPeriodMonths": %s + "ssn": "$ssnWithInvalidChecksum", + "loanAmount": $tooSmallLoanAmount, + "loanPeriodMonths": $tooLargeLoanPeriod } - """.formatted(ssnWithInvalidChecksum, tooSmallLoanAmount, tooLargeLoanPeriod); + + """.trimIndent() - var postRequest = - post(LOAN_ELIGIBILITY_URL) - .contentType(APPLICATION_JSON) - .content(loanEligibilityRequest); + val postRequest = post(LOAN_ELIGIBILITY_URL) + .contentType(APPLICATION_JSON) + .content(loanEligibilityRequest) mockMvc.perform(postRequest) .andExpect(status().isBadRequest()) .andExpect( content().json( """ - { - "result": "INVALID", - "errors": [ - "SSN is not valid", - "Loan amount is less than minimum required", - "Loan period is more than maximum allowed" - ], - "ssn": "%s", - "loanAmount": %s, - "loanPeriodMonths": %s - } - """.formatted(ssnWithInvalidChecksum, tooSmallLoanAmount, tooLargeLoanPeriod) + { + "result": "INVALID", + "errors": [ + "SSN is not valid", + "Loan amount is less than minimum required", + "Loan period is more than maximum allowed" + ], + "ssn": "$ssnWithInvalidChecksum", + "loanAmount": $tooSmallLoanAmount, + "loanPeriodMonths": $tooLargeLoanPeriod + } + """.trimIndent() ) - ); + ) } } diff --git a/server/app/src/test/kotlin/ee/rsx/kata/bank/app/integrationtest/loans/validation/SsnValidationIntegrationTest.kt b/server/app/src/test/kotlin/ee/rsx/kata/bank/app/integrationtest/loans/validation/SsnValidationIntegrationTest.kt index c3c3852..7f6d2ab 100644 --- a/server/app/src/test/kotlin/ee/rsx/kata/bank/app/integrationtest/loans/validation/SsnValidationIntegrationTest.kt +++ b/server/app/src/test/kotlin/ee/rsx/kata/bank/app/integrationtest/loans/validation/SsnValidationIntegrationTest.kt @@ -1,69 +1,63 @@ -package ee.rsx.kata.bank.app.integrationtest.loans.validation; - -import ee.rsx.kata.bank.app.Server; -import jakarta.inject.Inject; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.web.servlet.MockMvc; - -import static java.lang.String.format; -import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; - -@SpringBootTest(webEnvironment = RANDOM_PORT, classes = {Server.class}) +package ee.rsx.kata.bank.app.integrationtest.loans.validation + +import ee.rsx.kata.bank.app.Server +import jakarta.inject.Inject +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.content +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = [Server::class]) @AutoConfigureMockMvc @DisplayName("SSN Validation for loans eligibility request") -public class SsnValidationIntegrationTest { +class SsnValidationIntegrationTest { - private static final String SSN_VALIDATION_URL = "/loans/validation/ssn?value=%s"; + companion object { + private const val SSN_VALIDATION_URL = "/loans/validation/ssn?value=" + } @Inject - private MockMvc mockMvc; + private lateinit var mockMvc: MockMvc @Test - @DisplayName("returns OK result for valid provided SSN") - void returns_OK_result_forValidProvidedSsn() throws Exception { - var validProvidedSsn = "49002010965"; - var expectedOkResult = "OK"; + fun `returns OK result for valid provided SSN`() { + val validProvidedSsn = "49002010965" + val expectedOkResult = "OK" - mockMvc.perform(get( - format(SSN_VALIDATION_URL, validProvidedSsn) - )) + mockMvc.perform(get("$SSN_VALIDATION_URL$validProvidedSsn")) .andExpect(status().isOk()) .andExpect( content().json( """ - { - "ssn": "%s", - "status": "%s" - } - """.formatted(validProvidedSsn, expectedOkResult) + { + "ssn": "$validProvidedSsn", + "status": "$expectedOkResult" + } + """.trimIndent() ) - ); + ) } @Test - @DisplayName("returns INVALID result for invalid provided SSN") - void returns_INVALID_result_forInvalidProvidedSsn() throws Exception { - var invalidProvidedSsn = "89002010965"; - var expectedInvalidResult = "INVALID"; + fun `returns INVALID result for invalid provided SSN`() { + val invalidProvidedSsn = "89002010965" + val expectedInvalidResult = "INVALID" - mockMvc.perform(get( - format(SSN_VALIDATION_URL, invalidProvidedSsn) - )) + mockMvc.perform(get("$SSN_VALIDATION_URL$invalidProvidedSsn")) .andExpect(status().isOk()) .andExpect( content().json( """ - { - "ssn": "%s", - "status": "%s" - } - """.formatted(invalidProvidedSsn, expectedInvalidResult) + { + "ssn": "$invalidProvidedSsn", + "status": "$expectedInvalidResult" + } + """.trimIndent() ) - ); + ) } } diff --git a/server/app/src/test/kotlin/ee/rsx/kata/bank/app/integrationtest/loans/validation/ValidationLimitsIntegrationTest.kt b/server/app/src/test/kotlin/ee/rsx/kata/bank/app/integrationtest/loans/validation/ValidationLimitsIntegrationTest.kt index 94b1252..84ac43d 100644 --- a/server/app/src/test/kotlin/ee/rsx/kata/bank/app/integrationtest/loans/validation/ValidationLimitsIntegrationTest.kt +++ b/server/app/src/test/kotlin/ee/rsx/kata/bank/app/integrationtest/loans/validation/ValidationLimitsIntegrationTest.kt @@ -1,30 +1,33 @@ -package ee.rsx.kata.bank.app.integrationtest.loans.validation; +package ee.rsx.kata.bank.app.integrationtest.loans.validation -import ee.rsx.kata.bank.app.Server; -import jakarta.inject.Inject; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.web.servlet.MockMvc; +import ee.rsx.kata.bank.app.Server +import jakarta.inject.Inject +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get +import org.springframework.test.web.servlet.result.MockMvcResultMatchers +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.content +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status -import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; - -@SpringBootTest(webEnvironment = RANDOM_PORT, classes = {Server.class}) +@SpringBootTest(webEnvironment = RANDOM_PORT, classes = [Server::class]) @AutoConfigureMockMvc @DisplayName("Validation limits for loans eligibility request") -public class ValidationLimitsIntegrationTest { +class ValidationLimitsIntegrationTest { - private static final String VALIDATION_LIMITS_URL = "/loans/validation/limits"; + companion object { + private const val VALIDATION_LIMITS_URL = "/loans/validation/limits" + } @Inject - private MockMvc mockMvc; + private lateinit var mockMvc: MockMvc @Test - @DisplayName("returns expected validation limits (min and max loan as well as period") - void returns_expectedValidationLimits_minAndMaxLoanAsWellAsPeriod() throws Exception { + fun `returns expected validation limits (min and max loan as well as period)`() { mockMvc.perform(get(VALIDATION_LIMITS_URL)) .andExpect(status().isOk()) .andExpect( @@ -36,8 +39,10 @@ public class ValidationLimitsIntegrationTest { "minimumLoanPeriodMonths": 12, "maximumLoanPeriodMonths": 60 } + """ + .trimIndent() ) - ); + ) } } From 05bcd4708b649f545db53479023b7959453eacb6 Mon Sep 17 00:00:00 2001 From: Risto Uibo Date: Tue, 25 Jun 2024 12:39:39 +0300 Subject: [PATCH 12/17] build | consolidate config of subprojects --- server/app/build.gradle | 18 --------------- server/build.gradle | 29 ++++++++++++++++-------- server/gradle.properties | 3 --- server/loans/api/build.gradle | 19 ---------------- server/loans/build.gradle | 3 --- server/loans/core/build.gradle | 21 ----------------- server/loans/gw-eligibility/build.gradle | 21 ----------------- server/loans/gw-validation/build.gradle | 21 ----------------- 8 files changed, 20 insertions(+), 115 deletions(-) diff --git a/server/app/build.gradle b/server/app/build.gradle index f0a8e3d..ea63586 100644 --- a/server/app/build.gradle +++ b/server/app/build.gradle @@ -1,14 +1,8 @@ -import static org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 - plugins { - id 'org.jetbrains.kotlin.jvm' version "${kotlinVersion}" id 'org.springframework.boot' version "${springBootVersion}" id 'io.spring.dependency-management' version "${springDependencyManagementVersion}" } -group = 'ee.rsx.kata.bank' -version = '1.0.0-SNAPSHOT' - dependencies { implementation project(':loans-api') runtimeOnly project(':loans') @@ -17,15 +11,3 @@ dependencies { testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation "com.tngtech.archunit:archunit-junit5:${archUnitVersion}" } - -compileKotlin { - compilerOptions { - jvmTarget = JVM_17 - } -} - -compileTestKotlin { - compilerOptions { - jvmTarget = JVM_17 - } -} diff --git a/server/build.gradle b/server/build.gradle index e86ba87..33bfa36 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -1,11 +1,13 @@ +import static org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 + plugins { - id 'java' - id 'io.freefair.lombok' version "${lombokPluginVersion}" + id 'org.jetbrains.kotlin.jvm' version "${kotlinVersion}" } -group = 'ee.rsx.kata.bank' -version = '1.0.0-SNAPSHOT' -sourceCompatibility = '17' +allprojects { + group = 'ee.rsx.kata.bank' + version = '1.0.0-SNAPSHOT' +} repositories { mavenCentral() @@ -13,8 +15,7 @@ repositories { subprojects { - apply plugin: 'java' - apply plugin: 'io.freefair.lombok' + apply plugin: 'org.jetbrains.kotlin.jvm' apply plugin: 'jacoco' apply plugin: 'idea' @@ -26,8 +27,6 @@ subprojects { } dependencies { - compileOnly "org.projectlombok:lombok:${lombokVersion}" - implementation "jakarta.inject:jakarta.inject-api:${jakartaInjectVersion}" implementation "jakarta.annotation:jakarta.annotation-api:${jakartaAnnotationVersion}" implementation "org.apache.commons:commons-collections4:${apacheCommonsVersion}" @@ -42,6 +41,18 @@ subprojects { testImplementation "org.assertj:assertj-core:${assertjVersion}" } + compileKotlin { + compilerOptions { + jvmTarget = JVM_17 + } + } + + compileTestKotlin { + compilerOptions { + jvmTarget = JVM_17 + } + } + tasks.named('test') { useJUnitPlatform() } diff --git a/server/gradle.properties b/server/gradle.properties index 8f2227f..592f1e3 100644 --- a/server/gradle.properties +++ b/server/gradle.properties @@ -11,9 +11,6 @@ junitJupiterVersion=5.10.2 kotlinVersion=2.0.0 -lombokPluginVersion=8.6 -lombokVersion=1.18.32 - mockitoVersion=5.12.0 mockitoKotlinVersion=5.3.1 diff --git a/server/loans/api/build.gradle b/server/loans/api/build.gradle index bef645b..8b13789 100644 --- a/server/loans/api/build.gradle +++ b/server/loans/api/build.gradle @@ -1,20 +1 @@ -import static org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 -plugins { - id 'org.jetbrains.kotlin.jvm' version "${kotlinVersion}" -} - -group = 'ee.rsx.kata.bank' -version = '1.0.0-SNAPSHOT' - -compileKotlin { - compilerOptions { - jvmTarget = JVM_17 - } -} - -compileTestKotlin { - compilerOptions { - jvmTarget = JVM_17 - } -} diff --git a/server/loans/build.gradle b/server/loans/build.gradle index a018bc7..b0dc5cb 100644 --- a/server/loans/build.gradle +++ b/server/loans/build.gradle @@ -1,6 +1,3 @@ -group = 'ee.rsx.kata.bank' -version = '1.0.0-SNAPSHOT' - dependencies { runtimeOnly project(':loans-api') runtimeOnly project(':loans-core') diff --git a/server/loans/core/build.gradle b/server/loans/core/build.gradle index 97bdd4e..5d87ff2 100644 --- a/server/loans/core/build.gradle +++ b/server/loans/core/build.gradle @@ -1,26 +1,5 @@ -import static org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 - -plugins { - id 'org.jetbrains.kotlin.jvm' version "${kotlinVersion}" -} - -group = 'ee.rsx.kata.bank' -version = '1.0.0-SNAPSHOT' - dependencies { implementation project(":loans-api") testImplementation "org.mockito.kotlin:mockito-kotlin:${mockitoKotlinVersion}" } - -compileKotlin { - compilerOptions { - jvmTarget = JVM_17 - } -} - -compileTestKotlin { - compilerOptions { - jvmTarget = JVM_17 - } -} diff --git a/server/loans/gw-eligibility/build.gradle b/server/loans/gw-eligibility/build.gradle index 31fb1e1..f2ef39a 100644 --- a/server/loans/gw-eligibility/build.gradle +++ b/server/loans/gw-eligibility/build.gradle @@ -1,24 +1,3 @@ -import static org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 - -plugins { - id 'org.jetbrains.kotlin.jvm' version "${kotlinVersion}" -} - -group = 'ee.rsx.kata.bank' -version = '1.0.0-SNAPSHOT' - dependencies { implementation project(":loans-core") } - -compileKotlin { - compilerOptions { - jvmTarget = JVM_17 - } -} - -compileTestKotlin { - compilerOptions { - jvmTarget = JVM_17 - } -} diff --git a/server/loans/gw-validation/build.gradle b/server/loans/gw-validation/build.gradle index 31fb1e1..f2ef39a 100644 --- a/server/loans/gw-validation/build.gradle +++ b/server/loans/gw-validation/build.gradle @@ -1,24 +1,3 @@ -import static org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 - -plugins { - id 'org.jetbrains.kotlin.jvm' version "${kotlinVersion}" -} - -group = 'ee.rsx.kata.bank' -version = '1.0.0-SNAPSHOT' - dependencies { implementation project(":loans-core") } - -compileKotlin { - compilerOptions { - jvmTarget = JVM_17 - } -} - -compileTestKotlin { - compilerOptions { - jvmTarget = JVM_17 - } -} From 429cf03a984a1ece0e1b3c653e1234ec78bce478 Mon Sep 17 00:00:00 2001 From: Risto Uibo Date: Tue, 25 Jun 2024 12:41:38 +0300 Subject: [PATCH 13/17] build | set target to Java 21 --- server/build.gradle | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/server/build.gradle b/server/build.gradle index 33bfa36..94ffebe 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -1,4 +1,4 @@ -import static org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 +import static org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_21 plugins { id 'org.jetbrains.kotlin.jvm' version "${kotlinVersion}" @@ -19,8 +19,7 @@ subprojects { apply plugin: 'jacoco' apply plugin: 'idea' - sourceCompatibility = '17' - targetCompatibility = '17' + targetCompatibility = '21' repositories { mavenCentral() @@ -43,13 +42,13 @@ subprojects { compileKotlin { compilerOptions { - jvmTarget = JVM_17 + jvmTarget = JVM_21 } } compileTestKotlin { compilerOptions { - jvmTarget = JVM_17 + jvmTarget = JVM_21 } } From 7eda8eadfddd6cdb39d377fbcb22859554919fc2 Mon Sep 17 00:00:00 2001 From: Risto Uibo Date: Tue, 25 Jun 2024 12:59:31 +0300 Subject: [PATCH 14/17] server | refactor towards improved Kotlin --- .../eligibility/LoanEligibilityEndpoint.kt | 8 ++-- .../loans/validation/ValidationEndpoints.kt | 6 +-- .../eligibility/CalculateLoanEligibility.kt | 2 +- .../ssn/ValidateSocialSecurityNumber.kt | 2 +- .../limits/gateway/DetermineEligiblePeriod.kt | 2 +- ...{LoanConfigGateway.kt => GetLoanConfig.kt} | 4 +- .../segment/gateway/FindCreditSegment.kt | 2 +- .../usecases/LoanEligibilityCalculation.kt | 29 +++++++------ .../usecases/ProvideLoanConfiguration.kt | 6 +-- .../kata/bank/loans/usecases/SsnValidation.kt | 2 +- .../LoanEligibilityCalculationTestKotlin.kt | 42 +++++++++---------- .../bank/loans/usecases/SsnValidationTest.kt | 12 +++--- .../InMemoryCreditSegmentStorageAdapter.kt | 4 +- .../FirstEligiblePeriodAdapter.kt | 6 +-- .../FirstEligiblePeriodAdapterTest.kt | 4 +- .../validation/LoanConfigurationAdapter.kt | 6 +-- 16 files changed, 69 insertions(+), 68 deletions(-) rename server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/domain/limits/gateway/{LoanConfigGateway.kt => GetLoanConfig.kt} (62%) diff --git a/server/app/src/main/kotlin/ee/rsx/kata/bank/app/rest/loans/eligibility/LoanEligibilityEndpoint.kt b/server/app/src/main/kotlin/ee/rsx/kata/bank/app/rest/loans/eligibility/LoanEligibilityEndpoint.kt index 884a65f..3f60c9b 100644 --- a/server/app/src/main/kotlin/ee/rsx/kata/bank/app/rest/loans/eligibility/LoanEligibilityEndpoint.kt +++ b/server/app/src/main/kotlin/ee/rsx/kata/bank/app/rest/loans/eligibility/LoanEligibilityEndpoint.kt @@ -19,12 +19,14 @@ import org.springframework.web.bind.annotation.RestController @RestController @RequestMapping("/loans/eligibility") class LoanEligibilityEndpoint( - private val calculateLoanEligibility: CalculateLoanEligibility + private val calculateEligibility: CalculateLoanEligibility ) { @PostMapping - fun calculateLoanEligibility(@RequestBody request: LoanEligibilityRequestDTO): ResponseEntity = - with(calculateLoanEligibility.on(request)) { + fun calculateLoanEligibility( + @RequestBody request: LoanEligibilityRequestDTO + ): ResponseEntity = + with(calculateEligibility(request)) { val httpStatus = when (result) { APPROVED -> OK INVALID -> BAD_REQUEST diff --git a/server/app/src/main/kotlin/ee/rsx/kata/bank/app/rest/loans/validation/ValidationEndpoints.kt b/server/app/src/main/kotlin/ee/rsx/kata/bank/app/rest/loans/validation/ValidationEndpoints.kt index 7b1d324..ce69e1e 100644 --- a/server/app/src/main/kotlin/ee/rsx/kata/bank/app/rest/loans/validation/ValidationEndpoints.kt +++ b/server/app/src/main/kotlin/ee/rsx/kata/bank/app/rest/loans/validation/ValidationEndpoints.kt @@ -10,13 +10,13 @@ import org.springframework.web.bind.annotation.RestController @RestController @RequestMapping("/loans/validation") class ValidationEndpoints( - private val loadValidationLimits: LoadValidationLimits, + private val loadLimits: LoadValidationLimits, private val validateSocialSecurityNumber: ValidateSocialSecurityNumber ) { @GetMapping(value = ["/limits"]) - fun loadValidationLimits() = loadValidationLimits.invoke() + fun loadValidationLimits() = loadLimits() @GetMapping(value = ["/ssn"]) - fun validateSsn(@RequestParam("value") value: String) = validateSocialSecurityNumber.on(value) + fun validateSsn(@RequestParam("value") value: String) = validateSocialSecurityNumber(value) } diff --git a/server/loans/api/src/main/kotlin/ee/rsx/kata/bank/loans/eligibility/CalculateLoanEligibility.kt b/server/loans/api/src/main/kotlin/ee/rsx/kata/bank/loans/eligibility/CalculateLoanEligibility.kt index 63004b7..9b455cf 100644 --- a/server/loans/api/src/main/kotlin/ee/rsx/kata/bank/loans/eligibility/CalculateLoanEligibility.kt +++ b/server/loans/api/src/main/kotlin/ee/rsx/kata/bank/loans/eligibility/CalculateLoanEligibility.kt @@ -1,5 +1,5 @@ package ee.rsx.kata.bank.loans.eligibility fun interface CalculateLoanEligibility { - fun on(eligibilityRequest: LoanEligibilityRequestDTO): LoanEligibilityResultDTO + operator fun invoke(eligibilityRequest: LoanEligibilityRequestDTO): LoanEligibilityResultDTO } diff --git a/server/loans/api/src/main/kotlin/ee/rsx/kata/bank/loans/validation/ssn/ValidateSocialSecurityNumber.kt b/server/loans/api/src/main/kotlin/ee/rsx/kata/bank/loans/validation/ssn/ValidateSocialSecurityNumber.kt index 0d8d867..1e7ad5e 100644 --- a/server/loans/api/src/main/kotlin/ee/rsx/kata/bank/loans/validation/ssn/ValidateSocialSecurityNumber.kt +++ b/server/loans/api/src/main/kotlin/ee/rsx/kata/bank/loans/validation/ssn/ValidateSocialSecurityNumber.kt @@ -1,5 +1,5 @@ package ee.rsx.kata.bank.loans.validation.ssn fun interface ValidateSocialSecurityNumber { - fun on(ssn: String): SsnValidationResultDTO + operator fun invoke(ssn: String): SsnValidationResultDTO } diff --git a/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/domain/limits/gateway/DetermineEligiblePeriod.kt b/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/domain/limits/gateway/DetermineEligiblePeriod.kt index bec747d..67c9e0b 100644 --- a/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/domain/limits/gateway/DetermineEligiblePeriod.kt +++ b/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/domain/limits/gateway/DetermineEligiblePeriod.kt @@ -4,5 +4,5 @@ import ee.rsx.kata.bank.loans.domain.segment.CreditSegment import java.util.* fun interface DetermineEligiblePeriod { - fun forLoan(amount: Int, creditSegment: CreditSegment): Optional + operator fun invoke(forAmount: Int, forSegment: CreditSegment): Optional } diff --git a/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/domain/limits/gateway/LoanConfigGateway.kt b/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/domain/limits/gateway/GetLoanConfig.kt similarity index 62% rename from server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/domain/limits/gateway/LoanConfigGateway.kt rename to server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/domain/limits/gateway/GetLoanConfig.kt index a553ee0..991f80c 100644 --- a/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/domain/limits/gateway/LoanConfigGateway.kt +++ b/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/domain/limits/gateway/GetLoanConfig.kt @@ -2,6 +2,6 @@ package ee.rsx.kata.bank.loans.domain.limits.gateway import ee.rsx.kata.bank.loans.domain.limits.LoanLimitsConfig -fun interface LoanConfigGateway { - fun loadLimits(): LoanLimitsConfig +fun interface GetLoanConfig { + operator fun invoke(): LoanLimitsConfig } diff --git a/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/domain/segment/gateway/FindCreditSegment.kt b/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/domain/segment/gateway/FindCreditSegment.kt index b09312f..b796f13 100644 --- a/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/domain/segment/gateway/FindCreditSegment.kt +++ b/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/domain/segment/gateway/FindCreditSegment.kt @@ -5,5 +5,5 @@ import ee.rsx.kata.bank.loans.domain.ssn.SocialSecurityNumber import java.util.* fun interface FindCreditSegment { - fun forPerson(ssn: SocialSecurityNumber): Optional + operator fun invoke(forPerson: SocialSecurityNumber): Optional } diff --git a/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/usecases/LoanEligibilityCalculation.kt b/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/usecases/LoanEligibilityCalculation.kt index 9d13a11..ff172b2 100644 --- a/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/usecases/LoanEligibilityCalculation.kt +++ b/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/usecases/LoanEligibilityCalculation.kt @@ -18,7 +18,6 @@ import ee.rsx.kata.bank.loans.validation.ssn.ValidateSocialSecurityNumber import ee.rsx.kata.bank.loans.validation.ssn.ValidationStatus.OK import jakarta.annotation.Nullable import jakarta.inject.Named -import org.apache.commons.collections4.CollectionUtils import java.util.* import java.util.stream.Stream import kotlin.math.min @@ -31,12 +30,12 @@ internal class LoanEligibilityCalculation( private val determineEligiblePeriod: DetermineEligiblePeriod ) : CalculateLoanEligibility { - override fun on(eligibilityRequest: LoanEligibilityRequestDTO): LoanEligibilityResultDTO { - val limits = loadValidationLimits.invoke() + override fun invoke(eligibilityRequest: LoanEligibilityRequestDTO): LoanEligibilityResultDTO { + val limits = loadValidationLimits() val validationErrors = validate(eligibilityRequest, limits) val (status, eligibleAmount, eligiblePeriod) = - if (CollectionUtils.isEmpty(validationErrors)) calculateEligibility( + if (validationErrors.isNullOrEmpty()) calculateEligibility( eligibilityRequest, limits ) else @@ -70,7 +69,7 @@ internal class LoanEligibilityCalculation( } private fun checkForSsnErrorIn(request: LoanEligibilityRequestDTO): Optional { - val ssnValidity = validateSocialSecurityNumber.on(request.ssn).status + val ssnValidity = validateSocialSecurityNumber(request.ssn).status return if (ssnValidity == OK) Optional.empty() @@ -80,27 +79,27 @@ internal class LoanEligibilityCalculation( private fun checkForAmountErrorIn(request: LoanEligibilityRequestDTO, limits: ValidationLimitsDTO): Optional { val amount = request.loanAmount - if (amount < limits.minimumLoanAmount) { - return Optional.of("Loan amount is less than minimum required") - } - return if (amount > limits.maximumLoanAmount) { + + return if (amount < limits.minimumLoanAmount) { + Optional.of("Loan amount is less than minimum required") + } else if (amount > limits.maximumLoanAmount) { Optional.of("Loan amount is more than maximum allowed") } else Optional.empty() } private fun checkForPeriodErrorIn(request: LoanEligibilityRequestDTO, limits: ValidationLimitsDTO): Optional { val period = request.loanPeriodMonths - if (period < limits.minimumLoanPeriodMonths) { - return Optional.of("Loan period is less than minimum required") - } - return if (period > limits.maximumLoanPeriodMonths) { + + return if (period < limits.minimumLoanPeriodMonths) { + Optional.of("Loan period is less than minimum required") + } else if (period > limits.maximumLoanPeriodMonths) { Optional.of("Loan period is more than maximum allowed") } else Optional.empty() } private fun calculateEligibility(request: LoanEligibilityRequestDTO, limits: ValidationLimitsDTO): LoanEligibility { val ssn = SocialSecurityNumber(request.ssn) - val creditSegment = findCreditSegment.forPerson(ssn) + val creditSegment = findCreditSegment(forPerson = ssn) return creditSegment .map { @@ -123,7 +122,7 @@ internal class LoanEligibilityCalculation( return LoanEligibility(status) } - val newPeriod = determineEligiblePeriod.forLoan(request.loanAmount, segment) + val newPeriod = determineEligiblePeriod(forAmount = request.loanAmount, forSegment = segment) .filter { limits.minimumLoanPeriodMonths <= it && it <= limits.maximumLoanPeriodMonths } val newAmount = newPeriod.flatMap { determineEligibleAmountFor(limits, segment, it) } diff --git a/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/usecases/ProvideLoanConfiguration.kt b/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/usecases/ProvideLoanConfiguration.kt index 4cfbeff..117369d 100644 --- a/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/usecases/ProvideLoanConfiguration.kt +++ b/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/usecases/ProvideLoanConfiguration.kt @@ -1,17 +1,17 @@ package ee.rsx.kata.bank.loans.usecases import ee.rsx.kata.bank.loans.domain.limits.LoanLimitsConfig -import ee.rsx.kata.bank.loans.domain.limits.gateway.LoanConfigGateway +import ee.rsx.kata.bank.loans.domain.limits.gateway.GetLoanConfig import ee.rsx.kata.bank.loans.validation.limits.LoadValidationLimits import ee.rsx.kata.bank.loans.validation.limits.ValidationLimitsDTO import jakarta.inject.Named @Named internal class ProvideLoanConfiguration( - private val gateway: LoanConfigGateway + private val getLoanConfig: GetLoanConfig ) : LoadValidationLimits { - override fun invoke() = gateway.loadLimits().toDto() + override fun invoke() = getLoanConfig().toDto() private fun LoanLimitsConfig.toDto() = ValidationLimitsDTO( diff --git a/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/usecases/SsnValidation.kt b/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/usecases/SsnValidation.kt index e1fbf1b..28b90f5 100644 --- a/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/usecases/SsnValidation.kt +++ b/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/usecases/SsnValidation.kt @@ -11,7 +11,7 @@ import java.time.format.DateTimeParseException @Named internal class SsnValidation : ValidateSocialSecurityNumber { - override fun on(ssn: String): SsnValidationResultDTO = + override fun invoke(ssn: String): SsnValidationResultDTO = try { val validSsn = SocialSecurityNumber(ssn) okResultWith(validSsn.value) diff --git a/server/loans/core/src/test/kotlin/ee/rsx/kata/bank/loans/usecases/LoanEligibilityCalculationTestKotlin.kt b/server/loans/core/src/test/kotlin/ee/rsx/kata/bank/loans/usecases/LoanEligibilityCalculationTestKotlin.kt index 1d7450e..b9f4f61 100644 --- a/server/loans/core/src/test/kotlin/ee/rsx/kata/bank/loans/usecases/LoanEligibilityCalculationTestKotlin.kt +++ b/server/loans/core/src/test/kotlin/ee/rsx/kata/bank/loans/usecases/LoanEligibilityCalculationTestKotlin.kt @@ -56,11 +56,11 @@ internal class LoanEligibilityCalculationTestKotlin { @BeforeEach fun setup() { - whenever(validateSocialSecurityNumber.on(any())) + whenever(validateSocialSecurityNumber(any())) .thenAnswer(okResultWithProvidedSsn()) - whenever(loadValidationLimits.invoke()) + whenever(loadValidationLimits()) .thenReturn(TEST_VALIDATION_LIMITS) - whenever(findCreditSegment.forPerson(any())) + whenever(findCreditSegment(any())) .thenReturn(Optional.empty()) } @@ -73,7 +73,7 @@ internal class LoanEligibilityCalculationTestKotlin { val validRequest = testRequest().create() whenCreditSegmentNotFoundForPerson(DEFAULT_SSN) - val result = calculateLoanEligibility.on(validRequest) + val result = calculateLoanEligibility.invoke(validRequest) assertThat(result) .isEqualTo( @@ -87,7 +87,7 @@ internal class LoanEligibilityCalculationTestKotlin { val tooLowCreditModifier = 100 whenCreditSegmentFoundForPerson(DEFAULT_SSN, SEGMENT_3, tooLowCreditModifier) - val result = calculateLoanEligibility.on(validRequest) + val result = calculateLoanEligibility.invoke(validRequest) assertThat(result) .isEqualTo( @@ -102,7 +102,7 @@ internal class LoanEligibilityCalculationTestKotlin { val validRequest = testRequest().amount(2000).period(20).create() whenCreditSegmentFoundForPerson(DEFAULT_SSN, SEGMENT_3, 100) - val result = calculateLoanEligibility.on(validRequest) + val result = calculateLoanEligibility.invoke(validRequest) assertThat(result) .isEqualTo( @@ -122,7 +122,7 @@ internal class LoanEligibilityCalculationTestKotlin { val segment = whenCreditSegmentFoundForPerson(DEFAULT_SSN, SEGMENT_3, creditModifier) val newEligiblePeriod = whenNewEligiblePeriodDeterminedFor(validRequest, segment, 51) - val result = calculateLoanEligibility.on(validRequest) + val result = calculateLoanEligibility.invoke(validRequest) val expectedNewEligibleLoanAmount = newEligiblePeriod * creditModifier - 1 assertThat(result) @@ -145,7 +145,7 @@ internal class LoanEligibilityCalculationTestKotlin { val segment = whenCreditSegmentFoundForPerson(DEFAULT_SSN, SEGMENT_3, creditModifier) whenNewEligiblePeriodDeterminedFor(validRequest, segment, 91) - val result = calculateLoanEligibility.on(validRequest) + val result = calculateLoanEligibility.invoke(validRequest) assertThat(result) .isEqualTo( @@ -161,7 +161,7 @@ internal class LoanEligibilityCalculationTestKotlin { private fun whenNewEligiblePeriodDeterminedFor( validRequest: LoanEligibilityRequestDTO, segment: CreditSegment, newEligiblePeriod: Int ): Int { - `when`(determineEligiblePeriod.forLoan(validRequest.loanAmount, segment)) + `when`(determineEligiblePeriod(forAmount = validRequest.loanAmount, forSegment = segment)) .thenReturn(Optional.of(newEligiblePeriod)) return newEligiblePeriod } @@ -176,7 +176,7 @@ internal class LoanEligibilityCalculationTestKotlin { val validRequest = testRequest().create() whenCreditSegmentFoundForPerson(DEFAULT_SSN, SEGMENT_3, 1000) - val result = calculateLoanEligibility.on(validRequest) + val result = calculateLoanEligibility.invoke(validRequest) assertThat(result) .isEqualTo( @@ -193,7 +193,7 @@ internal class LoanEligibilityCalculationTestKotlin { whenCreditSegmentFoundForPerson(DEFAULT_SSN, SEGMENT_1, 100) - val result = calculateLoanEligibility.on(validRequest) + val result = calculateLoanEligibility.invoke(validRequest) assertThat(result) .isEqualTo( expectedResult(APPROVED) @@ -209,7 +209,7 @@ internal class LoanEligibilityCalculationTestKotlin { val validRequest = testRequest().amount(smallAmount).create() whenCreditSegmentFoundForPerson(DEFAULT_SSN, SEGMENT_1, 60) - val result = calculateLoanEligibility.on(validRequest) + val result = calculateLoanEligibility.invoke(validRequest) assertThat(result) .isEqualTo( @@ -235,7 +235,7 @@ internal class LoanEligibilityCalculationTestKotlin { whenSsnValidationFailsFor(invalidSsn) val invalidRequest = testRequest().ssn(invalidSsn).create() - val result = calculateLoanEligibility.on(invalidRequest) + val result = calculateLoanEligibility.invoke(invalidRequest) assertThat(result).isEqualTo( expectedResult(withStatus = INVALID) @@ -246,7 +246,7 @@ internal class LoanEligibilityCalculationTestKotlin { } private fun whenSsnValidationFailsFor(invalidSsn: String) { - `when`(validateSocialSecurityNumber.on(invalidSsn)) + `when`(validateSocialSecurityNumber(invalidSsn)) .thenReturn(invalidResultWith(invalidSsn)) } @@ -255,7 +255,7 @@ internal class LoanEligibilityCalculationTestKotlin { val tooSmallLoanAmount = MINIMUM_REQUIRED_LOAN_AMOUNT - 1 val invalidRequest = testRequest().amount(tooSmallLoanAmount).create() - val result = calculateLoanEligibility.on(invalidRequest) + val result = calculateLoanEligibility.invoke(invalidRequest) assertThat(result).isEqualTo( expectedResult(withStatus = INVALID).amount(tooSmallLoanAmount) @@ -269,7 +269,7 @@ internal class LoanEligibilityCalculationTestKotlin { val tooLargeLoanAmount = MAXIMUM_ALLOWED_LOAN_AMOUNT + 1 val invalidRequest = testRequest().amount(tooLargeLoanAmount).create() - val result = calculateLoanEligibility.on(invalidRequest) + val result = calculateLoanEligibility.invoke(invalidRequest) assertThat(result) .isEqualTo( @@ -286,7 +286,7 @@ internal class LoanEligibilityCalculationTestKotlin { val tooSmallLoanPeriod = MINIMUM_REQUIRED_LOAN_PERIOD - 1 val invalidRequest = testRequest().period(tooSmallLoanPeriod).create() - val result = calculateLoanEligibility.on(invalidRequest) + val result = calculateLoanEligibility.invoke(invalidRequest) assertThat(result) .isEqualTo( @@ -302,7 +302,7 @@ internal class LoanEligibilityCalculationTestKotlin { val tooLargeLoanPeriod = MAXIMUM_ALLOWED_LOAN_PERIOD + 1 val invalidRequest = testRequest().period(tooLargeLoanPeriod).create() - val result = calculateLoanEligibility.on(invalidRequest) + val result = calculateLoanEligibility.invoke(invalidRequest) assertThat(result) .isEqualTo( @@ -321,7 +321,7 @@ internal class LoanEligibilityCalculationTestKotlin { val tooLargeLoanPeriod = MAXIMUM_ALLOWED_LOAN_PERIOD + 1 val invalidRequest = testRequest().ssn(invalidSsn).amount(tooSmallLoanAmount).period(tooLargeLoanPeriod).create() - val result = calculateLoanEligibility.on(invalidRequest) + val result = calculateLoanEligibility.invoke(invalidRequest) assertThat(result) .isEqualTo( @@ -342,14 +342,14 @@ internal class LoanEligibilityCalculationTestKotlin { private fun whenCreditSegmentFoundForPerson(withSsn: String, segmentType: CreditSegmentType, creditModifier: Int): CreditSegment { val ssn = SocialSecurityNumber(withSsn) val foundSegment = CreditSegment(ssn, segmentType, creditModifier) - `when`(findCreditSegment.forPerson(ssn)) + `when`(findCreditSegment(ssn)) .thenReturn(Optional.of(foundSegment)) return foundSegment } private fun whenCreditSegmentNotFoundForPerson(withSsn: String) { val ssn = SocialSecurityNumber(withSsn) - `when`(findCreditSegment.forPerson(ssn)) + `when`(findCreditSegment(ssn)) .thenReturn(Optional.empty()) } diff --git a/server/loans/core/src/test/kotlin/ee/rsx/kata/bank/loans/usecases/SsnValidationTest.kt b/server/loans/core/src/test/kotlin/ee/rsx/kata/bank/loans/usecases/SsnValidationTest.kt index d4f5e72..951af9e 100644 --- a/server/loans/core/src/test/kotlin/ee/rsx/kata/bank/loans/usecases/SsnValidationTest.kt +++ b/server/loans/core/src/test/kotlin/ee/rsx/kata/bank/loans/usecases/SsnValidationTest.kt @@ -12,18 +12,18 @@ import org.junit.jupiter.api.Test @DisplayName("SSN Validation") internal class SsnValidationTest { - private lateinit var ssnValidation: SsnValidation + private lateinit var validateSsn: SsnValidation @BeforeEach fun setup() { - ssnValidation = SsnValidation() + validateSsn = SsnValidation() } @Test fun `result is OK, for a given valid SSN value`() { val validSsnValue = "50212104262" - val result = ssnValidation.on(validSsnValue) + val result = validateSsn(validSsnValue) assertThat(result).isEqualTo( SsnValidationResultDTO(validSsnValue, OK) @@ -39,7 +39,7 @@ internal class SsnValidationTest { val invalidDate = "021310" val invalidSsnValue = "5${invalidDate}4262" - val result = ssnValidation.on(invalidSsnValue) + val result = validateSsn(invalidSsnValue) assertThat(result).isEqualTo( expectedInvalidResultFor(invalidSsnValue) @@ -51,7 +51,7 @@ internal class SsnValidationTest { val invalidChecksum = "3" val invalidSsnValue = "5021210426${invalidChecksum}" - val result = ssnValidation.on(invalidSsnValue) + val result = validateSsn(invalidSsnValue) assertThat(result).isEqualTo( expectedInvalidResultFor(invalidSsnValue) @@ -63,7 +63,7 @@ internal class SsnValidationTest { val invalidCenturyPrefix = "7" val invalidSsnValue = "${invalidCenturyPrefix}0212104262" - val result = ssnValidation.on(invalidSsnValue) + val result = validateSsn(invalidSsnValue) assertThat(result).isEqualTo( expectedInvalidResultFor(invalidSsnValue) diff --git a/server/loans/gw-eligibility/src/main/kotlin/ee/rsx/kata/bank/loans/adapter/eligibility/creditsegment/InMemoryCreditSegmentStorageAdapter.kt b/server/loans/gw-eligibility/src/main/kotlin/ee/rsx/kata/bank/loans/adapter/eligibility/creditsegment/InMemoryCreditSegmentStorageAdapter.kt index ce2fae1..fba2325 100644 --- a/server/loans/gw-eligibility/src/main/kotlin/ee/rsx/kata/bank/loans/adapter/eligibility/creditsegment/InMemoryCreditSegmentStorageAdapter.kt +++ b/server/loans/gw-eligibility/src/main/kotlin/ee/rsx/kata/bank/loans/adapter/eligibility/creditsegment/InMemoryCreditSegmentStorageAdapter.kt @@ -14,8 +14,8 @@ import java.util.* @Named internal class InMemoryCreditSegmentStorageAdapter : FindCreditSegment { - override fun forPerson(ssn: SocialSecurityNumber) = - Optional.ofNullable(creditSegmentOfPerson[ssn.value]) + override fun invoke(forPerson: SocialSecurityNumber) = + Optional.ofNullable(creditSegmentOfPerson[forPerson.value]) companion object { private val creditSegmentOfPerson = mapOf( diff --git a/server/loans/gw-eligibility/src/main/kotlin/ee/rsx/kata/bank/loans/adapter/eligibility/eligibleperiod/FirstEligiblePeriodAdapter.kt b/server/loans/gw-eligibility/src/main/kotlin/ee/rsx/kata/bank/loans/adapter/eligibility/eligibleperiod/FirstEligiblePeriodAdapter.kt index 2aae427..5e9bd2e 100644 --- a/server/loans/gw-eligibility/src/main/kotlin/ee/rsx/kata/bank/loans/adapter/eligibility/eligibleperiod/FirstEligiblePeriodAdapter.kt +++ b/server/loans/gw-eligibility/src/main/kotlin/ee/rsx/kata/bank/loans/adapter/eligibility/eligibleperiod/FirstEligiblePeriodAdapter.kt @@ -11,11 +11,11 @@ import kotlin.math.floor @Named internal class FirstEligiblePeriodAdapter : DetermineEligiblePeriod { - override fun forLoan(amount: Int, creditSegment: CreditSegment): Optional = - if (creditSegment.isDebt) + override fun invoke(forAmount: Int, forSegment: CreditSegment): Optional = + if (forSegment.isDebt) empty() else - of(calculateFirstMinimumPeriodEligibleFor(amount, creditSegment)) + of(calculateFirstMinimumPeriodEligibleFor(forAmount, forSegment)) private fun calculateFirstMinimumPeriodEligibleFor(amount: Int, creditSegment: CreditSegment): Int { val firstPeriod = floor(amount.toDouble() / creditSegment.creditModifier) diff --git a/server/loans/gw-eligibility/src/test/kotlin/ee/rsx/kata/bank/loans/adapter/eligibility/eligibleperiod/FirstEligiblePeriodAdapterTest.kt b/server/loans/gw-eligibility/src/test/kotlin/ee/rsx/kata/bank/loans/adapter/eligibility/eligibleperiod/FirstEligiblePeriodAdapterTest.kt index 9e90e78..c5ce575 100644 --- a/server/loans/gw-eligibility/src/test/kotlin/ee/rsx/kata/bank/loans/adapter/eligibility/eligibleperiod/FirstEligiblePeriodAdapterTest.kt +++ b/server/loans/gw-eligibility/src/test/kotlin/ee/rsx/kata/bank/loans/adapter/eligibility/eligibleperiod/FirstEligiblePeriodAdapterTest.kt @@ -25,7 +25,7 @@ internal class FirstEligiblePeriodAdapterTest { val creditModifier = 100 val segment = CreditSegment(SocialSecurityNumber("49002010976"), SEGMENT_1, creditModifier) - val eligiblePeriod = determineEligiblePeriod.forLoan(requestedLoan, segment) + val eligiblePeriod = determineEligiblePeriod(forAmount = requestedLoan, forSegment = segment) assertThat(eligiblePeriod) .isPresent() @@ -38,7 +38,7 @@ internal class FirstEligiblePeriodAdapterTest { val creditModifier = 100 val debtSegment = CreditSegment(SocialSecurityNumber("49002010976"), DEBT, creditModifier) - val eligiblePeriod = determineEligiblePeriod.forLoan(requestedLoan, debtSegment) + val eligiblePeriod = determineEligiblePeriod(forAmount = requestedLoan, forSegment = debtSegment) assertThat(eligiblePeriod) .isNotPresent() diff --git a/server/loans/gw-validation/src/main/kotlin/ee/rsx/kata/bank/loans/adapter/validation/LoanConfigurationAdapter.kt b/server/loans/gw-validation/src/main/kotlin/ee/rsx/kata/bank/loans/adapter/validation/LoanConfigurationAdapter.kt index 7bf3c72..7e40b82 100644 --- a/server/loans/gw-validation/src/main/kotlin/ee/rsx/kata/bank/loans/adapter/validation/LoanConfigurationAdapter.kt +++ b/server/loans/gw-validation/src/main/kotlin/ee/rsx/kata/bank/loans/adapter/validation/LoanConfigurationAdapter.kt @@ -1,13 +1,13 @@ package ee.rsx.kata.bank.loans.adapter.validation import ee.rsx.kata.bank.loans.domain.limits.LoanLimitsConfig -import ee.rsx.kata.bank.loans.domain.limits.gateway.LoanConfigGateway +import ee.rsx.kata.bank.loans.domain.limits.gateway.GetLoanConfig import jakarta.inject.Named @Named -internal class LoanConfigurationAdapter : LoanConfigGateway { +internal class LoanConfigurationAdapter : GetLoanConfig { - override fun loadLimits() = LoanLimitsConfig( + override fun invoke() = LoanLimitsConfig( MINIMUM_LOAN_AMOUNT, MAXIMUM_LOAN_AMOUNT, MINIMUM_LOAN_PERIOD_MONTHS, From a76266b2f25dd051d154ba7ad54140657cc4cd46 Mon Sep 17 00:00:00 2001 From: Risto Uibo Date: Tue, 25 Jun 2024 14:05:50 +0300 Subject: [PATCH 15/17] server | refactor towards improved Kotlin (remove optionals + enhance streams) --- .../limits/gateway/DetermineEligiblePeriod.kt | 3 +- .../segment/gateway/FindCreditSegment.kt | 3 +- .../loans/domain/ssn/SocialSecurityNumber.kt | 20 ++--- .../loans/extensions/BooleanExtensions.kt | 3 + .../usecases/LoanEligibilityCalculation.kt | 84 ++++++++----------- ...n.kt => LoanEligibilityCalculationTest.kt} | 13 ++- .../InMemoryCreditSegmentStorageAdapter.kt | 4 +- .../FirstEligiblePeriodAdapter.kt | 9 +- .../FirstEligiblePeriodAdapterTest.kt | 7 +- 9 files changed, 56 insertions(+), 90 deletions(-) create mode 100644 server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/extensions/BooleanExtensions.kt rename server/loans/core/src/test/kotlin/ee/rsx/kata/bank/loans/usecases/{LoanEligibilityCalculationTestKotlin.kt => LoanEligibilityCalculationTest.kt} (98%) diff --git a/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/domain/limits/gateway/DetermineEligiblePeriod.kt b/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/domain/limits/gateway/DetermineEligiblePeriod.kt index 67c9e0b..1c5c0ac 100644 --- a/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/domain/limits/gateway/DetermineEligiblePeriod.kt +++ b/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/domain/limits/gateway/DetermineEligiblePeriod.kt @@ -1,8 +1,7 @@ package ee.rsx.kata.bank.loans.domain.limits.gateway import ee.rsx.kata.bank.loans.domain.segment.CreditSegment -import java.util.* fun interface DetermineEligiblePeriod { - operator fun invoke(forAmount: Int, forSegment: CreditSegment): Optional + operator fun invoke(forAmount: Int, forSegment: CreditSegment): Int? } diff --git a/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/domain/segment/gateway/FindCreditSegment.kt b/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/domain/segment/gateway/FindCreditSegment.kt index b796f13..3a7a6b3 100644 --- a/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/domain/segment/gateway/FindCreditSegment.kt +++ b/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/domain/segment/gateway/FindCreditSegment.kt @@ -2,8 +2,7 @@ package ee.rsx.kata.bank.loans.domain.segment.gateway import ee.rsx.kata.bank.loans.domain.segment.CreditSegment import ee.rsx.kata.bank.loans.domain.ssn.SocialSecurityNumber -import java.util.* fun interface FindCreditSegment { - operator fun invoke(forPerson: SocialSecurityNumber): Optional + operator fun invoke(forPerson: SocialSecurityNumber): CreditSegment? } diff --git a/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/domain/ssn/SocialSecurityNumber.kt b/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/domain/ssn/SocialSecurityNumber.kt index 8583dae..a40eb51 100644 --- a/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/domain/ssn/SocialSecurityNumber.kt +++ b/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/domain/ssn/SocialSecurityNumber.kt @@ -4,10 +4,7 @@ import java.lang.IllegalStateException import java.time.LocalDate import java.time.LocalDate.now import java.time.format.DateTimeFormatter.ofPattern -import java.util.concurrent.atomic.AtomicInteger -import java.util.function.Supplier import java.util.regex.Pattern.compile -import java.util.stream.IntStream data class SocialSecurityNumber(val value: String) { @@ -52,26 +49,21 @@ data class SocialSecurityNumber(val value: String) { return calculateChecksumUsing(DEFAULT_CHECKSUM_MULTIPLIERS, recalculation) } - private fun calculateChecksumUsing(multipliers: IntArray, recalculatedChecksum: Supplier): Int { + private fun calculateChecksumUsing(multipliers: IntArray, recalculatedChecksum: () -> Int): Int { val total = totalOfEachSsnNumberMultipliedWith(multipliers) var modulus = total % 11 if (isDoubleDigit(modulus)) { - modulus = recalculatedChecksum.get() + modulus = recalculatedChecksum() } return modulus } - private fun totalOfEachSsnNumberMultipliedWith(multipliers: IntArray): Int { - val total = AtomicInteger() - IntStream - .range(0, value.length - 1) - .forEach { index: Int -> total.addAndGet(numberAt(index) * multipliers[index]) } - return total.get() - } + private fun totalOfEachSsnNumberMultipliedWith(multipliers: IntArray) = + (0 ..< value.length - 1).sumOf { numberAt(it) * multipliers[it] } - private fun numberAt(index: Int) = value.substring(index, index + 1).toInt() + private fun numberAt(index: Int) = value.elementAt(index).digitToInt() private fun isDoubleDigit(modulus: Int) = 10 == modulus - private fun parseChecksumAtTheEnd() = value.substring(value.length - 1).toInt() + private fun parseChecksumAtTheEnd() = value.last().digitToInt() } diff --git a/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/extensions/BooleanExtensions.kt b/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/extensions/BooleanExtensions.kt new file mode 100644 index 0000000..e175671 --- /dev/null +++ b/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/extensions/BooleanExtensions.kt @@ -0,0 +1,3 @@ +package ee.rsx.kata.bank.loans.extensions + + fun Boolean.ifTrue(block: () -> R) = if (this) block() else null diff --git a/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/usecases/LoanEligibilityCalculation.kt b/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/usecases/LoanEligibilityCalculation.kt index ff172b2..63bdae0 100644 --- a/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/usecases/LoanEligibilityCalculation.kt +++ b/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/usecases/LoanEligibilityCalculation.kt @@ -12,14 +12,13 @@ import ee.rsx.kata.bank.loans.eligibility.LoanEligibilityStatus import ee.rsx.kata.bank.loans.eligibility.LoanEligibilityStatus.APPROVED import ee.rsx.kata.bank.loans.eligibility.LoanEligibilityStatus.DENIED import ee.rsx.kata.bank.loans.eligibility.LoanEligibilityStatus.INVALID +import ee.rsx.kata.bank.loans.extensions.ifTrue import ee.rsx.kata.bank.loans.validation.limits.LoadValidationLimits import ee.rsx.kata.bank.loans.validation.limits.ValidationLimitsDTO import ee.rsx.kata.bank.loans.validation.ssn.ValidateSocialSecurityNumber import ee.rsx.kata.bank.loans.validation.ssn.ValidationStatus.OK import jakarta.annotation.Nullable import jakarta.inject.Named -import java.util.* -import java.util.stream.Stream import kotlin.math.min @Named @@ -56,60 +55,46 @@ internal class LoanEligibilityCalculation( private fun validate( eligibilityRequest: LoanEligibilityRequestDTO, limits: ValidationLimitsDTO - ): List? { - val errors = Stream.of( + ): List? = + listOfNotNull( checkForSsnErrorIn(eligibilityRequest), checkForAmountErrorIn(eligibilityRequest, limits), checkForPeriodErrorIn(eligibilityRequest, limits) ) - .flatMap { it.stream() } - .toList() + .ifEmpty { null } - return if (errors.isEmpty()) null else errors - } - - private fun checkForSsnErrorIn(request: LoanEligibilityRequestDTO): Optional { - val ssnValidity = validateSocialSecurityNumber(request.ssn).status - - return if (ssnValidity == OK) - Optional.empty() - else - Optional.of("SSN is not valid") - } + private fun checkForSsnErrorIn(request: LoanEligibilityRequestDTO) = + (validateSocialSecurityNumber(request.ssn).status != OK) + .ifTrue { "SSN is not valid" } - private fun checkForAmountErrorIn(request: LoanEligibilityRequestDTO, limits: ValidationLimitsDTO): Optional { - val amount = request.loanAmount - - return if (amount < limits.minimumLoanAmount) { - Optional.of("Loan amount is less than minimum required") - } else if (amount > limits.maximumLoanAmount) { - Optional.of("Loan amount is more than maximum allowed") - } else Optional.empty() - } + private fun checkForAmountErrorIn(request: LoanEligibilityRequestDTO, limits: ValidationLimitsDTO) = + request.loanAmount.let { + (it < limits.minimumLoanAmount).ifTrue { "Loan amount is less than minimum required" } + ?: (it > limits.maximumLoanAmount).ifTrue { "Loan amount is more than maximum allowed" } + } - private fun checkForPeriodErrorIn(request: LoanEligibilityRequestDTO, limits: ValidationLimitsDTO): Optional { - val period = request.loanPeriodMonths - return if (period < limits.minimumLoanPeriodMonths) { - Optional.of("Loan period is less than minimum required") - } else if (period > limits.maximumLoanPeriodMonths) { - Optional.of("Loan period is more than maximum allowed") - } else Optional.empty() - } + private fun checkForPeriodErrorIn(request: LoanEligibilityRequestDTO, limits: ValidationLimitsDTO) = + request.loanPeriodMonths.let { + (it < limits.minimumLoanPeriodMonths).ifTrue { "Loan period is less than minimum required" } + ?: (it > limits.maximumLoanPeriodMonths).ifTrue { "Loan period is more than maximum allowed" } + } private fun calculateEligibility(request: LoanEligibilityRequestDTO, limits: ValidationLimitsDTO): LoanEligibility { val ssn = SocialSecurityNumber(request.ssn) - val creditSegment = findCreditSegment(forPerson = ssn) + val creditSegment: CreditSegment? = findCreditSegment(forPerson = ssn) return creditSegment - .map { + ?.let { val status = determineEligibilityStatusFor(request, it) determineEligibleAmountFor(limits, it, request.loanPeriodMonths) - .map { eligibleAmount -> LoanEligibility(status, eligibleAmount) } - .orElseGet { recalculateEligibilityFor(request, limits, it, status) } + ?.let { eligibleAmount -> + LoanEligibility(status, eligibleAmount) + } + ?: run { recalculateEligibilityFor(request, limits, creditSegment, status) } } - .orElseGet { LoanEligibility(status = DENIED) } + ?: LoanEligibility(status = DENIED) } private fun recalculateEligibilityFor( @@ -118,16 +103,16 @@ internal class LoanEligibilityCalculation( segment: CreditSegment, status: LoanEligibilityStatus ): LoanEligibility { - if (segment.isDebt) { - return LoanEligibility(status) - } - val newPeriod = determineEligiblePeriod(forAmount = request.loanAmount, forSegment = segment) - .filter { limits.minimumLoanPeriodMonths <= it && it <= limits.maximumLoanPeriodMonths } + if (segment.isDebt) return LoanEligibility(status) - val newAmount = newPeriod.flatMap { determineEligibleAmountFor(limits, segment, it) } + val newPeriod = determineEligiblePeriod(forAmount = request.loanAmount, forSegment = segment) + ?.takeIf { + it in limits.minimumLoanPeriodMonths..limits.maximumLoanPeriodMonths + } + val newAmount = newPeriod?.let { determineEligibleAmountFor(limits, segment, it) } - return LoanEligibility(status, newAmount.orElse(null), newPeriod.orElse(null)) + return LoanEligibility(status, newAmount, newPeriod) } private fun determineEligibilityStatusFor( @@ -139,15 +124,12 @@ internal class LoanEligibilityCalculation( private fun determineEligibleAmountFor( limits: ValidationLimitsDTO, creditSegment: CreditSegment, loanPeriodMonths: Int - ): Optional { + ): Int? { val eligibleAmount = min( limits.maximumLoanAmount.toDouble(), (creditSegment.creditModifier * loanPeriodMonths - 1).toDouble() ).toInt() - return if (eligibleAmount >= limits.minimumLoanAmount) - Optional.of(eligibleAmount) - else - Optional.empty() + return (eligibleAmount >= limits.minimumLoanAmount).ifTrue { eligibleAmount } } } diff --git a/server/loans/core/src/test/kotlin/ee/rsx/kata/bank/loans/usecases/LoanEligibilityCalculationTestKotlin.kt b/server/loans/core/src/test/kotlin/ee/rsx/kata/bank/loans/usecases/LoanEligibilityCalculationTest.kt similarity index 98% rename from server/loans/core/src/test/kotlin/ee/rsx/kata/bank/loans/usecases/LoanEligibilityCalculationTestKotlin.kt rename to server/loans/core/src/test/kotlin/ee/rsx/kata/bank/loans/usecases/LoanEligibilityCalculationTest.kt index b9f4f61..b04a1bb 100644 --- a/server/loans/core/src/test/kotlin/ee/rsx/kata/bank/loans/usecases/LoanEligibilityCalculationTestKotlin.kt +++ b/server/loans/core/src/test/kotlin/ee/rsx/kata/bank/loans/usecases/LoanEligibilityCalculationTest.kt @@ -33,11 +33,10 @@ import org.mockito.junit.jupiter.MockitoExtension import org.mockito.kotlin.any import org.mockito.kotlin.whenever import org.mockito.stubbing.Answer -import java.util.* @DisplayName("Loan eligibility calculation") @ExtendWith(MockitoExtension::class) -internal class LoanEligibilityCalculationTestKotlin { +internal class LoanEligibilityCalculationTest { @Mock private lateinit var validateSocialSecurityNumber: ValidateSocialSecurityNumber @@ -61,7 +60,7 @@ internal class LoanEligibilityCalculationTestKotlin { whenever(loadValidationLimits()) .thenReturn(TEST_VALIDATION_LIMITS) whenever(findCreditSegment(any())) - .thenReturn(Optional.empty()) + .thenReturn(null) } @Nested @@ -162,7 +161,7 @@ internal class LoanEligibilityCalculationTestKotlin { validRequest: LoanEligibilityRequestDTO, segment: CreditSegment, newEligiblePeriod: Int ): Int { `when`(determineEligiblePeriod(forAmount = validRequest.loanAmount, forSegment = segment)) - .thenReturn(Optional.of(newEligiblePeriod)) + .thenReturn(newEligiblePeriod) return newEligiblePeriod } } @@ -343,14 +342,14 @@ internal class LoanEligibilityCalculationTestKotlin { val ssn = SocialSecurityNumber(withSsn) val foundSegment = CreditSegment(ssn, segmentType, creditModifier) `when`(findCreditSegment(ssn)) - .thenReturn(Optional.of(foundSegment)) + .thenReturn(foundSegment) return foundSegment } private fun whenCreditSegmentNotFoundForPerson(withSsn: String) { val ssn = SocialSecurityNumber(withSsn) `when`(findCreditSegment(ssn)) - .thenReturn(Optional.empty()) + .thenReturn(null) } internal class DefaultTestRequest { @@ -391,7 +390,7 @@ internal class LoanEligibilityCalculationTestKotlin { } fun errors(vararg errors: String): DefaultTestResult { - this.errors = Arrays.stream(errors).toList() + this.errors = errors.toList() return this } diff --git a/server/loans/gw-eligibility/src/main/kotlin/ee/rsx/kata/bank/loans/adapter/eligibility/creditsegment/InMemoryCreditSegmentStorageAdapter.kt b/server/loans/gw-eligibility/src/main/kotlin/ee/rsx/kata/bank/loans/adapter/eligibility/creditsegment/InMemoryCreditSegmentStorageAdapter.kt index fba2325..77e1861 100644 --- a/server/loans/gw-eligibility/src/main/kotlin/ee/rsx/kata/bank/loans/adapter/eligibility/creditsegment/InMemoryCreditSegmentStorageAdapter.kt +++ b/server/loans/gw-eligibility/src/main/kotlin/ee/rsx/kata/bank/loans/adapter/eligibility/creditsegment/InMemoryCreditSegmentStorageAdapter.kt @@ -9,13 +9,11 @@ import ee.rsx.kata.bank.loans.domain.segment.CreditSegmentType.SEGMENT_3 import ee.rsx.kata.bank.loans.domain.segment.gateway.FindCreditSegment import ee.rsx.kata.bank.loans.domain.ssn.SocialSecurityNumber import jakarta.inject.Named -import java.util.* @Named internal class InMemoryCreditSegmentStorageAdapter : FindCreditSegment { - override fun invoke(forPerson: SocialSecurityNumber) = - Optional.ofNullable(creditSegmentOfPerson[forPerson.value]) + override fun invoke(forPerson: SocialSecurityNumber) = creditSegmentOfPerson[forPerson.value] companion object { private val creditSegmentOfPerson = mapOf( diff --git a/server/loans/gw-eligibility/src/main/kotlin/ee/rsx/kata/bank/loans/adapter/eligibility/eligibleperiod/FirstEligiblePeriodAdapter.kt b/server/loans/gw-eligibility/src/main/kotlin/ee/rsx/kata/bank/loans/adapter/eligibility/eligibleperiod/FirstEligiblePeriodAdapter.kt index 5e9bd2e..1bd698a 100644 --- a/server/loans/gw-eligibility/src/main/kotlin/ee/rsx/kata/bank/loans/adapter/eligibility/eligibleperiod/FirstEligiblePeriodAdapter.kt +++ b/server/loans/gw-eligibility/src/main/kotlin/ee/rsx/kata/bank/loans/adapter/eligibility/eligibleperiod/FirstEligiblePeriodAdapter.kt @@ -3,19 +3,16 @@ package ee.rsx.kata.bank.loans.adapter.eligibility.eligibleperiod import ee.rsx.kata.bank.loans.domain.limits.gateway.DetermineEligiblePeriod import ee.rsx.kata.bank.loans.domain.segment.CreditSegment import jakarta.inject.Named -import java.util.* -import java.util.Optional.empty -import java.util.Optional.of import kotlin.math.floor @Named internal class FirstEligiblePeriodAdapter : DetermineEligiblePeriod { - override fun invoke(forAmount: Int, forSegment: CreditSegment): Optional = + override fun invoke(forAmount: Int, forSegment: CreditSegment): Int? = if (forSegment.isDebt) - empty() + null else - of(calculateFirstMinimumPeriodEligibleFor(forAmount, forSegment)) + calculateFirstMinimumPeriodEligibleFor(forAmount, forSegment) private fun calculateFirstMinimumPeriodEligibleFor(amount: Int, creditSegment: CreditSegment): Int { val firstPeriod = floor(amount.toDouble() / creditSegment.creditModifier) diff --git a/server/loans/gw-eligibility/src/test/kotlin/ee/rsx/kata/bank/loans/adapter/eligibility/eligibleperiod/FirstEligiblePeriodAdapterTest.kt b/server/loans/gw-eligibility/src/test/kotlin/ee/rsx/kata/bank/loans/adapter/eligibility/eligibleperiod/FirstEligiblePeriodAdapterTest.kt index c5ce575..7f773c6 100644 --- a/server/loans/gw-eligibility/src/test/kotlin/ee/rsx/kata/bank/loans/adapter/eligibility/eligibleperiod/FirstEligiblePeriodAdapterTest.kt +++ b/server/loans/gw-eligibility/src/test/kotlin/ee/rsx/kata/bank/loans/adapter/eligibility/eligibleperiod/FirstEligiblePeriodAdapterTest.kt @@ -27,9 +27,7 @@ internal class FirstEligiblePeriodAdapterTest { val eligiblePeriod = determineEligiblePeriod(forAmount = requestedLoan, forSegment = segment) - assertThat(eligiblePeriod) - .isPresent() - .contains(51) + assertThat(eligiblePeriod).isEqualTo(51) } @Test @@ -40,7 +38,6 @@ internal class FirstEligiblePeriodAdapterTest { val eligiblePeriod = determineEligiblePeriod(forAmount = requestedLoan, forSegment = debtSegment) - assertThat(eligiblePeriod) - .isNotPresent() + assertThat(eligiblePeriod).isNull() } } From 438c5df7e6f3d53b54af2c481c5e040ad6bcd25a Mon Sep 17 00:00:00 2001 From: Risto Uibo Date: Tue, 25 Jun 2024 17:32:40 +0300 Subject: [PATCH 16/17] server | refactoring --- .../loans/validation/ValidationLimitsIntegrationTest.kt | 2 -- .../ee/rsx/kata/bank/loans/extensions/BooleanExtensions.kt | 2 +- .../kata/bank/loans/usecases/LoanEligibilityCalculation.kt | 6 +++++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/server/app/src/test/kotlin/ee/rsx/kata/bank/app/integrationtest/loans/validation/ValidationLimitsIntegrationTest.kt b/server/app/src/test/kotlin/ee/rsx/kata/bank/app/integrationtest/loans/validation/ValidationLimitsIntegrationTest.kt index 84ac43d..d026fd3 100644 --- a/server/app/src/test/kotlin/ee/rsx/kata/bank/app/integrationtest/loans/validation/ValidationLimitsIntegrationTest.kt +++ b/server/app/src/test/kotlin/ee/rsx/kata/bank/app/integrationtest/loans/validation/ValidationLimitsIntegrationTest.kt @@ -8,9 +8,7 @@ import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMock import org.springframework.boot.test.context.SpringBootTest import org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT import org.springframework.test.web.servlet.MockMvc -import org.springframework.test.web.servlet.request.MockMvcRequestBuilders import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get -import org.springframework.test.web.servlet.result.MockMvcResultMatchers import org.springframework.test.web.servlet.result.MockMvcResultMatchers.content import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status diff --git a/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/extensions/BooleanExtensions.kt b/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/extensions/BooleanExtensions.kt index e175671..2e6d8a2 100644 --- a/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/extensions/BooleanExtensions.kt +++ b/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/extensions/BooleanExtensions.kt @@ -1,3 +1,3 @@ package ee.rsx.kata.bank.loans.extensions - fun Boolean.ifTrue(block: () -> R) = if (this) block() else null + fun Boolean.ifTrue(applyOperation: () -> R) = if (this) applyOperation() else null diff --git a/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/usecases/LoanEligibilityCalculation.kt b/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/usecases/LoanEligibilityCalculation.kt index 63bdae0..e09dfd3 100644 --- a/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/usecases/LoanEligibilityCalculation.kt +++ b/server/loans/core/src/main/kotlin/ee/rsx/kata/bank/loans/usecases/LoanEligibilityCalculation.kt @@ -119,7 +119,11 @@ internal class LoanEligibilityCalculation( request: LoanEligibilityRequestDTO, creditSegment: CreditSegment ): LoanEligibilityStatus { val creditScore = creditSegment.creditModifier.toDouble() / request.loanAmount * request.loanPeriodMonths - return if (!creditSegment.isDebt && creditScore > 1) APPROVED else DENIED + + return if (!creditSegment.isDebt && creditScore > 1) + APPROVED + else + DENIED } private fun determineEligibleAmountFor( From f4620645789ab7dbd79587ef9461eca99ae81541 Mon Sep 17 00:00:00 2001 From: Risto Uibo Date: Sun, 10 Nov 2024 18:46:38 +0200 Subject: [PATCH 17/17] fix | use empty string as base URL, when VITE_BASE_URL is undefined --- ui/src/routes.ts | 2 +- ui/src/server/server.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/src/routes.ts b/ui/src/routes.ts index 1d132c5..33dc085 100644 --- a/ui/src/routes.ts +++ b/ui/src/routes.ts @@ -12,7 +12,7 @@ const routes: Array = [ ]; const router = createRouter({ - history: createWebHistory(import.meta.env.VITE_BASE_URL), + history: createWebHistory(import.meta.env.VITE_BASE_URL || ''), routes }); diff --git a/ui/src/server/server.ts b/ui/src/server/server.ts index 069841e..496c4f3 100644 --- a/ui/src/server/server.ts +++ b/ui/src/server/server.ts @@ -3,7 +3,7 @@ import ValidationLimits from "../models/ValidationLimits"; import LoanRequest from "../models/LoanRequest"; import LoanEligibilityResult from "../models/LoanEligibilityResult"; -const BASE_URL = import.meta.env.VITE_BASE_URL +const BASE_URL = import.meta.env.VITE_BASE_URL || '' const SERVER_URL = BASE_URL + '/api' const LOANS_VALIDATION_URL = '/loans/validation' const LOANS_ELIGIBILITY_URL = '/loans/eligibility'