diff --git a/services/hanke-service/src/integrationTest/kotlin/fi/hel/haitaton/hanke/hakemus/HakemusControllerITest.kt b/services/hanke-service/src/integrationTest/kotlin/fi/hel/haitaton/hanke/hakemus/HakemusControllerITest.kt index 3fc1333a4..a33a2669d 100644 --- a/services/hanke-service/src/integrationTest/kotlin/fi/hel/haitaton/hanke/hakemus/HakemusControllerITest.kt +++ b/services/hanke-service/src/integrationTest/kotlin/fi/hel/haitaton/hanke/hakemus/HakemusControllerITest.kt @@ -36,6 +36,7 @@ import fi.hel.haitaton.hanke.factory.PaatosFactory import fi.hel.haitaton.hanke.factory.PaperDecisionReceiverFactory import fi.hel.haitaton.hanke.factory.TaydennysFactory import fi.hel.haitaton.hanke.factory.TaydennyspyyntoFactory +import fi.hel.haitaton.hanke.geometria.GeometriatDao import fi.hel.haitaton.hanke.getResourceAsBytes import fi.hel.haitaton.hanke.hankeError import fi.hel.haitaton.hanke.logging.DisclosureLogService @@ -767,7 +768,8 @@ class HakemusControllerITest(@Autowired override val mockMvc: MockMvc) : Control authorizer.authorizeHakemusId(id, PermissionCode.EDIT_APPLICATIONS.name) } returns true every { hakemusService.updateHakemus(id, request, USERNAME) } throws - HakemusGeometryException("Invalid geometry") + HakemusGeometryException( + HakemusFactory.create(), GeometriatDao.InvalidDetail("", "")) put(url, request) .andExpect(status().isBadRequest) @@ -831,7 +833,8 @@ class HakemusControllerITest(@Autowired override val mockMvc: MockMvc) : Control authorizer.authorizeHakemusId(id, PermissionCode.EDIT_APPLICATIONS.name) } returns true every { hakemusService.updateHakemus(id, request, USERNAME) } throws - InvalidHiddenRegistryKey(HakemusFactory.create(id = id), "Reason for error") + InvalidHiddenRegistryKey( + "Reason for error", CustomerType.COMPANY, CustomerType.PERSON) put(url, request) .andExpect(status().isBadRequest) diff --git a/services/hanke-service/src/integrationTest/kotlin/fi/hel/haitaton/hanke/hakemus/HakemusServiceITest.kt b/services/hanke-service/src/integrationTest/kotlin/fi/hel/haitaton/hanke/hakemus/HakemusServiceITest.kt index 5169378af..54920ea2d 100644 --- a/services/hanke-service/src/integrationTest/kotlin/fi/hel/haitaton/hanke/hakemus/HakemusServiceITest.kt +++ b/services/hanke-service/src/integrationTest/kotlin/fi/hel/haitaton/hanke/hakemus/HakemusServiceITest.kt @@ -1343,9 +1343,10 @@ class HakemusServiceITest( failure.all { hasClass(InvalidHiddenRegistryKey::class) - messageContains("id=${hakemus.id}") messageContains("RegistryKeyHidden used in an incompatible way") messageContains("New customer type doesn't match the old") + messageContains("New=PERSON") + messageContains("Old=COMPANY") } } @@ -1411,9 +1412,10 @@ class HakemusServiceITest( failure.all { hasClass(InvalidHiddenRegistryKey::class) - messageContains("id=${hakemus.id}") messageContains("RegistryKeyHidden used in an incompatible way") messageContains("New invoicing customer type doesn't match the old") + messageContains("New=PERSON") + messageContains("Old=COMPANY") } } diff --git a/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/hakemus/HakemusService.kt b/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/hakemus/HakemusService.kt index 183c535a0..9de1e7665 100644 --- a/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/hakemus/HakemusService.kt +++ b/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/hakemus/HakemusService.kt @@ -1,6 +1,7 @@ package fi.hel.haitaton.hanke.hakemus import fi.hel.haitaton.hanke.HankeEntity +import fi.hel.haitaton.hanke.HankeIdentifier import fi.hel.haitaton.hanke.HankeMapper import fi.hel.haitaton.hanke.HankeNotFoundException import fi.hel.haitaton.hanke.HankeRepository @@ -31,7 +32,9 @@ import fi.hel.haitaton.hanke.pdf.JohtoselvityshakemusPdfEncoder import fi.hel.haitaton.hanke.pdf.KaivuilmoitusPdfEncoder import fi.hel.haitaton.hanke.permissions.CurrentUserWithoutKayttajaException import fi.hel.haitaton.hanke.permissions.HankeKayttajaService -import fi.hel.haitaton.hanke.taydennys.TaydennysService +import fi.hel.haitaton.hanke.permissions.HankekayttajaEntity +import fi.hel.haitaton.hanke.taydennys.TaydennysRepository +import fi.hel.haitaton.hanke.taydennys.TaydennyspyyntoRepository import fi.hel.haitaton.hanke.toJsonString import fi.hel.haitaton.hanke.tormaystarkastelu.TormaystarkasteluLaskentaService import fi.hel.haitaton.hanke.valmistumisilmoitus.ValmistumisilmoitusEntity @@ -62,6 +65,8 @@ private const val PAPER_DECISION_MSG = class HakemusService( private val hakemusRepository: HakemusRepository, private val hankeRepository: HankeRepository, + private val taydennyspyyntoRepository: TaydennyspyyntoRepository, + private val taydennysRepository: TaydennysRepository, private val geometriatDao: GeometriatDao, private val hankealueService: HankealueService, private val hakemusLoggingService: HakemusLoggingService, @@ -73,7 +78,6 @@ class HakemusService( private val paatosService: PaatosService, private val applicationEventPublisher: ApplicationEventPublisher, private val tormaystarkasteluLaskentaService: TormaystarkasteluLaskentaService, - private val taydennysService: TaydennysService, ) { @Transactional(readOnly = true) @@ -83,8 +87,8 @@ class HakemusService( fun getWithExtras(hakemusId: Long): HakemusWithExtras { val hakemus = getById(hakemusId) val paatokset = paatosService.findByHakemusId(hakemusId) - val taydennyspyynto = taydennysService.findTaydennyspyynto(hakemusId) - val taydennys = taydennysService.findTaydennys(hakemusId) + val taydennyspyynto = taydennyspyyntoRepository.findByApplicationId(hakemusId)?.toDomain() + val taydennys = taydennysRepository.findByApplicationId(hakemusId)?.toDomain() return HakemusWithExtras(hakemus, paatokset, taydennyspyynto, taydennys) } @@ -186,27 +190,17 @@ class HakemusService( return hakemus } - assertGeometryValidity(request.areas) { validationError -> - "Invalid geometry received when updating hakemus. ${applicationEntity.logString()}, reason=${validationError.reason}, location=${validationError.location}" - } - + assertGeometryValidity(request.areas) { HakemusGeometryException(applicationEntity, it) } assertYhteystiedotValidity(applicationEntity, request) - - val hankeEntity = applicationEntity.hanke - if (!hankeEntity.generated) { - request.areas?.let { areas -> - assertGeometryCompatibility(hankeEntity.id, areas) { geometry -> - "Hakemus geometry doesn't match any hankealue when updating hakemus. " + - "${applicationEntity.logString()}, ${hankeEntity.logString()}, " + - "hakemus geometry=${geometry.toJsonString()}" - } - } - } else if (request is JohtoselvityshakemusUpdateRequest) { - updateHankealueet(hankeEntity, request) + assertOrUpdateHankealueet(applicationEntity.hanke, request) { + HakemusGeometryNotInsideHankeException(applicationEntity, applicationEntity.hanke, it) } - val updatedHakemus = saveWithUpdate(applicationEntity, request, userId).toHakemus() + val originalContactUserIds = applicationEntity.allContactUsers().map { it.id }.toSet() + val updatedHakemusEntity = saveWithUpdate(applicationEntity, request) + sendHakemusNotifications(updatedHakemusEntity, originalContactUserIds, userId) + val updatedHakemus = updatedHakemusEntity.toHakemus() logger.info("Updated hakemus. ${updatedHakemus.logString()}") hakemusLoggingService.logUpdate(hakemus, updatedHakemus, userId) @@ -236,9 +230,10 @@ class HakemusService( if (!hanke.generated) { hakemus.hakemusEntityData.areas?.let { areas -> assertGeometryCompatibility(hanke.id, areas) { geometry -> - "Hakemus geometry doesn't match any hankealue when sending hakemus, " + - "${hanke.logString()}, ${hakemus.logString()}, " + - "hakemus geometry=${geometry.toJsonString()}" + HakemusGeometryNotInsideHankeException( + "Hakemus geometry doesn't match any hankealue when sending hakemus, " + + "${hanke.logString()}, ${hakemus.logString()}, " + + "hakemus geometry=${geometry.toJsonString()}") } } } @@ -711,13 +706,13 @@ class HakemusService( } /** Assert that the geometries are valid. */ - private fun assertGeometryValidity( + fun assertGeometryValidity( areas: List?, - customMessageOnFailure: (GeometriatDao.InvalidDetail) -> String + exception: (GeometriatDao.InvalidDetail) -> Exception, ) { if (areas != null) { geometriatDao.validateGeometriat(areas.flatMap { it.geometries() })?.let { - throw HakemusGeometryException(customMessageOnFailure(it)) + throw exception(it) } } } @@ -726,14 +721,18 @@ class HakemusService( * Assert that the customers match and that the contacts in the update request are hanke users * of the application hanke. */ - private fun assertYhteystiedotValidity( + fun assertYhteystiedotValidity( hakemusEntity: HakemusEntity, updateRequest: HakemusUpdateRequest ) { val customersWithContacts = updateRequest.customersByRole() ApplicationContactType.entries.forEach { assertYhteystietoValidity( - hakemusEntity, it, hakemusEntity.yhteystiedot[it], customersWithContacts[it]) + hakemusEntity, + it, + hakemusEntity.yhteystiedot[it]?.id, + customersWithContacts[it], + ) } assertYhteyshenkilotValidity( @@ -756,19 +755,20 @@ class HakemusService( private fun assertYhteystietoValidity( application: HakemusIdentifier, rooli: ApplicationContactType, - hakemusyhteystietoEntity: HakemusyhteystietoEntity?, + yhteystietoEntityId: UUID?, customerWithContacts: CustomerWithContactsRequest? ) { if (customerWithContacts == null || customerWithContacts.customer.yhteystietoId == null || - customerWithContacts.customer.yhteystietoId == hakemusyhteystietoEntity?.id) { + customerWithContacts.customer.yhteystietoId == yhteystietoEntityId) { return } throw InvalidHakemusyhteystietoException( application, rooli, - hakemusyhteystietoEntity?.id, - customerWithContacts.customer.yhteystietoId) + yhteystietoEntityId, + customerWithContacts.customer.yhteystietoId, + ) } /** Assert that the contacts are users of the hanke. */ @@ -786,27 +786,43 @@ class HakemusService( } } + /** + * Assert that the geometries are compatible with the hanke area geometries or update the hanke + * geometries if this is in a generated hanke. + */ + fun assertOrUpdateHankealueet( + hankeEntity: HankeEntity, + request: HakemusUpdateRequest, + exceptionOnFailure: (Polygon) -> Exception + ) { + if (!hankeEntity.generated) { + request.areas?.let { areas -> + assertGeometryCompatibility(hankeEntity.id, areas, exceptionOnFailure) + } + } else if (request is JohtoselvityshakemusUpdateRequest) { + updateHankealueet(hankeEntity, request) + } + } + /** Assert that the geometries are compatible with the hanke area geometries. */ private fun assertGeometryCompatibility( hankeId: Int, areas: List, - customMessageOnFailure: (Polygon) -> String + exceptionOnFailure: (Polygon) -> Exception ) { areas.forEach { area -> when (area) { // for cable report we check that the geometry is inside any of the hanke areas is JohtoselvitysHakemusalue -> { if (!geometriatDao.isInsideHankeAlueet(hankeId, area.geometry)) - throw HakemusGeometryNotInsideHankeException( - customMessageOnFailure(area.geometry)) + throw exceptionOnFailure(area.geometry) } // for excavation notification we check that all the tyoalue geometries are inside // the same hanke area is KaivuilmoitusAlue -> { area.tyoalueet.forEach { tyoalue -> if (!geometriatDao.isInsideHankeAlue(area.hankealueId, tyoalue.geometry)) - throw HakemusGeometryNotInsideHankeException( - customMessageOnFailure(tyoalue.geometry)) + throw exceptionOnFailure(tyoalue.geometry) } } } @@ -830,40 +846,40 @@ class HakemusService( private fun saveWithUpdate( hakemusEntity: HakemusEntity, request: HakemusUpdateRequest, - userId: String, ): HakemusEntity { - val originalContactUserIds = hakemusEntity.allContactUsers().map { it.id }.toSet() + logger.info { "Creating and saving new hakemus data. ${hakemusEntity.logString()}" } val updatedApplicationEntity = hakemusEntity.copy( - hakemusEntityData = request.toEntityData(hakemusEntity), + hakemusEntityData = request.toEntityData(hakemusEntity.hakemusEntityData), yhteystiedot = updateYhteystiedot(hakemusEntity, request.customersByRole())) if (updatedApplicationEntity.hanke.generated) { updatedApplicationEntity.hanke.nimi = request.name } - updateTormaystarkastelut(updatedApplicationEntity) - sendApplicationNotifications(updatedApplicationEntity, originalContactUserIds, userId) + updateTormaystarkastelut(updatedApplicationEntity.hakemusEntityData)?.let { + updatedApplicationEntity.hakemusEntityData = it + } return hakemusRepository.save(updatedApplicationEntity) } /** Calculate the traffic nuisance indexes for each work area of an application. */ - private fun updateTormaystarkastelut(hakemusEntity: HakemusEntity) { - val hakemusEntityData = - when (val data = hakemusEntity.hakemusEntityData) { - is JohtoselvityshakemusEntityData -> return - is KaivuilmoitusEntityData -> data + fun updateTormaystarkastelut(hakemusData: HakemusEntityData): KaivuilmoitusEntityData? { + val kaivuilmoitusData = + when (hakemusData) { + is JohtoselvityshakemusEntityData -> return null + is KaivuilmoitusEntityData -> hakemusData } - if (hakemusEntityData.startTime == null || hakemusEntityData.endTime == null) { - return + if (kaivuilmoitusData.startTime == null || kaivuilmoitusData.endTime == null) { + return null } val areas = - hakemusEntityData.areas?.map { area -> + kaivuilmoitusData.areas?.map { area -> updateTormaystarkastelutForArea( area, - hakemusEntityData.startTime.toLocalDate(), - hakemusEntityData.endTime.toLocalDate(), + kaivuilmoitusData.startTime.toLocalDate(), + kaivuilmoitusData.endTime.toLocalDate(), ) } - hakemusEntity.hakemusEntityData = hakemusEntityData.copy(areas = areas) + return kaivuilmoitusData.copy(areas = areas) } private fun updateTormaystarkastelutForArea( @@ -908,7 +924,11 @@ class HakemusService( return null } return hakemusEntity.yhteystiedot[rooli]?.let { - customerWithContactsRequest.toExistingHakemusyhteystietoEntity(it, hakemusEntity) + val newHenkilot = customerWithContactsRequest.toExistingYhteystietoEntity(it) + newHenkilot.map { hankekayttajaId -> + it.yhteyshenkilot.add(newHakemusyhteyshenkiloEntity(hankekayttajaId, it)) + } + it } ?: customerWithContactsRequest.toNewHakemusyhteystietoEntity(rooli, hakemusEntity) } @@ -926,10 +946,12 @@ class HakemusService( application = hakemusEntity, ) .apply { - yhteyshenkilot.addAll(contacts.map { it.toNewHakemusyhteyshenkiloEntity(this) }) + yhteyshenkilot.addAll( + contacts.map { newHakemusyhteyshenkiloEntity(it.hankekayttajaId, this) }) } - private fun ContactRequest.toNewHakemusyhteyshenkiloEntity( + private fun newHakemusyhteyshenkiloEntity( + hankekayttajaId: UUID, hakemusyhteystietoEntity: HakemusyhteystietoEntity ) = HakemusyhteyshenkiloEntity( @@ -939,53 +961,23 @@ class HakemusService( hankekayttajaId, hakemusyhteystietoEntity.application.hanke.id), tilaaja = false) - private fun CustomerWithContactsRequest.toExistingHakemusyhteystietoEntity( - hakemusyhteystietoEntity: HakemusyhteystietoEntity, - hakemus: HakemusIdentifier, - ): HakemusyhteystietoEntity { - if (customer.type != hakemusyhteystietoEntity.tyyppi && customer.registryKeyHidden) { - // If new customer type doesn't match the old one, the type of registry key will be - // wrong, but it will be retained if the key is hidden. - // Validation only checks the new type. - throw InvalidHiddenRegistryKey(hakemus, "New customer type doesn't match the old.") - } - hakemusyhteystietoEntity.tyyppi = customer.type - hakemusyhteystietoEntity.nimi = customer.name - hakemusyhteystietoEntity.sahkoposti = customer.email - hakemusyhteystietoEntity.puhelinnumero = customer.phone - if (!customer.registryKeyHidden) { - hakemusyhteystietoEntity.registryKey = customer.registryKey - } - hakemusyhteystietoEntity.yhteyshenkilot.update(hakemusyhteystietoEntity, this.contacts) - return hakemusyhteystietoEntity - } - - private fun MutableList.update( - hakemusyhteystietoEntity: HakemusyhteystietoEntity, - contacts: List - ) { - val existingIds = this.map { it.hankekayttaja.id }.toSet() - val newIds = contacts.map { it.hankekayttajaId }.toSet() - val toRemove = existingIds.minus(newIds) - val toAdd = newIds.minus(existingIds) - this.removeIf { toRemove.contains(it.hankekayttaja.id) } - this.addAll( - toAdd.map { - ContactRequest(it).toNewHakemusyhteyshenkiloEntity(hakemusyhteystietoEntity) - }) - } - - private fun sendApplicationNotifications( + private fun sendHakemusNotifications( hakemusEntity: HakemusEntity, excludedUserIds: Set, userId: String, ) { val newContacts = hakemusEntity.allContactUsers().filterNot { excludedUserIds.contains(it.id) } - if (newContacts.isEmpty()) { - return + if (newContacts.isNotEmpty()) { + sendHakemusNotifications(newContacts, hakemusEntity, userId) } + } + fun sendHakemusNotifications( + newContacts: List, + hakemusEntity: HakemusEntity, + userId: String, + ) { val inviter = hankeKayttajaService.getKayttajaByUserId(hakemusEntity.hanke.id, userId) ?: throw CurrentUserWithoutKayttajaException(userId) @@ -1067,6 +1059,44 @@ class HakemusService( } fun isCancelled(alluStatus: ApplicationStatus?) = alluStatus == ApplicationStatus.CANCELLED + + companion object { + /** @return HankekayttajaIDs of new yhteyshenkilot that need to be added. */ + fun CustomerWithContactsRequest.toExistingYhteystietoEntity( + yhteystietoEntity: YhteystietoEntity, + ): Set { + if (customer.type != yhteystietoEntity.tyyppi && customer.registryKeyHidden) { + // If new customer type doesn't match the old one, the type of registry key will be + // wrong, but it will be retained if the key is hidden. + // Validation only checks the new type. + throw InvalidHiddenRegistryKey( + "New customer type doesn't match the old.", + customer.type, + yhteystietoEntity.tyyppi) + } + yhteystietoEntity.tyyppi = customer.type + yhteystietoEntity.nimi = customer.name + yhteystietoEntity.sahkoposti = customer.email + yhteystietoEntity.puhelinnumero = customer.phone + if (!customer.registryKeyHidden) { + yhteystietoEntity.registryKey = customer.registryKey + } + val newHenkilot = yhteystietoEntity.yhteyshenkilot.update(this.contacts) + return newHenkilot + } + + /** @return HankekayttajaIDs of new yhteyshenkilot that need to be added. */ + private fun MutableList.update( + contacts: List + ): Set { + val existingIds = this.map { it.hankekayttaja.id }.toSet() + val newIds = contacts.map { it.hankekayttajaId }.toSet() + val toRemove = existingIds.minus(newIds) + val toAdd = newIds.minus(existingIds) + this.removeIf { toRemove.contains(it.hankekayttaja.id) } + return toAdd + } + } } class IncompatibleHakemusUpdateRequestException( @@ -1086,9 +1116,9 @@ class InvalidHakemusyhteystietoException( RuntimeException( "Invalid hakemusyhteystieto received when updating hakemus. ${application.logString()}, role=$rooli, yhteystietoId=$yhteystietoId, newId=$newId") -class InvalidHiddenRegistryKey(hakemus: HakemusIdentifier, message: String) : +class InvalidHiddenRegistryKey(message: String, newType: CustomerType, oldType: CustomerType?) : RuntimeException( - "RegistryKeyHidden used in an incompatible way: $message ${hakemus.logString()}") + "RegistryKeyHidden used in an incompatible way: $message New=$newType Old=$oldType") class InvalidHakemusyhteyshenkiloException(message: String) : RuntimeException(message) @@ -1103,9 +1133,23 @@ class HakemusAlreadySentException(id: Long?, alluid: Int?, status: ApplicationSt class HakemusAlreadyProcessingException(id: Long?, alluid: Int?) : RuntimeException("Hakemus is no longer pending in Allu, id=$id, alluId=$alluid") -class HakemusGeometryException(message: String) : RuntimeException(message) +class HakemusGeometryException( + hakemus: HakemusIdentifier, + validationError: GeometriatDao.InvalidDetail +) : + RuntimeException( + "Invalid geometry received when updating hakemus. ${hakemus.logString()}, reason=${validationError.reason}, location=${validationError.location}") -class HakemusGeometryNotInsideHankeException(message: String) : RuntimeException(message) +class HakemusGeometryNotInsideHankeException(message: String) : RuntimeException(message) { + constructor( + hakemus: HakemusIdentifier, + hanke: HankeIdentifier, + geometry: Polygon, + ) : this( + "Hakemus geometry doesn't match any hankealue when updating hakemus. " + + "${hakemus.logString()}, ${hanke.logString()}, " + + "hakemus geometry=${geometry.toJsonString()}") +} class HakemusDecisionNotFoundException(message: String) : RuntimeException(message) diff --git a/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/hakemus/HakemusUpdateRequest.kt b/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/hakemus/HakemusUpdateRequest.kt index 88b408687..46574c0ef 100644 --- a/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/hakemus/HakemusUpdateRequest.kt +++ b/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/hakemus/HakemusUpdateRequest.kt @@ -35,10 +35,10 @@ sealed interface HakemusUpdateRequest { fun hasChanges(hakemusData: HakemusData): Boolean /** - * Converts this update request to an [HakemusEntityData] object using the given [hakemusEntity] - * as a basis. + * Converts this update request to an [HakemusEntityData] object using the given + * [hakemusEntityData] as a basis. */ - fun toEntityData(hakemusEntity: HakemusEntity): HakemusEntityData + fun toEntityData(hakemusEntityData: HakemusEntityData): HakemusEntityData fun customersByRole(): Map } @@ -106,8 +106,8 @@ data class JohtoselvityshakemusUpdateRequest( representativeWithContacts.hasChanges(hakemusData.propertyDeveloperWithContacts) } - override fun toEntityData(hakemusEntity: HakemusEntity) = - (hakemusEntity.hakemusEntityData as JohtoselvityshakemusEntityData).copy( + override fun toEntityData(hakemusEntityData: HakemusEntityData) = + (hakemusEntityData as JohtoselvityshakemusEntityData).copy( name = this.name, postalAddress = PostalAddress(StreetAddress(this.postalAddress?.streetAddress?.streetName), "", ""), @@ -210,8 +210,8 @@ data class KaivuilmoitusUpdateRequest( additionalInfo != hakemusData.additionalInfo } - override fun toEntityData(hakemusEntity: HakemusEntity) = - (hakemusEntity.hakemusEntityData as KaivuilmoitusEntityData).copy( + override fun toEntityData(hakemusEntityData: HakemusEntityData) = + (hakemusEntityData as KaivuilmoitusEntityData).copy( name = this.name, workDescription = this.workDescription, constructionWork = this.constructionWork, @@ -225,7 +225,7 @@ data class KaivuilmoitusUpdateRequest( startTime = this.startTime, endTime = this.endTime, areas = this.areas, - invoicingCustomer = this.invoicingCustomer.toCustomer(hakemusEntity), + invoicingCustomer = this.invoicingCustomer.toCustomer(hakemusEntityData), customerReference = this.invoicingCustomer?.customerReference, additionalInfo = this.additionalInfo, ) @@ -349,15 +349,15 @@ fun InvoicingPostalAddressRequest?.hasChanges(postalAddress: PostalAddress?): Bo city != postalAddress.city } -fun InvoicingCustomerRequest?.toCustomer(hakemus: HakemusEntity): InvoicingCustomer? { +fun InvoicingCustomerRequest?.toCustomer(hakemusEntityData: HakemusEntityData): InvoicingCustomer? { return this?.let { - val baseData = (hakemus.hakemusEntityData as KaivuilmoitusEntityData).invoicingCustomer + val baseData = (hakemusEntityData as KaivuilmoitusEntityData).invoicingCustomer if (baseData != null && type != baseData.type && registryKeyHidden) { // If new invoicing customer type doesn't match the old one, the type of registry key // will be wrong, but it will be retained if the key is hidden. // Validation only checks the new type. throw InvalidHiddenRegistryKey( - hakemus, "New invoicing customer type doesn't match the old.") + "New invoicing customer type doesn't match the old.", type, baseData.type) } InvoicingCustomer( diff --git a/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/taydennys/TaydennysController.kt b/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/taydennys/TaydennysController.kt index 5616c08eb..bff279f2c 100644 --- a/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/taydennys/TaydennysController.kt +++ b/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/taydennys/TaydennysController.kt @@ -2,6 +2,8 @@ package fi.hel.haitaton.hanke.taydennys import fi.hel.haitaton.hanke.HankeError import fi.hel.haitaton.hanke.currentUserId +import fi.hel.haitaton.hanke.hakemus.HakemusUpdateRequest +import fi.hel.haitaton.hanke.hakemus.ValidHakemusUpdateRequest import fi.hel.haitaton.hanke.logging.DisclosureLogService import io.swagger.v3.oas.annotations.Hidden import io.swagger.v3.oas.annotations.Operation @@ -10,6 +12,7 @@ import io.swagger.v3.oas.annotations.media.Schema import io.swagger.v3.oas.annotations.responses.ApiResponse import io.swagger.v3.oas.annotations.responses.ApiResponses import io.swagger.v3.oas.annotations.security.SecurityRequirement +import java.util.UUID import mu.KotlinLogging import org.springframework.http.HttpStatus import org.springframework.security.access.prepost.PreAuthorize @@ -17,6 +20,8 @@ import org.springframework.validation.annotation.Validated import org.springframework.web.bind.annotation.ExceptionHandler import org.springframework.web.bind.annotation.PathVariable 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.ResponseStatus import org.springframework.web.bind.annotation.RestController @@ -62,6 +67,14 @@ class TaydennysController( return response } + @RequestMapping("/taydennykset/{id}") + @PreAuthorize("@taydennysAuthorizer.authorize(#id, 'EDIT_APPLICATIONS')") + fun update( + @PathVariable id: UUID, + @ValidHakemusUpdateRequest @RequestBody request: HakemusUpdateRequest + ): TaydennysResponse = + taydennysService.updateTaydennys(id, request, currentUserId()).toResponse() + @ExceptionHandler(NoTaydennyspyyntoException::class) @ResponseStatus(HttpStatus.CONFLICT) @Hidden diff --git a/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/taydennys/TaydennysEntity.kt b/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/taydennys/TaydennysEntity.kt index 2ea4f41f5..dcd5187e8 100644 --- a/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/taydennys/TaydennysEntity.kt +++ b/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/taydennys/TaydennysEntity.kt @@ -1,10 +1,12 @@ package fi.hel.haitaton.hanke.taydennys import fi.hel.haitaton.hanke.hakemus.ApplicationContactType +import fi.hel.haitaton.hanke.hakemus.ApplicationType import fi.hel.haitaton.hanke.hakemus.HakemusEntityData import fi.hel.haitaton.hanke.hakemus.Hakemusyhteystieto import fi.hel.haitaton.hanke.hakemus.JohtoselvityshakemusEntityData import fi.hel.haitaton.hanke.hakemus.KaivuilmoitusEntityData +import fi.hel.haitaton.hanke.permissions.HankekayttajaEntity import io.hypersistence.utils.hibernate.type.json.JsonType import jakarta.persistence.CascadeType import jakarta.persistence.Column @@ -25,7 +27,7 @@ import org.hibernate.annotations.Type @Entity @Table(name = "taydennys") class TaydennysEntity( - @Id var id: UUID = UUID.randomUUID(), + @Id override var id: UUID = UUID.randomUUID(), @OneToOne(fetch = FetchType.LAZY, optional = false) @OnDelete(action = OnDeleteAction.CASCADE) @JoinColumn(name = "taydennyspyynto_id", nullable = false) @@ -43,7 +45,7 @@ class TaydennysEntity( @BatchSize(size = 100) var yhteystiedot: MutableMap = mutableMapOf(), -) { +) : TaydennysIdentifier { fun toDomain(): Taydennys { val yhteystiedot: Map = yhteystiedot.mapValues { it.value.toDomain() } @@ -60,4 +62,19 @@ class TaydennysEntity( hakemusData = applicationData, ) } + + override fun taydennyspyyntoId(): UUID = taydennyspyynto.id + + override fun taydennyspyyntoAlluId(): Int = taydennyspyynto.alluId + + override fun hakemusId(): Long = taydennyspyynto.applicationId + + override fun hakemustyyppi(): ApplicationType = hakemusData.applicationType + + /** Returns all distinct contact users for this täydennys. */ + fun allContactUsers(): List = + yhteystiedot.values + .flatMap { it.yhteyshenkilot } + .map { it.hankekayttaja } + .distinctBy { it.id } } diff --git a/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/taydennys/TaydennysIdentifier.kt b/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/taydennys/TaydennysIdentifier.kt new file mode 100644 index 000000000..0c2617727 --- /dev/null +++ b/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/taydennys/TaydennysIdentifier.kt @@ -0,0 +1,28 @@ +package fi.hel.haitaton.hanke.taydennys + +import fi.hel.haitaton.hanke.domain.HasId +import fi.hel.haitaton.hanke.hakemus.ApplicationType +import java.util.UUID + +interface TaydennysIdentifier : HasId { + override val id: UUID + + fun taydennyspyyntoId(): UUID + + fun taydennyspyyntoAlluId(): Int + + fun hakemusId(): Long + + fun hakemustyyppi(): ApplicationType + + fun logString() = + "Täydennys: (" + + listOf( + "id=$id", + "täydennyspyyntö=${taydennyspyyntoId()}", + "täydennyspyyntöAlluId=${taydennyspyyntoAlluId()}", + "hakemusId=${hakemusId()}", + "hakemustyyppi=${hakemustyyppi()}") + .joinToString() + + ")" +} diff --git a/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/taydennys/TaydennysService.kt b/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/taydennys/TaydennysService.kt index c3c4a9fdc..37bc543dc 100644 --- a/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/taydennys/TaydennysService.kt +++ b/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/taydennys/TaydennysService.kt @@ -1,24 +1,41 @@ package fi.hel.haitaton.hanke.taydennys +import fi.hel.haitaton.hanke.HankeIdentifier import fi.hel.haitaton.hanke.allu.AlluClient import fi.hel.haitaton.hanke.allu.ApplicationStatus +import fi.hel.haitaton.hanke.geometria.GeometriatDao +import fi.hel.haitaton.hanke.hakemus.ApplicationContactType +import fi.hel.haitaton.hanke.hakemus.ApplicationType +import fi.hel.haitaton.hanke.hakemus.CustomerWithContactsRequest import fi.hel.haitaton.hanke.hakemus.HakemusEntity import fi.hel.haitaton.hanke.hakemus.HakemusIdentifier import fi.hel.haitaton.hanke.hakemus.HakemusInWrongStatusException import fi.hel.haitaton.hanke.hakemus.HakemusRepository +import fi.hel.haitaton.hanke.hakemus.HakemusService +import fi.hel.haitaton.hanke.hakemus.HakemusService.Companion.toExistingYhteystietoEntity +import fi.hel.haitaton.hanke.hakemus.HakemusUpdateRequest import fi.hel.haitaton.hanke.hakemus.HakemusyhteyshenkiloEntity import fi.hel.haitaton.hanke.hakemus.HakemusyhteystietoEntity import fi.hel.haitaton.hanke.logging.TaydennysLoggingService +import fi.hel.haitaton.hanke.permissions.HankeKayttajaService +import fi.hel.haitaton.hanke.toJsonString +import java.util.UUID +import mu.KotlinLogging +import org.geojson.Polygon import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional +private val logger = KotlinLogging.logger {} + @Service class TaydennysService( private val taydennyspyyntoRepository: TaydennyspyyntoRepository, + private val hakemusService: HakemusService, private val taydennysRepository: TaydennysRepository, private val hakemusRepository: HakemusRepository, private val alluClient: AlluClient, private val loggingService: TaydennysLoggingService, + private val hankeKayttajaService: HankeKayttajaService ) { @Transactional(readOnly = true) fun findTaydennyspyynto(hakemusId: Long): Taydennyspyynto? = @@ -62,8 +79,7 @@ class TaydennysService( return saved } - @Transactional - fun createFromHakemus( + private fun createFromHakemus( hakemus: HakemusEntity, taydennyspyynto: TaydennyspyyntoEntity, ): TaydennysEntity { @@ -80,6 +96,152 @@ class TaydennysService( return taydennys } + @Transactional + fun updateTaydennys(id: UUID, request: HakemusUpdateRequest, currentUserId: String): Taydennys { + logger.info("Updating taydennys id=$id") + + val taydennysEntity = taydennysRepository.getReferenceById(id) + val originalTaydennys = taydennysEntity.toDomain() + + assertUpdateCompatible(taydennysEntity, request) + + if (!request.hasChanges(originalTaydennys.hakemusData)) { + logger.info("Not updating unchanged hakemus data. ${taydennysEntity.id}") + return originalTaydennys + } + + hakemusService.assertGeometryValidity(request.areas) { + TaydennysGeometryException(taydennysEntity, it) + } + + val hakemusEntity = hakemusRepository.getReferenceById(taydennysEntity.hakemusId()) + val hanke = hakemusEntity.hanke + hakemusService.assertYhteystiedotValidity(hakemusEntity, request) + hakemusService.assertOrUpdateHankealueet(hanke, request) { + TaydennysGeometryNotInsideHankeException(taydennysEntity, hakemusEntity, hanke, it) + } + val originalContactUserIds = taydennysEntity.allContactUsers().map { it.id }.toSet() + + val updatedTaydennysEntity = saveWithUpdate(taydennysEntity, request, hanke.id) + if (hanke.generated) { + hanke.nimi = request.name + } + sendHakemusNotifications( + updatedTaydennysEntity, hakemusEntity, originalContactUserIds, currentUserId) + + return originalTaydennys + } + + private fun assertUpdateCompatible( + taydennysEntity: TaydennysEntity, + request: HakemusUpdateRequest + ) { + if (taydennysEntity.hakemusData.applicationType != request.applicationType) { + throw IncompatibleTaydennysUpdateException( + taydennysEntity, + taydennysEntity.hakemusData.applicationType, + request.applicationType) + } + } + + /** Creates a new [TaydennysEntity] based on the given [request] and saves it. */ + private fun saveWithUpdate( + taydennysEntity: TaydennysEntity, + request: HakemusUpdateRequest, + hankeId: Int, + ): TaydennysEntity { + logger.info { "Creating and saving new taydennys data. ${taydennysEntity.logString()}" } + taydennysEntity.hakemusData = request.toEntityData(taydennysEntity.hakemusData) + taydennysEntity.yhteystiedot = + updateYhteystiedot(taydennysEntity, request.customersByRole(), hankeId) + + hakemusService.updateTormaystarkastelut(taydennysEntity.hakemusData)?.let { + taydennysEntity.hakemusData = it + } + return taydennysEntity + } + + private fun updateYhteystiedot( + taydennysEntity: TaydennysEntity, + newYhteystiedot: Map, + hankeId: Int, + ): MutableMap { + val updatedYhteystiedot = mutableMapOf() + ApplicationContactType.entries.forEach { rooli -> + updateYhteystieto(rooli, taydennysEntity, newYhteystiedot[rooli], hankeId)?.let { + updatedYhteystiedot[rooli] = it + } + } + return updatedYhteystiedot + } + + private fun updateYhteystieto( + rooli: ApplicationContactType, + taydennysEntity: TaydennysEntity, + customerWithContactsRequest: CustomerWithContactsRequest?, + hankeId: Int, + ): TaydennysyhteystietoEntity? { + if (customerWithContactsRequest == null) { + // customer was deleted + return null + } + return taydennysEntity.yhteystiedot[rooli]?.let { + val newHenkilot = customerWithContactsRequest.toExistingYhteystietoEntity(it) + newHenkilot.map { hankekayttajaId -> + it.yhteyshenkilot.add(newTaydennysyhteyshenkiloEntity(hankekayttajaId, it, hankeId)) + } + it + } + ?: customerWithContactsRequest.toNewTaydennysyhteystietoEntity( + rooli, taydennysEntity, hankeId) + } + + private fun CustomerWithContactsRequest.toNewTaydennysyhteystietoEntity( + rooli: ApplicationContactType, + taydennysEntity: TaydennysEntity, + hankeId: Int, + ) = + TaydennysyhteystietoEntity( + tyyppi = customer.type, + rooli = rooli, + nimi = customer.name, + sahkoposti = customer.email, + puhelinnumero = customer.phone, + registryKey = customer.registryKey, + taydennys = taydennysEntity, + ) + .apply { + yhteyshenkilot.addAll( + contacts.map { + newTaydennysyhteyshenkiloEntity(it.hankekayttajaId, this, hankeId) + }) + } + + private fun newTaydennysyhteyshenkiloEntity( + hankekayttajaId: UUID, + taydennysyhteystietoEntity: TaydennysyhteystietoEntity, + hankeId: Int, + ): TaydennysyhteyshenkiloEntity { + return TaydennysyhteyshenkiloEntity( + taydennysyhteystieto = taydennysyhteystietoEntity, + hankekayttaja = hankeKayttajaService.getKayttajaForHanke(hankekayttajaId, hankeId), + tilaaja = false, + ) + } + + private fun sendHakemusNotifications( + taydennysEntity: TaydennysEntity, + hakemusEntity: HakemusEntity, + excludedUserIds: Set, + userId: String, + ) { + val newContacts = + taydennysEntity.allContactUsers().filterNot { excludedUserIds.contains(it.id) } + if (newContacts.isNotEmpty()) { + hakemusService.sendHakemusNotifications(newContacts, hakemusEntity, userId) + } + } + private fun createYhteystieto( yhteystieto: HakemusyhteystietoEntity, taydennys: TaydennysEntity @@ -110,3 +272,32 @@ class TaydennysService( class NoTaydennyspyyntoException(hakemusId: Long) : RuntimeException("Application doesn't have an open taydennyspyynto. hakemusId=$hakemusId") + +class IncompatibleTaydennysUpdateException( + taydennysEntity: TaydennysEntity, + existingType: ApplicationType, + requestedType: ApplicationType +) : + RuntimeException( + "Invalid update request for taydennys. ${taydennysEntity.id}, existing type=$existingType, requested type=$requestedType") + +class TaydennysGeometryException( + taydennys: TaydennysIdentifier, + validationError: GeometriatDao.InvalidDetail, +) : + RuntimeException( + "Invalid geometry received when updating hakemus. " + + "${taydennys.logString()}, " + + "reason=${validationError.reason}, " + + "location=${validationError.location}") + +class TaydennysGeometryNotInsideHankeException( + taydennys: TaydennysIdentifier, + hakemus: HakemusIdentifier, + hanke: HankeIdentifier, + geometry: Polygon +) : + RuntimeException( + "Täydennys geometry doesn't match any hankealue when updating täydennys. " + + "${taydennys.logString()}, ${hakemus.logString()}, ${hanke.logString()}, " + + "hakemus geometry=${geometry.toJsonString()}") diff --git a/services/hanke-service/src/test/kotlin/fi/hel/haitaton/hanke/hakemus/HakemusServiceTest.kt b/services/hanke-service/src/test/kotlin/fi/hel/haitaton/hanke/hakemus/HakemusServiceTest.kt index 8ffb35cc8..4f9aa9d5d 100644 --- a/services/hanke-service/src/test/kotlin/fi/hel/haitaton/hanke/hakemus/HakemusServiceTest.kt +++ b/services/hanke-service/src/test/kotlin/fi/hel/haitaton/hanke/hakemus/HakemusServiceTest.kt @@ -36,7 +36,9 @@ import fi.hel.haitaton.hanke.logging.HankeLoggingService import fi.hel.haitaton.hanke.logging.Status import fi.hel.haitaton.hanke.paatos.PaatosService import fi.hel.haitaton.hanke.permissions.HankeKayttajaService +import fi.hel.haitaton.hanke.taydennys.TaydennysRepository import fi.hel.haitaton.hanke.taydennys.TaydennysService +import fi.hel.haitaton.hanke.taydennys.TaydennyspyyntoRepository import fi.hel.haitaton.hanke.test.AlluException import fi.hel.haitaton.hanke.test.USERNAME import fi.hel.haitaton.hanke.tormaystarkastelu.TormaystarkasteluLaskentaService @@ -71,6 +73,8 @@ import org.springframework.context.ApplicationEventPublisher class HakemusServiceTest { private val hakemusRepository: HakemusRepository = mockk() private val hankeRepository: HankeRepository = mockk() + private val taydennysRepository: TaydennysRepository = mockk() + private val taydennyspyyntoRepository: TaydennyspyyntoRepository = mockk() private val geometriatDao: GeometriatDao = mockk() private val hankealueService: HankealueService = mockk() private val loggingService: HakemusLoggingService = mockk(relaxUnitFun = true) @@ -88,6 +92,8 @@ class HakemusServiceTest { HakemusService( hakemusRepository, hankeRepository, + taydennyspyyntoRepository, + taydennysRepository, geometriatDao, hankealueService, loggingService, @@ -99,7 +105,6 @@ class HakemusServiceTest { paatosService, publisher, tormaystarkasteluLaskentaService, - taydennysService, ) @BeforeEach