diff --git a/services/compliance/src/main/kotlin/com/fincore/compliance/infrastructure/kyc/SandboxKycProvider.kt b/services/compliance/src/main/kotlin/com/fincore/compliance/infrastructure/kyc/SandboxKycProvider.kt new file mode 100644 index 0000000..0945399 --- /dev/null +++ b/services/compliance/src/main/kotlin/com/fincore/compliance/infrastructure/kyc/SandboxKycProvider.kt @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: BUSL-1.1 +// SPDX-FileCopyrightText: 2026 FinCore Engine Authors + +package com.fincore.compliance.infrastructure.kyc + +import com.fincore.compliance.application.kyc.KycCheckRequest +import com.fincore.compliance.application.kyc.KycCheckResult +import com.fincore.compliance.application.kyc.KycProvider +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty +import org.springframework.stereotype.Component + +/** + * Deterministic in-tree sandbox KYC provider, off by default. Encodes no real provider logic; the outcome is keyed on + * documented case-insensitive markers in the opaque subject reference: "reject" -> Rejected, "pending" -> Pending, + * "insufficient" -> InsufficientData, otherwise -> Approved. The same reference always yields the same result. + */ +@Component +@ConditionalOnProperty( + prefix = "fincore.compliance.kyc.sandbox", + name = ["enabled"], + havingValue = "true", + matchIfMissing = false, +) +class SandboxKycProvider : KycProvider { + override fun check(request: KycCheckRequest): KycCheckResult { + val reference = request.subjectReference + val providerReference = "sbx-kyc-${reference.hashCode().toUInt()}" + return when { + reference.contains(REJECT_MARKER, ignoreCase = true) -> + KycCheckResult.Rejected("sandbox rejected by subject marker") + reference.contains(PENDING_MARKER, ignoreCase = true) -> + KycCheckResult.Pending(providerReference) + reference.contains(INSUFFICIENT_MARKER, ignoreCase = true) -> + KycCheckResult.InsufficientData(listOf("sandbox.insufficient")) + else -> KycCheckResult.Approved(providerReference) + } + } + + private companion object { + const val REJECT_MARKER = "reject" + const val PENDING_MARKER = "pending" + const val INSUFFICIENT_MARKER = "insufficient" + } +} diff --git a/services/compliance/src/test/kotlin/com/fincore/compliance/infrastructure/kyc/SandboxKycProviderTest.kt b/services/compliance/src/test/kotlin/com/fincore/compliance/infrastructure/kyc/SandboxKycProviderTest.kt new file mode 100644 index 0000000..edbaa09 --- /dev/null +++ b/services/compliance/src/test/kotlin/com/fincore/compliance/infrastructure/kyc/SandboxKycProviderTest.kt @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: BUSL-1.1 +// SPDX-FileCopyrightText: 2026 FinCore Engine Authors + +package com.fincore.compliance.infrastructure.kyc + +import com.fincore.compliance.application.kyc.KycCheckRequest +import com.fincore.compliance.application.kyc.KycCheckResult +import io.kotest.matchers.shouldBe +import io.kotest.matchers.types.shouldBeInstanceOf +import org.junit.jupiter.api.Test + +class SandboxKycProviderTest { + private val provider = SandboxKycProvider() + + @Test + fun `should approve a plain subject reference`() { + provider.check(KycCheckRequest("subject-1")).shouldBeInstanceOf() + } + + @Test + fun `should reject when the reference carries the reject marker`() { + provider.check(KycCheckRequest("reject-subject")).shouldBeInstanceOf() + } + + @Test + fun `should return pending when the reference carries the pending marker`() { + provider.check(KycCheckRequest("pending-subject")).shouldBeInstanceOf() + } + + @Test + fun `should return insufficient data with a non-empty list when marked`() { + val result = provider.check(KycCheckRequest("insufficient-subject")) + + result.shouldBeInstanceOf().missing shouldBe listOf("sandbox.insufficient") + } + + @Test + fun `should be deterministic for the same subject reference`() { + val first = provider.check(KycCheckRequest("subject-1")) + val second = provider.check(KycCheckRequest("subject-1")) + + first shouldBe second + } +}