Skip to content

Commit 50be6cf

Browse files
authored
Merge pull request #1239 from statisticsnorway/fix-fnr-logic
Fix logic for incomplete fnr with suffix exceptions
2 parents 025d0b7 + 2258bb1 commit 50be6cf

File tree

10 files changed

+61
-23
lines changed

10 files changed

+61
-23
lines changed

kontroller/src/main/kotlin/no/ssb/kostra/program/extension/StringExtensions.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ fun String.districtIdFromRegion() = this.substring(4, 6)
1212

1313
fun String.ageInYears(reportingYear: Int): Int? =
1414
SsnValidationUtils
15-
.extractBirthDateFromSocialSecurityId(socialSecurityId = this)
15+
.extractBirthDateFromSocialSecurityId(
16+
socialSecurityId = this,
17+
reportingYear = reportingYear
18+
)
1619
?.let { dateOfBirth ->
1720
reportingYear - dateOfBirth.year
1821
}

kontroller/src/main/kotlin/no/ssb/kostra/program/util/SsnValidationUtils.kt

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,13 @@ object SsnValidationUtils {
1919
private val CONTROL_SUM_DIGITS_1 = listOf(3, 7, 6, 1, 8, 9, 4, 5, 2, 1)
2020
private val CONTROL_SUM_DIGITS_2 = listOf(5, 4, 3, 2, 7, 6, 5, 4, 3, 2, 1)
2121

22-
fun extractBirthDateFromSocialSecurityId(socialSecurityId: String): LocalDate? = try {
22+
fun extractBirthDateFromSocialSecurityId(socialSecurityId: String, reportingYear: Int): LocalDate? = try {
2323
when {
2424
!SSN_PATTERN.matcher(socialSecurityId).matches() -> null
25+
SSN_SUFFIX_EXCEPTIONS.any { suffix -> socialSecurityId.endsWith(suffix) } -> parseDateWithSuffixExceptions(socialSecurityId, reportingYear)
2526
else -> parseDateWithAutoPivotYear(convertFregMonth(convertDNumber(socialSecurityId)))
2627
}
27-
} catch (ex: DateTimeParseException) {
28+
} catch (_: DateTimeParseException) {
2829
null
2930
}
3031

@@ -42,15 +43,25 @@ object SsnValidationUtils {
4243
}
4344
}
4445

46+
internal fun parseDateWithSuffixExceptions(socialSecurityId: String, year: Int): LocalDate {
47+
val date = LocalDate.parse(socialSecurityId.take(6), LOCAL_DATE_FORMATTER)
48+
val referenceDate = LocalDate.of(year, 12, 31)
49+
50+
return if (referenceDate.plusYears(1L).isBefore(date)) {
51+
date.minusYears(CENTURY) // 1900-1999
52+
} else {
53+
date
54+
}
55+
}
4556

4657
fun isValidSocialSecurityId(personId: String) = SSN_PATTERN.matcher(personId).matches() && isModulo11Valid(personId)
4758

48-
fun isValidSocialSecurityIdOrDnr(personId: String) = SSN_PATTERN.matcher(personId).matches()
59+
fun isValidSocialSecurityIdOrDnr(personId: String, reportingYear: Int) = SSN_PATTERN.matcher(personId).matches()
4960
&&
5061
(
5162
isModulo11Valid(personId)
5263
||
53-
extractBirthDateFromSocialSecurityId(personId) != null
64+
extractBirthDateFromSocialSecurityId(personId, reportingYear) != null
5465
&& SSN_SUFFIX_EXCEPTIONS.contains(personId.takeLast(PERSON_ID_LENGTH))
5566
)
5667

kontroller/src/main/kotlin/no/ssb/kostra/validation/rule/barnevern/individrule/Individ03.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ class Individ03 : AbstractRule<KostraIndividType>(
1515
override fun validate(context: KostraIndividType, arguments: KotlinArguments) = context.run {
1616
when {
1717
fodselsnummer != null -> when {
18-
isValidSocialSecurityIdOrDnr(fodselsnummer!!) -> null
18+
isValidSocialSecurityIdOrDnr(fodselsnummer!!, arguments.aargang.toInt()) -> null
1919
else -> createSingleReportEntryList(
2020
contextId = context.id,
2121
messageText = "Feil i fødselsnummer. Kan ikke identifisere individet."

kontroller/src/main/kotlin/no/ssb/kostra/validation/rule/barnevern/individrule/Individ12.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ class Individ12 : AbstractRule<KostraIndividType>(
1313
) {
1414
override fun validate(context: KostraIndividType, arguments: KotlinArguments) =
1515
if (context.fodselsnummer == null
16-
|| !isValidSocialSecurityIdOrDnr(context.fodselsnummer!!)
16+
|| !isValidSocialSecurityIdOrDnr(context.fodselsnummer!!, arguments.aargang.toInt())
1717
) {
1818
createSingleReportEntryList(
1919
contextId = context.id,

kontroller/src/main/kotlin/no/ssb/kostra/validation/rule/sosial/extensions/KostraRecordSosialExtensions.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ fun KostraRecord.hasNotVarighet() = (1..12)
2121
fun KostraRecord.ageInYears(arguments: KotlinArguments): Int =
2222
this[KvalifiseringColumnNames.PERSON_FODSELSNR_COL_NAME].ageInYears(arguments.aargang.toInt()) ?: -1
2323

24-
fun KostraRecord.hasFnr(): Boolean =
25-
SsnValidationUtils.isValidSocialSecurityIdOrDnr(this[KvalifiseringColumnNames.PERSON_FODSELSNR_COL_NAME])
24+
fun KostraRecord.hasFnr(reportingYear: Int): Boolean =
25+
SsnValidationUtils.isValidSocialSecurityIdOrDnr(this[KvalifiseringColumnNames.PERSON_FODSELSNR_COL_NAME], reportingYear)
2626

2727
fun Collection<KostraRecord>.varighetAsStatsEntries() = this
2828
.map { kostraRecord ->

kontroller/src/main/kotlin/no/ssb/kostra/validation/rule/sosial/kvalifisering/rule/Rule005aFoedselsnummerDubletter.kt

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,21 @@ import no.ssb.kostra.area.sosial.kvalifisering.KvalifiseringColumnNames.PERSON_F
77
import no.ssb.kostra.area.sosial.kvalifisering.KvalifiseringColumnNames.PERSON_JOURNALNR_COL_NAME
88
import no.ssb.kostra.area.sosial.kvalifisering.KvalifiseringColumnNames.STATUS_COL_NAME
99
import no.ssb.kostra.program.KostraRecord
10+
import no.ssb.kostra.program.KotlinArguments
1011
import no.ssb.kostra.program.util.SsnValidationUtils
1112
import no.ssb.kostra.validation.report.Severity
12-
import no.ssb.kostra.validation.rule.AbstractNoArgsRule
13+
import no.ssb.kostra.validation.rule.AbstractRule
1314
import no.ssb.kostra.validation.rule.sosial.SosialRuleId
1415

15-
class Rule005aFoedselsnummerDubletter : AbstractNoArgsRule<List<KostraRecord>>(
16+
class Rule005aFoedselsnummerDubletter : AbstractRule<List<KostraRecord>>(
1617
SosialRuleId.FODSELSNUMMER_DUBLETTER_05A.title,
1718
Severity.ERROR
1819
) {
19-
override fun validate(context: List<KostraRecord>) = context
20+
override fun validate(context: List<KostraRecord>, arguments: KotlinArguments) = context
2021
.asSequence()
2122
.filter { kostraRecord ->
2223
kostraRecord[KOMMUNE_NR_COL_NAME] != OSLO_MUNICIPALITY_ID
23-
&& SsnValidationUtils.isValidSocialSecurityIdOrDnr(kostraRecord[PERSON_FODSELSNR_COL_NAME])
24+
&& SsnValidationUtils.isValidSocialSecurityIdOrDnr(kostraRecord[PERSON_FODSELSNR_COL_NAME], arguments.aargang.toInt())
2425
}
2526
.groupBy { kostraRecord -> kostraRecord[PERSON_FODSELSNR_COL_NAME] + kostraRecord[STATUS_COL_NAME] }
2627
.filter { (_, group) -> group.size > 1 }

kontroller/src/main/kotlin/no/ssb/kostra/validation/rule/sosial/rule/Rule005Fodselsnummer.kt

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,19 @@ import no.ssb.kostra.area.sosial.kvalifisering.KvalifiseringColumnNames.PERSON_F
44
import no.ssb.kostra.area.sosial.kvalifisering.KvalifiseringColumnNames.PERSON_JOURNALNR_COL_NAME
55
import no.ssb.kostra.area.sosial.kvalifisering.KvalifiseringColumnNames.SAKSBEHANDLER_COL_NAME
66
import no.ssb.kostra.program.KostraRecord
7+
import no.ssb.kostra.program.KotlinArguments
78
import no.ssb.kostra.program.extension.fieldAs
89
import no.ssb.kostra.program.util.SsnValidationUtils.isValidSocialSecurityIdOrDnr
910
import no.ssb.kostra.validation.report.Severity
10-
import no.ssb.kostra.validation.rule.AbstractNoArgsRule
11+
import no.ssb.kostra.validation.rule.AbstractRule
1112
import no.ssb.kostra.validation.rule.sosial.SosialRuleId
1213

13-
class Rule005Fodselsnummer : AbstractNoArgsRule<List<KostraRecord>>(
14+
class Rule005Fodselsnummer : AbstractRule<List<KostraRecord>>(
1415
SosialRuleId.FODSELSNUMMER_05.title,
1516
Severity.ERROR
1617
) {
17-
override fun validate(context: List<KostraRecord>) = context
18-
.filterNot { isValidSocialSecurityIdOrDnr(it.fieldAs(PERSON_FODSELSNR_COL_NAME)) }
18+
override fun validate(context: List<KostraRecord>, arguments: KotlinArguments) = context
19+
.filterNot { isValidSocialSecurityIdOrDnr(it.fieldAs(PERSON_FODSELSNR_COL_NAME), arguments.aargang.toInt()) }
1920
.map {
2021
createValidationReportEntry(
2122
"Det er ikke oppgitt fødselsnummer/d-nummer på deltakeren eller fødselsnummeret/d-nummeret " +

kontroller/src/test/kotlin/no/ssb/kostra/program/util/SsnValidationUtilsTest.kt

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,24 @@ import no.ssb.kostra.program.util.SsnValidationUtils.validateDUF
1111
import java.time.LocalDate
1212

1313
class SsnValidationUtilsTest : BehaviorSpec({
14+
val reportingYear = 2025
1415

1516
Given("extractBirthDateFromSocialSecurityId") {
1617
forAll(
1718
row("123", "N/A", null),
1819
row("05011399292", "fnr", LocalDate.of(2013, 1, 5)),
1920
row("41011088188", "dnr", LocalDate.of(2010, 1, 1)),
2021
row("01811088188", "tnr", LocalDate.of(2010, 1, 1)),
22+
row("01010100100", "N/A", LocalDate.of(2001, 1, 1)),
23+
row("01018000100", "N/A", LocalDate.of(1980, 1, 1)),
24+
row("01018000200", "N/A", LocalDate.of(1980, 1, 1)),
25+
row("01012599999", "N/A", LocalDate.of(2025, 1, 1)),
26+
row("01012699999", "N/A", LocalDate.of(2026, 1, 1)),
27+
row("01012799999", "N/A", LocalDate.of(1927, 1, 1)),
2128
) { socialSecurityId, type, expectedDate ->
2229
When("$socialSecurityId $type") {
2330

24-
val dateOfBirth = SsnValidationUtils.extractBirthDateFromSocialSecurityId(socialSecurityId)
31+
val dateOfBirth = SsnValidationUtils.extractBirthDateFromSocialSecurityId(socialSecurityId, reportingYear)
2532

2633
Then("dateOfBirth ($dateOfBirth) should be as expected ($expectedDate)") {
2734
dateOfBirth shouldBe expectedDate
@@ -94,7 +101,7 @@ class SsnValidationUtilsTest : BehaviorSpec({
94101
row("valid unborn ssn", "05012999999", true),
95102
) { description, socialSecurityId, expectedResult ->
96103
When(description) {
97-
val isValidSocialSecurityIdOrDnr = isValidSocialSecurityIdOrDnr(socialSecurityId)
104+
val isValidSocialSecurityIdOrDnr = isValidSocialSecurityIdOrDnr(socialSecurityId, reportingYear)
98105

99106
Then("isValidSocialSecurityIdOrDnr($socialSecurityId) should be $expectedResult") {
100107
isValidSocialSecurityIdOrDnr shouldBe expectedResult

kontroller/src/test/kotlin/no/ssb/kostra/validation/rule/barnevern/individrule/Individ07Test.kt

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,21 +25,36 @@ class Individ07Test : BehaviorSpec({
2525
ForAllRowItem(
2626
"individ with fodselsnummer, age below 25",
2727
individInTest.copy(
28-
fodselsnummer = RandomUtils.generateRandomSsn(24, Year.now().value - 1)
28+
fodselsnummer = RandomUtils.generateRandomSsn(
29+
24,
30+
Year.now().value - 1
31+
)
2932
)
3033
),
3134
ForAllRowItem(
3235
"individ with fodselsnummer, age is 25",
3336
individInTest.copy(
34-
fodselsnummer = RandomUtils.generateRandomSsn(25, Year.now().value - 1)
37+
fodselsnummer = RandomUtils.generateRandomSsn(
38+
25,
39+
Year.now().value - 1
40+
)
3541
)
3642
),
3743
ForAllRowItem(
3844
"individ age above 25",
3945
individInTest.copy(
40-
fodselsnummer = RandomUtils.generateRandomSsn(26, Year.now().value - 1)
46+
fodselsnummer = RandomUtils.generateRandomSsn(
47+
26,
48+
Year.now().value - 1
49+
)
4150
),
4251
expectedErrorMessage = "Individet er 26 år og skal avsluttes som klient"
52+
),
53+
ForAllRowItem(
54+
"individ age 18 with incomplete fodselsnummer",
55+
individInTest.copy(
56+
fodselsnummer = "0101${Year.now().value.minus(18).toString().takeLast(2)}00100"
57+
),
4358
)
4459
)
4560
)

kontroller/src/test/kotlin/no/ssb/kostra/validation/rule/sosial/extensions/KostraRecordSosialExtensionsTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ class KostraRecordSosialExtensionsTest : BehaviorSpec({
8888
val sut = kostraRecordInTest(fnr = fnr)
8989

9090
Then("result should be as expected") {
91-
sut.first().hasFnr() shouldBe expectedResult
91+
sut.first().hasFnr(RuleTestData.argumentsInTest.aargang.toInt()) shouldBe expectedResult
9292
}
9393
}
9494
}

0 commit comments

Comments
 (0)