From 6f1d6cc6c8c60a557aa13e9039b3c68398e74fd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jarkko=20P=C3=A4t=C3=A4ri?= Date: Tue, 28 Jan 2025 18:11:51 +0200 Subject: [PATCH] Add placement tool existing applications validation --- .../placement-tool/PlacementToolPage.tsx | 5 ++- .../components/placement-tool/api.ts | 1 + .../defaults/employee/i18n/fi.tsx | 3 +- .../PlacementToolServiceIntegrationTest.kt | 42 ++++++++++++------- .../application/PlacementToolController.kt | 7 ++-- .../evaka/application/PlacementToolService.kt | 29 +++++++++++++ 6 files changed, 67 insertions(+), 20 deletions(-) diff --git a/frontend/src/employee-frontend/components/placement-tool/PlacementToolPage.tsx b/frontend/src/employee-frontend/components/placement-tool/PlacementToolPage.tsx index b96ac95bcf7..fba9470d5b2 100644 --- a/frontend/src/employee-frontend/components/placement-tool/PlacementToolPage.tsx +++ b/frontend/src/employee-frontend/components/placement-tool/PlacementToolPage.tsx @@ -43,7 +43,10 @@ export default React.memo(function PlacementToolPage() { files={[]} validateHandler={placementFileValidate} getValidationResult={(validation) => - i18n.placementTool.validation(validation.count) + i18n.placementTool.validation( + validation.count, + validation.existing + ) } uploadHandler={placementFileUpload} getDownloadUrl={() => ''} diff --git a/frontend/src/employee-frontend/components/placement-tool/api.ts b/frontend/src/employee-frontend/components/placement-tool/api.ts index 933d071d457..f90323708da 100644 --- a/frontend/src/employee-frontend/components/placement-tool/api.ts +++ b/frontend/src/employee-frontend/components/placement-tool/api.ts @@ -37,6 +37,7 @@ const upload = export interface PlacementToolValidation { count: number + existing: number } export const placementFileValidate: ValidateHandler = { diff --git a/frontend/src/lib-customizations/defaults/employee/i18n/fi.tsx b/frontend/src/lib-customizations/defaults/employee/i18n/fi.tsx index 57b3490ecd1..5c710972627 100755 --- a/frontend/src/lib-customizations/defaults/employee/i18n/fi.tsx +++ b/frontend/src/lib-customizations/defaults/employee/i18n/fi.tsx @@ -5082,7 +5082,8 @@ export const fi = { preschoolTermNotification: 'Hakemukset luodaan seuravaan esiopetuskauteen:', preschoolTermWarning: 'eVakasta puuttuu seuraavan esiopetuskauden määrittely. Esiopetuskausi tarvitaan hakemusten luontia varten.', - validation: (count: number) => `Olet tuomassa ${count} sijoitusta, jatka?` + validation: (count: number, existing: number) => + `Olet tuomassa ${count} sijoitusta${existing > 0 ? ` (joista ${existing} löytyy jo järjestelmästä)` : ''}, jatka?` }, outOfOffice: { menu: 'Johtajan poissaolojakso', diff --git a/service/src/integrationTest/kotlin/fi/espoo/evaka/application/PlacementToolServiceIntegrationTest.kt b/service/src/integrationTest/kotlin/fi/espoo/evaka/application/PlacementToolServiceIntegrationTest.kt index 745bfb712dc..c3b57994943 100644 --- a/service/src/integrationTest/kotlin/fi/espoo/evaka/application/PlacementToolServiceIntegrationTest.kt +++ b/service/src/integrationTest/kotlin/fi/espoo/evaka/application/PlacementToolServiceIntegrationTest.kt @@ -446,11 +446,7 @@ class PlacementToolServiceIntegrationTest : FullApplicationTest(resetDbBeforeEac ) ) db.transaction { tx -> tx.insert(testFeeThresholds) } - - controller.createPlacementToolApplications( - dbInstance(), - admin, - clock, + val file = MockMultipartFile( "test.csv", """ @@ -459,8 +455,14 @@ ${child.ssn!!};${unit.id} """ .trimIndent() .toByteArray(StandardCharsets.UTF_8), - ), - ) + ) + + val validationPre = + controller.validatePlacementToolApplications(dbInstance(), admin, clock, file) + assertEquals(1, validationPre.count) + assertEquals(0, validationPre.existing) + + controller.createPlacementToolApplications(dbInstance(), admin, clock, file) asyncJobRunner.runPendingJobsSync(clock) val applicationSummaries = @@ -473,6 +475,11 @@ ${child.ssn!!};${unit.id} assertEquals(summary.preferredUnitId, unit.id) val child = db.read { it.getPersonBySSN(child.ssn!!) } assertEquals(summary.childId, child!!.id) + + val validationPost = + controller.validatePlacementToolApplications(dbInstance(), admin, clock, file) + assertEquals(1, validationPost.count) + assertEquals(1, validationPost.existing) } @Test @@ -487,11 +494,7 @@ ${child.ssn!!};${unit.id} ) ) db.transaction { tx -> tx.insert(testFeeThresholds) } - - controller.createPlacementToolApplications( - dbInstance(), - admin, - clock, + val file = MockMultipartFile( "test.csv", """ @@ -500,8 +503,14 @@ ${child.ssn!!};${unit.id} """ .trimIndent() .toByteArray(StandardCharsets.UTF_8), - ), - ) + ) + + val validationPre = + controller.validatePlacementToolApplications(dbInstance(), admin, clock, file) + assertEquals(1, validationPre.count) + assertEquals(0, validationPre.existing) + + controller.createPlacementToolApplications(dbInstance(), admin, clock, file) asyncJobRunner.runPendingJobsSync(clock) val applicationSummaries = @@ -514,6 +523,11 @@ ${child.ssn!!};${unit.id} assertEquals(summary.preferredUnitId, unit.id) val child = db.read { it.getPersonBySSN(child.ssn!!) } assertEquals(summary.childId, child!!.id) + + val validationPost = + controller.validatePlacementToolApplications(dbInstance(), admin, clock, file) + assertEquals(1, validationPost.count) + assertEquals(1, validationPost.existing) } @Test diff --git a/service/src/main/kotlin/fi/espoo/evaka/application/PlacementToolController.kt b/service/src/main/kotlin/fi/espoo/evaka/application/PlacementToolController.kt index 6f72864adf1..573cf78073b 100644 --- a/service/src/main/kotlin/fi/espoo/evaka/application/PlacementToolController.kt +++ b/service/src/main/kotlin/fi/espoo/evaka/application/PlacementToolController.kt @@ -36,15 +36,14 @@ class PlacementToolController( @RequestPart("file") file: MultipartFile, ): PlacementToolValidation { return db.connect { dbc -> - dbc.transaction { tx -> + dbc.read { tx -> accessControl.requirePermissionFor( tx, user, clock, Action.Global.PLACEMENT_TOOL, ) - val placements = file.inputStream.use { parsePlacementToolCsv(it) } - PlacementToolValidation(count = placements.size) + placementToolService.validatePlacementToolApplications(tx, clock, file) } } .also { Audit.PlacementToolValidate.log() } @@ -101,4 +100,4 @@ class PlacementToolController( } } -data class PlacementToolValidation(val count: Int) +data class PlacementToolValidation(val count: Int, val existing: Int) diff --git a/service/src/main/kotlin/fi/espoo/evaka/application/PlacementToolService.kt b/service/src/main/kotlin/fi/espoo/evaka/application/PlacementToolService.kt index c27cff8ea87..97c2e3da348 100644 --- a/service/src/main/kotlin/fi/espoo/evaka/application/PlacementToolService.kt +++ b/service/src/main/kotlin/fi/espoo/evaka/application/PlacementToolService.kt @@ -59,6 +59,35 @@ class PlacementToolService( asyncJobRunner.registerHandler(::createPlacementToolApplicationsFromSsn) } + fun validatePlacementToolApplications( + tx: Database.Read, + clock: EvakaClock, + file: MultipartFile, + ): PlacementToolValidation { + val placements = file.inputStream.use { parsePlacementToolCsv(it) } + val (socialSecurityNumbers, personIds) = + placements.keys + .partition { isValidSSN(it) } + .let { it.first to it.second.map { id -> PersonId(UUID.fromString(id)) } } + val term = + findNextPreschoolTerm(tx, clock.today()) ?: error("Next preschool term not found") + val existing = + tx.createQuery { + sql( + """ +SELECT count(DISTINCT child.id) +FROM application +JOIN person child ON application.child_id = child.id +WHERE application.type = 'PRESCHOOL' + AND (application.document ->> 'preferredStartDate')::date = ${bind(term.finnishPreschool.start)} + AND (child.social_security_number = ANY (${bind(socialSecurityNumbers)}) OR child.id = ANY(${bind(personIds)})) + """ + ) + } + .exactlyOne() + return PlacementToolValidation(count = placements.size, existing = existing) + } + fun doCreatePlacementToolApplications( db: Database.Connection, clock: EvakaClock,