From d17dd2af5c8b62b557fd0fcf6428623161bfc39a Mon Sep 17 00:00:00 2001 From: LEMERCIER Denis <denis.lemercier@soprasteria.com> Date: Wed, 31 Jan 2024 11:53:35 +0100 Subject: [PATCH] Initialisation du repository a partir de l'ancien --- .gitattributes | 2 + .gitignore | 31 + .gitlab-ci.yml | 107 + CHANGELOG.md | 44 + LICENSE | 201 ++ README.md | 230 +- assets/docker-compose/.env | 7 + assets/docker-compose/docker-compose.yml | 142 + .../08BDB65B-73CA-4FF0-8C1C-1040DA20E7B3.png | Bin 0 -> 136268 bytes .../2A4575F4-EE3B-478D-A397-BD8B409390E0.png | Bin 0 -> 6740 bytes .../3E1ED783-839A-4094-9A8B-8B02F6E5283E.png | Bin 0 -> 60396 bytes .../56BC42EB-2AAF-4A9B-A98C-9D29DB8E2520.png | Bin 0 -> 3610 bytes .../81945681-5CEB-4178-8AB8-4B9B6801FBC9.png | Bin 0 -> 4381 bytes .../8B74D910-915C-46FD-9C46-34A44FD3CBBA.png | Bin 0 -> 8555 bytes .../A8AF1630-6525-428D-85A6-E3C413982D97.png | Bin 0 -> 66370 bytes .../BB5C2199-6CA3-4E0E-8899-A95FF625CE38.png | Bin 0 -> 8910 bytes .../DBBC8E31-4CED-497B-A53F-43D78BCB1523.png | Bin 0 -> 6897 bytes .../E64BA1C4-E956-4B54-9CDF-BA4F4E321807.png | Bin 0 -> 7159 bytes .../FF00E9AF-13DC-409C-8512-975183F81063.png | Bin 0 -> 128984 bytes docs/DonneeEntree.plantuml | 129 + docs/Indicateurs.plantuml | 97 + docs/MoteurDeCalculG4IT_V1.1.adoc | 865 +++++++ docs/README.md | 4 + docs/References.plantuml | 97 + docs/Traces.plantuml | 107 + docs/archi_fonctionnelle_V4.png | Bin 0 -> 198960 bytes ...chitecture_fonctionnelle_simplifiee_V1.png | Bin 0 -> 122857 bytes "docs/cin\303\251matique.png" | Bin 0 -> 36723 bytes docs/cycle_de_vie.jpg | Bin 0 -> 40376 bytes e2e/.env | 31 + e2e/.gitignore | 6 + e2e/1_load_ref.sh | 14 + e2e/2_generate_dataset.sh | 105 + e2e/3_load_input.sh | 27 + e2e/4_check.sh | 21 + e2e/5_assert.sh | 36 + e2e/README.md | 37 + e2e/e2e.iml | 9 + e2e/input_template/Application.csv | 2 + e2e/input_template/DataCenter.csv | 9 + .../EquipementPhysique_hors_serveur.csv | 101 + .../EquipementPhysique_serveur.csv | 101 + e2e/input_template/EquipementVirtuel.csv | 2 + e2e/performance-test.sh | 62 + e2e/utils.sh | 7 + services/.workspace/.idea/.gitignore | 5 + .../.workspace/.idea/codeStyles/Project.xml | 7 + .../.idea/codeStyles/codeStyleConfig.xml | 5 + services/.workspace/.idea/compiler.xml | 55 + services/.workspace/.idea/encodings.xml | 20 + .../inspectionProfiles/Project_Default.xml | 6 + services/.workspace/.idea/jarRepositories.xml | 30 + services/.workspace/.idea/misc.xml | 21 + services/.workspace/.idea/modules.xml | 8 + .../ApiEventCalculsApplication.xml | 16 + .../ApiEventDonneesEntreesApplication.xml | 16 + .../ExpositionDonneesEntreesApplication.xml | 16 + .../.idea/runConfigurations/Init.xml | 10 + .../ReferentielApplication.xml | 16 + .../.idea/runConfigurations/Run.xml | 9 + .../api_event_calculs__gen_sources_.xml | 47 + ...expositiondonneesentrees__gen_sources_.xml | 47 + .../runConfigurations/install_calculs.xml | 47 + .../runConfigurations/install_common.xml | 47 + .../.idea/runConfigurations/install_core.xml | 47 + .../.workspace/.idea/saveactions_settings.xml | 19 + services/.workspace/.idea/uiDesigner.xml | 124 + services/.workspace/.idea/vcs.xml | 6 + services/.workspace/settings.xml | 19 + services/api-event-calculs/.gitignore | 33 + services/api-event-calculs/LICENSE.txt | 201 ++ services/api-event-calculs/README.md | 2 + .../dependency_check_suppressions.xml | 97 + services/api-event-calculs/pom.xml | 288 +++ .../calculs/ApiEventCalculsApplication.java | 13 + .../domain/exception/DatabaseException.java | 18 + .../exception/ExternalApiException.java | 18 + .../model/CalculEquipementPhysique.java | 24 + .../domain/model/CalculMessagerie.java | 20 + .../calculs/domain/model/CalculSizes.java | 16 + .../client/ReferentielClient.java | 133 + .../infrastructure/config/CacheConfig.java | 35 + .../config/CommonIntegrationConfig.java | 31 + .../config/DomainConfiguration.java | 48 + .../config/KafkaConfiguration.java | 25 + .../config/OpenApiConfiguration.java | 24 + .../config/SecurityConfiguration.java | 30 + .../controller/ExceptionHandler.java | 65 + .../export/IndicateurController.java | 159 ++ .../calculs/CalculsApplicationController.java | 42 + .../CalculsEquipementPhysiqueController.java | 48 + .../CalculsEquipementVirtuelController.java | 42 + .../calculs/CalculsMessagerieController.java | 42 + .../rest/calculs/CalculsReseauController.java | 42 + .../sync/calculs/SyncCalculsController.java | 27 + .../ListenEquipementPhysique.java | 22 + .../kafkalistener/ListenMessagerie.java | 23 + .../infrastructure/mapper/DomainMapper.java | 78 + .../infrastructure/mapper/EntreesMapper.java | 20 + .../mapper/ReferentielMapper.java | 41 + .../repository/ApplicationRepository.java | 66 + .../EquipementPhysiqueRepository.java | 110 + .../EquipementVirtuelRepository.java | 67 + .../repository/IndicateurRepository.java | 327 +++ .../repository/MessagerieRepository.java | 74 + .../calcul/CalculApplicationService.java | 45 + .../CalculEquipementPhysiqueService.java | 64 + .../CalculEquipementVirtuelService.java | 88 + .../calcul/CalculMessagerieService.java | 48 + .../service/calcul/CalculReseauService.java | 58 + .../IntegrationCalculEquipementsService.java | 130 + .../IntegrationCalculMessagerieService.java | 42 + .../calcul/MainEquipementPhysiqueService.java | 23 + .../service/calcul/MainMessagerieService.java | 23 + ...richissementEquipementPhysiqueService.java | 97 + .../EnrichissementMessagerieService.java | 35 + .../rest/calculs/CheckIndicateurService.java | 11 + .../sync/calculs/SyncCalculService.java | 51 + .../src/main/resources/application.yaml | 59 + .../src/main/resources/logback.xml | 31 + .../src/main/resources/schema.sql | 164 ++ .../ApiEventCalculsApplicationTests.java | 165 ++ .../CalculEquipementVirtuelServiceTest.java | 68 + .../calcul/CalculReseauServiceTest.java | 42 + ...issementEquipementPhysiqueServiceTest.java | 164 ++ .../EnrichissementMessagerieServiceTest.java | 60 + .../calculs/CheckIndicateurServiceTest.java | 21 + .../sync/calculs/SyncCalculServiceTest.java | 100 + .../src/test/resources/application-test.yaml | 2 + .../resources/input/equipementPhysique.json | 194 ++ .../src/test/resources/input/messagerie.json | 16 + .../src/test/resources/logback-test.xml | 26 + services/api-event-donneesentrees/.gitignore | 33 + services/api-event-donneesentrees/README.md | 3 + .../dependency_check_suppressions.xml | 97 + services/api-event-donneesentrees/pom.xml | 161 ++ .../ApiEventDonneesEntreesApplication.java | 17 + .../EquipementPhysiqueIntegrationConfig.java | 167 ++ .../config/MessagerieIntegrationConfig.java | 121 + .../infrastructure/utils/Constants.java | 7 + .../src/main/resources/application.yaml | 55 + .../src/main/resources/logback.xml | 25 + .../IntegrationEquipementPhysiqueTest.java | 71 + .../IntegrationMessagerieTest.java | 71 + .../donneesentrees/test/jdbc/ScriptUtils.java | 29 + .../test/kafka/KafkaConsumer.java | 32 + .../test/kafka/KafkaConsumerConfig.java | 39 + .../KafkaConsumerEquipementPhysique.java | 18 + .../test/kafka/KafkaConsumerMessagerie.java | 18 + .../src/test/resources/application-test.yaml | 33 + .../src/test/resources/logback-test.xml | 26 + .../test/resources/sql/equipment_physique.sql | 14 + .../src/test/resources/sql/messagerie.sql | 6 + .../src/test/resources/sql/schema.sql | 138 + .../api-expositiondonneesentrees/.gitignore | 33 + .../api-expositiondonneesentrees/LICENSE.txt | 201 ++ .../api-expositiondonneesentrees/README.md | 64 + .../dependency_check_suppressions.xml | 99 + services/api-expositiondonneesentrees/pom.xml | 401 +++ .../ExpositionDonneesEntreesApplication.java | 15 + .../domain/exception/NotFoundException.java | 13 + .../ReferentielRuntimeException.java | 21 + .../domain/exception/RestException.java | 8 + .../domain/exception/ValidationException.java | 12 + .../domain/model/AbstractEntree.java | 53 + .../domain/model/Application.java | 25 + .../domain/model/DataCenter.java | 24 + .../domain/model/DemandeCalcul.java | 20 + .../domain/model/DonneesEntree.java | 30 + .../domain/model/Entite.java | 21 + .../domain/model/EquipementPhysique.java | 37 + .../domain/model/EquipementVirtuel.java | 27 + .../domain/model/Messagerie.java | 23 + .../domain/model/RapportDemandeCalcul.java | 25 + .../domain/model/RapportImport.java | 26 + .../domain/model/ResultatImport.java | 24 + .../domain/model/Volume.java | 4 + .../ports/input/ImportDonneesEntreePort.java | 52 + .../ports/input/SoumissionCalculPort.java | 41 + .../ports/input/SoumissionCalculSyncPort.java | 35 + .../ports/input/StatutPourCalculPort.java | 16 + .../impl/ImportDonneesEntreePortImpl.java | 510 ++++ .../impl/SoumissionCalculSyncPortImpl.java | 52 + .../input/impl/StatutPourCalculPortImpl.java | 61 + .../ports/output/CalculsServicePort.java | 7 + .../ports/output/EntreePersistencePort.java | 16 + .../ports/output/ReferentielServicePort.java | 20 + .../ports/output/SaveDonneesEntreePort.java | 14 + .../adapters/CalculsRestClient.java | 36 + .../adapters/ReferentielRestClient.java | 86 + .../cache/SchedulerEvictCache.java | 25 + .../config/ApplicationPortConfig.java | 26 + .../config/CommonIntegrationConfig.java | 50 + .../infrastructure/config/GeneralConfig.java | 32 + .../config/MessageProperties.java | 20 + .../infrastructure/config/OpenApiConfig.java | 47 + .../config/security/SecurityConfig.java | 63 + .../controller/CalculController.java | 59 + .../controller/ImportCSVController.java | 172 ++ .../controller/RestExceptionHandler.java | 80 + .../RestClientResponseErrorHandler.java | 40 + .../infrastructure/helper/CSVHelper.java | 75 + .../infrastructure/helper/Constants.java | 11 + .../jdbc/SoumissionCalculPortJdbcImpl.java | 67 + .../infrastructure/jdbc/VolumeJdbc.java | 75 + .../jpa/adapter/ApplicationJpaAdapter.java | 27 + .../jpa/adapter/DataCenterJpaAdapter.java | 29 + .../jpa/adapter/DonneesEntreesJpaAdapter.java | 30 + .../jpa/adapter/EntiteJpaAdapter.java | 29 + .../adapter/EquipementPhysiqueJpaAdapter.java | 34 + .../adapter/EquipementVirtuelJpaAdapter.java | 29 + .../jpa/adapter/MessagerieJpaAdapter.java | 29 + .../jpa/adapter/SaveDonneesEntreeAdapter.java | 145 ++ .../jpa/entity/AbstractEntreeEntity.java | 53 + .../jpa/entity/ApplicationEntity.java | 74 + .../jpa/entity/DataCenterEntity.java | 38 + .../jpa/entity/DonneesEntreesEntity.java | 58 + .../jpa/entity/EntiteEntity.java | 37 + .../jpa/entity/EquipementPhysiqueEntity.java | 52 + .../jpa/entity/EquipementVirtuelEntity.java | 33 + .../jpa/entity/MessagerieEntity.java | 37 + .../jpa/repository/ApplicationRepository.java | 31 + .../jpa/repository/DataCenterRepository.java | 11 + .../repository/DonneesEntreesRepository.java | 10 + .../jpa/repository/EntiteRepository.java | 10 + .../EquipementPhysiqueRepository.java | 49 + .../EquipementVirtuelRepository.java | 22 + .../jpa/repository/MessagerieRepository.java | 14 + .../mapper/CalculRestMapper.java | 18 + .../mapper/DonneesEntreeRestMapper.java | 12 + .../mapper/EntreeEntityMapper.java | 53 + .../infrastructure/mapper/VolumeMapper.java | 11 + .../ErrorManagementPostSaveService.java | 54 + .../service/ErrorManagementService.java | 114 + .../src/main/resources/application.yaml | 92 + .../src/main/resources/logback.xml | 26 + .../src/main/resources/schema.sql | 176 ++ .../src/main/resources/static/openapi.yaml | 575 +++++ .../ImportDonneesEntreePortImplTest.java | 160 ++ .../input/SoumissionCalculPortImplTest.java | 86 + .../input/StatutPourCalculPortImplTest.java | 106 + .../controller/ImportCSVControllerTest.java | 115 + .../controller/RestExceptionHandlerTest.java | 64 + .../adapter/ApplicationJpaAdapterTest.java | 67 + .../jpa/adapter/DataCenterJpaAdapterTest.java | 67 + .../adapter/DonneesEntreesJpaAdapterTest.java | 63 + .../jpa/adapter/EntiteJpaAdapterTest.java | 67 + .../EquipementPhysiqueJpaAdapterTest.java | 67 + .../EquipementVirtuelJpaAdapterTest.java | 67 + .../jpa/adapter/MessagerieJpaAdapterTest.java | 67 + .../test/DataTableUtils.java | 58 + .../src/test/resources/application-test.yaml | 36 + .../src/test/resources/equipementPhysique.csv | 2 + .../src/test/resources/logback-test.xml | 29 + services/api-referentiel/.gitignore | 32 + services/api-referentiel/README.md | 4 + .../dependency_check_suppressions.xml | 97 + services/api-referentiel/pom.xml | 206 ++ .../referentiel/ReferentielApplication.java | 13 + .../domain/data/ResultatImport.java | 20 + .../domain/exception/NotFoundException.java | 8 + .../exception/ReferentielException.java | 8 + .../ReferentielRuntimeException.java | 7 + .../domain/model/AbstractReferentiel.java | 7 + .../model/CorrespondanceRefEquipement.java | 16 + .../referentiel/domain/model/Critere.java | 22 + .../referentiel/domain/model/Etape.java | 21 + .../referentiel/domain/model/Hypothese.java | 22 + .../domain/model/ImpactEquipement.java | 29 + .../domain/model/ImpactMessagerie.java | 24 + .../domain/model/ImpactReseau.java | 30 + .../domain/model/MixElectrique.java | 25 + .../domain/model/TypeEquipement.java | 21 + .../domain/model/id/CritereId.java | 23 + .../referentiel/domain/model/id/EtapeId.java | 23 + .../domain/model/id/HypotheseId.java | 25 + .../domain/model/id/ImpactEquipementId.java | 24 + .../domain/model/id/ImpactReseauId.java | 27 + .../domain/model/id/MixElectriqueId.java | 22 + .../ports/input/ImportCSVReferentielPort.java | 70 + ...rtCorrespondanceRefEquipementPortImpl.java | 75 + .../input/impl/ImportCriterePortImpl.java | 74 + .../ports/input/impl/ImportEtapePortImpl.java | 73 + .../input/impl/ImportHypothesePortImpl.java | 75 + .../impl/ImportImpactEquipementPortImpl.java | 90 + .../impl/ImportImpactMessageriePortImpl.java | 74 + .../impl/ImportImpactReseauPortImpl.java | 87 + .../impl/ImportMixElectriquePortImpl.java | 80 + .../impl/ImportTypeEquipementPortImpl.java | 77 + .../output/ReferentielCsvExportService.java | 111 + .../output/ReferentielPersistencePort.java | 49 + ...ondanceRefEquipemenetCsvExportService.java | 43 + .../export/CritereCsvExportService.java | 43 + .../adapter/export/EtapeCsvExportService.java | 41 + .../export/HypotheseCsvExportService.java | 43 + .../ImpactEquipementCsvExportService.java | 47 + .../ImpactMessagerieCsvExportService.java | 48 + .../export/ImpactReseauCsvExportService.java | 48 + .../export/MixElectriqueCsvExportService.java | 49 + .../TypeEquipementCsvExportService.java | 47 + .../configuration/ReferentielConfig.java | 11 + .../openapi/ReferentielOpenApiConfig.java | 105 + .../security/SecurityConfig.java | 56 + ...CorrespondanceRefEquipementJpaAdapter.java | 66 + .../jpa/adapter/CritereJpaAdapter.java | 59 + .../jpa/adapter/EtapeJpaAdapter.java | 55 + .../jpa/adapter/HypotheseJpaAdapter.java | 60 + .../adapter/ImpactEquipementJpaAdapter.java | 69 + .../adapter/ImpactMessagerieJpaAdapter.java | 57 + .../jpa/adapter/ImpactReseauJpaAdapter.java | 62 + .../jpa/adapter/MixElectriqueJpaAdapter.java | 69 + .../jpa/adapter/TypeEquipementJpaAdapter.java | 60 + .../jpa/entity/AbstractReferentielEntity.java | 7 + .../CorrespondanceRefEquipementEntity.java | 49 + .../jpa/entity/CritereEntity.java | 44 + .../jpa/entity/EtapeEntity.java | 45 + .../jpa/entity/HypotheseEntity.java | 46 + .../jpa/entity/ImpactEquipementEntity.java | 74 + .../jpa/entity/ImpactMessagerieEntity.java | 46 + .../jpa/entity/ImpactReseauEntity.java | 54 + .../jpa/entity/MixElectriqueEntity.java | 51 + .../jpa/entity/TypeEquipementEntity.java | 47 + .../entity/id/AbstractReferentieIdEntity.java | 7 + .../jpa/entity/id/CritereIdEntity.java | 35 + .../jpa/entity/id/EtapeIdEntity.java | 36 + .../jpa/entity/id/HypotheseIdEntity.java | 36 + .../entity/id/ImpactEquipementIdEntity.java | 38 + .../jpa/entity/id/ImpactReseauIdEntity.java | 37 + .../jpa/entity/id/MixElectriqueIdEntity.java | 38 + ...CorrespondanceRefEquipementRepository.java | 11 + .../jpa/repository/CritereRepository.java | 22 + .../jpa/repository/EtapeRepository.java | 13 + .../jpa/repository/HypotheseRepository.java | 17 + .../ImpactEquipementRepository.java | 13 + .../ImpactMessagerieRepository.java | 11 + .../repository/ImpactReseauRepository.java | 13 + .../repository/MixElectriqueRepository.java | 13 + .../repository/TypeEquipementRepository.java | 11 + .../CorrespondanceRefEquipementMapper.java | 25 + .../infrastructure/mapper/CritereMapper.java | 20 + .../infrastructure/mapper/EtapeMapper.java | 22 + .../mapper/HypotheseMapper.java | 37 + .../mapper/ImpactEquipementMapper.java | 33 + .../mapper/ImpactMessagerieMapper.java | 24 + .../mapper/ImpactReseauMapper.java | 39 + .../mapper/MixElectriqueMapper.java | 32 + .../mapper/TypeEquipementMapper.java | 23 + .../controller/BaseExportReferentiel.java | 20 + ...ionCorrespondanceRefEquipementRestApi.java | 43 + ...ielCorrespondanceRefEquipementRestApi.java | 35 + ...orrespondanceRefEquipementRestApiImpl.java | 55 + ...ferentielAdministrationCritereRestApi.java | 44 + .../critere/ReferentielCritereRestApi.java | 36 + .../ReferentielCritereRestApiImpl.java | 56 + ...ReferentielAdministrationEtapeRestApi.java | 44 + .../etape/ReferentielEtapeRestApi.java | 37 + .../etape/ReferentielEtapeRestApiImpl.java | 56 + .../exception/ReferentelExceptionHandler.java | 79 + ...rentielAdministrationHypotheseRestApi.java | 44 + .../ReferentielHypotheseRestApi.java | 35 + .../ReferentielHypotheseRestApiImpl.java | 58 + ...AdministrationImpactEquipementRestApi.java | 44 + .../ReferentielImpactEquipementRestApi.java | 46 + ...eferentielImpactEquipementRestApiImpl.java | 66 + .../ReferentielImpactMessagerieRestApi.java | 61 + ...eferentielImpactMessagerieRestApiImpl.java | 59 + ...rentielInterneImpactMessagerieRestApi.java | 44 + ...tielAdministrationImpactReseauRestApi.java | 79 + .../ReferentielImpactReseauRestApi.java | 46 + .../ReferentielImpactReseauRestApiImpl.java | 93 + ...ielAdministrationMixElectriqueRestApi.java | 45 + .../ReferentielMixElecRestApiImpl.java | 75 + .../ReferentielMixElectriqueRestApi.java | 70 + ...elAdministrationTypeEquipementRestApi.java | 43 + .../ReferentielTypeEquipementRestApi.java | 63 + .../ReferentielTypeEquipementRestApiImpl.java | 60 + .../controller/version/VersionRestApi.java | 30 + .../version/VersionRestApiImpl.java | 18 + .../dto/CorrespondanceRefEquipementDTO.java | 30 + .../restapi/dto/CritereDTO.java | 33 + .../restapi/dto/ErrorResponseDTO.java | 42 + .../infrastructure/restapi/dto/EtapeDTO.java | 30 + .../restapi/dto/HypotheseDTO.java | 33 + .../restapi/dto/ImpactEquipementDTO.java | 52 + .../restapi/dto/ImpactMessagerieDTO.java | 38 + .../restapi/dto/ImpactReseauDTO.java | 58 + .../restapi/dto/MixElectriqueDTO.java | 41 + .../restapi/dto/RapportImportDTO.java | 18 + .../restapi/dto/TypeEquipementDTO.java | 46 + .../restapi/dto/VersionDTO.java | 27 + .../restapi/dto/id/CritereIdDTO.java | 26 + .../restapi/dto/id/EtapeIdDTO.java | 25 + .../restapi/dto/id/HypotheseIdDTO.java | 21 + .../restapi/dto/id/ImpactEquipementIdDTO.java | 26 + .../restapi/dto/id/ImpactReseauIdDTO.java | 29 + .../restapi/dto/id/MixElectriqueIdDTO.java | 25 + .../CorrespondanceRefEquipementFacade.java | 42 + .../restapi/facade/CritereFacade.java | 42 + .../restapi/facade/EtapeFacade.java | 41 + .../restapi/facade/HypotheseFacade.java | 50 + .../facade/ImpactEquipementFacade.java | 37 + .../facade/ImpactMessagerieFacade.java | 46 + .../restapi/facade/ImpactReseauFacade.java | 40 + .../restapi/facade/MixElectriqueFacade.java | 48 + .../restapi/facade/TypeEquipementFacade.java | 45 + .../src/main/resources/application.yaml | 67 + .../src/main/resources/logback.xml | 23 + .../src/main/resources/schema.sql | 91 + .../referentiel/CucumberIntegrationTest.java | 30 + .../ReferentielApplicationTests.java | 68 + .../input/ImportCSVReferentielPortTest.java | 47 + ...rtCorrespondanceRefEquipementPortTest.java | 86 + .../port/input/ImportCriterePortTest.java | 128 + .../port/input/ImportEtapePortTest.java | 86 + .../port/input/ImportHypothesePortTest.java | 84 + .../input/ImportImpactEquipementPortTest.java | 170 ++ .../input/ImportImpactMessageriePortTest.java | 114 + .../input/ImportImpactReseauPortTest.java | 148 ++ .../input/ImportMixElectriquePortTest.java | 141 + .../input/ImportTypeEquipementPortTest.java | 115 + .../referentiel/factory/TestDataFactory.java | 398 +++ .../export/CritereCsvExportServiceTest.java | 100 + .../export/EtapeCsvExportServiceTest.java | 100 + .../export/HypotheseCsvExportServiceTest.java | 100 + .../ImpactEquipementCsvExportServiceTest.java | 128 + .../ImpactMessagerieCsvExportServiceTest.java | 124 + ...pactMixElectriqueCsvExportServiceTest.java | 126 + .../ImpactReseauCsvExportServiceTest.java | 128 + .../ReferentielCsvExportServiceTest.java | 158 ++ .../TypeEquipementCsvExportServiceTest.java | 122 + .../openapi/ReferentielOpenApiConfigTest.java | 55 + .../security/SecurityConfigTest.java | 34 + .../csv/ImportCSVReferentielPortTest.java | 68 + ...espondanceRefEquipementJpaAdapterTest.java | 128 + .../jpa/CritereJpaAdapterTest.java | 170 ++ .../jpa/EtapeJpaAdapterTest.java | 147 ++ .../jpa/HypotheseJpaAdapterTest.java | 130 + .../jpa/ImpactEquipementJpaAdapterTest.java | 159 ++ .../jpa/ImpactMessagerieJpaAdapterTest.java | 149 ++ .../jpa/ImpactReseauJpaAdapterTest.java | 197 ++ .../jpa/MixElectriqueJpaAdapterTest.java | 169 ++ ...spondanceRefEquipementRestApiImplTest.java | 88 + .../ReferentielCritereRestApiImplTest.java | 87 + .../ReferentielEtapeRestApiImplTest.java | 87 + .../ReferentielHypotheseRestApiImplTest.java | 99 + ...entielImpactEquipementRestApiImplTest.java | 115 + ...entielImpactMessagerieRestApiImplTest.java | 87 + ...eferentielImpactReseauRestApiImplTest.java | 180 ++ ...ferentielMixElectriqueRestApiImplTest.java | 108 + ...erentielTypeEquipementRestApiImplTest.java | 97 + ...CorrespondanceRefEquipementFacadeTest.java | 112 + .../restapi/facade/CritereFacadeTest.java | 96 + .../restapi/facade/EtapeFacadeTest.java | 92 + .../restapi/facade/HypotheseFacadeTest.java | 109 + .../facade/ImpactEquipementFacadeTest.java | 113 + .../facade/ImpactMessagerieFacadeTest.java | 126 + .../facade/ImpactReseauFacadeTest.java | 154 ++ .../facade/MixElectriqueFacadeTest.java | 103 + .../steps/AbstractStepDefinitions.java | 65 + ...spondanceRefEquipementStepDefinitions.java | 71 + .../referentiel/steps/CritereStepdefs.java | 74 + .../referentiel/steps/EtapeStepdefs.java | 76 + .../referentiel/steps/HypotheseStepdefs.java | 65 + ...ImpactImpactEquipementStepDefinitions.java | 82 + .../ImpactMessagerieStepDefinitions.java | 67 + .../steps/ImpactReseauStepDefinitions.java | 258 ++ .../steps/TypeEquipementStepDefinitions.java | 95 + .../src/test/resources/application-test.yaml | 25 + .../src/test/resources/csv/ref_Critere.csv | 6 + .../src/test/resources/csv/ref_Hypothese.csv | 3 + .../resources/csv/ref_ImpactMessagerie.csv | 2 + .../test/resources/csv/ref_MixElectrique.csv | 21 + .../src/test/resources/csv/ref_etapeACV.csv | 5 + .../resources/csv/ref_impactEquipement.csv | 109 + .../test/resources/csv/ref_impactreseau.csv | 101 + .../ref_CorrespondanceRefEquipement.csv | 4 + .../csv/sprint10/ref_ImpactEquipement.csv | 7 + .../csv/sprint10/ref_TypeEquipement.csv | 18 + .../csv/unit/correspondanceRefEquipement.csv | 4 + ...respondanceRefEquipement_errorInMiddle.csv | 6 + ...orrespondanceRefEquipement_utilitaires.csv | 4 + .../src/test/resources/csv/unit/critere.csv | 6 + .../csv/unit/critere_errorInMiddle.csv | 7 + .../src/test/resources/csv/unit/csvHelper.csv | 3 + .../src/test/resources/csv/unit/etapeACV.csv | 5 + .../csv/unit/etapeACV_errorInMiddle.csv | 5 + .../src/test/resources/csv/unit/hypothese.csv | 3 + .../csv/unit/hypothese_errorInMiddle.csv | 5 + .../resources/csv/unit/impactEquipement.csv | 5 + .../unit/impactEquipement_errorInMiddle.csv | 8 + .../resources/csv/unit/impactMessagerie.csv | 6 + .../unit/impactMessagerie_errorInMiddle.csv | 4 + .../test/resources/csv/unit/impactreseau.csv | 7 + .../csv/unit/impactreseau_errorInMiddle.csv | 10 + .../test/resources/csv/unit/mixElectrique.csv | 5 + .../csv/unit/mixElectrique_errorInMiddle.csv | 8 + .../resources/csv/unit/typeEquipement.csv | 3 + .../csv/unit/typeEquipement_errorInMiddle.csv | 4 + .../test/resources/csv/unit/wrongCSVFile.csv | 2 + .../src/test/resources/logback.xml | 25 + .../org.mockito.plugins.MockMaker | 1 + .../referentiel/referentiel_criteres.feature | 17 + .../referentiel/referentiel_etape_acv.feature | 16 + ...referentiel_facteurs_impact_reseau.feature | 31 + .../referentiel/referentiel_hypothese.feature | 19 + .../referentiel_impact_equipement.feature | 25 + .../referentiel/referentiel_sprint10.feature | 42 + services/calculs/LICENSE | 201 ++ services/calculs/README.md | 27 + services/calculs/ci_settings.xml | 16 + .../calculs/dependency_check_suppressions.xml | 12 + services/calculs/pom.xml | 124 + .../DemandeCalculImpactApplication.java | 34 + ...DemandeCalculImpactEquipementPhysique.java | 90 + .../DemandeCalculImpactEquipementVirtuel.java | 24 + .../DemandeCalculImpactMessagerie.java | 27 + .../demande/DemandeCalculImpactReseau.java | 30 + .../domain/data/entree/Application.java | 24 + .../domain/data/entree/DataCenter.java | 21 + .../data/entree/EquipementPhysique.java | 39 + .../domain/data/entree/EquipementVirtuel.java | 27 + .../domain/data/entree/Messagerie.java | 21 + .../domain/data/erreur/TypeErreurCalcul.java | 14 + .../data/indicateurs/ImpactApplication.java | 38 + .../indicateurs/ImpactEquipementPhysique.java | 36 + .../indicateurs/ImpactEquipementVirtuel.java | 35 + .../data/indicateurs/ImpactMessagerie.java | 34 + .../domain/data/indicateurs/ImpactReseau.java | 35 + ...eferentielCorrespondanceRefEquipement.java | 17 + .../data/referentiel/ReferentielCritere.java | 13 + .../data/referentiel/ReferentielEtapeACV.java | 12 + .../referentiel/ReferentielHypothese.java | 14 + .../ReferentielImpactEquipement.java | 16 + .../ReferentielImpactMessagerie.java | 13 + .../referentiel/ReferentielImpactReseau.java | 19 + .../referentiel/ReferentielMixElectrique.java | 14 + .../ReferentielTypeEquipement.java | 24 + .../domain/data/trace/ConsoElecAnMoyenne.java | 15 + .../calculs/domain/data/trace/DureeDeVie.java | 15 + .../data/trace/DureeDeVieParDefaut.java | 15 + .../domain/data/trace/MixElectrique.java | 16 + .../trace/TraceCalculImpactApplication.java | 15 + .../TraceCalculImpactEquipementPhysique.java | 18 + .../TraceCalculImpactEquipementVirtuel.java | 16 + .../trace/TraceCalculImpactMessagerie.java | 20 + .../data/trace/TraceCalculImpactReseau.java | 20 + .../exception/CalculImpactException.java | 15 + .../CalculImpactRuntimeException.java | 25 + .../CalculImpactApplicationService.java | 10 + ...CalculImpactEquipementPhysiqueService.java | 9 + .../CalculImpactEquipementVirtuelService.java | 9 + .../CalculImpactMessagerieService.java | 10 + .../service/CalculImpactReseauService.java | 12 + .../DureeDeVieEquipementPhysiqueService.java | 13 + .../CalculImpactApplicationServiceImpl.java | 129 + ...ulImpactEquipementPhysiqueServiceImpl.java | 284 ++ ...culImpactEquipementVirtuelServiceImpl.java | 163 ++ .../CalculImpactMessagerieServiceImpl.java | 106 + .../impl/CalculImpactReseauServiceImpl.java | 139 + ...reeDeVieEquipementPhysiqueServiceImpl.java | 77 + .../TraceCalculImpactApplicationUtils.java | 29 + ...ceCalculImpactEquipementPhysiqueUtils.java | 49 + .../TraceCalculImpactMessagerieUtils.java | 50 + .../traceur/TraceCalculImpactReseauUtils.java | 44 + .../TraceCalculImpactVirtuelUtils.java | 82 + .../calculs/domain/traceur/TraceUtils.java | 23 + .../calculs/CucumberIntegrationTest.java | 51 + ...alculImpactApplicationServiceImplTest.java | 346 +++ ...ulImpactEquipementPhysiqueServiceTest.java | 2293 +++++++++++++++++ ...culImpactEquipementVirtuelServiceTest.java | 673 +++++ .../CalculImpactMessagerieServiceTest.java | 154 ++ .../CalculImpactReseauServiceTest.java | 220 ++ ...reeDeVieEquipementPhysiqueServiceTest.java | 228 ++ ...mpactEquipementVirtuelStepDefinitions.java | 51 + .../steps/DataTableTypeDefinitions.java | 152 ++ .../steps/DureeDeVieStepDefinitions.java | 86 + .../src/test/resources/application-test.yaml | 70 + .../src/test/resources/logback-test.xml | 29 + ...ulImpactUnitaire-EquipementVirtuel.feature | 93 + .../DureeDeVie-EquipementPhysique.feature | 47 + services/common/.gitignore | 32 + services/common/.gitlab-ci-library.yml | 47 + services/common/.gitlab-ci-maven.yml | 86 + services/common/Dockerfile | 35 + services/common/README.md | 3 + services/common/ci_settings.xml | 16 + .../common/dependency_check_suppressions.xml | 20 + services/common/entrypoint.sh | 13 + services/common/pom.xml | 122 + .../exception/NumEcoEvalRuntimeException.java | 18 + .../common/utils/PreparedStatementUtils.java | 50 + .../common/utils/ResultSetUtils.java | 55 + .../src/main/resources/static/README.md | 22 + .../api-event-calculs-async-openapi.yaml | 789 ++++++ .../static/api-event-calculs-openapi.yaml | 782 ++++++ .../api-event-calculs-sync-openapi.yaml | 92 + .../static/api-referentiels-openapi.yaml | 1484 +++++++++++ .../static/asyncapi_equipement_physique.yaml | 335 +++ .../main/resources/static/asyncapi_merge.yaml | 26 + .../resources/static/asyncapi_messagerie.yaml | 68 + .../utils/PreparedStatementUtilsTest.java | 93 + .../common/utils/ResultSetUtilsTest.java | 110 + services/core/.gitignore | 32 + services/core/README.md | 3 + services/core/ci_settings.xml | 16 + .../core/dependency_check_suppressions.xml | 3 + services/core/pom.xml | 377 +++ 607 files changed, 40458 insertions(+), 59 deletions(-) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .gitlab-ci.yml create mode 100644 CHANGELOG.md create mode 100644 LICENSE create mode 100644 assets/docker-compose/.env create mode 100644 assets/docker-compose/docker-compose.yml create mode 100644 assets/images/08BDB65B-73CA-4FF0-8C1C-1040DA20E7B3.png create mode 100644 assets/images/2A4575F4-EE3B-478D-A397-BD8B409390E0.png create mode 100644 assets/images/3E1ED783-839A-4094-9A8B-8B02F6E5283E.png create mode 100644 assets/images/56BC42EB-2AAF-4A9B-A98C-9D29DB8E2520.png create mode 100644 assets/images/81945681-5CEB-4178-8AB8-4B9B6801FBC9.png create mode 100644 assets/images/8B74D910-915C-46FD-9C46-34A44FD3CBBA.png create mode 100644 assets/images/A8AF1630-6525-428D-85A6-E3C413982D97.png create mode 100644 assets/images/BB5C2199-6CA3-4E0E-8899-A95FF625CE38.png create mode 100644 assets/images/DBBC8E31-4CED-497B-A53F-43D78BCB1523.png create mode 100644 assets/images/E64BA1C4-E956-4B54-9CDF-BA4F4E321807.png create mode 100644 assets/images/FF00E9AF-13DC-409C-8512-975183F81063.png create mode 100644 docs/DonneeEntree.plantuml create mode 100644 docs/Indicateurs.plantuml create mode 100644 docs/MoteurDeCalculG4IT_V1.1.adoc create mode 100644 docs/README.md create mode 100644 docs/References.plantuml create mode 100644 docs/Traces.plantuml create mode 100644 docs/archi_fonctionnelle_V4.png create mode 100644 docs/architecture_fonctionnelle_simplifiee_V1.png create mode 100644 "docs/cin\303\251matique.png" create mode 100644 docs/cycle_de_vie.jpg create mode 100644 e2e/.env create mode 100644 e2e/.gitignore create mode 100644 e2e/1_load_ref.sh create mode 100644 e2e/2_generate_dataset.sh create mode 100644 e2e/3_load_input.sh create mode 100644 e2e/4_check.sh create mode 100644 e2e/5_assert.sh create mode 100644 e2e/README.md create mode 100644 e2e/e2e.iml create mode 100644 e2e/input_template/Application.csv create mode 100644 e2e/input_template/DataCenter.csv create mode 100644 e2e/input_template/EquipementPhysique_hors_serveur.csv create mode 100644 e2e/input_template/EquipementPhysique_serveur.csv create mode 100644 e2e/input_template/EquipementVirtuel.csv create mode 100644 e2e/performance-test.sh create mode 100644 e2e/utils.sh create mode 100644 services/.workspace/.idea/.gitignore create mode 100644 services/.workspace/.idea/codeStyles/Project.xml create mode 100644 services/.workspace/.idea/codeStyles/codeStyleConfig.xml create mode 100644 services/.workspace/.idea/compiler.xml create mode 100644 services/.workspace/.idea/encodings.xml create mode 100644 services/.workspace/.idea/inspectionProfiles/Project_Default.xml create mode 100644 services/.workspace/.idea/jarRepositories.xml create mode 100644 services/.workspace/.idea/misc.xml create mode 100644 services/.workspace/.idea/modules.xml create mode 100644 services/.workspace/.idea/runConfigurations/ApiEventCalculsApplication.xml create mode 100644 services/.workspace/.idea/runConfigurations/ApiEventDonneesEntreesApplication.xml create mode 100644 services/.workspace/.idea/runConfigurations/ExpositionDonneesEntreesApplication.xml create mode 100644 services/.workspace/.idea/runConfigurations/Init.xml create mode 100644 services/.workspace/.idea/runConfigurations/ReferentielApplication.xml create mode 100644 services/.workspace/.idea/runConfigurations/Run.xml create mode 100644 services/.workspace/.idea/runConfigurations/api_event_calculs__gen_sources_.xml create mode 100644 services/.workspace/.idea/runConfigurations/api_expositiondonneesentrees__gen_sources_.xml create mode 100644 services/.workspace/.idea/runConfigurations/install_calculs.xml create mode 100644 services/.workspace/.idea/runConfigurations/install_common.xml create mode 100644 services/.workspace/.idea/runConfigurations/install_core.xml create mode 100644 services/.workspace/.idea/saveactions_settings.xml create mode 100644 services/.workspace/.idea/uiDesigner.xml create mode 100644 services/.workspace/.idea/vcs.xml create mode 100644 services/.workspace/settings.xml create mode 100644 services/api-event-calculs/.gitignore create mode 100644 services/api-event-calculs/LICENSE.txt create mode 100644 services/api-event-calculs/README.md create mode 100644 services/api-event-calculs/dependency_check_suppressions.xml create mode 100644 services/api-event-calculs/pom.xml create mode 100644 services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/ApiEventCalculsApplication.java create mode 100644 services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/domain/exception/DatabaseException.java create mode 100644 services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/domain/exception/ExternalApiException.java create mode 100644 services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/domain/model/CalculEquipementPhysique.java create mode 100644 services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/domain/model/CalculMessagerie.java create mode 100644 services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/domain/model/CalculSizes.java create mode 100644 services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/client/ReferentielClient.java create mode 100644 services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/config/CacheConfig.java create mode 100644 services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/config/CommonIntegrationConfig.java create mode 100644 services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/config/DomainConfiguration.java create mode 100644 services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/config/KafkaConfiguration.java create mode 100644 services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/config/OpenApiConfiguration.java create mode 100644 services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/config/SecurityConfiguration.java create mode 100644 services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/controller/ExceptionHandler.java create mode 100644 services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/controller/export/IndicateurController.java create mode 100644 services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/controller/rest/calculs/CalculsApplicationController.java create mode 100644 services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/controller/rest/calculs/CalculsEquipementPhysiqueController.java create mode 100644 services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/controller/rest/calculs/CalculsEquipementVirtuelController.java create mode 100644 services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/controller/rest/calculs/CalculsMessagerieController.java create mode 100644 services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/controller/rest/calculs/CalculsReseauController.java create mode 100644 services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/controller/sync/calculs/SyncCalculsController.java create mode 100644 services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/kafkalistener/ListenEquipementPhysique.java create mode 100644 services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/kafkalistener/ListenMessagerie.java create mode 100644 services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/mapper/DomainMapper.java create mode 100644 services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/mapper/EntreesMapper.java create mode 100644 services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/mapper/ReferentielMapper.java create mode 100644 services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/repository/ApplicationRepository.java create mode 100644 services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/repository/EquipementPhysiqueRepository.java create mode 100644 services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/repository/EquipementVirtuelRepository.java create mode 100644 services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/repository/IndicateurRepository.java create mode 100644 services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/repository/MessagerieRepository.java create mode 100644 services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/service/calcul/CalculApplicationService.java create mode 100644 services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/service/calcul/CalculEquipementPhysiqueService.java create mode 100644 services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/service/calcul/CalculEquipementVirtuelService.java create mode 100644 services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/service/calcul/CalculMessagerieService.java create mode 100644 services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/service/calcul/CalculReseauService.java create mode 100644 services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/service/calcul/IntegrationCalculEquipementsService.java create mode 100644 services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/service/calcul/IntegrationCalculMessagerieService.java create mode 100644 services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/service/calcul/MainEquipementPhysiqueService.java create mode 100644 services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/service/calcul/MainMessagerieService.java create mode 100644 services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/service/enrichissement/EnrichissementEquipementPhysiqueService.java create mode 100644 services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/service/enrichissement/EnrichissementMessagerieService.java create mode 100644 services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/service/rest/calculs/CheckIndicateurService.java create mode 100644 services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/service/sync/calculs/SyncCalculService.java create mode 100644 services/api-event-calculs/src/main/resources/application.yaml create mode 100644 services/api-event-calculs/src/main/resources/logback.xml create mode 100644 services/api-event-calculs/src/main/resources/schema.sql create mode 100644 services/api-event-calculs/src/test/java/org/mte/numecoeval/calculs/ApiEventCalculsApplicationTests.java create mode 100644 services/api-event-calculs/src/test/java/org/mte/numecoeval/calculs/infrastructure/service/calcul/CalculEquipementVirtuelServiceTest.java create mode 100644 services/api-event-calculs/src/test/java/org/mte/numecoeval/calculs/infrastructure/service/calcul/CalculReseauServiceTest.java create mode 100644 services/api-event-calculs/src/test/java/org/mte/numecoeval/calculs/infrastructure/service/calcul/EnrichissementEquipementPhysiqueServiceTest.java create mode 100644 services/api-event-calculs/src/test/java/org/mte/numecoeval/calculs/infrastructure/service/calcul/EnrichissementMessagerieServiceTest.java create mode 100644 services/api-event-calculs/src/test/java/org/mte/numecoeval/calculs/infrastructure/service/rest/calculs/CheckIndicateurServiceTest.java create mode 100644 services/api-event-calculs/src/test/java/org/mte/numecoeval/calculs/infrastructure/service/sync/calculs/SyncCalculServiceTest.java create mode 100644 services/api-event-calculs/src/test/resources/application-test.yaml create mode 100644 services/api-event-calculs/src/test/resources/input/equipementPhysique.json create mode 100644 services/api-event-calculs/src/test/resources/input/messagerie.json create mode 100644 services/api-event-calculs/src/test/resources/logback-test.xml create mode 100644 services/api-event-donneesentrees/.gitignore create mode 100644 services/api-event-donneesentrees/README.md create mode 100644 services/api-event-donneesentrees/dependency_check_suppressions.xml create mode 100644 services/api-event-donneesentrees/pom.xml create mode 100644 services/api-event-donneesentrees/src/main/java/org/mte/numecoeval/donneesentrees/ApiEventDonneesEntreesApplication.java create mode 100644 services/api-event-donneesentrees/src/main/java/org/mte/numecoeval/donneesentrees/infrastructure/config/EquipementPhysiqueIntegrationConfig.java create mode 100644 services/api-event-donneesentrees/src/main/java/org/mte/numecoeval/donneesentrees/infrastructure/config/MessagerieIntegrationConfig.java create mode 100644 services/api-event-donneesentrees/src/main/java/org/mte/numecoeval/donneesentrees/infrastructure/utils/Constants.java create mode 100644 services/api-event-donneesentrees/src/main/resources/application.yaml create mode 100644 services/api-event-donneesentrees/src/main/resources/logback.xml create mode 100644 services/api-event-donneesentrees/src/test/java/org/mte/numecoeval/donneesentrees/IntegrationEquipementPhysiqueTest.java create mode 100644 services/api-event-donneesentrees/src/test/java/org/mte/numecoeval/donneesentrees/IntegrationMessagerieTest.java create mode 100644 services/api-event-donneesentrees/src/test/java/org/mte/numecoeval/donneesentrees/test/jdbc/ScriptUtils.java create mode 100644 services/api-event-donneesentrees/src/test/java/org/mte/numecoeval/donneesentrees/test/kafka/KafkaConsumer.java create mode 100644 services/api-event-donneesentrees/src/test/java/org/mte/numecoeval/donneesentrees/test/kafka/KafkaConsumerConfig.java create mode 100644 services/api-event-donneesentrees/src/test/java/org/mte/numecoeval/donneesentrees/test/kafka/KafkaConsumerEquipementPhysique.java create mode 100644 services/api-event-donneesentrees/src/test/java/org/mte/numecoeval/donneesentrees/test/kafka/KafkaConsumerMessagerie.java create mode 100644 services/api-event-donneesentrees/src/test/resources/application-test.yaml create mode 100644 services/api-event-donneesentrees/src/test/resources/logback-test.xml create mode 100644 services/api-event-donneesentrees/src/test/resources/sql/equipment_physique.sql create mode 100644 services/api-event-donneesentrees/src/test/resources/sql/messagerie.sql create mode 100644 services/api-event-donneesentrees/src/test/resources/sql/schema.sql create mode 100644 services/api-expositiondonneesentrees/.gitignore create mode 100644 services/api-expositiondonneesentrees/LICENSE.txt create mode 100644 services/api-expositiondonneesentrees/README.md create mode 100644 services/api-expositiondonneesentrees/dependency_check_suppressions.xml create mode 100644 services/api-expositiondonneesentrees/pom.xml create mode 100644 services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/ExpositionDonneesEntreesApplication.java create mode 100644 services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/exception/NotFoundException.java create mode 100644 services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/exception/ReferentielRuntimeException.java create mode 100644 services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/exception/RestException.java create mode 100644 services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/exception/ValidationException.java create mode 100644 services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/model/AbstractEntree.java create mode 100644 services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/model/Application.java create mode 100644 services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/model/DataCenter.java create mode 100644 services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/model/DemandeCalcul.java create mode 100644 services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/model/DonneesEntree.java create mode 100644 services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/model/Entite.java create mode 100644 services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/model/EquipementPhysique.java create mode 100644 services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/model/EquipementVirtuel.java create mode 100644 services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/model/Messagerie.java create mode 100644 services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/model/RapportDemandeCalcul.java create mode 100644 services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/model/RapportImport.java create mode 100644 services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/model/ResultatImport.java create mode 100644 services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/model/Volume.java create mode 100644 services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/ports/input/ImportDonneesEntreePort.java create mode 100644 services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/ports/input/SoumissionCalculPort.java create mode 100644 services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/ports/input/SoumissionCalculSyncPort.java create mode 100644 services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/ports/input/StatutPourCalculPort.java create mode 100644 services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/ports/input/impl/ImportDonneesEntreePortImpl.java create mode 100644 services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/ports/input/impl/SoumissionCalculSyncPortImpl.java create mode 100644 services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/ports/input/impl/StatutPourCalculPortImpl.java create mode 100644 services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/ports/output/CalculsServicePort.java create mode 100644 services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/ports/output/EntreePersistencePort.java create mode 100644 services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/ports/output/ReferentielServicePort.java create mode 100644 services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/ports/output/SaveDonneesEntreePort.java create mode 100644 services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/adapters/CalculsRestClient.java create mode 100644 services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/adapters/ReferentielRestClient.java create mode 100644 services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/cache/SchedulerEvictCache.java create mode 100644 services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/config/ApplicationPortConfig.java create mode 100644 services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/config/CommonIntegrationConfig.java create mode 100644 services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/config/GeneralConfig.java create mode 100644 services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/config/MessageProperties.java create mode 100644 services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/config/OpenApiConfig.java create mode 100644 services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/config/security/SecurityConfig.java create mode 100644 services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/controller/CalculController.java create mode 100644 services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/controller/ImportCSVController.java create mode 100644 services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/controller/RestExceptionHandler.java create mode 100644 services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/handler/RestClientResponseErrorHandler.java create mode 100644 services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/helper/CSVHelper.java create mode 100644 services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/helper/Constants.java create mode 100644 services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jdbc/SoumissionCalculPortJdbcImpl.java create mode 100644 services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jdbc/VolumeJdbc.java create mode 100644 services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/adapter/ApplicationJpaAdapter.java create mode 100644 services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/adapter/DataCenterJpaAdapter.java create mode 100644 services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/adapter/DonneesEntreesJpaAdapter.java create mode 100644 services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/adapter/EntiteJpaAdapter.java create mode 100644 services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/adapter/EquipementPhysiqueJpaAdapter.java create mode 100644 services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/adapter/EquipementVirtuelJpaAdapter.java create mode 100644 services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/adapter/MessagerieJpaAdapter.java create mode 100644 services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/adapter/SaveDonneesEntreeAdapter.java create mode 100644 services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/entity/AbstractEntreeEntity.java create mode 100644 services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/entity/ApplicationEntity.java create mode 100644 services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/entity/DataCenterEntity.java create mode 100644 services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/entity/DonneesEntreesEntity.java create mode 100644 services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/entity/EntiteEntity.java create mode 100644 services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/entity/EquipementPhysiqueEntity.java create mode 100644 services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/entity/EquipementVirtuelEntity.java create mode 100644 services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/entity/MessagerieEntity.java create mode 100644 services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/repository/ApplicationRepository.java create mode 100644 services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/repository/DataCenterRepository.java create mode 100644 services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/repository/DonneesEntreesRepository.java create mode 100644 services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/repository/EntiteRepository.java create mode 100644 services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/repository/EquipementPhysiqueRepository.java create mode 100644 services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/repository/EquipementVirtuelRepository.java create mode 100644 services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/repository/MessagerieRepository.java create mode 100644 services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/mapper/CalculRestMapper.java create mode 100644 services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/mapper/DonneesEntreeRestMapper.java create mode 100644 services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/mapper/EntreeEntityMapper.java create mode 100644 services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/mapper/VolumeMapper.java create mode 100644 services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/service/ErrorManagementPostSaveService.java create mode 100644 services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/service/ErrorManagementService.java create mode 100644 services/api-expositiondonneesentrees/src/main/resources/application.yaml create mode 100644 services/api-expositiondonneesentrees/src/main/resources/logback.xml create mode 100644 services/api-expositiondonneesentrees/src/main/resources/schema.sql create mode 100644 services/api-expositiondonneesentrees/src/main/resources/static/openapi.yaml create mode 100644 services/api-expositiondonneesentrees/src/test/java/org/mte/numecoeval/expositiondonneesentrees/domain/port/input/ImportDonneesEntreePortImplTest.java create mode 100644 services/api-expositiondonneesentrees/src/test/java/org/mte/numecoeval/expositiondonneesentrees/domain/port/input/SoumissionCalculPortImplTest.java create mode 100644 services/api-expositiondonneesentrees/src/test/java/org/mte/numecoeval/expositiondonneesentrees/domain/port/input/StatutPourCalculPortImplTest.java create mode 100644 services/api-expositiondonneesentrees/src/test/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/controller/ImportCSVControllerTest.java create mode 100644 services/api-expositiondonneesentrees/src/test/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/controller/RestExceptionHandlerTest.java create mode 100644 services/api-expositiondonneesentrees/src/test/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/adapter/ApplicationJpaAdapterTest.java create mode 100644 services/api-expositiondonneesentrees/src/test/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/adapter/DataCenterJpaAdapterTest.java create mode 100644 services/api-expositiondonneesentrees/src/test/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/adapter/DonneesEntreesJpaAdapterTest.java create mode 100644 services/api-expositiondonneesentrees/src/test/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/adapter/EntiteJpaAdapterTest.java create mode 100644 services/api-expositiondonneesentrees/src/test/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/adapter/EquipementPhysiqueJpaAdapterTest.java create mode 100644 services/api-expositiondonneesentrees/src/test/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/adapter/EquipementVirtuelJpaAdapterTest.java create mode 100644 services/api-expositiondonneesentrees/src/test/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/adapter/MessagerieJpaAdapterTest.java create mode 100644 services/api-expositiondonneesentrees/src/test/java/org/mte/numecoeval/expositiondonneesentrees/test/DataTableUtils.java create mode 100644 services/api-expositiondonneesentrees/src/test/resources/application-test.yaml create mode 100644 services/api-expositiondonneesentrees/src/test/resources/equipementPhysique.csv create mode 100644 services/api-expositiondonneesentrees/src/test/resources/logback-test.xml create mode 100644 services/api-referentiel/.gitignore create mode 100644 services/api-referentiel/README.md create mode 100644 services/api-referentiel/dependency_check_suppressions.xml create mode 100644 services/api-referentiel/pom.xml create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/ReferentielApplication.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/data/ResultatImport.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/exception/NotFoundException.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/exception/ReferentielException.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/exception/ReferentielRuntimeException.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/model/AbstractReferentiel.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/model/CorrespondanceRefEquipement.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/model/Critere.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/model/Etape.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/model/Hypothese.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/model/ImpactEquipement.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/model/ImpactMessagerie.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/model/ImpactReseau.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/model/MixElectrique.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/model/TypeEquipement.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/model/id/CritereId.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/model/id/EtapeId.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/model/id/HypotheseId.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/model/id/ImpactEquipementId.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/model/id/ImpactReseauId.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/model/id/MixElectriqueId.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/ports/input/ImportCSVReferentielPort.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/ports/input/impl/ImportCorrespondanceRefEquipementPortImpl.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/ports/input/impl/ImportCriterePortImpl.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/ports/input/impl/ImportEtapePortImpl.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/ports/input/impl/ImportHypothesePortImpl.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/ports/input/impl/ImportImpactEquipementPortImpl.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/ports/input/impl/ImportImpactMessageriePortImpl.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/ports/input/impl/ImportImpactReseauPortImpl.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/ports/input/impl/ImportMixElectriquePortImpl.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/ports/input/impl/ImportTypeEquipementPortImpl.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/ports/output/ReferentielCsvExportService.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/ports/output/ReferentielPersistencePort.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/adapter/export/CorrespondanceRefEquipemenetCsvExportService.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/adapter/export/CritereCsvExportService.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/adapter/export/EtapeCsvExportService.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/adapter/export/HypotheseCsvExportService.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/adapter/export/ImpactEquipementCsvExportService.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/adapter/export/ImpactMessagerieCsvExportService.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/adapter/export/ImpactReseauCsvExportService.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/adapter/export/MixElectriqueCsvExportService.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/adapter/export/TypeEquipementCsvExportService.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/configuration/ReferentielConfig.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/configuration/openapi/ReferentielOpenApiConfig.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/configuration/security/SecurityConfig.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/adapter/CorrespondanceRefEquipementJpaAdapter.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/adapter/CritereJpaAdapter.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/adapter/EtapeJpaAdapter.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/adapter/HypotheseJpaAdapter.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/adapter/ImpactEquipementJpaAdapter.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/adapter/ImpactMessagerieJpaAdapter.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/adapter/ImpactReseauJpaAdapter.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/adapter/MixElectriqueJpaAdapter.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/adapter/TypeEquipementJpaAdapter.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/entity/AbstractReferentielEntity.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/entity/CorrespondanceRefEquipementEntity.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/entity/CritereEntity.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/entity/EtapeEntity.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/entity/HypotheseEntity.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/entity/ImpactEquipementEntity.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/entity/ImpactMessagerieEntity.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/entity/ImpactReseauEntity.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/entity/MixElectriqueEntity.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/entity/TypeEquipementEntity.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/entity/id/AbstractReferentieIdEntity.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/entity/id/CritereIdEntity.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/entity/id/EtapeIdEntity.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/entity/id/HypotheseIdEntity.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/entity/id/ImpactEquipementIdEntity.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/entity/id/ImpactReseauIdEntity.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/entity/id/MixElectriqueIdEntity.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/repository/CorrespondanceRefEquipementRepository.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/repository/CritereRepository.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/repository/EtapeRepository.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/repository/HypotheseRepository.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/repository/ImpactEquipementRepository.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/repository/ImpactMessagerieRepository.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/repository/ImpactReseauRepository.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/repository/MixElectriqueRepository.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/repository/TypeEquipementRepository.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/mapper/CorrespondanceRefEquipementMapper.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/mapper/CritereMapper.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/mapper/EtapeMapper.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/mapper/HypotheseMapper.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/mapper/ImpactEquipementMapper.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/mapper/ImpactMessagerieMapper.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/mapper/ImpactReseauMapper.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/mapper/MixElectriqueMapper.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/mapper/TypeEquipementMapper.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/BaseExportReferentiel.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/correspondance/ReferentielAdministrationCorrespondanceRefEquipementRestApi.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/correspondance/ReferentielCorrespondanceRefEquipementRestApi.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/correspondance/ReferentielCorrespondanceRefEquipementRestApiImpl.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/critere/ReferentielAdministrationCritereRestApi.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/critere/ReferentielCritereRestApi.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/critere/ReferentielCritereRestApiImpl.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/etape/ReferentielAdministrationEtapeRestApi.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/etape/ReferentielEtapeRestApi.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/etape/ReferentielEtapeRestApiImpl.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/exception/ReferentelExceptionHandler.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/hypothese/ReferentielAdministrationHypotheseRestApi.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/hypothese/ReferentielHypotheseRestApi.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/hypothese/ReferentielHypotheseRestApiImpl.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/impactequipement/ReferentielAdministrationImpactEquipementRestApi.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/impactequipement/ReferentielImpactEquipementRestApi.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/impactequipement/ReferentielImpactEquipementRestApiImpl.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/impactmessagerie/ReferentielImpactMessagerieRestApi.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/impactmessagerie/ReferentielImpactMessagerieRestApiImpl.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/impactmessagerie/ReferentielInterneImpactMessagerieRestApi.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/impactreseau/ReferentielAdministrationImpactReseauRestApi.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/impactreseau/ReferentielImpactReseauRestApi.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/impactreseau/ReferentielImpactReseauRestApiImpl.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/mixelectrique/ReferentielAdministrationMixElectriqueRestApi.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/mixelectrique/ReferentielMixElecRestApiImpl.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/mixelectrique/ReferentielMixElectriqueRestApi.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/typeequipement/ReferentielAdministrationTypeEquipementRestApi.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/typeequipement/ReferentielTypeEquipementRestApi.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/typeequipement/ReferentielTypeEquipementRestApiImpl.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/version/VersionRestApi.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/version/VersionRestApiImpl.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/dto/CorrespondanceRefEquipementDTO.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/dto/CritereDTO.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/dto/ErrorResponseDTO.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/dto/EtapeDTO.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/dto/HypotheseDTO.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/dto/ImpactEquipementDTO.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/dto/ImpactMessagerieDTO.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/dto/ImpactReseauDTO.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/dto/MixElectriqueDTO.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/dto/RapportImportDTO.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/dto/TypeEquipementDTO.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/dto/VersionDTO.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/dto/id/CritereIdDTO.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/dto/id/EtapeIdDTO.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/dto/id/HypotheseIdDTO.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/dto/id/ImpactEquipementIdDTO.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/dto/id/ImpactReseauIdDTO.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/dto/id/MixElectriqueIdDTO.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/facade/CorrespondanceRefEquipementFacade.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/facade/CritereFacade.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/facade/EtapeFacade.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/facade/HypotheseFacade.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/facade/ImpactEquipementFacade.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/facade/ImpactMessagerieFacade.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/facade/ImpactReseauFacade.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/facade/MixElectriqueFacade.java create mode 100644 services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/facade/TypeEquipementFacade.java create mode 100644 services/api-referentiel/src/main/resources/application.yaml create mode 100644 services/api-referentiel/src/main/resources/logback.xml create mode 100644 services/api-referentiel/src/main/resources/schema.sql create mode 100644 services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/CucumberIntegrationTest.java create mode 100644 services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/ReferentielApplicationTests.java create mode 100644 services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/domain/port/input/ImportCSVReferentielPortTest.java create mode 100644 services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/domain/port/input/ImportCorrespondanceRefEquipementPortTest.java create mode 100644 services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/domain/port/input/ImportCriterePortTest.java create mode 100644 services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/domain/port/input/ImportEtapePortTest.java create mode 100644 services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/domain/port/input/ImportHypothesePortTest.java create mode 100644 services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/domain/port/input/ImportImpactEquipementPortTest.java create mode 100644 services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/domain/port/input/ImportImpactMessageriePortTest.java create mode 100644 services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/domain/port/input/ImportImpactReseauPortTest.java create mode 100644 services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/domain/port/input/ImportMixElectriquePortTest.java create mode 100644 services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/domain/port/input/ImportTypeEquipementPortTest.java create mode 100644 services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/factory/TestDataFactory.java create mode 100644 services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/adapter/export/CritereCsvExportServiceTest.java create mode 100644 services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/adapter/export/EtapeCsvExportServiceTest.java create mode 100644 services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/adapter/export/HypotheseCsvExportServiceTest.java create mode 100644 services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/adapter/export/ImpactEquipementCsvExportServiceTest.java create mode 100644 services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/adapter/export/ImpactMessagerieCsvExportServiceTest.java create mode 100644 services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/adapter/export/ImpactMixElectriqueCsvExportServiceTest.java create mode 100644 services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/adapter/export/ImpactReseauCsvExportServiceTest.java create mode 100644 services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/adapter/export/ReferentielCsvExportServiceTest.java create mode 100644 services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/adapter/export/TypeEquipementCsvExportServiceTest.java create mode 100644 services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/configuration/openapi/ReferentielOpenApiConfigTest.java create mode 100644 services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/configuration/security/SecurityConfigTest.java create mode 100644 services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/csv/ImportCSVReferentielPortTest.java create mode 100644 services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/jpa/CorrespondanceRefEquipementJpaAdapterTest.java create mode 100644 services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/jpa/CritereJpaAdapterTest.java create mode 100644 services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/jpa/EtapeJpaAdapterTest.java create mode 100644 services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/jpa/HypotheseJpaAdapterTest.java create mode 100644 services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/jpa/ImpactEquipementJpaAdapterTest.java create mode 100644 services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/jpa/ImpactMessagerieJpaAdapterTest.java create mode 100644 services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/jpa/ImpactReseauJpaAdapterTest.java create mode 100644 services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/jpa/MixElectriqueJpaAdapterTest.java create mode 100644 services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/correspondance/ReferentielCorrespondanceRefEquipementRestApiImplTest.java create mode 100644 services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/critere/ReferentielCritereRestApiImplTest.java create mode 100644 services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/etape/ReferentielEtapeRestApiImplTest.java create mode 100644 services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/hypothese/ReferentielHypotheseRestApiImplTest.java create mode 100644 services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/impactequipement/ReferentielImpactEquipementRestApiImplTest.java create mode 100644 services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/impactmessagerie/ReferentielImpactMessagerieRestApiImplTest.java create mode 100644 services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/impactreseau/ReferentielImpactReseauRestApiImplTest.java create mode 100644 services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/mixelectrique/ReferentielMixElectriqueRestApiImplTest.java create mode 100644 services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/typeequipement/ReferentielTypeEquipementRestApiImplTest.java create mode 100644 services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/restapi/facade/CorrespondanceRefEquipementFacadeTest.java create mode 100644 services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/restapi/facade/CritereFacadeTest.java create mode 100644 services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/restapi/facade/EtapeFacadeTest.java create mode 100644 services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/restapi/facade/HypotheseFacadeTest.java create mode 100644 services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/restapi/facade/ImpactEquipementFacadeTest.java create mode 100644 services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/restapi/facade/ImpactMessagerieFacadeTest.java create mode 100644 services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/restapi/facade/ImpactReseauFacadeTest.java create mode 100644 services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/restapi/facade/MixElectriqueFacadeTest.java create mode 100644 services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/steps/AbstractStepDefinitions.java create mode 100644 services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/steps/CorrespondanceRefEquipementStepDefinitions.java create mode 100644 services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/steps/CritereStepdefs.java create mode 100644 services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/steps/EtapeStepdefs.java create mode 100644 services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/steps/HypotheseStepdefs.java create mode 100644 services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/steps/ImpactImpactEquipementStepDefinitions.java create mode 100644 services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/steps/ImpactMessagerieStepDefinitions.java create mode 100644 services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/steps/ImpactReseauStepDefinitions.java create mode 100644 services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/steps/TypeEquipementStepDefinitions.java create mode 100644 services/api-referentiel/src/test/resources/application-test.yaml create mode 100644 services/api-referentiel/src/test/resources/csv/ref_Critere.csv create mode 100644 services/api-referentiel/src/test/resources/csv/ref_Hypothese.csv create mode 100644 services/api-referentiel/src/test/resources/csv/ref_ImpactMessagerie.csv create mode 100644 services/api-referentiel/src/test/resources/csv/ref_MixElectrique.csv create mode 100644 services/api-referentiel/src/test/resources/csv/ref_etapeACV.csv create mode 100644 services/api-referentiel/src/test/resources/csv/ref_impactEquipement.csv create mode 100644 services/api-referentiel/src/test/resources/csv/ref_impactreseau.csv create mode 100644 services/api-referentiel/src/test/resources/csv/sprint10/ref_CorrespondanceRefEquipement.csv create mode 100644 services/api-referentiel/src/test/resources/csv/sprint10/ref_ImpactEquipement.csv create mode 100644 services/api-referentiel/src/test/resources/csv/sprint10/ref_TypeEquipement.csv create mode 100644 services/api-referentiel/src/test/resources/csv/unit/correspondanceRefEquipement.csv create mode 100644 services/api-referentiel/src/test/resources/csv/unit/correspondanceRefEquipement_errorInMiddle.csv create mode 100644 services/api-referentiel/src/test/resources/csv/unit/correspondanceRefEquipement_utilitaires.csv create mode 100644 services/api-referentiel/src/test/resources/csv/unit/critere.csv create mode 100644 services/api-referentiel/src/test/resources/csv/unit/critere_errorInMiddle.csv create mode 100644 services/api-referentiel/src/test/resources/csv/unit/csvHelper.csv create mode 100644 services/api-referentiel/src/test/resources/csv/unit/etapeACV.csv create mode 100644 services/api-referentiel/src/test/resources/csv/unit/etapeACV_errorInMiddle.csv create mode 100644 services/api-referentiel/src/test/resources/csv/unit/hypothese.csv create mode 100644 services/api-referentiel/src/test/resources/csv/unit/hypothese_errorInMiddle.csv create mode 100644 services/api-referentiel/src/test/resources/csv/unit/impactEquipement.csv create mode 100644 services/api-referentiel/src/test/resources/csv/unit/impactEquipement_errorInMiddle.csv create mode 100644 services/api-referentiel/src/test/resources/csv/unit/impactMessagerie.csv create mode 100644 services/api-referentiel/src/test/resources/csv/unit/impactMessagerie_errorInMiddle.csv create mode 100644 services/api-referentiel/src/test/resources/csv/unit/impactreseau.csv create mode 100644 services/api-referentiel/src/test/resources/csv/unit/impactreseau_errorInMiddle.csv create mode 100644 services/api-referentiel/src/test/resources/csv/unit/mixElectrique.csv create mode 100644 services/api-referentiel/src/test/resources/csv/unit/mixElectrique_errorInMiddle.csv create mode 100644 services/api-referentiel/src/test/resources/csv/unit/typeEquipement.csv create mode 100644 services/api-referentiel/src/test/resources/csv/unit/typeEquipement_errorInMiddle.csv create mode 100644 services/api-referentiel/src/test/resources/csv/unit/wrongCSVFile.csv create mode 100644 services/api-referentiel/src/test/resources/logback.xml create mode 100644 services/api-referentiel/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker create mode 100644 services/api-referentiel/src/test/resources/org/mte/numecoeval/referentiel/referentiel_criteres.feature create mode 100644 services/api-referentiel/src/test/resources/org/mte/numecoeval/referentiel/referentiel_etape_acv.feature create mode 100644 services/api-referentiel/src/test/resources/org/mte/numecoeval/referentiel/referentiel_facteurs_impact_reseau.feature create mode 100644 services/api-referentiel/src/test/resources/org/mte/numecoeval/referentiel/referentiel_hypothese.feature create mode 100644 services/api-referentiel/src/test/resources/org/mte/numecoeval/referentiel/referentiel_impact_equipement.feature create mode 100644 services/api-referentiel/src/test/resources/org/mte/numecoeval/referentiel/referentiel_sprint10.feature create mode 100644 services/calculs/LICENSE create mode 100644 services/calculs/README.md create mode 100644 services/calculs/ci_settings.xml create mode 100644 services/calculs/dependency_check_suppressions.xml create mode 100644 services/calculs/pom.xml create mode 100644 services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/demande/DemandeCalculImpactApplication.java create mode 100644 services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/demande/DemandeCalculImpactEquipementPhysique.java create mode 100644 services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/demande/DemandeCalculImpactEquipementVirtuel.java create mode 100644 services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/demande/DemandeCalculImpactMessagerie.java create mode 100644 services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/demande/DemandeCalculImpactReseau.java create mode 100644 services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/entree/Application.java create mode 100644 services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/entree/DataCenter.java create mode 100644 services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/entree/EquipementPhysique.java create mode 100644 services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/entree/EquipementVirtuel.java create mode 100644 services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/entree/Messagerie.java create mode 100644 services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/erreur/TypeErreurCalcul.java create mode 100644 services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/indicateurs/ImpactApplication.java create mode 100644 services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/indicateurs/ImpactEquipementPhysique.java create mode 100644 services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/indicateurs/ImpactEquipementVirtuel.java create mode 100644 services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/indicateurs/ImpactMessagerie.java create mode 100644 services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/indicateurs/ImpactReseau.java create mode 100644 services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/referentiel/ReferentielCorrespondanceRefEquipement.java create mode 100644 services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/referentiel/ReferentielCritere.java create mode 100644 services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/referentiel/ReferentielEtapeACV.java create mode 100644 services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/referentiel/ReferentielHypothese.java create mode 100644 services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/referentiel/ReferentielImpactEquipement.java create mode 100644 services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/referentiel/ReferentielImpactMessagerie.java create mode 100644 services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/referentiel/ReferentielImpactReseau.java create mode 100644 services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/referentiel/ReferentielMixElectrique.java create mode 100644 services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/referentiel/ReferentielTypeEquipement.java create mode 100644 services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/trace/ConsoElecAnMoyenne.java create mode 100644 services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/trace/DureeDeVie.java create mode 100644 services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/trace/DureeDeVieParDefaut.java create mode 100644 services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/trace/MixElectrique.java create mode 100644 services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/trace/TraceCalculImpactApplication.java create mode 100644 services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/trace/TraceCalculImpactEquipementPhysique.java create mode 100644 services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/trace/TraceCalculImpactEquipementVirtuel.java create mode 100644 services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/trace/TraceCalculImpactMessagerie.java create mode 100644 services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/trace/TraceCalculImpactReseau.java create mode 100644 services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/exception/CalculImpactException.java create mode 100644 services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/exception/CalculImpactRuntimeException.java create mode 100644 services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/port/input/service/CalculImpactApplicationService.java create mode 100644 services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/port/input/service/CalculImpactEquipementPhysiqueService.java create mode 100644 services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/port/input/service/CalculImpactEquipementVirtuelService.java create mode 100644 services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/port/input/service/CalculImpactMessagerieService.java create mode 100644 services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/port/input/service/CalculImpactReseauService.java create mode 100644 services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/port/input/service/DureeDeVieEquipementPhysiqueService.java create mode 100644 services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/port/input/service/impl/CalculImpactApplicationServiceImpl.java create mode 100644 services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/port/input/service/impl/CalculImpactEquipementPhysiqueServiceImpl.java create mode 100644 services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/port/input/service/impl/CalculImpactEquipementVirtuelServiceImpl.java create mode 100644 services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/port/input/service/impl/CalculImpactMessagerieServiceImpl.java create mode 100644 services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/port/input/service/impl/CalculImpactReseauServiceImpl.java create mode 100644 services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/port/input/service/impl/DureeDeVieEquipementPhysiqueServiceImpl.java create mode 100644 services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/traceur/TraceCalculImpactApplicationUtils.java create mode 100644 services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/traceur/TraceCalculImpactEquipementPhysiqueUtils.java create mode 100644 services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/traceur/TraceCalculImpactMessagerieUtils.java create mode 100644 services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/traceur/TraceCalculImpactReseauUtils.java create mode 100644 services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/traceur/TraceCalculImpactVirtuelUtils.java create mode 100644 services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/traceur/TraceUtils.java create mode 100644 services/calculs/src/test/java/org/mte/numecoeval/calculs/CucumberIntegrationTest.java create mode 100644 services/calculs/src/test/java/org/mte/numecoeval/calculs/domain/service/CalculImpactApplicationServiceImplTest.java create mode 100644 services/calculs/src/test/java/org/mte/numecoeval/calculs/domain/service/CalculImpactEquipementPhysiqueServiceTest.java create mode 100644 services/calculs/src/test/java/org/mte/numecoeval/calculs/domain/service/CalculImpactEquipementVirtuelServiceTest.java create mode 100644 services/calculs/src/test/java/org/mte/numecoeval/calculs/domain/service/CalculImpactMessagerieServiceTest.java create mode 100644 services/calculs/src/test/java/org/mte/numecoeval/calculs/domain/service/CalculImpactReseauServiceTest.java create mode 100644 services/calculs/src/test/java/org/mte/numecoeval/calculs/domain/service/DureeDeVieEquipementPhysiqueServiceTest.java create mode 100644 services/calculs/src/test/java/org/mte/numecoeval/calculs/steps/CalculImpactEquipementVirtuelStepDefinitions.java create mode 100644 services/calculs/src/test/java/org/mte/numecoeval/calculs/steps/DataTableTypeDefinitions.java create mode 100644 services/calculs/src/test/java/org/mte/numecoeval/calculs/steps/DureeDeVieStepDefinitions.java create mode 100644 services/calculs/src/test/resources/application-test.yaml create mode 100644 services/calculs/src/test/resources/logback-test.xml create mode 100644 services/calculs/src/test/resources/org/mte/numecoeval/calculs/CalculImpactUnitaire-EquipementVirtuel.feature create mode 100644 services/calculs/src/test/resources/org/mte/numecoeval/calculs/DureeDeVie-EquipementPhysique.feature create mode 100644 services/common/.gitignore create mode 100644 services/common/.gitlab-ci-library.yml create mode 100644 services/common/.gitlab-ci-maven.yml create mode 100644 services/common/Dockerfile create mode 100644 services/common/README.md create mode 100644 services/common/ci_settings.xml create mode 100644 services/common/dependency_check_suppressions.xml create mode 100644 services/common/entrypoint.sh create mode 100644 services/common/pom.xml create mode 100644 services/common/src/main/java/org/mte/numecoeval/common/exception/NumEcoEvalRuntimeException.java create mode 100644 services/common/src/main/java/org/mte/numecoeval/common/utils/PreparedStatementUtils.java create mode 100644 services/common/src/main/java/org/mte/numecoeval/common/utils/ResultSetUtils.java create mode 100644 services/common/src/main/resources/static/README.md create mode 100644 services/common/src/main/resources/static/api-event-calculs-async-openapi.yaml create mode 100644 services/common/src/main/resources/static/api-event-calculs-openapi.yaml create mode 100644 services/common/src/main/resources/static/api-event-calculs-sync-openapi.yaml create mode 100644 services/common/src/main/resources/static/api-referentiels-openapi.yaml create mode 100644 services/common/src/main/resources/static/asyncapi_equipement_physique.yaml create mode 100644 services/common/src/main/resources/static/asyncapi_merge.yaml create mode 100644 services/common/src/main/resources/static/asyncapi_messagerie.yaml create mode 100644 services/common/src/test/java/org/mte/numecoeval/common/utils/PreparedStatementUtilsTest.java create mode 100644 services/common/src/test/java/org/mte/numecoeval/common/utils/ResultSetUtilsTest.java create mode 100644 services/core/.gitignore create mode 100644 services/core/README.md create mode 100644 services/core/ci_settings.xml create mode 100644 services/core/dependency_check_suppressions.xml create mode 100644 services/core/pom.xml diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..5cc42d7b --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +* text=auto eol=lf +CHANGELOG.md merge=union diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..63686729 --- /dev/null +++ b/.gitignore @@ -0,0 +1,31 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +*.iws +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 00000000..2631adff --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,107 @@ +variables: + ONLY: + value: "" + description: "List of components to build: common core calculs api-expositiondonneesentrees api-referentiel api-event-donneesentrees api-event-calculs" + MODE: + value: "build" + options: + - "build" + - "dependency-check" + description: "Mode: build or dependency-check" + +.maven-components: &maven-components + matrix: + - COMPONENT: [ api-expositiondonneesentrees, api-referentiel, api-event-donneesentrees, api-event-calculs ] + +.libraries: &libraries + matrix: + - COMPONENT: [ common, calculs ] + +workflow: + rules: + - if: $CI_PIPELINE_SOURCE == "merge_request_event" + when: never + - if: $CI_COMMIT_BRANCH =~ /doc.*/ + when: never + - if: $CI_COMMIT_REF_NAME =~ /g4it.*/ + when: always + - if: $CI_COMMIT_REF_NAME =~ /main|develop|\d+\.\d+\.\d+/ + when: always + +clear cache maven: + stage: .pre + script: + - rm -rf .m2/ + cache: + paths: + - .m2/ + +core: + stage: .pre + variables: + COMPONENT: $COMPONENT + MODE: $MODE + trigger: + include: "services/common/.gitlab-ci-library.yml" + strategy: depend + rules: + - if: $COMPONENT =~ $ONLY + when: always + - if: $ONLY == "" + changes: + - services/$COMPONENT/**/* + parallel: + matrix: + - COMPONENT: [ core ] + + +child-lib: + stage: build + variables: + COMPONENT: $COMPONENT + MODE: $MODE + trigger: + include: "services/common/.gitlab-ci-library.yml" + strategy: depend + rules: + - if: $COMPONENT =~ $ONLY + when: always + - if: $ONLY == "" + changes: + - services/$COMPONENT/**/* + parallel: *libraries + +child-mvn: + stage: deploy + variables: + COMPONENT: $COMPONENT + MODE: $MODE + trigger: + include: "services/common/.gitlab-ci-maven.yml" + strategy: depend + rules: + - if: $COMPONENT =~ $ONLY + when: always + - if: $ONLY == "" + changes: + - services/$COMPONENT/**/* + parallel: *maven-components + + +#e2e: +# stage: deploy +# image: docker:latest +# services: +# - docker:dind +# only: +# - main +# - develop +# - tags +# before_script: +# - apk add --update curl && rm -rf /var/cache/apk/* +# script: +# - cd $CI_PROJECT_DIR/assets/docker-compose +# - docker-compose up -d postgresdb +# - docker-compose ps -a +# - cd $CI_PROJECT_DIR/e2e +# - cat .env diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..dbe3dd08 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,44 @@ +# NumEcoEval ChangeLog + +Tous les changements de ce projet seront documentés dans ce document. + +## [Non livré] + +- Defect: Libellé erreur erroné quand donnée de référence manquante +- Ajout du mode d'utilisation et taux d'utilisation +- Ajout d'une API /version pour récupérer la version courante de NumEcoEval dans le composant référentiel + +## [1.2.2] - 2024-01-11 + +### Ajoutés +- Ajout de multi-partition pour le topic de donnée principal pour permettre la lecture en parallèle +- Amélioration des logs liés à Kafka et aux temps de traitement des équipements physiques + +## [1.2.1] - 2024-01-04 + +### Corrigés +- Augmentation de la taille maximum des fichiers en entrée de POST /import/csv +- Remise en place des champs _discriminator +- Déduplication des avertissements en sortie d'API /import/csv + +## [1.2.0] - 2023-12-11 + +### Ajoutés +- Contrôles sur les API d'entrées +- API Referentiel : affichage des mix electriques pour un pays + +## [1.1.0] - 2023-11-21 + +### Ajoutés + +### Corrigés +- [Code non cohérent avec la spécification](https://gitlab-forge.din.developpement-durable.gouv.fr/pub/numeco/m4g/api-event-enrichissement/-/issues/2) +- [Aucun index lors de la récupération des sous-éléments - problème de performance ](https://gitlab-forge.din.developpement-durable.gouv.fr/pub/numeco/m4g/api-event-donneesentrees/-/issues/1) + +### Supprimés +- Composants api-event-enrichissement, api-event-indicateurs, api-rest-calculs (regroupés dans api-event-calculs) + +## [1.0.0] - 2023-06-01 + +### Ajoutés +- Initialisation des modules diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..7e131afa --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2023 pub / Numérique et Écologie / NumEcoEval - M4G + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md index 7c72fe27..a88efec5 100644 --- a/README.md +++ b/README.md @@ -1,93 +1,205 @@ -# NumEcoEval +<!-- Adapted from https://github.com/othneildrew/Best-README-Template/ --> +<a name="readme-top"></a> +<!-- PROJECT LOGO --> +<div align="center"> +[![MIT License][license-shield]][license-url] +[![Java][Java]][Java-url] +[![Forge MTE][MTE]][MTE-url] -## Getting started + <h3 align="center">NumEcoEval</h3> -To make it easy for you to get started with GitLab, here's a list of recommended next steps. + <p align="center"> + Calcul de l’empreinte environnementale d'un système d'information + <br /> + </p> +</div> -Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)! -## Add your files +<!-- TABLE OF CONTENTS --> +<details> + <summary>Sommaire</summary> + <ol> + <li> + <a href="#le-projet">Le projet</a> + </li> + <li> + <a href="#demarrage-rapide">Démarrage rapide</a> + </li> + <li><a href="#feuille-de-route">Feuille de route</a></li> + <li><a href="#contributions">Contributions</a></li> + <li><a href="#licence">Licence</a></li> + <li><a href="#contact">Contact</a></li> + </ol> +</details> -- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files -- [ ] [Add files using the command line](https://docs.gitlab.com/ee/gitlab-basics/add-file.html#add-a-file-using-the-command-line) or push an existing Git repository with the following command: -``` -cd existing_repo -git remote add origin https://gitlab-forge.din.developpement-durable.gouv.fr/pub/numeco/m4g/numecoeval.git -git branch -M main -git push -uf origin main -``` -## Integrate with your tools +<!-- ABOUT THE PROJECT --> -- [ ] [Set up project integrations](https://gitlab-forge.din.developpement-durable.gouv.fr/pub/numeco/m4g/numecoeval/-/settings/integrations) +## Le projet -## Collaborate with your team +**NumEcoEval** est une solution permettant de calculer l’empreinte environnementale d'un système d'information. +Le système d'information est défini comme l'ensemble des équipements physiques, des machines virtuelles et des applications gérés par une organisation. -- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/) -- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html) -- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically) -- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/) -- [ ] [Set auto-merge](https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html) +Il suit les principes décrit dans la RCP Service Numérique et préfigure l'outillage qui pourrait être associé à une RCP "Système d'Information" qui est en cours d'élaboration par l'écosystème. -## Test and Deploy +Cet outil a été construit sur la base des travaux réalisés par Le Ministère de la transition écologique, Sopra Steria, l’ADEME, l’INR et BOAVIZTA. -Use the built-in continuous integration in GitLab. +Ce projet est construit en modules présent dans ce même repository Gitlab ou dans d'autres du même groupe. -- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/index.html) -- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing (SAST)](https://docs.gitlab.com/ee/user/application_security/sast/) -- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html) -- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/) -- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html) +<details><summary>Liste des modules de NumEcoEval</summary> +<ul> + <li>[docs](https://gitlab-forge.din.developpement-durable.gouv.fr/pub/numeco/m4g/numecoeval/-/tree/develop/docs)</li> + <li>[core](https://gitlab-forge.din.developpement-durable.gouv.fr/pub/numeco/m4g/numecoeval/-/tree/develop/services/core)</li> + <li>[common](https://gitlab-forge.din.developpement-durable.gouv.fr/pub/numeco/m4g/numecoeval/-/tree/develop/services/common)</li> + <li>[calculs](https://gitlab-forge.din.developpement-durable.gouv.fr/pub/numeco/m4g/numecoeval/-/tree/develop/services/calculs)</li> + <li>[api-expositiondonneesentrees](https://gitlab-forge.din.developpement-durable.gouv.fr/pub/numeco/m4g/numecoeval/-/tree/develop/services/api-expositiondonneesentrees)</li> + <li>[api-referentiel](https://gitlab-forge.din.developpement-durable.gouv.fr/pub/numeco/m4g/numecoeval/-/tree/develop/services/api-referentiel)</li> + <li>[api-event-donneesentrees](https://gitlab-forge.din.developpement-durable.gouv.fr/pub/numeco/m4g/numecoeval/-/tree/develop/api-event-donneesentrees)</li> + <li>[api-event-calculs](https://gitlab-forge.din.developpement-durable.gouv.fr/pub/numeco/m4g/numecoeval/-/tree/develop/api-event-calculs)</li> +</ul> +</details> -*** +<details><summary>Liste des modules hors de ce repository</summary> +<ul> + <li>[ci-library](https://gitlab-forge.din.developpement-durable.gouv.fr/pub/numeco/m4g/ci-library)</li> +</ul> +</details> -# Editing this README -When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thanks to [makeareadme.com](https://www.makeareadme.com/) for this template. +> Pour tout renseignement complémentaire, contacter numerique-ecologie@developpement-durable.gouv.fr -## Suggestions for a good README -Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information. +<p align="right">(<a href="#readme-top">back to top</a>)</p> -## Name -Choose a self-explaining name for your project. +<!-- GETTING STARTED --> -## Description -Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors. +## Démarrage rapide -## Badges -On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge. +### Installation -## Visuals -Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method. +Le démarrage le plus rapide se fait via Docker-compose avec le [fichier mis à disposition](assets/docker-compose/docker-compose.yml). -## Installation -Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection. +La version par défaut est la version de développement `develop`. -## Usage -Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README. +Pour utiliser une version différente, il faut modifier le TAG présent dans le fichier [.env](assets/docker-compose/.env). -## Support -Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc. +La commande la plus simple pour démarrer le moteur : +````shell +# Aller dans le dossier assets/docker-compose +cd assets/docker-compose -## Roadmap -If you have ideas for releases in the future, it is a good idea to list them in the README. +# Faire cette commande si vous voulez les ports en 808x +# Par défaut, les ports sont en 1808x +sed -i "s/PORT_PREFIX=.*/PORT_PREFIX=/g" .env -## Contributing -State if you are open to contributions and what your requirements are for accepting them. +# Mode Live - Uniquement durant l'exécution de la commande +docker-compose up -For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self. +# Mode Deamon - Exécution en tâche de fond +docker-compose up -d +```` -You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser. +7 conteneurs doivent être démarrés : +* Zookeeper +* Kafka +* Postgres +* API REST Référentiels +* API REST Expositions des données d'entrées +* API Event données d'entrées +* API Event calculs -## Authors and acknowledgment -Show your appreciation to those who have contributed to the project. +Pour arrêter l'application : +````shell +cd assets/docker-compose +docker-compose down +```` +_Il suffit de couper le processus en mode Live._ -## License -For open source projects, say how it is licensed. +Exemple de démarrage avec une version production: +````shell +cd assets/docker-compose +docker-compose -e TAG=1.2.0 up -d +```` + +*En cas d'initialisation, il peut arriver que le moteur ait besoin d'un redémarrage avant d'être utilisé. +Si une des API REST ne répond pas sur son URL ou un ou plusieurs containers sont indisponibles: il faut procéder à un redémarrage en arrêtant puis en démarrant le moteur.* + + +### Usage + +Une fois l'application démarrée, les API REST sont alors disponibles sur les URLs suivantes (contrats d'interface): +- [API REST Référentiels](http://localhost:8080/swagger-ui/index.html) +- [API REST Expositions des données d'entrées](http://localhost:8081/swagger-ui/index.html) +- [API Event Calculs](http://localhost:8085/swagger-ui/index.html) + +<p align="right">(<a href="#readme-top">back to top</a>)</p> + +<!-- ROADMAP --> + +## Feuille de route + +:construction: _En cours de rédaction_ :construction: + +<p align="right">(<a href="#readme-top">back to top</a>)</p> + + + +<!-- CONTRIBUTING --> + +## Contributions + +Les contributions sont ce qui fait de la communauté open source un endroit incroyable pour +apprendre, s'inspirer et de créer. Toutes les contributions que vous faites sont **grandement +appréciées**. + +À ce jour, veuillez nous contacter au préalable à numerique-ecologie@developpement-durable.gouv.fr pour avoir accès en contribution à ce projet. + +Vous pouivez ensuite forker le dépôt et +créer une pull request. Vous pouvez aussi simplement ouvrir une issue avec le tag " +enhancement". +N'oubliez pas de donner une étoile au projet ! Merci encore ! + +1. Forkez le Project +2. Créez votre branche de feature (`git checkout -b feature/AmazingFeature`) +3. Commitez votre codes (`git commit -m 'Add some AmazingFeature'`) +4. Pushez sur la branche (`git push origin feature/AmazingFeature`) +5. Ouvrez Pull Request + +<p align="right">(<a href="#readme-top">back to top</a>)</p> + +<!-- LICENSE --> + +## Licence + +Distribué sous la licence Apache 2.0. Voir `LICENSE` pour plus d'informations. + +<p align="right">(<a href="#readme-top">back to top</a>)</p> + + +<!-- CONTACT --> + +## Contact + +Programme Numérique et Écologie - numerique-ecologie@developpement-durable.gouv.fr + +<p align="right">(<a href="#readme-top">back to top</a>)</p> + + +<!-- MARKDOWN LINKS & IMAGES --> +<!-- https://www.markdownguide.org/basic-syntax/#reference-style-links --> + +[license-shield]: https://img.shields.io/gitlab/license/20519?gitlab_url=https%3A%2F%2Fgitlab-forge.din.developpement-durable.gouv.fr&style=for-the-badge + +[license-url]: https://gitlab-forge.din.developpement-durable.gouv.fr/pub/numeco/m4g/numecoeval/-/blob/develop/LICENSE + +[Java]: https://img.shields.io/badge/OpenJDK-000000?style=for-the-badge&logo=openjdk&logoColor=white&labelColor=C13D3C + +[Java-url]: https://openjdk.org/ + +[MTE]: https://img.shields.io/badge/forge%20MTE-0000?color=00008f&style=for-the-badge&logo=gitlab + +[MTE-url]: https://gitlab-forge.din.developpement-durable.gouv.fr/pub/numeco/m4g/numecoeval -## Project status -If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers. diff --git a/assets/docker-compose/.env b/assets/docker-compose/.env new file mode 100644 index 00000000..1935e94a --- /dev/null +++ b/assets/docker-compose/.env @@ -0,0 +1,7 @@ +REGISTRY_URL=registry.gitlab-forge.din.developpement-durable.gouv.fr/pub/numeco/m4g/numecoeval +PORT_PREFIX=1 +TAG=develop +POSTGRES_USER=postgres +POSTGRES_PASSWORD=postgres +LOGGING_LEVEL_ROOT=ERROR +LOGGING_LEVEL_ORG_SPRINGFRAMEWORK=ERROR diff --git a/assets/docker-compose/docker-compose.yml b/assets/docker-compose/docker-compose.yml new file mode 100644 index 00000000..f0eb0f0d --- /dev/null +++ b/assets/docker-compose/docker-compose.yml @@ -0,0 +1,142 @@ +# Les variables globales sont présentes dans le fichier .env +# - TAG +# - REGISTRY_URL +# - POSTGRES (user, password) +# - LOGGING_LEVEL_* + +version: "3" + +services: + # Zookeeper basé sur la documentation officielle de Bitnami + ## https://github.com/bitnami/containers/blob/main/bitnami/zookeeper/docker-compose.yml + ## https://hub.docker.com/r/bitnami/zookeeper/ + zookeeper: + image: docker.io/bitnami/zookeeper:3.9 + container_name: zookeeper + ports: + - "2181:2181" + volumes: + - "numecoeval_zookeeper_data:/bitnami" + environment: + ALLOW_ANONYMOUS_LOGIN: "yes" + + # Kafka basé sur la documentation officielle de Bitnami + ## https://github.com/bitnami/containers/blob/main/bitnami/kafka/docker-compose.yml + ## https://hub.docker.com/r/bitnami/kafka/ + kafka: + image: docker.io/bitnami/kafka:3.6 + depends_on: + - zookeeper + container_name: kafka + ports: + - "9092:9092" + volumes: + - "numecoeval_kafka_data:/bitnami" + environment: + ALLOW_PLAINTEXT_LISTENER: "yes" + KAFKA_ZOOKEEPER_CONNECT: "zookeeper:2181" + + # Postgres basé sur la documentation officielle de Postgres sur Dockerhub + ## https://hub.docker.com/_/postgres + postgresdb: + image: docker.io/postgres:15 + container_name: postgresdb + ports: + - "5432:5432" + environment: + POSTGRES_USER: $POSTGRES_USER + POSTGRES_PASSWORD: $POSTGRES_PASSWORD + volumes: + - "numecoeval_postgres_data:/var/lib/postgresql/data" + + api-rest-referentiels: + image: ${REGISTRY_URL}/api-referentiel:${TAG} + depends_on: + - postgresdb + ports: + - "${PORT_PREFIX}8080:8080" + environment: + SERVER_PORT: "8080" + MANAGEMENT_SERVER_PORT: "8080" + SPRING_JPA_HIBERNATE_DDL-AUTO: "update" + SPRING_DATASOURCE_URL: "jdbc:postgresql://postgresdb:5432/postgres?reWriteBatchedInserts=true" + SPRING_DATASOURCE_USERNAME: $POSTGRES_USER + SPRING_DATASOURCE_PASSWORD: $POSTGRES_PASSWORD + SPRING_SERVLET_MULTIPART_MAXREQUESTSIZE: "45MB" + SPRING_SERVLET_MULTIPART_MAXFILESIZE: "10MB" + SPRING_JPA_PROPERTIES_HIBERNATE_JDBC_BATCHSIZE: "1000" + SPRING_JPA_PROPERTIES_HIBERNATE_ORDERINSERTS: "true" + SPRING_JPA_SHOWSQL: "false" + NUMECOEVAL_URLS_ALLOWED: "http://localhost,http://api-rest-referentiels" + LOGGING_LEVEL_ROOT: $LOGGING_LEVEL_ROOT + LOGGING_LEVEL_ORG_SPRINGFRAMEWORK: $LOGGING_LEVEL_ORG_SPRINGFRAMEWORK + + api-rest-expositiondonneesentrees: + image: ${REGISTRY_URL}/api-expositiondonneesentrees:${TAG} + depends_on: + - postgresdb + - api-rest-referentiels + ports: + - "${PORT_PREFIX}8081:8080" + environment: + SERVER_PORT: "8080" + MANAGEMENT_SERVER_PORT: "8080" + NUMECOEVAL_KAFKA_TOPIC_MAX_MESSAGES_SIZE: "52428800" + NUMECOEVAL_REFERENTIEL_SERVER_URL: "http://api-rest-referentiels:8080" + SPRING_SERVLET_MULTIPART_MAXREQUESTSIZE: "45MB" + SPRING_SERVLET_MULTIPART_MAXFILESIZE: "10MB" + SPRING_DATASOURCE_URL: "jdbc:postgresql://postgresdb:5432/postgres?reWriteBatchedInserts=true" + SPRING_DATASOURCE_USERNAME: $POSTGRES_USER + SPRING_DATASOURCE_PASSWORD: $POSTGRES_PASSWORD + SPRING_JPA_PROPERTIES_HIBERNATE_GENERATESTATISTICS: "true" + SPRING_JPA_PROPERTIES_HIBERNATE_ORDERINSERTS: "true" + SPRING_JPA_PROPERTIES_HIBERNATE_JDBC_BATCHSIZE: "1000" + SPRING_JPA_SHOWSQL: "false" + NUMECOEVAL_URLS_ALLOWED: "http://localhost,http://api-rest-expositiondonneesentrees" + LOGGING_LEVEL_ROOT: $LOGGING_LEVEL_ROOT + LOGGING_LEVEL_ORG_SPRINGFRAMEWORK: $LOGGING_LEVEL_ORG_SPRINGFRAMEWORK + + api-event-donneesentrees: + image: ${REGISTRY_URL}/api-event-donneesentrees:${TAG} + depends_on: + - kafka + - postgresdb + - api-rest-expositiondonneesentrees + ports: + - "${PORT_PREFIX}8083:8080" + environment: + SERVER_PORT: "8080" + MANAGEMENT_SERVER_PORT: "8080" + SPRING_KAFKA_BOOTSTRAPSERVERS: "kafka:9092" + SPRING_DATASOURCE_URL: "jdbc:postgresql://postgresdb:5432/postgres?reWriteBatchedInserts=true" + SPRING_DATASOURCE_USERNAME: $POSTGRES_USER + SPRING_DATASOURCE_PASSWORD: $POSTGRES_PASSWORD + LOGGING_LEVEL_ROOT: $LOGGING_LEVEL_ROOT + LOGGING_LEVEL_ORG_SPRINGFRAMEWORK: $LOGGING_LEVEL_ORG_SPRINGFRAMEWORK + + api-event-calculs: + image: ${REGISTRY_URL}/api-event-calculs:${TAG} + depends_on: + - kafka + - postgresdb + ports: + - "${PORT_PREFIX}8085:8080" + environment: + SERVER_PORT: "8080" + MANAGEMENT_SERVER_PORT: "8080" + NUMECOEVAL_URLS_ALLOWED: "http://localhost,http://api-event-calculs" + NUMECOEVAL_REFERENTIELS_URL: "http://api-rest-referentiels:8080" + SPRING_KAFKA_BOOTSTRAPSERVERS: "kafka:9092" + SPRING_DATASOURCE_URL: "jdbc:postgresql://postgresdb:5432/postgres?reWriteBatchedInserts=true" + SPRING_DATASOURCE_USERNAME: $POSTGRES_USER + SPRING_DATASOURCE_PASSWORD: $POSTGRES_PASSWORD + LOGGING_LEVEL_ROOT: $LOGGING_LEVEL_ROOT + LOGGING_LEVEL_ORG_SPRINGFRAMEWORK: $LOGGING_LEVEL_ORG_SPRINGFRAMEWORK + +volumes: + numecoeval_zookeeper_data: + driver: local + numecoeval_kafka_data: + driver: local + numecoeval_postgres_data: + driver: local diff --git a/assets/images/08BDB65B-73CA-4FF0-8C1C-1040DA20E7B3.png b/assets/images/08BDB65B-73CA-4FF0-8C1C-1040DA20E7B3.png new file mode 100644 index 0000000000000000000000000000000000000000..d1575ef89569b25578693f1061e190b6aeb2d6f0 GIT binary patch literal 136268 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¨aHD;#gp+hRDJ@ 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 literal 0 HcmV?d00001 diff --git a/assets/images/2A4575F4-EE3B-478D-A397-BD8B409390E0.png b/assets/images/2A4575F4-EE3B-478D-A397-BD8B409390E0.png new file mode 100644 index 0000000000000000000000000000000000000000..3555a08ade0eced2a0361511cfe1084ab2f2fbb0 GIT binary patch literal 6740 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 literal 0 HcmV?d00001 diff --git a/assets/images/3E1ED783-839A-4094-9A8B-8B02F6E5283E.png b/assets/images/3E1ED783-839A-4094-9A8B-8B02F6E5283E.png new file mode 100644 index 0000000000000000000000000000000000000000..5eeecb145a43b97ddf695a4f110de4f4604a2ef0 GIT binary patch literal 60396 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[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 Q`~Uy|07*qoM6N<$g8mf8Gynhq literal 0 HcmV?d00001 diff --git a/assets/images/56BC42EB-2AAF-4A9B-A98C-9D29DB8E2520.png b/assets/images/56BC42EB-2AAF-4A9B-A98C-9D29DB8E2520.png new file mode 100644 index 0000000000000000000000000000000000000000..5d7d2a007b81e64fe257c6e957eaccecbc6e7f79 GIT binary patch literal 3610 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 WK?OsEhjD-QGO)IhR*j}T;{O1B;MR)( literal 0 HcmV?d00001 diff --git a/assets/images/81945681-5CEB-4178-8AB8-4B9B6801FBC9.png b/assets/images/81945681-5CEB-4178-8AB8-4B9B6801FBC9.png new file mode 100644 index 0000000000000000000000000000000000000000..69cb210d8e71138b010855c51431436da7eb4796 GIT binary patch literal 4381 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 literal 0 HcmV?d00001 diff --git a/assets/images/8B74D910-915C-46FD-9C46-34A44FD3CBBA.png b/assets/images/8B74D910-915C-46FD-9C46-34A44FD3CBBA.png new file mode 100644 index 0000000000000000000000000000000000000000..54f875dad89af646c559fe128d33694f3c3e37b5 GIT binary patch literal 8555 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 a3@seg5wa|oDE8k+2aPv6DwWFCVgCcxeVADQ literal 0 HcmV?d00001 diff --git a/assets/images/A8AF1630-6525-428D-85A6-E3C413982D97.png b/assets/images/A8AF1630-6525-428D-85A6-E3C413982D97.png new file mode 100644 index 0000000000000000000000000000000000000000..493c303644239b67f5c629231757380e3bb34c4d GIT binary patch literal 66370 zcmd3O1y@vmwDtfaB?t^s(%s!sBMs8s4N7;{AV_zEl!Byyba#h<bayw>{T}}J-tX|P z1&d)g=XZARXU7nxq#%Wk`VJKY0-?)Di>rV@2xK4-Ts<-(@J`Eopgr&d&RIoD6jU}s zv<LhmZmuO`AukVN0A3@5;6f}x;Fn8)?{~mAaC;6M2m$yB_wrs2{QvzGfh-5%|6aq@ zzg%d;0NMh9pdcCX4{9E8hnYw|x?5Avqe*7k%Wln;cqjx^K|vt%l)oV2>T1@K#MZI1 zf}g&+S9IoyDmj0Q@?W7=|1{@<(tm|qDUf+%R1yg|LTKHZZDnd*p4+6tZ0F)_Wm@CQ z@>U*w5}UH_1$TVPDVLnHImPQ^K`8&feW4}7c*fp%2>nMG^blpY(XOj-uCz4Xyh55s zSeqoq%rf1~yJcyn@p!>K$M=2h)O>(j=6y&VF<(lwv)f@wtAMZu(aCJ>ZD!SvD4h$j z5}3b0bcrkB-Dgc}pZUKb_DB9ieR{L@5=|O5byt;3k>}`qD<EIO$=WG~sIfY7A^mp_ z=p&YPzZ(8WG^wc|bWH3OqHsxgs@P_*5tL6y$1L^_P2ed=KQv_TgU9cYuJ`4pM{T9i z0l_PtLX@6+;$;MAnC8usFn#pYDWm%woxlov(uJv;Lsily{i`*>D{lKu%73yDGB^lX zfCI`H2qgu7Apy_@6ce6}u2PSMARKAsJD(8R0l5~QaVc7as13$`Yw0J%z2FcNIZ$s1 z()AW7SiLv&?y%<*o`n{J!;0IQDxBCW>r#Tg|0NqA3PTKq!<>5soN9#$i;F`_DUk7? zBaY$S-<W)t9@B+z$mc<UlZW_KZAwJ|`A^b^O6-H9`kjTfry70Ru19NHw6|L(y<fr7 zFt9Wk7@i9%3hSn!g2ER`tp4_%eB1m=1QjD)brK09OY5LZH3D3N{uP!k@uA7dJaP5J z414<9X<gO8TC6-@ok*haZ1BrDDq$e>ShdE5$t2)N7$`EFs6aKkLu>=>@<ZUBM`%XS z2E%FkYZSCtSiXs(k0i>p^_ez$7uZ_<)gNc_7g;4zK@q5^?FEu(0w$?Ixt-DNCzc+_ zf^gvIKB>~a4ZgP5s^^LnEIM5`N@*Q~r`l0%J=_jEx=CsDLk@a9IS&6Fl1JN4x|G+} zqZSKRNr76I2t%sg(FV4soGSMsTD*5jvPyo9k{S&{i`B=pq8I*Xl*-g2H-JiH&5@b& zHuQxJL}?geKM3xh6QDq6Oqj8;t#yLs(UEYV7y)s?>MWbHv=qHGnGZaha5G!ZOb7lg zzNUYC9C@}wGL=ts^B1smyASv?EcGNyi2~pW6S=)!T1>Kd<J7^8hGe`o@?&d|sW!RZ zAPrUQZ?F-}rf21z7TM_;`QVy^vt}?zJ@^Y?DtSzB@5t!A4~qCusBq!%@p$lw`Z`zn z5QNV^8ToRcM|U@dB=WoibD7P@yBoze2fDPAc0es7d+81nURa0(1lcW}ei#7|!v@dB z!i0>kNl-M>{>{MLAtT(BKN!6|TAFMCx&20|MjChH{Jm9Tb5>TFtt<yl(Hp>`;xTjJ z;;DG!tzw}+u$2&zO!%TD(fSC+BmfC~uQxRKaUgLkLuLRrial(fEjM-p_lDZL<uMyn zI_SMv{V6T*_-w>F85r0^Hm5<GG(03%n~i6rW5))cNP-;)78nN|#2nkt*)K^Y@I>kD zUtm*Mq|beiLk$OEBL+CnZ=wct5uXCG;3r~9$q2@OW9~5grGuT?v*xD-m5~O#j*#9v z!>;EJJY|0~@<^<a$ka;>Wfq9E3Sh|b7riRX5VqHFlk52Cfg2^UJJuoV{1<3d{gV8E zY<!aDpHbfUOG%cF4g|gk%j5$LQJRU)84V&$4xhRu?fhOH{mm_duwfTdU*21l?BaN6 zC!}4jpQoAX2I=k1N6BD~xZmMQ4gjYU*g0?}ce>(mGVKpwBbQA|iDS>$i*Ic2e@{FA zVdbW#f`cGSf`UUCU^lptaU!`lYu2-xb2a}3W|%QxCHTSd#Boq7ETuAa4{3f<os!W# zHc^`*3D|r*lmT<>>cdchapXgs1cvIGpQ;oj6de#w#oDm4G#Nc|z1|&FxAOyr@Ja_$ zUv-{RxGWISE<Ub0<lMdH`8Hg&P&4&t(2fZBG}RaqaG!7c8a+TWR-JBk@Ey@J0`B0h zks#7DoitewN;|$M#$MG9Mdr!3*=bcGq$yfBNGJ?&XXKnS2t?WqK6NiO{0l6l^Jtx- zUV}L9@J>YpFm7q&&xFRI!_Iu0q-^4PaTo2@fIL+afcqw!PbSmB@W_0bcUs*pdZ^dM zAK51y)u0F@|3$?da5V!y#kp~)AgWj%&u45BKvHCIB7pWDj2sfcDp4Q{Uevf2RKyUy z=%Wo)dH?;7@Ki=raHVg8`)%+kFgh?Tc`83$NdPfh4-g~O%wrx8)fg*WQYq?G|7ed~ zAI|3o^uvYwmQZwnB55)+cpaQYUjLA{tubAgjv+n-gz<orp}jbnb4k(XVr*E%6dyjZ zX2+j>r+TfV{P!69ittpJ`hb%U8iy`^C5US?UBF)YJ1YQq2L%@39n$U*mGRJ2Fk6$c ztz}&KMrFwv*(uK9`Lj<d+FS-$+6Z7#{^qHH*j3zs<g;RP7C;3*@~JTu|1o6^S%4kb z=<}vPrW3b?fZvxRtR)4fkQZBAhk~leKAvS>RQ+PI^&|@3i3QY0LjkyxIB(#O*mfvN zb@gu7SYOS}g9tteKf+PW%u8Z{#et22JlF~Uffp<NzS>$0b3j<`DFVR2jZ^`4xDwO> z%gF7g_yN2T?yDh>_iTdrYRiOlB!H6HP~eV092QGxVw>e`;#$LFRPJ9*Vxhl~$3S5? zmeFGH%48tZTzpFxd$F0pDFz`_vELtGTs-t096o7;El<O|-ORMOEvB0spr7}E{^Z<0 z(qW=pD1v%}5!gv)jHWj<XY)(cpZjNDB4ZvvXCaz)423PLg<}n3Lbyx?z%|-SxPqo0 zh6BC`I@Z85%~<I=#m9*_<3oJG3Ef{nkumxXlr0pLV%{RS8;)pz>D>TIfQVcXrGYys zR-N`#g-C8G(PC6P8)Kt@up>?O0l6Jn`HU>ah-VYmrj90b_AL+@$>abEJHfr%RG8NA zY<x~wJ7<KE_s%v8CPKn}lrJer3mbM;LBNu~A~m>dUP1D0BMyWjo(&lEF6TBf6(&1e zy{>*Z=~_dfo-ozAf(Q8veN<#{_~b`7Pxty3`HQ)=*h;U$0cniA2CS<Ewq^_or48t^ zbR`X3JDc(t?5OyUyeDuqPJiNM%Y}=r^Mm<!Fra2h%-{HbQSJfZvIT?mepPQ<Jbv1> zMTo*MO|8TONE4^cA&t1H+bS)+bH~=l9s~^3Ar$lpdXoZ}2sw=GJnSaU7b4j|K!X)# z0+1C8D?)C^u@;7(Ebp4PW$2(o18i3d@Rp#j5m7Sj9`IJv2p_iF#7D1<7mOc}593}8 zFg^s3^-t*;Z}Ra=4XPN8$sn*w#EU;Su1rpwfmc=7ARm(bzpgn34~XaTy~GwH6L_7d zDTWk&qIq2!-1r}Wz%E2#?ElL;4`3!K-Yu2My;9XORsH}Qo0K_t>-XpAJ~7K-CrZLY zR4eHDk}?V~PBIz4z@n^M<kdbWOue$K_B(;?glGtGTrAWC3-)oEG4%B<$ux5qHUCfQ zfJth=v|UexxML9D@@9lkx>`XSrsDtWFjTR5fOre>{-NWOdnc%UD3*Yik$_Dk%eV>@ zMQWq8uZ|h{ETY{C|2Q+AZW3<**oLIQ{Fp3~QyQ1<s=KEG;uE0zn5$^g`-|3A?`Q!_ z3D?!xB$&M9<5i$=(0Z|dX%zTWILk_{O%-rKTNVUWRR$uh1fKyXGe#&?fgK2fr&8?G zZJ6Q`hHUsL15hxan+Ap_Jcq2{kHa}KLsd(Ym<5ji1_*YuwQZ6m8hnc2A*6EH8baFr z-SKUp*nenI7OUO8>{T|>De(hn`r<;ms7cWr`zz?#lx8sVM+S-*NW_C6^->AvPun*( zQB|e?$P$Hj7`PMQFna_YTv4gTFvJ153y4;~>!QDWi-R#Rh3-9Tt~zmdMgCK!`R@j9 zAjBC2VmV<=kEl&#-o?YT0l><BkojE?MGDm0mtyS01UpmCaOhMu`tN!Z;CkTztRLeS z)3g=oYEfWFFvt`N+15~2;>3oPpFNW=ZxFvDwP^jA7zH?-I!ZhBm6{pl1LncFnmM#Z zrd<xqhhUQ5^g$aE4e}di3cdEgKgg!eIDMP?Vt*=;APZ>ihb>M{&KiCC{P$DTaH@c( zn-nj6FouMIBBSN-Nk>h6{U`?Hwf}RZB$Re!`Ux(6N=h+CMe{e4nc#I9AfF5FIUfd5 zgK~4hy?xeNq}jh^|4Y~qX=Qje(LoGCa&!7{m{kH)58yV8(eF)GC^$pmQ5yV>HRz0; z2WzWf_{FxN@DyyKZr`o$(hAed7As%!c6Tf&aLg3>n;5k)oLS@ho{hNC&puR7EqO1A zwn!edvO#&uu_tz^LSG}J<P8ZGddb+IMzF9Gq0Uu)!W{Z75ptS=5Vcp~@-OF8txAC; z``Mb+?#8ypY43=)X#jIiL>YU<I0&2bb;5+EFGYMG5IHq9|M(`V5J3Ac?tlkpTRz25 z&_!%*J;QGI5}_i1IPgOv`H==4u3lds&O9skERL%#G5o*FG2rho>rGo?PVU)-6dY0j zt)K!?Kpc*tNsa{Gp{enmou*>f&FXW~W<x39?|>HIkypL__*zerusAqYsW5<3@MU2b z>cQ(~p9aK1cAC6o%cjD9iMz$Aa=gin|2>2ieCjKrLqLA>L^}Nun}i04VgP=?L?Ek- z*rbH<8Ucytv)eyQ$td7>;OmY8cJf5&@FesL>3S-92Y82qww<>ru4*GmMizKNTgPx8 zu}m?^6vYcLfFOs#YRNfO6{K2X(__=CumB<SzeKXgaCFrGQ2tVRmu`7OQ4XW)e>eiN z8w0BaB4HYv=>EO+z&Rl2?*LMRyl1SHa{@TNtC%SfiQBYN5qdeBEPxFF!de+|<W;}z z-fVF204Q`f5O7W5)l;>7%=Y0v>ai>BVLvt&23I=)Zs4zE0kOax9;A}}Gr!$}UEl~` zV&03@WqWvV;Jjd-nYiai9DGtHj#O0MjQ0T6h=q!StDifG58(01ZL#!6A^?ux1Gcv( z-Vvon1*$9IjCDFg$0v<eo6LUy;{70$AO*nxKBl5HfUOmGH7lrrGZcaJ<po*u8^nax zmTYjzFNd7PWpc=>D*q23aFDUA4!M3DHqi?>x9%YsGINx%6(MtbUn|Dw)EF$XQ_^>& zMUU>#qyId|2mbClQz4D?$@QgZ$$8=U4MeX14bI%9{b-+i^3Eu(D6!!{E%8NPk(98= zFp-`JCfpM4H}u}J>;NSyhrtDnjcme9rKSIfB4aFzM!wwr59#6xpf+0G_N42we(%*J zs@O(w>Lsn~iLb2Hn+;PQKd#oe_(9dZRIJa|)(QGe40)x0m^Dh|QBe_PjE#+@H=6WV z9GjNn6B4jhym<fv2&IL4R2UAbHnXtu4=s30^bHJ&0=y9WC|Y?r6y^7rQTMKgu?RTv z@9EWoegCIy=J<dAIOeJfRGRyb4%_ky3WieIrTVKaC(L^zH_Wz&H+n1tF5Y||O8)b& z1L<UNzNTbrQnih#%EDK@vZ|`buI}Ew!xj)}0t5=ZlV=m%jW-{^7poYS0&4EtV-)Ie zcp$5NqB!1Q9`<vXW<~W;tr>c}FWLN=Vi5cMbggb_Wrg}Wkb_M@@a*keh3Pk&l~(56 z8f|Uu*13c@N~wWF`U$iBNlT|mWmWbP6ZH8kBtRkSFM)uWP1KK61ci?Sh3X9uIKrIE z=`4f5N}nAv`Y1nQT`OZ6<26SD?%903yujgp>^r`=NaVnhjuajqzNNczN6P>FVE@s; zz<?{nWCgH#W5{o<>IDvtYVWS*$Uib<TL7$+Y0%BmK{f5AZ22i69C*eqRKrzJ`<G%> z{OL~!NjkHAbAY&&qh8|28CHMKT%RNBojzsMH$bc!O8`a2E*J+bPMq^y1N1fmXYHAE zlu}W_R@<eFgC3Yr)N^GW$#TAQ**BlzReD{@c1eyZeoSz5=zI5d(4x3Fjj)2U2LF?* zekPacep%br{mOH|!)b`fUOB-iU^hTyjf@ol9zfhh&i@vL>MbAu6xSv?8$Pg+j}H>y z3u}5^wNzt-a2SyR3-)c=xXgF;bu{FpqhJ3zDp(fz*q$2kLwZAWGIm#9ZFB|PM7u;t z2qNpu&(6|it7#6Ecwb4IdV7~YuJ~R*I^FGky7J1R1x$PW1N2^ABiT(;P;xt!%E%3{ z>|wtUIzw?Vri(D*s8h+Rmjvx?DDuA_S>WTgXFKis)61-ou6e~kW5>UzYfdbYG3u#n zO4%IUv1*;AC4X40lZlTc&CG5rBqU8N%nG~6zhP}Y%`dks&AXzNeVKZ`o59P=%d6hN zoB>pnPnPqDE7?z+kNH(J78V8wmHqFZ9J*f2_Fq!(_I;T;@Ayo;%vg&Qpsz9j!po6@ zJ~U`$D(f)~Sy`KliHXhn7O@_DkRH-Sxc+4{r2XknyufkJf+rh~pd|bmb<X1($^Q0M z=z0lAiWaWxCAAaL3q<n5)f@Z?Jae>d-depG_>hyIFIiT<ZekhQ{&|;7=?m;ku3qCv zTr%i0ON_{qW*VQP6IY1Qi$P&wjU5MK{VNG&*^;1&4FWUC6k@B(8pHqjb9c)nta5`v zhFUzD=6nK+#GqE;wqX8Y>r?2M<?~O`st0H_wYz{XbkCe3xkcsN4e6Ac0cOMSJfp4o z;P6Q#b}wYQ1S~@6KN@WfZdH1m(IQN*M8N)OF%2a%f1tj~tiTU9eTj-Ppf+XRHR?H4 zttv9R)Wcc9+ZUs5^-iyRM}EBV0i20fzyxBj;|nOr@)|xB&RzOmJUsI9@L%8y(su0g z6*f_hKMdqs6aiX5mjm*8kce_0taS7s==0qW_cDMF=uBueUN~#F`Pp{v-B0U2U;aeB zx-*}W<)fx!L!sh^d$jg2z7qDUn*1e%2a%S6M-x>XSjf_o-S{X-Xt+)In<PyyUHO{_ z<mPtNpXp$_G^_v_;hpzl{h8B2se72tORI~G6i$Ak_H%kh#Fww;|Djx{3yK3GRin?G zHyV&y>Qj29(4Yllm<-VraZwnqCAGE-3?k3BF=hktqbj>2%IXklBKVu7_Ymx)2rRtL z#IrEx1-haSKuW|1$1in<fGWc?LgV~#v;Xp2RYE)-Fa2r1T9y7Dv2T0K58};_U$;;~ zfgsC5Nh7smW0VE2?>{J>lc7-211=}yWvcptxml=^vSBPi>->Zd$Ic_t#m3$q4@WAF zE@EoDj<$4YD_-EFSt4CYCi68=X8@R;cX;h`-(NtlYtTv<g&qq9UlE33*UM=h!%hK{ zqo^Z4z$q!~33V?<%l$HXaBzSvx+EiZzWM!xgm$mCwQW$Hy+`D6?|aZ#m}oq-7y=`A zV)mO^?)FkD=Ezwt63aO;ES+3~%40`@@=K&l=kJoi*x%M^Kmk1wA_^dSjIf(F+L$El zUfe;kluP@d=BK+76^&X-W0%|<s@Qb!Mb#RA!|>L4L0<mT<a_lOdr1<=H1q1b+$U+K zuB1)VQR@SPBk2V3Uvu{+Z}z7pAl?VRcbg<j^B$1f8zp9p(;G7Bh&7Fc5umKr+5Q4z z@#&AFK8wT{jyM?Bi_kyB`F1D57D^TSU@K93IYdm<$TrK(%85-X6ik|#!U_+$B9n6c zt~+x3su%7nz9pmQf-*qq^acQ<99Ve&!0Vz0`5%+{k5PWIB?G9*)U&b<AM>+yg2?LY zx#u}X#9XIPHL~9}bMZk+&KxEp?=3_gUbXJlD?3eZid9iO^$FBs5Y4jQUu(J^IsviT z$O-<Y^qxnra@vGv6D=V^_Dq_L>814T?CCwDfral>VS4zv{pb8LMvr&hbb>cS<#LCY zh>LeM^zkWi(+8ft0T<!Hw9@T~K*f$0xNz2#irzrF(DJ;)^Yh|uIQYv8p2~O~lO798 zzl0ecp2*X^laPgnzqywU^&<F0c5I4hBJ8D`c;+~OlItzN?0t23d`YATIB;qLs5P7E zW-*dUJ3H55tztVdUL_8?`-0|1!kIVZ+V>KN*zO(WGI~}|Wn_)|qK|+iXJd6#Id1FJ z)!^IbN%yK%*jB6Di|>py46r`v56IXjGK5XYaIhdakcwBiCT9`oCj|K;pnL!sR_`&K ztBWFkB*%_#PiFom&w4%eiG-e-KiY4LC?ka&8xhH$$5IRv=cT~zriQJEF$%XqZTe~O zDe!2@Xc7gJfRHJ()sziw+=#fMz3C)Qf>NkXf+>Id-fO6S6ePCO#G~jJoHXA~?aYy% z7~d5od5eY-5B-AGZojVNgWFDRO}<XqN0WN<mbd5Ss523mxDu)1Z;gIWTZ(>J0_+s1 z_MF=UYPnwdv7o<IU3JqViuZ8;mmK+j=Ygac^QkY;00M*gmCZ070G+6am5)zIiZB65 zEQ!ys)AIHUFx&l^txr+#)BeoGJ7p**gslqB2`Mth_*I&*@iz`8k<a%Z0>6w#f^CNb zfdCH3q%DphG_D!v!@jxn+`aYTC)%LZw;TXc1j)?lW-Em|Ur~Oy(_BY6aB7g#g`N5< zk`PvJOZg{8R8IWF%+4C>kE`DD=2#O(p~8R@!gz(||60rpX<e-BEBUzpnASf!MR`al z042eV+C_8>tx&~wyxp*(LIKxk;(!+K>u$T>Y?3NHi2iQVL2PBa%P#A&#{s)<Ld1g6 z2<BU`u7P|U9}6>xTp95^C7VT@`Bn1UR#B9?NCh4+xcGKnRtfQ)L#gn^U5{c*NrcTj z#~!EAx)>D3<6PRVthC=W3K@%4C@=6NJ{5$9vAH=-+7&7N3N5bbY>hCKs&yqS?;U!q zc%v>o4da8Bl<3w;Uh2FW?;CJ&2}ZZ^<Z_6huV~S5t4DC2cFi*3SBLyhME!rIsv;GT zXJ>={qYPDS(`^!S4rTPq&KW;FN>ZLIia3D$;g^1?VdJ=pJYm?|+Z5&VUPcK8G&au9 zV$2VPGHh>P;QhgDa$PkXWpFK@8aMAA&hQjv&Hcv)m@flnyS>#r;gr=X<SKH;(;p|O zHb{sOKjG2#@ipNQp1($v+z<(BqpZGWCq(RzNl+KrH)nyjV1r7c4aQ5gxi>!$v6K4o zbittJl)fitrV&2Ao7>xilSLDVlccOb8755we}@MC_5A8s+uJx&FieM{>U3c!#c#kR zLjKq5|7Ks5;<~@vHO0C@%S`5xS~1&V_STzI-wbxBBtxB@RTo`Y|G4&ZSJ4c-77>zA zz_<1ZUwr+EK~XYpLP0=`3D<9{??}?w2*Sn;r!FA={Jk%??Pny`aJXRbXd0WO8afLL z6($aRwM*_7?x#$@(*Qf&)<sWEiLd9n7($!Pt0`?RsZGPq7-pxck1Q6@`dFwOav@i9 zg`S$%qNx<#pmu`n7FW0TY-IXB<R9HdQpx>4X(N-dKV{-=GGV-QhhQRbEyxn+<(+XQ zIy%)1-QmqaD;s!mQ2V%i%cCde26=b{^06%<!B_*2(jw&Htflvo6Bl@=Apd3vnjwj+ z$GPX8>kR)gS5i(3av*Cn7N2@_9qYsW;a4mS!jG$(ZH*rQpgB9M@K-mQQhr3(>i1i7 zfjmB1{lsrmg`G?*wQGTw6KjLx_JiH@oOhNz6M&adVM>E~rLeQ;g*(o>?|Hs8XD8E? zI1Ure#!E?)aRF^2lql8C>U5!|Er`zt!Dv=gLpV@OAGL<HmvFwuvw*wZP}GcJHa{y| zVnb-J6#U{(DhQTHp7P&(u2J%~yZOYz3u1)=q;E?qGn;5TklO!BQU&-Md5xs0(XOf< zPg`3GTU#Aw16;pQQ&O$ce|}z#wco4QdH-+HmxwHHF*;o(o`KOGbKZCJ<1|c9x5;3- z++RudqhkEoM)_}#mgENK?dp+gEh3TT<M*Xg-Cp`OCUbN;5XkSfBm*wom>D9!pr$vq z(#{$W>v}&rl6`?Hw>tr50F#P(P1f=1$YEF%^WLwyPdn&w^!F1Qi|){wNr=7Bx@Ux( zFb^d}-Y-xT>m&eaQTq5q?|UT1l)+3(rSH9SrPCJiy3>@jM2~grQEe?^#sur7moA>r zh0?+I{z)aoP-8-~HT>5EF%fIAMB(7Pni^_`xAQ{VY)}TP^ivqn6=dK|%LHhF!3@5v z9KSxjMc}aZ{epUUnM$r5IZA4jkrA_j?6M;YuMeagmkR0rQhw(QO>e=h(X(v-FDf}T zHL}UfP3nV#25P;M1qwAagaGIw%&VwC>oad$C}r)l>9@>8m#c)E<GVMG_l}b_nVg!E zkkF4=dG07g!!#*qveL~f3PY3*t)Gnq<^VA8>4S0Kj<}Cq_xV5xn{=^p=&zs;94Eg& ztUF_quCg@?2E4@Hv2VPeq7Aq{DGXVs9;L=SueMT%-e38`Q*P*3ex8M>F&3P|m<;_& z)YOZ#Rp@(n^zqrASwC;^5sP1;zaw)r$sX*IEO{f{)JMdgzBTpdbS`*<2S{o_@r(c` zSg`7;njL*v6M`Q94xcicV(BDPi&5+=z5j-T;W|GRdaTEIjKg7rv+n5F<utcP!=3At zgtvEz&Fm@Jy3qD*?ft&StDoh%yhtoMqv10L5)x`B8~y2*;Qg<&_l+#LJF9+QwTUv5 z#!cY9nYut`Yf9iHZ(B!vDS6ZNw)=I8OrWO-_IXw><4B;-LjyaZBowCY(m!DNwN^}x zWE3ih6bPoj*(<|gp9MF@z64Vy^#z39PW@DB4?GhL^&h-kHkW_A4wvqgwbB!lTBu4i zR}cavZd(sTBwJ!Jwnt}>LZ-qxqW1jE*_7PyAOZf*_djFjCB_S1Axh_Rwn~PE2WqmD z-6+S1zcLVeX}hrlW$L_oWbqTzVof6ZKX&1n4l)9qzz%+KqL-os;X*oCyt%g!hcV+f z$A!RIMPW$TzkiyTCyo)U$CUBH)>ayW<3D+2KYsk_#m8S%Q-kA3eq8?wCic@_{0CAw zd4Mzu7Z1;1=ht!xFHh;2qtX}!n74Fb?%!yHsCBFBVlSv3APH$8d!GuL^z5klY<H5) zp@^9g2=jJ*G|AZeS^y(|0azTxH{9Zn(XWkNerZU4i5E4mRUs&=wev*`?rQfL*44Rc zM{bZQE=kE{CCER>iZMRaBeAyqt}EPYmQ<}3s915<{A1{YeXfo50%dlcU9#cgle1Zf zRW!p_Vx)D#hnCiVN-&|P7zuSnH-#6e{3U~(l%?G)#&}uiVzsxz_x9J!#+39)a$U&P z2<Ij|2v5;T%B*;vU!cYG6)l4vs!c-?kdpg*qt3CE=4Ty0w_9%o*LYo?C^2r{b68Ej zdLvjM7`S>-L@AZO;`LV|{Zl(a&=}l<h@vK^06CXAHe@#i#=%uAShFO2FLbH0V+(Or zU^Z7|ju$H7xm=im5;nIss*Gf=pQjRi*2L0pv%4lo=r%Eq{3H(Y1X_SVwY<xD8p)Ac zqy7!ubW<oo;wd1Vvb<3Chz1h>BM8`#kVcMO|7*4^IHOiRa|=&0NLzB&Vo+x@_fK=K z#oKtYTbYyL@^Zm!FwroyC*IH0v%&d&WKM)~^{LS$qR=AyqS(mh>-o>B3d>J~7r1kC z8q`}y>Xl6`I#*tAWShWsnXYE}4BlQ9hmR+XsUU3~M9+O)b7=!fx!wc*{^EZ+E8jCR zGO`Nlfk_R3yktO}WcyKrOx@ihU)aaNrkkFJ&-{!Zb(3RJf98w<lQWNroe@`qtuqhO zptCilb#9*H3-fv_yJZ|#%j*-v$ohC84%_+Cb%(?Zbjr_jhjV52%O0k8cc0(9+1MEV zd^Osd5Ci>Lpp#tBGjvtkq7so7rIcKYSm1LOQkUoVn5C3XI4(&ry~X{<ksa!m`_xw! z7@{{^Sy_qxHzf0=hYL$XZpT+Pv@6Cuj{$;I@7-ux+CL@>iTyuRwB~yXFc#J&&u&+Y zD)d05g$<m%KHpzmH?uVd==fmX4n?86Ua63OgR9CCuYs;~(dtD-`u3yE2+>TPzzAVW zFFy5{d-&hS%C=4FoUSOV59d;qi#6P48@b!(9NNA|UsB(Z61%s*D?mOSt<`i7C7YY$ zg1$F)00#7AGAO%!tDYvp9F|8P5KH^?SbSCD8PZR!{({&CnAOw=gh&i+W9;eU7LUCQ z^?LCP9|(>$D_F_$P2@Dnnm;DOx$GW&xu)*-K&nL_{Tav_t}5q5NGJIPQ)_LW21yW| zT;^bYg^uvVQ;cWjIlrDA8!P6O{rWolFP}zp|DF1|IhT(8pbm7PKFFhieM*w+9_{=l z&SRvpDQ`CRp7-4-s|-5(;NV|S7{}^UqZAyJ5%u+QwA;xBr(TVSheK3(ZOEV;&Zva> ztP0{-Z&XZZ-^TQ5_BC(p<%*)vPr^H@T=>EDBHBI$CU}ydxV9a=sq(Qb+`2@OCN+(x zmDegw&CM%Ue{IH${a2qcpIFhEob?NxG<X{}&x!Rp$@uvjz8eg>y+DrGx8OGf#CQDt zIW?H90=%904XAzN38D>P%`b@3MD}Fk2hq@+Yoy=fC%yj2n=M!UwYH--79@IQVgb31 zZ_Ax`g}j$jwubzpF4~aPi<i%>tuwPnzwK~wkn*C(D$FnL#<cI0$*Y_k`O<VntotXG z|BBzz6k=>L)FZbNS&o=;`iv<4Cy1-Iv1ba`H*Hx<H*9V~jG!&?Ni5_|1TSLre@@^? z3ETM<<}m7o7Th1*Rpi6DL4$^b3*@gmN@#M)>(~}!^e4`YV)4(Ga}i!dyh9_I<=rwr zT0?9<&#p7-Xc<0#bs9*I<V!M_p7K^$(fsP;=GD(U$MzGY{=)5ctbvCP!Ty`n6^kA# zqu*V|;lskt`0_5=JPQeuZ9mRj#irS;xxaoDk&R8%yiF)la9^9|@iD#%>g(5WYMm() zKhE)aU&1mO4$3Nl1)cDD?2|w3y0~2rPeI7WX-GSh7JKkhO51L9Ad>DU05D{`qqVr7 zAXQpUY<TGI?em!p)}+H}SFF!)IIb(6@?8G%!B@05!rkdTC?CzhD{8-q5dqs%XerUD zz-Ss22oM<<r%3YJORsCC6PEYyGxqo-duJn?TCy9bWvc3$jK8DB6ua{b9H(gZGWX`T zEf`g~^}w`BaS0L=$fwX(H(fr~cCj?1?U{5r<|b=u9_<hN{k$W3Ke@AHsdjz%-#S4& z0D>a%I<9N0Kl65AdV)?qZ&zeVAKEdZ$Ie#Zn#HH<sct;bE~e%e8mq&#%YS>KQ&*2L zq3dZ6{^zxTh5fIPi#rLZ(Q$CgXE@xi4yR2^HTq(dSo63Sm-SiJ4yEeH<i=VFdjCyd zEmfBK#*`oT!YMAT{qxiB+|sTzODwHEXb07$8+rJw9}XfOaSARkBX^l5)ngQ-6R%<_ zHwi9HFV**d`Mc&)caIj^tAV#zsTXgon7|~JZg0zfScww`m3QFx-k+$nOL;>miI7K# zdHfQnptNv8q{036MxhUAtl}pF{jq(<q4ffa&Xf!QrW1c<ZB*bP{Y*ZHBK(E-snyEN z)%BZijZnr2LO>Mu>I<?gR8*mnBK6J9`?=+ZN6<_IbV+nm(NP!;*&^A2rn5s^QfP7g zip4d{?GIx6nFS0mK9SHAAH6S!k6_dC!?hsk@zedt)OxSnM~Y#Oi%I4siz82I)Jo3o zrQ<W-$pH=bZzs966B9P;N~BHlhxI&af!$G{YeKwVi98YE6*kKHx5-$PlCCx{y_%P5 zArZi;pW;K5XHRv8Lm%F8QjB(C?xE(r_Cm*ylm*gQ-lk~taRAcA!`0O^?ONFd@q;m? zAG~_%kU{58cIy#l6T5lU{QPCB7kMAiIpZWgz7WD4b2e}JrGHry%SM9It6trZv+uR$ zO|#g1yS9l_WyF>FwgThJ(;-YS{i*@SJ#10C6+<!VzWD8^$`heskLG$6y{YdwB!}fI zTA8EONbkr73+V=i;(cA^$CoKNlTlSPv+ufif}BQWt8{$rbS!75v4&eQBygOu6ysk} zN;G&JffJ-w7AS^>tjs10{}ygcB=wsNCgz&{Wa#^EO)m(ThxeGC3ZDOR`XhR`Oi1+R zXemgeKd+PU+G{)ywO92Q63J)&h8BJAHx_-;)FBhvN8c_BOBUWLR-lnCSjndhBs?nu z)e3GiGxRUyXoq)k)q^52fNklP{HZFNL{_&p3_U<&iP?H_!JJzdCv`8>jGx^%-iPx1 zW1QZ*i}aP&vmBp)7uM_V$M7$j=P>-P4xD?YBXpBEgs-M}!jPst3P0k&ANpU}oD_w| z2;bprH)-I2aVzF*Djn8BOzIZIj<SxVji6v2n^Rfe-4jMcF5H)Ln=KmZ?M>JTKR7hD zp{r4HG?kp(cwN?LWuX@S*AcP1{(gE6o1~DrAJdf+b@c(aH{@jMllqj2VY8IC_sO1^ zfPnOGbf6SGnErFt^EHy#+wxsxY~Pv_kMks5-zaNip`pkiJ4(LYczDy0GxR0Zzt>Ew z4x=>@n$nF*AeNHwX?gw%G@au-V@8sRzt!E4Dij`}4f|bK_|QpA9`>$&vEf%*S=@`B z%Kl=T)!G98VC^TD$`;lu<q!2-W`iG$wnHwJEHpN)qy&lwQ((!Ac~C$~DCIG)W$6*S zNvPy(?o(Z+<DB`120o{#CMy@TQ47D4+dfO!=2&k>1aNij2EC$T4MU%^L^X|8Q|V`m z<Gf#o65U@9W9e1Px^!oTNppJ#Hjzjt+noP!>!`Ce7#IE|C*{pG4eSW}%bwI?Rpb3_ z^mKM-&uNaJ{}g*%vY%|I-Aes<PL)1J=ql9b;_p2IR?piRT(e8Vb6EoNpSN==6SEY| zWlx4Ym*z07wc@1g7%JBgJZ{3yuV51iC;{nyUAbvdX=#waYF4y<8PFV(lg+7iA=+<w zWoG)vsfn;oO3dsX8KO$2(84V`*!208K5ntyRM*Yj<#}F1!+5*$@RGXgC8~DqAq}`- zg+W`pOTo_CT0$@M{>wpG6;h&W-^#5tx2~C8RJ>}Vj(q}?n_c48G4uNPz5gnDNGH@n zM9EOnz-#Z7=f44wSN9ZXQfL#^%!9`(Amz^D)L4`5Ag=F9#<r>@9*^1UUY~Qn%y{ee z#$?0jFMKl#6a~)`M{Mn4q17mpQ@7fr_=mYMS&{)uqq#y%_rL}hK3M=`*-dUWd1=ct zd`xnUrXG9C$aAXl0v8mG?Dn+Ft8r+`OJrN1Y;U*h$GXVP_@jUYbY9@i&tV+`y+PzO zjx8N}hID=+ZbjiV&VtM5B?qzI6>n(Hy*dWj-{sQJE3Db?dHaKFoy;ospK^@4XMaxn z@OSc1{U~!goMmZyezZ3~-;To4x1Wwc+_{CUd}T7=fgj?Wn8^Fyv-NR;kclTxYS^o; zwLnosW7+Q>eWju!Fa#LJ%5lx0W4iy4&1r2t@Rp#~pQ~Z?SQ{%u?HB<R1nhJ<n3-<- z6do>UP9MF;CyQtzW^)Ju_HvDX)b@$jHC?`P;gomXa+?_lM#uOlFMk5}1A&NxoJEsc zVft5tW^Ysz5rHR*!>ai)XJ|y)dndEI($LpuX|sGh6y%9A@Z`KTNAVcCGoj(<q1#UJ zrcSd<rnquhp4*nU3oBXT=BLN@hMmX5L3ok)i3GK3Ry<T93*?>-D=IA7#TKF1+7<6B z%`eRbiwR<PvPH}evYkgXyGXv_#X|GI^K5DJhj`ms+6`EoY5IFPlt(EuFDn`;?Wb$1 z-nVU5k9<0m6CeW698hnU!-o2OT-Llhm5!wdc?7@EGvz-kX-x%gO_z?6kvTT8e#EUb zU(!-u6GU|6NXhxU_924>ALM5+j{$c{;v}}9a0?0!DPF@mq86(fVz=Dre3uW*@{SvJ z+_W1w+QnFZ`3*06_*ib9v3cAFtp`chM8xPc8!V+Tmo)RVE6%tt2XtRrT3WW+UXK3K z{ts~j!?1@>B9lg2TP3-|G)O=L0iQ1bi!#3*gz}5pwxZq=hDKEGC?xn)pEWbP>qCTr zfLCJBvdoj+l*BO7M;!|*z5q65^5vV<IXGln*U$J;MzW6XaM5BP+NT9Ml;HP|Zr-`4 z`Gj`*-ggx)v5z^%$cw0qa~Y1;#Ww0xn52!~@OiM^^(srlOBDT0H12&&D@ntO=-Fpf z!k1gbR^UUCe8y{4rOj~ofkrwa(`%IB!So*NmA|~y;B4-KXi^U3(_jd`upR-~(kI3< zc$=ZE6vcV#U8r9sJ7^X|lI~m8rzS;U#}C#1+P7EA`P@#s@q*_2*<aq4{QS&S7lg|1 zzof(-k>$_*YV9&VNv>#~0rTf(Ho@ek+QH_#6my_*VdoZB+t*jWr<{~%K-Twv9HpL% ziuwL3O(p#R8RujF(XW=txBLgcv@5NqP}O6k{Qe~v1}%~j#sj0G2ZXVuV_Yh+Su63n z!<XVF96UZwq@u(I&oZ`TnjA;DTSfPV5m?60E2-rElQ!X$5W1skbN{74-Aeybsg$Iw z;X*N|v=pn1$DM-i41Fsr>2a^X)=<Cp*FCAvbIWfDMR=04yYbzTB7fzlseeV2IaVIO zQcx_rezwOio&09#H9cOu_n~(?;~lfV7q@3~zQ`#o#QaqzEh!l+m+2L0vsiD?dN*a? zKU*OsZf@fy3*P~(vwYz#)c-Vj7&2(p_cBOb1Wz7H=@LF6Tx&7m3RxX%TG92wE<hwU z*8oHzFFZPP7i7)+T6aCZyc|S&D>C@>sLcH)Ff9JGPfa6{we_ZomBnm4FB{mu_&E@O zU~oIAd3dU?=y7RM3=LU3Q$EnKoJ8>DHKiX$As_auuCDISbYF(g6>?Q^%Hn9HVyGjZ zAi0_Hy_(w@+@E6fkwEgl>B^Qg*8_-c!8*-;RI+tDmOBz>YH?h5gs>3?EN_U|a6#8S ztAONG)6#NT9{XKuKv_59Z9XB%+kz^cBd4&~_T`onN)|KTby2%sj43kDTl16*_bmN? z{}MSdQd*JeG)r(CYP?9<NiGfA5;=YK-}X3LZEr{q!!ByLxVZ;hXCBddck)})sVDhM zrdS#pa?f}La1Y|*6FBuaIFTqvitxYkahhrNeB-dKmbKjF(JNn!gjzp#zGh7V#?*4h z^&2GgOiWxpa&FS_zF51KFg8#NtB#-){5H7*-Ba%UN4fg5<JO0Njtf8N%~!3fKSw7+ zeOa|~78n`yt~=#7--nhK@m>qKHN!%-QheqzA<{S#;qzm^U)guri~kk%s2u>m&2hf9 z5S5J1`($L#Ya8Vq(!9`!YNvtT!X0+&$bJ7eWo~wMHq-;d+{Y%m!AA1UuEX0OWq;Is z@}(Wm{`RK{FXLe@FW{TT+|&xzEzr~Avc&J*GIl%B-MiTT>3W});z`G-(I?SI`b>@g z!IQ&z`^;%vrofG8I~{By2~~%lXsK<T-`)8VH|YVp9;u12sq|nxjoh`c%94;Eok@M1 z&#((<nH#BBKYwfUqUM5W@Vxu`>!C*1^V6?hbWM@(dwcNVS!O-}^_5$IKr$MEQ~tws zo1GwZj*YWZMBcyCu=*&uPA6qZSh4hZt=?W2OVaI^uTQxX-Nr`aL0K}hF68`K;X_AU zKb33xNv?%Jr+x-Wh|lk5CT(?ZCbT$3bEjoQd~1a32=`eg4#$U?tq9xKXQJJdB6oik zeV?v&vY(F}B+R@a_-t@;4xQW$CExxzH5$79UByL7O=<saoN(rk_sYkMDcvHh_&(`f zw4$d_PHt`A_5A#W$l7n#A8sqHib`p>?29?adjnp2y-RSIN|ur8kBPm#nH+u(IF+B3 z1=g)y_%|ZC>MPN5HmK@d>I!t0ZM10w=KtLCIEMRHn`w^+#@mUhS1#B!k*`H!n17BW zzIV<38<)h*S7^6J^2>TC<?DSbON6yT(gu<*i|W7WP*)Hqh5Qk})5S#zmHimkaKBA5 z3byDd6u0cqMiKrIYj@AD=O*SU^h^*XH4ysyqDhtmpNv#ox3g7{w)5S6a^t7hdd$bz zN`$5~RHRXLK9nwhC4VIN+_0LHtwbTDkx%?~vuY$;G_aOv`SPy1bX<IewNWD=rAUA& z-L+|Ie~GJUb1+rdQRp%;OI7#a0b8$(MKzrA7=eXSv46RRv{;{ookB^8?G=kQF9n$p zo*hd%D+`st!f%lYd`tFI<HU$5U1mlz--VF?cibKlzl?Kr0#iox_nr2B&fl7!p9DUP zrkxqd44|d)ok51Xed+ht&F9j$3Kw0ST$Zf{>Oy!+rjBa{MBMdhyfeiFwuT4(=H-be zbObWTuI%5p1)k^v(<y9tLLQGba)-}!Nw-!{4-p7fsNLe2Z$yF03^yQxwH1Ta%Q^N- z-2#*JQj(?uNl^4@KS_%@R7813{mHgb>Rb;IVdH>}Cpw6=9d>sBWZ<cswi@IhZNx6K zwApc7AuXTVWjkB6`83{9A?&>mhDw+8YANXGy5^5}p5x;XRl<HUTSyz>TgD3<=HU^N zs2qbh=aGty?+o1QqB_kih7Pwt=fz<%Z2$QY?)m#$-Sf@)#Z?v&Md4f8TVRQbW0l?H zZ%2RJr;gM5x1M7$kv$Gey-J3ulpKs^dRMso6xI=&a~L8$uGUjEsz(gTbtNMSLZw}e zZxH8H2)2UX)EMFRG2}|=rY@2rQ<885!v?FH%urfO-}vjRYuF|3WViiI9hrL>wND}! z{;1V7d~8<J)N~{4vYL9je>_pV^QsF}o<&*c*mCssoX;Pu5&2Sd_Bv)+SXqdY_->j1 z;m9;Gwy|uyJ|lo2jI%qCIY!s7;eM~Q5G?o{@Lf~bteb6D_Zv{H>Xgqoi&RT@38SJu z5vu>mzPwc@WJkP5&W2ha_yOn}H%;*NIj{P{%=1#cF2had-BszDdor13VPa!MCZTeR zPgM1O?<e_i#NdqyTJVwE*}5Wj=S}}GQ`|;O+be%l&cJ=Fh`@c_svqWgC+Dph%&$_> z>0Q-iDq3VKZRAbr>U)lAK8opP9a<4nH|a_SnI(7L%MT8<w2uCe_(CQo-y2zRCv0UA z_ZkIX0{X|t1)sE7QA30`oavrCU>AZfcq2XRQ;2k!6|psuCH13%bei>VO;dIwvPf!M z*C#!$Ae5hq@?3RO@qU|&-&2x8##`%nFoX+JYU>1^5py?)rX*j9l^Z^jjUSdB96?B8 z5CYguS%O5Kezp#dND-SSsVd5gZGY!OhV+mpeVY>P_MG&>8b~9HKoxG|K7kAzVm(*b zuNZ*E{V~o2D_DUKO*K+R^O08BXm(9usM^ri7OKq*re^c~4NmN!&ljBH&S)RWY&@Ks zxP<@Luv6COXY#3;lL#U}1&0mcTMLTVEX*AVs8bZf*c(@9{x^7*ea-jz#!i}(%>BqP zR{y~6fzd*z&9okD>=s&&)g<M<{B@`d?R<P>`+S@K6M63jL37O+fBDHun>decdW>xB zw%_M{HL|Zfz+|nv%O*`O&50R)igRoA!_rRtPrWLLZ;?%T+;c5UT;`EFO2MGneIaN= z><`(p^C)pXkMQA@Apcpn02U)wn|6%H%Hf@tzVQ7oV*1|;#*4aV{zK^;EYW8c*^wJ1 zZ3WRiTUH+yH<SOj*ptgife&Kd+G<Ma6Fu}>p*C!KKH1oxPfw(N>%NhTfV?ix(&l$E zksxp-rY{2E+ThMF*OW?6^5#AsEp}%0UZT@wE!n+Ng_s*-WsP3^dX;vfYn`$W^`2Wj zc$xj$OP9HLNO6c<W(hO&taUyN{x%PaqVGs$Z;guF4gGGjr8QeDpf*1|D;O|6rnO`8 ztIyU(HUBi*FYU)^8aUnyGn^CQL6AdWPM6kKC0~ojPm0sEU&cej@}RRR2N9AL&Kn{U znq+GCCD&j>m80FgzXfnks((a+y%*+%JHGwN{Xr`V7CLC0+L-dk8%e?tITZX+QQ?Zk z;j)P4Tb-ZpWQf(g)Pxs2fxgfi2ICM;#QG75bs2bF?%xo*%$<5-k^x3D*1}PH3Uh85 zw)Gg$91v6{z5d6f%^_Usjalu~;<X-jORf~Eh|PWOxSbZ0qqS0agKYFqImx&jHK6re zfzaYN<dSt_Gd4j<@Ocj7C&__7)tY?1tc*-f-X`*7G^!f98_Ewv)2a(Am|h8Mzk|S1 z`MPT`d)O)ZG!iHNN~6W~iUQit1%788R_BHb_4;kTDZiOAUY}kmRaFVPl&UBh<U$=L z>*>V^(gsbVnuz9myKkdO;OnW2q2yXw&%!ms%HOVO^)Agka;xSRcQ>%X9sj$DuFzbk zG!@3Ay^YO;ljOt0vg(@rx07JeB7(S`Ea#5dn1R=9*q`OF5Jht+FYTmAfr>|fr{SfA z!w#qZ+;wD!PVxOHkPIo;sc;4#^CT;u*4Sq_bg)p;wb;`yrcB|WwCVQz@^D&Xrz`dB zv?rpvhWiF}xu|Ye50U^2=bUZ4B*>FpJ|2p%L4D<3Qqsa<ovc3|IMWqT^22r6!n0+| zd2TjYu=m)-`<m#j^-NhZhKLVqS;qalhr--4@u<i>U+y)25%|G+O~lZ2oEMagrgm-G z_2(G;@EUV^>Imm@4aiVNe((%WU3-VxM!MG4wxtnXr=CceIh<rO>cYN|$e0s1E{=nD z7`X*cmH*0jkP==d?wm$GzjoP#t*oZ|cd`u+oYzpMv-%(rQ1vMaL8>JR2J@3LeFm29 zt=CGt3Am*1X9f{2S!t8b*atNz0-5-i*h<n#EdXfTJD4YvlBO=GI#lCbhe!WgzdzQ* zik)6B1G##AtLQn87SOT_B_9)tIMnEL6{TZcM8m*!YB<$*1Bn~j-1xu)@4N04Mw4}q zxBL42YjNOHIordIPn9i8=LEGeldc&1UV@Y*8cSOrYY6ek`9G^6b8#PCG>lJ8m-R)4 zvaq%t##dI7DMTp~v^*=MG}TIfD-V@_w#EM0b8&H8MOva+9;8c@|NSBP*Dt2FmK!?~ z??Z0swZ+>@OhaRcgG-X@am|e0xI)Y?aI^PcoQ6*iQrdHayKnlJf%XF$xsn<eYFoqy z;v5rPAmgI2LCfauQ&=gdj?#iZ69g)(#&KHyUCt*h`cC4f#TtBe_P)|VFmzn{Dml5O zwQf_uy%mBW!zV6?Ojp;K#CUSib6FV4^ct^G-tu0Q>i_;1z(!jc!&lmgA6EWy@`Mxe zKVQAhI#?uq?~*#4{Y<6jHXKLkd2>4Pv;HPk;kF)&ARzsnzVIVg#YQs4M26<2MElAk z>VW<!adU0?Hc%;08A0G%di=={rll(0vbTe<VxFqBP>uSv8lS$+#gIO{gSU!BP<t-k zcRpFh09G0fPAz=Ck&FGtK8Gk-lD6Hfk5)Qv<o556ZF52s$$y~5d$4>>Ui5fle>00c zAt@bfadKE!Y?aMq@DOI-mY1hS;<d|O^lK5Fw+|uHcD?4iB`*KnE@_|jO&+29h9t(r zJvP~b*mxXoxhT3{&Cu1+Jv<4b->t_VMjmR)fJ9n0lkQ2QGg5<;p+`%mH5X2(-TPCk zH-09qBp=}r-zVo=$Jbft6mjtxblLP{Ced3iH8vpg_~Sp>js&=k9$<&IX9=AQgyYoR z>m4OBCOZwNlxd&og#Q{ahY=<CiSs7fw_GBM_P7iT4JCM<Z&Qvny{+KW=cHJA*XQwR zIQ5*0_LEtpl>G79ba?@(hSSR0SdFz+yq%q=fcy9(gNv7q46r@TW+rX)fHZz$_wY9+ z`aS-~fkpzdva9{qY=NtxKS2ltL(>`5JttYr{DeN-C^dI?tZWC4rE2Oulc9v6sLj(x z@cZ^gd*g)09yVvMewM7d;N+3Sm@7g4z-HH2z0BrM?eJIppKVv3nClkFDJ3JtKa5WG zE8#vf^fMeC1HR>+kR*UW``c8?=ZO?u&*{pDD<G@NSis#a27%K{@aje7XaQfod^f2w z2fGBH?xptn)4>5Gu1~R`#SZ2naWG2C4s@P(8|Vks{WbtTh!DVJ_{3<aL6s0LS79RF zzV$w|?W^GsAjtd5>gK0L!hrF9=64%E$L57})gnW9UHG>Al4$fh7K=}fwCCP5tbTdD zm6xD!8Fe|x$olTRMP6nU(JjvOL+D!r(iv{<z5F4rnUH!x90~X*#L!~u-|i=P%0yB6 zK)DCNr5^u!y@0T?Um8Pwl2KSzBAJN&`gib6G)|I*c)cZ=+ZD1M;~VB<KN5PzlM!RN zkwsgH5&O@D#i$IN9QB;Blq$#wd4<1N?I}{SbTdZa*4(1v6~9B0WF}k~+*<h);eAvF z+Os<IXP{5cJu+?Ds{nd0^vuLez*aG<PoK4w)n;zY^2s!ffq+~Sq0ql3H-Z~dIxmyT z9Xd$+Oc5848+s0qcvE2iC0@n<q4pIrGcNNsqVy+{vPR!Ov>n+Cg14N3i+$De|Bt4x z3TUehw#5nV?(SOLlH!yWx8m;Z1&VudcXxLw?(W513KVyT-2CU9`;h1C?94Z_W@fGR z!7%CiB&-mlKQBSxjTWEX3r=A4ZrpeXTp(synI{@B#S9?ZjWjHa4rA5je9`XSNfo=n zqqV<9MIo>}>Z_O?C4buWy9NGGCWlU5jjLZ$fBxr%=D_cANFyX$)AON$>nEc2etq7$ z4VqMcqfiJ#eNH$yj5(oa@ra^PQr=CrT8KK06{RDGR(~hWH^@dg^>Z!QK|o5&@vi{S z&0}AEZFu+ICg~Ag>h~a~e3BLvUJ45Pr2nu)L+6KALFCQqIqS4*bi-u2@q;0YeT7d7 zsZ5AUK6ML<=0F3Jkd343AOX^UOvF)T<M>m>ukM$}gPVNl@y_+W=WsW7Pt;Zx{EV>o zk7`82$;^FC4!kes$o><(DtyU$)<RL6jfBG<vpmbhb&(^gZ*Wy#eVR%`cUO|22j{f} zHhhyIAhD14Pe(d9z>OG)Co%NT(^#^&j_bHZLUrJYi4B8#kG66umERpHmHW6cfsE)( z_R&SY>MQOS0v$!Rqx~z=YM4tk)!81@-F@%X`E{Gby-hCvtRHN%{WA3(@cT_fVGR}L zT3me2x3io5Sq>ACo6F9Wb5SUkOrXx7#Ua-3_vzc4lOxK&BV1}3mQtc_a<Xe{1n~~s z@ya$-d2MZf)y)=MTn06SK}PD_gB%6!MJZ0}FevYsv+}nrF!AcsmG@GQNMaY!<YvZb z_fu1QL&S*Zmv?a~Zig%8LHgW#>P97XYb(ME4ExTbC-zo1^{6a1)y1~H{xB|qRy(Sq z#`}0HbZnbr^v@fC)?nhoHZz=LT-i=M_Doaso{^nB-up6}=DNmzdNnr3gI?%03N`|L zS@KwYQN#M-cFXW(Q?kt_DIN|Ti%?9&^G(LP<L|pC=M51h|Kgj8fH^2~j|w!)rMyn# zv8l%71!k>pF^yc;V}KM=wARWV%DlAp;~?(+zBnWZk1D)M>#;w%Xow%Bz+6Jxs}uJs zTvOlSJGCNj7t{UEK}Uh6Cb1DV*(;*B-6i9(mb={a*MmgG#1Zn>N>uVyQH>%+0fZb^ z0fezK<&p<#UZwRX_u@TJTM<1p1O`g^UsIGndw{BO5_{vQaIQD@FQ__Oz>zG&4wp`x z^c`ZbMN{V&`bMnu5{ML%M5GlhG8ZpQ-KgTcyexkkXKLazh!2S{6ryBksFd`&NNarf z`>s}ka1-<mbVmbV!wv<K`mVJG{LJcYOnUT$-<X3CZL%EX&WPe@&htrdp9kbk=g%Cz z>_(Ub<%gftbD4BAUztRo5T3nZLMI3E=D>^npxBeitEsWERS#||F#fg-{hyLDEd^w5 zPTq-&06%I`uO)^iKv)PX1}nMDw#gDTWO<b{V0uOjomzQIOV%u^My#GJ?zUL5O+A4( zZx3-F34*Be)m|d;H8g86)%SOjHFa;sO00i0U8|ShsfF=qdjUUD24QbN5!sQEVk45F zTXXRF(O$Uf(Qo<0p!RUO*Ao(dX(M+Ue5M*3lODzl*K|@eDYD>*$G*yiR&EAnDVXO) zCDGN=1ZZI&PrZLxxKlJ{3Il6Fp3l|cxK{AJmd{ehM#e7IY(#Fiv^0FkZbawb7lW#> zFZqzvR3HaxH>&=AE8K?Nxi@jqcoAe<U!4^tl9*ro@7tvX0_{jbM0)4!OJqq-{9W}t zAhr%K%=MpeHH(?Iw<hOF*+fh#O|7sF>%;NP7gTZ|k*%{}bH<P&RFZr7yiSoOIP`b- zK?Ed;JiKC9$c)G7pYP&Opw54CsxJ7vefTZJ>x6eDZYi|*hL4CY6UOLjc8q3Gg_Ncj zk?k_(=9(|{l`7}XR1i&$QVpC=s~Bu+!a4Vk8?fATNkoE1CI_OTKCQmDqTkpB_Np>X zVXc(t;E^TKeBL8~``;;bVit)xTf)bkYJt7|IvoVEBFO6bta*+RW-(-5=rCTa9siqI zjfJ{X2Ig@al^>v`NYvI9h>;_P1OJOuIyBhXeLqD;Xr$mThNE$E(AN~4V#Mq{KH@vE z9%b3+b(_0Hm|d4@Vz8t|trLwcRCb5!M^8~0$@!M&Yrwp^9SSR-JA|l_!Y_Fh*6|RM zCd&pgdbr89A(P-+>*{h|zDVAY&Nsd7M$P-s6_|G^Z$7bOyWrEs5m5|}3k!<x)G{+4 zF{aV%XCmeY($doPsAOIZ@p=uln1C`{S!VZ0`4dc~dLY@a_07mvu+#|qf3h)p`na4N zIzhqDT}1d2XEopzuBAQ;|K4@l7hi1ONGATueno}rc0H>c2??FJX&wd(9vC^C1-&6f zQG0q$Xm#G75KgNk9!C?ex%~BdBdv+YC6$W%KVg&<@%Yp{C(5BEiN=Jk&1uL2eTJut z#d>o&K|hI2_3||Zcam^bvoU-+n#hH``_mJ#7_}#CXSfq3EL@slDnE}alCE++@h3yq zfm7;!hzkox!0tdPWoFeU5a`zsaVwN7-6Z5?UcXu+#m&vaKEMujgKncXX<%N+J{?l0 z^POu#zzjmxK7_W47XV@Zes8}~fKc`Fwbtx7Qxuao&{PA_DaKJ{$@f6mPE1XA_Wn)3 zMl5{J9-oSb{z=H$gzC|6y5}V21U(VMrp^`4@YeCW<DmycB8&Nl4zR?p^HD8zZgRki zRpv6>Y!;oBOM{3g*){V>nwD*zIdfQj5W=Mv3UbZAEIxp&HxnMw7axwl{VOO)cYeF} zi<_Lx@8aEc!VVXL)_WX3oKnnF7%l8x`_yhz`eJ_0LrDAai!FF;K$ne6ug<x7Cp0mm z_T6GZMrBL?Z*0M~-IR5_6Hru78hn}hVn^~NsGy*b(`N1UOpWnp-M0gX0^#2OMKJzx zm}Tl~UQ0_yD#AW+(x;V?1j&_w)Ajhk{|N(Y>Ywu>+xZrEi?<jooW7z?p<q>Q3t}X4 z#a>{(DZ%(8DFTgY3ohOed$I-3y~TEfyx~mFBPYp?O{b^;K(h)f93`EurKDz!-0B@J zpJmDg#}#3ac(8fQTVV4dwB;t;;iL|wWMVLLn#eJfb=p_b2L-hwu8}e{pT|w+1Ns;Z z?X|e3hGjpy9KO_%Hf|5HyPBs1`pH7Ui#&34om8ZUAO70j`vj`e_)f%z#myWh|1Y!b zydG!?^|f|@nZL?v!Ct~`-{D~mA}pfBhdVjxzFI_qu$xch)0A$O1QTK?-I|L+l-}4! z60lIX&?pM0*F{Ib`Dw8prp?|X{v*sHR#l{KkIxAy3x3J>hC$z1t+<T`)B6;zwNQ~b z(P=~L_IZ-V-IrEYcm*tgzqK5H!A&X{uGAG`g0a%n@!<gkLptuc(A<!b^iRM$-(7Mj zGW(2yZxybuPlcB5L-?H<^uPTP11i<hElLd=U2HcgT7Hf%VldVSrq4I)rjU2}v)MWl z_|{<RkOM$u-S*2D!&CS}6o*$Mfm1mCY;2+vA~JvFusW}?T3WN4HJL4-Rr2ux<CJt5 z>XKetJ?>-HdKCPVjDs_!w?;1tmMf*~*{d=vsKqbkqoJX!tClDCCMg^aJF7Pz$`$t- zNsE;No;310V7in+Z$X2Q*IiqpW=5UP`!qh(^}DuL14KlUD?qwCRY1rsAWRi70N1(2 zHyA7uD8#E+=w8bHTv;;FaS6#*uG`_xUJ=E|uN6<ZdfP3kz6L#@i%LKJkGZ>?KR+_j z2$)SQyJrAn<eJzT+a|+VI2zu3G3d|?*p+A7+oEEWLNxWuo>*5NJu1A3?&sP#9LfUh zkYR8doZ5SAOUuP4O3&8q3&*x8F4%&*`s0@{qZ7QQA4?B3MF&$Zbk*#@<7Zin!-J@J z9ECI*c=*0XEU7gPND*^HQd8>;ZHw!;%KeQs94_;5&ymXWcx_w5Z5EQi#-b}|Jl9Pw zEtAO4`7<}(Wc~J&*~w)$6Z|KckU!NK&xG8AX`qKV1k*w>q&jA=Tu<zeBER?8pRX-> z3MwAUlfjyC$Pgnd?7G$YqB>5_eb$^OO#|0CWhZd#h9K--o%M`m!F~2@Pi&^CRZzh% zg_OYJ5qU`M6w8otWoBd3G@pEmhHRMsWMV-dNQGjRRqGv963nl(siKq4{x|n)a4@(G zS#4+<H6}yFSves4K5xhUPg8b5xDw3MH4vH|xkkaorf2Hxb&XeBQ1ICE`t)UZ4)F^J zFU$6NVr%RvHURtYJuRj%j6~?y!C&)vPKOmxMjNRLum{~#K!H4)mmcbP#;y{jFZekO zM0hkb?IKk+YIF4wIJF65iGJ+(@{dVnMs9<{w45Lqg`@wOj<@BpKD^6<U$^bsEa4wV z3ZUnKwo?UTZIz0tSgDg|jo0B=7I!~#7)<01!M+>~gG26LWAI5}J{l>k2XAf`>xt3| zA?!NTG?2qJ1r7r`_Yg^^0<5G}#dp@&utF}FWjc!iQzow?7Gs+jyXv5PiJ5-eQPG4P zF-Fq()f6~Xx}E#qrsr#`|EW022&iv%!|ZP6WbC@_zyS?xS<u%<bIT8-@-eqW)Z>)@ z+KWn$<gBc=5x&gWv{C56FoS+PUhIT=xXPGc&e7+=%nGNp@G&Gf+McZZ&4BEj58wia z{21dIz7o1}?X^cmCx-h=@xvbUQEB=DwnH_=|Bb9=P)U`Y$G5s?STb@G#zOK^V6DEa z{l$|f>I|1$ULr(i_X{FQ?_zvdpoXk_4i+^xH+2abuzBztN&~sJlxnna96ncLwwXV3 zjK7Z?a}7<9_|>cUX4-3wJu~b8VGGQGo%+NXV?RpE)f=#sw#22R0-TcD+y9MHS}|GM zC2tSw9N8m;V-M$)=tZ~3t#!k>oDtC%JGOtRMdon%X7E$mOJT}?GV58GNa$`-wqwVQ z)({YIa$;6S;EaPBE85^utEulb_V*Wta`pl|i_c(C3<Zs<?=4!rnMd&sxnKf#W`Kd{ zokc5-^Cike6oy<$37BW|=jKLTPj~}jlEBFu&=)j9BVl3aqpz%N9<~8=>$tZf6Ta3$ zB&4jM50?tUDzFM@$IF-h7$;J>o3koApSxGW#OQk;QIi^@SNs6}L2bu2Ml3tnPFknM zREJ?>OYSZRfuQl?a=BE<q<U<rntW`Qx-tYu-3TY|mQ<bPpc{p@i;={^?xv{7R2oJ$ z+l#t_)sdGs07z6WL;Zv2L9wT@X{|jGLH1@t&pP1@PC6<rk^m5J4~+bUfhQe`lG5Cz zcQ~9VF<W|%7^?RiD0N3cqv_~zi^La!vnbv<zPcGaQKo!E!+yXnCvOtKjkA@Lge3=o zvHCwTeCrGX<Wok4FPr41oYfR!6Dy4if@VmSDOE3fK*L|NhX_#VyVJAaxvr-oKs<hr zmBYi<<<-1304un?D{Kgzn-g${4^~!l9feZX>jMZ@Hmif32{(z?nmU4-4VE!bUw2h| zAS7@k9I9CA(YjF&WPCXnr;xUmW@=2=h85dG?@Apd&P7cQ)2WPC$zs{R;xN~gdC2Z| z>kp_O2p&(|E$2F!EK-OHsX+{E__doewN&K&bUSH~Vlv9aDAvXS9ioU8?{R)J-XuqE zrPT$L;^9-6ii#IZ5aU^ILq|I<&(E(+Y~UU^!S!$(XM;R&8RqO&mA<YBf<B?|cUn#r zbMp*Svtl<AoLJ_^onEF*^kK_VO<2ij@N6B$+wEZx%9g8eroDe0fnng!1~Zk)L)~rv zDuznatDUhj1^ez*gs?)%7~u=w?>!$z29bXV>kl|-F+q71C#r+NYU52-t&t{|m`p!j zNz29VO{`{4KZY<JsW4n=@|tz%_IvB>N7_u-VZ0wai>+i5o06^_gg1l}dFOOjNi#DB z`MuZk5%+;YvX0+i4jJ;IFmOpwF`L@jK7AD4lIo}AoVlJH36ZX4W+R$3VKExT2KWJw z3tkXIKXx7v!`=-eQ_9g2jH-_!)U(KAQJ9{WcF-KuV_7EaZR(3A=47@RO-A&M(E}Kz z#<4AJS#O6z{w&#J>X`6F#eGju78Ei2Az9$*wYXD;ClD=3u8$)N`f<o-UwDlrTXIQb z6*N!Bwk^Vy{AF!zt;Um_H*?~J;^($R4juF-ETwy(E_)AhL0AY2>zQ-UO)A0mrqfvX zVlDI@$3YOh=8~w$^*BG<?{hXc-Q}({qbKo-N)JW@Zg5G-D!K53vEX6a`VKq3<#W2U zEONpf97L1=!LaC)r0lrz9p8|FV(QNPU*){IqE$Ex6B;R`>!UP)?Ha9?Q&qBr?Aw!b zPKqNGlns7RUXXs@Nr(h??dm^ah9D9ui90TH9-=QnY7(3u`ta@RB*Y#}r+$f^6}Cu1 zpmNa(*zk?)Y^An#JG!axH13AQa_lk2+)0N3Z%VQ&4%iE}v)nM<>7~s>eVEG31vTG2 z%O1?KLy+s2U53(dgrH#gZ3uPxZfXS8LS`)<`Io#WoRati7c+mSO~J+*f8t*Y2hLaZ zq<}_mY|b>0)Afo!`NBeVLgu=~#=|D3q_OrN4r<tgco~J~ja^0og@q~-;U-(V>Vfsm zWZ74<X^Q;GNh3%$2?_bSzv?v87K0H0ksA!zj@EKXX|oV@$jJT^ZX8@Pt%3UU6o({M z(e+!FRY4;GLt%teX0dh&APj@p7A(~p(Cbn;pc}%ge(&NJ7Md-r8Cr}(@}G4`EXZgV zfO~pQZdp^r#EL5RI!9M7hS%)Jez6w&p&<!L%clW|G3&e60<jRnPpy0S>efXdRs(VT zBcc#`x^?wNU5w!2GDNe)|Ba3yL{1dO&CTFxPn?*zC9)d*o((hW0+a;{@mY%zm4Lyx z2`>jEl{Cx#4pDCy=weDYx$W<wJvo2htW6>r+3L#tKi(W?=zoQ@tKHz9@Sc4#P|}v* z?znP$6B_Mq5NF#m9SELuShYpR>)m_3pL2^<Q@2Y9LY(+UL{KoF7){|90-<9CX;mL0 zznwnX*MC;Qa%{K$4~1UAf}xSU*NTsWIOHD=Bjkz;vBqaPKLr}QzCtLQPd;q!zXU=v zIDY*HhV(j?dYg%dqW<%(qJ<0C+5^5Z=RH<H)Zs{MY&ZA%B!oW3AOzVkRtUfOukidl zH+%oUPm9B=wKZo#YLaiSf7SmNWm3!o2t3OAc##IP{Fm41Aw)%1ofZ+c-<qj!>I-JG zM-1Hi3%kPksEs^rhC?pO7>46RN%D>`?o%{DL|`mZ1qzu@(nb#haRxhL2uQbTB>e){ zQ1k6c$#d@&I0z6?Dl`C>fC7uMc*;CJFi;GVHER2WUu=Xu3ObEembYnQvp#AkC!=t% z2pYO14i68B%d2!lEZ=&augL87+NZ~e_J5%;F$yr}7(3cx$3;din?hzXJK!&crYX@W zdmc0RHu8RAnPV8!slNQv)_go?pT7n7Mo~NAPw)(2gk2l0U_@9oMRrq~yY}2b=wonN zV44>{MA87bDC)RWel1QS%-QS;-<nQ8@$Z}{H)gFmK>DN>SUZ#B%i)doQoB3P1Fc^} zANFB}<4;ObC_36giRNxu+(T>jGl@i>E>d;}W_UwHaH-vpNJZK-Qdrxexzzp|0|jKi zYzxWE@4r0&w>%{99HNd260CXnH8c4J3`AC4qrN>>qGRXR+Koyl@{RgL;Hg1*jm)oT zp=q9M{vU@bt2Z1=H4eah72*emKHkI|_<GE{87pZqqXa7%@id5IP!8Uwn8|`?Is1$? zrAaDTOuE_O(e-CB{dNMCDa34Vu+sGAAeYJ;O0_`xNHMxARJ`}!M(oaArn14&CxgS% zg2(BEv-1Geiu}7|R6EkwJ%Y4Y{?Ck~EfZmLr)K_cbv>n|Hkk+O0>Zda-wZ~$M-Cs| zgzS&rEI?`R%=WDTu!#`UE48<<^>frVzCozF=~K?{&P=}Gf{%ThHfU!pSTgqS%O#~2 zJ`~cjr7~9ErqBmNY^BlQTt3n;$h`Q>B`)Ic?gFPtAmXcVUg+9oI-BK>&+ccQ`aHE# zH>u<t@Dmo1o)Vej(aoK6#4{SX1e*Ljyti{p?#HL{U6Q-lvW9BE4f?`+7ewY!0w(7U zE8g=5C6N_GIw3;}{pqJzc47!_y2SzDgN^DfW&23BY+0f(gpK=ZVp@i&h$vC+;FnGx z#Y6>ljUc<OCcl;Q^wRIFOA@tMbDvcgd<R3ShB3VJlJV`qbC$Wez&-+kgiK*E;>|`y zY(RQ%3>k{){fRf0-z@caoe}wKM2liIQB)aqtI?_``!X1a3Cg3ZyUh}HWj4&P(Z13- zhRf2=NZ6RTFjv$uZ28b<^R@SdOJ=k2iRIubod4WP2U9`r50td($UgO4fSR0fK!o8l z5^DSvYbqxq(il{`Y{%%r#awPr$<05!<ul)xmQ&_jv40Roj$v^kWf1V~s+=Y60^f<s z>X)zrOOG`kf$`EL_rHH3qjC4bMBE2a=<)6J0d1INHTv`uDVtG*6<rD4Ju3tR#<gU) z1mBHCEnfnj%*rZjYbA-VYtpr2M=lo~t3Ic>iWr<@nD*{rlLtg`9ag^Q^FSKKwN?if z-(2BD?a)$6o&WOl|AcDLuwQV3FMnH|SN#H0Dm>6)a&FdOz;bP(F4Q;3$ifqIwZ7iD z`ZhIL@e)(do%(k-YO|_`Hjg}!_&J8x(65q=WqR^jnbv}P^tp{KpeLB??&aZ&pn62j zeC)+)qeA4bk$e#!AJ}g}`D)3uu|g*V5c;Kdi~TzZxpZe=#x)_5WHtyw_%)d8FITNi z6;HpIh*S4|!?q$)C%>=X7X-b0B($MkI`j7JRQ}^@aqjC+)gDck{H>Q&Buta$#eXJT zS)Bhc;&{9Gp%nRqnD^;}=1gY-MV7J#E)*S&9IULHg;%euU&}%GULG!deAFL@hM8Hy zW(hR)C%wQ{{q&o$wQB4n>^mEI4}YjNIBxkapWl@DP`*l2Bht*QrfqwASdN#jXd(bs zN9uit0G~X-svwTuRBf3{M><BtprKI)!iIG5VNx;S<3yx}nB|_IPdSdfRFgUl{^j<% zgXgxqmP^pkp+p<08U_~}=*$zl?Csf?bWeE6r{tS|w&la;Q1`R5%Fsk8d=X5&0+_~3 z;!RcfzD_ptP$wZ0u?iEEY@bU)4x20hhK_FkDuGq$PeDO|r}95lZSA{#h#_NffGEbB z?5N_KOPGE;(I+z4m@f<FJmK#R+H(!RWqEndq*AJyuiW@cGY9fM%S<3KOa@SKTkj06 z!Xin^f<#FjAka8!w>|B%tNe6{oXXQ~>d+o~b+1zJz%az|Y08&}pW+x*<g#4ACcqVo z$jAZvG@mw153zlbBOt2&(4)uN#-&f#NVVs89rS;o2p~Wz=qbjAib)UL4){$;1w2I5 z^?T|CKe!v`jSM89mbZU!%|b8UuZ3&Kz3%$fqyO{zyXVRWsq0w^F6Qb^0$(t*KD?Qj z1m^&75HdD3y^{Jp6NCYb2O$NkWk)5nTF7Zj7+!rJgiYHKfBjfN%&P=JD6Cyw)-wWe z>^!bLC&pMODZIkp&KLpa!$Ux*pUXA8X=p}E{GYZM#w8)t>9Qw`R2D~Rp_`}J8s)34 zTkOuasBxX?I#kTcp$FDM>r-sD8#cC9y((~rrmgC4=TUEwlc@<}E8D8b&V5FGtN#VN zv=Fe1J*2$C3$fMBt9L?#JfeTctaN>!UOAx@$|vTqdq1PVCMD>{dVRGe7I5!v&VDAZ zi-DqGVOOoSA8y@)%u3p)Z^S<6K!>+O_wq)Uqly}`|5<kA_$%K716e9C#3L_i5rO=5 zg+@e_E(m^pJ@jifup|wq)>9=B_d1KlK$3D&4Ech<uE$F-1o2{RlTYSXpWd|DMU#2g zle$dS?qK8)@e>JfBDHPaHy96*r3vaXrnqN9CTkf7_Gt-MhE;6oT(w{<)~iki*;e5z zG}Rft^19lM+O}ZD-YSG}WU)tZx!Ie{(_@Ne97qJI>?zhzKHlk5jk41K@&uGJvNits zOdl@UDA&2Xn{f}E6ID#jSJnIb4hSefSM3Y^+J-9GnYWCON5(Kh_UJt}$zy;}n+J1i zsDiEJaK`W8D-PMx((H+^(RrM!F}>a4&Kbtho)`lOk0*%j^dKUrJH8a=FURV&zf^tl zAvnqp>K`3vMWgt*E`{$AY-d04-Ck8S`p+NNlyhRknNEnoGvylG{KBXBYF<tgQ-v5Z zx6f75Xny4MaHt;gfOV*J{)5QYxB-()Wo@^+!f3nCx}(tb_#W@Ql|od>$(3>Igr8{X zbsHK&UnBAH@fG;<U2&O`0T9Jh&}y8)07jDrw|jKlyoY&2HPY%Kwqrd-lwNk?!-Iy1 zqaA>$t0p7cts@b9?k6vgH;;LV@sg^Vu*6J+Ka2<$vM}<wap^;e`gO#Q^WC0;KfYA9 zHyAGIRh{#3r(b+u#MRKJs1NP5L%Ncp1v>Dnme5A=AqiXfi6RR~cNFh+kT3rfQ6XTn z%exr@Mr2Gzp&zXp&HBhr-HCWlGGQ-%kwILj0e_bLb|K@i**bMww;uonpbV*c5aPU< zZq2vKh-~Nvqk(PH?QXMCTx@N}x-Tj5XTOkJw(Ixr^Q>cvUe%S@f0C)y3=_%r*bEOv z4<@qt=7c~z_dHV>aggp5@>-u__x5Ie7rZXpUy0hRh%Z9%TA&Lgzjz$H!8cM~k@EV^ z&AZj>YtdPiR(!|U<o9>}wA$)W_-9EYjhh`#Iz0*kp3B6Y@a5GK;0ZA@MCs`u3p?o) zjvlm&Ulfr;W~l`j-K?vb4D?Ag4?*x;@vZev5KDjtsDL+NL;v6u7QLzQ3|_t|M$3~| z_mLQ|_)N{J<zg#ZGca`RJx3#&m~-rPj4+;N9Kf}|2Yk&xs4UYmk>+B;fs8~s;Jki; z8QJ;l5e{Z&UtCk=`FTcpAbrte5PO9T>nKgv73V?SU)RA9T>#05?<hbyn_Q$#wW5MA zgHOuP>K{`$N;`(Hhg2x1Ap<QY5_E0rH$GcW0LHQ()@G&rN5wyttLIe7Tf##O;s`A& zJrlcealOP9+*h6E6C;C-uM$DrAaQS<B6GxqpZJ5P_p&yp(5AGy8|OJ_GJjp<RZ_an zN=kNxiA-+LE*Hws-z#%9)$ImsW!1J=i0Rm0A+pRX8pFm_$mFbSJ6<A_h=}hGJeSxZ z>d==S0?rjEbV*bHr{RflUl313g+voy{`WIYjbFuir{8p4T+n<8H3yEW-Jq;xF6Xnd z>LHtkAYkO!97(WfA_xG-%={5_^3S8N*CNq0IXXV!aXRvcdpYLd*%GY=^s)B?Yd`DU z5jeN&(Lb?>^>!q;;zUGn=E8RRjJ>W19_}*;4%L(KP<Y03<azn_Pmds`h?;^L?mcF* z1g4_WwIr>a65QBXg8OP`qu^I`vXX_%9~Gsgl^vD8A)ks$>l_PUbMn?z=XOQ8Z|<X< z^L7yu7TkNTct=LiA@uhFIX>L$7&4E)%?+~-Cr4tn9IaWhR2Wa*QTh=)=w!vPLNYzv z=$iHsSamP~-Q9}<1ndesn~RHv8joN@(}5R1Hx#dT{R!bPNzAX;@XGFVuHW)qUZ~>m zkKV)?Ne{b!k~lV-%YOgEHzH<$lI1$D&grlf(y|*tl%5ShMel}E6uk!z3`zn$5ry+! zKluF}db^2*F6*8vhj{w7+PEJ7QL7d@K2c^Up6GBzO3Ta<ZFZH&euqAo%`d&`5~Kw0 z@~B*3Fr_wHxqtAgi&=MsZWYJpsWNqsv~!)W-TPbw#}YzI_dWnDpZ#22j~9+Lg+M$& zeWK_RO*L)BN}oj-052itu5cMV7dTbG-<RrA>gZ0@MH>TI(Ccbv!m_7J(Z<%TiyeSn zrF~yp=XF2gWW2u-SLOyEFU$`YAbY@Rt^f|qYT^{73OG`TA`-KWPFF34y^6`atm?2^ zzvR5u{_qH*U-NngS$=hYzfg;8)&9ZVM@?;zYPWuo6ubJY8=AidJ|lhqQvM!3V)Equ zyM6E430~SCtBe9hRCJ>LHY-y+CD9)B)ehf?_T^W!bq784L8gnCEAD%(17~Eq%6VL2 ztLq2m(^jOiCGPEWUtjhy!4318vxP5@e6w<_x64sZ*jgMIu4kKgWX&ro{<R4R)74cA zpIfVmeh?Q$-}`z5QIU?=a>LD}wZq7Yhy{Fbwc<v*JKsRbCVW6)<HmRx@5w3)+qcCn z`p;^)pTFQ#JPq`YA+q>2h<$}Q?9+O4rz?hPaM+f&tgxEN(s3V(eKv+WiSVcWG5e>d zHXJ@zF)#&RwMX~?6J&XyANyq2`15lT0A(a5=<*$wlIQQ6xFxmYPA^JDeO$8JsoTbU zgUU3){KnULW*izrGMIwnFkTz6h(mve5v8S|6({p%NfBHar(aWF>*rxShcmyzTihP5 z&z=<tBwaiSAW=ppqmk*x;RhEOw@1r%WEFe*V5<cA4Q)_7=<bZ&q;Gm>bYh+jEdoj4 zvXR-W#v05f-qZGG{0A=AZm_siqx|V%Tn?)S{l5-vn>9|>;zo5MH1CI+qSVO2ilhGQ zcnt~*Zt`po6kNavY3I5_5k#HuBz>x12|gphj#&8=8MeI*&5-NEJDGkfv~azPP6#dT zk8PIYI?vC^yq@qj`Ymmoq5T7sR&$M|6`f82SB2LvfrsMb&!6KcIc1}KOIIj(%%5VE z<G+gnidj$Wk*6yEK;Ak5f^$jjc>Vf6t-A=$qrm67<LrsN%Wo(~yT$UK@0K}EGAWTT z(;RB}YVqGjMxYru+VR~lx8j6%X&{kQCkmteK`|V*JrxGycUokLyOreE{QHxH@43Ok z^3_kC_%$sul*L7CVpYm6Fs7$s_%sh_R|bRB6Ki-FeOkA10a~zq-uf;QkD`k4Bqn>f zw^INXn!8=-<7q;fB~5w(Ipo%n68gH;HfYca_jwFqVh~wiimzv_<s6KOazExu%U!mP zq}SKrBOvArKtOQc37#gSXZ-?Adw|j(af^obt`9IcKYC(WdK?a!<*aooVRLB25K_3v z5Nv&+oLvUc;;tqq9ctp>NN@_Yjnq_e8$?0T<OTYtfWuZ{0QG?v0ikI|L^mNmrB~CY z+JO^}Vu_8vRG=r1<#7d7eHJJ^IR^idGM$Deh8D`Sj_DwGnVrd^;Oj=MF7zOfD_$Tj z#Q1P4PD}U)ZzQt-k$>U8<7DWf9nmGq-v=9=h-OkY%Nltb6;fr``8In$c4ZVsOUpr( zPr}KsYO(T#j;R=YK6)64Sq^3VR;Qr%o7{)cW^4LScMZ-P*-!+n#LtqD0#=l;Y!^Sw z+i|#HTVM4DCuc}X(g1`P0M5@*B0Uq7Zr8T#<^0q6^XQ8XDG(&!FvWLYyC}SnQ`P)b zg2Ukn1`|hmAG@hAi5tWLquOynhz5gric(phQ9S3?rDR5~9Z$-ajT_QJyeb_9rSoWE z0zmv(o_ovJ*+MO&{8Vmd%HcgGI7OLf&yOJ3TggczvNU)$+><eK{PV&&EL@QFr`3GN z?!v-XF2><QpTyRJt+St>nf$Z5Zkiv=HE{FHzI8CiQVW?Z1r_DN1qj~b_>M=XFZ~^b zW08M<pRvIBF$3U)K?mh^crzjBk573tvL=GV@KVqieRzh99Ti%(tsg^?C<pz4vrkf_ z@A=m0<;6L5l##wvGN7|5D^W%huD>V|(s%~z+Cp4y_M9t0)Bl!19*_SJS|_Kb_~3+u z6$5Y-@`{4UH=NBQtQ@eh2`&Pk1mDZpoA{Yk20Ld1cy3&#pT#<#r&<diuJ@0Bxe>qk z9B51>j+Gfv*!TMx8VZvNHY~Pm=cqV{^A#8>y6{(>=_yxOZeH?E+Ca%k%*qboDxl*# z6YB;~A3ZeKPm;U?hQEPuB8g5eTmN`@l^BQ_!Vw0XV9wrvkB>PY`PIg%v35NxTLqPz zxqte6dli-ewu4K+y$c4F8+`8xvr)TJKu1h$zq7yt7NT<{ZlXjQhz|!_SPA_(p3*}4 z!m)n=E}6&1k>Szwx>F^JEpgVh83QGVs56!I2L^I|?SQG*zz^0^;R4JaN8#BmBwA?~ zn1JtZd_0q%ahg5GNuGsJ(e0&OrU(w$vK4mt3kV8Lzm1bv@mNzKFA8(kp-DLMkDVjH z97;b`glL-S-4xRZO<b&D9L%j(*x6$W+WJN^6O{#mO`(f+GO`tt4>e_C&U!CHMPXZ4 zkUkxTVAbKkblvpF8RHpcwC9k^^VZhZ;^9cZV;Ud6{-pT*4t&_Y^HQ7hDbhHssEDPx z(RIch%+Ux3!&4wkQ(00w9gN~<sRhr8*~c5PWUF77DqD>uc<j=eaUC+YDJ&>!6fzs+ z6a${#&(n@?5-o-PXwdr5rea9!b!J0#@5LtR?P3ocW*FkdWF~~TyVoSQn#BCb^;tif z03bcRiz9vjd_U1s;%5%p!KiO73Z{=3?`u&EguN0UNX3EjhElfkSjASIa++_V&gj-J zim_w1aD=NK-ENu@zaUGwL#Z(NJKnJfi(nlFV3nZ;OivFzWhV60$(Cr6k*%R{C*Lb| z^L$9x^j0Ijit2VZc_FDrai>h4zYBcyi<NE_^J)$)NBLjs9TQ-WRvX7eoekO6a;)-c zf74v{68M`04pg#?;iYuqh>^;xwyQq7j91`4$kdf%3eW8#z$Q3%OuG7vA>IIjEqDxA z7}+>N$p?Pp_x(dKu+csu?r=%`1G+FjAEmKGlbto*oGwwo?Gplk5EMZFXg5ERiuYF* zr>Vq&*OhOLRWWLOr*!zw&y4<8h5)|GvFY>~q+d+IELBRV^{~Ge9RQCbfIp!i&1Gs` zHI9~R+mRs*Ic~ErwPTL7Fcwx@f1Czz?ZJAUCSNf_<dp$}6+ws#kpOv`V}ii7xakcH zQ9AVX9={-4V<YVmUY@V*BulkmJ_Rp@vV6Rn6LUCxb{~=q%%3m}*oh799KssToQk@? z;Z;U7yC~!B#&1Obf-(1#k9O-4lw(I?f5D#5F7Z)#dBuFrKk$4L)?>Ee^%0A-)O;(< z8>V;;N5#18EBnqd(Z`)Irn?7hzR@r!bnI{O5X(iN>%_pJ8+O_D_??v&H4s07u7#HQ z_7sG2p7|OCrECpk1VJW7nN=eZ-lmWPT7i|u@4z-~aNA7rCk=1cLp`k}7fSrAKL1fr z2VER{Z|=cw;xuGCq${5*Oj#D^L!4_*-0TJZ{=wku8?0~ip<$2X@s1CdexKbXt9f`z zM;82J8Vo{0P_`>>I-lolrTqK(DE!`1uOwX1rQ65BjbGRZhcG*8K@}q}>IM=yVG&3G zv|YtwtjG*J)E1DuJjKgSRUCZ6^z`4^4H21#S4M_7r6#8F?~>Zzv1N}tOc4_(kxo;y zY!NlaZ{koHc4lc_sbVrPu_fu<qx-hN>+lgsry;+4btm7IM-IZw`ubrCBWH%Ye{$qM z@H}>um5QYrL4NfM_3UOBDG?)I^~G7!G_>e1C~f9tT7SX|*aWkvhwby%BQ$V5_>kXd zdJz$m<8&_FT>7fC?{=Ezbx`=wHqrslAU0LX8C2|;en2q4)Y6NYY0F(H;B2asQ|-wk z6iJ1F0iEs_j})fAfW{5|z55Ne?e}k7$UuRrrQF_{Q&e)RoiyO^@UP<}S1zbyrtK_~ z^ONqlweL99SJpPa!vq`*IK1SBh2hWjt7CH_r?v|88blNcPJ+Z5A~khC$4GRw`Nj$^ z5ud~2RyA9qJB{(Pt85+Mf!5wx;=5fqv$2^~{>&eGEw+UDbk!;M>UJ`aD%#=h^t5U1 zbl?>Q>cTue(GEmG9718u&OStRVj}&dpc*R$jN{9yJ(THXVl8vBk3>pos6BlVyJ}+$ zC)h*vQ~I%4Ve+TfR)%BK&+61h+v~k_+Y|but_P(rBVs~WWFt0kl>QEd$u}?gN>-sF z45KH5!H4lQG0z3k6W~2c5LH9erz8SY^*y33&YeECq%gOKdg1*e7LM2?Qi+wimUFI9 zA@RSMD%%@FFo?>N#BS;Km3Se>u%LNX8(A+eja`^QZA-gs4iqBoH7xDAywUG>Y)D~Y zS3h)sg<>ro%RY{i$9-O9=F*=tFvflDEyTU;aKl|lA08b>v#+pddI2ls5Q3ms)---5 zGe7_9oW<Nrr_9qUZJy%y#j%2~1fb&xUxy1saHw49av`R1dLPpl^Vt2sgLUm!v8J0> z7vR$ZOR&dy?$&9I@P2CmXa|)icP07fIz0p89KO5B$GG!D$+RdPB@01IV!??dwS<t) zB%Y1EKBQ(Z?;V)AT~JfBlS9Wc0Bn|gkQr(m#CK_c1&S3O*n5|osO2Hg%h@QAbh!n; z`}g~z_M)Tp2;FR$4ygn+mO52TS$V1L#%5>je>es6&}bl0qp85)H&-w`sO~4$nqkKM z1lqDouryOZ&>rLQ!|lEa93eC!mEk8a29aQvjiz0w*=p^_qnB7yBioKJS@wcoKwvnN zjXGek4XU4Q2TP%&=5oLS^5et&{6Z)XycwxYw_mqbIi7uTaHQsLWgj+a(BSFViquD} z!>KopsgVDDM|<{RbDWpc?CbnNpr)FADu5ag8VaALb#-dlz+WrNsX)#TrJ8CV>RM<l zxL~x;V!WKhA+F5P(oQKyZfSWexgDR(7{@wtRArx^Av<Ua#+=X5rV5{YEMUS>9RdWW zG^y#1oFIRJ4sNE<D4cocf@!A7O9mGzpdYV7tF!s1oxmpiW$)-I*r%{+e}K&uyt>V$ zo4=wF5o7nR4gHB45(5l?1Fx^KZ2Szl`ut6Cnq}UKOT=}3!;`g}P^BZqbAC)_p(l48 zXZt`9A=?Yi8M&C^B_@Wh%q~q6OCA?KY_e`Ve!I4N?|TjTW@#sUrG_5;Yl!WtGfjr~ zqB&nlsUkasP}9|AVFDHCL0~qg@R|@O;(!4u$R|;)rlOaN4`~n6`W7ou+oQ~=a~&wW zMJFXqNtr>Uiz6ekAO{(iNvkgszZ`HgMD}NQ(xG|?hul)(N<^`Zc(#x^qDe9KBMu`e zMtlsO9khm`e|HA94k2#wou1dS1SGRQ595xjp!et?dFiHL)gi(pfhDGxTPrJX>aGxC zjwS!h@)y`h3<1d-=AW8=ae&c_=5zVtsMh(U(hIv7;oyi@CVN{jB8VDs19rsyrNG8k zsTJAx?}yoxv{kJp>SiVA1Y?qkFeau<2S^f?9)MOX%X>754nG(#;^BOxM0s}}_<W|K zb4GGS@7?Rd+7OGdWfPYd+aMuPWr4<jN-f@7r;>R;^!RzmO)j?+o$kDR{uO6&^~U<W ziT!zqJtn0hfJA427tQR(23o4_jhpWAG=};7%5e~Xx09USY9j4!KM2&;qe9QR=&v@O zHTRdUsSlry13)O27=dL7cNPNTCPbL=q{nIR5@-y~d{BbH_izZ-m{C5qAq`prVF&th z!2Ki#c#1B_mE5Jjc!mgc*1Q^NZ8hPMi}*m3M!Thw!Px-E#np77%sO@o#(BRDp{09E zS^2shj4I%ZO@?IZSiqsm_`d;-xuR&5LHB?jon9C#Gt1&%@NwdX$GVc<Hs7y6=ZHA} zr4*b1$=j7Gd=QK=;a2_2PE(%ISIOe;9WhUELFUfToZHpPTq9+I+9U6Eo{oNQtq_>U zyZhUcBi^wc6q6;`O{5sA|4?(OlVk&h#*p^Ejx#&kjkhNuEF5O^dNFEo@sl%m>y0Ou zsOWH0sR=!#FJ+AN52qJKE1+nFdV<H@a?7|%a%gvrDCctnhs`%{xbSBUj#Zhyd3@=m z{W{WW>mLi*9xGUT6A$jSk#dd*M~KljvSRx~*DB0Hw*wOh+r7n&vNyMT=KSMbAa^Cu z7}EV0Ptm_S773F7jAwXOGeZx4$n&dHOCM^$41W{{`+ohekBkPz{z0H{5JC}WbcKWO zVRgk#$&r;Z;^ak;Ni3{m_yOlWjM1-TO`ok`bX_rR(sk^2b+cA+@;iTCZQSC7^UWA_ z`kyZ##$gl6Ps|7F42>i0p98dT?u=pnBk0a}PcHN5Bd=cR;kIhunwl$Qj@)2LSdx7N z(lPOYC~>WmZ)Naa{NP~7QP1_W2#U~IIjV=THr-$VNMs3!?;=|J%3&UH9j??c(0wQ_ zqQ%*kB+4Y>>*9+1S*R0#jnGT!cZ$n5+s=dE$JTD66x`53+_(r#w!a{o0I@H6BK+PX zhTQSLh6!*JXa6pul9yFZt!jlk`vDT(3EHG(Tr#S=ybbzI+Zy@zHf((qe@srx7=FB& z*qwquAFGJ>%Sz^zwfNP-+3nxIC*Ro{e#aJ|<$?T%s~h7Wt#&495KaJJJ=QdGRS3BD z+4#xymd-KK58jiD+`TzT8YeH}?|Mb`#gmtQ{zE5w;+XV`P&alCg%}Ym2{LDV@6}HE zo8@=hn#cYvU;qX$wPe+M*~KCf`w{_W-tVR8%3@rQ065I4%~?LmUmbAwP%~KLjDoc4 zdrIodKM~qlJB%@iM~oR?pi|$jzBSRy5api?xRb4RlsiUkqjcmJ5p9OgiLwu^clB)p ziM~bE`rr|}bHL1$eWLg98ZK&$GN@315+wW=k{XKN%K|IPnb8|p7o8rinK_~{5Ev?Y zbvDaXSLH-fW%mOMpZ82WHiu^t?INsori1Ka<y8z96vbP-Ags@jddu^p&jR*n#+guI zmjzGbji>=i1)U2*DPF;VW&_6xF;Gsze!*q&ko0JR91x8kGr5X{0l#TLzO=O{q+uEH zCUZPC1aG`A)TCcDc68Z`caB&60%~NYv)!=jVN{&LrJ50mzrh>6j_R)PtibAo3K?2* zhcG+d@q{wzh6@H0Ep?b~w+79L<waXgJaOGD`A48=^4g^g+)g8%g{fxb1R_|~=!Sw9 zVnL3|l^^|XtmS`vzbc>)%oTQ=j-q)?K_W4k8WX^?$KO@R2o59oIjhZyRG|mpr95T+ zJCqIs5K2)sWr@H2q4v_*E<$671OxGR^4!M$v5vb98!O4*{}C((j5n1aL^p8d;OS~W z$k5k=7jN)tWS2*<QDVTkrf+jtc1DHcL>6O$PGAIK`*0Fr^lFK~4b;;M2+W}d8O=2H zekMCwzb#ckddnx436r|tz)g&x_e4*`ndjqqLap-Go9&2#kNZMlWEt^^$MwKJr+Bl2 z-}L_m51;dD9imIXVN-#uWWTy8sc(`dJhc%_Mvy3N?bQz>ZtSFc%>h*p>`-eh!;b?4 zjNxZ;q)Zb=+F$xtoH+Uq63Awo+~|Vc*<hTQw^Q?NY0z0Xb1Xv<(W{`aslVV@2{)A| zL2-+9qlAX?84uqxP;$4EY~7|*Hj(TJR{);8#VJFwt{2KD!E@W-&J)ik>i=>nL4cr( z29?Xu2Zg&Vs7w+qBvy%)o^`-RCwW<(On9FqlE%=bQYAM2k_+ZR#xSH6!*V@a0M+kT zH)?HxdC^m21~A5SpEfG%NhFH22TT0*D8NG=<LSX8aQhM!C+%(SAeRn15i^2xxJ**{ zk0pT76Uv$$#!`%KS337F>JSll=9%@G*(J-0q46bzoa`UmQuMwU`(@#rM4NF7BE%%k z2^jU0R2=C4&MDZA@j5P$ql!|>Du5J7*cMdI4VSg&qtH!p0GmKJ4UU%ze2nsU`444R zj8}cT@jJK<?J+V9{Ywok$=5nAkbV@Mr1;;AJs!Y%bJtriOfFbEYBHy}^Sr{xL(AM% zfpJWNtzU=OUx&r_cI<9xr-z`Yck0-K+e&AN(gBzFv??Qk9UHrU;Gl&MWn51cE{%BY ziTMcIMwZ+vDZWdlyC;_bN<eC$%8&cJcHY!ELz)Pd5#&<nx>hBqhTN2=$%}<40t0tc znb~TkQL%tl3?6|*wh~lzFF~b(h)XLZ#m7nX{z8%b_D%0eCxL=qPx5x7h41xk%S<pQ zCfH5pp>tzvi5OG)2F8xB(S;?9Y1;w<ImrO7;Zs*jv;Q1@cO*TzADBh()=~GZe#WfO z(5wfvu+H&Q<DR5%{VsjX-nr)snpqClN$iX_r`i{$AI(4zD#6@JmlLxhoxP+Mq_d&3 zR20K6;*~8v*0y~S2n7Og;$&y}kcf;-P-~<2oXm5bkY}7r9kv5vLQC0%qzrb6<s;Iz zeWjKgDne3;J3MUfd$Xh|sRogXczTS%7z`p@hw)gWGLX3E{ir!;T3YQOBm4YD*0}8f z@C*GU*WVD8vbwGUh2^M1X~BJD8fZpNv26}#En~<{7DB*fjEAK#0o^rv1)g+z2+taS z7x?kG2kZ$ax%la+pi?aw-#Cp%j&TGRoA@r76KyATTh&#DGj~2guUN#ufU(QWVXn#_ zuC1-zyI=m($7}VvNQ4u}qv&v*F=4mX*nY@ot^@niB?Tm`^%zw#l$vC0V$#0W;n4_| zK}QoahRX`@*Z7EyL5FysV*P(FfOYy)Jzb=RrT5<_yOC4XcX@+mgwPULgt9SxwG3bt zw@li!CPS5e_YLhK`i*^hUh(z2dWuigr&QSP8eT_ZI<%tjnR7Cx3u-`HiKF`ldTlSE zvb-o-xfEmyyt-kj({Rv8|Ao*)P0wir639-kM8r&NyY<rSJ-MCszrzX>aAAM+<cDe4 zc2!@5y<<z`U)Qcq7aKjn4fF7sGE`w!CV}RrWEc;L7DaTk?Y`G|hV(dLe0>%Xo+9g5 zhu*<y(y9KF6K3`@MjygIsb+T6p2PbSoD9^<G?FGZm_WtghaY+^e-+2l=HDWNF|ftc z{yQ#$0NG>T*)<`}MYzhwg_=Hz`Sat%l_>f-<o8OxLpK7@%Ep)1-77NbUNit|@vxDl zH7VcM^SH1Dx@JSIt&qB<4#0~DnOWGe6vJ7Sxv7+d@CFKhQnjiLbneUxoTACG#Leuq zjI~+z>9T+CcZ+P;(k$l+Ym`8YZ|J92`eodzhXpE7Mz@vRhS!h;848%p?m+&D{EGm6 zZ4IU@o4{_=DU{AMeX>q}cEIo*4wJ8S{??}rm1ai>E{0xaw|0!+FY7-P5xpy7FtOI@ zfCxpyk=%QjJo@?ltiL2D>19h_RGfw%t~mqZx^EAxcB*S1kww;DEvV7E$ggz`gXqWK zX+38E0*KJ#;P?pFv>6u<4~Y->%xpUB2BMUo@R09oSS1-a<AiY&H&onXq^EyFX_6w| zjn2QXL*S*kFYy9U+>?KL%$3+d1h(M|33os)PBU>PAaj*mooS3uAK1mp!|ePI-|eu% zHQo5FutDb|SNtZMV_Vz@6adH#9EJMvhE|P?AxB*r%20q8y7ij~DloSVLsmjX(HOcY z=Xkltf!q7(p%si18Wwgk^A(Sj@z$^Ip%59W9hpYS0*+C{M<>&(N%Zx_Z_?lV(|_6U zCTN5t$>d_DZVVCf&Ak9d%oQvkU^({v^K?>7yyU4ktN$N(cO5q&9v-;ux5MNLzLgeZ zwGg6WsQK*~$heAf4}x{7v5wF60s?4C23D8^?Cc+cwiTZ%&g7FN`?>XC#Sb-TJ;PPr zH@=;{mZg3A4@65+1`vTRqD5f&W57s_#jylieD_)Xm-EAe0Laygcn|}|Q!@BL+{0<q zFc;RxM~BJ(ogfIn{8yqXUi<`l-y*N7dk})Bs!6YNJ|#x^+hH9mk_J#jy^}W<>7;VT zHdD0xBs=L}8NdJ&LCAmszwh+Em5v{4!oqey_Oa>ari)S8r<^eMpCKE7^b@7uzil)9 z9YYOxJhg-KJ>8zQ2y3B@Tq@HM`~G_?N+d8zum?3)iC4GRuZBFl-*rrctQ(;UV2i$M z9FzjzvCg*6QDy9%K{ME|ex~s|qBJ|CaE!<%-mgCDp&h>iE0)h)u|hD68#FN#Ly;jR zz8KM&ec*pUOdTu^mM=v!Rp!1oOSQK04}1`Fpp6D@j(A6jO>Gx3b$s~w2xOaf_vlb3 z+A~QB`GXE<W*3{`RLb9MT$g5dLX<{{YEYlbb(KT{_gF%sLMN+f#}Jd$lrV~kGbqom zPnoR0q$g1Z5?aGY@+fg&)8J4Jo`HijIDFa+r1LNXEQQc9By-q0ZC3=JeP>T_aA=gm zdjnu!cSA{hp>kjT9HBU@9%lIf8-7e+M+iVJ^hmJtG?9h`Acz9S<%oRW6pZmBjuG4S zACcV@O!&cdeo(unqCq4#brFd+?$HvtifgtS%|mSGa>zUr=Ey2vEBlH|X_P+#Ox61= zR4B&;;O*SO53(u|Aku&QeQO&L&i%6Z@y;PZ9YFr?e~tnpT+<s+Na)PLHyNIo#>mY) zR{p$_udkp?BWa<~c^4Mcy9iGIDp%a*@=w&snG`M=^Zya`6>L#=U$o3Hz|ccC4BZ{l z3=K+mNOz}zNViBzm(tx`(h5j73QCuBH{9X<-}~Gzz;J$NpB-zhy|)7g1qb#g99MXO zqB^$E9Y@T_t1kw8_4h_v&~w-H5;I*-^FKHpFL=9{1b~NQ>!RCY{O_>I8`+bdjwxt& zI!KEx2yEN6d5X-Hk=y+*n^KAeRZ6sY(_4q)lSOEQeW$3NTn{R^7GXWCM9YM-V2a7_ zx?@EncFII`oay|17ozaabLr0cY8OecJ!E%RFX;TA`2qr*QU3AcHG6FNT2HO;4+q%L z@@uIz>6#k(vOG9Hrh)X<R}~{ZIOwYF<=Cd2Ab271?MtLlTiye-W##FOVcn~^J@@+8 zm##keapb@3SfA2K{#TEtKFjN-c{Eb-y_Qz4P2Sd#6S6~F`(+nFABUM1h~5T*+l*c` zf`bHG_|j?4pZ7ZywN{_r-P-*G6!UfeZir06PVN76%nS6Afj@;Z7DE+qcZDUnMcZsO zfV*$`^>Ff;UeC=_LKF*U+ml{Ygk*ZAk%iZz^cJ8U2$&COUFS9sh{FFd{P1+$jaip2 zrt~po&D;CXcbtd3^@Ygg!U~RylP!-Gq6U}&HkweOa+!t%Tml{}VPmRn0y7zyScTki zepaD_J#1V+S#T|KAsDwy4H@7r*L}PFHI>=w$%*G%CM-0B&Fv|k4Y$(<lpPA6E=R6p zqn`bxZ=WEqsYwOLup+WX7$5`jn5v55X_+Ps#m^7}g6+h^b`MBu=6QGaV}dAfOfjP< zzD7U3Z-0K0=#9X<oD+<R-3ob)g>g~9)rW3$73h&JEeEB7yO`PYz{VC7inOogXH0sw zjj~wVlrEQ3RcC8#fvH!*D&?#aOdP%N=Nl&t8yHnoih=QiA8rO0B-85==0Cz&UwBnG zBfaWO5=;u8)~H|nI|Om8M$I&2-c#6!e7wRm?5w7G5KqacGkvCieROrtR1+X_B&K{1 zOn<1I`vz~y;09vJ1@S|%0({<C`@6a#DAzexk`cWIST1nDfu|OfUJ70yKXjGm89OE< zg%A=Q=k7WL1fx;fHH=lEktl{D4Ci|Ah!CEcjJ+=>@Mtgv%D+Gtwb<}SS3A+*%EE{( z?h|OFRJTq%C5Ahko7!`7lnlsufp@aQVVatZH-6vqykymb;^KlDka@=rXSHq>^xidy zC`6KK;Z+Ob+T|#fOpVh1ijV^Thr%Gk4PkLxT`)7zH{Bc)+XA`IPT18HZ@|VaWQ7gu z<;o(0nxEwn*pn18Rz>?@!B!Mg`;0*#vYg0L0C|bjSOVJUcpVu6)vI1GaU6mJ3?tH0 zM?j%iuVwkn<IUjsuL<_>ED;Nkl4Nd)O`Cm3PR@4&oVgHz&Zq!rp1b3mJOYl4!o79c zZOWM>-BL$zr{_z$lKTXIq8iUOtAtpTKC6_Ajl;scOjf`?cp27;Bu9xt%T)o{J$PFv zY|JQ}Yl9N9);sb$ZtgoZHHj1^0vdHgs;IF-vu6k&eCsdd;w~chyJ5F2XH&@5_fiL) zd*QjC4Fs3|#4*(#Ed*k7^YWpPpoju$F5k@W$1+Y6;SM{rCo9j7{x?V7ff&X$ubv}% zZSZ~24=b(EBIVbH5-E_cd&andF+sn;VN&c6jAkj8a8!S^(u#@$@~+$ne-*dbZ*1r9 za9|)lEW96)`;qu}=DADXw$!`NkYj>BPhnFbw(b};T@IH=Jl4{A4$X_Enpame0qC$= z%CV-B{-qsyT~6*c4pza1xCrLZ^ksbEkmJym$D@UlBk$*=GFzr5D$025zxRKS(|=RF z8V9$c2lEY>tIvJxPT45j47Kl*cVI34ej@0dM~RfMUKx$lGcSvPLyL#xsP}>8webl) zde%Ho@3)sDutQAo*bGIHb-CSka=d%sNfYMz4kwq>+n*;L;(T^rM7&e%{N4Ivf|ejr za{Wv}vn%<b`3tf%8?q<DzaFBILwzIaNg+GYy0Y<~IJtl?0aplE2`D3ni%Cg?WoQ8S z1S4!TNWxYy8+h&${1YcVGy1pG4Rl-)LA_vnyk30Se$LTDIUbb|9oqc<?I;-T@!)?Z z$PltvZ^gUpz4E6R!-kwlX=?aC2$MwyO8HePR@%SHwVrKeU6+RYa46-Q;J4!=FTY%d z`5=5zO>wK291DsK82z2;Jr0Ly)WHvDY0rq#(xGzvuV&(=pDng&sNlZ=Qv@}R=DgP7 zIkiSROrGA;QpZ+S7C*T!%HLOM&mBYw<A<vF=8v-e-~Y%00=6hECv$m7Tzg=7l)!Mw zCbI;+je_xH1iAWPFg{%AyV87_EiLH(wXMs?a0O8RQ12HDf0o1mB;WyJK0+uqru^z& z$=4vbT4%Y|Yxu&xkCKosBK9MhIcQWSh~X|Nsh}BG+_uvat61*OpK*9bjv{k7e8vxd z{-z*&@S>fpQZFG@J2?33<3TqyX)GxZjk|cu6iqd?#X3ByH$b2btRiSsjVCEe<s!M> z?}Yw7hq^m8U`Oe5^0$n@zv>Yz2npgXDlZZr@&F5X+&9EDOB}1g2u>u+KmsR3^v3m} zNY-pU<Dk9I%j;dHE7lS9<?Ch1LJ9MSRr=sAqrgAJA|c?8+SDBjNXMmOhITSMtoZ)Z z;G8bbp+J7e@sq@2LrJ%FvmQbjiIlS`A(V@sAjx0+Kv;|YFmk5<*!t8OQ-KimFYk+) z<Oaii1z%9zFDqPp+C*?nTwFf(0O-eQ{P2?CkHR944hi}7=O1K&dU)kb-qgta{Jkk8 zfCXBZBJ$kl`(Jje$*c;o!ot}u0sLYd^FgIzrd>Z?3<pLCm=Cf{tljzY1zR?WrZ*G> zY1&Ddi&``S|7zT`^4Uy8sX)$bqSBTgB(MPhFRrPu;+GhPO__(9&H^tk1XlpxW?NIP zMStRnTff+mX)|@MKwgd$@uFMjAVmunA;(*cmSy)OjzeTrYRY&NU~qj`{5y&x_Lc{X z`5Fm|!%i}mZA2?B1|lezkPvczf8>_7Z}jBm=}m4~8K$tvJ?yiOfzBAMin)*Aw6AS= zb5*f&!@q>w!c_QW1@gVRAN@D~&udOw6paIZ{QzVt9$-w*fi&z5f%ttw?O>E04Gu=c zs{iplR1LiJx{<VyI`7VlVS01fYiUfoZU_}z5i%x_GQ^qJ9mwmr@+69Zb(H3hZ(hfB z#}nI=C*5JXKfmLOJI0;L>TgP7bB-eqrs5NT!-iL03+X0PT>1#q_%Hr92xn3%h@rI- zX-aCaeV<M+ffGe24ZePWDp-vPwAAcW6$S83r3+ikf$!*6t@YTsie8jq<@4;SDMce{ z#sO}o)xO(r;No%DOR6X<1EVpLod^x`jhw`i!@syn>u>g6(>Uo%S~Sc`IU}PZ?(RPM zeqhvZ>1_wNVZh5698>lvTug^})DO6#3cN6DEtkOij<wG7@yJN3E~@hP9J6x#mzCuF z%Gu7XYj@F&BJkLFhDx$TR5f|Le4D@hv!%X8iv?9a{z%(+-GTsQ+Z!Tv@ELj7j*kok z@nc{M5<L8F89)NBihV%`&dnBLG}~7Sc&^dr{1N(>wzPS{yOj;&!GTXZ*dtx!*kTv3 zIVC|!ba<=1j?ZfF2haFKiph7<RvjVtS5Cc^3wL<xDoPkj&KO?iQG7C%&D)^Bc&TND zrRweHwbQ7{zs3TtT4o;2VnnipFvok>bngGfbCqFwGpd2|jj#YeML}W`l0@aI<jm-e zcnKWw%d&})VqFn0$Ix#dDol`yclJPeXU$8_$Y`{0<J63$C9B>tce%)eXc86uyg~`; zikqa2)oD4{lvc3aR~c<Q3<g|j>}~_qZj64$9TD~o_LD+dM3vxCvj?0WiJ+{Pa~En& z$?~I7sTH@&(pokI1r-{u$7$G1-gQwll2=uwd~{J(@`OdhkvAtbRr*A*V{lhi#9`*X z<n$`dDZ~;#>wZ3sqTi97OF%&Fbn=_=b!C{Ys81}Krl$Hs1V+L8q6Uh*#zp~WMOdki zA6pPp&#Lr)PK*x+ngnIofDGA-47G&n+%{P2gp08mcY?aEnkw>2O1!t{JtoB19i*%| zwA^5i1ZcwAd=|bb@Lk--i%kwT!vTxW1$3g#T_m5hDe?2_>gMJx>OZ$t)}Y9uc=WA! z2st)_Ag_dSRV{je3T<h@@WdEyPcU#TiO%6MKpA4Yt_qabS&|HiB4jse$EfZ+Gpsf3 z?aQF)@0UIcd;7UC)7{tdeTLm)G0MQsM8MU?=jFO*c|{*b{|0yEF9;MA9!_Emtmy^a z=+UkzQ@*A0vGpO?wD-9#)JP;ubn5tAC^6s8i3rXrH?ol+6i_#u{|;>l2n)DKXOR!` z6Jqk?K&7zMDU?q)Y&#=v{a5aA7KZ)r_Lt4qFJ?iX(dNyG4)Lqzx5sLFR-UfXGTd0P zhc3{lt=yS_y{4M7+1FPb!0r|8TPO>u1Oa@D)W$}CT#T!>eQjCtU3TX;u^@($qtj<7 z#z~2me=S;&J#W1>-$3MlPfCykfGggY>=>*_1f=tX7}I&G(XnP8>=Uf2h`<{L@BTUD z)wmd|n@qqPWk>l9#ESi(bGWApS$Mld-8UIAX4EyCe&g<L)GXsdE>4G^d2{yH()p;w z#Dtu3zX&_CNN+3R8@x(dkT&%AH_M%5S|rDD@tkXx`gl^0kbfm`@)E<+Q@{xu^?3P= za~o?3u9BIUMU#>~DLi#7n@No%F8=#VtE?t%Xv$c^=q@9R@Clg_o%;dZCRig`-gO~( zCAN|g5y)%a-S_XNh9V;MS#?3tp&SaIQ3%-$G&R6DARMH4S*8R|@5!MUi<GXmwG%=6 zJ2}_h9*S)p`(3ygyHy>bx2d2Wd%6G_*W`uchkJG!*c1BL*ciXOnY(ZRo$KTPv_cjN zFY6+toepASl~1PP128z+cgI%GZ)c->ij_Glta%vO!FKxP)Mtu$cTMT;+A}Wv@19N~ zSIlsKpr4VS{Q`U=r90Fv$;(61sT3r+I$mLj+#K5uhOWU^+u#iy{M_-#!G`^cgnc5~ zEL0^~{iAZhJ|=$MSLgMvZF;kM@mG=QDq9dfLSM-|bpkR#BiMRG7~>59j8uv05=D#1 z{j(V_RqQRINIn%>^z!WNtk%rWj$k(ZX?$g5g+(<qiY74m>*9DJYfQg~jjthd6$`2O zAOUAT6@91e#f{TkqfV1K8N1?yAAs24Fi5$JHx2c1aaI7(<Y9e_r(#F|&C+Wp^L8dQ zXkkBzpE{$vf%!oMK0a!l{q{Py#E3C9LpenL>K6Vr8yjzW_uqZx#$_IDuvqvFhf~5a zOkPm{0BRuWZq}*H1uoPZ`8ZaUR5fxN%p0O1OE#d6<ZFopx-g$_#zMGw%0aC0))ohg zvG%b)<fP=*tdncihZu#jX5>`n;d_S`GM4;zpWW{EYI1~1LWzurUUh5y#Xt_NhpJ<K zwPe`_0m6RVkMgSaMPN?!qB3TOJ@G$as&^qe3l<*_InH`bHjJQACKGgJc3Ut?JpZx` zkexsctQfJ@YqAyC-du=y|B4`(Zriw40fGA+dGrs(Vq66TGahn)zA1mF_93#JY^DGW z&syHFbXBpa+`~!gdhqWF=K=3#iQh9I=m(YSN@(=S#lMQIpI)*S`7&V!7PJS7;4%g^ zm`;UsNFm;CF?<ljesu>R-1NFCMi>ydJ*5ZQJ&qr(WXf1|gpWf=Jvu_xl?wTKKIM++ z@nfNNndzp>11Gl>Vx0B}_+bGp9k}y4k~SYl1!T6)V3%orWVlK;?5~#T3r;;r+!At# z&>r)EvMX3vSTr*m9$z1?pj`jGm%qSlU7Yvh@rZ#jgKVdj#VJ)LjE(JMe2=UM17-_+ z+h(5;iS~b@LpwY3cMjKhim9Q+ZnA{Z$88@61S8$w&75!6>4hR>bfnPYs+D7oya|Bv z;kcsx3AlIKtsO;$WuJAXPhbEW)uc1AqHSfV-`RAWNS4wi{X2rd(`LTYrsI4y_G7*P zd-<PpHh@BMDZdp@&_JQDqvMQ-KprA>u*H;ufTfi8G9B$IDd++W<<ny+A&{*gC&oc} z@2RSy(z6l2V&JUc_Z6oW30RmCBg4QQyb3vDbe%q|4*zZdoA1jnD7Yh+Z#uD%HPUKM z<L@;G3`8^b`X|dFD5ow<0Uaci{F5=C&MG#Gd3s{ALJ;f(SWYKO1u8kVKyIH2EjNCA zFm-jUVNUlbh=EmsEgLDg^=G)bx!LaD>m6F?>=Rd())XQAJMgr+F(T2xC>;Vu5pWV+ ztFr><MSjfxth4v$W5narsw&^QHUAFgLlT8tv};-!aT9Ztt9?9?>}QC)faSJ#qN+@X zsQnG<e%P|%@SPULe+7lH-W6^Q*^2y_8h!m|U<lKq>Kz921<D-mPha2V3PA7d8#qvV zcjoGUj-&AAseZg<+rtcoO}rqs_3jwuJ3<(S-FkW(dqiV@>>^6=Yb3~eHV7`-W1%mx zlk__DCG<Mfn26WdTZFTvYV1HubAs3i^<CoBN0FfkX$Ny7Co1R45_npV;hUS+n?ve9 zYs=`G#da;q`s|zLkO&WwDKxo>1Z~sS2WsMn*pj43I1J|y!9XYMDBJq}O-fKX4s6J= zA}_BU?3<Ki6-efU#9=3yZ7FQcdXk<|jswjL&DTqm7Ru23Zf@p>okNCNYY(+ARTws! z8yb6}B9268;7Vh}>NljM<uIcG+GWcW%2I91c%!fRJSanDM(nBn@R*JaPR-#5c~^eL zZjopjJnL$c_JEYiAyBK-W9{LumE-0wa}=Vwy>{>G?G-!OEokXE+p^K&QQ{Z@YsDgD zXxG)M^ZW4<xbakz^7X^l6xgi_R?YAO%mJ3${A;1@&{fn{g_|L0`qd5eP_m&u>_hhc z{+V;~?nUx<3g}1R!&;j^m(X`sclzA=M|Pfq@*l}8BDIbY7YA*B#pwCydI!hb)RQgV zuZ4HymDypP9q1ZwTfAg(+(5b@K0ZDR+_?vG8E0jfLOgJ=@(qHSAr4u0YeO)9W@$&F zM!>^jEpuvF{MA7C;YoN)W$^9cfwu+AB*QD?sw>(NLR1J{nAU5{ZgFw(BjTB2A5ox) zx48Nq7yLSVz_U1jdP1zo!4pGXp7W~HTRL02)44=ekB%SQX=4B>@>=A9<(;0YG-+t; zRU+Yc^y@;|+1D~7ZkCIWjhj~NQD(N~uVirQGDH8x`2lLcy2kF=zOr~V;}VBXlO0D7 zos<j=45ou|RjxNDYZ?1l0^cy;lkay%+s;;vj_>64I(<H#ecq27hR;o7{NjUQ?B^z} zyg?gzVV^xRT$5i`M$#W8OuM%7MQoz?;dWnb)xHXqvw9R>h|SY*@u!@AwR3cYLx_G( zQBB!;(^(vtV037zDEg5MI)K~$4|X6|0y6i{*Ai7WYqR^q`2Sk9nO0@bN%>~#GwBrB zc=qATvV!qcMd{-SctoagVdLWW<a!O;i=AiAV(%}eA{{76QF)Td!a(0%?;%p%FdZzl zpu7ZQZG*Y9GyK$+?q@f_jhJh_zpuP)DS>r*<ST!o-c#r;yUWWrd>0cg5mAip&lZ|P znsEKebeU;dhp?W^_|p$wa0Rgd4S-P-7A|I*tiouRJjB@))>ehVi**)R|2(QLXsqNJ zOFZ4V7Ty>c{izLj+;fHR04wnN2NAv9-0-cKuUdxBC0T9VkgBzx``3QjLqYZlKA|A= zqz93cc*&N+@IEq7FEVaH^jqZyZK=aTD`nvuMDt^l-FB&=9C*qaga=VbO#6y<>%<Y1 z9L*(NaoZ4lb+9*hFq|S=%M$*ks5K|xF2gn?Tz&t9=v?#booJ=;w`Q&L=C*5zXO99W zA2T_7`P{lGqbG#5+9%nd-%}fe2N4KQbo*y;7<a%~HSYFB#v2WpoDRBi`of*V4-G=S z^3uAf93fzc^!!FLt+?B|8uDt4Pw{sWXTT|%UajfcD1Qf~<5DvcW#EH)NBy9mb`;E_ zpd+MChK4Oj>jtg0Trj`@<q{(b2_nYi_5yC#vI8@LQDgkjB2a=o-odXcYCA6LMg*xp zYBFrN96zU`2`Wr~)o;E34!*AOKO~!65@9@Ur%-kNcFDkWiS;YdW?PXWSPhJry6a;z z=V0iqjZZ(CgC|vGCeqk+Ddl}FwYzT^4B(BE?UmSB;1aUS)|qLC5A>0v&ecUQqZKQF z?0CVGyj%{j?M<$9PaY}6;Cl@}pOX=#3_(sNb<m_toKoFSD-ntN0}pLHoaUC6er;VN zM3cK|8&lI&TY`f6;-87Mk!xQ*9EE)149McAaOW(hPIzzgcr$K5RHW|2MQ3{BC%FtP z&!V$aB|$;Kmf{m;enl1wPk1Aa9ov2YhaBHei>k<(rqbpX0o=;*?i{BACLKCHT&rmQ zHU8<UV)nj?4@8%nu+c{N&dY09YACJyfrOZna-K!!dcXU2tj3kxCFqCTLgZzo$h<W# z1mlhk2Uc%@(JjH`z-y~2g>ooH=`z=^HE)GiS#IVjD;vX}KLUDVnEeh3Llg|Z0auY1 zEB?a|Rg8Uj)j$s<tXfT*(U?6u`kcTmAW-~!p#2>qK(>cP)-vMV?bb?IO~7S?dtaZ1 z)&9Hqla7tJrB!LFy9M96o=4I!n6xwkzw^Ii2Yw@k^vLMZd>%tvgGF#&tyx_6M-2qj zNPR?&F<y^16fs;9xi)NULA#P4M_zC|CmaQPnO^sD<icqMm1ch~D`MXMhu6R8GwQ7P zsekT!`aRte5p3Brvq|r4Ca8#0r<pvxAGIdeVb!>Hcu8t}Hpn==!d71Y>8<BR#KfFK zM=tCIBa@Z1FuXs$G$7L-*%-c*f3KWw%?T_GnlA#FF<$(-hp!AG0q8;#rd@$`ABc^s z{0JIk5zU(sq&QnLrSF#8{9XG{8TSIN(>RQfb>E&U|EiJkrbkC-NK%AGdNSD1PEICd z^`w!a+6iQ4iKwl<#dwoSj_t7U1$B_zgN4nY5!u(5aQ&OtXk<Z6mByo%uWA{<j4I*v zl!xm33!B%g#jMz12iWrY@_V|No=O?m2xz3t%*W;vvgMA7h4e}Jw})h>FJX|Ps}Njl z4ePttw!i8L>LKo1DJDE~j+MAylC8e_taHOxT4?7E86QDM58)hXf=pFX=XYpq=C~&h zCNqBP^hXf*3j(+cd70GB0SL+d_JtI!3{Fg)cGT55`8voZv3%GD)ATaENd#w^TGgdH zeR$xdm=>`ngk8{l--5r4o>Z_c8Wan&pA2oQPyaWrYiRoK2AdX1_7rCZhX~nBU^Lg& z-GYgp_K;=~gom43y?sV_`SCkv0J5%$zY~RTb^RNBu7ML3NZ$t-yeY*WE;12;#bAdd za5(izm&woEh~jCwDTEayM49&lGm8>sQd1a!%$LwkJS%%1-A_EL0MwHP^hxvD4)$oH zo^k<X?CeJFtxIZ5NlYHw#md;p21H&`Eql`S5$s0Wx&vtF(0NhFQ8*k5Rg!{@pbWIB z>`Epj)T=fmDu9Mi|DfeVO7Qh#$ftFy-^N<AzNt9!+MC|0XV{Ep?E-t@NeQ?FAbknA zLxp0-v!jNixgA&~$bCg|vqX&A$WotF(D}|{^&Uqp{Q}8uM0vTa&hsDB0E3tGL5c{q zumF}O9?pp;75ZN!n_!zK{neajQb^|&-kBmoUMFc))t}O)2CBd<%47{hp70RrnFARF ziI#TqNmL3#nDvfGUx78~ZD>#`c9d#;i(-UVl-`cAIE#PA)!~w+T}tN9B<c2volxEh z(GF{qIC}W>5Xc$(a+j$Hb(o)|iukP-bcWYZO&Bmexgsh2fS^FD8k)ef!Y2<w0`tB4 zX^4Iyg=P^V7b$P*zBI=5G<r$bmdoYNaHHFe;>Y^eoXji?lpy3mxU^oU-5)lMLYJXr zMtlH54qj!p=GPPMjC00}Qtr~gip=^2S0M$rQ|H=+Y&Bi)C>tWwmBh9rV*5;d382lI znbVP%N7)|r`Bq)Og_cOT^^>)xY%#6yZAa7JmF_?)(VJEOCIL+ke>XoopO!%E5LF}K z)0he3#H2+kn94pTiHq=`ZD_Uj8?z%thrD-YoIO(|X6t`POm7B9%+|jI2HzaX&z2kb zViE{52^#z^%FDqS|0xr`#^oT1fwV{+oA>gd$j>U%mEnG^6el=Yj@Y<FIBz0?N$<n8 z0cQ}r(c4#AdXD1-N_x6<v55N1X<9G!Cm@Z#>u{?=(>S4FLR=L~65d`_M?a9q*xPZf zlkmf2YJn@ml-_*TR)x%7e$yYgzy5Ca?P{Q<W;hwC(bPdU`IudQ%pa&(AtMe&i`{YF zw|2ai&p5;T!NtZGRXyy&!dMpf*wT(O6lV<YD#KRoz5<@h-|+e~u^M<D$Vxbme%;ff z;jMHGpmmpVaq#FCDzl(URg7ssO;zl1Z-B!xaH6=ek!qmGL%DXj22d7>AVtz2TMS>I zF%q{z7e+4TJFvT5VeevV3qES!SMeDX`Kzh@_>;9rlGF#k^F9D|eV}=IZi)`Qn5$l! zJm;o_ffp#hM>^v@b@-N0AvwIR>+@tITBqRJrNDYbpmbwOF6<57Bq7;X8OcqE3`$GS zz}F|FzW6}AkbMcH;HIdr>7Nd?@|pXcg^UoWc`M7xqGC|sblwN{?D<LcZx|waYbD-M zZ{y%Ea5);r40w5gMT{!WKB+ocrcF&v{qS&-38b)6dj9dMEA;B#L)or7Q|=j_kJ(!X z*A$CY3DejM1IZ-No^&}E2*ov+JY^=?{CpUDdMXesKENkU$ibEIvV~hv08EO}Lw`x~ z2}EDaR;ZjI)oXnQ=>cChp_-aYpot)byq5g>RE+#fwBo~<d$N8=0_iWLB<u$fjOPc- z^xtlQl6D@dQ_Aqwa~FtfYMpTb$=bXVqzUDD8`1D@zDfsbq%oZ$7Q|~8`${L<LrOmD z#Napt9T&Uy)1+t+5f)yPcs@6{K)<EYV}Dx)ka@2faGqt-8!6$J@%!+1&G*5cg+N4w z$Rhw>oJL6*H$Gp4Iuo&Ox2i688j6pagXFd*o#mwQcxlxy`(k!nduyd;wFGM3YC(IQ zWc8*Y&rf$Hu^<w(?_{=v;<HVpbWs=N9k>iw6ya0;kx>5M*c;t}L_TT8pxB*9qNuU+ zra`kwim&lMlqDl_6n{GI!lCPPlKG$S=z$6!x55m5t=|0y@1=hV(gCq`+y_Ef3{-ZV zuK{(!KtePBaiN`=aV>iXha`U~CXaTC-KYC!%h#nAdnaNL_?na60v5m3O`&MKCo{h9 zq_WELc_)%ma<z!L?*XtuZUm<&ZA~gzDPGS?_UYCnp`CkebSWk^UImN{hA5oU!Uo#S zcCK;5sunjjm$DLJ_Kd<Ruo{}w338i_wzV~JJp(Cww408%YeO6-HnBb)w-U)Y8(=4v zbfulj54)M&;AEHzWt{Otql9KEr4eS%Yf<MbKl~Fll`|!z$+ZgEYI%<<<*Q`?1aDQk z@*e*Agr|vaH&;H#<;|hu&YoLb%pfE@|8LHLeYeV!=px|ppKdpkQR(N7^u*=se<`ag zS0>P0<ie<-N_$t|5LMETY>6(&c9g$U5gKa{pn%{oOrAQPOoaj|po$LEZeZuj4muAz z6w^r>U{_NNrOBh4Y%|AcWS6T#c}>431L_Ox!2NOLgI+j1Fhb|vj*vf!gbPpg%%>g9 z_pP~g!kjM!9AJ)*Oc_FO2gMW~l!{>P{fteTl!|~0&Q*;5wAT~tpBgugv2(702o9Gz zX{s^8gcnrqW#*~NA1<ckO&2&WB1;oNo*dYYH((!RN@i!~IF-TvrKy#51mS2_h8#-+ zS?f0x6>3#MPu!HB{Ii%0bRhh6RZKUgI-cPC)bMI6JA1zV>rFG|F*yrWluRo#ol8@O zJ{`Z~4X(3Kyj+gQ{l9cD{)lKJ<h^CJu;}L!`k1bwNtX|iY9OMhB{Kd4`ZD21UI>rx z1N+@5>&JnJeVVEW2hzdXwCnkZ5mqFhUI;rj#4M2)9`}wqmp*9m&vPhbB%*>%CZ3_z znc=6l8Nuyi0KDXCsQl+#O!eCL<~ygcdRmuFdb;?{7N-kKr`A;4&ZH5<XUzgCr{1H6 zdux^;G&6*iTwKs5Qs`!@A4VEqRJv!I$PaGIdMAwQU;18Bv+63v_Gm5zPsUbCu~^@d zhfs6sAGAZ<Q;Bf}@{?XF84bH{(PagBZSYA`{MDI6O-s!2r^KAOMM6{xJiG$elU0?) zuE+O_-GQuO{X!TP+~HgX7A876M924d$ghbJ%~qMBIGqtrzet2!G(00uq?jP@E$JE# zM{nHWf~X_N<U%IpZ54(G{$r(iaVBwL*^VIuXl+ZNk=K|*4#wvOVC4M*_0L=l%WyRr zlC7g7@NBS@$Z7F}yz}AgQ?1vb8$rYBAAj12g-2~SF|-1ah!8qdYJHmLIJsHR8!n7$ zqmQFqq`o{-aLr3TaSpl_L}A@BBi|#jK-kU!E#FD~^S7%l^eOqj=o=_;Lj|8i-&Y-! z{`Hv(v~aU1*6>;gqMPaoba%&SF(am;;xLd=X93gTprAod1;K{g8d=DwOOpc$xJ|2l z$Vn`~_OY7MW+|$&-^3>H23B|$*bKIn?R9U)^Yv#K)e*Hc;1~0$HKqSE5LYG8%|eb= zjojD$v_H5u{bt3;XJq=^(4|TX{WEZ-*&#X{JF`-C9pq=}t#SixD2{NR^<isiImwgB z_F*tdp0~O!Hk-on)Z(1nRa&6^BeNgUHW^}=YMvEHEC2JgCWr%Z$RdW^0)erO!MqNP zfl1M~>s-?Z-^4wf8JCz?AloNjEw{}?n#0m*`E)w~it_#om}Zw4+_kYK)uT?lCCG*3 zD<)Te3CC9UQA3zJ3{r_*JbAZ3PPX#GxWCZ<j1I7M770&2Gx`SVmT7n`$M0_vL+PQa z%2}!JQ8_iHGxzty%KZeqmkRjm&qY%CzXb!krDpb%S<S#mO;vy8+hPsPUB>_<pNOX~ zCaH}S7PcrgqZN|dI%DUq%y>;yDs=7!j5|K=xr{sRXn)?|)JLd;sg=R9zgTj&+x`#_ zFMM);-R);%zaY)!b#Iuvci0W!jvBw>SNp~fZG=H$Ro0jNfn#Mq+$4;3Ce^Ugx=iph z)bOX=sB8-A^nl$qgi5^Xtkh4p;t(pBvIU(57SmeOro2S4Wz9j|$@C}sk-{u4AD|{P zNDqNI9Bbu;)3}DNHgk14#|JgVlMAuu`#MOVMc!_|dJbg^dmH%Z?W&Q+*6DP2>(ufG z1Fp_B*z;pbm+$W;arzVab$BPz-8tsy$8ejuc9tKrl<oK?c}w^MH}iyo>q-6|ZHCz} zwXgadZRGy<IoIhUd^KqzG=@?clHaK~Qn-;{Lfv8#<{pkdNalh~_#vp&EDm`5jitsZ zA*KW7YZ>gMm<1=2ndBNC+VYm=#eDtwj=RNZP$sUAf&Souj_05Wj-5cMWpq^u)OE#u z>LNyB(32S2jT7!yNt<(AI=+P~$2WY3sfGk)E$k5GKOwB~q3|r^AR;J<A8P*+@16yH zAUUb*P=k+RJJ}0{lW<fv``x_IWLQP%^QDQ&;76+JMneU<2eGrEP}jfjT_z9aS}wD` z_n`=-C!Hi-UPC3Ki*LudTc*jKdNHx>DTmzx$amDZ`M%Sj6}0JRh9z{-NLB!HrZrFL z3<?gDJ~6~~g>35`M{IjPh$pG3cNyFU3P!$)QJBohjLwJQCA|`6$P2x)U!kHEF7E!u z7T&KpIdA<%vFs+W8|n0KN5db2!|zMZ5wrCKrA5-c0D9WIKq7FKG2quMT2daGI^PV9 zCVLJpl>;mq%F&r{Yr)i6?lumLLgw<zcsY1{P%(Ct^m)+ih}rLQM*r0ye&CSXU{&b2 z%G<ma>KxZTMb|m)1=H<|ntWQ9k8EoUCr|q(D`<3}_u7vBog#tYF9q1!*TVc6Q)rgF zF-ZmQNAi{C&mKM>3DT}Wc$m=(xTIA6$s{X^g_E-4w&N!`w9m~heSF$?(@M4c`Q!SL z;>Yn57oU+IdTN!*XE<abxfaxpyBT{ojGMt<G^G@yRAj44hDu`RCtf%O&7%nqF{i;v zL&9RQp-+@EfGC81#*rEwN1{<$zt@jeW2ISJA-NbsF&k`-sZ}rcYx|JC6In(FB=BF= zM_~R)zw+X!L}+uX0KM7<SxE+zkQN(B2~V(Y8auQj!-N3q{q0$b>*d@^Z(+Deh~(Rk zP0^(__eULMf6r3ioV8yZ+?$+G-Z=f`SI89_XUZT}Ry7|s2?@gvr4irqwr=1R(&AK! zXK)jjJ+~8*;p8~dFiry6=SZONQ@dS^QGGsEa_S&spnBlr%MLV>96l>S!H$y-d`>+> zaxw_n<Sjy$8IhT1y3(}iBrKGtV*ZuQ6$7MmlAG)et!F?&v6FFdb}XO)6bj?~iLE{T zPl@jawteim{dV=-ScVFP=W0T>k|4Ielg63x+Q$x2m%o0k+nM{Oy&>VLXLlVdkEB&9 z?l>U>i95l4hub(-JXGpnD)EbWTO(1Cuy{5|@<6Q}V62o^*SsLZ^3$*TX#b`B+g+&; ztlJrU|26BVE>DLmt%Wa*1QQb7^1AvRge(_MXSbt98!DXo$hDL0-BdhlpQ|5{!SYjl z@rsxLLfEX@+KUcjRUE{}4>h*2hb?C#<uEWJrZ0~;3ZIWIlw}fU{BhwwY-#Zx%1nTh zj5?a%%7ra#Q{5MgysiJKV1@?nFHD(6q~4Z%tj|cAke@$@=1Ff^rBkhaUeWRbM;s34 zb)qWQ*VD<5uzc}DBfc!dI}Zcn7r)Ew!}ce{Ma*g_p}%&!puau;+R~dReH%AR&-&|t z0aeZNsi^SkDqF3%(|ObFEc-mxvD(qFZ=M}$bV&d)_LXd?taU(P;Oqnx*7#w8MtgY; z!5+Ky2t0<A0`W;W>4#w3yC8+Dg&_QD!s!Oj2IS3?HR<}p_Fu=2%#02V_tnMzzT0pB z3e%t3y<!hmG;ZYmi1n!YgQpJ#jao=bGHkS30ZdIZc%Wlo*8IGZG+IOQtS;dFx;CBO zU1>tN%%*1TFW!mt{dD$69nzd@#;2Q!-F}tCqUetgLdeJrYrkx+`zX8}gZ%G#7~X!T zKMV{XtsbJHx2Q>7Ybj%n0uY@%a%W8Ms}AnW*t&BfjmZ~4jh>AlRJR^S$akw?RZ-;W zxwO!WSXRlN+67?1`GCp8+m@sc)!aK2Ya*~|n>tTpbEfTogRebDTi`JeUMUj*O6*~? zVc30rW8zSqbdyA(8J=k04KA9JlL`MLR@aSKiF!H*;SAHqr$CO#5tZeH_*u~1uG%as z%~aOuZF+2GJO;4snOTXm>L}?%k}pEA$-<M7IWUC6-X+{tmGQ2|=E3)Ks3Eocxx4M& z{^)Gd^Mq}d0>WNzKV_#E%TGZVnTCdA*lC0&FC)t{Iu{cAMquc0j~nDV6}xc~KL8iL z0zn2iqHWM`YJG|biFS@iEIyAux?e}3h6JEu-MfB{_vK-x2p+#dCO)G?ccb1&_(Sl4 zltI)VLdYv+U}(-8wMPsROV8RUI)|mh8tP8~3fEtd`c~x>(8Z%_?$`?3jdf>Dw24k! z7{yw!dD9IKL_#N2ALgnMYVuPnswf6ZzV?9azf01GC7R)DQHITuag^KpV3m79!V~Nw zx&7b5scmdVj_>g7Pe29+2Su)<t1_QaddZ|Hnhl51P&_+CzNYy$H%2Sz&%D%*y4`oO zW%&`y_cJTT;og;y+yZW1>IkMgLA3$KjLG9*@@hq}*-t3!7WSDbdl+j^a3a4j2EVxl zit~CuL@0g1Rm)4MBsy_Q(H2d%mn!p$ez;NR>{$&lpqmc!fSHB}4fzTBH|;9TQND{r zUfjQblFd#gCc0o7X=y)EfUt#N@S6HPh>+iL;-*h6E;)~Oa-oqo=~cMB47PMA_9<A7 z=Tcr>kk$ztaUZ_6gA*jl7J8`WoPj!BO%L-u&;XlUpfUI!FdsGavG!e6K^>P2%-A=g zKTSv&Hgli;m5}<^v;&nR9_sy8xM4L))%h*3==}ZGOIL^~=nSH;#*$2W>*yR(L?6_u zk0`~UXN^lQDRO^sK6=pixta6Z#h;Ups(#3H;S#|l6a`CwhkwI+iX8OBbcgb(q;BR4 z`r{dAIMBcZ$9eB*{8*~d?syv-@s?&;v?TUU0ns2+T`3j@;}k7#DSc}39ET!g>+mU@ z1m{*tB+APbfRVDMf_>8Sys0^GKw_of69o#<Z(ZhNxD*wio8x6*RI?=P4I*xB8(w=V z!n!qP)Gy%BFymJ=cJ>)Bw&wTJ6&7KinVR$TQRvxX^1rp|dp?igzV)p~@EG#u+xI+@ zp2^Iewj8SbOR>nYb!96#HVL*zu$?B8J(SEP32*YF3WG2(XZO^0)=kAIVr{jNQK?93 zW{vZ&aM@!@QGgT1&#=O9r#E@j$WSeT{Lz^muWi?86skl}r{APW`=Z*{K9KMV3%xzA zNH07I$AmijVDa;pe&<erp9Q7t)wel6vq9JwR>x?5C;{-t_ZR6}FW43p8j!_EGm8sb zt^zO{vkRQfiCizgc*P^9DnoA?2G&kOq(p%kAH`Mj{j~Ts8DVj!FVaHd$?9K~_J8$a zewe(bE##ue%)M^+LF#hiwtrJk?N>KIj0PQ+aTC&}vvpUI0w*y5lc(PLQI54=;95(o zJ}z}jue0h({u^@%zu>O03B$Ihi4C;bo0$lkwfY*g+0uowr)g5qV&%W(C(dV6p@vsn zW7Ux?W!U|5===IRE10Vt++18Kf$2WFDSniSm+eLz8b8eW%`u}$S+;5tw?p}$&IR<X zQukFZMkvX=18lyQhLAh?Vy780-{FX%%IX+9-KVUDoBmhDk!Mw5YSPK?U?thiEPWwF z1?K!-JA8^4RiqT<rrKgL)D@bxq=Q;92r8T3O;!y2j5c2Tj@2c!{v%b$@u`>l_%K2j zLnq-*Lbvj4HtU@CQ72aBck1R&cXXE^Q&?-7myR%>KyUf~Hy>rhb?L)M^R+2~P8O1> z>7Xzv%%A4fLX@(+AN4R8?HO=x0}`;K$jT}z$huBrkMp7K^p3wT>er+TKb}@`j)vBB zQtF-|Rz<X!Xom8&mBbu0mJACDNiy1Mh_eR`1@2xfPxWSf=?=pa#l>K95V@_j3=hjp zpDbOTP2>Kazmy~o5#f7gb^Yz~Z(mB2EhdWpumD1-D*fgIDP25Pq^J8PgZy<W{rgOO zOy{@W`nxNbeM#{)jrhuA338aoeCvc5Ave4I&|M|_OyNCz1^)|+8xAcFy%LiT0(6Az z$a|41h-Rf|vB`soWi4G48bqB_mlKe+KgN4+hJXr1QEu<y7gvi3w1;y_j}l-}b^u+J zM747vt^DXan78`{tJp*b&N;bSbpqAHs)5hLsr;o@S?TD;hu@|jWJ0XG5+s<G*@evH z-9{on^Q5t&bzcP>@+gF6`emy5GLmm6CHmtL3%(`F0I^UV2N-i}VWj9*U69Uyy#&V* z)dI<?le(af1I2Y~e2wq+Ah1#?(U86|fFn_Oj=tQqvIIa-t<F$V{Pj7J7Q5dlUzih} z#Hrnh-#&{Ur#iy?oAf-f)80U&-K>(9h=P%|3gbFtP#VVnl&-D}@H2t!U<WF{fnXK; zw+ixc<N{7w$=9K0fvq`Ax<fYN=d!JvpS_92wK9oU&Mb49TnhgX@1qd=5m<ro^+r&s zeAS?45umWCKZ{4HN(zgWS2C)P?pCCeM06F-MUWq}CIJ31*#-e5<+w!7sCB6}X?yQ% zhV>bPukKqp?kkafS%gzE-8yTCX`6Ud?UgKJ@;HXJzzpBPj!zs8MhX)d=pQ-XiaUg1 zKhO$>nGNu-i#p_O{)3?Z4&4@c$<(kwvwbIbPDjs@Lk{qiaxBcb7-79}bf@WQ>v2ZH zUtyfcym9!~Ob6YfUoK^zh)N}+I*j6X9P8-!kIVMo9g4-Rycr`;^Qw3m)OoMV#h^(Q zBn|xJ@*sFe$`IL!WG(HhUku#le)#+hDUhuxV#zh7aRFmR+4~e_bV>!Uub6fw;}XRQ zZ@z77hhHY|lzsY(u`62CsWs#FuhQePWg+e(>(qmNGvw#0NIA*=fn+JEXDb$5+ptIq z;Kz(wF^pH#vf{E6%In}qOVtO0t&+syS*A8}7y4@x*WN5HGczK;9g!8T*_Q3p?hk8O zX*#CX8my^qd}*Zec(lJf%jbW1&J#_^(^WK0>K9qonG)IStHGedA&mri&W;k#WV4fV zakn=^TCCDZo9NSqWj|0wU}I|S8-hW`__hi+%)60N%;@s3V3`kc4}z!+|D5}tC!gUv z4A2yae}{GPNRDa*w6}L-MC226(k5Ib)xxYThNL+uHCHrgp)p5co)d~bab2&{q3Xnb zsQ&!5E-u!NeM8ojpKka#Q<jKCZKDa}Lj-T=Kwkog*583ZKR^HdMj5uoo=$HRHV_gR zwyEyeK-jmFCBC0+V8B%U$%$d&8$zLBfU#fAYILo)YA=EJ*L53WU2K+;^;h+00<FmK zZwGHwm4s;{xYeSj8WxyhVvuuhJwlZ~8#c*pA&A^=UcMAWk|>uoy_0G`26Nx$xL=$6 z<?K5tL~z37%M!>KvI*g%(!mq@M5efTqp~ft*&@}Zr9h0TffY}VWRU3=cO)y}JKF~> z`fSH04hTlhoYjR9bLfK$EQG^>3}4fv@}Ja8@rn~rBqLMl#Un%+VJ8LK%pVP@mCfQX z|M5m-QWQ<f`Hx)J)_~^m)Ox6?Ki{dCX03a!Xm0qRpDN~0${%X#qjAS<d-dUq2fmg| zk21Z#@$dNnL?%6Px?>o?)x~$$n0oI!XOB@K!XBip)!?~yaW{kD+(B}yi)f<m?Wp8s zpxWZi#L<2YF^L_Ui{UU)swqaQe(B7Nf#pCHROHjty^byMDq#o(*R2V0l>G7tA7v&L zy$SKc_r7gs9{2Z!f3la$t5E;kL<J|Q3NK3|Pb4dDp0+dVSnOfUu0tw&xAeA0e{;F| z^E;C8gh62s);u;WhZO`K7fn;Y`M`S?@vm%d(i^$Hxz<%ThyUsDO!1JWeju?13}9k^ z9(=i@I~)agC0q9aSBf+z6FVwP_iPLT)3Bw)9mJm*cCSdhGhviZJ=on%=BH-NcGb{+ zi1bUx^f%&s*c2P=|1HFzG>+azvlhw|wGjGX8z)n7DTkNIJxm=`X7qL{tsjZ#0!NCM zjb4^vW_MaWyqGy;F;U`EmR)biZ@I`oyy8UT?$7G8N29n>fq{)%71@iR3ir|ftY;Af z(8gm!xd|#og$5(2n7>s>N=h<~D5lBM4XaH)A#xSK%Q*7C9ntNLef0!}cVK;~SL4f_ ztn2Ims2I*7y9gHJDS#81yGZ##6YJFB4gA40>2LCM)<Pe!MK=c}MQ1>iEp#eF%(_v2 z#vv|fgARm=yuw?E)Y18S8i;oxOhZIy`CG|UTS+nTn9MG;#IKlSI7kU_bTA4OwKlS& z?S;x*Nt2=(7L0-GpU5PAZjm6(F03wBg~X=6WdL-4mT5lpGqr0<#K_InbR4kAbw}r! z+v@W7!~3X7XaDC~fB<d406@KVHhc0<Ie>&1X6S;az#MdQO61y%cd!lB{C~3mWWTo` z)T{PP7mci~ZZr12YDV$<%!E!vXUc~nIytu+8i&l;_$t?^i)C$7ybGD}hI19v{Th#Y z&g;~i{P39>O`BilBNAe%<VnGEgif(ncG^fL%0I@40Pv}6fU4Gn?x~|hcdUjDd>q#U zEgA??{<;f_m$<wl&G%UfP>8mc0TSv9aZhYb*}jrQd)0cP%v2^T>9ZvZ3G@~L3wJTv z6?I2X-IS(3mV0rh;P7c5x9!sO)ggr^@ThSAL$n6SP!M(fgj*UG3^s-`Q1%|SObQsm z0La++78NVI_f$BLe%kpONZqr)f)H%ujNsOZ#(0dMX-2QJx_PC&;Zt~0XX&fWtiH+c zQ?Odz6&mO~9WzNNgGW9crIg>p%b&P67m4S^%UFz$8hw}A&|<M-$sqfi6zMjLj5{Gk z3IR1~R*POe^`g0GagZ(j2NGh!k6d)-QEZw&4>R>aAN!>fcvxSlwZdxWR^<dUKa7*g zzIAIeK=C9M70K&ZCs>u_0yf~iL;+1+xcf)HTg^R{7|=gN`F6<`RQhmeAvjY;K$OC( zpiw61&pa2}!!g}uvPTi^BWtaZ0zsv48o5NJvOoOHHCfL@+cWu?;YVQ~j@iELG|I14 z8r&=Lv>mbRFG_P=B-1qSOt%_y*so&`F+XdqoBEiVySPviKTOT1&igU!k?Cd+y0*s% zd)VTHEq0dCoO4qsFO`GES+vr0<lzdZNV0{O)_pQ)?snLb7N!DIvV>3_pfjSDOVIYf zGRQqpNqNiK$$r~0g6rr)ZaQ?Ap|1N*osMXIz|Q&dvVmXmL``ur?ORoQ$wxn4VH$@F z!!n)eR$^O3+wViNI_a#p$-tq;ZQ0(`u;IN<%-b&GI$O+Q=VW;+%aZI+>8aQa>>2S< zihQX(uUW^vJYC~?&GIuI;tQ$Wm8u%bfRcf3anQ7PH_{hXW8~>r3AtEu;m6h(HH2S! zhY8lfN7V+lt&}eTmjdw04z`?$IA9A1dzlPTKR}wmhI{+L?m?Ch^>=uZW_DwdI++?K zy&9q<owk{HN?8VuIQ|6d2YnA!Ar6CWG6xR+9Aec5)dFWV2}?A&nw{kKvbgRt98HJK zK6!asV3<Mq3F7dX5=-*b9}=g;Fb0#&Y}I0u&v6NV=~mS#s_NaR<=CXf1;eWsXyW4U z^4r~E2yZ7+C#LY(m%faCS|_jN%y$&yG0YNT_&cQ-*ef9^T$s0KD|Ze%yOz?d)S&Bn zpebZf6Lio5hZV8xb}~Bpbvdq-D^qv(@v4Tjx)T(Ri|aN;X>zV_EW*{u@SFQYz5s{D z;RBFzw-MLFxH|1CuHfpV4>|crQ&xHi>VwMQ<6bh`4O+9$>jR{hQohwcrNGKG_jR5s zCo`G^MPHT7#r?y1i-vYW8H!Zsek!RXc_}qAx@_KSBvfV7m@zZs3_5Nk!_;DjFsiKb ze&>wxpSe)46_6ka^pQ$yO7C!Id>9U4QlbN4b$;enq^*LequWPB)msg#G6sjpF4d2| zqY`e@{J=+v28BH~aZW7L;&H8K{hL0A3(L=hjv5Z9-a8WOK4BwzRu1^;p-_KbB3}fR z!5Hau{czVS#J}2U4nEsq)I%M8+dLH;o$$W;U}$V&TR}f!|JBuW*h98J%Fj&aF06MY zrkKRmUc&!|_R^!?0~W*$rnAQ9pzB_EN+N1fY%t83G&C$>6bZ*JkM=-oF^n==drvUd zULQ?N4FD;U_9(8XZMXh@#uG%1ql&~EeHivI_F&F-j5~mJgYdN&Txs3}Q1RInDYhEt zvu&N(cl<>YU`sQ4WSqCvFoykwiAQ`aTX$JhBFH?!tLC7`;jR17Y-}>MJMC~PHY>Sq z<Q)<;zAKk_axeou7`W~r%oM3f{q0q0*mM<&4DSu2pkh9Mq2SsfuI0!wPSXcEfX$?@ z4{Ux{4JHvs&Yq6diKJwo`b*2r^etXhmvYDqNxsh%<#Q`{qsi(MB|6ot&1n0jbAIi< zYskkQZASd|b*g;DU!1p()nUmb-&xCSO6n<R39UZzp&)JcMyb5ce;f9j2l;PIEYqis z<Mc*40=2uvcXzJ~inTho82qh9l5vqyB=5GKlbN-*1vtJGe)uqg@4oCr2qV89N;iWS zN?afSKNVz0IIrVX@iV;Z6IJ42ZDMtWLyvJMzjS*|^UJ5JtGN7fxW|wUxBi7h?L|D} z!=$r<#8;6ab)IFw9Q#|AXE;+Bho1~d5-%p53Hw-vH%}QsdTFxJmMG##mO2_l^4eH1 zJOtB<M3vs2BE0RAK0HEN7Ln$*(>5ig`-+US2q~{HG{rV?l)~tbVH)G9FkU>`E;`Mh zGmimmq&9myINM7V(Bg%1CdAZABzkSHU;jqT6IP>$U`7S$5LMiM;aFJYV9E%nK8Mfx z1)_xEn>thp2ZCa)z`)ZpgigED)05RyK<oV$ZS7;}Qz_T}d`^_vn@?l!^$<VH3bqi@ z+<P#3$|PNtrEwJ=y4@~Rh`lLbu>VKLcl`SkgQl%}N?*!T>ywRsy<w)KjXzT(+WM&c zc%bC?e0@Lv)7Z%U-uc11syl1riJ2g#u%12fM2|+EbBixB_{GJ{i3*|Jlfxn(lKj{e zlJHpwyBhva0M9xw#YJY*UcCDc_<rcunh1aUE9S$QfYiTy3?rf8`%wW=E)Vhg?*JS` zlfoJ)-RC;<D@v>LR4uUB|ChR!(Q#cyfDv#t0%~D+y%PPz+ndx@?cExNH~S={kzMF) zn*t9$p9@!a$=mM1Fg*C`0Bqbp4*f%6h^zSYWb2=I-Uaa3V}H$_&g@wLnSo;FKuDtR zfRf0}C55IuNfb<^KINSnj`8m-xd7Yk#{F|Ov{OY@TG6KPzU6X@P$*x2a}^!bP(|rL zuVG#!7OdcFn_x}zL0TX`YMhk*B{i_<#Hzy3Ac&ZRP|mAS;Q+*q&w=pO&q3;~Z-?}0 zXQ0!3`QHc<T%_B7Sy|L846~d*{R~K>)<nP!#>a3!Ds!7cEwH;tI2a5H(~f4<jf@45 z>Tu&)i~u8G0|W|vK)jL&l34!7rQWQoO`6^61CqsBI<h%?II~~W5rU7Lfs;fe(q`=$ zj=;`=Fbqw`VIr1*NF%Nqw$2Bi&j+WRavD7M+%q$Evj=KeX7A0~Jw-B7Qr3hgi4$4F z1d!<t(&*D5;-s!=AwER3$Vj4~L#y@)jE+=`>5G#8s*cgjNtu_Usy3>qq>Or%#l4^z z!IN)-rBO^#3$;R{sBLOrir4b%Dhj#wP<Z@rC=A?+njn0q3MqzKreV|&5s`+2G~l#H zVL++zko`Drx)I?1dm#0i*FtLbT7Y@;Gxetb7mlFm;wvwK_~^p``}XDC({qx#=q-@C z_>#P$lTZ8|_I~Q$L3r_F5LmGS0xMU7fB8y0#to142t#=d+T;XfxF>66g$0mW5#;)e z03%R01PV>C{9b4LIIB&XU2TGeGc0LfOUd_y1jofFkkFQ;$SIdWx_M6a6x1+M!*Zs= zEIklXIrAPFg6s@8iT(s;k$}RYrw>=^F)F9D2VjuQ!}XiW7|vDzH6Kc0!0-+<8dUa~ z#l4`YSdd^brAJUBvoy+l-h`889V_?+T{`wueEu0(^PxF_3Dh)XO|odOlu>->kJD9P zy0(8Gh`;&Yj2PF@0O^(%6qqp-@(2pf1tGpQ`&7A+?&*cp8{blNXW*wdKw=>CcNgEc z7selcXr@f)lv7~I|Nds?++hbSfH>@dF}sWaBVZr`MFNONGO%^?no^rIt4*%W<G|XM z#o^r`#0w_HWmri2aB`p!KB%~wmPpN<txVJIzpbSKN#kVAR3fu16d02j?Z=OPB5`ym zC#_34qUcwYn}STLNP9wCGLhM*=@>VS$-HLM{#MOfrryXi!9vZ23JIvGRg#9Kma<6Z zm?Dj(&^f;uwM_*=OmSg{Tp@p*5|l2LbdUtMQun7&XrnM=T3T_;OfDFaog6i^sgL|y zM(d&&OF9~ZiKm`YT**HSKl2Qv!eJ0YxkZ=!681eq8Tg4>xnm7Ttq5{`Mt~8h8v=zs zAWld^NMNZ6AZnBAx&do&$I{Va^Q&@QBs-QB6=}c~%R*Aavarl-I~P&~CRkYj$~qfl z3)Z?vLBzK@v!&1<C5bEd$)h+K9Kvl363B-H5p=tro}hbh0!H~iO633{0YoXF%m^ye z0t-3XXU9jA^Lm(H>4dE?3RY6^LOPG%GG?Ucl%DRufC3Uupy@b-S`jV(5IGte3Opzn z3BG-h4q!B<5c=(8N%jvy45>Z_#}MKZ*d7WvM9O;!)DobukoNcBgdRf;iy;uAV|b3j zCPn_j=OIiS#`dvoFZtc1F|qj}NMl(+jLB^ZQ4)YfNHwkmadHq75zdFiz7s+E{&yhx zk;?#7{=U;_dYS(HZ`E^pkogtL7n($xA}!%Vvr_IxBA$Swd-p<oDh_?imViWNU4?{w zq(1sFNH1Dkcn2n){5zBqpae7(bEfbiBmx1b+3`yS%_0m9zmS&DQKz2a$ncLSg^?;S zgQMYN)7zjxA4*!`VPXUrfvOQG^Z{{-0HU^P?^+EAP8>Z)Dh~mh%w}9xyd8l+P;r(y zREsE9RA{zc5DI_Vr&rts{N1sd#73M1uEI$vZb*B>3*qj=Z-q!Q1dY-tG<gQ087HG{ z{)5mN+zH*m&Cu#c6Rtuc(<Tp2bTM9(CXtXrf`}fICe21_y`j*0D9y1-G%KQ(N0>tG z1`!EQ9fXy}{DkHXgPTF1Xj|l9-!dO4?6M|NNMjOi8c{PvWl`{haDT2ig*c`R=v`|w zJ`>(H5VH59=vy(4feSj<Aoe80G=Z@1A@s|_$+j041E{?NkD}nvh3d)9%=s{Cz34f_ zQ3&oh1_h)+jF^Vb*wp6K5Z*#TX7E)^kp3wUfBS!U{YjY@DHes~=f0fdD|>{K{?uD8 z$%NC*Ud!&WPD!eAp85aU`w}oWt}5MArM)F}x7uyH?RaV1iS5MBW)k3KaY8~^vH?lh zK9=w%4~Au47&5E_4BPM#J|KiQ%zMlLVZt&)LV&C!0g`b7!~utdY{dK8Zuh<~QcJ3O z|0$`|rBX>MEhVX>I$x_*Rrl6iPAxkBJ)49v4jt>Gp(B03v?)1_kM*9Gx`(xWN^g57 zNr=#AMAMJ$Fglz$=IZLoY};uAkJq!1pK*aJBmG;dverzVErmJ|LcC7uQH+sa4`Qeb zQT*@#*NNPN$4GDhNS(OvblWT($T7jPK^Ao8z0s<DvuZ$G!@D6|l@_9v7d4ZL!)^AR z>S#c+`H^o=cIiwyGJ;rLe)5%JCL}lgVe}$;YV2HI8&{4^B|3>e$D<pvrV@0|(1!4{ zbk4FLQkPF7EN9g+*kOU+RlnbLWke8EGn5B_NQrD9n6((n0E3^GER9H}McZ$%)=QXr zk;Gr#4j{B>ekJVqZxS!K2oB;G<jhgNU^~eVKS09ZK&p%ZW&*%qzKB_8=DyEq()a+{ z9XWo2296x1!~(P{8k!g!qKReQ)Y%Q7l_vR|my>kKW$JF`TZSenwr4j@J#cU4n9+tu z)0R<rqaMTB{bSV_TV38p;@k`+>j*%q!MIA=5Moe0$0`Pol?=f-*Wo}@a6s?8X%`KM zF#yS3-EW4A<UW>K;Vfk?QW$0;UY!t)28=+O5id*Wt10X10Z7W_7E`UuPd@FP-b_1& zE(O?N)12Zon+VZ!<LA><yq&IF|FPmHG*S&-{m+oGPnxWnilyu2$jPiVXi@-@VGY0T zbS_Eaw{J~Fr|QH4HET-t?<L`xXAnUO0S%5GC7OUnhIv=W>?+d%Hgvu=+9IRv6Ne7d z;E`iSMar?h-TNr?iVkvflr4aj+1WXo{(t|MW}o~6EgU~i(hOu<gx@V>cBO6Hy#&5b zHY7}aM>U3Bm$#7+k0DEI0%lls>Ao6#tEdfe3NIIj91hf#0|K!rq>{`lf#)&nN3*U2 zGu=6Z5Bz13&!!p>KMZQKRzb67W#1if@+i3`N?5^8PgUwZ*>KWEW@<*}Y=$Sd&_UUy z1GB1x3txj;kqpqYcX|^&IeH!q$Cj1cttiO!%HUTar1s%Gg>kdIz~3(3PVbl8Y7I`% zdX%et*LBP0JX*?8l&D$X-MZ_jJ*gSD5sAum<NJx=Pod=dsMJBcL`VIEL4sH<o4I_? zuBRywY@^w!X-e++owm5-ZCAv800h5NQXoK;O+3vhJkfQcd8inJ2!GaMB*Yg;<heWK zaG+)!(0~nq4oUeTUwrgMmf|^Pzs*Q*e#OK0>f-A+>Xh`W_C}^fjXHMo<C4j<=Rxz; zH8qow4Vt~hO-BQ=$cutodJPBz57<o{jlGhdn|LF6#RYs~<a5O+1aqgbdn)zuOe@C= zwq2l*LC3)S3fc{g$L{GZG@Hl`8c^W$OcCB#^xBS3(kgLBc39O+4c-s{Mzo+Lz3qox zeU3z*b)vO9+T@4~mJCSNnP7=V{-~UMu%p-D?B+fsaLC$p*xHI`$`0uK`)0&ngT}jh zdT6viZRTZA&WGZNy2YPrQ0%~dMbkrhfNC}RsCFA*C;NXOW^v`TD1}tsR!J?0^FWU+ z2tdSXNBdFIjp9@;kppS4p{x?Ra=?aBW2^}gQhs(u#SME*9|4O8Y8MkM{i?l@VBN5c zH#soRfk(*2&aEYqjC)r?@<Agq(tzl}oY8r$c1-r_oLeD>v2Voad@q2(sRh}=;DkX* z$UU9@o+0+haN{N67UJ+l=I;c#;LskEd;l3?ngP%ljP}rp=t;C784KM7JJl2h#2LCW z@<CebeN=PQXcK}Z3;X=RpF-^a!Oqb2q@-psM>KYrPsX!-KtwmIa`_@@XaUi~*c6LH z6gyNN2WGI?iwIT}L5OD)A=-l-?Gy-yX`Zu~*MR0<c%BZu<85@(SHDi)HEU|nW93}{ z%k8MVl~fCL10Vv&XJ#jCUcqP)VyAO)cB(~Y>rB&n4wMcy498TWn%c09IvmtTz~UkJ zZEAvLBv{vhrPb+jVUE1}9whg`b7i$BMS3BNxeU)W0%{Hb$z>IXH_ai0LzT7WK{*vt z2NonRjV*L3pFCA8Im{+kXSCf#=ZC&T0TGdz%AtDY{?Lv!sc~L52y`Q32|1}-LHBrc z;6rV12CMNNm|=04Vu#A(06UWp4Izs>K)K%-5Go)A=3cCs>OSQZI`;I_<@Hs@S+l$T zMEkFLJ+1!Mw<&z_s|;J1fA%jl`osUA`RAUcp3i=s!rL!0tXGSJxP2e1RzG!>{)H$m zcnfJk#3{T~d7;B0hXW;Zph{rFpkJBosclG!WbVUcR}(BF!P;(@M2Jq3yZ_JR?)y{8 zNe8uaO-9yhceAzwFh16k13>f~!M+<w9K8sqx&WtLl@P)<DE6s)4f7pzP0;CWchVVw z+o;`D;rM(FVAc(vx(soC3?0RhoK8>fd;o+6hRI_Hd)te}+T6DE?bvF+mRff<!y1G9 z)!vS9#A&lYJ~j_+6@x(}17u<t7(p(Q6L5l0aAx!xkp%E_<b5~L%Flj|y54oY(yI*M zH2J{&Dd6$^;$qJ6Pko#=J^DCutknK~Nc(>v?s9dgt^h=wYauf%L~amgCaZJRoK~8g z1J;2JL&mXvENek@ky#AuHqb$R1S}q!qXDV$1;Rb!8h94kj%P_6KY|}{{HRw*g#d`l z<su(`cw?FsWW70Ch21yJ=~7@dxwHTT0&I_ar*e(dFu1jzEDLxc_Xu^l4$&I#4qEGb zoR)i@B_RkWb<Tt?ot!eMQj!cpLRFIu@;94W6$+NY4%<qekXp}5+aK<K)dY(db_{$` z5_1|8Eu6}^?F_0dHp+7!KLCFanCN(r&!Y)Qc}vXm-B3efz%dCKjNcu2k0>F0b-@65 zG~)OeZpAK|sUZMSxPY<rf8ld9vvVhTR;-}O`|hCxgC-p%1_x>M$3LX*8{Vrcsmbo2 z0YWsZ8kO}~R{&x+fu|Byi%}r#LNCus8h6gMF$Zc4Hu7*vf+?hEGXP@bgl-of5dEsX z9W@}wpV>{bM-S0iq0+<b=D7_?_V`H-_v9;9j<Xg-3qbU|EO>GY70Lk!tdol?r{oP% zhifmL6aES9nto@=oM`uq(^CIY>h>R|u&18_?r|iUmMAXyDVpq{Sh5`%&V3}wF8GJQ z+&Df$UTK8<;s~{gqZAhVDdIi^{}F@S4RP4&x50OV7eb0<L+8xi3Lwz{6b2`ap#9)^ zu%OFi5Js90Cv+MR7b4)vJojRR05&rGL1h0@Xttsxw{OIpVOFFi^ex1O@EjqGZCYa@ z#3hUcx!1zI+M3F?5JO@>Ap}tBLwl#^loK>kVlqh*m+?YX5-9|b9b9fd4cE$tZD<KX z^CX90@)uZ!zTrR~LOtdBK+s2)bQy^#1td4*_65$}!DBxZBN4+V-y8{T$dWz^%~cq@ zn?+wQKueKCJ{PMfnhgN;-ONHOGJ_<9^`fa?KUB2qk?;H~b-nfN$Y)nuK#JR|_~43; zez8Jb0f^j&8?8l1Y73F*#RJQ}FAlXR2O0xx7&2c8iEXcY(+GfgO*k4F&cDV7^{e)F z@&Sn>*8R?hj?xQ5>mg{ard4YXQnwE_)>WWn$J(N;w6z6kLbE#Q01#uANx==-1@Hmk z@U<6(5M9dqM|ytYy@*)V3n%jSjNXbJ(1UP3Z}*G>XfW6~fX^OU0?^@grOZ^a8!fQM z{tNRENX-yu%{D`<DscHzpGLGC)h66@8w-aEIt_>yk;sHO@X7=Ub0?JS;^c=JQS5l? zqdkv(d`4b^sSyBy3dlU*nGC1uspqvdQ`_VF0r=ow!g+U$$~hA{#^&NOHIfwfJ%0p! zLz^&l3|Z0<)F6TDJkbv`r@g7jU7W+j1MzYI);jD&-?l8({v5{4nd(;ogq{icFA4y| zj`mB!C?Cce?E<;B9jEwzZ9-Wd@zjg^m^Bv+W~xka_<$TP5(TSRmje5lBqicxgjP{v ze1b;4|6N-4;g49=g<-q61s*mmDDz;RD_mx4C7vb)j33Yvo3TlC0hxV!V5)!z*P#~R zKm&sfBktSK55x$7`1r@x2Ba67?V<sh=|4gDKPb~!Y`wzeBv!j08n~R^x%PJKr&oR} z;J@O4UgUf|Skv>e$muOpoa{I|cdpQau+X=``%Bue^ck#-hZK#4OfIZ~!%P^Ft*S<T z2$*I82n)^(LOc+L?M!h*u2PA4Z9p;PkI<4oP{KQlwJQq{{CyE^6HT)X-3b5@1ot2S zhN`H5Q@HfX3rIfoG?F&~>;wQnK}xvZXOX1vk2^`+`Flm^R|+clcYJ7=;v5{Q1PW~3 zO5w|1OKoRwqqeioCb2Db4<`l&U{3V}O+E5kn)(goc4(5SNh7y?kCuM$@1R|(6%rRF zp?rR_nscZd0FgXEiSAl4u9_?>zb)D8Cn*$xF9=o@9BLvC*bHnKaNdqdL5+Ms>}rB# zhddw?M~>4y4|CYn?1iI&=nDGF#5Ovs^Unr%R%wT%&f*#nzdzd%-2ouRJeJ#fa~;t= zDFC9J!O89bKvJNiy26ptKCi6v!Umh;n4#IBl(V&&x{X+eO<3b;^D>?{hXmG99NG^F zCQK@^!0pSvoz<fJ-}7Tl3qUfHW{iQ*xzdL|OwxH5WDaXL<ZhU)U3wYG{6XFj@dw`} z;lCfzRw-$F8jk1_(C(TgH~L@s#eb&u*SyZ~6gP5;biU~t<-=fP;A=P2#65Q#)-C6t z2sFS^yJ`hMp#AtQ{6DJtq9_4+m&2m`=g4hPxwRlABi+bA4LetdY{~%!8y@_yx4{-S zOv$zgB}3gL1v*K>ikHOp%ZfRZ&8Eldb(@9bl?)c#{jB!40qKQiy-?j~Q(&{J2Y%zp z0Uzo(|1feP`~XUoq|~!h-wcAK@QUn%277w2rZ;CZIHhF|<!C{?Z1$|IGsA=i<}X#y zS`}ojzoTGrRv~K-IQNBJN#SRD@IK~qE=6_P53gT<1A6*;<W@I6!XRg1%DAjaj>;Q1 zQQ|wd<pm%5X)+i|e(Y0}yzwT0>e3gsIW+R|A()Ywr119bbjm}I<OLu4W*CgD`o^ua z^2;|@C3;ocwwKZRJAO`LS{tPA&x-wOn_ux5>jFUdV$1&QRWvIY){t$&hBX=KX~>~D zgUaRr%ePGlHnOLg*|S?XULzAMue-z_^UUmPyP*dldZD_uqbc6o?p?=ea=}=r;<M)g zKAspgUa2hJda8y3J_?2dO4ZYwtLb@Zw!c=Wm+aV<JxyL@@TGrydBxspfuEhLO<dE= zAZL!lRK=vmBy&=OY_LCrN~-^bx}W}zO=SZt9{@;M#97kW9IHu5zWxUIrIfx%Ex+z^ zO5F0VBrk=}iH_#se13d*m<C2hX&62f6Vo%qT8}vd1vm!Nt0?CSF+uI<W6i&PpIn_K zfa`R-U2lCGZMf@h@~ni<O&KJ>|E1^Sp9J{0i@a-#Ym3Y1qR=T-_5a|G>H|RhP|dcm z7C?qWy4+I!FNN;u1};NnDBEi`N@1_fG$2L*#4Z{T{i?lQs4nHQ)uQgmSaHqLU&gl~ zc^w>1%Or^%VNH-8tT_NAKhvbHSLEtAyq$|DXocp*qt)9IaNh(<@K<2)F?l%Us9e2K zSl?1I2B+w>`4yX98ThiuP}xQ&vjK=dSSA4BTq84ZNN-lEB$&wh^_2YlKO?L&+i@+@ zk%1uqk4YLB8wU^>p~EKz=s;gT?LB&o#wN{al>DcjN~^#5t%}T^xQ(^{ejBtGrD=@W z*JQ)p_fz-#-oF?SU~zgDZwY@51cam2?xcPIgdKhA_JQ3T9D+C`T)jS0dsjviUhRwO zv{xMt2sR5#Tj{Jh6tA5~?*rn7DMP8&4E36?UTD@kR@ZA>`TC87i?2#cSkKxqbZNdO ziXN9kxvQ2&C+W@A^t?1Wr&W*1-lZgUoRvL&A6AytO!I8Y!%>N>B!p)z$t;zHdCq&m zA#E!qMayGOxU8tFd5vFZo^0&%6O$xs9Fg77)|RaS;lW1X3&N489LnXuy7eU8@VA-c z{99pG!;v{jx52&^tG<3~Mc_ki(!Y5#^?d5n>Tc0*--eTE{hhxc|Hh3)>snC(=2!Px z(QaKN4%g%;Rypvg6N#|l_s(XoMAT_0PPdKB0akJIkFy<pI?Caz%>amA1LE`8$G1u^ zG&i~i<kYh}>8XP;T0owMLUeF;10BL=T@V6fi6jNEC}V>3U`@|U6HT;KmBg~wl56rW zi*sg^C^ivd0hZr!9H}a3p;<6%R>LRZH~+}u7>aGN<NCy*bO=|Nag`jUkLLwYFb7j5 zPH?FBz7*@U%yN$4#g5?$L8HO_8PM#oS|?T57A{l!p>2B_CTr}|lgH;oTh&h6vdcJv zSSfRmF29n*2Ou{eK19)&IrFMQ?Jxb?50HP;rb1OpD(iatJ80}DKceWKJw?y+tXx5C zepN@h$BMq|q5-jDB2JqQ2dv-#Ysy-OL?n%}j}V^&K-~6uo#};Uy-?j~_ndEO51qTl z9H@A7@G>Q|YpHXPO$t$*2SiJHb2e?6vkgOo?u+qU>x7rKrHia#;hBYKE6EG3i%Ec) zH4M%XF$Mu`>L>sc00nDDfGrG(&9`ER>^0qoTo}7yqSTKCB+RR1@M73{{)2P8n2NBm zOn#WRVS!UmNkqPij%RCPg5fH>uQ<6e^M?7<BqSq&qU_V$pXJUA%l64PT%(*Ftu+4{ z+KM3YUsVZhF1h}1Of?z(>t9XPv!;?T2y3xFTG4i0G$2+?#A(yvfE64Nuo5RDOKi&! zYkO>BdZq^;UK<0DM%RGw{eSxQUfMe_OvBN_!ohevLc7LZL9bZyL`jd0tU5;*Ig2x) zYe^60v}MjVIN%CUa><3{9)4tz9V{fXsSrnQVj-4==%6_hxWz_F+zPHpIhb?!-%^MH zM-0L{f0$T<!zN@9;FJ%SCwYZC4>GY5fH3d0xlRJm))>&-DEO=)Z_r0#*Gd((Xi9XO zBK#QgBs5fN%9wW?eBn&vZ5vQGK!S|S>cLge`asK-MaYsqy$}AU&~~}ol@dV+lg#0g z+i-1|qC+rGN2c=Nn$*630xR#!NbcN%vSF0XqO3PnHs6oF!1eYsi3U+Nb^-vb3*Zm; zRwAw4L3Bu(#s2IZ_=JT6PGX3BGnB;GMv(!19>91C?T@Y|`Q`9e68w}n`9|EE<@=JQ z2ABFTZ+{ihw{NXj0}?v#Jo2na^GMr4h1(rhUPb-?^l60CEo=sv7#*c}e}CD|>lF_3 zUWDJx^KezxvI9UW<DYYOlXE~QF)5Ml*};MdJ2={alDa4`b~3?|IFql28eIdzLlr|^ z^qPw&>5hlvl#p?s6rd+Z&!f}YpQo_LoUp0nq2UnLCP=RVaR5khrX^NhlMO)l6xM>M zp<{Ukoi!9znwv6U=zw6$*|SwJQGDpRHCX89k6AL%NSHmfXuY}N#j&sk&z-wi#gf|$ z1avKhLkSY6!G~hRnyV!n^Vj0c>)cKkK>6IhR6SO#%pbx>cN)JhM_x>s!)eXL;&?_U zVXDS7-H-P_z(N9AF_pMIHsNEZcaECYl?GGnf5LtHp2fZ7!Z(EIF`U;8K6ul7VBp2i zbpk+X%_dxXjF+i>Eda<KfEUb9NKnlGMv}H(n<}g3e}t)(PuWDVV<vcApx^MtmlP;1 zzlf6-22MYN=AL=FXe0A4JXaS0f_PE)79Cuf75LV2$l-v)fqWdW+Cej4zjZopr-X!( zE)j@blMwQy)(h2jy8K1E?O4B#E;$1xE=4E~fU;xw)kSJ87RyM8XO{F}%>f|AIVXj; zVA1)k?20TbtKnkV1Z#F6yWEmJfROgpi~U;Chpthp00^6dsoD=-@NYBLAkB|WAVAjQ zq7FNsw}tow6C|;Q1Aw4ON#Q~Oz-9Sz>t?vFmx2Lkc`6{!N$_E^h|KzxB!-488bSVM zq4}8g`RKR#YgAU^0FcV~=bYX29Dv%ol|YD<E1*@rSM(-Wew#R<8wt#Mv!W`qs;bSM zeEz9)-ln5P+uSp~nf6U%)zbvY9GS@^>CM%Q_-V4KmZ}zC{jS{khG34P=IP*&yw!+2 zmD`K%NE(FXF;~)6u54E^)3V6WWBLTNAFSU1`C&4YM}s%jm)0(%YEpZ|B=|_zSIpzo z?I!(}mstlu<VY7Si*(ZF-Zivx=@JUTgcE{wNu2bqv<`r{yP=6RLCM*)_Q*utDpvV5 zconT;ds$6603?eY&W^(Y0}iwh0Fm)xmemlQ2CQhLLkXP*#4bJ{dV$%8B_>g{*Q@xf zOE=Ql>#_waqkH4mgRiHVL~(E-8{|zeYLAcUH6V@?dOl8NAD22`M#=E0*$3G_LhJj% zCOE24on5o~dklUe>*1pUO^j7!mH!8uA93b(CSJ4&8Vk_#a6FICE!sl4xoUff&-w!T zN?0ZXrsh!R|NOgS8Kr+G{D>m8EE1uUSD!?uZ&*jYD|?VMy|5{%Iwn~K>FR-Me(TI} z^EE9aX=1Gjm*pNS`z-NEuyQC)+YSdBlmjj22V&8RVU;g$vzTD%1?noZ$+G7D$LrV9 z%Qt1)w(C3N4CxOJT!q6$1Po6=e`?-+M+1_VHAM^JtFO<k6Ts@d3If=yh_h_hir4|V z>*O>DDsqpMGbSql2!{yf2wN(J06q)^RMw459na=vxU)==Yo-?Faq5I>|1FLbevec% zqM0;M)HK2iKAoWu^(=wklL<-;K-*;zC8nlKwY<<*nIDU(PBfLE`G_Nzqh<ad+*O%R zgJzV@ro(|I=RixDVBuQY#Sg@;CRq9vd%Zwyr^o4>OIOn+=Z*tjq)kx_27hFBtr8W> zpeS>wlzUDO*z_8uOry?54hN*JS5gw6Y=Yp=c{)^LzlKVj#MO38f1`i)pXlC96k3tj zl*x2++KN^H5Dt5*Mhax#C0|6_V`h5}c|R6r7R_e7UV%a#Wr|#^ZJDdR4?t!zR&NYt z1GQ>)CmcSQw>j2|n4x(8aWmD*suSC@+thgz_uhkuS)ZYqC;mWk93a?Yf+Cd7uMqp% zIx6#Nux>z3%MJ$`jRUP03CkWD5PcLZuN#qAY(R0{kB45Mw!sv#hPe6M6?EmR=g2E^ zC{$zm{m4ahVqPQU=2SDk`mG0S4gkqvX2Ams>pz^E7lcjVv}Qyz5V$z6MlFd=KUo_S zfkq^<Ar+O2H8bXKjqK5KdtS_|n#7L!@w7=5gPZUw!`3B)Rh>Axs3qYkM3z=4Zp{LK zsAgY=z1sNT(|@*R@Cyqh?BAESZLi1NZ{76of0wt7N($s?jOJf>-dqzv1Y`gCV><lq zchE~OJClz6{r{%q1kCd+A}EROw5aJ!vlTuOnRc8FhXYN_0d_=3l$aKwQdXnu$r8N= z#OJZc>r5{&>jmn1^0eH(S8rTG*Igr0hi}@jiKHyj-Tl|nR9v5%AXQyREZWH%HlR7E z2W)zNYN|S$1{{$6D=5C|ow+3(me-T^*Wvdauk@D^?l!l@@{b^&1%+0^JnIyMp*=0- zE5iGnXM)Gb<u0sVbA&7Y=7?V2AV3SW5$s#ixgHvoQ&aWJA4T?=Q3!i;&<?584)BpD zXvf5{0g{k>P$kZK?)8_8q|{dMJ&9|=p>Ugtpga|y`V;OqbEZh8OkH^L3FI%y??2;4 zj9Jly|H;f_J18-p4nJJV0E*O|dgM1G&6|Jssm)372bsM8KAL>+e)UjQzoGev!Tjo; zsy1rSm_2^64F>KUayU>^4z!XV2-`E;AR~IS_<#h15S*HTI+um$jn@Pz;x}HEn@t4i z=Y4ObnB>oyt-!*aNpi!Jmp>}$P1@As&UbzeBv!wjq=3QdeFwBus@W2QLIs56H?Paz zLlsN-eZ;;ZoTnrV5Yo9er6K9s2!OH)AACmuR65oHd|><>U8{tk2Bt>JLjO$PVpTR7 z^B6z3v6(rz(JTTLQ52|i3A8cQAO?g}hapsJ@^n<A-8!|Qq;wg98Cx<*B82g3lX&;f zi#8c-%Y7f|PvS&@lP22IpI2;u<Okm`R;6IMtE01Ec?E?J1B94=DVN#N;#<7T2AtCz z4zvOXS}h|wYe5=a>yx7av55(me#PGJ$7*hqP%zR)S6vmOJAW}xQ8=LMX>?%;-F57( z^ybw+g~`@pZUj*YX^K^MlM&b$v`tQ?ejoMx)c5J^8*o7I(88wAAcx$Ckj+}7-4!4b zaI(OO!_a=<xGH#uR~t0aB8T)qjB}@GBZq0&zzIgj4jj=O0bW`S;M5){p7lKIEU-TS zFr@|u(3L5>(mWoQ5XsBW1L#^__}Sqs=5zh2y52}igQROy7<-1~@X4t%KeS;Q62_h- zIkX<*VIa6Pl~LV`c`e6%5rE%L6j{;ZW2yEzi#f8X3p4P&;H>Ms;4q&+ru1Qw7tl9I zSQsX0w2P8YA!*@}6p#~t{2wG=xSeE^VQDqPGu#w@_ZSJgUotGy9&E?EWO^20y9f>5 zdJDC`?((W-M4x={0h<5IGevqWT<qTn@NEG>DRid=&E_=e0FWG(I0qaK7;vD4Lh<tK zWtcVy4W1EQ?*n2N6D<9TeY0pl)Tbu2G(s0|J3tTZf&gVeC!#CqE@(ooTXjbUgm9n- zaqKWjOJMtNg4k@$0U#zARiKI#+D!3vA0Y3+Z)ewH^CnIfI0ZqPL-MkAinS1|U1&Ap z@VAr92WTFc*y%iv-v<|uAMDFYIv9r2dC(31&EiukF#y#1<>=4njPhKkqZjQLF<;_L z_w2vI8Oo!1>P{ZO+#ravncEENYiVjf#sV<GJ~~`Bc?e(x;v)l7?wg}@E#RBO+Ro#f z!xZ!QSd-4@EQ=uX`4JK#&|Wc34U*XHA$hexQW}5|Op(O@a|<QE_?1+h3jPa7+UNTA zt)^NCBOAFHl8}d^|I?qOwYS}Frh36T?9|@>*?%lpr&yr?Ui;Po6jqs_`G{H_{Yo7& z-YO0`9B?>Li~}v_h;9QDtY*=GG=mR_{`pyd?gm=ZF{nSB+3$x#`W?sKL32q6bZK&* zFuxk8^#gG<ASwq-|4yvDjuKtx=bp%dc1UBQH9roalLcM?l2##3$~b{le0A}OLq)ek zJCw&~U}k6(E+UJjQC=TRuevJ`u~>+2ViGVnu{-;I5}w*cu5Wz<JWm6@a^?@ssOt-# zA(}S!N7FaSk+2|i>kk|5Pe1+`9skEqR5ZTC*ccso-whQzkoz`l00;!Yt3Evlewbrk zg?(4>bX3^0bGpL;TX6s{O7(}xR^u*Xur?7g^gbYV&4g~JEa;`&O&4C;4hPTtg2Z5S zIsNC+cheZmvY1>$e<icNAX?C-k1AHm7@fLUxZe0Fl6+X*){`HmM%wVaoDh%w540v* z`g59PpI7rIc&_RDm$RI=WA)x3-v>{5VJ?d5$1}5MzmM*o3dO7BLQ0d6X|)MA4CH6w z;Ri|l@)y9=c*zqMkgvn_@sAK4I%xXb0RRHIgx|dJJMW+)A9_E@u|*S7vz;v*J4%P% z{uYYv-fi}jy!HGW;s0a}-C<3$xvf_Cgyc5p9C0|%tQ-)V5rE*u%jPcvLQ4v<X>!et zlWTUA#QDZwebZ||ye@OASoOX?^(*#mZN|>5^&V%X{de?4=-iD%1)G^z=%61Tc@G_! z*{Jv;cn<F<SYIvFYCuX69@O)lZw&|BA&PDJD#`A!=3si$AAyMv%xu&Z;sw?U9D<EM zgyUu<&V$0v_iAn%b~fjzT6rQ~B_Jq|>^=yrk^(EtDh3}ecNN}89C|lfNzklZ&zhIG z^AF^D-}NN0W4cLjAUyOda((E%BtXbFLkoyfcHsC48qjF1%$&z{rhoHm+I!h0H1W${ zm>%EQ9Y3Z0SG<m52Vl--k_2dHy59OW3cva?YTNcQ@~&M=A`&R_r{j&+nrx+%YQj_? zU~Te2?Pwey5G^;HZHEJeIAHfsybLyY^^)WJ19XTS*-M#|J%8bHd_cVRc%9_}5OX%j zg>KD8E!%cUFC9D4PyJI}d55?ldFk%s*V4AgF1k?OLBi_uNDgK@!Rk-egErQJc-a2j zp|Ut2x2**TxtV;=--HDT-O8>PnvixpTS`PMyg%44V*CKKBP)@F4`t0l{BE4;!^*qH z`os$-X$D|<l96z}kKx@1fX7mRj3LL8WiIjsssKKiubi2l0Z|(wd$kxTejMNUD$&pH zAnDbwCF$&!5$Sv$1o%=2Pd`QCukI&03}9@M#-^sJZwQX?6_|RlA4lKEZlaOzeup}* zeKUp5zmWW!H)oAt;qYOa`TgTG_Fq4t=$<`UWoCAkzW;-??8d)0Q?*)k?t=MMagY2e z^=6M>>?%#xX~y9|MI5MgVy;RC+hkzFh!uJbh|et=Rcp9odM9+PIZ?w6w!#7NYVc)O zcR_}jrI~nsO%FrhKaZbFhiBK*rQTiCz5VinJ?qWZ7}Plcq#(<R7E0kQ6x;j-^1bxw z)Ix>!49>C9xi009Enxve5!R2v!5HCbgDdiyCTW{!=<WC%zz62Tb5n_(r+IF9h=teT zJ%=a(^DC7&*<c`4CGep(%Fg2)R=C=sh1K>O{To8O_`QE4<?S90lN`Zf1{ThQQ8>ew zkwar*W`>Rq<o8c2l^xN&duiY+Usg_Yb#{=u8zGu8zWCrEvEzEV<Xg9n7=YB0kbLk* zHSMpKojH9u9I!J78rBg#pBWX*sl?a}iE;cuD&}MM)O6&#XS6;bF4$bV@v#H7k_LpY zN3h*buYavb9#JFQ<+)HCruWVlE}@6-9i_3c{P(LKwCQ=8r+Mda5e`UQucU=jJ_)NI z12E5i5S*pl2d#ig?3_NnFI#J%mf7K(ffQ$bSDek$_b2;#u+}5Qz{+?MvLw^!zQa(K zEDD$%U5I$qZooI=CQ3W1fFo$ETb<xoE*wQPtv+a~$^akCKE8HV5oackA@h3d;K7Q( zhZykFs$0HAY=TuQg688WqF+5$tNyqji=LKRPQmHR;Xti8fEQQ2&9GV&Y#3c(tp>zv z6YU4jLoY1rg=wR?+WpwlH4(bv;u&%Y5co{cfxaN!cVCQPS!GE2(8YS58lL1FG{u4B zvdd`Uv@en@8VEle9gibHDZ+VLI%mx=hpOeUy^@W5LpYh==w1l208uJ&2;we)EH*pK zLlTnavDsC09FFQB=0XI~uG%9Oic_qaVUWZoS<OU?_^oEM>TC~_cL}sxjY?1(um0x0 zP~h}4YC1xQ7C#0Eu@0RX%*Mhe#-KOnki!9+bAS^P){GpmQIOSI9}v5kU-5+W!m`$( zy5NLuUbu4e3VQu{BM{h9&bq~>!`2Ib{G;*nrcd6eH)|`_wo_i11IexnD7N+Ma7@o* zHpk{kog1<f+98MqjqQU*p+DtkR=A7$m2zH_aX11`W1Or3Q3x=CvztfOV_DV!c%^G4 z;kZ2X;(m%v!SoEo;A6>hkwVD#VL7qmKZ6i97i$8Nqi#w*uhFK}XioC`|Bjaa!^dmX zTj|}b`0VEtr}NU!tn3W8ySC<6JX*&Gq_S(~oZVs^U^6V)il>Ib2LBi~iGz48Zi_^6 zak0kd!OE;nNDn|-K?9;*nl)#xr7P($J-D-*5>oL8>G|iAv}u!@mM&#cZ;>1g$RaPS z+LGGOpy=uULjD&&ip<r}7U)Sa$pQ$8LW97G==lAG_ZZEiE`K<JRshb^ZlC_l+VAtY zViSZ!9l2TrFQR|3M#q&Oc<_J|(HITAbP!62lomqZP`j&w<O?F9UJYg>`+HK(=mOq# zoFTmn{N<k2Z0U1@sch%nXXlQ_gX<7lw+I*$vm}ha1b>Rta18z>Og-QO^FT;Fmt0QJ z2+84IWIP8@U|^Qc4<&fPN5M<7Z@F?R#gO?NJkIQs3t8IXqamCG^E|Z2iZRZjJ_qmz zu~QCgMfnkOU&SgA1dm7_AbB)M$sNl`9)qc$MI>K%5lL6Sks|QlVdwGKp+kl}wVl0< zylZ=jvz3<<Zr89;69+pkzk-&$`#mOVR<{Z?AG>@$VRv;~E#6x3Ux4CmI5iv&v<wG0 zWNpQy2OHcb)cVSgNyUw~P6HAP6=|}Q21LJFuNS7(IW_C|>N9)k4c>!v|KnX0OBNP3 z_!2+<c$}`iHd{E<gEl>=YgXRXdR}sHJ<Xr>J@W1O61gUyG47s$2K$A~!m*dZi27Xe zHh<W8+TV#f_~gl>Vcc*PjwYZjV2wu<;3K2$NVm&^dk8{31CSy#KQ>9D2al1AU<XWI zuK>-5i`*Xg*Dc^v15$o_d}%PJ{TWiX7XS*7yikZ0aIj9M%*Nn3pRPNM-yY2~$Uj3W zF<?s~Ji8ZWX-wKOjueJW=$XFas2hiWhT1nLQB0zbB@i(gw6NvCDE2*gphY;4htS?A z_~u930UR5Rk<f#BuQ^VMp90icL~=KP7!$m$mwn`;bnM0trS|b(0B<|5x|+J)c|8TT zZdHyi`JXq?(GR?jIO0_;$h{o*V}0mH_2{++dc%55%jwhMfHfSDO9dbdHgHZR&j##` zl{_0-2XPLETaz_3Yj{$xR<%ALc1c3WywVHHdUK+B@w1lwE<bfWUGthLYWG5rDMEvT z67Aowm42`g+?HOoO`oghX^|$K;!-%^1yDTw7K*QV58hJQny5@Oyec1BiB<WHDXoXr zy_8Lc*xYFpE9xA|mqCeDc|<sI-~!B}7!0vF*f>m<=CKm51s_UA^VJyZDMi5t=a?Bs zg29o!hce(J6o&A+3g9Cf6K7aq*+v4<Q4>w(1EOP=kj6T?!&TcAAA1z{Ug}-K+J??I zT|@p&n`rq*KS7&*_avQk^Vc%qgF6ju*+QrM>fw}DLd}I!wbA4@G<E;Infola)S4sE ze#DXc<EPfNUbt0915%hjPN~CzEDo@mnE{1tl$46gv}RO}W+RJt)!xwp5U(2okWEN0 zEVr5lM7>Orb!+G?Zyu*d?>|ZhC)cQl4ZnBpOwhV@E^?(Wv!ekqWRryl1qFa4+s`E5 z-Y*%OWO45z%xPGwz<FF)@Mi~a)o%{(M>tQUK3vy@x=4aZ9<1uSunNx-eF6UqJ|&Bk zcP(st*fE<0a|SxRvd{e(kr#cjNf>{dpae{<M)w^Bo=JI1W6_*A%x+t#j_bDbL&(1Z z@YJm2vY5AH<0Sm<F_JI7MB(77uia9VlbrN##Xo(XBG+6?eV_PfS@}GQc9O0TA0DR3 zUp+vbSHHpRDb=WVA3z9HVYMd*fYhGnPLFoy0I#56#ZLtriiX1h8#TYG+GvV?kHtS+ zRh*p`fN<^#mnv0Pu@Ad6<?V>j>)sZk7kBKXC-$wTIFb??(e$)TFTIqcty@zFM6UtS zo3$CW;T)@+15)SNG{5bK<T?EB<Q}*W`j9-rK5InSshdA>2=Hvq!=_NIp-_qa-I4$( z+C_a{Wo8h==d-yMOZ}=gMM)Y5Z83i{_~3VLF=F#CPL}A$C+*u%>=f2|Ob-krEyzG7 zyLlUOdvq=V0Is*h#5@dur;XrOQ;d=2l<PE`<up<39P!?}DVYW!#p-Iy{aa6`-XHvk zcAxih?cu6#kNxQOngS3!V&AL!Buq9~@5jmHFr8`+2O5V1BCGgg{KK2)$G35Y+SDV_ z`heIq46k00Zt4q`Z*o`(-*V{|TDRo{?R<QY_KvJJe6n7AF_8fvjs_$jGiz`hT8sF} zH&L?Z3i9l~g<Lax3U}&3BrW~~VWP!gB=dnuk^V}v_M#H=ECw%LfDYEIaPq_=j^+A+ zM-km>{7@ey=2Lzh9uI&=w?O{323p|Yyd|CM^+Rux{X8aqMze#qMD9dV!liIr-?)+F z4X2O*4TAWS+i_dv|7{j_|CQ*#exeO08?{>Sm>9%8WtOn8<c80QcjZY+NL~MytrS1e zPeb4OX2J2zJ@YipzxbkJ{$;kSa_d3!aSX0650`s>rRO;Sq*4w#=eHgQw5xcnH~WT} zvDO63XCo6V?P|SVkZu^})?pxRJuB$aw-B9ke2AWVdY1N|h|qju(Lr{6d{L;-12?T! zsSZPL$u6bPCW@W)9Yqu3KKerv*$KK3v37&apHvelevifhLL{g_*o2BrsTh2)W~0zM zR*_MmAz>{5n{2V^7CV%44v!K6hdNx2a5dRCh#*;G${`UEuG%{V3U>l{lpqtU?OFQG zre?Dz3TA1yw0|obFLd40{mLs>lJv!|kh}tbR_B+YydmahN&Lmn3XW1ZbP#~0bO4gz z?CV9yyLK)0e*bnv(OR1`K0Z}KGR2Yaf45*O(LK9s0ziP%egF`nQA-vaR@Dm~08(|9 zIt@A;a5zv44rBw623nykGI_1AtQVw<oZ1q_9VacL3r`}t5VIW~mT7WQQv61{k?c*S zwzlk*NG(s*I?i`3I3Q4>=W0qUznoly50R(uCnQE89Gf9Fv0^O+WnW9>R7H|hO|9^) zm}c?y$ES*swBpcoOabuDjRJU}Pqv0+&=EpL^D1^@h8v~TnS&tA;d9x%teL2Q=0csO ze8tryL#vRFq<8-f3A>&o;n*?5D)RP=N#6b{!?I!r6Qe~yHX7)9$Ggbe+ncwk<u`ql zH(S%*0007fNkl<ZrguC_u>%JT>vUXkC3WIsH8JqOV|AO={S<270U(9A<CHlZa5&Jg z90>c}G&+M-Q_%AE#g+1g9dLSJOO~WVL1KUH+I5XS+`5jB^M5Q|3aL;>9UUFi+1W`G z6YwnnX?d!v>g?8n1Msg%_FRb%fXL7z<mtbY#F>5N_pE3g(qMyeN0liyJx9|g29d4= zJ{2G~wF-3z6lCC`^j+l+t!Qov0(J=6B2Eg~BqYnl-{bN*MSL*49+Ljy6Xd$-MyMMW z7x!i4_K@E5zeql1W7*tFn$UoRFS@v3`yB3<o!<{!_ol4zh0Z^pmVfM%h{APx!SkzC z2(0(pwl?8Nwc0DE9pzEme%;q@mSyP^<uqW&b41u2`vQem4rWIW()8d<cATCwW`_d~ z2OJJK9B??`aKPb!!vTi_HRS+6MgPWL*Seqm>}Rm2EKC-xEfnt1E~N{z#Z_%~nTs<{ zhXW1=91b`fa5&&_z~O+y0fz%tasZZ$EBE$ETg>#~0FZoKvW*@#-vpfF4hI|#I2>>| z;Bdg<fWrZY0}cmj#{oBqunjCujt@v49{IyPl!zi}l0z8|_=GsQ>K8B}GmdgMT!?Jq zki!A1IUsm^Nkrp8q5_8;4&>(m-bRY6b^aRmEHU9nY%sB;Ajs?!i*J(gVcUJ|qcNC= zr(fI}cBz|OL*{HG0*)Xos~}?2g+nfUyVY6-{W*skmIJuvqLe^%16+S8DGJFXZU=ls zx10zD<GD^c<=t^*&mVV}oJ<Hp0%J-hM488lec9!fh4yxgFZEp4cvK}#XLfdOGNBE} rSkl==D$8<z*tZ4#3qLDTk?8*eM)Y_2w%Pfp00000NkvXXu0mjfu?F{l literal 0 HcmV?d00001 diff --git a/assets/images/BB5C2199-6CA3-4E0E-8899-A95FF625CE38.png b/assets/images/BB5C2199-6CA3-4E0E-8899-A95FF625CE38.png new file mode 100644 index 0000000000000000000000000000000000000000..79b5c5438b6f087219c485561fc8c8019d810fdf GIT binary patch literal 8910 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 literal 0 HcmV?d00001 diff --git a/assets/images/DBBC8E31-4CED-497B-A53F-43D78BCB1523.png b/assets/images/DBBC8E31-4CED-497B-A53F-43D78BCB1523.png new file mode 100644 index 0000000000000000000000000000000000000000..38e6919ff3d2e0e705c049f1ada86616f2b9cf2e GIT binary patch literal 6897 zcmb_hWkVDUv!x`K5~Nc?K$@jt0qO2;mhSGBZUuy0x<R@@a_L;@7U@or?(6gA{RQ{K znKNhN2h5pRH5FNG02u%Q0RdZHPD<k+`~HJJI_kgZSp`}CM~EI8vJwb26BLL4gp`$@ zytT420`tEd9RV@g4guxAk$)ij2LuG<d_)A~e~kEFEg$KBUEh4<|K<Mw4Z72!CjDD7 zl$R3M@<lw&!N|^*PU|tf#*!ZqXMZclKx32}df*-#hvFuPLP&^$m?kM9XWh3M7uJA8 z+d<o*qKbnbgQ_)E6&BBKtB0c=%pMzx6gHNoq=Mt|^;1&Dq08&}@qPPoWp@Wmv^0kw zmRW0(Q+pJ2eKcSBP?5+(=*yK{(=XK-Zo43yI-!I@9Sd8}sNP_a!cuXq50JKg0)P<L zJk2tTq>W|c=|v?CReDcNWU|szZ4i~ZrHi-cfq(fO(-9{&?m9>l`DZ$)lS-qp;k;$> zO=54A{jNFQB=e~i9GimAw$-TdJesLHf4eG5Z9#D7OxzBsLg|*m_m*>FcD!4;<X0Ux zMzfi}aFOMOgD2y$=Ii)Vok!4<`EO{%CMX6kjruUgL)&N@tt}&P>Ov7mHYQjOw_U7n z8t<_Iv(2<FesVia-S9gjn%0EyTkhCrW{&<kSLRE<y`5iuU}Q{kGpwX@#aq!+&Mj=2 zt$nkBoHVbXC&?XHR)dc`#De5{4}EW^YzCZoYS>bBi!!Mhi8q#L#5qM$g(a|jwDps= z8s5vDt`8>k-9D$etSt|36L=HjCy2~XfYFP7>s@;6J7iLN28&S&U`uO21SvvpY9fYO z=s#M|Bbfg=xqw4FD;87EWcxv@T*8U6o5v3**Q?qR@|;Vc_>M0iOwv;ag%NT;4M=bO zh{Jc2&y0~7EFmS_>Jf!eH+lV48UxQYQ`h1qaSpC5QbMD`k96vq+KztA=>;Ka?T8G8 z+hV13^U!JB`eX1PLPVldkC$B9U%3TvF2Zk*k_k4%vDw2s8e$}JbiVFCvJ%)MD^O+^ zfH2@9`JSFSs@JyX^57F|>-Jgit4;1b)Q4!iewZtVN}|2J7a=oT*fbw<BY&%f74oQP z*J;w7#g*tROnK-|b=ITAtb_|5(5=|DNjFj&`DFfwh|H3dekh6Ho<gx$ChQKZ=J75; z$7vPyfWkdu<f`nn$KcMQ-C5(OV$Oqux}BdFbiuV79Lu?et~adl-Ip4NAv%=U18r>a zX}VyF?0x-Cq28jhiDBKR8u6}Ot@@AIhuTexG7(9qP=PYZWuEBvXX%#pqX=zEFB$3i zW0r=nd*NwP*_69HzYCnLHV)P3g06GgZ0Vq)KGrJv9OeoRCC!7N><<KVfqUn(;Q8z3 zBZJXPrLiXM-$#J7njtI~26#^IYzu?csI6!9eI+X%Hi0F36tv$io|lo4{lm$e0p9W_ z1)asRgBZU^(yXJ*>niTDxqzIELWVL~LXPuk&n7UPW!baZkMy$sihYN#UapIa^+?Z6 zD0}u_>M)yjkB+>QwLVmCS>-A~N=p|g_e(F19>_v_FUC`>MOzE3$!RdTj`BXvxFaM9 zE*Tvbw@3WE&-6;y^uhREK*>j9tTK;s#9Fvd!8~(|PDfnA{KgP`M6sH9f?ur|`H+jI z4#<;O^*hIo9&eS73c?=LP6S}1+hh67+Q<97=|=<4M+<Qa)WEf!+NkG_wu2I^WT<xK zC&8n@{gU@1X7h104YE&CI~0`9*r++Rl2hM#)azrLm;5S<V$QP3tZUXiRn&GYYDy39 z-rXmK7m&~7l!&^x6!+35FOpJT{Bos0f<E%~9Y5itX=R@EKmz^Gv9{-Xk4DS{XYFSC zInQwzS<!@!#bu81q(mVf5Q_$c$$cv2CQZ$F9Cq(y8;dL5zUB_L^e)Y&P~w%tRIqMj z3FCvw4fJT!khotN8ZgJ=?irjlbF*V4KGvUrO46vCr$-ImX*%SPfN<TITdeH4qj%3d zh4Rs_D1@rRKcpY%Y!Py<XjsOLn9R~!2mWYZ36xi7)U;av=4!F~Mn&SxwNK%bu-4}` zs`@9FrF$;$4F$F+;jMT!vynY{4fl^yo$^uins?zMlx}=dO|=dnKv4Vo&lVP21ek@t za&`~=a}sBe7q4AqYRrvH1i96bzZ#p8;ykCUQ~-&uw`U_weS{|UuvX=Q5SN~RHmV?& zk9ign@6rVKr^i=1y}xI&v#>8YrCmS`EfO_G)Ydv1vaouZDnY!f(7=>2VIrBvp>qjO zSu}?BLie;{#X^Bn=_2!k(;v_3QER=kGlf6tZ2PH4v?VSh98BTDYQ>Q5KPqL$(KdGE zW2N`>&lh6)TM9T8wBh?F(N>2VOhS=EC&_sYE6diGv+Bo7f@rg{wlDS>n26&rFx0Zl zD_UawQ^otjeX!oa0PTEq)B#O8fNQbL-+{D=Up$>O#Il%5RZ__Hroj#?no6k1)@%T4 zVN%o|DtiU}{v0cBqT#35g?U+O`!J$!3^jr6@Vrx8r^p#&4nvpr%x;|;zgV~@M}}EC z`y~)x%ab)`TNGzW4|SVld@ky232?VpY+VgWNFGpU&Bo>1>Pk!*+E^cq@gwx<rC)Y+ z@hgF3Demvrh)qSGD?{FP#8H+x%=#X7$B?Xg;Y6?v2<~PunjP^PWx1(}kQ{msqmpu4 z$NNI)Feg8pu{j5Z_HSsXT5T62)c$Tf2BG$_9X`M%<<86PIz3HM(yNxCsP8T*_X;tF z*f*~>lsT7j4He2b3n^HvPHgxUX(yPxuyF9S-^z2<uSb-bizpkAY6+knH%rH6fzMQk zNL^ez_vod@S7pG?tqI-iG_ll#{YI2s%!SoAb5p;L46XoGqJUJLP4rw3GWc$GEjHHx zT&W6}i1-`0hp*bn(x5imALw4j$mRn?6SJ@!$mApK8XU<>7TpacW9ob}n*9*g1mTLt zB~4i+m!DsU{7pM7|JoZH-*-)xE_*755j~G<_+dLqPHr6==MCqAk+4=2>yojFpCDx` z;&}y+L4#@rd7%EeX@S}h#mgQ+*^1+yGNXKO6A~Wf{zz=_9YyHbPLm0xS>hil)^!AR zHX=3=F0MTB&+UA0BNpjplgZ3qY18XAY=+GAr2>K`UVl=i0VF3m2Xi+L$?-2o5%bu_ zA1q2~GbQ&_$pVJxmwk29V%%j6hiq93S5N>B=z2OjsDzwLdPTaioUg{DAE)=u6j%H; zBZ2FpLh^jitb}jdhDEd>J$ojA?|t^&8#_HRrleXuW_}?*?9VqjO<dsFj+$olQO(~g zHk!oTNHs~$wd<!MX%9{zD%H9IgUnPsRzP2_m0Xei16}mf$<o+L*q7X99<|06WrinR zukycV-eY1E;{<!d5=tfsN%?taBfOhWYfe=%FH|@zlq{RrjhK1SgtpNopU3PcxQq07 zaLkqjk_COK2nMGz%fSAfWe;<MA!w46qYPgp`VxQaOGR-+qoVd_F_{H1_jzRs=Yaw) zn?B*<o(0y85TA;+Y{lQU5arFOEbrUK=&Q0Uk=mb~+Z>oQS1&1*b-#v#`>Xv)M`H)& zF$qg=`+9^x-Z~5(Hbrw4gKByt154Q%`g53ur7?U(ltTPMcVbKxx>T9muJAmoUWXkK zqP^dF8N#2J7h$nr{Dc>9{dgUaH2V{L>8-!TX-vqc5Mht{HJ9=-t++SyV7MdSJ+8UO zW~-pP{%kpaI(c??3Y{_2(ij1NBh)gG?G_Yqm<>cPpgI8I4+SOU7m);|`XwpKkbh%T zl|82R6YW{}$QN-~8_{6>-q_T7=OsW$uSAN)e^S~$Y#hT1sK*Oyh7-<kAY!E|3STya zunXE)rAecBd>ua1!t^k9^H8($;?p=g$F-O%{9XbtcuU*?4L*m<T4m6#z`po7wVQQ? zwPIdquuUoIKR6${P|)@2@>OW-ho>Kh;K!4+_6iRVHQbL|))85~Wut+T_0dYV6o`-I z>Le3RIi&$NIwgrE++8vq&wpc8Vc3CYb;V@|jd+6?h=aIh3Q=Kjmk32(G0g2(Cg_^7 zQ}B7yjV&5(0U@XF628KW-w*1}pwva`H@*a_&6CcW^`5Rjng*)DAb%{$QH+xhwOVB_ z2M$afQnKSWlhA$EDkRiASZ?cR#)im^Nr%c$eM5OXkDHC2C)LruJwJ3k<OzRP0*yB) zEK5j6hA>05Nais%eCcsim$<~X7XV3>G<Fvb!>@pJy&?S8eki&0)iC=QVwZQprGIHA zEcub3427Tr3=se)7gbtr`C3(JyJk4mE5yLZ=e=k+B_+Ry_k2|_TVar4rGslq2CEUN zo@<;`@An><;wJ$Y9?(os7J|1)uRE?|I)8A_P(sX7L7#P411>=k`^njr!Dy3?2p$&0 zb9?;wE#31pp>4I`&|O<}))E$iGde^~phX<cG|QiD=zgd0h<Vz6x!@(dIJ``>hfZCW zndGE{&Bmj8=CWjum9dL42u<ML#Ql$d;e^l7+F0O#7E@t(p)i!if{{zX6kUSWzk3x1 z4GF1+c4(}4tHj(_D2o9v#K~)G#V%r9pX-PFeM#bDdJSxa_gX~OFj8R=hQu|c^o}g$ z=i&7UFCuvB_T}Y2m+haodQ*R{i<5C#(If7;kGmJKjtjkFRGfTF=#JZc2H-tZBEHy= zusU^ae|$#%@f$o-1Jdq1Fd!AKh)!ku;Y>j%R$3JDaT7LN(?uDgTwpjv9cRuD=@OWK zqtRpX*O+4F(ULj8CRYGZHS9?e`XZz>T!zH@Yc`X+Q5r(N_p3PXeQSGrj{>O=sEpaA zP*QF03aEBT`_jzmNV!gb{XR-|gZfY&$>HmtL=6rOFlwiHaft{WPgv*+GR72RRBA49 z%V<<59iB)u1a4iU8gV4zSD=73VQ8aF3tkNrlH&F3d32q*Sbx3j*ANwpy~LfqxrH_b zb}GMgulzgrB9rcWHL6s1l6ZkPp%@L~CHgpO<l&eP+l%)!eS!iZ(#;7$(%u%9sBpF} zYndAty!q(nQU9jf3Pf#`jl`qmfj)%s7tcjlCiBJb;-54DIM_}KsJC*HFKt=b+}3Cb zT^-WqjNJQF%@9Y4VqM%jqdohwD+}GR1JI?NBiT$~S}K~B6V63O#^&f(z&F-cLyCcX zHJP^ctsUCR2G^5eeqflLFaCR1(#$`npy64X?R*~rE+*eK>zve!S378vXm?g;OPiRY zG8Ky@oFo79t`&a%uKYyGMVEU`kk_lwZlm1Kk?T_|ah(I_L4j=(*a<hIoaUGZSKPP| z(NgcUemJZsq?@HA`(U<jwJ85Mc*j%$_DLycvfvhpmebz=koH4D=3Fw<#!WJ;iH&x+ zWwi7W8T9C}hN$-xOl;ktkAgelHVgS!xR{$`T|-pfA$lFj3j_<myAqN+2CUh&x`0Lo zbKh26zh<MFx2{B7$KL-)w7Gp0tMlr9ck5T9DmsFp2-|g*3SNbWmFuJ|(PEZYET$G2 zE{>=6>u;X-?SIsSj;{ee$li{TXNONb{SaoTR~#OHSgcggdK1a*^n}n3BOyQ(<^8iC zQgK~(37&ahPcU~g6~LU!7}WG?s*te!`TqF=SA&G8lAw+eWT^tEOp(2{B+bEW-;L$# zh}+=B%=M=5Sk{Kyi8b_B>~aJdXB9`Zy~bAGw6y!aiz`z{(<#jB4X$~2|LP?iB3L+& zM}vA%23ycKR@>W>fdACoM(vh<s0wDtWK=FzpDI0Gkyo&whP|I<>xg1CZ(}}A%ZZBI z{_FA@Ig2AMKbv<b-y0yH-Dm^o*Te}|ZNbbB8w7XeLe&-Lwb_8~ucHl{No1Pln0U~I z$IIXk%p9a$4cD_OVf&OI+zMIWPT3g@EhQIW)<g*0tc!KF@kRk#F)>;31gUa2+KqVQ zivRFkr&jIu`49~271vjhqijvm>`02T*{LD3Yu_OJzQLG|&X1Nyv`J@Zc{U?QYGLHy zHs$PBLC=orV08Fj^BWhSU0*tv_h0;dv-4AL>!&aCN5m>n#q*MGd6j1zz&IUp*Oo2^ zR6V8urCIFjmCftq__amTolkFF?VFE(RQn{Fzg?fDrCKd!C4gYY+n@}fV~Hbl`Ph`J zq~$NcRZ|?la3taGMfM>V*UE%WVT_7i(IUs*$Bw$a3!hVL^0PvbM|$HeM!onbB716% zP3?r0NN*u1D6FCT7h7!?R?+#eNSy{>cN}QnKlZIhJ;@+@?t60GrF+ar<084cj(U7q z*8~g-j0~hkp<O2d)uRRShZ!XclnN9kq;G8)Et{6aEEhF*V$sLc(>e*ek!?b_dFyNz z{B_G0A6NjjE`Acs2j~+hmXobtkvzC90Fk~$*#rgRhP2`DSYuHEzEE{dgIioFBDytI zi~Ezlr%k;`Z#sg*Z+9eq-Jv1HS%7Ro?~7Ze{sn_Z`dL%-<v$#imRChsk$#+-rb*cd z7K_|(;A(hz-0ExS{Y`c-poyG3rkk*fU~sV;iO~V0874%pi=f3zS-Ila6U88(&L~9F z?iYC=Q6oik!*?;KV=D{x`z!xQNHWSG;@(hR%UF!hdyyK3VIh9vq99&)SMWDz(6~)a z`q685gYgqLp9XVyUL&{W9SyiMxo*h`hxZf=@~qVsV2*5q4euChp7^vG44~*=u9!4j zklWt8GY9Tew`&d}hHXO~aOta@8XVirs~lIM6*ZQsi=Qo*hsTBxR<}8bO#TWj8F<_e zQjGTbHyE33--+Z*`p;hZo^Qp}b^zpLql2K22BM>F=iwxuT}q3z@EzHgeG5G<>HV$+ zRh*vO8>&qEVuFN>t);uU``Uii{<ix*fVbKmh5Cy&R50)n(x1iySf~dWoV$I$mm*ta zQ~7N=Pd)Gr_P3MV2l<jFy*(-X9z@9IZ@}-Cu<{nZAuY&Gpg}zF+I$tCT`_&r-@~KE zd~p8Hfa^v4zGo|H(3wuQgPGf?3VwOSvHBG6Y%l9KX%#5cG=Xiug)wW*1yn?QzH|Du zy(7>wb`8Aok?|?`(o#*49I&0ucP%tCctp%#aU-)VTP7zNOQ*+ub|DF^r!|hq9KM<g zSe!nrGY`6mpEe4SISOJz-olR;2usk40T@sLAeLqd<@qrADoAdGnGcW4O4XwLJn|Fu zFHcW^N6xrWNfj3AvFcO?_o&09OA&agc1po(py!W7ohyM|GSXT|)a<d!Sn4p*m~r6K zpXSY`Uc^XOTX5Cyd6AJV%5e8iCZ&+g6lT?e#isx)U`O6h#cZWqNc9Pjq-yXEyBlPb z&;LyUFZv?dl^QVo1{x>aMH!5#kl^F|_6Vfv^G9LIX%lX}&uQ|pbXc#Wy)VbuF5~{M zPrdzN5R|}TraX{xtFg^_j>EjEB!2A5>t~_c1QV79srF)sBFIG-tG?2p%Go-=1w+F! z<S2-`=%#O|iz^`TW5EjqAq0INs%@Pm+~q3BS87;Vq-Udh2^rp&Q+_q)FV4gEzbmA3 zj$V2J><d3v6>AP|+`7`8gwe=A=%2?^qy(4~L@<SLhC561Z-X2^+qSL9vOJXWukLIr zN?ZNP=+e{|RUUAC^RASm)q?r<*k`7z(Qdd5I1YSG{?eA&t%T>K%j>}9AR|-8&aHq* z$hK$r<M!+LGEgx;@aKB-w;AB<(657U<G_A8O!3r~-hPI<Z9AzJg=X{z?b`Uc^Qr*T z;&exN7wR67qiM$0m^1V=FNh$sj_+%7>TxF|M0U=JQ)j(fdvw)b!Go<3L^2qBATp3A z)eelxH&IIQ4b@mnXI+1xPK$BQkf59*5$Yz;8fX`~G0ya;bnr#Tw+yLBYIyeDni$Ga zTJCQswjd=a`tqx~#6Df?SsagB;JaM&4!;<J_j@VY;OAOoaOn$tB0=`?;=S9eCaz0> z@n+A`HP23*s~%0&C6C5bTY;fpm6AG_-gt&sChy-<g5W~!&%4(l97iE`92{A}TCJBt z$<wtT_*wT7BGl!|*FJVKozwk%ln-wtWUBoB&(m-asr;sDS2?hO)fb1Z)(msNNCfd^ zFFm2BZemp_Wc6+G?VZmDXTD_>W@f$=|AvKgbO@rR#+Uc4$Na27|K;#w5K+4t4ittI z9qV~3JqjsuO?oel%%vx<F@5xm*J}95?A3!CTZ(>=YA@EC%Axlm((*u3;tpmnlm@cM z0SQjobLv%Qa%1?{^j-SdiqFX)?zuGKN_*cLy0e7?=4Z^XbljgcA8)7SZNCuq>X@Jq zB%_$GMcH!+e3zW8E>B@nA}pc!jQ#f!vXcJ;;IA+|^S)`-1{tWA<v#mjTkJC}ZoB_M zGOYE57_JSWtv9tK-Dk3!w|4zht0L%;p&8&cCWxM?MV&G7Zg8|!;(E(Q<wob-D+R|k z-Nq8OHK>2KK#-~N%EsYP-#&R;f((?mJL^q}p*w@Y#2xu5Bz(zGO7?ez`He=w-29i@ zj4R<I_5IM5I#q4dOJI9w4Ji!|Q+RWv{SJg&4|52-;>LMS|EJ5%NuVdZKnUpyHFqgI zVL?p#evk3^`$9<b{vs+Fv{xMM?$&D+yObR_-xgOw26q4`fr^}2frd)Ds&xW)guf5x zek?AR_<xW}soZ*#N|E^~#uSV$n=F(Pyo(ri?x?q?yI04|XdH;V<NdLQaUfcP32ld} zQPNdzw$%a|++_$Rg+yIJvRJU{>~aK_PJI=`-Fu=SgSa%SLbRVNzu|Kc4<CK^1UTZQ zAI;m>;@-iRVf^)ZJxef_tDv_Xq23rCw0IFzkcE6o#VB-)@gjRn?8<t$#z|Oi7>TaZ zS{6n3!$o|XOSePLWo!CT2p~R;Sv)0q<`rdhh3b<Ov5iT!tpDE^Ay{DK9HF}xwlO7X zn8RPTbn}4L9k0Bae;DPi;qj}3Ch4P@*{5E-Y|ENgou)Sl?6r2iIV8KgH74whQIY{k z!1PMKt|t#jKi+3knm1@y*EJIclI;HBFW6GPDK%jpe`ZR$SW?ZExS`t7lnY}RFXALj sB5JNEm3lfu6^ajN3bUw(b5wUB!~!Ezt;ANN|Gk^!rB$SAB+SD859nfB!2kdN literal 0 HcmV?d00001 diff --git a/assets/images/E64BA1C4-E956-4B54-9CDF-BA4F4E321807.png b/assets/images/E64BA1C4-E956-4B54-9CDF-BA4F4E321807.png new file mode 100644 index 0000000000000000000000000000000000000000..0f5117dd29d6c7f2d4555c6dfe2bace6f8a7697a GIT binary patch literal 7159 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 literal 0 HcmV?d00001 diff --git a/assets/images/FF00E9AF-13DC-409C-8512-975183F81063.png b/assets/images/FF00E9AF-13DC-409C-8512-975183F81063.png new file mode 100644 index 0000000000000000000000000000000000000000..ae81b84e8de59d7267635a69feb00b4aad0c3679 GIT binary patch literal 128984 zcmeFZbzfCo_dQHXcXxMpch`|PbT`u7A;O_cx>FjY1Vli(TS5>8>68XV>bJSV>+=5I zui$y^Kll*m9QN9Ct~J+~V~o9`)j<kq$i&D{P*7+}in5weQ1B;EP%xQ@Fu-4$^gvm_ zH)sz{1!<_)lcamVFS6EpN;Yb0P|U#3h)~dx&!FHQ-vWFQ17A>3u!Yc2u)ufd$G;WA z{QWBYNg?dtKf`1`zHvTtp&ANG5=u!{3hWDgn1}EdJh1d!PO699yP=R;r`}RZ3clg{ z3STs_TCu#PMe-_PeDuts!(qb^u}E=610_DD++n9>-|Ocw4=5fy<Wooa0WF^ce2#;@ zueFV5@oeSzY>yuW?&P=#2R+<<pS^zh9FCR}8c9(S20Q7WAMHqCW(5)|l$rm}E01sD zOnZ(@HWd0_KgbEg>`7eHhpYSB|NHA-S4_L1{>PR6cqQ}+O`&vokFcs6-Txg+(&NSc z8QQ-``M-<(mb?Fj@joV)-Tx-Y|9>XPbX(76ZS&jX_cuqWOj?XyKljQ_U3=gij61fJ z9;=&V9=7X!-E4ve>p#^lG!Rbs4P>$pL!#q@GD|J@H;=0&+Z`Gm?s?mSpsR&9Q$J2O zROqYL2ZvwMNc{L3#*A#&Q^PCth76HSFJ4YX5~dVs?i^RcV4wSF&O4#@+pVcQ53};8 zBav2WW}D<+MT;y?vi!K~;k`J~$<f6|qQqp9({`3^e#rfGyVpVbOZuCI?Hrf;Ma#sO zWj@C%eUTUi&&skIXKh#S4on~Xfg4G&X&Ar{xR_CB!pZRRIoliwL%;~Wzusy0+>>St z_|~kezhp>tx?f#)KS<)DKGTChaT_UdTZCu-1{q0_61(Doi8{tWt#C;QKh0;(p9f=# zdyJK6e%||js-j}lknEL5^(!myUtyW%LYg~Huf};QdU`g-;nJeJogZ*SB@=O2--mv8 z^WBBtjk|};=QGwDp*#^E@q?PCs<~}imDheh(hZ%Za%37-1J5UjY;!dtk@K<zmSE^8 zLvP`7sbQ)&5=4$-SY9Kpaaj&pF?!}coF}qtDgV9mgc4)#&ONI_lidh@|8QUU4*PvD zp0E&C5OCMJYB!E8OEwI7o*#@nCej#ayp?V*FqkmJE~eiH-E5D{>pXksb4*^HA9S^3 zxd?=P8Vm8W&uUOX@J}YT0eZK1z6D0dMlD&hvtAUA@a_*P9G%nBD0nZBxh5Y<hY1US z$S`n;wEO(peIzH4c%Ms`Yn6kVCio4jwpN3_f5|Wn&Wh{!*0MYpPa@#BST8D^=kU61 zyKN_*M&#ZDU1&4OO2~`QB;@Y0b;%|t$K}0WC_KKaj24-9617sH91>~3VT0vRLTQXt z*b`&Ftu%<2kQ^L6N9TnmqGNr}6G*B>+<Azsc_Ayzgm|=dnvKPu`H)ngS9kGd_xo8a z?i5QbS;TYPaV~y2rv-`fv%VW;sVm$Te7HSdij{rw(KIBe>+Y)k>XQQs4uhqul4~y# z17;WjIltiqXCLZF`z6u+j>N;rQ>B$Y4`SUQ<q9ycDoC(hV}6|UVU#TM@fwx>?7i;7 zCEEBEi05#`36ijkp2}Q#mI*>S`BzIoBw9b&SARu0zrQ&iV)Xyo9a`#+HKoC%KoP_n zN-pNl+R%?p#B{K~AbLKorF+I02S=-}3n%;o7i+>}dakZ{!Jzcr;D5UcizoE~U53#a z#lw%Gm4IV13-->74~`J@G4e@)=q0p2YP)0!P87Q5f=R$PV1hM(J#rsZUUHfIgEb1* z5Z@p`(5D5KtZ*Ei7DMHHFLq~^zC0hfGj!=3NT`Jnm}K4=Ek&ao19R-gYA8=FVs@g} zsFt+Pa904Bw+D4yCZ02L06uzO-260~m&u(mt;}wanWpml0D<W%%&+H_YxIq$8$*^A z&2yS6Uf(`7D^sxy=SN<*t_I&+ExXWc=6Xy<hb>F*s4ra(9<nME1rt>-#JQhHz%z>D z_MUVrXv?qn8d}v5x~py4f_a>;m)m`g;}Y(H#Y<?DYl2YWZXDg}iO2RG5%)@WCNtYp zHVwA2!i=i1QHbAQ*Xppf_?CGmM7bMBf+lGv!zDCM=gqfTCatm#Anm4sr|?6qmJ<fA zIp;$}-dxDY##RwDZVkF(`|^3%G?MoH!gz&Ak|G+r)AiOF#{7()K(xhzOY!^%vjp9! z-*a+woJc1??WCr3zkpu@3QF8gp0=)Wa}tO<f?`PUbKCvTgAyJaoejdPhttFmBbzMi zWG1aOhJwH|28nB>sE(s|2l&8Hz%P9LfHG$icsgLzNPxO3$(7&z?%?$pOd6D}JFt$N zPw~4xmI!?~9iou%J^89pBELoz6=X{_0TTJ<`EyS~m?&X&$@T5m7btp1^5``I0-x+k z?!K>vG|%hALO$}l-h69a_1%b{5<N%#B8vd&Jw=x7l(<=%($O=Aj8>Hh?c{o(8;Euc zHln}6d`JFm1*m0RW9;oUtt<YO7S+cI$eBD>ghHN2vri=^q*-F-%AZnvHl7XqF*Mu@ zW&HF?g}se7i%bqx;rb9*<040IEj0$zi*kJlg|0VK_U@jFU(?Qgs5KP0Fzvd(tP}X+ zIos=RsoIugIfz-2b}KmY^Jev766;HkEq;|`xWsxCO>%FOk;KhmlTbS`#D;L?xq-(7 zpP%a!RKg_e8cdCeYQ+V;1)A>6z16OeVe%*ZYgjb$w6@IT6>|9cw6B=><Y?h5v=zXj zNy)vq5iI$ZG8!||<rNB>z-*{8ljWNKT7ow3RDspZ8gx-EaaU<OmEWK1v*NdtAHWWM zF-ICp{^6wcr%*X~2Or(6D&T7<!Vh*caoJTYD@oI?k0t?kz^9z9`ky1eCquTjn~99V zpO+oJwS4=Tfs3=weG(P&;~z-Rt5SH!=vf?(gklpsye?0*<Qx3us9}h_gHc5ud%Kl5 zmReloxPzxW-{;7PbQlN^M?yx7eU$p}PC-cUPM*&SP(*g65`_0Fz$2&J9hm4D=2XNk zr7)~bey1u{MTD7NaB|pb^d1dXX!=oB%@Lw@2<yi`tVS_yo<Spcnp7HENkRH;Bgwo4 zw^Fs<dF~}Et548%{RnxuSuIguWVPxIM<NxTB93kvM3=Zd>4jV3sv!k7c8WRD5i3ue zcg*6C*$PD<R~(}A(^)0M<5c*jDR`Ac<gyy*+<B>!%I5~fuq;hzjJJ%mku1I<hYGSU z@!CCBpK92QZIz~^W9WlL`<TgPnLMKBbz;|z$pPN=x5@Kb*!+U|8p^#f$zWt^HgrFR zFN8IhfozMl_xvQ^`_r?2k4duX1uJ6eHBR?U#koRB5Zk3zltFO}r4xLjq-M*a)pqrd z4xH#BwGM6jm?$Lsc`Q7PSBy$Cl6)SHj7bXlP*0d3`7>3y4@~%YYc3%-%e1c4M4Y*= z1p|Matln-5Adq=t#^g_nRQkRDBhmB-NO#hm2&Ar1#8XhT_tsZty4=o9p&gUL2mL?5 zg^ezesi<t<G2x#%P+RF+3NCdkvTi8Rs-=0@)C~N>F4%VIk~c!!d-J-cXtmZfO3FDN z$DQASFfF*r#ls_rsA;BiKiq%s(my&IYsVzQXYacCa$L-h&<?c{<FO({|DBeeJQgaL zg}}yfi_lXHfg;c~f*%2WF-+D8-5%Y9_51!j3lqaTy!0VCA=;EJj3!)j1X;}-*pP8s zJ<kC$8P*h#=xM(@(HXUWADTeiC(`k3PW4Y-AB`JEv#itglaeUb3AJY~D=)Q!*K*XB zSkKM-B8pg|EioLhyrH;!@r_)2-hSKzY77Op6{5pr@BZ}Vw6#V~-(pFYl`Wh+39pX2 zCy{?c?;%$Ny4)#%X?os7dbzkak&M=L-F<W63PJ@VwdtJAhGn+FtD)%_JGR7JV5UW8 zT=M5IKC@JmnFT<P5GtdE3=DK`*-$39aI31~5UVhBm_odv&d=t7K;ZJh3ZpDL<z3Am zt5@^v6?=7orG9Z=mIcapLv5i4E&2!{JzqH|bmPABb^wOL)3ItDnLhmd2-GXl`HF=u zSr_s*LTe%k(bSdnHjNhk9S5)5V~}-HTq4s0qF#*iVKsi{GCE4p+sf5{;}e(qD|K{8 z^}DrG7!rloT%(Z_f)mQaPo0t>7l&PnWwu=7XB?lU;lqSr%a<eI0MN`H^W^q8y#02| znVVycA>S890g0>Ur2=`hG;LXrLmz;u$Bctl&(9=i4TCP{En#31#edq3lY0ly9n<%P zT#uPbj|SiUq}xC$2}W+!GoW=j1qtH6UyotIF7wt=z>v7*93%*|uuLJf!$QWbv6R+$ zgD5s>ljDRRYhkI<m~PYOIEU<~zxWE<k_anyON?K_n~M+m*^kR9_E(D{Z+T-rlp!;9 zY$^x^Je3~t9eBSML6ZDz+D%=z=6f8^WRwq3&u_<9{dUN#Vl6md^@$f!qUH<U-|k4v z?k7Q^5d`6G(VhTXS$}Z(*y55@C>L~s<1YP$2$jf|Sv}4%w&LhWgluxd!a(73e;Muw zga|ebsKJw_De)ZEB8%aoB;wJj72Zt+({li%n`g>gSGc%_BzUv(Qgm+O8#+p5`aS1& z7Z^SPwiwG~{0}Dt-}D+om1Nn#@blUah=rA?YLa`osS-p9GIV@$No5ga8cUMOqxyue zgA+<s^U3IK#qHQqv}t!bk+(x)dLt4PXwa!zeiRACXmfT4-=7zFWb8!E;3adVTfo4~ zPYIO@3kW^$g9DKy3T+e8M>0a03a_a!4-Y;u_a+%=r%HMSK5CWGi~wdwQj?Cf4o^f* z5O#GgkJjCK>7S%cp)q!*voPzyQ-}Vt`SMMzTEhKvt#s)~uV=SC@fz}ta*k5WUM#d5 zclJhZrzNEAcUIo23>Mxxb0daaxkg;cZ)<Ud!SO<3gOnQeR+J+}t|JP{g8dPN^xaf) z+yUNiQ>7!*p1z8FS7k@FSl`dPD2%73MK}_$op}Jj9ClSnRA(A#Vo+x^QkAI@9RX)* zK~L_M4{oxUwMK*$)NFJGm(^2+GyLUMX>V*Gg%3(AG3MU4=CwG<Yw?VOIEo|D6j__x z9A*7=TDaJ3dT|WuT$}2OPFq7HxWwJBe8W$baUcxcnzib-quqnbEVP#9q!&^}xwysY z6~*+4l1*cZ*;@7!Q?;MGW#M6&rr9rnjj;th($BEdIj1W#oF?{=@`S;`EzTT9>kT_N zo(nCMUt<>z`XpZqEDipq(K}}e`bk^r-bH4xMup5)Rg{F6xMow(&%85U9Ax|IuMp>9 zRXlB0Bp<$H@(WmW_u5L+wQ#|9Omh86IK{ybQG56Ug47x{RH9r|iA@p$61xOnak$2Q zbV$XYttW_R9a>8+c+Tj#UTgK-3#Pi#N;M~Z$u5GYupWz*PBIzRP}jFKSGk*x^rRSD zu-+Zy&ZwHp^F$`v^zJxJzQm3_-Q#TwTI>3KWieCA%TrkrY7Hw|mjTY>4>DVXP6OYt zPo33Zh{j)HI;m7Zz7Vm#iCGv9@Vb%;H#`imU`8^>wV!F?WA{~8!ktJkCN$ycWpl*g zG-Jv}EmtHoZy;$nIRfBYEIDmILnU@f2P=WQ)7DPg``zN$<o8K0x-&l(O~u5oBMp3I zF^PbJC7Cxtv?Pll(_)r->sZ8WaD>J9DW-QA2);FJrFs?NY_W%<`%1Ohy@Fys6D{h8 z;>w{PbjtatX0U_|4OW3HkqBtS;87f+$(CXfC*H4z>amS<OxoAqrlm2kiWk&}vlG`8 zlYu?*H-JmY5c))Yx6sPm+VIDk?w;EaS)}lD9)I64<*=oLN%Hhpuw>&DFo@3P@utYg zC|>|5DgN2IOI|fyv=VOP<?aYIJtFiZA+or>)@g0vh<q&o0o(0L8#DyJ3~$$e@m_{t zTcj7mZ^y}ModB={*Xu3G=-QhyOfwaY$z;D}Qmr#XiHH<veuD5(q39ZQf+SUaH^DHW zPg#1b9GxY7g@i<~yF?A~>rL0&IZb=4{R(ol833NqQ#B&&cGGk;=GNt=1I5;hs|@yY zCO*MeUE|KhA#l-#6=G7lJnIv>Hc~)BDts27ajI8Ahy$ai+E3^`-S5ghEv^0LGffh% zIE~SALWLuxb<{^7xM}AS78qp0yvT_ubw3IRhB3RJ5eVmjtyoOC`#7gg1T(4#N<O;A z){O-mwK34gY6V@cqO)CKQrV+HYOV<rXpz7?w)A6MtiQ-$cWgT-2uP4G4Axnb0)*Go z7{S9~p*3^-m5kp6J4#}xp>RZ<F$sG>eK<->n=~)n^>%)&j@O$CGF%*@YvQ-1*>xFP z<`O;pmX(85)V*6nr4r&cj3-n`$5hJ5#M(8QxyZ+O?2g|&NbJy`nz{Csj@G2bhB8cu zApu@#x)G1OJp~tALL@U2f`LFDX*lD<WSRJsJBV2|a`4RZHJEtNmYLp<DBftXF49e> zLmO?AQAus{sl8LNFBRsRSq?r)vNCMJ+vx!h^?m2Jju7UU1rll&UpQ`Mm6a+d1u%b7 z^4Y6iHsPT`zq-b23XPQYG|lJ;{I%X1hmWr-7n--}=%qczxfJB%6$y_Ay|HkZv>(1r zyJKkl87}aD2^V9qvd`rBWeTdLuj&K>I%y_0Qx&c6_sq^b22y15#tmA*8q|^=M278G z${^!2?^J7{dlAjkTEDD@Z!GST2jLsbsFlfM#)7W+n)2(T&PCj26v+7m>ycV9qio1` z#4cfOQ8M!UGlhEQ?a(_)Ycuq(PsZ4qUR)53_hpHsj-ad|D<>@FkOkl-5Fv^<AogSB zRJ{~4t<2oCo}<PZsdPG>{%KG7QRn`!+0#xXq72&!z2I#g+tL-GGlbc~n9mQ+;@MK@ z9}AXArdQA+hinILyuody+VNiD@OE7TK0RWD0F|iLIofanW|K>yE$*qOePYQA*H_@X zR+%v|Sw7sxZ2(~A%#UY{xT6BR*`JMQXf7PUX!OaK_geFmk<KuNHOa-~`^JBtJ=ws+ zKXe>gRs=ETmPsk=wB*9#HP-u;FLm7JI~zhkOi21egYlxf;)3(-;8Aek*QA)5Lu@bD zQGyPQIC8OUPGc>7p%Ej}xp;zQps&OlLC)$VF~50I0)K~I0<(oa_QNV&2ZhIXeX&}y zD24RhOpjW`N!(O@U+e@f;~28(oGqgWcg&WKb&>;nNJ|o$TPq!Ts11q9c|wIRB_3Y0 z+|HuaGE3$I)?1@f-wy}+MKZCOjPh}WQcw@OMGE_C%T$WVr>!m03*M*%F~&=|`g#dm zzj|*<jK|V+qRDmpM6D%~;$U$Dt{M*X&O8OY=8SS>e;O;A#bTQo_iEE6u++3dgPE1t zIe@8SzJ-fa(~t-O3_GtfISL(;tE50apFyN-mVsP8xv`AsqU@GI+G>(_)dpZLU*mCp zF@CG&3yvSX_@N0@#Onc9hx(b6A`uO`I#-)NWKr}hrbk}0Z#Y{g9Vpz#l{J5=O%H&} zc9PEXk&T^fz%^OwP#A=2F*@b^;Mk&Iz%J#E&8Us?tT%(n6q!nGIank$nFntbQ(8WY zC5Ok-8}`@D0rVAG;WX1H^26m*PZIRH9_COUeUy056K&G-Gm_YIk#*}CC>`=14=2vb z%tyMe5opI*B2?g$g&~^HKGBMggqDgk9NI*Ml83?;C6jVv3k1aCVXc&Hv`H_H6<Y1h zR}&0ADE3AhNYXz)uxK&mGc+)V34GZ&pD-1MAs#ikTNFvYK6jlNXR`5`-T8Dh6&iEH zkV>+Iuvm&IOt;M40Z!#Jb*ilq-4KIzhM}?dPh`rEX}3$Y4twovcZgPFraNSzz8d$N zgb6PzCG%~mY?LMg@WvlfQ*b$+&d=EKj!tKXiek$TyQM2?QaAE-k)8EDL!?TXs5F~Y zwGsHcMoNNUT)OV86Um!4RX>}g@Ks4i-Y)`VMF|03!odpgl8(N0nw7*FGF%!o3N!WT z>lZMLeyvNc+yO1CS?Y5<Q=+quSxZO;yLL{*z0Vrg(hh+(;~HQT#0}$!SMG)+?y;JM zNE0W7*5&ff3Zp8!<3XArZ{1U>h<H$H1cKUO%L@t07XNPmY>!K_`YF0*|1K=(g<d)& z2n`PvHCNyBcty<dQw$W3Rl-KfdwNUxQG4gDNTY%?-Px;{^OjFE>fm1C<0$+*%M^$% z=R+TlzI0%Jj63?vCFf7v+=GVm<3a$pvxtoDjhgmhu;r(BRIM}rG^RAZlia;flbtus z-wfg~rRy-&cT=~4bDz~tr`7i!wzLl4TgzN_@$dJZh9I9c3B<sTp)5B7EKkHRz>ngu zIPuflBzA?VSogF@96m6sCV6gPzfplalUBXB(PNexIBlS+-CKvI2R0JxB)_}Mk2{KA zia(KJe+_C|FDvCPmS3`nWm}BA?1sgtsqaNzI|8`-lBp=2g9He^0ppy>5;uppev@UQ zLGcG$fT2}{eil7uL#6dhY-|-<H70!t0Eo)Tz`ftDTaPzX(hrU4`C6Y#Z1rPjYiFF0 zf%i3>@`TFuk!JRWviOBG`uSV7TV)PQazi69xl7pmw#VZRSN>-HDdnJ?CZCmyovuZi zy0mSvtp;krey^!KBiU_iz!N5QZXTh}f%}{&dN%CT`XybOf()u@eIkU`o7JlOIW*OH z7Uj}A!eG@5W(t-fJ+n8yiCiPyKu?(4QDZJR5}Iet$0}69SO|1ux>P+2hAzg;Tm8{E z8gEnI8M=vzr-6jFQZw_`uzG73$N;$jop1f6uwR)fF4Gfreq;9$I-tSqVt@Z*U2t4I z6aqJqAoj%MvmL(2K(8UMIx=^fywbk@Jw8Nc&FKa)d|}>CVa_UnZ9Z|tuWs(vh}k77 zk%0l2T<tRxelx7T{ZGl$GF4}VJ<wq|Gw2W0AVcva1eVm^7Rjjw-<GMAlX-x-vHeVC zFgJ^#{n_7|)N{${ZOlyTboLCRM)M5z_}bZu#-}9jPaR)b1gI3AWOUuqT?TlbPY7VN zMoFBH^YEC7o%Z7huCIA~b{iTfxIf&F(dTYVxfkfVR#VMFrkx5W=dn@UckZyVRUacu z#uK94*SF2XV_`$3q$0e2<lhAufcYdr-}Vj><QKC&Fr>z9J;Ph6$WLEbgH+%;stq92 zI1aD2IE3_O1E2bKHf0D)0U<joY2ZoEBN-6#y?u2Jf%#{PS&E`+))(tXI*YVnMA`Cc z6lPwsY>+>rt5N48zplHP?m1#V(`y)AEw2?UzAQ_XOM~3+%sCHC{7fs4%19OU=)+}Q zi1EJj!MG4=OI#h5RjjEwYB`#~XHI1jre<JD)ri>^g2Kmx9~(E1V$Q6;x{5FyMSBnD zcBipW*Z@ygO0>FZef!E%?<MPu>DXN7@OYU<1zkg@W?#DtfbuXM)j+Yueq{E<f<8rl z%yirP15$;lOak;=Z(obF@!JR~iqskbBAe)hB&vv#jF|V^a$da}>LW(YXm#)Qt6stq zS3A^ow%A>}{#cp{9y|^fccjP!)TtQyF`_I{AIBdjYjPf9fF41FQdF!T)z5>d_T%Qw z6i|c>Mo;_MT0di}?0#wYL2Nbfah2vQFOO7S@=wR5SC)h7%WXe?FVg=Nf%#~1ceecL z1rnsVd~VV5tDF;7Urorv{YY;$k}hnP|FY4&{U?*^j%I(j^{Y-`+0&IaIqS>;OEi1D zp_CE~Fet3+14}nAP}m>UznF!A0LTzUNXIT4wV~6bxsGZOto+NUhvSe35_T?!0T)om zJpC$q!&PpmFc$ZXQjPoMcyzDTUx*|@mD1kBCf6-ujAOOJWz|po-dA(2i=*qdWkcQG z7lLjK@seqE{vut_@K%kiCFs+$@>3p`&tiZy#@=z#`xb*ycExj6?NB)?rzrR|)->Gr z{!AFXRO+qlP9wnDoaKmLFXW+dKWZiR#0zU0mpDyBl7a0}YWL_OhnQmd;czu@i}_*p zG|b5sgTzmgNvDen6%fV4Eb;6}`4`Q?hSz{qVcnsv{~Q$UeFi8#chL;_nX(ETlH}*1 z05@L1BQ;tJ6Gw$&qXvL^+oJ5al-m2N)-DL|u8v;j0JA}@NIexYP0GgWtJRQ)6p--V z`IJPWMPcN6x7Xm)s5QmBifmM_@_c`LLhKHq9en12&1t%`#xeH3`3nNmKq-Ip`Qq4X zlCe5*bu=q`7mw>|26bzs$IL{}#d+X1`lc|c)cYSr8B8>kJ_q;=_nnZ@&CnQA=!y@n zbqO5xo0h}6P*-t|S+s7vbu74Lc-y)8VgZ`Rk3`cEefA+%m>YEpstef*uxiT?sfQ}! zP?wT&?fEy%>Pd<U1gbxIjPqH}zj+K;?5IzqpTzyne3l(O9KIjz;NJ-x)Ywjm-$>{h zosQBZ`0kz#N!YE{5dTQ6UgQF9R-z2;_7lyMuEbr>b?Pia6Lk+}Z`Z7GY+@P0g=;_< zya2?@$TOH$z7f-%&oMf9MS+26t}EAa1jhYzKM_~@-5n=@aKz%>0ZS<CAUBCj1S|N+ zavjIL^q$<c8+y?WE;9A)u$;?_05g-@WP&Zw$;b8u1Bcp<vr*r3wXNED>txvC#-2zu zZ98F@$P5#IradSa^kJ=_`Q1<CK#5Z4{ya@RBmY^(;tBk_diDW9r^jY>^!Qs1wk zZ&jsH<Y8Ei81YL9RfyB41yhSoc6B1ojiz78juCtS78xamw>fK)9a8}gNHFx?`lOy9 z4x}Lc`5^pB<ERA`pp}e|TyZP!(dbz0hD|dxg2SkcBV@E1OGaXDhKC2%tULNaHg)1g zzmgk-1fYNZPj)mdU)H<{{17>_9`(KOC8v)i3g<dyf0sA#9}RrD9y;?UKzEf=Ba}lj zr5k4ujR-vcsuzhQI_^^8==;V`I#&4uvIja2Jo0+Lac2h9EP5~JYy?MTMY}DrY>t+o z%^&+k5o1N%us>$w5<|nN5KqbV-}|YjD@?BXWR}h~Rp9;gW`m`~=IZHC;wSHA6F@N( zR$=7|xIaWoZ4_hl?7piuy+d|@)W4Uzet}HySBNc9rZ2rwNvjuCC{8Hm?^%_ir&02q z%91%>#Uy%4_@G8ZzmH`p701+Mw863O0c5^{*_X5IF(u{$klpC)F8a$&UN4@iCQ=C? z^=4DaFNi`RctS^#x+{Kt-i-Ju9n9t`=~fTSx>xHg?Xm;Ui6))fzu~9qS5O^woKIS5 z5I&p-x4$sfbwt=Ob3cQLhp_)lkj_{Hhx;eoDYJGar?uGl9KM+a%_V$bUfkxqLeApR z(eMJQr@IoBjBj1%Rq@#F+j_fx2=e=M%@N5^Y!5*EdODyRqK~%epVZ|KGoPWJqEKks z6)Y+X=+u5m2+dxZJ;@P-OT{hUBuCTZ+vlemh_oL|CAYu!dl=5QD9yN&xfk5z-rRGi zwVXR<2mKj{rt+jZ1BiYAg4R^62g^Aa*%#a00V08~AN#V=FhFqa?~IF*s?z+)1MEJB z^S{W1;EW=my$emLwG%gd?Y$b5HJK-ZC&%S?mU_C;Ma;L~ne-!q#N^%gmDbm7HYq(} z1WeiQi)b9$q)O1F#`%{WcbQ&5vAXN+l!vdS(&ZjE##aP<KdLk&m68^H?-gtBA#H)` z;oNaL2#DsTXJ_~q*$%IJHdLA4pJ0a(coHo;HmQAlZJgrMz$ld$8kw5<fhwyk!W{0a zs7p&4T@YjVS+o?#lSqSS9?}gRI%LA0j0({V7^7SGEe91V>Q>+mm8frup1g+DzI};# z8p+sW3}7L<vc-LETt^3Qgqz7}DDvPRYfMyFnKN&P$OBMG_-iWo8Qw(s^wvypLbHs6 zGESv4(b<|6Rk$lzjsD2R9{@m&9NL?;n4^1v{+%_O?r;m4;)Jr{9chk+9KglUSzh&) z%>jN2TBo}xQ$TIbdYGXvA3s2bJc9^D`X!FQ4QPF!5KZ>n`(Qmp=F{)X5b#GHq?IQI z-?SwG?tuBJtO;q(zYKVVme{+?KF*%P079Doh-~=!{q0#!)0D}>XAc3!Tc6W`ut@YT z?zcY*zPJr7RM+xf&fgDFJpF8yl5^CyQ+pNpT8neTB?OISp1S5pNF{SZ$G4=L{Yf*7 z_g*PB!JsAF#V3b44H$cv+e7-P5;j#M-iX$AKq2l0tTBFRk;`NYuy}RTG3f<7+^)U= zR$F@xAKXUjzL2aeU+uO&*6uuwSH++iJ=Xb(O3-~U8GcLg1Cf)rQ0)@gVXnp3(;k!P zmwlMb4nYa#1b##TzGM~@OTDw=GZJ8L9tW>h?%|><V1nzAGP9TeRDg;*Ea}lA{M5Z$ z+|25?-$7GVV98_m$E@DGM*ga_Nn{1H<5H7Q8ocN}P$K4ixJX!lJm)%DRAJKT4>@bl zRwZ{IV(szf#5RK(zDU@!R*s7s<G=PEPAcE6<tO0YNzt1CG?X>@zf38_fNuw+)tMUj za3__<XLY!kSe!Qr61fCqjd+{PqPKY<#*k6mK&?2zweU(3Pr!pBY%~5kmR;DvT3eUN z&pM4ir@D{HGn>u=<}Wix8=O0{=O`OLO;Ch115HNj-%$9`hf!g|ruiBaIe5P<fRXR{ zVU9=1=Zn`fPsudCF8>jhY5@^lktO%EFN(*%EHl3dIoj!6xa5GkcU}j%pRi2dcnwjk zc0DbqjF{IozU*z93O3?Z3zM&!em0$;<@n3y^S9MfGLi<<Q#g{e@eD$Mo-ZD$9Ok62 zK+#E`=_=@PcgfMrU?OC&nxE}p_-nAg6eestE0|qRkdXBk(NDS4qiO=ijtly>w1Gop zbpr0~vyIi$N;szm|JXBr5kKBCW@TA6fKXpqPStt;bLU%`2Jb760Q)$nzYM8wkV5y$ z3&ctF7qe}r24t`+JJk%FO8=o5G!VxHR_AD@02EU#k0qFx%j|ik&K>_JN~VnME}9kB zXR;4+0c=AXs5b@~_J03-HVFI2W{MK8BT$j-pH7_TnFQ>sEmQv!%)bNEortKK#ceFO zTBVSvAL3ck1L(ZGz6i|k|GdiIH}l5_OOfITPbU#5RKGYj-~U9(0L}aQ0Pi*_U~2d; z?-oM3y{t=wqyqKJYLXT0r78p@<C!A;UlOa%ABw^eXtP*fS)PVLI9(F*06#WqIz$BC zopak^%>FO`I}Q8o&wT<6(GwGQpFQqAC5BqVXIx&{Pa&W@?>LJ;tZ@J2@6k=GP8901 zdS|~HbiCxZQ<TCGqJU(Z%jMcUzg=BZ@or+PVasco51Ib|#zzt-bggeSa4YR;lbdS3 zsP*lSI-p#Bxj72BS#~K;^8L9dlIPkhd~@6-?L8>pQ7v7L6;Qh|dJEW1)G|4X05&z2 z<Gt@qp}J1&3c!s3Alp4bXj{*&2r}vv%;z^&J9hCaX@3UW9`v>2e9$=HTdXzSa`net z+q~wLB&yi-v}z6P))2@fVBzujXzZgVad$!D{B{kMXB<;FA``IQ1y2`BQI*+c%kTBR z%B}RKb7>Ac0f34Ru#JYC4iJ#Ibhu4!gO{4L&q#&bH-WzHJ-|H|zF=Z@t;AQ$7tN{? z@i`hVwBDsZN2d@E1g7pM0KeGgx(!+d=DYIwou{rwJrf5Ef`GGaT<rVj>euh=le$K^ zX6Z3Uldl2iP~dtrgWWN<3hNC02ZPFgXU!lRirnJ~s-E3WUchnl$429Kw{}YNAGBL- zuDz<J+qSbE^pITMUvC4@HAI6H5M6h*o{Ba1BD2&RuxC@sTVy_pjvj5F-N)G8*`*j9 zJvyAYtw(<z)KTCH%gy#<6Eh~+>%uDKD}en;ohCJB#r-~{l#S-sChq`dr-b)~F01cL zafM3_9_xJP*(l5K6yotriGm|5?AxaW;+y`rr$ZkB*BxO2gSy&^=$3#T@pcvvAaX&C zBCV~=XE^BABe#Gm`smXuQApG&nx)UP{%4^|!r<Wuo(%`zeET>)Sqb#M=ArO5CcbO> zw)t(Kc``hKE>6q57T5LuXw#reyekw4b|m47VhWT@gw=~2=XcQ%WO=k$)<(rRhH-l| zp2#?3X1@>pFlckR-GD_aS_)YQ8|ZemD|`1&0iJgPfVT&=ttOSckkiZ0O+;)K{nSGE z!YHiul2sajbJ6xP0WttM5Ep9=Tj#zUj@h5tWEdTN5fw9W&T3XRow6(glf;f1d5XUZ zPt!FaDtVQrV^9fxHu#IM2G&5KIMT%)kHEveRtx{jDiEG<TEh<@*r%LlfQCfh)Wkz4 zqlT%ut(3w#>*|CwZS6@qnjL2W*id9pk?SXWLP7G4bG80QOL6!!gSD+I!t?2_OQk9q z02}}Y&$D=6_?;=c3~a^cr;?_xADB;GSF^eAF@K)PH2<!cn;;K^$$aTqfu<Z0$8YPA z7dh0sbov?Y?x;R=D13Pk^RQm4KC?5HjR+)NL?}mJZ6#MbQ}sU!u|epg3k;y2XaKh> z(GjQQpmQqFkwinWyDz@!eP0W-kdbV<Zzb8`-++mc0|C6S67mpa)H=@h(Ma&3CroKn ziOm|6&AV6oi~_{EztVXF_$$ZJqx5aE==90j`;OX=#Z_q+xn#LN{{(Qbi0=vDdiB!* zGS=J4BUBC2d5L8ciNa3?ILDAQ0UeT;fhOqf<zds*hZ?}z5fPD+CvC?h@)46T7V5*- z9ry>DM?+`n+CF?F2gJHj0x^+OuLQ~u@Hoj8NrrL}ogUspad7cw#p}Xxit79Cm0A*C zt;Umy;2RZrZ$@&8k>JoSt+za>989E0<n+|3%%^j)R<$Ut>;~9^+s%}x7JM?I6IK#q zfP*hf8HXds3F`^#_mu#bwL%CKy61Cw-AVDALvZ+rro3E(JWSe_TH^$db0s1oY7Suj z%Mr7VLl%w&B|cU2PGqQ7@v^p4Y1&Oz13}z!1~$e^hJ16JT9*8;4%=?e#>zjq>f4X= z-Y5?4K&VD>!(r*k(GDj8CjpndrF;LNa+Cgm)r3x7vXG_tD^<8Kr#h2PO@#q2>Fm5A z3z95^Oq^Zo2x=PF5xNX3M)}Y`t&zWqdadNwNNqP3@PfN`A-Ci=>`_}aY#7)lsAgDr zI&Kh$jaQs<vxU)w{EmbuigWC_EV9>(9D03^2HToaOYa2(pnw{yD|znEMaywGkndZM zq0$p_5ycwfr=*4fR%j9ztAPZ?x!5=TF0Eg@?Os@{=V}jKEJ5q|{79llTc3&1H4RGR zT=q^@PAR4<#Fyd+gLaMdpp)}TdH~;-F@81BIG@X4h=sdT$u_cErztL{81NWG>cChO zxoT=s>N7HvufPdZAtFhV^@eIjS+U@gH`17FDws!7!&=x4NoNnI!|wyF(Tk-1l$Tv{ z0qH+tvcieALSF6z%dq@uG{(fM`O<{-(OK$+okFv(bbiGjEF|S|ITZw?K7JJu?UMjw zHm`TQbjo*AF@ChAi2KNt7Ml5nWp}y*1&1l4-}bw2&FgxWksJi$ig&xFyYSi^K5i`o zVcj_KWBcIsVoUld8lwCRK)FpMdY;UbPG8bZ&2b4dd@UN)c=nFz+KTDr`>EqZ=Tz3* zMnuMT=Fds1zPe2MvA%hUkc5!6MnKOMLCs)0^Kp0DGW5xBpei(-ej0=;%zCUp*i>Qj ztVcTA=lFRn$~`h=qa^YtQdE22_Q~G3jBmj14izhzZf++dowZBp{QlZzp`s(t#yW}) z21O1`LMrVEltE{}49o<u8rA*Z4?ZSdEF#t~wgZgq%WJ^6n<%d3u9MfKQS*6v+YCC~ zA7zXn6`l2}BEcW4P+tzJc^dySUP;dyhe{8SEgW3HAIKJhfI&`*K3atTwQ%4oV?)It z@=uN`dZL$aP_)r6zV;OpOx55foc2zI+<VBr1FYUJvLlAy9>EhJ6Q8VKz<Q8?Rd(a1 z3Bu{vvzrTP5E&I!sXXz^5X=%G;q4<`^E^uF5gtd&&h2RR=BQ>9vXKcS0_YAU(nO>7 z8jbi70+9(}FP^NUXUz)+L7$Wt1ZQJ416&`y1}zWvYkC((=Bc-Ibg_Z{2ndqCre(Go z4i-=35-oWkeg-Etd0#df-u1q&Av9*h>6xx7WHGYM^GXVRZ9eDr`ZfxSM*2lum$g_j zp*<I^9cvS&7N%vIfqIxUW##d3AM(r5Dq;Sj&)4;z{ZPMeHcbk%Ff-eT%8Q3#Ub0(~ z*P7-t(TzU)4GCgbL`aGVCFYe{f>*;`J8I(44UzOV3w<OLG1EtLMdOW;+T)WI)GdQJ z&3oal(j4E+$k$lu(G{ne@r_mG94?l>vbXgcw9kc2Za6K{!$$am$<fL+iWu#olgHe) zOUGUR1(lM}?c(T5P8$Chr`57q7+S?^^^Rgl64^Xb@dx1rmrXjNT6vl9d<Y&l1u-74 zh#ipy)h?FZ9#CI;M$>uiycZquP^s-K6`#QQ$1wNOnTxNn`dM_xx`%%utq507#l*03 z6q`OQ)Q?@jr{Zv2+U=Ll785^YW%)dSpFAI~#K?SW-IkCKACG4o?Td&+2S=}JJdzqd zX0O4`p<P&E06dVOfowL8_tOF}hhH#>SQn<M<|6B+2;ipKeqMh;kOxnOjBw%a37wZP z9w)}XThwKiq-EfpIJA%4-e}>O8hwqll+ws0O4!L55=N(6(K@%<NUnsNCrUF;!l=4O zmM<dDN<$iiV4tTh2hyQsuij^rm_H?jjA15ZaH(-a<u-0}DXu@u_e-1sanv=AGK;># zjY?qEuQC6v_W?ZlZ*U->2_ncP#(^Y$UxRBpyF+dB0<l94mL(@4)i`$!9w?6XIUW?3 zoq)PM?FLd)x5{9>pskY<_d+O@xb6Iiv@!$>YCz4^B3U)e0@CvJ3cfnhDsYB#8+Bx! zGA1pFwaqk)kXQV+bX*Uy+JCtJg6h5cG0rT(lU^7?I)gL(UUhCR=-@LQjSR*SxNwJF zT78>o^P&Nnk>ogAR9X(1x1aF`u&2?XaNzahsJh*=SE!O|rz`1QYF;n7srYclxl!QA z#t?DQOGge{n}-bWLP8(sIT<99;Uxm}7mG&rROgD^WG=ZrV18GeYu5Pk_FfGtQc6@e zR<Sb&<_}Zq-TR>?GeMZRz_<(zqG{Rkzd?kGWJ#dq)TV><9xiUz_eI@lJ+kjA;tLfu zcbX1_<M@j_aweK(%4S4^%)}BC9p6DKTTVg;%U}yfB45iykwwgc@_jH;DSMwV8qo`d zL@Vxb;kQ;rV-8nVf#!nE4ybg}ieZZ*{XJ^#MKH0vj02QP@o%^)$R#tz4S7|GNxfZB zhLyxLqSjodU-frXRMbnvWR$}2nPa0UD4w`S?tLcg8)($fHnVrrGEgt4UJgk$2A zBKGIUc-oj!)7arR=(L-#X_t5wISdXNX#XQT*v7sfl$)rkXe$h}BGi%0M)HxuB~=>+ zQbgHK!(bh2?Yt`99XiLcydmV}sAJoz*c83VDz)F+AFzxh1mxzgXZV%s1skp4eZLGG z1*`LPkeMK+X;+DL_dR^4yl-qibJV3MG8rz>2%Ym>#4BtDj&pFHY5^Ad0dIfzsoIQH z)W;fWhO~x?QTjA`lg2byPe1SGepasTJt3;N_5#8zR5&$dZu+NhTT}^QYa)3DC5`%M z4Aa<(n-=LJ*(r}E$7WZO%Cb9BlWae0!ZT^@cc}+Np>E2kwuMwHWUv{whyrPsLxRiL zfyVOMvenj^U#eyFYNbEd8<lMU8~+AtC1Lnz)yt8|SYhm}+}pLPCf0Nm)8A=VtK6D? zB|@M<G`e<I#7r=DgXxXL^IBBHIg~m{d67`!Di&r5rd7&n*i5Zms`5z3G0ABXK&B9o zYr-`S#8=ao+MF(uaq|e%#}VmRd>P8AXL5r6P?qC7s%3zgpE_gg$xPtz1+1Ds_K6$m z&d?t96|esM8^pfi2f+5XcvBCF50bG%3LQasSeT*GsNB{~X$5CNPNC%puPWQ;MNx_> z?58!Pn;J{u*gM7_6w8FgaPbgjQ5tQwkdUNTZnX(w@)*jtwi`s7bN*vyXG-QVdoI@X zt2oXv&XCg2OFLu9G=c*sIP`gFU?~EjHWt$^d4_B@<{{ent*%LN%ZTF$n%P)j(Z$5V zE%PDS0cvY-Uema`2s55&&gQsgwUr}v-&-UY(#y2Z*6DBbMqZ=zzD*eHh&tpu-2KEY zrMS>nL4Yy@WsslhU6PIOhRu(j=sJM?R1f8%7u4#QJij@?F61X5s1hU3IATb}y`{#K zRk-BJx63H%h&7s2s}C*xLNw7zp3SXX6TapQxHev;6`NRMY1O&r?`sw>RN0Z#UfV}r zh_rsf7{tx|^Ic98SWmiy6>-Eus7LjQwsc&jVE0c_1ojblp)1P8wt9aBeVm|>C*LHC zFwi}T4r8Upv^)iLTEIfplkBvg(GKKmVvzGFESq~?e&|U7<rj~tg{?nPXY5G+M2ftI zyVF~&>}Av)>jY*-F^)UA_L3Pn<g>-r*@KwGsF%<lpsmFi;u7ba?TtCt*-Us;v}Sie z)EMz_vIosn#QA9Dhh53_6)_t#_D?k-IHp{}XA$Desqv>%v?&_T-fAz<M876tvuad5 ztm-9900o{kF<0+fm4=_opr6foldB+Y{G|o`wF#?xzyN*onyfi4w4D>c0YuF0N5L-w zy%{><2eY1+wAfeZg-23pNpW?Q<NS}><`n=NpT4`^%q5fU{YuBKCX`F~ojOtMN>6j| zd0o=itI|37az{S^Eg37mjom@_G=X}22rPNU@6<u&AOekK!98F>vfn>>i!nQck=Yow z+Ls8nXu~Y$!wg&phWHP&GeIlQK!C{~ix*fK{wbQCB%$s>zh-s=^s7Y!u@=+&k>vi2 zCWDd#Z0m=#$ZNX?dbniDt@J~-9cjqm6aj7UAl<%7U}TAdWwkMwvUuVe5HeL-Ux@kr z)UG6-V(fa{FzMk<HJFy5qFk6jjbjzOpP3I{zEa36EUv<Z8GrJlGW01}a_4dVS4M0W zV8AYanzUpx@A$_k{>ZY}g9<^dMh!2m032&MGkFgIo_~ScLXXwd0iF|k0gaBJi$ToG z2nmaqW7N^pHT7MCbfl~TQYQ;%5|VW>n}q}3&nQ4XOD&~@si&362+sp`1>f~nyJ)&Q zoP6zJMbS5CVzcXkCy>p-$-Qi3W652}#co_0%heVz1Svwn&1vou0IIZTe;)x-wZo?u z<o1Bue|+PR-&U^D>hR^G$nCq6I7Av}p*OV=5VZoQQUr>;WQ%=@D-84-e9@<EeaI;= zryVjJVFYOCqauS)orznxKVcRIgKn<Ww6o6}TU4jDTr<V?NzzZH6l)6V*Q7-w9F*m> z-C`FVA;jnbxqy89yd}~W`YDT`qW!|)tLzS;ml0Muj*UnO!JHaGz*BZaj-o+`aTUXD zTj_@MK9Onrr*Zqt@(Nj^^}~a^$c=6mWEk9yT=zmQ!TnKKM3?T>H44dC_n$qcX4EhX zM|#8)x&gy`dTxSyBQS@-EN4@ZY+Q+@;ivzxf`vkbwRfjMV66DdkiX9Yz}WQ^w2k2- zjKLF^ZRFgrzDZVLkJc$of;WH~7ts6OcxdZ@aXg4JyBTK=-2y1h79=nH8664DzeOe^ z`V7}RNr8Mdi5#F3an_?K?&{u#c;Oma>KHsLwQ9UwyVc%dO5vk**aK*vrg~Xv#|*P5 zbc+eTk8$4eexsF?Xe%aqBTeUsc&HS;j9q%d_{^SWvB_J&12mF*GNWc1*!HqSq1_zs ze^_*@(85{*eiQ_kfln3LJo;Yq+C+|)gK<5CF&JWqCiqu&n8<C~3)I3WF0#O5t_qff zEk2j~^C(>)8ifSb8frW(wH2_&QpWQyfQN@uCnhXj%xCv<w<eX~RUG+LMKQN$ktFIt zQv7vzC~nQ5ZxUdoR1C46BXmx?ROA*HqD<@|RfEqG*k$t68;SWN%2wCmW7!xE;{P?w zp!s7^RhudVzPPMsKeQQbn}jtS8RwNnZ$hFdlNM2|$vRFr8ZO~pp+4gy)pp&BjDH7; ztK?4V+84!JD<ja>b)p;B;@D>9RASpxv4-O_F+e%yR__^W%ze7{CC1Wz7%mU}#wQ7< zcLE&`Zqg1*(|q%V`^j@ybVa`ShIn)stwQ7oA3i@V3*G=FoW)sO?>j37_W2E>HtuQb zP2^TgR6|aA%Ng?p$E!GnWSOlcoX8{k!)DYsM6WA+8)GL%m`nB#&&Ki|@6S4R$M_$3 z8=4?fs8v2o@~&YuBt+eK#Iy@vSV@)8=TW>J$vkw0clZ(qqo2svPK&an#j(aSI_}33 zGVh0MMS@!(%H)vq1YY4DMun|O<7K@IONfw=)<P1Ja&@CY_6&8+hor6w<hX{3tSuqK z3}v0ij7^wqQ#>p67cGfF^(1QRtz`}neW1xI@CNBzSA(P&eZ*_7MbndqRQ?&@Ba=oC zM=VpMl~QI91-D-SHAv&AgyQUWLag-w!&3Bbij<ZTFpBO^^wO0tdgTCA`4{i_7hbY3 zj!#h(^w*-16hz6eLY>lo@gIM3#-XHe#C?&VgosR8m~Wv_paele6at|ij`kSe_7t7E zTd}?j$+zW-@QPrQEdPGY#a#Y#<h)Nj&9c9Z_OI<_x){#hn_Mea`PtfkaMOP^@-Kl8 zNA>>4VE*Ll>`)&M$0#XC{e8Xvb)tnV@FF$xKTgVjHI;>2h!xY%;(|4m|J#r-kt(L6 z8UOpi8iSO;!5UmYm6-na<NfRSj3i0gLTQF#&cEf3-^NTF2HeNsE<@?>yTt!)@{bJs zFOx|Bo38&`lmDM7Jf6;|TxpM0SyB$}{lj}ELqy#BEpOEnR0bLuEC`pH+|G|Wt#S&w zNBW^Z(r~0pg1Hyvw_03iiKAXL+i<hGpi)?bDC?z)N-4gEo$)ZcH77+97Osu_;n}{e zJ(1Lx6y(TZ=4#IN-!`~uMmXUh(-bfH5?););JgffI?N01kuF7I_6LFr8hEu%P1%n; z+FLk$;&5dJ_mN}o@C<u?Cyw2LTrg#1?g1aRM*>u9Z7rPpdyc}!GJ`6=GX#=?iuLGF zQ9rf6ygPsG0&{rKc*0)uWXLq3-ink}F82iLK3b-oq;EuL(h=A&9&Ud>Psa-cupW6n zN$X05r-+4bqaVt?Wod@Z`sPKYGb|p%-6b&R4<1O<{c9(-JB9=AzWGkx5`=B%2cr>$ z<LG)ZK7TfB*dFm#m&MKxFi|wa*NguaNlH23D#%e8Kr+tZUTox43~7YTiZ*&@SO5p1 zWS{hhe0v?lulVnt_wsREo!^trlnOzJh5gq@?Qb(ijlo_Y*u1FZ>sNam5$+NOMZ9L{ zkg5I=&8DG&^}7d^=ZHh6&J=1EhI1j9y#`ip=-WaW6p`xU`G$LmuF(DDH|Y=Wamm9E z>rjQtQx{^Fzx1y@vX*NLvteNL**DRH|MylYNrMH<2gT0P8f-f%7-lyE#F_zln6<^4 z!p}2~3>IAf-gHA_faFyY<`L0NFTxc+CG`fk<yAG@G|s&G=vY4Euj==&&3pe5U-%#5 zStm=PU?G-k74>LD&q{~0wXniuelu&DZ950Mj0XQcHyVLqGofTe(@iJAEo$JU3Tl=4 z3Tw+;nY9xL+j;j!)OfGzkud%f8p_aYO!XoyoH%^y(04SL-r%Aksho|3s&@zE@XS(B z6X;Jwgf-YhhwT2Hw_8a`$Rq|Us!|~t<}_hBnP;)ubEshN7d8qQqX`KAekU#+fJqqf zY3QcS;Cd67#Wr~LTA7?Z;Ch$zq?%@p432+`x#eRV8-6Y?v@pOpSJ^-9d}i2)6ke-m z=gqq~@?`6G%-fwJCk!%O{@^VQB{tc*3KDQz_XIq$cbg-tA>YZ!-95cNgM>fYq*8ZN zK5XY46J4b+Y~WOZng2QPMDm8A`}kpuJ;1s3OT?GJ^XhM1%`KcM&+&z`d^?yz-%(*+ z2+rn`w&&L7m}n&NQv(O9<h}Detv}~4c4@}8Gl%&Oocb`Y+jX*EUC?>;$ri8=&Uc)9 zAIw-TG=DU4Ezk3>tC#+c|N8U#V^VpcLP=Z#t#h25(O!sOZ#oCvOn$+Z2E^hY3<b9* zUwpb`L*7hFH`R2UbpU70NUDIXvgR>&jnr(;0jmg`nxq&=1*hd!p0*nUB{fG7BwyS2 zQV&Re=#|uSy3vLqabf?9n~`nROwQBm9f|il`2uZxW|D~>llmcTTi^p)BqCs~3X_K3 z9EXUsT!$j8yl#`W{c$1y$V)2(rnj^U*f3%NlfgakVF+jjf_TT)B-qo^<!Eb$&2Y-t zl8Jr(X|>u~Wp;^L$;kq=P|%+b3Kx1h6DR;^UxY*&bWsv=f8JL~U)ZV^gy9WXQ7X&P z1y8!gD^pEch@pdvFTMrHRUD-owj3^bwCGfSiU9^L3DuVseD^wE^tye?wqUO`<ElTo zA;JncZlL*FYgf>RS1z&XBea1lek6yT-y7wQiI;vpRv2*)hg3;?=v2W9cTUM+<xMdp z#jitf;Dg)DNL_5;6fochr~G!}QKYAMwRVE8^u2-OeD>xYCfjz7eqPK{m^XQ>2Dxl- zQvZw-xjDSV5R}XVEnl3aGG)r=lkW!~wTS_$_+-n=J_EYx2BeOM`;Xs0pD)<tT;@8p zu=CNn&8pP_2JmUXrjfu6Fy!x@TJ+uH`L+uLPums9i2&z$*U2bLLBu<dxL<c&NT(*+ zL372gHLtD$b1RBmmHWe?Ila^{9ZvWO^t>=xNr97f0F7%O8V8`VIu&UL{T%mLb~0pd z9Ik=Hcide)j$jgSgJc`(9mNqh^PrO<rx4A4cnk)w`B(;EC)=IfiE-Nymxclh>6)~; zeS9r(==gBb7oFo0%#%%aWI=Q>NOCjIH5BK>Sfz~slkdKzm?UY4-R*px<04oWqgDO> z;mR;oh<N*rwU3@p3C>rf^<4q<zTWYPMBsoWOgQly$FsU%FH=mf0lb~g?<-D#TT`)q zuPiqH|8e!!@qCBh|G)0;j_K}hhMAZ#HQh1Y%uF-gG2M*m?(XSky4x_#@ACfCzQ4cR zZoFPk&#SL<&g0w<VfC;d@~C9;7fpE5>G%@L@5RwCw$ec+&}?`E!UD(#Xx;g)7`I3g zkZGP#1`<(%vT-xN@FI!{U9ILsQe-2gM~)BIRgxiAtfg^V9_(+D^p%nol6`)YLX-%> z)G8PE;gH+<nZX^N%6`%EUE9SdJ~vTrY8V^Y2w4dV#Ga@3^ZksZA3#m)exnm*PDpm6 z25!FFftXD|rBUTYj(?ko`Fu6EDsq3+A0_q#>8mmbSOyzK?@s}di!UDwC&Df}mhN4K zq_IH4&d>U`j*x=ocN8*z`BrXnu2xyUT>OeR&q!f68-b_48H#PWi)1ZnV#L;9v+4cK zZP-QKcR-gJA6)PESDo4L%APSgk(q~%%W~3<zX4F6#)iH`c6oKF>U3VD{{9PmCT7bG zh*UByVQsWR+vC~gfiOTO;J!PWj)cutJ)(omw!Fp5WZTZDV5aa1Ydpf_(_|ehq5@6F zEMqIF5DxYDBuqiEi~Z#q<SvWi;vX7iT;Id;uMAtOpUO=ytg{#G+HsZm*U#%$?$UJJ zme23rGX07x%?KO^t@0cdc=y|5_fXDmsH9x<84~{Tw`tN!G5#p!sp`8gHJxyL7&{P2 z{bWn~CFN}(!2urNm>SF5zW*prF$~(*&@x@&8q+D)Z@-oD#IvSL=iUkD${-PYk~dP8 zb~u|AJY>GSZohtKBW}?c>ree)I^lkQTuwRm<+>F&b-DJFInn0<N+x`fds-S?+k1e5 z!So{da@MJlJ+Ec9Up4y?Im7#|!!#+KN&U4!CoJ2;-zD<>(AkfNL<I}cJBy$AbnRNn z1}_8GKg>{z2VO3%My}bNEl7+w4*&XcvtDvkw=}8S6z_GeBO$B*`B*+}QR)_Gyyi`l zP#NM`dM=M{%RwI1;wey4EkPlVD6AEVwchWf>QkPD>0drxWX>TBB0aYOU1CYOH&6K< z1gGnf?ZvNa|F}h@y!;28l84{%@iy}ne5?&?mMzMycUEzSang7xKS9EX_jR)xP*7x> z3?X&##8g1><GPtvMexvFOv-1HxlDJ{(+ChU3e$oFVr(a(xUuKQ)dL{bb^L_&vCe6f z@bgZJvA-2DCE&ufbi#$DdHrc>{XF(9r%pBs*?-Hf^(rpK%xV$h>hF?%S<PfZk17y< zo#Lh@9>fDz^WB>NY5Pf2QrAT<>knevd1hKxl+tF~D52Bk0@cF(M7=xNF>g-OdRs-# z5w0<Gf8mr4d#0Cb>rltfvQ4ZtIt}G*i>N6FcwZJf7pMoQ49<LPa{~vg#Frg?1)=5c zz#LGIB%-Y{za$P*ksYQi*{%MmofD;Uh$0hnto6?yDQjNwl`O1C16t|R%Kj6<$4fff zCd)8s{KxmKSk-rTXWhkR9-2hwrmbV5-Q|rtftmwutBREO09`fQ$trd`=m@kS+dhzU z?GJy@3vY8|4CA1lD+rn{Mt{bkz11$;q9E%N1)`#(AmZ=wM(=mNVYj#5Y?!k)ZY@kH z5#J2!|BX1OjE)Z_m?oK^a(Uw;V+>9+b-mjk;I9?WPfR^W%1BHz1Q+N1*v84wZbl6s zDAcaVpV<GLekD)1)4d$wB0sJk(*-+HHQ#EB$Aqc9DJg2)_%P5LnC44<&l>->SjkeS z`H&NqQ6Qb9|Kt@U2|r^p9%9q5o+D?YWICy$8>;qXi*!af*iUk?Hz6w)+Xd!T<^{S` zxY&c?Rc_?R+#+0=;uHB^4F<}5HMqi!GHXiCETAi@+@tYF;#UF3k=*m+srCag@6i?L zUm!g#UXoTNAA#Uo*eaOQ&lJq=9RqnkpO<XPS6t?ZUlfpJpP8}yY+8t+!pAV8`sh5E z78o927Cm-_P8titBi`@Xt6D9atOtlaxlazW@iD-M9fYaww_3vjhto&MDm7<kD1}S@ zA;T!O1EJbnr^-hx3tT8+H({Su?<LDVLXW8xMzxRMV5p;`I=}`M9N;u6fK5wz%c(e5 zIJW#CPa{)XdD1i|eKoEk|4YDP{FQ~5yM&12R<ni4o97;E^2@{PQ?QFP#;20az5$?^ zTni`n+O`RqFqTRfIHA)@yD%3bE7F#~rLBMc=09cqTF3Q#enS3P*#~7U*wI9tEGGPL z)(o<H4$sz?@=P|XXx^HoP6iP_nfHXaNxA?&-{##R+j8@@j<t-GHB7;z{^LPAGa83G z1I{;s+0SWsQM|!UAs{DCRDqXWRFbQALSQ6~<l{*5BS=QjavT(pbd9%-A1wc#wMOCC zeC#>lwr=~j9q$#_lb++CQ$Oi70U{8rd3XG&dye1}?O&vyXE3nmc4z;&L}^;68W%Yl z{o`!5AocHSUc9kOA?-8vIYtF36f-JH?=g|D{G*MulZi`;5-9PhX>vGS^QLEB6B6Mw zpHIJB>=)9FNA;84(+fGg2&h3Iw>?2l6Pq75MAwOvE{**zgh~cR&K@_Mf&6*ATKM^) zKioW{2V}kUBpyHA*Pkv_TUM*Q377et?oyV{o-M}|Dt`l#0!PH2M}RVhW^B!AOc7kS zo>?njPfcmq%1<0+83PRwEj&tKMT@Z!bA8$e&jJZJ^q6GNpB0jxWJ)xDt6W|T9MaL* zNe4s)ZMh#q`wh??iF1g)mSJ(lLKVt~2J%O;P*_FqP|R!o1f{zrl{i_^R8b^^wE~4{ zoNHPB>fc2x^>^!*^p?z0m*$<%RzB;4$eV~%u&q|jEx}WTrBRefV(VT`=X)}*cUgRf zZn#0jOl9sguJn(0EcSD{+OnINf5VTehDke%c2AmDduU{@PZR|sHRa{wy&leUb9r}L zF5r&A)$jeaFWq;MzYSd0ID&r$f-5;XFQjp3;VO`CE;7l@ip8Aw)CYeV0bTQ~k`U^) zIES-fDPnoM_f#}2F%47=AEWcr-CQk`P+c18TfmI7udCK|`&|GSfv<58BPI=lMcik# z-eLKFJeXF^d}EnwH91k)B<u_1s{mj2In!h3Lrhug-`*S~QWEsQ+^?DfcFo7JmGj@< zXxs9=7g|yA<Av|Cu3T^&?)87ue6pAd5m^ENk)lxeV2jazs@f^7R{zxX(KldP51T(* z_x5=<4TbgPcBp`{Mkl8A^l$-BW1g{_`3%!at^K+(RPel~e@xiPKxI1f`C$btk#MCn z)YHaY&U03)sf#i4dH~?EZa%i*s49i1J{+VIiwwH{GtBqVW#%?kB|%L|VMOo4NDpiK zLn8)Qyoddd`Wz9yyp+*5WuM~ietv*DPJ$pV<!^hN*?(LgY@@Y_xyG`={cbCQt`SQf zpn}9(u@-APprm=Jw(K}pn3M$aEfs5@pT2-5$u5!%Em&7)@aG}Gt~m66s$W(y(N=E& zO3xg8J$`yCrw|>(z>{+W{2BpyR)JkoH*4Hl0cwMM=i6<moZN8(RGhc+Pd{x2-N*U= z&KK{nctwi1{sbyPQSeO(;Zb5e(}Y`u-1ysWa<cE=%jW6QursH3qOHcW0zt~xjMN{Y zQ?FdjHmgl`?GC@So6UQ=S^=CPtn+DAcodQb`ywAwDLKAD5P`2E^6@W>7X7}SSEcXv zJ4}@&35XExk3atscAC2XL-ZDmTec<&mQf9Oz4tr*=z5qxY2?48!K{Zc_Gw_#YjhEH zF~}c%kL!ekr{XcNt(}Pl3WYm{R>T7(ulKNs&lj0VaXCi*{w3uY-7EEZyuoi)gxA@^ z-T&IqI3?ifS>}5H?%Dl}M!7HVZsV@#$=s=xs{<V5)9LdaM{y!e_&4oHVAm%ae;OoB zf@m($<aYFOym38kps;R)If#OIM`NX)p{&Pw53(Z`6U9I5jR@JPjq+5cA~Zv2cJyqk z{;+I_U2y0x-a+)*P>;7ydH6hU$VA6OwT|~W#w@MUvVX^QiA7*e#}b;P>I_Yonl*pC z*k+KMP!W|Uhc0v|$wbO5CI5nF&Kk>Tez9-o`_^v)(T$K9q||O*om~SS@wG5E+9>kX zJh!k$D=Y(Y3<abx$yC>?nN*msC7RN`Up(?k4Q26XQ!%@}xpjErd~`M3TUitcaYY6n z)p_FUmTkNE6fqpL14B#Rvb8#Q?`E$0f3?U$N>!CmXvCVuP|}(NO8`#GcPoO=R-NC4 zV#x<makOyZO)u{t{V~s63qjY7*wa+zUGI$vMBr#2@wGx<JZ_(&N`mUAqLaHr{l4|< zsdry{XzuIyM@hQNs;D^}!mz;2CdZwnr5khY3MDwk$;9!-bYXqb&YxzS0>)j^<HG{x zKG@AHq@Xgtu54u_N=xz>E!#^?m7M<K(@#U?%nuFyY*;ZaS5|7|Ia*?Lq0tf!Y0ZkK zTQhju>jNob<i&~c8!*1q{@DJKk1X^z^ouE_C+xdcR1RBv?Rs-Y8y8^$)pfpC_CZ(? z&0H8WibMLqR*}_h!E|3`dqnJO{eXa_l3Gb5tD2pR7k5@jQ)+T@ZdSK9lagFF{M+#I z?3!)YsgIvnFnSvH3pB{ST(;7y48sm3R0P=%S$#N+OZj|KmM-)zJ>ESt-gzLx@?A6u zonpSLANH8{U=<Moc5d`<1OM1KDTP^UALHrkd1Hc5T@|Ct>zmEyqY5dTc%NVDHhyU^ zm)UXH@q^Uk!jQkWxyJ@@u!@MGrW9Px5CvySl!j!QY)w93Al5iRogX_mBY)KLcMv7w z#1oWP!Xt2DrH=)Q-jc4(GW&~^W%+~=Vhx=JX2rgKd*LCzK5A$lwv_H~Qn%cUE+dLn zdOEwpH*3?-E%qyE;ppp)I|qv;8gfY`H{-npKJmrXpOJ=(nQ5D=8;F%oyLhab+to=) zkw?hqzg(NnH}(xfD9D36=Gs_70c=4{6DNxIBy`k82{pLRQ^3;TJ}l*`LNM<}3$Ch! zT6LMrpVPKmKYBkTw=yC1w#s+#%oqM|gkTJTzHdOSAGYZLVNtlkXjk0e{6@sxPTV`% z3D_f6l=_XcwUZ^gub8g{7KKwdaQg(AapNwqNV%Kx(!O!UKHkk;Z)&E)FqQG2N}(TT zt)`)<+qlRFDrv{mueeJ4h^WWW9o{zRUKK~|Q@|9Y9JrTx!0+3nYPrGWI0jS~G2)?V zq)$xJ{jd&AoNc~NJ4!<;{It2%pq%Z$dYMC55>;w5NP><d^2xe(hQ*_YsmbQYo>__J zM;p_~-XQV0vh-^7<!`z`yQ^rN8$_CpMLR}gu2kKOT1W?SmJxTS%4;TtIq(@XFqBT} ze`JG>!QL%EC$5|6BVO&v?VJ^!=6*mnPsyH@X!JKPqW_8_e18;RhAWsHhoice$4A-6 ziHL)iN7N;!9#5x3m|`a1w-MEv2+$N{yCD%oS7iRY$u)VLN*C4KJdrRHiJ1{O#U_Kp zZC`dfJLrA{#MW1J2hLYdB{dB*_^!`d7NRPrs)nj8^(EE3wb8CD)rE?Xq&TTWVlyX_ zw%!JQ(pSEg1%j%r2R-^9&l6u_!P1DY14oD1@GHp&wXS5z6~h^-qi%Y;VG}ti9XK^c zR_Gxk$5}O|qr}RVYpohxw&li7@{VB5g_%S?+R-$ZDhdi#9{!N?my%zw>m|zQSp38m z&+TUAWVs+bkbMerbw~;Xvt{|WBYFj7d$0GMZmmil?<eI`BbkP#^lKHT>g=tQ;GEih zq03KKpBJUjTQjFlDuS4P{F38TXwpSz+}ZkVLZGJf;gNb%n=br^h^WtmqUPZT8<tx{ zi|n|}1O}RD+71DA>uQyRxSfq!7@}whv1D4Mw5CU^Alk7))Ok4RMDbkA5Oso<Wtq)r zn{UcL8~~pvDBU5HcZp=hv&1r0ic_OF_K;W9gMP-LAEezWqTFhs!ab7?vuO0Z%0(p} zNEvc37@I$Ns{wb;FG&v*t2;|e8i8-FMAN?m8<*DpRBJhhI~v|~+5sd@>m!yhwoaBY zzBZrm^@VRnxaKy!@;{$=0BG}SXR(uy0B{U`K6arCoz9KYqv|=sFPC||=kqv=0MMHc zHTfIEP1a8Q+p&Wdzkyfe1zlq3^eVUqSZq-8Y$Eu7b*!a~_x?onl>xSEF_yh8?y)V| z*9U))|Hqs_q32kSiYpjL`vsr6Gq3D!^Vm2SWjYZYZ;O9wY9b|l_->CXHwq19Eh~=W z#o;MBq8x>3-1aAn<<(BKgdiAF>3xYX0V}6hlWwglNvc#p)FVmvyHfTL>l0Y2!6NGX zC%tko!zf;iP9#{3eG&KcQrZ4atM;5Y3?;(!BHCSvZDDZcJ1NaDbG*sKJ;u%-n^uSt zqs|C8sTDt}1j^D$T>P5bswq+@O|KN*aRce^ulknIgqU3>q~EpQY@j!n`9+xoIS5O! zaObH{7F2w&AM`K>s0!}qfiTI7K4N`=t7$qvv)yE4GA-}h695MOK#!+C{ZNt>>R3IE zq|~cR2s8SQHrE&A!MTm^WFa>nRZ)hqiGDWRC~E#D>Wsi#H-D6k-xuy7244K&XGMp4 zm|=$3-vyB()-;bftE-RiwP2PV)$mxc<qa4iQJm(}vw8a`D12Sze58K8KWupXq3c=} z@q-Vl^bihRkNS-ZxEOORSp2;JS8XstGhwWrn9oXLkAu-|8s!2~YsH14BlMIj(!?qU z{JY^tE-p+&Z)=V{%ZX@>`^{YNni`eeRrXaCW&7i%L&>D@4!UeZzQ_?qfOIg^?%!*C z@Q-iK*sHb^<wpd}r$!5sTZt@<DQi)gTh2-WielAa(=`k&9HhBNEUL#;*I2Dh=P`lY z0+}G}tPFRf1CGCYLS?P4Q-IKOIWPgO^rx^lQJa@kzc+hr0t5Jd&cW3cxp-i<#pDw~ z<jieSHocvqM`;N?2uw*=5-|8~z$Axg?JH)Cgn8LGg;H^<Lmm4-9DIzg^O#LM&Z^{< zfU=Q?ASajb0S<G(5bEP$8iB<ZCLQZvuqvUE^!(cu0AVIdSgs8`$TQI>{bhuOseDXQ zBGmJqhXr*Yd34E!{#|jP1zF5I{0BzD`%^OatYTdfY4SL=sR@uMw$(@QQ<0e+*Fs&W z?xYEsrR`?gsgr*pw4J&4xqkJB+(aC61(9Xk_O$SF?IB7qS<_p3UVexl9=N~oG*Q#O zJDa{o9HznUpL`WI(zNE+JJwDtcv+j{a@^t+C-DkT5KNJG6WqW6`5h%HI3pEDj%WfR zFL99l+E_Q3kdalB?7OnBTcXA9X)x;a4I_77t}E}MmTc;`WQcigqxf&u?g2~cD*RwR z=weq)j0Ee&-J_;%DM%V5LS-o1K@}Aa$kV~0i6B*I3_yRX*VT32;wmm+#5nkmMewzG z0e4f_kg#D_-gZ*E(Tc~|?&^G{A{?8vY9j8pO=6wF0A{;3Efs-D>lBa~;915_SU&O> zIG!sa0#yfjmrBhQz`WU%_4vC`*tF^K{8$dq-@i1?Pz)Yirr$^P4Cg?)4%zbx)Psb- zO)x<0^ZOMnrnwP#EDHJ&HG%otQ4+&-2S1)qe&4G+{r+;TEzRY9`94nn*kjiwLPR}~ z^8+++kdz9f?4cmE4`9>bRUS+7c&rDG;qYcAdq3{1+SJpw%O~mnJz5Wrx;d&I<e(u$ z7u@sCh&lP)+&93Bxc2vcE3UsLVWV8}`?7rk$bfEORb8v2^yyENi{l=#B)*wt1cp!t zbd&(uSlf%6OBxYmf88DV<{+i*TUY=3NtbcMUr1DF)pN3A_m#MJLfEh!1)d$;_YeEI zhgpT@p6<&rSl<adT$(23N1YN1I6jPhUAaW_>hK|3vRyKnz?QdYfaq6xzAPMuuLM52 zwvsYWN>jmyF62!6oLjCjubvo9_tz&&ZYM-_9JV)t*UJuswC{h-0$<k3bqDk*b=*W~ zZlovwq6(?^&5s7TP{bem7S0^!rRUw~U)h`b(E@WH7Qb8@u7$cJAi)Hpxd9~XL@?QR z&m68oAAFI@5+Y0anGCv&>U@V^VY%c_$1NAicw5Cf^fJ%4W3-<dij)-dE%%45);=sJ z@X+}`Z)Po<6s}MPFK2q<fu!6pm)S<NrEV4WcfdtKlVrWfI@D8p)`gEsVE>3FghGSA z&Nb@p_sjGJ?nQi8ZWwTV<Z%wn=L(QUM&hy6olj)swS+a!;JEas{x2LwM;)@dGokk! zQ5I?_2snOt>*H3ZTHt(e*X@@x={G_Li5O4ev-yqKZrSVp*zWnLS}tvE*yuh0n{3rA zmFBFfvvG%>*1qn~&E{$Qqw7?Yb9SIv>%or*)qdWWOk-f~(=ur?cD8Mn0`xawl?C9h z0nY!NyMQ5+`aBU=KM9wv`?>*M!10>Xq}<_()0lR7+Z}`0d)MDrk^X2+z`u&8_vt4r zzvWGeIV}q5ou@rKsJFnC%UNRc`MifjCm6_hnZDfXEwn%T{cduRDFlw!@V5&MukWZL z2{;ToTvDwk<DdktgHf3YNI-ZYAUb9S)H7)C{1I&>c*obSWeS{D=N-R@h%4(?+vci< zUCjqX9xnq7yY>1VcY*&z@cF?(|7-dz9j$3cA^M4(Y`FVw5OKjt(*`!_VZiX#s#Tt1 zqN?o;e*512*jLfs4)OH*jZh6Vl6n8HAVlO(Wkr?`_J3NE1jP$<L5p5T<Z`vU{|s&| zjlcG`{*$A$l3(y3FhN~@@vw0oqY%{~Bz`KAtg!0T0ztC&J4Cv0vuRw%ZsCLMVAr*1 zW(DNNu=4AMspw%4D+5RfsT{m2_RmZ`26{Le;`n+uAnUh0#eq|oX}}m)`bC2f+WQ8P z8}rP-G1w1=x}%&>i96GhwuKBy3T$1NP#`ON02<x8eZPPY_UA7Up>bL9P{e~Nz6(Op z0!qeUoD=L65K}`Jso8X0`i<u`R3VP^3DhRxeqFy@B|SN4`^UCBP@u(YYk(BQG-iXz zGpy*kCLfy)^|th@D2r-r+rN}NcqW>xgr9Nrmkur_5Xt$g$+KG<+I@HmO;!Q`FSUuh ziUk-tsz7fUbsdKl!|uA1S(~7Ef*I#aJZ)xBRK?%>Ph2zo#by*;PO}_d+s=374EGJe z(vNX5(%lyJ%6kFYOh38pg056~Ch>j>np2ZHoVG4yFL*s(jae{QaX#h$0Vw>csqoE3 zYYuP*gf>_0nDBRGkRyUJ2S_bhZ6G=S2LC<ut=(7cL-J<=QX{0V(j$E2g6X6newa`_ zMixeO2G}VZ>^vV*G?mVM&cge17O8#QEA>8<SvkFmXhLe({!!K>Wq1(g^AvShA{H+L zWg~0wT}hU2&}I&Db{P_(g7GD0sU&1$u<Rdgb(s@3>iTeg2%u@|fs1L;ju{?JXyk3b zr15PU7OgG{|7<vKl0R#jPZy;Nl+n19WU~pF+_gXd{tH}f*-vkH^32B;#`uj}lk^su zq<%*a#XiT{D8Cb~Jbb#p?tGxz`C(u@ke_4=;2+usbXdrgKJwq&pIe{((QWk)5RR<` zJwdoYx)7U$xc}_}4?wp)Qi6o%vINX0ef~mWlU1j&S0~ZNgD^j0c(s%o4biH}g<R*L z(e{>rOP6cpOse^8unL4ibQ}jU){G5QY%6OO_5ahYbtdBz509sxxQ<Jk4!o>kBfQ)p z%A2g><XIQk`{I$ZXgd3E)7@+zM}@>5?+&5Go*#TBa@}N5acTsT5#bLdt2E9+5`J)- zKeXMKVW7S4t~<YO*Jm+jjjQGKRdNWaaqLy6^Yx@%<1O(#AvKK?*T!6{|B%Ew<zl-@ z-eEK2d6*#1gt^A#K3#MTn6}k(diN+%0;OX3ng(Wg@)NL*WCHV+y-%bhhdG|&*a6vZ z*PG9Ux4D(SU$5K~IiZA1XqHBsWws`)9B(4yRYR4s#E#m&bs*;2@mEuC3~er&*Yox$ zPd>_xqBZ*IKekAdnEa)(*KD_9%biI`Sxa`aIJkP!KG%N=d$uHug;)DM9sh!whJ8J! zB~uZ7LZGM-6|u4L?e^}DaKZF%!NyV)r376qZO-^U1oL4sT3!*)5f$u;%|1mCCUy)3 zRGl(x+9QoP&S6HFF6$|6+r~!ZU)w{B{XHzQKwL9iS-T}e1A0Q{SYSI*yaZ!!()7p2 zeh>%-B8GVRta!=7Wk&@zUm3cyyTi=Qkr4F}{>{sq1%}#G#9$*(5x}4NaL&$GmTaMN zLi^_fi$uqDIE%y#Cvh^wQxu1-^%@*~69_7I!L;4NEzK%%)lCkc;@O7x{*Mx5`~apR zuX!uhyz)ox4)DURpLJ_6;6ReE$dU<g_MEo5ai!iPRTEE1yl8qTNNu=M>3rlUm7H?e zu6MYSC84nPia?z7%gny>^hy2i{}u=zAXJo}jZ0|&Zo@tO4p0}If$-HDHRp&u|Lg%+ z{;eS11@0EhY8oQsc(lXJQ(pkGs-#TPsa_jIP8MzE7AvX{DvZf(B3bi?{6M`PXA`KY z2OK4lyP^N;m>2aFeG&v~aHji)e;8Xv(>YQg+H+{44?cTZ=V4a?JqZ=%ng5uR!<(#b ztPab_#Wb{~dtMztOz246>G_7c%bWW@Dly;{6#O2>N+b;Y+Q@9e+V7FT@guqwozNe# zMB4sa8k>Lt5@8Q#!Ek&65dh4b-Fx=-aID(_+=JnTqheJ!b%H)#j1zVHGGGtlYg6y& zz{1nkaWeP|_!&?3-O(_D?7o9>g)KK5bYsF?zTE?Qp8Kj9ks!-C#px03&I;XWXu_<2 zcy$AnK+4~zBp7GkW!B~y8M&+k#G)xn(=~N)djF4J3<9!VM*u~T6|zptBTKFS*jt;` zcM-G0e7(jdwU%ij_FsL{I0+<+^LMJP$G>knpnonJmoL&H)^I=`J1;gsm;PUAALo}^ zZi1S@OUwKG$vXH0k<c%<H3aalF-!6O5pXMq154BBL-?n5n6`Uc!G|4pVUPP`Lv31L zUfS_u)xt8}tC;1!R)eemm2YQJVT=bW$?lmh&A%46iIkE6CzU1CUYNje+L!eIM@j|6 z-#}kMuZLVt1!d)QP{j3=*?I^O_2cc*pb6)GptEO;5&}JG51ii~Yff|B@0qJ4m*ZB$ z{-Cl@)=<)6KO;z(JbVU~co<1J3q;C@%ay{hXC<`d<z<ExNAsQ05jXM-d%#%8xcG7^ z;AS@Zxm_{`14=Xkf}b=r=4gsbVF(i{0D@8+0psNd1tLtycuWs>G)9?>cnQrrCJkeS zM{KA7ad@cs>mSg_jQ@UE9H9XYGBkUWNT(RY9mhv^qM!Za3j$0iVIH6%!NbWf6h318 z^YAr7m;uRtu7;^1TPdG;l6nb6Xz(~62uQO+s-iqqNgoUf7zD`w`5}%2UIi0Y=HPV> zf(7?k&8sDnZ=-16q!DhQq1K!+s8Z1W=ac{aJ#pukFOaGnC^I5-@XF>=^2#z!NjPFa zd51wb2zm=$<?;VdGhbTED(!<nn$#tq{u#yhZ<>0~jyuX%qGa$2Hwe+Exa6pepxOW1 zFX9Lxeh`>~IUR4(-ylOKX6t<9jNt}<URy=jbMDjGasE&5{dXt6{4k;v4~W!!qavmv zAzogHvaOPW9P9s|hhr3W%lZ%f`)CX(rB~q7aS)ToladhDXe++NC|lMvKRqZdU%G=5 zLP_=~E&KoV%zw2P6N5rh76zjj`5T3}b>wSWzn+)gV?wZ~?b+A#4QBm+M`}J4ynfv) z>0|0hh;YXNUfB(&*BdV%k%CBzl#w#{|G&S|%loUIP5hda07JDu0xKs8TBZ9cYXYNi zissGq|2tM4L{P8yG^K*eJdmM6<a|pzR$j3T%Kc|bVNfUqCqn+)@c}+u2m|SjDJL{A zc|H)r_-pMHuySwUb}`2-xO;o0i6R8)->m%9*75RcNf1?IO!)XSGBmVmG6=LtMFt{G z$-p2htt~WPSdN<b-^Z#*`*=&gbuoLxx`Z*lA8yxZ^GW_Ebtr^;3Mi-a^mO0W)%7qr zi6xSwt<NthDS3C@ZZTaH#@1-EJYTuFeN4QXmZmD?W-I1;G%#JPF_P@S!cQ6~>9CG= zmx*!42{Ucw<%e0gX<99_@$a0bfQ5l#=&Z&m@HMeVfqeUEo4|Imv81ZXXnTm9*IIK4 zti${dF)=Z5w7P)Ok<!w_<=2OaiOIgZKUHXFbx;&TB^yD&!AVd5W;{p<5t4x_OBth( zYWtRQ;otF&eg*pXjf46dT5SyRk4*WTJ_K^w6|rWi`d@!H*{`Rvu(0HWB}GT`n~&<7 zkF{~o(#l+p7E4KXNF!hrZoV=o*8X>`c;Q06a`tjD`=ZZQA%KIhAYZ6e{DJl3<!E2T zp$?<iaxIz1R`Fq3>ha|JA}2}l*DxVdn3xI&q5m$=Oj)pcGBb@<KVbMqGdM>kDX~bd z2wzQi%SM_A(tfqJKkFD6Xj@+o5z3i8Je)qDy1HTs$v0VV*o<u68Eu{GlKw#pLmdnj zW^5`JR>;4PE`B905tA>-MV({{Lyu8qYMtjTT{!hC^EnOLTFKe|Y-DTeP(=k!Rz^}T zU#W2NP|Dv=(u6a;(>|xz@}0}o;^ZXlPj!gS$$WiO68@V%!{u1hQw)?8iZB>d3kC=% zXo&^R)VTl7oB*Ey3<_jYL`Ar7M#xY&g8g{$)Js63rT4qWvQ>8jcG!gP`oKle@z465 z=fkj_CBu709(+zi29v54Lqp(wz$ROv;=cUgbkS7BdBR8xxZUlyJHdH*Ce3~MeE~l3 zm<VXJO8x$4Q{(@xdUtp*rtba1G4Ui&jA}kgvK~VM!Y#!<^aH?p^wY)iFK*=I;K(gI zaW~Fo;e&&N>0!X(v8v5Fud(C;r>KhrO*$4>5I+2*3}a}6GXGg@|J5oVoR_Tusal*9 ztop#wN?eBh+LGep#*0yHBAZRq<Qq9k%5_lj_vg<Xi;D&GRTNS|o%pgMY{?Fhm+3;5 zW{LCfbZNnXOi3D2G`gpJ>6;R|$KP)F^SwLb3s$usnR($`S+xFq(|~@{1^$lkh=!N? z|KFE8m_b960|OcJLwvpwN8kifr;NOcPVumReiAWb(<+{(GKh7TmFe^C>Vi7I%gsQ5 zbfbV!3KqRvhUAWr__tm7FWV%((s%vPa28aR_M7~JwyaA)reuWWQ7?OtKr)I(3GmSc zJ0M&STJFnkn<fE4n*$3Adv$d+>b${nE&0oBDQa?3MQP{Y-~jNm&H<3=92<kbx3yIq zd-s^@%^z&2G8v!A#AYJH1AQ2n=m`XEBCzeV3j2zC-jOdX^b2!T*^jVj^D3nAdraoZ zn{sYc1+I-Z#$$?8yC*QRQ@<YKjI|E%!NKGQoIn7Au?K0YiHUf$`X(k@pe&y2FiSlO z3BPhERv4wKs_N9iOv%I)sG$7$a<8*Vr_!i5l2l0fWVQVnRKld}<DU)|!pQpO1V6+L zN4fS30V8|!SM@GmRb0QX#=9#p!Qa02PI!B@?DP(egntjz7~L#?o%f0Z0a6<SqA>RZ zkwCimmg9dqqDfM`WClpj4wvd|u(_>mZC4r`Hp|<e1npK@2)WD<%9`%D>FCT4XMYlG zmo=Xtp`*K9**`qNqY!i`N$i~+7Bf4*SZq*>N>Gzgt~r2n_WTAX8bX)CCppVgBxTpx zH`auW%34u<_PyLUw;hEOfApur1dWyN?1r&<FjR&JkcS9sanTNfCWdCD^i6NUq1p6; zM*?a5KCftKwn5z&c}2zJ*`G@b)#mSjP8V>==4NI(ZxJqspwWdf_GA)qZ1sU-RV1(J zYF7KV!d}P6LXC30&dyE%Auqt)kVY8q@t81)DGKnB#h_@1j_OeIUknv*u1@$FQJfBg zsoCxw0h4d_>C@-)-opGoZaEBkEIBI#ig<#JeTr>zQm`-$V10qtGqF<OtjuqC=A}|r zwp1`J9UZ9Gh*-e!Qhju9BG*BvA;INvR%^e%pB3!ee)A-DX{z7(?b3K+Q!wm_!9;IB zwIUk8grUc)qAHTxA=7N3vW_tCMT|{xQ}(@|8D<;vKW;Ve&7DAm{ETU_>r9{C*jn-a zvzTO7PR<6X`9h9E1`tXuWd?DtltsZZ1*Hl5^{0V?beDY>lRN8wFG(HR7!*lNb}0;B z<KxnPwtShzL~G2>?)3kNa)^1+p(d4av!3;~gbz2jpa8@o@c>Kvq9Ca#2qT&w>Wz8X zoNBVkw=t}5yM!=xcB^PZWX3=p-Yp>^VHT)Nj{~?<LKdVqdg=TQMq2|h4`zi*>30D9 z{a#;94VRcWjDnVL*}fZ;!@aRl_6Ab)HAxqssa^p+)DFsDf=+bZ8o=C&K3J+h4M4<x z0@WPP8uP%WhNQtkatMQ1I6u?qv>Qpp^LBjc?&@%2eZ4!Fa`!tAX;9fXGMsFG6#3Zj zuvD^nj|&;6$-w3Ql!Pv#375#Pv9aK`qW<x3-@ZM-HM<@!wtp2%n;04%zQ5dengAqa zz_nCh^yX4`g&>hXNOm2yiX;;W<(EB+Vq#{7Ipwt&&jSCbd*m&9>X^ti$;uE|cDp}H z%CMXhQd95OgUNHSB3<ljv2}3M{_J9QCa_1bpCTb6cmHSrw63_=*l{uof0~hj0e-g= z5(>XTlr0H~aa<)E%qLJFg5POJ!NOwi;r0SH!_6LAr^TJ)uJevi%v%`Aj82I;<+xY5 z)Irg~nCIcC*{h&I>7%xGrNP-+QhdBY0lR6%KsVPXOVp*>wZ`{F;s|f>!0}8i#Ph^T z;28g@_6g0e{;>V)vt5STdZsFz0s%(*-svXUEo(P(VYT<;A_$h%Jw3v^t1A(uW7tDL zBE=m+SU87?QB;_ZkrjQj@%s4jJMI^-);%jIEyls}@~#FD4<NfN(Lp`>VsBvQ@!z!* zbwrR1#KrFb1L!kQ3LD57i1e44)skny0Ucp=NC<-ZH&o#ZDC5F6?Y&6zu2oQTOG61v zgcp9ETU$mN=lH^CFwof@Gd|~?z5(ihy7*^1Nq=-EmBr`Y0E#~0ZJDo3Zl@-oGj3aT z%&pDWSYjiv?J*&+Z$a=UPOr;s;Tz1~gE{tmIrWYtc|OyBwo{KZehT3D1`EZ+qu28% zKox>pkqXW~UL2+~ym>Qg(LhRh0Z0pdo)A|-d8YvIt6OFAO^3A|)TRUreSroJllCz; zN_4|Xunzg_aqZj_D0>Wbqe%%U3<PxTdgWgM%}Oa#G$LQfc9~z@&UK?Fc0muNmncb9 zOL^_CemlkJ+V7VHH8X~xjz;e>JXDStDABom(Ob6Iyw)gM3<io23{;-5kT(nQh*Lwk zQ#)K&YQk#!>P^X_YQIljQAtp~!6nC+O}rJszcJf{f=&~)>o~5bt>_+a-w-8X*uz2M z(Q0KQR2T%VDXcoPQLZH*bbMtL5+crVYV>6;hJ+^cHAMT^)dE!tiS%+<)mTnpyd`vf z#lFzN^yT%6gz~ypwHcxy3=i}n1h-5J*?err0IWwrr)Wn7l)3=Ai1X7uY#kY>=(q}w z2cNf>K3{~YN!Gcbiu&P*|1uNcuY14e$uRjJ7Qi?$KNK&?C6qag6@#cKnZTS@xH&ti zd=e{c@g)H8dEIU&q4o6pvKu&wYf{u;Fn`(D*zh59M<ro@<GVgKU92kbg%qMcR!}F? z56U^%28Yp?E(mTDP7vCFRlkj-aksOsVgYlRW{3d?KEh=R1a%#HaW#pP8AkZ`s0k(@ zycBxCLUIT2ISRN{P=A>M{f2bl0zfY9bC+0!6-vYteVZh`PJn8hsCrNkG7gp(zE2`q z88V?SqKl26jjNq+X}FARuFo6^%2*GSY}Tp}Fu04ZK}&Q1i2-9HBR$YzN(QI?yRhiA zh2M3NGn&JY0(^Qf!9{5Ud#i9`odv(K=JWk>rl1Rys3O_)M#eC_<GPbU;l<5};i<Kt zA>-KJW9^V}j=(^x7FgfE(SneU9uyX&g5a7C5rlhWgYgh_B%NDTg_SDR(jDy3(-nxE z;*YM0n@9Mu!k`lnJvzF4j4+g?4ZJ#_gzxrpt@$yy&*FXS@tI#62tnid9FGt>K<A>* zqUazZZ%|=iiRx6Fk9i*y7n-D3FC5QT=2lj62nybTXl#%WBH!$MrEw>=grhsOfcRbq z8kBD5=RKg}qdUZI%6-Ql>DvY=)rlz>>1!-U>N|UcgAMNo?F`qlw8q$5>)VctqhDiz z$xmv}1l7+}mJ$b>to-}y^W#gQQ!O$3A5fcr0b-Fe2-a4a<S3*F^*!c$Vqqv|pEbN- z?IT>WYB(bVif2$i?-n3VI*WaO`tx8kK_Svt5LM@WkK@!ns2ldwi2Ah21uG!2V4#)- zffUZA(#;h{6Jd_w_3DN{Ye=NJj_rn3A`33S#~CA9P8KIN0Amd!OLKq(XGI%quxk?Q z!Pg;%I|6Aad{Ku&HVqR7vi2X;*3GQ~vtogfbP6sr(lQqid9Zo@7-upt74otc1OXR5 zt<Xg<<{#Glrujjih(ogqpE-JNM)1Luc7n<`{?uCR@DO&dFwkLO>LUGy+8?&riX;Yz zy*d$CwZwY&3ars=eM#Y$BPl3lnJT>51xI9*gH98!G8D_Z<^u%m5%-|kmDj4-NgCiz zSo=FF^U&s4+I<`h?E@Tct<gUDh)dIVGzOh&b+^yma_$jF{xWQ(<VL63G`ObpNr(N= z<q1*SNwmZyyy)-mql-8YW;&dhjU6qUjHzkXNjNU!k`}+v_g*2j(Nl*wlc57vl`{lh zCc)S#xyIca6GMcN5owH~NK@T%&W~^AyA=ZD;LBCmq_FQRY<I@%LZQ=;lXiOd_ML?V zS&^b1DvivqzJp2Y3x_JkT><+_;}l}g%<Y5;h>&)#Ax<<xppnhhz{D+=g3x9QsQBIb znJ-PpAj3d@8Czbkt1U>kD`Q-RSpEtj*-yTra~*2co~2UE1k%l*yi=_T>Hv0@yC~QR zuNeuQS>p3yq5T!-@vl3U3Oxzv9=p1gkxNDj$p^D(f8{l!yJTKto=D$KA3-x%1Hp}0 zXshqX<294q8!8G4DB98$vzRrqYt-ug1|oj)1%-(LfDj9x3ngLfI}~6}__0f7qy}D| zrz~z^GNg26W^?gIp|dtLP%Kl}qpG;WAe{g;7((;thc5C;jnofH80s*HMjbg<s|O8s zVyO9BHvW|y0qdyhDq)#D9RcR;*fxg(P2XRQE|TqIuG>hg35gll)8h=3YY_-*jooJI zg<MVgF0ONDGB$6_zDhu78xfe=FLoI~as>8=6L!zct97tO*2;>7bQ{gJnwpwgpldM9 z7bdTSyb&yVEw0l^)AtR5vC*K9T@*{F$~!cge3rO_1AZa~GYh_fvJJ(rg}t+M)pVcn zRU<_f=Sav4!;~J%oZ|+&^KdAB(h((%b%uk9fIviZgMt@>q&}P0*(5jBhFP7a-tmTX zLkhQWq&QT6EXKAJ@;O$w3q)DJ!l?#9f1nlgbx70=`9{N~@2x%bU;Ez}R9xak5f(y} zXY%y9oZ1q8OBRzL@U?^k50FX*>DPGFcCWLH1#+Himh`a<9<vWuAxfB(f9T2y%`ErC zA`UFy?wzT6Yc|CkfcYYk_4@;zGAlX)uW{rK%iHKQ?*IsT^v<$*LS)i_E7N((%KBD{ zuGP@res5S4Po$qn@?LHHy|CzOab&K=uOvHHkKPV#7jkKHf`O1xChHNC*+r-fhsI2o zl9a>{gqP4uevd`m%VB?%fB~l-66!}Wb!Jv|tru1Arr>U3<Y$A1hdUyta8dqQhan2V zV3VtNnaj}4n3n{X+C1n(#YU&ES_Bbjpe#<I1R`WVFnjwsmiJgSDD6Ad7u6nB+fO7Y z=wxrurbIT4;_u6e(r|FCEF0pJD<~Nz3WterQTegIc+-G&aVqV@H3L~Fg;>$zM)vsn z&-}b*5B=?Clvr{bf*LdxX0S_t9qNe^0_rA2wtqUMo-~CH2R>yo8(jb9*#e0-bW&mr z45>Am1se4^<dR=HBZ;2@d;mS=fL(9)t`7<XxvZQVqOdB)YdlJ`lwUX_GSMnXyh5e# z_<W#Wq2Cr7CPTyXnb1OY#v~^rz-H6B-ZX6axY{=N8m`BfqR1mVcHJNs*Rtc=#w{$t z8@ZZ<g!}i;P8Xmos%dC!vOy_v@docNwXHPkovomu4W4Se7>XKGEH0+!qCq20d7^T4 zgGotr&Y!A|Ma{K!m-$W#9Qr~a)FUe&H(>+Nu;M>jzDq)YWRnJC4IdCW0nHRGf4h@E z6g>~z#C{l5K?vcm@2*duzdm1oT|-NIkM9M~iP0$spYd8wPA;*j%gpZAcb<k&=iiTa zS5lBGtn-9^?>AJ0w3_A~uijXo^1|!jVwoXB@k$x&ZcFHxP{VaWYQy!2Jpm+d^atcJ zbLMXipF1GaVR+$<f2H!B0*TNzlPRwyZS^Mmr!m;|+xq5g>+&;&yg0gduY52G;Cm!j zs?E^}xYPW)9sN735#$Ul%4gFI(H@v&BS|bhg$~{iY8{^wJ2$S~MieU`ydi-^3S~Y= z{KIzMGTt%UPCMXgb8&H8=ThJ5B+s&Lef8%kM)yQp_F914(D#szS6#DPHwTJx?HHjW z9Tu@-;AUcJccOk99TioY*~A1N1ao&W8D`~7Q&y3Vra^u!Hk0lb@Vhtot&ZDoBQYdk z^lSJcVvX~$gV`2L2|)s7pYt-Xji7kpq4=X$heGEoOq03H(H?$|!v!hT*4CC*5_kB1 zLF$A086<oTh4|}WATe}7*y#55){ji*i&gnqH2jBYaM6X!I05yzq>b2${G-PJeY~wi zdi$D<0p-?jL4}OR3Wq8WLv5%=#7jU6!6^}Lv<9-IvSy8?w$M{j+z-qMs!q9c)fjru zNH)b5S%mZ?v#6s5mKJ#e)z5uMi#@$Am;ox|8UHz5vwlMlkI&+X=QX-1lE@trK)pbf zV(pee`j|RFUGlLBxSv%>b<*U8?4bC~>msNa55cTcN}jC&s;kraMz5JrO(mA)4=wp> zZDsj%pEDx|56Yk{)Q7y#&`_8J-i)sI$leM-wUT9tJ|Yb(m$WwD>*)l|Kf)R4WV2ZV zao%ZN+(4vGAneYLG%ktZLA3A-nEEUhM+d5(1A~P%%@97qyfr;pZiFD|bXIOO9mdQ1 z>{MH$^Bmh19XHem#q3O=1<g7i-=z(1Ibq+0jr&k$v%x7LLs&#e3H-Gnz|qAVjUoeM zQMk@^2m!Zb_A^)a$C_9nN`sYVD2o*J>!2Q4BI8X5*-TMgcS{EraMgEbgPKSBG4uM+ z{S+p4%a!76fGUbz4B7g?0HzZYZ~hh^2d4!%Cf<oXa)&@y;DSo<*mG>*$`!SJ`@otG z`{im|HsMBqjz+>aPFCD7nkKNB$A?EEGH+jj;x~h6($R@%Pz=#-Q0EXRhv|BcK;)Mg zg3xR=!{|=uKpm66#4<y5%N;8xf#TeSE&yYU&HuE<3RAU?;<pSj3hW{d3te4O;i&ni zG(s(k`g+Sc_Zk?d;P&|%Hq8nM83lUds&TC2-<gLUdh0Qc#OH;ST%6h*b!e5kYmdR- z-Bvt;1Mj65W`gtBoy8YkY6sEKz`lc-Y(1T?^k;z<23*`bh72@kkZM`)Wccmq;~5%) zR6Pb%hNO?to;#8UQ9U-)jR<tRkCK}6MlC{oLPC?4tFi#P0<qD3mQ=Is>F`gZ<MW;B z&u-={4>siAK};w^IxBq^($`kOKkzFEorr#em}I0v8o_=i49RHjbnn~Ibha&FK+3v> z<m!gNH4?aPz`qQNvr{0<mg}P{!NZTRj+5Cj*yE0p&VTM6?k4QPphnmv6LLi<UN9u` zFVp+_c*RuSQZ+1!SttPPsrv_D6QVt!IUDYmt);_!9Ezoky|V3u>6}8<$~T~M1ocol z$l&*L*3Dqw!tw#ljo*Z}4qX`>yj-n;0%Y?K)MNNpuc+9B{vNPrOER*C49RsNg;YaB z=cfy~RWZnXKH8PoHu%e-da*l(U*oo4tlQNcgy4{SW{J{F$9lxw1C5Eui+bZfB>|m; zG$NRU6NI)Gwu_H^14<T1-!%f$4ij?te8iD(p{#L-NN1y|)~(ZtPsR7eS!0q-1$Hj{ z^8w-hB(ud|0i#>K-^y3{n}){)yaHnOLWM{Lr*A@|&?Gy`F~nO;%zpaM3(|^`cn88T z99AQA#KFFI?2s_{j7}qg>QiT6jfN%7vCE(gQE=Byu)F&F#Mcbt2O>_TA26!%T06yt zYg$`*DhRq*iD0msV_+fotKz1oKOp!#=vVBBKaf@kyg3Bbi>`fE_y6cIp_+|lL{MwJ zfY|i#eFG%847pG~!arbI$16y^ZN?!HhitC{3fEL=9!YV+3V{lgyPM6Z6!U0t%vmx< z`{T}?Z~$Yrb)aa0iO~IGm2?IY6u)x#8;pabg!gR*>`dUh_sZ3u=2&m^&e<A^m>Ez= zr!a@FfR$)4Yf|B6Y>k$oKOR%?F1QtWg(GH<RkGt30*oJ->qRs-2_=O%#3u|6Axj1% zgY%Us;IeDEKY_X)_<9??1^bosCqh+J>PnpI?G1Wxa)8^i!`yNsDVzN=6>o$j!W($V zhb&nuC(XlwqtD@axsV|Ar6~nLji$qHkP3Z)XHpHt)6tc#*<ASB13mx>?Wm~})geQ6 z_O$AdafEwK#UzaV!PWs;r^BEI>f>9TVsQ?LSMwN^5P}a9u%-_NPCWef%Y7JU%GH?U z+<hzqJGQ)EYQ?=ZG8dUzO13L2zk=f|cjUn=>$xH*rU>mdH3F$u?Rok4L>v9Hf@>92 zN(zRTEo|EPrkLGbx^x1ga!;tb-Fjh8K2dX+<yGR3kW{B<H7OHaK!70psg<ApH0qkZ z(r^F-3Dxg&!&4OZC}18)AXAvx`3WpXA4%X;GXIUfluP)K?6sQx@X}QNQ~#%O@Mk$+ z(`xhO9x49$NG%mDiv4(jAm?jQZ}F~E^YC&#*+^$b3P1Uoyir6*w^tBi0Ch*{PwHl* zWc+XZaSa7{M*00S@K=%93d<BQf9H|TlQ`BnV$Yrs73AkHxJc2+2Cwi|J}YkC%^#Z4 zVC)xEei7f(aa~A2$fj#}T^QgaM&*NU;>Em4o=kdU*l7?<hWS+wJx}QW;yC}~jbV*J z{V>}0Ws%F=*=dK~$oSYz9aEb=UFnFdSnM5D2bNz56D(Fl)IPqLjpiRysn~5aeyx-@ zTrT#LHrRKH-#Lc2kkmjl)CUuiUFi6!qUUJb^ZyA;=e(Sy6W<(SG*MB5t`28RO+7E4 zkL!PkJ<3g#XVU$`?qH=f6d)B1^0^35`e1Q(iOGN%ll|>I<yqMQF}bhb9=(XDlO}B- zGA60|!T2u58kbPTtnV+hC%K-!sof6Rz<&dO70|%NbObTHl$s%!zf`A~t3n9fdEm^L z&uKElc!jGm&>wz2&*|fVcB}1i-^7{FK0E?dP>*00Yb=@B%&Ml$aA#=~*3_oO;8{OS zNx4>nu(nA=zBp1E8@OorfEFXvCb*f0|4$#1Kt>&J(L{zteS@#QO&8+nkF-qDb8%T% zSa?=c$`G~~2Rk0o&@d{OIUJR`-#z-DdPlJxTY~o|mqtdng*q%&4j}3pfZYAq@X`AK z#7!xnDBK#{|B^I9dj~_ExZA5(So~jT>5LCXxboXIU%1FmJGBJE--0eSKPPGj4aPG? z?xiF;-#*~6F@BtzcIIU{-jAcpb-<-`$;nwJQh|R`7bG=hy1u-#@4T_G`Kk2xhs$sm z{SQmqZ&Z@~gN->$h>*G2G?~kWHE3Kbl+OQlC<Tm8un*m4VS^IWL5kGc0OoJE)nbCw zsaf@ym~(6IC!~iPGt`5FNOJx&6IE4j=|JxVPT;P#jWCzm5m@HF)y!^dTak-!#!oDY zd(>=iZ78d4OhF($q?k~X>=1=HdxR>`%awq0meSVtQ6YrDet%a}kN(<9@!v3(4VXd3 z#7cVeNbPwV<?42;WwTZhmx>;@8PM=#pR7kPi*i1wlM3HhOZT?Ts&|Lt9vJs8Cpm|H z&Sk>*rqoL6dv_;Dr#61D+{uEcUT;w`2izc_UH%XQq^)iK%kPWB7seSd+CFs@=pp{E zoT7sq69f!a)2=p*9)nO1cFd>}a%bw&MIW`a7Hs8zyb^a*pKbp23l<vg!RAoi#iP;d z?$SV`U&3O{PWnkbai=HjFf1&Lk#Q*fLzb3{=t|s6^GP|J@0xmX2AT+*6EM!;VHR23 z1YtjzEmS>7xJ}0VyRCpp77!WcbER27pZWCEjQ4zYNGQq8yJKd`5>Ty{1hu_eZfz|r z<hX_19~n3|O;FaZhe@`wtr*Eif$Y^^8TU8+(`>1;UWa-@om_DQlFK@L{0fE-f#4B` zgPy^GFZfs1vJhjWAjJ%Q0#wK&9rCvo@|=$S_Z83}9UC{Y&H^QkbAh*i`u_UUnG>O` z;rc;%xJO-ufWqzf?<1F&g&(!$-@bJoPGpFA9CFD5mXx5i$h;GX)fFfnzQ)Wd?OYfb zn(gJ+p2?d$VEA3-62HetxuybhCKBi)WW|$nOtBLgvq70EkOZZa{(lk@pQx7(I90Gv zinQHV!EGQY%2Sy!F>8xYZN|YwIhw4ft>qSZ+&?(*S5#aJ`aq}Dz?g>?_`c$kUf!}M zxk1qkU;0G+(}e56IzG9zr6s2HVzpWssz#cwQTTHBV;=pa3f+qwg&K;a+Mj>Oz!hxr z|4#coEJ%D$Zf5~r9t>p2hrhpT<(;+$ZZkX7)ZmrUZr<ctI+lL@S$8!M!v}}jlxw$2 z%xn8WLj(R@IwSsEG~|vzO^mCxtFSlzUMi9<>2<lvoN~FH3=Pciv!nk0dYqA5!_%8V zo??-ngI_h7rA{)Evb4yYt*?1rUP%H5Lssjjj4g96U!o2fgdOvX%k&@l$jFN)OG~q* zxT<Nf!dt`wKbIy_uY7dyc4xU!ag+(-Fe504tGF1wun?Z2_%`#o6am$xZSF!O+9T3J zxhX$aM(UGqL5{MvgOavxLCFjp!f_pn;Fl+j`(a*_Z}du@-Y!~F+SkeKnD<+5E)C<! zp`g}6sJ@&jc!y{NsN)sQK8hi5nNxxviTwY#ddsLdqHPNocXx*%!9BP;!Civ81b2tV z9fA`i1b4R}p>cu}tZ@zQPOw)w@7;Uuc>LiHV^H0-YuDOqeRIw#sar*9SDb%O)Bj%M z3eX}R_Np$1;OGx|pG?;e>?$k6FD;$7G~>n5*;@U1jgSAWUX;(uqV)I9U_MtMHauKh zU9b!aV1*8jR1`es5w*&$K3IABeO_1nD*NZKt?h$lW-K#iz;~3-pA4lBa;rYG*79^j z%1BGcGV|X4o8xa&Ls=v0lt0j?!iO&Oiv9n-FCv(mu@M7Zm_js%rNYSx5ocy~!!Ay~ zd(v)Wnp#n+7z{EWF4ohEkdQB9nN`*k8fYZ6lB;xjb8*b{x9<gEBBA<KSv#vd5*GYx zA8FoB2>s`Hwg)D#<MC;0%P8yaH?Av}^et(z1%(aE<&orxpeOO;W(3)ue3CifXi5J2 zs=$KD1HPFuZVF>Jue02segvLv0x9E`j;(>1K_rV4>Lu(rdT#gE&0cr^`=582CUdFu zWAw^y_DjHhi?dc(b;7Ke)U$EdN>}#%c3vT*$8gsEyuNAQD~8|D#aEVcOF;aQO_zFG z!3Z0MO%4h{hy)$YD<0b`H0&4V*q@MDsQ;oAR90}g%h`(hoF?h$K@5l(w)f|YTr`=> z%SFRU>>3sEGvbEcRN4!NvnC&S{-Bflo&O9(2%r0LCI}z&QOHXTihS6`ecz}9>ZNY9 zKSOq|rE;o9RO9cMdiQ8cg)PyyuoqGtf3;+!QZuEZBkOren<;|>X&D&<jZ~mNCMn*C z<bv%rf~nZ(E*<*ck|%`pUoQ%sDvlRU$&J@xI#<ZXqlt^i!`F8MAFrCA=Dt>tiFIve zHNa`E+-5R2=IKnxLOu_oCx^xmJ$;B`H)ZlB<=H6VJQRn5rm{3t?Jh<+!^^lvWtBYM z4mK^DohwB_{SpNickSD9sfWCO1LUTv@-awC{VQDJuuOyWI3V9I1lc6BI_Unplo@HL zCJ{c4lKhQ#u$esIbJk%_m*uNkSZJ)Sj^N04ZSqP;5I8(kEh!Np$Krr9OL@DYm+1-e zT2Tfz!9D#ZZ~k<NPDNiOmrUcLDoD`PYOIo{KZ{wfHeuc<yJbrZie}mZuS__Td7o77 zhxwU9^v_&VoS>bZ9Vr>5^*w65@_!K00SdJ2G1aVqk{qMGo38DQ=YO|?3-W(lLFf<Z z>Fl*mp6u-XHG|PnF8~I}Hm<vhK48*5{Vv50z6ZQZ?5cDs>e?kmt>Xi$kV>y8zN|(e z!o!EfYn3b?Aq0MI`i}lqi$cC;lE0lb>aVTO`WFM1j3&M5boYmLHd}0fgN(fIv@b44 zGQX?3*?dCHgo3AKIklj$^jSIL!gBv0OAr3g=}b{(`ph8?u;6_NVIJJnt6-xSE^GB^ z?EcRyl^E`1C02+VXAS6)@bLUA2DP>QR%_>5fs1yP85AAvz&U2?Hjd*MpO}aULs<La zUiQ6fmBZb-K7n24m#|#5zU@qvTg(2Wt>TnjRh7qWKnja2!HjPH2}FFt?+=9)Z|9=? z9|sXvr+G8)Pk$~3F}`?1f_sJKDi%VgK~L#3u!`7Z*d*~i(FMIdgML5sOJ3ZZMHfqL zRH;4qIe<~}0}_n%@xzF83dw&T7gJcE7KcEk?0>biI`ldLpu3?pzrdG@lau?jQKqSw zS-aiBLQiGo#}V70D@zZ@8j62qSI7D%-0!o<(od?R9xY{agJms)gUd{!G5zOuZX+)G z;*0LRJrT_3Y82|E%@oa}TeK3;Ui{CqPG8LIYcSr_mP!{kzcnb=3*)`vxxvSKRi_Bq z=e<^Z<Kk(=dSGdE0;acP=imNOk(=B|9G?&yJU7Gf=MKXLY_w=){&z11)(R3Zuq%uc zFF}Dc%H*Z?Z&My$U+eDezePKUrQo#HvOR%8^m0i-Y*Yo5l1JnDH3v&g{l$IN<^Kqz zlCjZGfwS22OGRJ1n2f@1hhoJa+mqidtmt*bs$(?gGQ8cwng1!T%wgqmHwbJmoE1e2 zP}WeA_6=;(S-hkw|M2Pgvu&BJ_#qr3e9K!pF%0Y&Y5I&=EK&qntpWV}L|pKMK$gjD zn{!V`$F=^JhUV;QDJr@&u%n;JK{a?@SOMqsh)|r|PRgKRIl!$-*UG9y>3t5jP2#F7 ztm&{^kr1%9z^GWZj*e^fX0RiE0bJul8L*Vc|L9V2A>g*jyS_`yI9jY*{J=}%U87*d zgXVs8tg4`{R^+2iOne=Q8O$KyJE|&HQKpQlqIlelOc<?v=?)j7C+5gqo>Y0ASbiPb z-1^BnK^+YR-Gig#SuUq%{J}o2vhr7lmQyDQF_RWtuSbdn&Dil*&%JU9ED^Fr&6(nW zO(E{=MGJHD>}Loq8fE?wuaPa)J(bII`_J#Fld~eB(+uGuO#pZGH6!D9z6j0Vn;3<U z$W2b5gA-GYj-KM;U4R8xuy6S{ie8=Cs|FH<fokes&UidAcX2q%FLO#5$lcrBiZkGk zHa<0liHr>B@d)eR>S}9^fhHcoJWY}?4ypg^a7f^b;IiLTz6jva9vv6g33$?zo%V11 z(_-IWIB?;`#>G+kpKu1t4cNViJV^7i3VBW=14k&q-ggnHu!zl1cXpS?*nL`BCvwwd zH492Gc8t-ZmeYq?`ugY8#gie>ZH{Y|Z0wn;-x5dO@4?eb4hyrrZyD^Ugss%nuMAKQ znNb)Uf(_Eql$3)&vNl+lj}QFgHU#{cqD&5#+_I?UohBnt5cnC_sH;dmWu2$fz2gzQ zrm$02pE=0@!uV#6$rTTL?L_o`4#~2o=MB7v3r8x~s!>+1)GWKG<K^eKU8w%)>Uu$R z`?52}-UF923xFU0=CV!;ttBN^TYVAsErNV}_oK{pXp#LC<i^oZkX#_15_f?t_vBym z@#m#(wO)h8Fou@7Cy>lbzN*cK*Vxnq^iS8yR4J)F$fTZtuN^)Imw*7l$?5g^ky*Dk zm)~`}_+u*Bal)B7lFvm<0V5V6@Rbb=&Q`2{&4C3fK|*|Dz6*JC&%ZaBbaX4ZM>7e0 z`}=ja_JTQI|2{NWY$5FeI;GeP7w&%C?-mGn#`ss-<m%s`(=}^_&ItxOA)c=CF6aFE zT8pPXtO|;XqAxf7Yn=&VuC$&WarNz<&}Fbfd0$HxbkaHF6PNl~zT@KI4Rog|Dv~<h z$yVMN!-4l)zHxYZ(s_E8tXH9aJljX2rN`9jH8|<}K7T|*^RkUddcxT1xPm~6;;^zh zRVFPhO-oO|^CuWsC~IPWBbG95-o`RmKik?8ahk&+vHwB=GHrE+_5k${-M^d%Yg}ol zVsZ{ss8nKLs2V}vtAt%%UhV^MF=}sMSWH8JQ~+W<@Ba~NKu{eZB5Zq5&rD4XBOcaH zw%W43rzjQCP!@u@k$xDLyolRCH0Ct$_5C}MGw<jq+XTZw<I-er$9ReIOW<8q6{eM? za9+lp!^e+PR%2=Id*QV7-Y64s-2}!tE~gb>xdN4qE)(DF0zo;edU=hDl^oU;tNfzH zW8K<kvtKEy4=V^wYktLC?|xH%hxOeIOeVseSuL&o>@=HWpk;mPjet&~p{_nTm;K>T zBk4dUtOEn&XWZJh*4ZQIzK6M(AR~ecknfv-dTGH{aGZN*TM|FdBKC)e2c{P=!}zw- zc?jfXkO_K}%MCmnRn=jT3w{9Tn51~N7m2MDud`JFX^t;k-`%$dkg14qabXde2Mo5< z@&w$ifiOYwd2|Zltg7PB0c`Nl*1WEXqrE*q0QRBttwPxWUPip_^}ys5Gz&eWsit-T zkZnb;z(9jea=3P>n7)s#y?8YXiF|4*QHAhg#rdmLRI^fAVWG)nu72d)W`5>)5VE)= zX!fIWgvl}FyhnE;3tKqAM^2vH{mJQR_~M69Y;(GQ`f$=`lwNOvB11Q|6Pc?s-aD5& zhjqX)bh$!vh*IRYu!ed3`D``i7qi%l<jo-+{NLHAg+~|L@4ataF9$7Joz|kS)6&k0 zWQ|r`K?<TEajE`CdZf#pxVWUpaFyb4F={Ll5@OPoqAuZYnxc%qa(^*<FGZpJ3l`c! z-RXVd<qg*?Q@MR7{i2TVGcIv@xifsdM1}z85ciCY?YuzDmJG~2s6Idu58r^BUjc}M zk(GyJiyF6{k9psB=`iNvnJoGpG`665!GSboOUu8KB0YWRVc6dD<#LB|ZZ0ck_h<m= z?5B2>Bak=W3kJ2_pZdcJk5pELm8qm7wi+A(&8e*T)UVq{j&j&+a%$(0!`<nv*41cl z0Obj~?RjIJ0V$+i%m_`EqN6E!n$t*GP)lwGm1uoVCt;0?bI2eMi<rFMU=RDvZDY)A zHvjR192PjKBLj72NI^k`>NT3Cds-ws1P<$9{&TS@G(G*wjV}ZAQXYP<W~9L6sPerc zGIWUsvTI?{RMdECiY;y28WL6MZM|7PTwCumO4NS<j-G$g3sn{1BXyjvNU)J^F!h@p zK3rn*d__=2q5TahKy!1?<mKIa_oVIT2X*kTwT4ZyjJ+;d?Ap~rI)3k}+&kG?WYzJh z!2=0ZD;|RaV&%-k!vipG=GJ??YzBT^y#<y+q0sOM2(sZ!7ntTW1^m^XD93_*svJg= zQc^#%khraZqV|%QKp$#S81aRGfCBr6_faC23k^)Se_5rL<1=(>s?=lIPhF?apTpQO zVa%uo@u{iHii-__1W>?l&SZOMM>&1a8EuzGE*fxF=v8GHkouRay9pQ09vVd%M0ENF zfRA9ou(byus)dJ-kLKl3Ti1y00L6=pGU(wQlL_G4>9p3-P!&DldNfx7Y}!|0d(_<+ ztAbR&C-^qdI-%OF$i;%lV&(z(5UT`!sU53CW?Dc<Mpx|hfkrr_va%BE1QwfllN$); z#gnl3(BEDjj*2{jRG#tWi@KH;+&lpCb^w^pR5f5qhnUH35_ET>-<9-H8F+#qKx`1h zL2TYKKbuM2M(`WC-S<Q?0SyzAjW%bK_BJTlGez7_J-|fkfGl>CorJr*)BF72yw;6t zZefA?6RWIP;NK6SSeRt|Sm$q`q-#c`P14tXOfjgtt}fw$TG=V7NVIIxK*8XuB*lw{ zP!1QAeiNTRrVDD~wzLECve2k|z~e8s1>jvuN=nkufMmD*sYbsEfy#8Wo;E~GY3PKS z82c3dOziORkb#z#R$TZ2^X_=DE|B3m_~g^`UMCWgI6Wz!lk_vAu!IkT=}zQ#Y^TU1 z%r|4I7^|TSP8c*ZiZbP#$Uy@9`NOiz?oG%_evIYVZevF1Fs>@~`>NWrfa#A)%mxka z`;+9r@YHE*EDWNWWyr5(PW%tgdQjSglHfMT*R1Wf>y^c?e~%qSgolrcOnZPxDfgfz zS#KlE6{;Aj`O|nZz&tCE8luO`rs&TDm}O!D)4edt-Dn5aizF$Q;=wpV3y)XCc5zq1 zmT-W;$Vhf|?EXYv?^(4s$5>>NZd?aS(pH}lT*NrMTW4mv>xKOc90^fTQhov+S2l^% z?@Kl1$=einAEC{7-L&$06n%>|=(*ha-dno3m_~W$=xbw=k3!{yZ5UYBDb#Sal!Y<M zm^IO)!bdQLQ##?kR{~lD=sfgFS#wpo&J~A>SNt(hVg|kv5~Zo#GpGO52?z72;VMxv z`btXjdRZ5+6Jgsiq9434?l*$Pf+pNWENl{CT^#`r6V&AQh9b-XfHs@O5<y60A;@99 z>#^tUh286OIb2WwTIdS}uN#EMmB$cTT|m$O!NVmT3pQPzePX^K<YW=Db$_--Dvdxy zT`R&*NCuIixk)-K4Fq1x(J#HXVdSD1HzDMrg&l6U3?WixDIa)w3xgeeAllN%y`Y&d z#mELrz&I$e6Ll9A<7A!E@8Df+3cdqn=Im#HRfV91G43SAbxHmU9EM<NnI~q2S3}#9 z51m{Tp;}N(Bjdbl;(b7!{QUO%vUz9>BuU1xbwpRSwLz^z%X58=T=Q>#fuaMU-2++z zfvVm2X?f-zWDM@5f(3V31modRJUigwQ;4NGi%!xIQc^dNZ4HzHxjm0Ix)ZOg%*?Rq zA-6-dlBQ%MRfGFoaD2<15AbbGnjWFHy;L*pRcg413>+MUEN5um$ne4}S=hA*wjsE^ zC~rVf%&`-oGHmQ};)qG?kZbt9X7h3&6^2JZrXEH=TQfTqQ7rJA-w~rQdo}bu($3u7 zegvt!TD=iGi#!sVpwu+15WI5O6!Y|wb=?)SKzHu*0LPQGDb-45NjC`)XWjxZ>^)!s z^O8&4sW3%&t9YK=j11X#;}7nR7E77I)DR@lQ%-t1F|UK#;v(r<^`}phR#vPIPHZkc zL{3E5ekEmM*%@~p;^IeFe=4)N@w@$>7|V(<IV>wDPjo11X82+fg*o!@sGH^R<6(4* zrbG?KUkq^#)0gd%oVI?=?DbqY;PH&m{&@rxD>~H=6m>4=-HQ(>nHZE(A#u^wI&C^y zM6e9g5=cHYFulFs*E()%t66r#Rh4e6f|;Kahx5svK42l%H-fCty@WVqde?E%dQ$<f zSU`GuWwL92KL{bqq5vNth`lt}ASm|f!D4*S#RGFXZu}%)VWm}pkqQqKA|;dt1DtRM zi#`r9@vj6xXQ6J5?*@dk_uoMy@YGjB!^5r=0up(I8FA~0uY){$C{%b*BLFYR?Y2A8 z*Cz?^Jo}Aar=Rp#LF*lWg(O(}-7U{u!vWkLJ){8^TGZ$dxE+Gf-wrET_$CpCke->@ z9}Ojqw!YpE6N)X1MMTOmf;@^~z)!@y2BekqN7O*${%mQ%UxhxwGbM%DA<vpJMa|33 zM%F7B0fN(CZx>D6?U%Jf1aJ`G2@%|D0Le7Ge4iIWAe&JK9vNKY_jgkws2$-wN+a;7 z1O?E2yfg}ILz1D22C@MFf^RyA=7g;oDf~GEzR$wVj0Y;v%*mn~cbYnjzZ!+8Ydnh+ zmbk?X&SMh{U7ZbZ2`kkl@!fG7)llGlp!OS9A#(2>k}2hl@ASWS()tOc`r2MB_PnNd z5@te1Eh|Qc;)%nM0YD+2B?MssmKU#-K>IG#!oq)rJ~Q>3$v;fqY#b(`UQHcSHY9s_ zi+GDqidYeeb_DoPNw=^DKPW`}>Jc!6*+cX5v?Qi;y2~bm&9f32MjJnzX3T?I6X_VA zfXld)h&?pD6??hC&0tnkOrku>a4Pdt{5LW|Cf1#|v9!HEnK`;0TBI6L$WOfn6ku-i zrZ@+>MRud(0lpWTKf3)*;%p@>h7l4-_f4u)2n)o@N0(}Zb?SPswxmvciE&n|a;kT{ ziwpb%mY(l>oZqg->?OL>REO`5Z@lK9)&JxnlLvf^ClmOauAAR;w=9N9wE5sKiAek1 zU^N5^=DiDJzFB_QHyS6Rv`Ap;0)iTM|3G2^hjk|Z@la4wcoA}p0s-|S!93ackh-#4 zU#1YOQ$uP%QW7_s?b4jx^;-jsqnoIv4SlV7n^g6l!)JIa)|r)<7Q_xS0AP;mfV3nI zX||{WOm0=dX|S{~7&=MhT=85N1SBM^hV9AND9N}2R?`S`XkPhhpfTd{m;@2}_&`W0 z-VpnRf2aT^5%X&31ZEa{>wGU=G6rO{?~y@BD2K*_jFeCbz^>Qe1S4ZW6CAcY$y^+C zA;@UkGv;c^@D9g=98vJNZMYWRW;nfjrW9K@11#p^{U89=YDIkj$M!Z{RMoqHh++N? zJiD!{V&OQsibFB=izHwR5`FnN$U>zQvFpNX5E6*~K63|Vn!Xh68KWAX*Kv6OiKU6J zQ9<cL2J>ivZabbS7Lg^8TN1|4(Y;tZ=<Fv{1u`QcLQj*RHv=^&#G``F0XC8a6RW^w z!s7CKLxwmAL<!dmr^vTg0H@p#4|E97coE~s;&46UP5J$M8ydP#Q<;;a@*01c&wNpb zEn&HIDj?jN4v$LQNV1qh?LkcY|5lqyka-rlN}82jtBp>~Faaf@5A}r^Mm`}WNx^4{ z{B5QK3&pc49l>Hn@dBD`%HAz|IIMd2L6qCGHIjOPPmb3O7S7*<9~+#T1kf}ra|Htb zqDmwEKAhWZ!eU%fHUAwpx>P54>Gh)e*&x`PJO?(JUXORGSFy?Wn&O&`7m>tBP~O(U z`Fw|Xed&+=-<!`sy%la!=)S*h<E%Sp{1?e(PXMc>V6*-P{yj1>v?5X#YQatufuKtO z*#DC_NS=-?r#~5b3Do^zH2C=vF$%x+%oM%e2$8if3;qmey}Mr+B;=_Mo$(&{yGW?} z-qyeM3%$p<Ix#O|EIg+5uo()b6ro0ZXbkm5k!+=s=2iU=GB4yhYW_w9&cFxu+6GrE z76c`q_8?%L6xE0D8;UVdg<!wJI6C+}-e!!BTVSjU<_6<_jv<ob$PZ1A1@H@i!jD+_ zPBOe#pZ|_mP>y(Aw|WSOmx-Kc5U6GzFf+?Hrz-9^2{>QxwuL<kw{VBBvV<nt{bcng z?9qodC4g(h%&dPE;Eie-h%U%5J%T4lnZP)LrU@cwq>n~T9$2<0c>$0RIpSKcdPVc} zRg?eR?3MR~mB<jX@;%6PpIIM@lcod*n~>)fZhy0?dPa#|+cQZ(B1jwV1%=6XsnMz( zPKax|baS{N{*kr9;a>|4l9;Z^dVPKS_Vlf>e)_@6MA!Q{NsOYZ`gyH_qWg5Ov5_1R z^Jjs^JOSQ;ZjHtzO_pB{lhGI|VilV9WG6cFVjG1mmq)DQ((Qy-Yl!3I=H_(;8GKux zz}<}8xuWX%0}qSs=F)5dl}wvc*wbz2d)Ctp&C)Kv+xc%(`Q3kqNcoaTpAO1{y7SUl z1zEB-`hiEX_48sK;J4VZgq8E0p>-z<_YjUDBYihSOq(qLw+wsW5%5`B8ct5e^Y4e9 zPk+Us*#scQlGqP{S(W|OtQRsvGCxh9G>A@f(7B<DC}vfptD)zqITX$E`a7WE9l|$Z z2kjy26r}P5A(7gRJ)7Q;A)HtEXFr`tyOaW)8GCiutG>BhM9x5$n4Wtt2g4By1wL?N z<VP4x7LdHPYPKS_WCPeVe4=8>dRvF<Dq7~+_2)hyrONT@b2-3)%OV2DBemsB_$K|> zx!8@Y_r7%JyF#rmggJ~PX&<{h)+c_(6DDwVF(k5;GYtLn#m<&#uE}N1lynP3cQEm< z_F*T&DBD(HJNcm$<#OW7c{;3-(9V=aKKM%&ZB|r9KcjRF#eVZ@NZyiPg<6e-Oc<1& zuBI8fayW+v{pJ?LIlqC<Q~wf`G!LlX!r4JJqf1R<czD$}8EWshw`U<)srj#3{_=V6 zm~|^_e}0-NOTyHMm7BL|Ya_3nA?;aSa{&tvS2r;R?TTE&h3@9Lg`u@#Vv~@3OXlEu z_l}2?la<#iS;=~+DG)5f$0k;mi`R3$4UuXDU$YB4Qiis~BlB5znGZi);uaNI3?-6_ z`CiSTdfxP?zMFoPS}s>NGP08jQ(nRR_WN7fB|cneXz0$)leV^J7VLu;6#0WyeT7sO zk)9b+DK?v8H9~`AHo2$<xL`=b#|TR#lwv@n<pUd8!NnwT&oP*<fmey$p>-?Lf(e%T zumDFP+ju)?zrN~ZzYv@iYGlrW_I6)vu|r*!UO{UpOf`8j(v04NfFT4ipgu#}KBs!c zM$42<qCn5bScUG%=C(?Hf3YHIN%`CbJ%Q-W9a+pBcy*4SwO_A@7R4s9sbs3;%eR0@ zfUs?I9o6K0d42w86da!JX!u;+P517HpC-)Mzq@yS^o!?-YpP*C0@g&$r3EE)3sRB# zji@v*wvk1-lcz@FN)*eU9}5Fbck9f?D^bb0n5607+nj)4hUs$M2cSM=A~*}eH1xC6 zZ;hqGvc0(q6X+ikO~1PzOvm*{uB~)GX-A`p_%;U2F>L6Em+|bkHj}d~&i`<v2!*k< zz_Siw-P)hrNusff)*>KfarL|HKzl1JdRl@>)FA9=a$DZ&;7r}V#{kUOh)liZ)6{-A z92AJ<i8cYC$;+|H{k!tflO?`ot`9Ajf^a~;vw8VigODKB!vEZpDjEC}BtDhj-?Dto zf-uWKN4LtS#a#uT=}ZKzKf!C7p8E%tnPUuZ_Kqr0Jiz5cvK=h_=kYW38TxUXO(=LF zY!wx@DQtHJZXvcVpNm0DdlHM*hJ^IQgt7bGMpvp?>x*p(91rP@kTCoKaV`ro+sIv$ zk<Ve912Bu+vlP{HRI3P$hS`m1I!>;eDctV8rTH~*O;R~*6u=I;<b*8=f2%WuzdEQB zT|$q9U`2&QMbwtuzH$mbe0O)2ZK#4B1k#-SwLcfebTN-z!ZO;}td29#73GK_ieM%Y ziFD7qE;BJsy(#2x*L39DdbRs;t<{mFWtr%$y(?g<Li3w_i|nkqm{@Wt`K8C#uP6W+ z;JzX1XjQT`${hB`_$SWPVI^Oib?$t}$_Fa#8FonmYCZJ_fv4wdRRz!lbRf4W_#1df zTDll1y!lVFF>YtYv+C35(!``E8lFAdWRV5IiRJR<xbv78()45(`;la`Rx~+BKyCiB z2Ooa=^7lvD24a}d@IU1@j*?M|CRQY$Bcb~kFrB#3#IU73TXxOEYNC%JWF!5mBNGD_ z!Dc^u@8)w!QF$TtlpsPJbJ$CRMy}x<$j?Kb(*cKsvLUNJQx9n!SuCr80~tfo2Pbe1 zni684E)6kvsUFtdNHt1nI?^FPX$Og4h=xhwjzni=5fM#N8Dm}<TuQNIuOGLP@ZLtl zhWhf|HiZ$D#x1N5I_nT-n3L@^w$E6vTrhJo&^9-xTt09Nb2c$uJYiRzEd9d&<9k_+ zVy+Cgn8Yxaay;P&9hv>WAx)aaye$RGxmnXzR;7b=apiaeKe3E<B<ZRBrK3Y<(Xj4w z?QBc);XFy0j>YsL?9COpU^Ap(Q{1<mg_endMS)<Mm4VBcBAqHpC1Y6nS09@P&GH)u zE<(EdcKBsYgU*WA-<y3at4`_THF`m&e-u^Pw~RY2C#Mfw?NeV%qT@dt3($HxZhdMg zxm9Z|1=EBkViMa6o`u3+CsEwi=Sgb(rjv?VU+#XA*1SBav6~)nsLJ}$v-cZ&M&FK8 z&K%!}RNU_Ns)Y_PCP279=HsR^-D|X}EG`D8;pyL>NyNkycEvaABHN!!!nKDOK3J(4 zI=oMfMjbA#MTS<##*&uSVF)!h58w^KNZ!%-9n!e9DpE~_@<xs&$dZ|C$Gw+KS8ns| z0m+4?e3)^i^Xq~^r9`7kQ|6{Ze>5#Y36bNbP>3UUC}mDUxp^zDI+F+wn~BeEwFCnJ zK>|E?Xuxlm_&)y!09HdOyt_JY)Q}ams27NqXbO0A17%}kwYn7vvAb#}@z+I5IS4i% zZZ~*e=G?`Q)Z*SuNlNE94rv#P=#&Ca)d{IPn|sc6oDIEh+WsILK4chsfm@hPIXHg# zDP};qDjMENX6YgOq+T|9FEM)Lp<SHCQt!GO@HgVOf-8wWdQjC}S3XFywa0Azlcz?Y zZBP1WJP`V*N!ok_<-%O)XQU`jZ(wcFA6+m_ape?<7ZZqw{DS{$M&()vA77S_IT`Pu ztAUeG=&5@_$D7m%2`hQe&d7*Y@+h9<mc_vhTf=5gN#_1+9Z#c7yKecr@Kw|itt6E{ z+}1!r)~nlAT{fMf=u(44j@+(U*xezw<H7XGrUhLs8tZcO&2kKJ3$A|@A$t7y4+d&_ z*%?!>XnXDLg;;&7QHs_1()X~hfBtNJnL|KCyg7JxJkMNiBBrktuj>|_YpewY<I~v> zKh4N2RHE?(pSA|oSw{Rl>`s8Xrzdh5a3oR+zH!$LEC^wdRKx1oxY$`IkVIUuzJMt} z#zG>9X@*AvUrMNjX-aQ|KgKU<DMt^PFRv@07b=3AK`R$7tX~d^tr+?SIVDtF>xHe0 zc3r%G6mGV<IyQ&HKu2jE1Nw7VuS%pkK#c?Yc8N=`0#yYlh4sOE1sc`QI+IiI+gN52 z=KAKGSvZ2zh4uAn9I@STCN4rkgYmEg^&IseE;<jYsjobrF9JX&7EU_0m><EfA=@8# zvLvIaYYm$EV+<Jo?k=~p2(ab`8=q)HQ_2yMhhzLMN7duja+}!@KwdMc_tqCI#m?CH zr_4zI*X#X9s6WK5UIWsMS0J-|Hz+;OLfUy*%stxTt*h?OTu!=JkLC|+8%>>v(q6y= zf6c05x=?ekax}<M@Xm80F>~~1aFO}EGwTpL=5Qh~{s8zL@Sb>aU*9}2@f1#u1^lnp z)=>af!^eM0-ZMY_bQ^<<Aq#Hd3TI|}NBKx$b=NnPKB92O$cyNgfJsa}?ezBAJ(dy3 zTJEu8=i~DmU$=|T?)O+>lRz$t>16Dur<3yBSDDurlf8}3pC^&9JgH5jzumq@ZlAv% zRfrM1?F7TWF*zRpTBBwF`_<N~^*Ddq>pa&Ido5Q`UDVO`jZDHtk}XSoH$9DI+Rg!j zhtE#*l=KEndc>*dtm>)m>@`x@5vQT|<jdoye=tzp#oC4AJ)EpA1-U$twQ9@m#sM7& z0}d;IE!_xussWM@$30*Y1z;S$#MlAp-KD2BbDQ4e2gnj)roR7FHmeckZ5NxSZ8WKz zDAUnK+xNc&L7(CX+4mOXG`#xT$7#Y=9^K7>K4lybG#66wGunPYc#LJxam$axK-`vM zNGR)dQm*g4M3z~TNP8KMU}Kj`y*w8k$`;p<oBCx0umk)na2gsK#?|4MLW!;ijU>%x z3S}q*E}*4>v9iT2xAnL<^kj@Y-h^`uS3cKnza%vBo<5qJ1ni9J;8(**#SstvPnk=P z`-8^#{EBRFummHKw%es~($hoeWp`|f!yoz(;iF}eC4iPQJZ`M1pc?v@Ij{5eQv~ur zH>7hKkHqFbJ^BsyGB2+v37j}t2Xg7HTbYq){Eka2+zc_=RdYh#_8x+;l5f`5KXU<Y z5L*BAba-R=*u&@hrIYcx+h*(MOzV3m<xZ&*53>;Od4KdO-WDtH%v6{LHQCi*FYF^s zT4pAz-UV0BP~M+7wy%K`Jc_BQ6SR>YUx+-9oXSX|&MJNexIcRKq^*baxQsD5od%kD zZ`{`N@E{a_>-T0D$?K15adb4=kYp{6gxYt8?`5EFoU@MFZ9oOBcgvwDji;mr=1xP4 z7RX}-7X#K4fTBOj8Drh22*&}H1vNkT)t6|8Dv!0Tdw}#d$EgDuo!5Ci{szL*f`#T% zkLv>r{rN>BFojmEX`m-U;ZgPW3lOkY;6J*jE70{=Wm=6O3q=D_c<`m-1)fy2aQS@B z6yvHKABfOv5l3PYP*i$3&5mJM)rQ(HhJArt&o5J`?{R7`hTQtw9oWFd;90mXC^_eM z51e4oPhg%)uA#=s0EI2g$RWXD(s4D)ED!)S@^UJdX;%qG<m^uc`f(&VB5-jM^an2U zmD=L@E5F=D{Od!C$MmX_u|fe+HY}CtkOZgcR_{)H$v47io5gLy)v>&7PLLQ$yrvpB z47}>Ksnufn3uk~qq6x4zap!b34Zb`Nej`mM<#l+t=*L{bg;V!FU50PC3;{d9-QX_% zP((FXX1jl^2gGDVj=hg0QLHIKZ(Wcom@J+>`e?(oXp;W$0Sg#snmKt(k(Cw}@@XU` z5eV}^;<!$HEj%O8m++YDx}FsB>2S;zvZoOjXXDSjViYd>S@m>kYXGW>m?QDX$>DO2 zYoY1qTz_<4EqAixJfmaIRpDIyA1?rA%v|BeFR~WT>XK0jNW1%W;_`WObHj<0m~}Ng z;1?!Cza9(4W**XI3_M$A2d&YPiAd(Z)bH{5;DY$NUjHhdEHddH=7&mR!&c>{;oYkX z6duECY~F9Q)GrWoP%-fEjC2~lNz3(j3hpA2pM;N%x^*e~q-0=i(oi84&}5En?+A(R zKC%W@#1r<7oCX5-YAb@Zb)eoue8hkQUP&&i8_e~s4oFN8f%^nU+}l702C;r<ErRrz z*}&&y`k(!X+90wj)(d4sBx?afZ-HGN3gObj97G{1<KM(grq@Rqx!f)Ivm~j|m_n}# zaof@A?;wiquR7HTL2#yD{)vndVG+%xz+)lZAjD44_lA~Mu8jUnLlHIzbO`dL=?e&! zapgO?%G*_D4;T$amKq6!CWWa))=qID8iJ%5roS`X3F0QXPJ+cSvX2G-=IH6;1(J30 zrF2cnvN#FJ+UvEz%$`eO{v;tfP)mpgu;IQiol%=q1&@%r#(zEE6z+nDK1k=o<Gq^| z(IHh2k&fsyO79>T7WD?b$a8wHag41$Ws{@`N*Mb)@t&PnGo{||=*s?Rfz{|JPP)M_ zECkQ*9N6Qye&~hU+1XtjhV_t|yII@->_?s|)u%7R|JkHC<bWgwB++egvwnW~ho+FF z`ej(U*#6k&vmch{jm?TQ^}5kXi6W@uMMHomiyfR2*ae!vMbH*Juh}aL*EJ+r_G$4Z zXkL)7UI48<exVDcMw#rL-lm9uGqWzq5?Qx#;L+UvaALQNLt?^l(nfX1UsKtkSyom$ zE33cw%xNiZ;dTqmx-Dlf?T3Uh^#Wk!<eGr_O3J3j@o>ydnv+;JJ-zK_ySVPJ6I-S; zhcIsLo>jV4mzcj6+x?gTlNA2x7DJBGaNopU(^*A<UzJj!kK8bjU3z-V--m+R&%GN8 z@2}(dO``K*?V-#>!FwTJ%^Wc9PV2WGp0B4G?DpBwPIMa#0Q?Fg?t54QvlD28z$YA# zI|V7~Ak_P8p^=1Qqd<f}(ZqaCWDrU;v*mR^^wtRu_|OUrJ=&o?a4OPE&YDOc5#j<C zg#5dz!%6R+xQ{ecga+^}ci)0?e;oaGZ9s%Yjb#0Rgo=dhlU-hb58eT@Av@tgYJkX} zE>fZdPKzONlWCEFgsdvOWC^inza}dsW<N0Aj+N+%8gR+pVCj$z&Xx7BbSOlBbpmvh zG1yd$b*x!v(XkXQFV-MHUa1)R8K2-?Tv=EY3_vlX3p275E1imp({19=I875dU{Hu$ zq}TgNL1+|<+6#@Vf5D%k9ua(C#!WHEXdOA(y*95yu}%62x@TD67R9}YfommX4qv)i zqOK=hhP1Gp{zm$Ymc(JpVmYwG45LU{{c#SghZbr5tf<ur4{gCBXt3<tsvP#y)9ep> zf?%6$rxNP1zwYXX<Jg@)7k_W<v9N3xRt=*ch9@Ru7M#CVEnp{)^_!KYCUfvIFhCuS zBVuASuCLGQCP3$;4P!s+Ssgs&ogPVao`}g3TzZU8tg;948vrEJ#F_Wc2uC0TH<*Cf zF8*@Qf4D$G0+)WYsE|J^P*tXGvAknBIo17@*<q!1P%g>o$}e3WI#`BEz;_dHq}S11 z{32;~V>^)f@q$_S;)+0rmC8^Y1<8j@lg#uYw4Cx`8@8m+H?4kGp+2+sZ1RTSByV$N z(Ajx;$YBCdh@h|F*30M0AQ#d|X>ka}*Kzt2Rt-!!#K*z<6CFgbwxthI8@t`MZ_a?> zp?o3vNCQJ?)FwFXXg{?vdM1EjQWz06klvVQRE*|>D2vk@_{)*wYOu9?JOgpt#y}F> z%-l)Pb9h>fFhWy~OM%W|HJ&}Fp*Uv|vFQrtLfC;7Q1+!{zK;$12;9B+gHe#>$4N2v zyGkf8oMYm`P<`0%?*)qsJ!}^nZ%`7XK{BCvKA^Gm_u_5E)}1*^WJ>c5t$i?_+F}ux z^RY`iyC2EjQmf{NPNiKhhgMgeZ~Tx?A16jxUK9WUz`+bWEaLil+d2A5+h?pla5+b) z&LOL;L5Zc7QY)0&9kSk-tMfh<QV5rJk-#Jl{Nj+PMDW3CwCd;Ro+*N9jr(2oS&HKB z&X8~KzNN0>5MLb)HJ3LYf#14XHf|)~xYCVubCh1G;~l$`o`PfMOuxQPVW423#GCaM zQn$RGwFYxlqWC%}i`Jk!Y^G>Igc@?+3;f1jY4<a9kT6TP&@q*cB?s`NCB^C#{eybj zpE_TW*+2uXO)t^nP%EmACKQI>fal_jO#*sk<yFlzf}g6)RJuV0DA1zp827&s`fEes z3R*^Uw8iN8#Lg!fjs0f+fP+e0ak~RgnofCiM^IHI0<+pN6d#2%-n9$LdrKt$TFp{) z-Yh%o^ZXFyLU<J&E-Jua>o$<;<S<Su`ux2=3bwKes16yFu2MU6Zv6ts{w|2A*wWfN zdp4L;mAy4s$a$UqO&9VsFdK+|Q&i}HIQIKmQt_KvS=yR~df(2+qAn>4QzN0<Qvk}I z>pg+nTdN0~pV;#_Fzdj;>3|`f+<FAUN>BR`1V!$gMVz8;31>PIf;R{eOCmoW{C+Qu z5Ya*N5Ay~i!_nGr587!J)C$ji9nS0ckzB?!r9*&<Amfr?HI|(9Yh-V3j_hS8X*hZU zaI70;bjzkG61e(GNJvwhi1H;x=5QfH#fveid!t^V(Lu$;S0k4tR?`yT&e|JSP&heZ zBhjBaS0b-Z#P=r;7i+cOC6JS+NJ1^CXjNY0s0T5BR7OYMfr4RVlzM>nz`^Om!dn6b z9S}&0ZzF-#F)po4`({gY^9JzWUH*sZ1@sphZ$IiAIA@#WYn;b9;5^ijv$F*)`D<(D zX(%7FxfL-f^OMm2<fl(Z$vHc*$*Tre*0Oar%+%YbGl^{|W2<6Tx@{iN>zlG;9CAzv zxX}XXnEkoshGfvSLqJ#2f&v}QLz4=Mu)<WgZ4-B~M$>4-#(c464x)|Z@>s(m9`rM+ zhtaTv_V$<(iI(;T9qq)DGcOHqkFqkZnl3lQrQYWkwO0Z^;lYB#Cl747%hcMe9(=Lh zgl`Ng_WyAK-K2&Sd<k)xA;R$Xmo>^SV)A_OQg>omoO@gU$}JFpw2t7sTr8`E?y<7` zDe_@BA;CtPI8?A96iRrRi`#@k*vIL7eGw*Dn%h-MJQX;zf9jTQnnZ*jkwx&fjCeRK z`-9dEOie}Ko|yqhEtcP^pL#XZ6ubS5^0W9qrPX$o<A%F0cj~$i;`bdS85f5p#f&~0 zyyLZTz2u~vO`N-Ah8u^UM!si;8vu?-k?7{0gXf5L-yh4gybwUZ5%mT*fu;IrS=ite z`~L@Pcj1OfX`NnL3HUwXl11*MX|cCNJ@NKhX6c}!zcdM`)987JS#)a4rJ1&8%X$EN zI8JjFngV27@&OLFLu~dqFT1x%577_p-g*V0w`%I@KZD;W(g1vkko9kK@vfn#XF7M@ zikq`(0SoJ?lfkFI#yPn39%M0_{>#33vI&Ht!1(-|PKDv5g&(;nZ9U<;>CSt3V9|#Y zepEucrGEUVB?)GOi=JpeWwTfMr>G0@gez7&C`Vh~d4Ku2nt3O-3KS{6wACy2R_y4+ zO9ic|v(UFQEH-_ZR!bE3w>ebY?yxQjqZ<j4kKefW1w}<qheXJi@tu4}_~b!y9RF#n z!%{TV?+eM`^X??@VYQy?`lqPRo3WB_uT6!8f7=Uf>JGStR)~b-Fp_AdSINM>V0^sf zp7de*a5xtf(Ak<0{IuOY@fFQCowE9(oc~n{yvd3K|AEsAG_<+Zo!Ui5?fP_6zSG|z zPeP&s2n~U#JL5NFj{G?a&5&e4K!ky08_MwuDdU9NZoAn2dl6C^CdS#IUAM)sPTKy2 ze}(tC=!hp^TGwynrVd>(8in4uCX?qc(`GZ(VL$I1@%J}&iZhe8+BCMTf*?)`ZFoom zlE?9a6x%r-Cp}}#bBzOB(%1i$s=OwKcp&G|IKt=W<z5tue$26Eti;{Ca=$xKMZ^@1 zidW3XkCBavnCuN>Ps=zH_sRi~M7g96S`XI%H8~8%k7gLbRhuE#b&g|WV^`Nx>n{Du zfKNw6a+PkKOIUf}Uqf=@rG9kSk6uW6M?bi?zFN9ow40KBFVbmCPtUt+7p4V}e-0R= zf+7vi(Zb9W$?(K}m){BhBUcCsen+J)DF+Pki<qwGwBU|HJ4%w0R`_H}k##EUfk#N# z|IU9}Fh3<~=nqYZydb?wKvwECKK1vrm9nLuMn&#v^8())4QCs_Z3r-Xr^>*8T~H`^ zugn>ex_z-F_3Ibo{n|Z;J@5mZ!+VX~jCzAL(hYj++aWXXHYJ)JmY6Vpf#S0f<$vV$ zR46C!LgXCjqJS;AT+1<_AthCa_~e3JJ^TO4#YACnCfw5NYPAlH$R0B=Ve7~Y7g>Ye zEPabOV-*0kSSr2PjO|z7#R>*wxY*bQ9-cpCB_esLxY{OlJ$ocFmc`_><Lb9dd80+? z9e+W+)+frA&>+snXus~}zUoM#ml{8}1Mg#k`vKgk#@%XzHm5lOrO&P+UfKuif#x0_ z8?~PvUvCSkU&_^g)|IC5IM^a_7$OzIOUjKF0=}f;nMlROXm*aL4;?YF8sSljH*NZv z9cr-u72yCo0M5(IW`)C_FF(R8Fipq`QV%^Ub931h6<UAzirVt8V3${osi+168ndO4 zkRV{FW<9CpalYO%s9#aLm$|SJ*Sy~(5rG()Mb4>0K*u9#Q_xlR9NL3#na}UaMgQ39 znA6^Pj3O1ey4^9^UB3TYL!|F=l<x1+p^Ug7o4r&&ydXVQ2+RS0BF!2jG+bdJj-*r( zz}{+Tgv0GpWB*qR0v5tL@Pj?U7D1V3oBGKztyqc?^LE+@39|t2ec;JcRt*N+IiaO- zgk`jf{@<pVfdTA7tu{?nGyc1Dgo&>Ufs{P2eGLM(WF#)4@a>yJ{D%=U#3ZY7RJ1}h zm6hXTLKb%^(di8hu&JrT(U9-=UhoFxDL8K6n_W1!a1|*jnZWMhTz3&<=!8>dq~<3B zPn3e$ZlOnm$I+7%ivL}~$JogX-!%&>T)WT6X~MN=>6|;>R}UOaX=vRj$_{Y@$072} z3R*L`8$WHG1tDMGF;Bt%S|gHYsQ~m@8dc5Ka#zm|rm~%hApatcY+uwo2v~krMOY8k zyYXGuvrfQA9Nbg_79T$9z7}@RU&2vI0ueJ?MO%A~vbfW3y&@4f8J)xdBq|<oez#oc z1ya~wWTLVKY5zwQ7C->ah$D#pcfmUEw{kKmOi`2U`;#n!Ig(nPMM}|UdzHyWzwW5n zlFpCblqB!Pf(K`cP@9)^hz`M8hzLTfAgGmN1I&P>t{=UIO1aZu(^|ToX=QT)!R}BT z8msRC$d7**HQAnSW}P+udPqaIOgw**k&rC5oqOy*U1n2Y$qw6iTnftYUz5qJKznpK z{gpR`hFR;xoVTAhw6ZN1W*U8N!&wCC#{Xl)?t&!$Wx8U4*X`;@GZYRzu$3O1Xl04b z%~=8_-$gt;cqX$Y$({ArfRq*xv$eHmWTYQrYm`-v+Qz}+?9UFNKWWpmpp%i<uK!`u zdC!WP*Bi?=@)ctIv48!CXwYvc#jk>zfB|m{2(&+2A~c&cwHlYZl#l>p!TwEM<tTP; z@muD!DF+@td3r}XE#*5?V$j7!R&+G~>unm|;_|1f;X<zesw5wvU^Hkwjy#2*P%_QP zbXJ9YXRDKs;EH=?F;4oUOlECK40%aBPcYyB)$#AyZ<V#8bE^(|-Je?aEpN9=Tj)CM znJuF9>ZfB;iN4RWGYrNO0Q$|?jI|@I{%ITQyN-{$@_Zl=h|uW6%ILwXZ$*1nyoWIN zb#4UkzP2k#(ZKSvCa~2~$Ku#`mVx$=24x7o`=49?4F8{7-+>B^W5|sPS}t%R-r2r4 zX3}I9ho&;ea5L?+W;mK(LjoTk9nA2dpz6|KsxM5+`|e#pg|&F>y(3`pD74%HK`%IY zl$pyHtsCUqS`9clX8SH*e>Z!24Se1lE*rxuB%UiSj!%lNm!;Ro9*<(jC-@WxKJAb7 z*J){Og-So_LfUoyv-G22CQlChf!a+01%BF<Liay{?*BaM7c_9iS$?`!E@nuizJW^5 zuZ+FuoLQE;4U$f7mJIhr$oH_?ThU)~I-46fogXfu3RN)BYvXG28dd8KkTk%<;@GyY z*icb8_(Th~E>R6cCKOG7@+FWncBP**0Z4w-;+YheofIzv$Ng$T`A86Zm;RbewAGL} zrWPy6;Ju7OZt4soYOXJaM^W^MD1UEIsmA?TKTzD_#TG#Hu8qM#2xt`moc2Fa0j<<j zgQw98o0Ym7*ft37o%4`+5i7nVlKVgH{z{6OXz(oYG3bnq4Fq1TRa!a`8D+FBsvY|1 z)mU^55*jg9(?;uzBQPdk0GV2B#>N9rHJJcE<7r-x^i3RJx23MKo}PIUh2}}epehN? zB2FRCV@%_3b=;Y_EDn25Qp4I}8X~}}1QI7MFE6d7ML#=MT+GqlzPy6jLjON^9SJ&E zRy;O5QB?fdw&<cZcEg=fxCX{kRu?ZDm@HOOzPMeTmTb(W1#f~=0pEwK(vg80?!L)e zu6bQ-Q_gSl@s@oDv<SrLqe@A<VSaYkUe7$;;a`)(N4_{u9(sMGY|?eCKG_J?F)hFo zqjN~xD4Af0PgLCES@GS8h<=ojk-PEJRe!UNDpimaHw5-11??&!x*H{xj8OwPSDupf z|Fge)BFVc}Su>_K_cJh5;$Xk=pBOp^VZJ<V2;0xOy9qp2U8`TVG|J9qWj|j1aL=*) z<oldoF*B?QPx7f@I_AqqE60JcaV)`QC~`=y!dgU{7|h_ZYFHm$fBv_0VfXeQalG@l z$KDCbr_YV*>e$#QPI(V|dr$*I=5i9yR<s_>4<E7)kV2-_D+GN9-_tApQIzwr0zCEq zSGw*3j5=sN`Yg8}fsOM^y71wm-H4h~tf^!l<Z@>N1M}R>FUCFq0Xj?~80domHoyzV zEi=QieQL~VE7*K;6sDX$E*=G1#u>7RD7x$p5P+vPDz7<k`wA0g347kzwIN3Dwe-7L zLb5NqoPpa~xs>&#skCExdf85B?;A(E>TyR0#*Q*d!H(#v9ql63bkVR&5L8|PmA<58 z^ykRJ|Lgr=gPlzMJ(VGe0Ny#7KSroj#wMXTWuHN~YyaW$oR&5^_fQ}2u>uzxn}PL( zhx-~ny^<jE(?<iFksKQR{a%7l>K*%LOBRn%K_QV|dhCy*fXK#ixNIMt5HrxPw7a5i zM7T^b{Fi*0VzjgaSVr2(p}M+G?AwC!z-tAwB-jxN4l!zA$ZH=nGmGebsyY-<+-tr6 z-xUd}{daY<%#bDd+G(NcV%@7f{dehk)EHj}VU2Z*WJNaG&Tp;jG)ktZ)J%c}MP4d~ zh76?#Re%!|pcsl3;-4;zP)TD^qV8Bc&Jp;yytC&ZR46(&57CSMCTu!T)f84F>FB_o z3V(kVn4XaE(n=gL<GM0OQ>Ldm`#1v_D>>HBinB=;*i2#DdzPW)02>l@`F|4=z-|97 zFD$`BDS3K$;Lc7QvIEo@+rc?ZnoCC=5eFlHuvfrsH&sHy^1?yr_xd6ZfgtdBn|$hk zm1xBEHp2F`w^a)nloJ;u5RB6L0VY|0Pya!NlDGEPOd3Bz;LUW}m$x79J}kXQ<)>}a zU??Ob9SJ^TVk_kkbozQ432gW9Nmb9!N_+_l{a7%y5(OgEZlpcV)E*DEYX7J5bdaX9 z1>}6Zyq-2q+(%bTj$yDg2z;{iCmmeX&1JCnva)xeb8ThiQXNGQCmDE3$VoyTkbQb6 zxf-q>)L&7!Y#*%f$y+DROZemgyQ0Q)rG`ncTHaI65kx|}_IH7+EUn%Ftr9sT%c!*0 zv0;6xT>V~JMj-v&nBUt^SA0&th9GL{5%s=5*ZS&arUZfrVP;QvNe{hphN8UaKzd37 zI2^f+1daR~sY~lue$FK^qgr04)po$f`}gnP08AE#-lTe~Yhu#lbGiNUbbo)m$j%gN z-1acH8(X5}>3Iu`cYoMS<<At!MMW;#e{uBk*zo6=2({3exCQ?7Q!@iU|3hIM8y6~u zHjs)S1zwfaf#Jf?AX+m!5uv_fQ&<jAb_uNR=5sqOD*UUOb>Y<J)MqGf2+Q<<03#!F z%*DO``Gc~6K*zUWot7*bzGSE|1q>Fxo#_H+y9I9rm*aY<Sjfc}XTuFg2NPxyFmIW^ zx7;>^!Qref@JdVhk|?y-`t!9bP{r3bQDbQ<{2;Nx4oy;A3vTyn^Wj0oj&EnXZ`Up6 zl>%4WlRK}ZtPGIL`2k?0b*(x+CaARZJyZnX)E=p;EF)O}3&LmCIROlQ0XKfYMq5#? zMvCtL{jinRii%@^v2YTZz!l&iYX3o{Tgns3ry~Z-1r9pqHH3&zRLx1CJ*J|NOWEc5 zC*#`h;r@-$n@S^|-rku^!z+b4<FuM3Hb_^UtC7(H2v(Js2VYB@Sf^SoBlB3<GaAX` z=q1uF3a8>r#7L9~AG2@d;No`?e--rRedx4kV+!n|@{#e;%V=6rVTK@0SL8v4kVH(? z=5OT--A`)0qVE_#GQC@QwnWKQ=ccDe%;4hx^h1@-rlSaGE?i;}0s6}0>fpE6?b{<| z5q)1_M>u+FYH9%iV!`{9C0tp-^<v>)N5e=SyUrPSNZ5_j&GkRCYj|aX&?{ojTb*OO zWSVJbL7>L(Qs*&=6h23DWh0)fjF$35*w|`J%-mdiC8eUdX&-ziNN%k%;V~)KW3b@9 zYZ7kev_gRz!JvDS+Gtev*R2A^Vt^30`}C2=8oRuDu1X8~9bVuViy*bO+n&HdUHwJ0 ze76Q)c!&)a2YNwj+OE&4_|(DXa1j9>UclSSJ=7*3SwzPyi@Dw@p%R$~yc?FPblIn| zfEa@RhpBJy%WVDL&OO;plQnVTWY=UHlWiN5Il0NUZQHhM!epCM@1FB{&-woTf!h0i z_Fj9fYh6gpbUKe29^EzP=Ft6qw*aIE3<4B%<gmXF0wMs+<v=b_h#{US3UXwN5fEio zpIM--;M+_S@)iOaswX5Q_F70-UMS!lI?kudf9HVMuck}%69D@dvzbhBKMk8J-T%7{ z0qi%}-^gkKLgM1m4%h15NC_3hzTzDRld($)Sb@ULfuX~Iq(8o(fKuczwv`x)Cqw{f zSXfeJsViHZ$q)#pCSW6_L64{qWTFw5nrua5=Jja!s11N<(U}>fkkkvzf<WL40IDD= z=jR9A)#X~qYpVA~nKKQELSK_2d^5l3E?8bB*&TZ4_xuG;C}!X3qwPwW@m!)_SsUru zgQbKRJPncam5LwaCod!;RFwk-0gjFsXyEYz3j&W5IAlfb#%#g+U<K%YMpPM!P9A&; zVu#8)8a<oZmO4FelUG(!Y&+de@0;BpXdf`KqvW!IsfHMe&Gzl3*4UL)@%nhd&*yD! zY@+yPj>56UL$CMBVy7Xb4=x-9+=n(mKePb7OC%^16tkovtd3d!8=H%93JxZWPYLxK zdI2j0BRD++5qfWVHRF^On|wJ;g_{51KhWvluX-(nA8%1JOZw!<00Nr_u67RzLv3>A zA^~fG?gYD=@`Iyq5TNaH?S6o7shmS4Rg|EH`u43;qxJSU<x7>V75@EkSteO@Jx9i~ zRb&nsM1|94a!J#h)dbPo?!}SMqG%M4`n)f_Az8IEEftJIeH4=7j_b>_lg@?S!}D`% zKeU3J5HS*JaVahl2!tr;+<JZO-9(T-{w2lfnPflCJljT6<q>c$((!)UW3^m7U2Q09 zZpI;Di~{t4f9}^9auu_@uHl#kDzw`gOG?av<OXp$$9V;@iDV`+gWb>s`%m3ykU5HL zYik-0!0tUPXqn?--7yG~q!C&1!0WOHcCE=)%y$7W=?uf56mrzZi-kx&0{qrnJ?@SH zCKajo5TFu|xj9KTjEcqSg*5Nwd%gVHq2n{jc>od%<0gy6?H&6oH@wtY0o=spIQKAr z6ZfsBcqC7jYt#YB(N4lRXpINPEMI?{EH0bW0p7Jl4#Ons;g{oDz{c#moZJ*3Sa-eb z+0i8cmg8=~1(DnUFB=^-wSI394GGE&pw7}zQ!}km2O?yV_&nSMmuU$odQHAh3&H|9 zt#I3`28woveTthdYt|wnBJF;{%IvnAuSs36&>hvB8M2fo^uhvMHeQF9Lb|#{zlm>| zU7CMgRF#(Y@^N1EV~mZC_Tn`)H0awJhE!=bA&0X9!sp}>z4vVVO?Bk}C+$$9^rR$L z!2U4QW*eOw>ct39oB6S54Swe#^T(jaW(Cq-7%?-ym`-Y#W_+?DwxmZ_=oP?0&I1xM z2{JM=md7ve{Y$%^CTuUwKw?GAB&d*n^%PF{Fl!?NFMLaHaM$zaDPQH)+O1T3TH!n` zl<7Yrf+`%$z2Ax>@v=l_Efd7ZuBg9u@2z$^9jVL}YUj*Nc>Hmm$`vtL3w?a=l1OR& zfqa^*&UzL-RMm1fA)kcSwTPLJ@uxTDQ$+)-vZ|;UlgeUio#WKQdm^24X@sB<9C!sP zpomXhTBF3DI68f3>b=0PQj?o$=3oZIcJ!0=ueI)50lVibrdi)N-4CujFj4h!L)2q% z+tE$Qy*2?s6R_LDh_vU~Zz9Demt1bPC!74H)%ydBR{f(9QWaF1BOtUQW2UWIVY>u! zoFA&%uO;=RHo;&G$K&6pD~KN3zg!B!A75WPY%&{wQwrqo_Ryt+ai)>ki=si-O3E8% z&x%IifRFbGy1C9BYr^s+PsV+ciV@Hz2mc0ehR)h&&s+lD{qf1QJx*(egwv6inbP&_ zi4V@)!KpiN=DoDmcWC^G_3EK%5a8k6szRYRX=NxB)ztxDJ<EVwjNIuB&=(l)WZwi@ zML8)8#xSk>@`weH5hJi~YAb`u*#oH}aITNhkXcQe9wz{m1@;%ERkSJ8Jo<Iejih`c zu};lFjoc0ew3wkZW$Fn=ro9yjAS!iRANoSdp~a*RUf0)LQ1I2jW;iw6DV~dqYdOx3 zuJyRItUH!TCXidV7%u_NFBFu|ngbd6&=`3Y?8V^fh4wo=itqQEtZ5T05{%5B0ntdV z!fHFjoeJV5XJM)mW&~8<rG;8!9~|!c@tM46pZD*&3%4{IMjf(U2RtdzjuC9oFQIYf zt^~jSgyIo`&#cOSXMxhoS5i$p*Dacfm%H&ew_VuSnBC-i)TEqnrnm$7nSlafr>Pl; z5VJ8rf&PLWjr`WRB`G`4dt*rfW4`Esw+8zuccE-XtNC2v6yFOX1(4-<gV@^%#Dv9a ziL|G{rWrD)=w)61%zA|ys#YmeWfJ-gJTgHI2)+83gJ=lopR@pGPc`%-5Ew8+=m4a7 z)0^->1Q1hTFTw+BQ3Bb3&>s6`{g8<)exiG7K#j~hKWL5`jLxX9-fWj69|9!GVB7BZ z>=U2E#f#xD1_uW-bUs2f!fgq9-<9e1h{9pR_tN(=hoS?K59O6W6mHg^Q`6VP{*PDp zGw>Xxee)pGeSj*0Igar7VVaa9GQh6oe$^6)&x9k%f-qVx?SV1`h+aV4!k{0%c}~xS zUw*ItogXY=j6o8b%tzJeFFfO4t3P3XR%i5nb<-nk<84dh!{}LrwhT}Sc80tCK!bw4 z1=LBQ2|1r<C7mkF(y;Bp1-&3W#>bam?}Xg=CE0p{0MCi1nr^voIfO9}()j5xFZx=1 zo0wgY*@K7h^lXg2&8jyLZtL?Uhu)eV|LZo2j~l>W61o1_67k|jL8i5+Wt7w30C5BD zaIkC`J+K)pxC2P)AL(dNA?(qHeKCf;UUaZKo`^DZ@1bBGO*%T4S{CYRs%jl-NAgR+ zMI=V*pKdM&rVxL(MumT79hQf>Y_r%MJZNoWm&x>!nETOttqt3F_R^JY+%zT@JSmyF z>b?iP@Iq8+8Mm1F4Gwj=k&eSqG23!QR#1Bp+nS8Py>F)-Q^oFDTRWf^xVIY{Ex{-9 zAE?ua;P~wslT5t4ZFycNmoB5}S+;<rGEVlkkJ~n354svGTSU(g!etjw7k<HgF>KvF z3@2v=EQl!)!MpRf?1q1$0_vtAGrL&{%0D|Fy7Z>A$<{yFaX~Nz_W5W5)<009>)qBN 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 S)MZK-fWXt$&t;ucLK6U4;G|su literal 0 HcmV?d00001 diff --git a/docs/DonneeEntree.plantuml b/docs/DonneeEntree.plantuml new file mode 100644 index 00000000..3c99b1cc --- /dev/null +++ b/docs/DonneeEntree.plantuml @@ -0,0 +1,129 @@ +@startuml +'les clés primaire sont désignées avec un point +'les références entre tables sont désignées avec un carré + +class en_DonneesEntree { +'Nom de l'organisation liée aux données +String nomOrganisation +'Le nom du lot permettant d’agréger les données provenant de différentes sources et d'en faire un suivi temporel +LocalDate nomLot +'La date du lot permet d’agréger les données provenant de différentes sources et d'en faire un suivi temporel +LocalDate dateLot +' le paramètre suivant est facultatif. Il liste les noms des critères souhaités dans le calcul, en cohérence avec la table ref_Critere . Son absence déclenchera le caclul sur l'ensemble des critères présents dans la table de référence ref_Critere +List<String> criteresDemandees +' le paramètre suivant est facultatif. Il liste les codes des étapes souhaitées dans le calcul, en cohérence avec la table ref_EtapeACV. Son absence déclenchera le caclul sur l'ensemble des étapes présentes dans la table de référence ref_EtapeACV. +List<String> etapesDemandees +List<en_EqP> equipementsPhysiques +List<en_DC> dataCenters +List<en_Messagerie> messageries +List<en_Entite> entites +} + +class en_Entite { +String nomOrganisation +LocalDate dateLot +String nomEntite +Integer nbCollaborateurs +String responsableEntite +String responsableNumeriqueDurable +} + +'Equipement Physique +class en_EqP { +* String nomEquipementPhysique +'le type fait référence au ref_typeEquipement présent dans les références +String type +' Pour type == Serveur, Référence au data center = en_DC.nomCourtDataCenter +- String nomCourtDatacenter +String modele +String statut +String paysDUtilisation +String utilisateur +Date dateAchat +Data dateRetrait +Double consoElecAnnuelle +Float nbJourUtiliseAn +Float goTelecharge +Integer quantite +String modeUtilisation +Double tauxUtilisation +'Nom de l'entité responsable de l'équipement physique +String nomEntite +String nomSourceDonnee +List<en_EqV> machinesVirtuelles +} + + +'Equipement Virtuel +class en_EqV { +* String nomEquipementVirtuel +'le type d'équipement virtuel contient "calcul", "stockage", "null" +String typeEqV +'nom de l'équipement physique sous jacent +- String nomEquipementPhysique +Integer vCPU +'en To +Double capaciteStockage +' la clé de repartition est exprimée comme une fraction +Double cleRepartition +Double consoElecAnnuelle +String cluster +'Nom de l'entité responsable de l'équipement virtuel +String nomEntite +String nomSourceDonnee +List<en_App> applications +} + +'DataCenter +class en_DC { +*String nomCourtDatacenter +String nomLongDatacenter +Double PUE +String localisation +'Nom de l'entité responsable du datacenter +String nomEntite +String nomSourceDonnee +} + +'Application = Une instance d'une application sur une VM +class en_App { +* String nomApplication +'les type d'environnements permettent de distinguer les environnements de développement, de recette, de production.. +* String typeEnvironnement +' Référence à l'équipement virtuel = en_EqV.nomVM +- String nomEquipementVirtuel +'les catégories permettent le classement par domaine applicatif +String domaine +String sousDomaine +'Nom de l'entité associée +' permet de retrouver des données liées à l'entité +String nomEntite +String nomSourceDonnee +} + +'données de la messagerie +class en_Messagerie { +'nombre total des mails émis par les collaborateurs +Double nombreMailEmis +'nombre de mail émis multiplié par le nombre de destinataires +Double nombreMailEmisXDestinataires +'Volume total des mails dans les boites mails des collaborateurs +Double volumeTotalMailEmis +'date au format MMYY +Date MoisAnnee +'Nom de l'entité associée +' Pas utile pour les applications +' mais utile pour retrouver des données liées à l'entité +String nomEntite +String nomSourceDonnee +} + +en_DonneesEntree "1"--- "0-*" en_DC : peut contenir > +en_DonneesEntree "1"--- "0-*" en_EqP : peut contenir > +en_DonneesEntree "1"--- "0-*" en_Messagerie : peut contenir > +en_DonneesEntree "1"--- "0-*" en_Entite : peut contenir > +en_App "1" - "1" en_EqV : est contenu dans > +en_EqV "0-*" - "1" en_EqP : est contenu dans > + + +@enduml diff --git a/docs/Indicateurs.plantuml b/docs/Indicateurs.plantuml new file mode 100644 index 00000000..b64755f8 --- /dev/null +++ b/docs/Indicateurs.plantuml @@ -0,0 +1,97 @@ +@startuml + +'éléments communs à tous les indicateurs, ces attributs sont dans toutes les classes filles +abstract class ind_Impact { +'La date du lot permet d’agréger les données provenant de différentes sources et d'en faire un suivi temporel +LocalDate dateLot +'Date et Heure du calcul, même valeur pour tous les indicateurs créés avec le même lot d'objets d'entrées +LocalDateTime dateCalcul +'Version de l'application ayant réalisé le calcul +String versionCalcul +'Statut de l'indicateur : calculé, en erreur +String statutIndicateur +'Version du référentiel +String versionReferentiel +'Trace du calcul, résultat +String trace +'Nom de l'étape ACV associée à l'indicateur +String etapeACV +'Nom du critère associé à l'indicateur +String critere +String source +String referentielsSources +'Unite du critère +String unite +'Nom de l'organisation - Metadata +String nomOrganisation +'Nom de l'entité pouvant agir sur l'indicateur - Metadata +String nomEntite +} + +'Indicateurs d'empreinte environnemtale d'un équipement physique +class ind_ImpactEquipementPhysique { +' Nom de l'équipement physique présent dans les données d'entrée en_EqP +String nomEquipement +'le type présent dans les données d'entrée (en_EqP.type) +String typeEquipement +'Quantité de l'équipement associé à l'indicateur, uniquement pour les équipements physiques +Integer quantite +'Statut de l'équipement physique correspondant à en_EqP.statut. +String statutEquipementPhysique +'Impact unitaire, le champ est null en cas d'erreur lors du calcul de l'indicateur +Double impactUnitaire +Double consoElecMoyenne +} + +'Indicateurs d'empreinte environnemtale d'un équipement virtuel +class ind_ImpactEquipementVirtuel { +' Nom de la VM +String nomVM +' Nom de l'équipement physique ou de la VM présent dans les données d'entrée en_EqP et en_EqV +String nomEquipement +'Metadata - le cluster n'est pas obligatoire et permet de regrouper plusieurs VM +String cluster +'Impact unitaire, le champ est null en cas d'erreur lors du calcul de l'indicateur +Double impactUnitaire +Double consoElecMoyenne +} + +'Indicateurs d'empreinte environnemtale d'un applicatif +class ind_ImpactApplicatif { +'correspond à une application présente dans donnée d'entrée en_App +String nomApplication +String typeEnvironnement +String domaine +String sousDomaine +Double impactUnitaire +Double consoElecMoyenne +} + + +'Indicateur Impact Réseau +class ind_ImpactReseau { +' Nom de l'équipement physique ou de la VM +*String reference +Double impactUnitaire +} + +'Indicateur d'impact pour la messagerie +'Le champ etapeACV n'est pas utilisé pour cet indicateur +class ind_ImpactMessagerie { +Double impactMensuel +'date au format MMYY +Date MoisAnnee +'Volume total des mails dans les boites mails des collaborateurs +Double volumeTotalMailEmis +'nombre total des mails émis par les collaborateurs +Double nombreMailEmis +} + +ind_Impact <|-- "hérite de" ind_ImpactReseau +ind_Impact <|-- "hérite de" ind_ImpactApplicatif +ind_Impact <|-- "hérite de" ind_ImpactEquipementPhysique +ind_Impact <|-- "hérite de" ind_ImpactEquipementVirtuel +ind_Impact <|-- "hérite de" ind_ImpactMessagerie + + +@enduml diff --git a/docs/MoteurDeCalculG4IT_V1.1.adoc b/docs/MoteurDeCalculG4IT_V1.1.adoc new file mode 100644 index 00000000..629844c1 --- /dev/null +++ b/docs/MoteurDeCalculG4IT_V1.1.adoc @@ -0,0 +1,865 @@ += Spcécification des règles de calcul de G4IT +:toc: +:toclevels: 4 +:sectnums: + +== Historique des modifications +|=== +|type de changement|date|type de modification +|majeur|28/06/2022|Création du document +|majeur|Avril 2023|Ouverture en Open Source +|=== + +[#_documents_de_reference] +== Documents de référence + +|=== +|identifiant|intitulé du document|source +|1|G4IT|Sopra Steria +|2|Référentiel services numériques_v1.0_FR|ADEME|Principes généraux pour +l’affichage environnemental des produits de grande consommation +|3|WeNR Methodology Note|weNR - https://wenr.isit-europe.org/wenr-methodology-note/|méthode de calcul +|4|BOAVIZTA|https://boavizta.org/ +|=== + +== Introduction + +NumEcoEval est une solution permettant, à partir de données de dimensionnement du SI et de données de références, de calculer l’empreinte environnementale d'un système d'information. +Cet outil open source a été initialisé à partir des travaux de Sopra Steria [1], de l’ADEME [2], de l’INR [3], de BOAVIZTA [4] (cf.Documents de référence), avec la contribution d'IJO. +Cet outil vise à déployer massivement le calcul de l'empreinte environnementale d'un système d'information au sein d'une organisation, en connectant NumEcoEval directement aux sources de données standard de celle-ci (notamment ses CMDB). +Le volume de données pouvant être important, une attention particlière a donc été apportée à la performance de l'outil et explique certains choix d'architecture (notamment l'utilisation de l'asynchronisme). + +== Description des principaux concepts + +[#_analyse_de_cycle_de_vie] +=== Analyse de cycle de vie +Notre approche s’inspire des méthodes d'Analyse de Cycle de Vie (ACV), cadrées par les normes ISO 14040 et 14044. Ces normes structurent l’analyse d’impact tout au long du cycle de vie d’un produit ou d'un service.L’application de ces normes à un service numérique est encore jeune mais est à ce jour l’approche la plus poussée et celle retenue par les groupes de travail liés à la règlementation (AGEC). +NumEcoEval a été conçu pour pouvoir intégrer d'autres étapes du cycle de vie des produits selon les besoins des organisations, sous réserve que les donnéées disponilbles et/ou les standards évoluent dans ce sens. + +4 étapes du cycle de vie d'un produit sont retenues actuellement : + +image::cycle_de_vie.jpg[] + +|=== +|Étape du cycle de vie |Traduction anglaise |description +|Fabrication|Build|Phase de fabrication d'un composant de l'extraction des matières premières à la fin de sa phase de fabrication. +|Utilisation|Use|Phase d'utilisation d'un équipement +|Fin de vie|End Of Life (EOL)| Phase de fin de vie et de recyclage d'un équipement +|Distribution|Dist| Phase d'acheminement du produit et des matières +|=== + +[#_criteres_dimpacts_environnementaux] +=== Critères d'impacts environnementaux +Les critères d'impact correspondent aux types d'impact environnementaux suivis par l'outil. +Sont retenus actuellement les critères décris dans la PCR Service Numérique ([1] des <<_documents_de_reference>>) comme obligatoire : + +* Épuisement des ressources naturelles (minérales et métaux) +* Changement climatique +* Acidification +* Émissions de particules fines +* Radiations ionisantes +* Consommation d’énergie primaire (indicateur de flux - complémentaire) + +Le raisonnement multicritère est important car il permet d'éviter les transferts d'impact lors de la planifaction du plan d'action suivant l'acte de mesure de l'empreinte. +Comme pour les étapes ACV, ce moteur de calcul permet nativement l'ajout d'autres critères d'impact environnementaux. + +[#_Organisation] +=== L'Organisation +L'organisation regroupe l'ensemble des tiers utilisant NumEcoEval appartenant à une même structure. Une organisation peut contenir différentes entités. + +.Exemple +Le Ministère de la Transition Écologique et de la Cohésion des Territoires est une organisation qui sollicite NumEcoEval pour calculer l'empreinte environnementale de son SI. + +[#_architecture_fonctionnelle] +== Architecture fonctionnelle de la solution + +image::architecture_fonctionnelle_simplifiee_V1.png[] + +Comme indiqué dans le schéma d'architecture fonctionnel simplifié ci-dessus, il convient de distinguer différents types de données : + +* Les *données brutes* : ce sont les données stockées sans transformation, sous des formes très différentes, dans les outils d'une organisation. Ces données peuvent être statiques (elles ne varient pas ou peu et sont valables à l'échelle d'un an) ou dynamiques (elles varient souvent et doivent être mesurées régulièrement). Elle sont fournies par des sondes, des API... et ne sont pas structurées au format G4IT. +* Les *données d'entrées* : ce sont les données normalisées (format NumEcoEval), capables d'être comprises par G4IT et peuvent être interprétées par l'outil pour faire ses calculs. Ces données sont donc généralement issues de la transformation des données brutes (traduction, redressement, nettoyage...). +* Les *indicateurs* : ce sont les données pouvant être affichées dans les dashboards SuperSet. Elles sont généralement calculées via les règles présentes dans ce document (exemple : impact en CO2 eq d'un équipement) ou sont issues des données brutes (exemple : nombre d'équipements). Ces indicateurs sont de deux types : + + ** Stratégiques : ils permettent au DSI ou aux sponsors des domaines d'avoir l'état de lieux et de fixer les objectifs + ** Opérationnels : ils permettent au responsable de secteur et AMOE de mettre en place les actions sur leur périmètre. + +* Les *références* : ce sont les données normalisées (format NumEcoEval), qui servent de base aux calculs des impacts pour les différentes phases du cycle de vie d'un équipement (cf. <<_analyse_de_cycle_de_vie>>) et pour les différents critères (cf. <<_criteres_dimpacts_environnementaux>>). Ces données peuvent provenir de base de référence externes (NegaOCtet, Base Impact de l'ADEME, base BOAVIZTA...), ou peuvent être calculées par le client (exemple : calcul de la consommation électrique moyenne annuelle d'un équipement). + +== Description des Modules + +=== Bloc Références +[#_donnees_de_reference] +==== Données de référence + +Les données de références correspondent aux données qui ne proviennent pas directement de l'organisation (<<_Organisation>>)souhaitant utiliser NumEcoEval. Ces données peuvent être issue de la littérature scientifique, de bases de données de références externes et reconnues (NegaOctet, Base IMPACT de l'ADEME, bases de données BOAVIZTA...), ou issue de travaux empiriques. Ces données doivent nécesairement être sourcées pour assurer la traçabilité des calculs. + +Le tableau suivant permet de connaitre les données de référence utilisées dans les différentes règles de calcul.Ces données seront précédées du préfixe "ref_" suivi d'une suite de lettre permettant d'identifier la table concernée. + +exemple : +|=== +|Identifiant|Description des données +|ref_Critere|Précise les critères pris en charges par l'outil, leur unité et d'autres informations qui leurs sont associées. +|ref_Etape|Précise les étapes du cycle de vie prises en charge par l'outil. +|ref_Hypothese |Précise les hypothèses nécessaires à certains calculs des indicateurs (par exemple les données par défaut) +|ref_TypeEquipement |Précise les types d'équipements autorisés et leur durée de vie par defaut +|ref_CorrespondanceRefEqP|Donne la correspondance entre les équipements présents dans les données d'entrées et les équipements de références présents dans la table ref_impactEquipement +|ref_ImpactEquipement |Donne pour chaque type équipement référencé, son impact environnemental selon le critère (<<_criteres_dimpacts_environnementaux>>) et le cycle de vie (<<_analyse_de_cycle_de_vie>>) regardé. +|ref_MixElec|Donne pour chaque pays référencé, l'impact environnemental d'un kWh d'électricité produit, selon les différents critères d'impact. +|ref_ImpactReseau|Donne l'impact lié à l'utilisation du réseau selon les différents critères et l'étapes dans le cycle de vie. +|=== + +**Pour chaque type d'objet, des endpoints de CRUD (Create/Read/Update/Delete) sont disponibles** + +le format de ces références est contraint pour garantir la capacité du moteur de calul (cf.<<_moteur_de_calcul>>) à produire les indicateurs. + +link:./References.plantuml[Diagramme de classe des référentiels] + +Toutes ces données sont nécessaires au calcul des règles d'impact. Elles doivent être chargées par un administrateur NumEcoEval avant d'intégrer les données d'entrée. +===== Précisions sur l'import CSV de certains référentiels + +*Référentiels dépendants d'une organisation* : + +Le référentiel est lié à une organisation donnée. L'import se fait avec 2 paramètres : + +* Le nom d'organisation pour laquelle les données sont établies +* Le fichier CSV du référentiel + +Lors de l'import, les données précédentes de l'organisation pour le référentiel sont supprimées. + +_Concerne_: ref_CorrespondanceRefEqP + +*Référentiels généraux* : + +Le référentiel est global à tout le système. L'import se fait uniquement avec le fichier CSV du référentiel. + +Lors de l'import, les données précédentes sont supprimées. + +_Concerne_: ref_Critere, ref_Etape, ref_ImpactEquipement, ref_MixElec, ref_ImpactReseau + +==== Exposition du bloc + +Les données de ce bloc sont exposées via les points suivants : +|=== +|Endpoint API REST|Description|Paramètres|Sortie +|[[GET_Etapes]]GET /referentiels/etapes|Renvoie la liste des étapes de ref_EtapeACV|Aucuns| Liste d'objet ref_EtapeACV +|[[GET_Criteres]]GET /referentiels/criteres|Renvoie la liste des Critères de ref_Critere|Aucuns| Liste d'objet ref_Critere +|[[GET_ImpactsReseaux]]GET /referentiels/impactreseaux?etape=+{etape}+&critere=+{critere}+&reseau=+{refReseau}+|Renvoie l'impact d'un équipement sur le réseau selon les paramètres|Le nom du critère, le code de l'étape de cycle de vie et la référence de l'équipement|Un objet ref_ImpactReseau +|[[GET_Hypotheses]]GET /referentiels/hypotheses?cle=+{cle}+|Renvoie l'objet ref_Hypothese correspondant à la clé|La clé unique de l'hypothèse|Un objet ref_Hypothese +|[[GET_typeEquipement]]GET /referentiels/typeEquipement?type=+{type}+|Renvoie l'objet typeEquipement correspondant au type d'équipement|Le type d'équipement|Un objet ref_TypeEquipement +|[[GET_impactMessagerie]]GET /referentiels/impactMessagerie?critere=+{critere}+|Renvoie l'objet ref_ImpactMessagerie correspondant au critère|La référence d'impact messagerie|Un objet ref_ImpactMessagerie +|=== + +**Pour chaque type d'objet, des endpoints de CRUD (Create/Read/Update/Delete) sont disponibles** + +_Tableau des endpoints CRUD en attente des retours sur Spring Data REST_ +_TODO Ajouter le tableau une fois les endpoints stabilisés_ + +Pour certains référentiels compatibles, un endpoint d'import par fichier CSV est disponible +pour permettre un chargement en masse. +|=== +|Exemple d'endpoint d'import par CSV|Description|Paramètres|Sortie +|POST /referentiels/<objet_ref>/import/csv|Importer plusieurs références. L'import fonctionne en annule-et-remplace (suppression de toutes les données avant import).|Le fichier CSV avec les données à insérer|Rapport du fichier CSV (nombre de lignes totales, nombre de lignes en erreur, nombre de lignes traitées, liste des erreurs par lignes). +|=== + +Ce endpoint est actuellement disponible pour les référentiels suivants : +ref_Critere, ref_EtapeACV, ref_Hypothese, ref_TypeEquipement, ref_ImpactEquipement, +ref_MixElec, ref_ImpactReseau, ref_ImpactMessagerie. + +**Points d'attention ** + +- Par construction, une suppression sur le référentiel ref_EtapeACV supprime les référentiels référençant cette étape : ref_ImpactEquipement, ref_ImpactReseau et ref_ImpactMessagerie. +- Par construction, un import CSV sur le référentiel ref_EtapeACV supprime les référentiels référençant les étapes : ref_ImpactEquipement, ref_ImpactReseau et ref_ImpactMessagerie. +- Par construction, une suppression sur le référentiel ref_Critere supprime les référentiels référençant le critère : ref_ImpactEquipement, ref_ImpactReseau, ref_ImpactMessagerie et ref_MixElec. +- Par construction, un import CSV sur le référentiel ref_Critere supprime les référentiels référençant les critères : ref_ImpactEquipement, ref_ImpactReseau, ref_ImpactMessagerie et ref_MixElec. + +==== Précision sur les hypothèses +Les hypothèses de travail se trouvent dans la table "Hypothèses". +Elles sont uniques pour une clé donnée. +À l'usage dans la suite du document, une hypothèse peut être retrouvée via la formule : +`ref_Hypothese(<clé d'hypothèse>)` + +.Exemple d'hypothèses : +|=== +|clé|valeur|source +|dureeVieParDefaut|1| dans le cas où la durée de vie d'un type d'équipement ne serait pas renseigner on considère une hypothèse majorante d'une mise a rebut de l'équipement au bout de 1 an. +|=== + +[#_api_donnees_dentrees] +=== API pour les données d'entrées + +L'API pour les données d'entrées permet d'importer des données dans le système NumEcoEval via des endpoints REST. + +Toutes les informations reçues sont envoyés simultanément : +- Au bloc Données d'entrées pour historisation des données reçues si le module est activé +- Au bloc Calcul afin de produire les indicateurs issus des règles de calculs décrites dans ce document. + +==== Endpoints de l'API + +Les données de ce bloc sont exposées via les points suivants : +|=== +|Endpoint API REST|Description|Paramètres|Sortie +|POST /entrees/csv|Envoi d'un objet en_DonneesEntree aux blocs Calculs (et enregistre ces Données Entrées si ce bloc "donnée d'entrée) est activé) | Les fichiers CSV correspondant aux différentes classes présentes dans les données d'entrée (équipement physiques, virtuels, datacenter...) +|Rapports par fichier CSV contenant le nombre de lignes en erreur, le nombre d'objets intégrés, la liste des erreurs par lignes). +|=== + +=== Bloc Données d'entrées (optionnel) +Ce bloc est optionnel, il n'est pas nécessaire au calcul des indicateurs mais il permet d'historiser les données de dimensionnement de l'organisation, permettant de réaliser des rejeux de calculs et de connaitre les valeurs utilisé à un instant t. + +[#_donnees_dentrees] +==== Données d'entrées + +Les données d'entrée du sytème se trouvent dans la base "Entree". +Dans la spécification, les objets représentant des données d'entrées sont précédées du préfixe "en_" +et sont classées en 4 catégories : + +* en_EqP: liste des équipements physiques (type, d'équipement) +* en_EqV : liste des équipements virtuels (vCPU, équipement physique sous jacent...) +* en_DC : liste des datacenters (identification, PUE) +* en_App : liste des applications + +Tous ces objets sont groupées dans un objet en_DonneesEntree avant d'être envoyé dans l'outil. +Dans un premier temps, il est possible d'envoyer les objets par groupes. + +le format de ces données d'entrée est contraint pour garantir la capacité du moteur de calul (cf.<<_moteur_de_calcul>>) à produire les indicateurs/ +link:./DonneeEntree.plantuml[Diagramme de classe des entrées] + +[#_bloc_calcul] +=== Bloc Calcul +Ce bloc représente le coeur de métier de la solution NumEcoEval. +Il permet de produire des indicateurs en se basant sur : + +* Les référentiels exposés par le bloc Références (comprenant les critères, les étapes AVC, les hypothèses,... cf. <<_donnees_de_reference>>) +* Les données d'entrées reçues via un objet en_DonneesEntree : cf <<_donnees_dentrees>> + +Le moteur de calcul permet d'intégrer toutes les règles de calcul (<<_regles_de_calcul>>) qui se basent sur les données de références et les données d'entrées. +Il permet de calculer des indicateurs qui pourront ensuite être utilisés unitairement ou associés entre eux pour fournir une vision de l'empreinte environnementale d'un périmètre et aider les utilisateurs à piloter leurs plans d'action. + +cf.<<_moteur_de_calcul>> + +[#_bloc_indicateurs] +=== Bloc Indicateurs +==== Historisation des indicateurs + +Ce module permet d'historiser les indicateurs produits par le bloc de calcul. +Les indicateurs sont stockés dans des tables dédiées. + +Tous les indicateurs sont autoporteurs, c'est-à -dire qu'ils sont compréhensibles et apportent de la valeur unitairement : ils font référence à un seul critère et une seule étape ACV, et la valeur de l'indicateur est toujours couplée avec une unité. + +link:./Indicateurs.plantuml[Diagramme de classes des indicateurs] + +==== Exposition du bloc + +Le bloc Indicateurs expose les points suivants : +|=== +|Méthode d'exposition dans la spécification|Description|Paramètres|Sortie +|[[sauvegarder_impact]]sauvegarder_impact(ind_Impact)|Persiste un objet ind_Impact (ou une des classes filles) en base de données|L'objet ind_Impact à sauvegarder en base de données| Aucunes +|=== + + +[#_cles_de_lecture] +== Clés de lectures du pseudocode + +* Pour faciliter la lecture des règles de calcul, chaque donnée est préfixée selon le module à laquelle elle appartient : +** Les données en entrée du système sont préfixées par en_ +** Les données de référence sont préfixées par ref_ + +* Les objects manipulés ont la forme suivante `nomDeClasse nomObjet` +** Exemple : `en_EqP EqP` indique qu'un objet `EqP` de classe `en_EqP` est disponible + +* Pour les référentiels et les hypothèses : les éléments entre `()` correspondent aux clés uniques de lecture +** Exemples: +*** `ref_Hypothese(dureeVieParDefaut)` correspond à utiliser l'API GET /referentiels/hypotheses?cle={cle} avec la clé `dureeVieParDefaut` (cf.<<GET_Hypotheses>>) +pour obtenir l'objet ref_Hypothese correspondant +*** `ref_ImpactReseau(etape,critere,refReseau)` (cf.<<GET_ImpactsReseaux>>) correspond à utiliser l'API GET /referentiels/impactreseaux?etape={etape}&critere={critere}&reseau={refReseau} avec les paramètres `Etape`,`Critere` et `refReseau` correspondant + +* Le "." derrière un objet (déclaré ou obtenu après une méthode d'exposition) indique l'accès à un attribut de l'objet. +** Exemples : +*** `ref_Hypothese(dureeVieParDefaut).valeur` correspond au champ valeur de l'objet ref_Hypothese de clé `dureeVieParDefaut` +*** `ref_ImpactReseau(Etape,Critere,refReseau).valeur` correspond au champ valeur de l'objet ref_ImpactReseau correspondant à `Etape`,`Critere` et `refReseau` + +* Toutes les règles qui calculent des indicateurs l'enregistre en base de donnée grâce à la methode sauvegarder_impact(ImpactUnitaireReseau) : cf.<<sauvegarder_impact>> + + +[#_moteur_de_calcul] +== Moteur de calcul + +Le déclenchement des règles présentes dans le Bloc Calcul se réalise à la soumission des calculs sur un lot donné via le endpoint POST /entrees/calculs/soumission. +NumEcoEval lance alors les calculs des indicateurs et les sauvegarde dans la base correspondante. + +La cinématique globale est décrite ci-dessous : + +image::cinématique.png[] + +Voici l'algorithme général permettant le caclul des indicateurs + +pseudocode +---- +CalculDesIndicateurs(en_DonneesEntree) { + List<ref_EtapeACV> etapes = GET /referentiels/etapes; + SI en_DonneesEntree.etapesDemandees est renseigné + ALORS mettre à jour la liste etapes pour ne contenir que les étapes demandées + FIN SI + + List<ref_Critere> criteres = GET /referentiels/criteres; + SI en_DonneesEntree.criteresDemandees est renseigné + ALORS mettre à jour la liste criteres pour ne contenir que les critères demandés + FIN SI + + BOUCLE SUR CHAQUE etape DE LA LISTE ref_EtapeACV + BOUCLE SUR CHAQUE criteres DE LA LISTE ref_Critere + + initialisation d'un compteur d'impact applicatif + + BOUCLE SUR CHAQUE en_EqP DE en_DonneesEntree + // Exécutions des règles sur les équipements physiques + calculIndicateurImpactEquipementPhysique(etape, critere, en_DonneesEntree.nomOrganisation, en_EqP, en_DC, ind_ImpactEquipementPhysique) + + SI en_EqP.type est un serveur (ref_TypeEquipement.serveur est vrai) + ALORS initialisation de la variable NbvCPU correspondant à la SOMME des vCPU de tous les équipements virtuels de l'équipement physique si ce dernier est renseigné ou à l'équipement physique dans le cas contraire + ET initialisation de la variable stockageTotalVirtuel correspondant à la SOMME des capaciteStockage de tous les équipements virtuels de l'équipement physique si ce dernier est renseigné ou à l'équipement physique dans le cas contraire + ET initialisation de la variable nbEqV correspondant au nombre de équipements virtuels de l'équipement physique si ce dernier est renseigné ou à l'équipement physique dans le cas contraire + ET initialisation du bouléen vCPUoK indiquant que l'ensemble des équipements virtuels de l'équipement physique (si ce dernier est renseigné ou à l'équipement physique dans le cas contraire) possède un vCPU renseigné, entier et > à 0 + ET initialisation du bouléen stockoK indiquant que l'ensemble des équipements virtuels de l'équipement physique (si ce dernier est renseigné ou à l'équipement physique dans le cas contraire) possède un vCPU renseigné, entier et > à 0 + + Soit ImpactEqP = ind_ImpactEquipementPhysique.ImpactUnitaire/ind_ImpactEquipementPhysique.quantite calculé dans cette boucle + Soit ConsoElecEqP = ind_ImpactEquipementPhysique.consoElecMoyenne/ind_ImpactEquipementPhysique.quantite calculé dans cette boucle calculée dans cette boucle + + BOUCLE SUR CHAQUE en_EqV DE en_DonneesEntree dont l'équipement physique (en_EqV.nomEquipementPhysique) correspond à celui traité dans cette boucle + + ind_ImpactEquipementVirtuel = calculIndicateurImpactEquipementVirtuel(etape, critere, en_EqV, ImpactEqP, ConsoElecEqP, NbvCPU, nbEqV, vCPUoK, stockageTotalVirtuel, stockoK, ind_ImpactEquipement) + + // Calcul des impacts applicatifs : ils correspondent, pour l'impact unitaire, à la somme des impacts unitaires des équipements virtuels d'une application pour un type d'environnement, un critere et une étape ACV, et, pour la consommation electrique, à la somme des consommations électriques des équipements virtuels sous jacent. + BOUCLE SUR CHAQUE en_App DE en_DonneesEntree pour en_App.nomEquipementVirtuel = en_EqV.nomEquipementVirtuel + SI AUCUN impact applicatif n'a été enregistré pour en_App.nomApplication, en_App.typEnvironnement, en_App.critere et en_App.etapeACV + ALORS ENREGISRER l'impact applicatif + SINON + AJOUTER ind_ImpactEquipementVirtuel à l'impact applicatif précedemment enregirsté + FIN SI + + FIN BOUCLE + FIN BOUCLE + FIN SI + + // Calcul de l'impact réseau associé à ces équipements + CalculImpactReseau(en_EqP, etape, critere, ind_ImpactReseau) + + FIN BOUCLE + + FIN BOUCLE + + FIN BOUCLE + + // Messagerie + BOUCLE SUR CHAQUE critere DE LA LISTE ref_Critere + SI ref_ImpactMessagerie.critere EXISTE pour le ref_Critere en cours + BOUCLE SUR CHAQUE en_Messagerie DE LA LISTE en_DonneesEntree.messageries + calculImpactMessagerie(en_Messagerie, critere, ref_ImpactMessagerie, ind_ImpactMessagerie) + FIN BOUCLE + FIN SI + FIN BOUCLE + + SAUVERGARDER les indicateurs d'impact applicatif une fois l'ensemble des boucles terminées. +} +---- + +Liens vers les principaux éléments de cet algorithme + +* <<GET_Etapes>> +* <<GET_Criteres>> +* <<_regles_de_calcul>> +** <<_calcul_impact_EqP_unitaire>> +** <<_calcul_impact_EqV_unitaire>> +** <<_calculImpactReseau>> +** <<_calculImpactMessagerie>> + + + + +[#_regles_de_calcul] +== Règles de calcul + +=== Règles Générales + +[#_rg_transfertdonnees] +==== Transfert de données + +Certaines données reçues via les données d'entrées sont des données transmises +en mode "passe-plat" de l'entrée jusqu'aux indicateurs. +Ces données servent divers objectifs tels que : + +- Le regroupement des indicateurs par lot +- La recherche dans les indicateurs +- Permettre d'agréger les données qui concernent une même catégorie (Organisation, entité, type d'équipement...) +- Les traces + +Les champs de même nom dans les objets d'entrées (en_*) et les objets indicateurs (ind_*) +doivent être transmis à l'identique sans traitement particulier par le moteur de calcul. + +Les champs concernés sont les suivants : + +|=== +|Nom du champ|Obligatoire|Objet Source|Objet de Sortie|Description et usage +|dateLot|Obligatoire|en_DonneesEntree|Tous les objets indicateurs|Date du lot de rattachement des données, permet de regrouper les données d'indicateurs. Définit une seule fois pour tous les objets. +|nomOrganisation|Obligatoire|en_DonneesEntree|Tous les objets indicateurs|Nom de l'organisation rattachée aux données, permet de regrouper les données d'indicateurs. Définit une seule fois pour tous les objets. +|nomEntite|Facultatif|Tous les objets d'entrées|Objets indicateurs du même niveau|Nom de l'entité responsable pouvant agir sur l'objet. Elle est définie dans les données d'entrées et alimente les indicateurs associés. +|=== + + + +[#_rg_traceCalcul] +==== RG_TraceCalcul + +Pour chaque calcul produisant un indicateur (classe héritant de ind_Impact), +le moteur de calcul produit une trace représentée par un objet d'une des classes suivantes : + +- TraceCalculImpactEquipementPhysique : la trace du calcul d'impact d'équipement physique cf. <<_calcul_impact_EqP_unitaire>> +- TraceCalculImpactEquipementVirtuel : la trace du calcul d'impact d'équipement virtuel cf. <<_calcul_impact_EqV_unitaire>> +- TraceCalculImpactMessagerie : la trace du calcul d'impact de la messagerie cf. <<_calculImpactMessagerie>> +- TraceCalculImpactReseau : la trace du calcul d'impact du réseau cf. <<_calculImpactReseau>> + +Tous les objets de trace de calcul possèdent un champ "formule" permettant de vérifier le calcul de l'impact. +La formule se présente comme un calcul mathématique avec le nom des variables et leurs valeurs entre parenthèses. + +_Exemple de valeur pour le champ formule :_ +`ImpactEquipementPhysique = (Quantité(1.0) * ConsoElecAnMoyenne(29.198638586342973) * MixElectrique(0.0813225) * NbJourUtiliseAn(365.0) * tauxUtilisation(1.0)) / 365` + +_Les formules originales (sans variable) sont visibles dans les différentes règles de calcul._ + +En fonction des règles concernées, des champs supplémentaires sont alimentés pour faciliter la vérification. + +link:./Traces.plantuml[Diagramme de classes des objets relatifs aux traces] + + +[#_rg_dureevieeqp] +==== RG_DureeVieEqP : durée de vie d'un équipement physique + +|=== +|Version|Type de changement|Description +|V1|création|Initialisation de la règle à dire d'expert +|=== + +La durée de vie d'un équipement détermine la manière dont est amortie l'impact de sa fabrication. +Elle est exprimée en année et autorise les décimales. + + +|=== +|parametre|entree/sortie|Description +|equipementPhysique|entrée|L'équipement physique dont on cherche à calculer la durée de Vie +|dureeVieEqP|sortie|La durée de vie de l'équipement physique +|=== + +[pseudocode] +---- +Règle RG_DureeVieEqP(equipementPhysique,dureeVieEqP) { + + 'dans le cas où il est possible de calculer l'age réel de l'équipement, ce dernier fait foi + SI equipementPhysique.DateAchat est correcte et renseignée ET equipementPhysique.DateRetrait est correcte et renseignée + ALORS + SI (equipementPhysique.DateRetrait - equipementPhysique.DateAchat) < 1 + ALORS RENVOYER 1 + SINON RENVOYER ((equipementPhysique.DateRetrait - equipementPhysique.DateAchat) / 365) + + 'dans le cas où seul la date d'achat est disponible, la règle du "Pseudo-amortissement" prévaut (impact de 100% sur l'année d'achat, 50% l'année 2, 33% l'année 3, etc... ) + SINON SI equipementPhysique.DateAchat est correcte et equipementPhysique.DateRetrait non reseignée + ALORS RENVOYER ((equipementPhysique.DateAchat - date du jour) / 365) + + 'dans les autres cas, une durée de vie moyenne de l'équipement est déduite + SINON + RENVOYER RG_DureeVieEqP_Defaut(equipementPhysique) + FIN SI +} +---- + +cf.<<_rg_dureevieeqp_defaut>> + +[#_rg_dureevieeqp_defaut] +==== RG_DureeVieEqP_Defaut : durée de vie des équipements par défaut + +|=== +|Version|Type de changement|Description +|V1|création|Initialisation de la règle à dire d'expert +|=== + +La durée de vie des équipements peut être déterminée par défaut dans le cas où la date d'acquisition ne serait pas renseignée. Elle est alors calculée sur la base suivante + +|=== +|paramètre|entree/sortie|Description +|equipementPhysique|entrée|L'équipement physique dont on cherche à calculer la durée de Vie par défaut +|dureeVieEqP_Defaut|sortie|La durée de vie par défaut de l'équipement physique +|=== + +[pseudocode] +---- +Règle RG_DureeVieEqP_Defaut(equipementPhysique, dureeVieEqP_Defaut) { + 'la durée de vie par défaut des équipements est celle renseignée dans la table des types d'équipements + SI l'équipement physique en entrée est présent dans la table des références d'équipement (ref_TypeEquipement.type) + ALORS dureeVieEqP_Defaut = ref_TypeEquipement.dureeVieDefaut + 'on retient l hypothèse d'une durée de vie par défaut des équipements égale à la durée de vie des équipements enregistrée dans les hypothèses + SINON SI ref_Hypothese(dureeVieParDefaut) existe + ALORS dureeVieEqP_Defaut = ref_Hypothese(dureeVieParDefaut).valeur + SINON + ErrCalcul("la durée de vie par défaut de l'équipement n'a pas pu être déterminée") + FIN SI +} +---- + +[#_rg_correspondance_RefEquipement] +==== RG Correspondance entre les équipements et leur références : + +|=== +|Version|Type de changement|Description +|V1|création|Initialisation de la règle +|=== + +Cette règle permet de retrouver l'équipement de référence, c'est à dire celui dont les facteurs d'impact seront utilisés afin d'évaluer l'impact de l'équipement en court de traitement. + +|=== +|paramètre|entree/sortie|Description +|organisation|entrée|l'organisation en cours de traitement +|equipement|entrée|l'équipement dont on cherche la référence +|refEquipementretenu|sortie|l'équipement dont l'impact servira de référence +|=== + +[pseudocode] +---- +Règle RG_correspondanceRefEquipement(nomOrganisation, equipement, refEquipementretenu) { + + SI ref_CorrespondanceRefEqP(nomOrganisation, equipement.modele) existe + RENVOYER refEquipementretenu = ref_CorrespondanceRefEqP(nomOrganisation, equipement.modele).refEquipementCible + SINON SI ref_TypeEquipement(equipement.type).refEquipementParDefaut existe + RENVOYER refEquipementretenu = ref_TypeEquipement(equipement.type).refEquipementParDefaut + SINON + ErrCalcul("aucune correspondance avec un équipement de référence n'a pu être déterminée") + FIN SI + +} +---- + +=== Calculs d'impacts unitaires + + + +[#_calcul_impact_EqP_unitaire] +==== Calcul impact unitaire équipement physique + +|=== +|Version|Type de changement|Description +|V1|création|Initialisation de la règle à dire d'expert +|=== + +Cette règle permet le calcul de l'impact moyen d'un équipement physique sur une année. Elle possède 3 paramètres : l'équipement dont on cherche à déterminer l'impact, l'étapeACV qui permet de déterminer quelle étape du cycle de vie de l'équipement est regardée et le critère d'impact que l'on souhaite extraire. + +|=== +|parametre|entree/sortie|Description +|critere|entrée|le critere dont on cherche à calculer la durée de Vie par défaut +|etapeACV|entrée|l'étape ACV dont on cherche à calculer la durée de Vie par défaut +|nomOrganisation|entrée|le nom de l'organisation en cours de traitement, récupéré de en_DonneesEntrees.nomOrganisation +|equipementPhysique|entrée|l'équipement physique dont on cherche à calculer l'impact environnemental +|en_DC|entrée|le PUE du datacenter lié à l'équipement physique. +|ind_ImpactEquipementPhysique.nomEquipement|sortie|le nom de l'équipement physique calculé +|ind_ImpactEquipementPhysique.critere|sortie|le critere calculé +|ind_ImpactEquipementPhysique.etapeACV|sortie|l'étape ACV calculé +|ind_ImpactEquipementPhysique.type|sortie|en_EqP.type +|ind_ImpactEquipementPhysique.impactUnitaire|sortie|L'impact calculé pour le critère et l'étape ACV, vaut null en cas d'erreur +|ind_ImpactEquipementPhysique.quantite|sortie|la quantité d'équipement physique +|ind_ImpactEquipementPhysique.tauxUtilisation|sortie|le taux d'utilisation de l'équipement physique +|ind_ImpactEquipementPhysique.modeUtilisation|sortie|le mode d'utilisation de l'équipement, il n'est utilisé que si le taux d'utilisation n'est pas renseigné. Les deux modes possibles sont (COPE & BYOD), les taux d'utilisation correspondants son enregistrés dans la table ref_hypotheses +|ind_ImpactEquipementPhysique.unite|sortie|l'unité présente dans le référentiel correspondant au critère en cours +|ind_ImpactEquipementPhysique.consoElecMoyenne|sortie|la consommation électrique déterminée par la règle suivante +|=== + +[pseudocode] +---- +soit en_EqP(Equipement) = EqP + +Règle calculIndicateurImpactEquipementPhysique(critere, etape, nomOrganisation, EqP, en_DC, ind_ImpactEquipementPhysique) { +'Récupération de la correspondance de refEquipement +refEquipementRetenu = RG_correspondanceRefEquipement(nomOrganisation, equipementPhysique) + + ind_ImpactEquipementPhysique(Equipement, EtapeACV, Critere) = + + SI EtapeACV.code = "UTILISATION" + + EqP.quantité + + SI EqP.consoElecAnnuelle est renseigné + x EqP.consoElecAnnuelle + 'enregistrement de la consommation electrique retenue + ind_ImpactEquipementPhysique.consoElecMoyenne = EqP.consoElecAnnuelle + SINON SI + 'récupération de la consommation électrique de la référence de l'équipement retenue + x Ref_ImpactEquipement(refEquipementRetenu, EtapeACV, Critere).consoElecAnMoyenne + 'enregistrement de la consommation electrique retenue + ind_ImpactEquipementPhysique.consoElecMoyenne = Ref_ImpactEquipement(refEquipementRetenu, EtapeACV, Critere).consoElecAnMoyenne + SINON + ErrCalcFonc("donnée de consommation electrique manquante") + Arret du calcul + FIN SI + + SI EqP.NomCourtDatacenter est renseigné + + SI n_DC(EqP.NomCourtDatacenter).PUE existe + x en_DC(EqP.NomCourtDatacenter).PUE + SINON SI ref_Hypothese(PUEParDefaut) + x ref_Hypothese(PUEParDefaut) + SINON + ErrCalcFonc("le PUE est manquant et ne permet le calcul de l'impact à l'usage de l'équipement") + Arret du calcul + FIN SI + + x ref_MixElec(PaysDUtilisation = en_DC(NomCourtDatacenter).localisation).valeur + SINON + x ref_MixElec(PaysDUtilisation = EqP.PaysDUtilisation).valeur + FIN SI + + SI EqP.tauxUtilisation est renseigné + x EqP.tauxUtilisation + SINON SI EqP.modeUtilisation est renseigné + SI ref_Hypothese(EqP.modeUtilisation) existe + EqP.tauxUtilisation = ref_Hypothese(EqP.modeUtilisation) + x EqP.tauxUtilisation + SINON + EqP.tauxUtilisation = 1.0 + x EqP.tauxUtilisation + SINON + EqP.tauxUtilisation = 1.0 + x EqP.tauxUtilisation + FIN SI + + SINON + + EqP.quantité + + SI EqP.tauxUtilisation est renseigné + x EqP.tauxUtilisation + SINON SI EqP.modeUtilisation est renseigné + SI ref_Hypothese(EqP.modeUtilisation) existe + EqP.tauxUtilisation = ref_Hypothese(EqP.modeUtilisation) + x EqP.tauxUtilisation + SINON + EqP.tauxUtilisation = 1.0 + x EqP.tauxUtilisation + SINON + EqP.tauxUtilisation = 1.0 + x EqP.tauxUtilisation + FIN SI + + 'récupération de l'impact pour le critère et l'étape ACV en cours, de la référence de l'équipement retenue + x Ref_ImpactEquipement(refEquipementretenu, EtapeACV, Critere).value + + / RG_DureeVieEqP + + 'conso electrique retenue est null + ind_ImpactEquipementPhysique.consoElecMoyenne = null + + FIN SI +} +---- + +.Calcul de l'impact "moyen" en CO2 lié à l'utilisation d'un serveur sur 1 an +==== +Equipement = exemple d'un serveur d'un client, sur une DataCenter possédant un PUE de 1.13 + +EqP.quantité = 1 +x en_EqP.ConsoElecAnMoyenne= 1000 kWh +x en_EqP.tauxUtilisation = 0,85 +x en_DC(NomCourtDatacenter = "googleCloud").PUE = 1,13 +x ref_MixElec("France","Changement Climatique").valeur = 0,0813225 + +ImpactUnitaireEqP = 78,11 kg CO2eq /an +==== + + +[#_calcul_impact_EqV_unitaire] +==== Calcul d'impact d'une machine virtuel + +|=== +|Version|Type de changement|Description +|V1|création|Initialisation de la règle à dire d'expert +|=== + +Cette règle permet le calcul de l'impact moyen d'un équipement virtuel sur une année. +L'impact alloué à la machine virtuelle correspond à une part de l'impact de l'équipement physique sous-jacent (décrit ici <<_calcul_impact_EqP_unitaire>>) + +|=== +|parametre|entree/sortie|Description +|critere|entrée|le critère dont on cherche à calculer la durée de Vie par défaut +|etapeACV|entrée|l'étape ACV dont on cherche à calculer la durée de Vie par défaut +|en_EqV|entrée|l'équipement virtuel dont on cherche à calculer l'impact environnemental +|ImpactEqP|entrée|l'équipement physique dont l'impact est en cours de calcul +|ConsoElecEqP|entrée|la consommation electrique retenue de l'équipement physique dont l'impact est en cours de calcul +|NbvCPU|entrée|le nombre de vCPU total de l'ensemble des machines virtuelles associées à l'équipement physique sous-jacent de l'équipement virtuel en cours de calcul +|nbEqV|entrée| le nombre total d'équipements virtuels associées à l'équipement physique sous-jacent de l'équipement virtuel en cours de calcul +|vCPUoK|entrée| le booléen indiquant que l'ensemble des VM du serveur possèdent un vCPU correctement renseigné +|ind_ImpactEquipementVirtuel.nomEquipement|sortie|le nom de l'équipement virtuel calculé +|ind_ImpactEquipementVirtuel.critere|sortie|le critère calculé +|ind_ImpactEquipementVirtuel.etapeACV|sortie|l'étape ACV calculée +|ind_ImpactEquipementVirtuel.cluster|sortie|le cluster présent dans les données d'entrée pour cette équipements virtuels +|ind_ImpactEquipementVirtuel.impactUnitaire|sortie|l'impact calculé pour le critère et l'étape ACV +|ind_ImpactEquipementVirtuel.unite|sortie|l'unité présente dans le référentiel correspondant au critère en cours +|ind_ImpactEquipementVirtuel.consoElecMoyenne|sortie|la consommation électrique renseignée en entrée +|=== + + +[pseudocode] +---- +soit en_EqV(nomEquipementVirtuel) = EqV + +Règle calculIndicateurImpactEquipementVirtuel(critere, etape, en_EqV, ImpactEqP, ConsoElecEqP, NbvCPU, nbEqV, vCPUoK, stockageTotalVirtuel, ind_ImpactEquipementVirtuel) { + + SI EqV.cleRepartition est renseigné + ind_ImpactEquipementVirtuel.ImpactUnitaire = ImpactEqP x EqV.cleRepartition + ind_ImpactEquipementVirtuel.consoElecMoyenne = ConsoElecEqP x EqV.cleRepartition + + SINON SI EqV.typeEqV = "calcul" et vCPUoK est vrai + ind_ImpactEquipementVirtuel.ImpactUnitaire = ImpactEqP x EqV.vCPU / NbvCPU + ind_ImpactEquipementVirtuel.consoElecMoyenne = ConsoElecEqP x EqV.vCPU / NbvCPU + + SINON SI EqV.typeEqV = "stockage" et stockoK est vrai + ind_ImpactEquipementVirtuel.ImpactUnitaire = ImpactEqP x EqV.capaciteStockage / stockageTotalVirtuel + ind_ImpactEquipementVirtuel.consoElecMoyenne = ConsoElecEqP x EqV.capaciteStockage / stockageTotalVirtuel + + SINON + 'Si l'info des vCPU est manquante sur au moins un des équipement virtuel de l'équipement, on répartis l'impact à parts égale sur l'ensemble des équipements virtuels + ind_ImpactEquipementVirtuel.ImpactUnitaire = ImpactEqP / nbEqV + ind_ImpactEquipementVirtuel.consoElecMoyenne = ConsoElecEqP / nbEqV + FIN SI + +} +---- + + +[#_calculIndicateurImpactApplicatif] +==== Calcul impact associé à une application + +|=== +|Version|Type de changement|Description +|V1|création|Initialisation de la règle à dire d'expert +|=== + +Cet indicateur correspond l'impact moyen annuel d'une application. +Il est calculé *directement dans le moteur de calcul* <<_moteur_de_calcul>> et part de l'hypothèse qu'un application tourne sur une ou plusieurs équipements virtuels dédiée(s). L'impact alloué à une application correspond alors l'impact des équipements virtuels sous-jacent (décrit ici <<_calcul_impact_EqV_unitaire>>). + +|=== +|parametre|entree/sortie|Description +|critere|entrée|le critère dont on cherche à calculer la durée de Vie par defaut +|etapeACV|entrée|l'étape ACV dont on cherche à calculer la durée de Vie par defaut +|nomApp|entrée|nom de l'applicaton +|typeEnvironnement|entrée|type d'environnement sur lequel +|ind_ImpactApplicatif.nomApplication|sortie|en_App.nomApplication +|ind_ImpactApplicatif.critere|sortie|critère calculé +|ind_ImpactApplicatif.etapeACV|sortie|étape ACV calculée +|ind_ImpactApplicatif.typeEnvironnement|sortie|en_App.typeEnvironnement +|ind_ImpactApplicatif.domaine|sortie|en_App.domaine +|ind_ImpactApplicatif.sousDomaine|sortie|en_App.sousDomaine +|ind_ImpactApplicatif.unite|sortie|l'unité présente dans le référentiel correspondant au critère en cours +|ind_ImpactApplicatif.consoElecMoyenne|sortie|la consomation électrique calculée (correspondant à la somme des consommations éléctriques des équipements virtuels sous-jacentes) +|=== + + +[#_calculImpactReseau] +==== Calcul impact lié à la sollicitation du réseau + +|=== +|version|type de changement|description +|V1|création|Initialisation de la règle à dire d'expert +|=== + +*Note importante* : cette règle de calcul ne fait pas l'objet d'un consensus +Cette règle permet le calcul de l'impact lié à la sollicitation des différents type de réseaux permettant l'interconnexion des utilisateurs et des équipements. Il peut s'agir de flux internet mais on peut aussi considérer les données transitant entre deux sites par exemple client via une liaison louée par exemple, ou même au sein d'un datacenter. + +|=== +|parametre|entree/sortie|Description +|equipementPhysique|entrée|l'équipement physique pour lequel l'impact réseau est calculé +|critere|entrée|le critere dont on cherche à calculer la durée de Vie par defaut +|etapeACV|entrée|l'étape ACV dont on cherche à calculer la durée de Vie par defaut +|ind_ImpactReseau.reference|sortie|type d'impact réseau +|ind_ImpactReseau.ImpactUnitaire|sortie|impact calculé +|ind_ImpactReseau.critere|sortie|critere calculé +|ind_ImpactReseau.etapeACV|sortie|etape ACV calculé +|ind_ImpactReseau.unite|sortie|l'unité présente dans le référentiel correspondant au critere en cours +|=== + +[pseudocode] +---- +Règle calculImpactReseau(equipementPhysique, critere, etape, ind_ImpactReseau) { + + SI equipementPhysique.goTelecharge est renseigné ET ref_ImpactReseau(etape, critere, equipementPhysique) existe + + ALORS + + impactReseau = + equipementPhysique.goTelecharge + x ref_ImpactReseau(etape, critere, equipementPhysique).valeur + + RENVOYER ImpactUnitaireReseau + + SINON SI ref_ImpactReseau("impactReseauMobileMoyen") n'existe pas ou ref_ImpactReseau("impactReseauMobileMoyen").valeur n'est pas renseigné + + ErrCalcFonc("la référence impactReseauMobileMoyen n'existe pas") + + FIN SI +} +---- + +Références: + +* ref_ImpactReseau(etape, critere, equipementPhysique) : cf.<<GET_ImpactsReseaux>> +* sauvegarder_impact(impact) : cf.<<sauvegarder_impact>> + + + + +[#_calculImpactMessagerie] +==== Calcul impact lié à la Messagerie + +|=== +|version|type de changement|description +|V1|création|Initialisation de la règle à dire d'expert +|=== + +*Note importante* : cette règle de calcul ne fait pas l'objet d'un consensus +Cette règle permet un calcul approximatif de l'impact lié à la messagerie. Il prend en compte l'impact moyen d'un mail établi à partir de différentes sources et en prenant en compte le poids des PJ (cf. sources) + +|=== +|parametre|entree/sortie|Description +|en_Messagerie|entrée|les données d'entrée liées à la messagerie +|ref_Critere|entrée|Critère d'impact en cours de travaux +|ref_Messagerie|entrée|Les paramètres associées à la fonction affine pour le critère en cours. +|ind_ImpactMessagerie.impactMensuel|sortie|l'impact calculé avec la fonction affine et les paramètre du référentiel +|ind_ImpactMessagerie.MoisAnnee|sortie|en_Messagerie.MoisAnnee +|ind_ImpactMessagerie.critere|sortie|ref_Critere.nomCritere +|ind_ImpactMessagerie|sortie|Autant d'indicateurs qu'il y a d'objet en_Messagerie en entrée +|=== + +[pseudocode] +---- +Règle calculImpactMessagerie(en_Messagerie, ref_Critere, ref_ImpactMessagerie, ind_ImpactMessagerie) { + + 'Production d'un indicateur ind_ImpactMessagerie par entrée en_Messagerie + 'calcul du poids moyen d'un mail en Go + Double poidsMoyenMail = en_Messagerie.volumeTotalMailEmis/en_Messagerie.nombreMailEmis + 'on applique la fonction affine en utilisant les éléments du ref_ImpactMessagerie pour connaitre l'impact d'un mail en C02 selon son poids, et on le multiplie par le volume de mail envoyés + ind_ImpactMessagerie.impactMensuel = (ref_ImpactMessagerie.constanteCoefficientDirecteur * poidsMoyenMail + ref_ImpactMessagerie.constanteOrdonneeOrigine) * en_Messagerie.nombreMailEmisXDestinataires + ind_ImpactMessagerie.MoisAnnee = en_Messagerie.MoisAnnee + ind_ImpactMessagerie.critere = ref_Critere.nomCritere + +} +---- + +== Gestion des erreurs + +=== ErrCalcFonc +Les erreurs fonctionnelles sont de la forme suivante : "ErrCalcFonc : [règle calculée et paramètres de la fonction associée] ; [message d'erreur]" +Au déclenchement de ce type d'erreur, une ligne s'affiche dans les logs, avec le nom de la règle où l'erreur s'est produite, les paramètres entrant de la règle et le message indiqués entre parenthèse. + +.Erreur de calcul de l'impact réseau +==== + "ErrCalcFonc : ImpactUnitaireReseau(equipement, etapeACV, critere) ; la référence "impactReseauMobileMoyen" n'existe pas" +==== + +=== ErrCalcTech +Cette erreur se déclenche durant les calculs en cas d'erreurs non prévues et sont de la forme suivante : "ErrCalcTech : [règle calculée et paramètres de la fonction associée] ; [code de l'erreur technique]" + + diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..d9bb705f --- /dev/null +++ b/docs/README.md @@ -0,0 +1,4 @@ +# Fichiers + +Le Dossier d'architecture technique se trouve dans le wiki du Gitlab: + - https://gitlab-forge.din.developpement-durable.gouv.fr/pub/numeco/m4g/numecoeval/-/wikis/MTE-DAT diff --git a/docs/References.plantuml b/docs/References.plantuml new file mode 100644 index 00000000..3393d834 --- /dev/null +++ b/docs/References.plantuml @@ -0,0 +1,97 @@ +@startuml + +'les hypotheses generique regroupe les hypothèses permettant le calcul de certains indicateurs +class ref_Hypothese{ +*String clé +String valeur +String source +} + +'liste des types d'équipements autorisés et de leur durée de vie par défaut +class ref_TypeEquipement{ +*String type +'le booléen "serveur" permet de distinguer les équipements inclus dans le calcul de l'impact d'un serveur applicatif à l'utilisation +boolean serveur +'le commentaire permet notamment de détailler les catégories équipements contenues dans un même type +String commentaire +Double dureeVieDefaut +' la référence de l'équipement par defaut doit exister dans la table ref_ImpactEquipement.refEquipement +String refEquipementParDefaut +String source +} + +'Impact environnementaux de référence +class ref_ImpactEquipement { +'identifiant de l'équipement +* String refEquipement +* String etape +* String critere +'description de l'équipement +String description +'le type doit correspondre à celui présent dans ref_TypeEquipement +String type +Double consoElecMoyenne +Double valeur +String source +} + +class ref_CorrespondanceRefEqP { +'modèle d'équipement de l'organisation, la valeur du champ doit exister dans en_EqP.modele +* String modeleEquipementSource +'Equipement de référence, la valeur du champ doit exister dans ref_ImpactEquipement.refEquipement +String refEquipementCible +} + +' Critère d'impact +Class ref_Critere { +*String nomCritere +String unité +String description +} + +' étapes du cycle de vie des équipements pris en compte dans les calculs +class ref_EtapeACV { +* String code +String libelle +String description +} + +class ref_MixElec { +*String pays +*String critere +String raccourcisAnglais +Double valeur +String unité +String source +} + +'Impact environnementaux du réseau +class ref_ImpactReseau{ +* String refReseau +* String etape +* String critere +Double consoElecMoyenne +Double valeur +String source +} + +'Impact environnemental Messagerie +class ref_ImpactMessagerie{ +'L'équation d'impact est une fonction affine de la forme a * x + b +'Cette constante correspond à "a" dans la fonction affine +Double constanteCoefficientDirecteur +'Cette constante correspond à "b" dans la fonction affine +Double constanteOrdonneeOrigine +'Critère associé à l'impact +String critere +String source +} + +ref_EtapeACV "1" --- "0-*" ref_ImpactEquipement : dépend de > +ref_Critere "1" --- "0-*" ref_ImpactEquipement : dépend de > +ref_Critere "1" --- "0-*" ref_MixElec : dépend de > +ref_Critere "1" --- "0-*" ref_ImpactReseau : dépend de > +ref_Critere "1" --- "0-*" ref_ImpactMessagerie : dépend de > +ref_EtapeACV "1" --- "0-*" ref_ImpactReseau : dépend de > + +@enduml diff --git a/docs/Traces.plantuml b/docs/Traces.plantuml new file mode 100644 index 00000000..6798e034 --- /dev/null +++ b/docs/Traces.plantuml @@ -0,0 +1,107 @@ +@startuml +'Uniquement pour faciliter la lecture du diagramme, pas encore d'usage +abstract class TraceCalculImpact{ +} +hide TraceCalculImpact + +' Classes de trace + + +class TraceCalculImpactEquipementPhysique { + String formule + Integer quantite + Double tauxUtilisation + String paysUtilisation + String nomCourtDataCenter + ConsoElecAnMoyenne consoElecAnMoyenne + MixElectrique mixElectrique + NbJourUtiliseAn nbJourUtiliseAn + Double valeurReferentielImpactEquipement + String sourceReferentielImpactEquipement + DureeDeVie dureeDeVie +} + +class TraceCalculImpactEquipementVirtuel { + String formule + boolean vCPUok + Integer vCPU + Integer nbvCPU + Integer nbVM +} + +class TraceCalculImpactMessagerie { + String formule + String critere + String sourceReferentielImpactMessagerie + Double volumeTotalMailEmis + Double nombreMailEmis + Double constanteCoefficientDirecteur + Double poidsMoyenMail + Double constanteOrdonneeOrigine + Double nombreMailEmisXDestinataires +} + +class TraceCalculImpactReseau { + String formule + String critere + String etapeACV + String sourceReferentielReseau + String equipementPhysique + String impactReseauMobileMoyen + String goTelecharge +} + +' Classes des objets associées aux calculs + +class ConsoElecAnMoyenne { + Double valeur + Double valeurEquipementConsoElecAnnuelle + Double valeurReferentielConsoElecMoyenne + String sourceReferentielImpactEquipement +} + + class DureeDeVie { + Double valeur + String dateAchat + String dateRetrait + DureeDeVieParDefaut dureeDeVieParDefaut +} + + class DureeDeVieParDefaut { + Double valeur + Double valeurEquipementDureeVieDefaut + Double valeurReferentielHypothese + String sourceReferentielHypothese + String typeEquipement +} + +class MixElectrique { + Double valeur + boolean isServeur + Double dataCenterPue + Double valeurReferentiel + String sourceReferentiel + String pays +} + +class NbJourUtiliseAn { + Double equipementNbJourUtiliseAn + Double valeurReferentielHypothese + String sourceReferentielHypothese + Double valeur +} + +ConsoElecAnMoyenne "0-1" -up- "1" TraceCalculImpactEquipementPhysique : peut contenir > +DureeDeVie "0-1" -up- "1" TraceCalculImpactEquipementPhysique : peut contenir > +MixElectrique "0-1" -up- "1" TraceCalculImpactEquipementPhysique : peut contenir > +NbJourUtiliseAn "0-1" -up- "1" TraceCalculImpactEquipementPhysique : peut contenir > + +DureeDeVieParDefaut "0-1" -up- "1" DureeDeVie : peut contenir > + + +TraceCalculImpact <|-- "hérite de" TraceCalculImpactReseau +TraceCalculImpact <|-- "hérite de" TraceCalculImpactMessagerie +TraceCalculImpact <|-- "hérite de" TraceCalculImpactEquipementVirtuel +TraceCalculImpact <|-- "hérite de" TraceCalculImpactEquipementPhysique + +@enduml diff --git a/docs/archi_fonctionnelle_V4.png b/docs/archi_fonctionnelle_V4.png new file mode 100644 index 0000000000000000000000000000000000000000..826a8eb0fcf37993acdac349d84d822a11f96dd7 GIT binary patch literal 198960 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≦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	UJy9BV#5 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 H1CRX;V$(8U literal 0 HcmV?d00001 diff --git a/docs/architecture_fonctionnelle_simplifiee_V1.png b/docs/architecture_fonctionnelle_simplifiee_V1.png new file mode 100644 index 0000000000000000000000000000000000000000..e3e532184a154fa93d604f521c7dc4f3fcec2581 GIT binary patch literal 122857 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�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	KD{sqV>{^THdmVj41B0h<bZdi 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�I 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 Bjj;d# literal 0 HcmV?d00001 diff --git "a/docs/cin\303\251matique.png" "b/docs/cin\303\251matique.png" new file mode 100644 index 0000000000000000000000000000000000000000..4629fa24e74dd82a74b1fbf88df1b9ea3fe3dc18 GIT binary patch literal 36723 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 literal 0 HcmV?d00001 diff --git a/docs/cycle_de_vie.jpg b/docs/cycle_de_vie.jpg new file mode 100644 index 0000000000000000000000000000000000000000..54bde5cdce29343e215ad084324b26cb1ec8d55a GIT binary patch literal 40376 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	x3OVJC?bZ)dMgwEZe8u@ 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 QCt8tDP?gVfJ#_o^->^)*0ssI2 literal 0 HcmV?d00001 diff --git a/e2e/.env b/e2e/.env new file mode 100644 index 00000000..9e2bfa27 --- /dev/null +++ b/e2e/.env @@ -0,0 +1,31 @@ +REFERENTIEL_URL=http://localhost:18080 +ENTREE_URL=http://localhost:18081 +INDICATEUR_URL=http://localhost:18085 + +### For dataset generation ### +# Virtual Machine +VM_VCPU=4 +VM_TYPE_EQV=PY1ORA01,PY1LNX04 +# Application +APP_TYPE_ENV=Production,Recette,Pre-production,Developpement,Test + +### Dataset sizes ### +# XX="EqPh EqPhSrv VmPerSrv AppPerVm" +E2E="100 100 1 1" +XS="100 100 4 2" +S="1000 1000 8 4" +M="10000 4000 12 6" +L="20000 6000 16 8" +XL="50000 10000 20 10" + +### Scenarios ### +# SCENARIO_X="E2E XS S M L XL" +SCENARIO_E2E="1 0 0 0 0 0" +SCENARIO_1="0 10 5 1 0 0" +SCENARIO_2="0 100 50 10 1 0" +SCENARIO_3="0 200 100 20 2 1" +SCENARIO_4="0 400 200 40 4 2" + +### Reports ### +HEADER_RESULT="Dataset,Dataset size(MB),Dataset size (lines),Nb iteration,Average time (sec),Total time (sec)" +HEADER_DETAIL="Dataset,Iteration,Total time (sec)" diff --git a/e2e/.gitignore b/e2e/.gitignore new file mode 100644 index 00000000..a0d6c697 --- /dev/null +++ b/e2e/.gitignore @@ -0,0 +1,6 @@ +expected +input_ref +data +actual +reports +progress.log diff --git a/e2e/1_load_ref.sh b/e2e/1_load_ref.sh new file mode 100644 index 00000000..19400954 --- /dev/null +++ b/e2e/1_load_ref.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +# CONTANTS +REFERENTIELS="criteres etapes hypotheses impactequipements impactreseaux mixelecs typeEquipement correspondanceRefEquipement" + +. ./.env +. ./utils.sh + +log "Send referentiel data" +for ref in $REFERENTIELS; do + log_n + curl -s -XPOST $REFERENTIEL_URL/referentiel/$ref/csv --form file=@input_ref/$ref.csv + echo "" +done diff --git a/e2e/2_generate_dataset.sh b/e2e/2_generate_dataset.sh new file mode 100644 index 00000000..54aac7a1 --- /dev/null +++ b/e2e/2_generate_dataset.sh @@ -0,0 +1,105 @@ +#!/bin/bash +# Call: sh 2_generate_dataset.sh +NB_EQ_PH_HORS_SRV=${1:-100} +NB_EQ_PH_SRV=${2:-100} +NB_VM_PAR_SRV=${3:-4} +NB_APP_PAR_VM=${4:-2} + +. ./.env +. ./utils.sh + +# CONTANTS +NB_CSV_HORS_SRV=100 +NB_CSV_SRV=100 + +set -f +ARR_VM_TYPE_EQV=(${VM_TYPE_EQV//,/ }) +ARR_APP_TYPE_ENV=(${APP_TYPE_ENV//,/ }) + +if [ -d data ]; then + rm -rf data +fi +mkdir data + +# *** GENERATE DATASET *** +log "Generate input data in the 'data' local folder" +log "NB_EQ_PH_HORS_SRV=$NB_EQ_PH_HORS_SRV, NB_EQ_PH_SRV=$NB_EQ_PH_SRV, NB_VM_PAR_SRV=$NB_VM_PAR_SRV, NB_APP_PAR_VM=$NB_APP_PAR_VM" + +# *** DataCenter.csv *** +log "Generate input data : DataCenter.csv" +cp -f input_template/DataCenter.csv data/ + +# *** EquipementPhysique_hors_serveur.csv *** +log "Generate input data : EquipementPhysique_hors_serveur.csv" +head -n1 input_template/EquipementPhysique_hors_serveur.csv >data/EquipementPhysique.csv + +for ((i = 1; i <= $(($NB_EQ_PH_HORS_SRV / $NB_CSV_HORS_SRV)); i++)); do + tail -n $NB_CSV_HORS_SRV input_template/EquipementPhysique_hors_serveur.csv | sed s/physical-eq-/physical-eq-${i}/g >>data/EquipementPhysique.csv +done + +# *** EquipementPhysique_serveur.csv *** +log "Generate input data : EquipementPhysique_serveur.csv" + +for ((i = 1; i <= $(($NB_EQ_PH_SRV / $NB_CSV_SRV)); i++)); do + tail -n $NB_CSV_SRV input_template/EquipementPhysique_serveur.csv | sed s/physical-eq-srv-/physical-eq-srv-${i}/g >>data/EquipementPhysique.csv +done + +# *** EquipementVirtuel.csv *** +log "Generate input data : EquipementVirtuel.csv" +head -n1 input_template/EquipementVirtuel.csv >data/EquipementVirtuel.csv + +VM_LINE=$(tail -n1 input_template/EquipementVirtuel.csv) + +n=0 +for ((i = 1; i <= $(($NB_EQ_PH_SRV / $NB_CSV_SRV)); i++)); do + for id in {001..100}; do + for ((vm = 1; vm <= $NB_VM_PAR_SRV; vm++)); do + type_eqv=${ARR_VM_TYPE_EQV[$((n % ${#ARR_VM_TYPE_EQV[@]}))]} + VM1=${VM_LINE//virtual-eq-/virtual-eq-$i$id-$vm} + VM2=${VM1//physical-eq-srv-/physical-eq-srv-$i$id} + VM3=${VM2//##VCPU##/$VM_VCPU} + echo ${VM3//##TYPE_EQV##/${type_eqv}} + n=$((n + 1)) + done + done >>data/EquipementVirtuel.csv +done + +# *** Application.csv *** +log "Generate input data : Application.csv" +head -n1 input_template/Application.csv >data/Application.csv + +APP_LINE=$(tail -n1 input_template/Application.csv) + +n=0 +for ((i = 1; i <= $(($NB_EQ_PH_SRV / $NB_CSV_SRV)); i++)); do + for id in {001..100}; do + for ((vm = 1; vm <= $NB_VM_PAR_SRV; vm++)); do + for ((app = 1; app <= $NB_APP_PAR_VM; app++)); do + type_env=${ARR_APP_TYPE_ENV[$((n % ${#ARR_APP_TYPE_ENV[@]}))]} + APP1=${APP_LINE//application-/application-$i$id-$vm-$app} + APP2=${APP1//virtual-eq-/virtual-eq-$i$id-$vm} + APP3=${APP2//physical-eq-srv-/physical-eq-srv-$i$id} + echo ${APP3//##TYPE_ENV##/${type_env}} + n=$((n + 1)) + done + done + done >>data/Application.csv +done + +log "Generate input data : End" + +echo "File,Size(MB),lines" >data/sizes.csv +total=0 +lines=0 +for file in $(ls data/ | grep -v sizes); do + size=$(ls -l data/$file | cut -d' ' -f5) + size=$(awk "BEGIN {printf \"%.3f\",$size/(1024 * 1024)}") + line=$(wc -l data/$file | cut -d' ' -f1) + echo "$file,$size,$line" >>data/sizes.csv + total=$(awk "BEGIN {printf \"%.3f\",$total + $size}") + lines=$((lines + $line)) +done + +echo "TOTAL,$total,$lines" >>data/sizes.csv + +log "Report sizes generated in data/sizes.csv" diff --git a/e2e/3_load_input.sh b/e2e/3_load_input.sh new file mode 100644 index 00000000..1368dc3a --- /dev/null +++ b/e2e/3_load_input.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +ORGANISATION=${1:-org1} +NOM_LOT=$2 +SCENARIO=${3:-E2E} +MODE=${4:-ASYNC} +DATE_LOT=$(date +'%Y-%m-%d') + +. ./.env +. ./utils.sh + +rm -f progress.log + +log_n "$ORGANISATION - $NOM_LOT - $DATE_LOT - Load Input - " | tee -a progress.log +curl -s -XPOST "$ENTREE_URL/entrees/csv?nomLot=${NOM_LOT}&dateLot=${DATE_LOT}&nomOrganisation=${ORGANISATION}" \ + --form csvDataCenter=@data/DataCenter.csv \ + --form csvEquipementPhysique=@data/EquipementPhysique.csv \ + --form csvEquipementVirtuel=@data/EquipementVirtuel.csv \ + --form csvApplication=@data/Application.csv | tee -a progress.log +echo "" | tee -a progress.log + +sleep 2 + +log_n "$ORGANISATION - $NOM_LOT - $DATE_LOT - Soumission with mode=${MODE} - " | tee -a progress.log + +curl -s -XPOST "$ENTREE_URL/entrees/calculs/soumission?mode=$MODE" -d"{\"nomLot\":\"${NOM_LOT}\"}" -H "Content-Type: application/json" | tee -a progress.log +echo "" | tee -a progress.log diff --git a/e2e/4_check.sh b/e2e/4_check.sh new file mode 100644 index 00000000..2d201305 --- /dev/null +++ b/e2e/4_check.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +ORGANISATION=${1:-org1} +NOM_LOT=$2 + +. ./.env +. ./utils.sh + +while true; do + log_n "$ORGANISATION - $NOM_LOT - Statut - " | tee -a progress.log + response=$(curl -s "$ENTREE_URL/entrees/calculs/statut?nomLot=${NOM_LOT}&nomOrganisation=${ORGANISATION}") + if echo $response | grep -q "TERMINE"; then + echo $response | tee -a progress.log + echo "" | tee -a progress.log + break + fi + + echo $response | tee -a progress.log + echo "" | tee -a progress.log + sleep 10 +done diff --git a/e2e/5_assert.sh b/e2e/5_assert.sh new file mode 100644 index 00000000..f61d80ec --- /dev/null +++ b/e2e/5_assert.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +ORGANISATION=${1:-org1} +NOM_LOT=${2:-lot1} + +# load INDICATEUR_URL +. ./.env + +export_table() { + curl -s "$INDICATEUR_URL/indicateur/${1}Csv?nomLot=${NOM_LOT}&nomOrganisation=${ORGANISATION}&fields=${2}" >actual/${1}.csv +} + +if [ -d actual ]; then rm -rf actual; fi +mkdir actual + +export_table equipementPhysique conso_elec_moyenne,critere,etapeacv,impact_unitaire,nom_entite,nom_entite_discriminator,nom_equipement,nom_source_donnee,nom_source_donnee_discriminator,quantite,source,statut_equipement_physique,statut_indicateur,trace,type_equipement,unite,version_calcul +export_table equipementVirtuel cluster,conso_elec_moyenne,critere,etapeacv,impact_unitaire,nom_entite,nom_entite_discriminator,nom_equipement,nom_equipement_virtuel,nom_source_donnee,nom_source_donnee_discriminator,source,statut_indicateur,trace,unite,version_calcul +export_table application conso_elec_moyenne,critere,domaine,etapeacv,impact_unitaire,nom_application,nom_entite,nom_entite_discriminator,nom_equipement_physique,nom_equipement_virtuel,nom_source_donnee,nom_source_donnee_discriminator,source,sous_domaine,statut_indicateur,trace,type_environnement,unite,version_calcul +export_table reseau etapeacv,critere,source,statut_indicateur,trace,version_calcul,impact_unitaire,unite,nom_entite,nom_equipement +ALL_OK=true +for file in $(ls actual/); do + echo -n "Check file $file : " + res=$(diff -qs actual/$file expected/$file) + if [ $? -eq 1 ]; then + echo "KO" + echo "*** REGRESSION : file $file is different from expected, see file: reports/diff_$file" + diff actual/$file expected/$files > reports/diff_$file + ALL_OK=false + else + echo "OK" + fi +done + +if [ "${ALL_OK}" = "false" ]; then + exit 1 +fi diff --git a/e2e/README.md b/e2e/README.md new file mode 100644 index 00000000..6c8109ce --- /dev/null +++ b/e2e/README.md @@ -0,0 +1,37 @@ +# E2E et génération de JDD + +Dossiers : +- input_ref : référentiel +- input_template : + - DataCenter.csv : fichier classique + - EquipementPhysique_hors_serveur.csv : fichier avec 100 lignes d'équipements hors Server + - EquipementPhysique_serveur.csv : fichier avec 100 lignes d'équipements Server + - EquipementVirtuel.csv : 1 ligne variabilisé avec VCPU et TYPE_EQV + - Application.csv: 1 ligne variabilisé avec TYPE_ENV +- expected: + - export des tables indicateurs en mode csv, triés + +Le test E2E (non-reg) génère 100 équipements physiques hors serveur, 100 serveurs, 100 VM, 100 applications. + +Lancement du test E2E: +- generic: `sh performance-test.sh E2E $NomOrg $NotLotPrefix` +- exemple: `sh performance-test.sh E2E org perf` + +## Générer un JDD + +Le script 2_generate_dataset.sh permet de générer un dataset à partir des fichiers présents dans `input_template`. + +Il prend 4 paramètres: +- NB_EQ_PH_HORS_SRV +- NB_EQ_PH_SRV +- NB_VM_PAR_SRV +- NB_APP_PAR_VM + +Exemple d'appel : `sh 2_generate_dataset.sh 100 100 1 1` +- Cela génère dans le dossier data : + - 100 EqPh hors server + - 100 EqPh server + - 100 EqPh * 1 VM = 100 VM + - 100 EqPh * 1 VM * 1 APP = 100 APP + + diff --git a/e2e/e2e.iml b/e2e/e2e.iml new file mode 100644 index 00000000..8021953e --- /dev/null +++ b/e2e/e2e.iml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="UTF-8"?> +<module type="WEB_MODULE" version="4"> + <component name="NewModuleRootManager" inherit-compiler-output="true"> + <exclude-output /> + <content url="file://$MODULE_DIR$" /> + <orderEntry type="inheritedJdk" /> + <orderEntry type="sourceFolder" forTests="false" /> + </component> +</module> \ No newline at end of file diff --git a/e2e/input_template/Application.csv b/e2e/input_template/Application.csv new file mode 100644 index 00000000..f6d03dcd --- /dev/null +++ b/e2e/input_template/Application.csv @@ -0,0 +1,2 @@ +nomApplication;typeEnvironnement;nomEquipementVirtuel;nomSourceDonneeEquipementVirtuel;domaine;sousDomaine;nomEntite;nomEquipementPhysique;nomSourceDonnee +application-;##TYPE_ENV##;virtual-eq-;;Domain 1;Sub Domain 1;MY ENTERPRISE;physical-eq-srv-;; diff --git a/e2e/input_template/DataCenter.csv b/e2e/input_template/DataCenter.csv new file mode 100644 index 00000000..11043f09 --- /dev/null +++ b/e2e/input_template/DataCenter.csv @@ -0,0 +1,9 @@ +nomCourtDatacenter;nomLongDatacenter;pue;localisation;nomEntite; +default;Default;1.75;France;; +B1;B1;1.32;France;; +F1;F1;1.43;France;; +G1;G1;1.45;Spain;; +X1;X1;1.42;Russia;; +Y1;Y1;1.41;Germany;; +Z2;Z2;1.75;France;; +MAG;MAG;1.76;Belgium;; diff --git a/e2e/input_template/EquipementPhysique_hors_serveur.csv b/e2e/input_template/EquipementPhysique_hors_serveur.csv new file mode 100644 index 00000000..09c3d4a9 --- /dev/null +++ b/e2e/input_template/EquipementPhysique_hors_serveur.csv @@ -0,0 +1,101 @@ +nomEquipementPhysique;modele;quantite;nomCourtDatacenter;dateAchat;dateRetrait;type;statut;paysDUtilisation;consoElecAnnuelle;utilisateur;nomSourceDonnee;nomEntite;nbCoeur;nbJourUtiliseAn;goTelecharge;modeUtilisation;tauxUtilisation +physical-eq-001;P2719;1;;2021-03-30;2023-06-16;Monitor;In use;France;;;;MY ENTERPRISE;;365;;COPE; +physical-eq-002;P2719;2;;2021-03-30;2023-06-16;Monitor;In use;France;;;;MY ENTERPRISE;;365;;BYOD; +physical-eq-003;P2719;4;;2021-03-30;2023-06-16;Monitor;In use;France;;;;MY ENTERPRISE;;365;;COB; +physical-eq-004;HUB USB;10;;2023-04-01;2023-06-16;Consumable;Consumed;France;;;;MY ENTERPRISE;;365;;BYOD;0.1 +physical-eq-005;P2720DC;2;;2022-11-04;2023-06-16;Monitor;In stock;France;;;;MY ENTERPRISE;;365;;BYOD;12.1 +physical-eq-006;PIXEL 6;1;;2022-10-18;2023-06-16;Communication Device;Missing;France;;;;MY ENTERPRISE;;365;;;test +physical-eq-007;UP2516D;1;;2022-07-31;2023-06-16;Monitor;Retired;France;;;;MY ENTERPRISE;;365;;; +physical-eq-008;UP2516D;2;;2022-03-10;2023-06-16;Monitor;In stock;France;;;;MY ENTERPRISE;;365;;; +physical-eq-009;Unknown;3;;2021-08-09;2023-06-16;IP Router;In use;France;;;;MY ENTERPRISE;;365;;; +physical-eq-010;Unknown;3;;2021-08-09;2023-06-16;IP Router;In use;France;;;;MY ENTERPRISE;;365;;; +physical-eq-011;HP 8440p;1;;2021-08-05;2023-06-16;Personal Computer;Missing;France;;;;MY ENTERPRISE;;365;;; +physical-eq-012;HP 8440p;1;;2023-04-27;2023-06-16;Personal Computer;Retired;France;;;;MY ENTERPRISE;;365;;; +physical-eq-013;HP 8470w;2;;2025-08-23;2023-06-16;Personal Computer;Retired;France;;;;MY ENTERPRISE;;365;;; +physical-eq-014;OPPO A72;1;;2023-08-07;2023-06-16;Communication Device;Retired;France;;;;MY ENTERPRISE;;365;;; +physical-eq-015;CROSSCALL;1;;2022-08-20;2023-06-16;Communication Device;Retired;France;;;;MY ENTERPRISE;;365;;; +physical-eq-016;CROSSCALL;11;;2022-05-20;2023-06-16;Communication Device;In stock;France;;;;MY ENTERPRISE;;365;;; +physical-eq-017;CROSSCALL;8;;2022-05-28;2023-06-16;Communication Device;In use;France;;;;MY ENTERPRISE;;365;;; +physical-eq-018;HUAWEI P9;11;;2021-01-28;2023-06-16;Communication Device;In stock;France;;;;MY ENTERPRISE;;365;;; +physical-eq-019;HUAWEI P9;4;;2022-08-18;2023-06-16;Communication Device;Retired;France;;;;MY ENTERPRISE;;365;;; +physical-eq-020;HUAWEI P9;1;;2023-06-18;2023-06-16;Communication Device;Missing;France;;;;MY ENTERPRISE;;365;;; +physical-eq-021;HUAWEI P9;32;;2021-01-28;2023-06-16;Communication Device;Missing;France;;;;MY ENTERPRISE;;365;;; +physical-eq-022;HUAWEI P9;9;;2024-08-18;2023-06-16;Communication Device;Retired;France;;;;MY ENTERPRISE;;365;;; +physical-eq-023;PRO MP242;50;;2022-06-21;2023-06-16;Monitor;In use;France;;;;MY ENTERPRISE;;365;;; +physical-eq-024;SAC A DOS;534;;2023-02-18;2023-06-16;Consumable;Consumed;France;;;;MY ENTERPRISE;;365;;; +physical-eq-025;SAC A DOS;1;;2023-02-18;2023-06-16;Consumable;Missing;France;;;;MY ENTERPRISE;;365;;; +physical-eq-026;SPIDER X1;2;;2022-10-17;2023-06-16;Communication Device;In stock;France;;;;MY ENTERPRISE;;365;;; +physical-eq-027;DELL 1909W;5;;2022-11-20;2023-06-16;Monitor;Retired;France;;;;MY ENTERPRISE;;365;;; +physical-eq-028;DELL 1909W;1;;2021-12-20;2023-06-16;Monitor;Missing;France;;;;MY ENTERPRISE;;365;;; +physical-eq-029;DELL 1909W;14;;2025-10-17;2023-06-16;Monitor;Retired;France;;;;MY ENTERPRISE;;365;;; +physical-eq-030;DELL 2412M;1;;2022-12-26;2023-06-16;Monitor;Retired;France;;;;MY ENTERPRISE;;365;;; +physical-eq-031;DELL 2717H;19;;2021-04-08;2023-06-16;Monitor;In use;France;;;;MY ENTERPRISE;;365;;; +physical-eq-032;DELL 2717H;1;;2024-11-23;2023-06-16;Monitor;Retired;France;;;;MY ENTERPRISE;;365;;; +physical-eq-033;DELL E7440;5;;2023-03-19;2023-06-16;Personal Computer;Retired;France;;;;MY ENTERPRISE;;365;;; +physical-eq-034;DELL E7440;31;;2025-10-27;2023-06-16;Personal Computer;Retired;France;;;;MY ENTERPRISE;;365;;; +physical-eq-035;DELL E7440;11;;2022-04-24;2023-06-16;Personal Computer;Missing;France;;;;MY ENTERPRISE;;365;;; +physical-eq-036;DELL E7450;2;;2022-12-21;2023-06-16;Personal Computer;Retired;France;;;;MY ENTERPRISE;;365;;; +physical-eq-037;DELL E7450;14;;2025-10-31;2023-06-16;Personal Computer;Retired;France;;;;MY ENTERPRISE;;365;;; +physical-eq-038;DELL E7450;6;;2022-04-22;2023-06-16;Personal Computer;Missing;France;;;;MY ENTERPRISE;;365;;; +physical-eq-039;DELL E7470;33;;2023-02-11;2023-06-16;Personal Computer;Retired;France;;;;MY ENTERPRISE;;365;;; +physical-eq-040;DELL E7470;23;;2022-03-16;2023-06-16;Personal Computer;Missing;France;;;;MY ENTERPRISE;;365;;; +physical-eq-041;DELL E7470;9;;2022-03-16;2023-06-16;Personal Computer;In use;France;;;;MY ENTERPRISE;;365;;; +physical-eq-042;DELL E7470;4;;2022-03-16;2023-06-16;Personal Computer;In stock;France;;;;MY ENTERPRISE;;365;;; +physical-eq-043;DELL E7470;325;;2024-09-01;2023-06-16;Personal Computer;Retired;France;;;;MY ENTERPRISE;;365;;; +physical-eq-044;DELL E7470;2;;2025-03-03;2023-06-16;Personal Computer;In stock;France;;;;MY ENTERPRISE;;365;;; +physical-eq-045;DELL E7480;349;;2022-12-11;2023-06-16;Personal Computer;Retired;France;;;;MY ENTERPRISE;;365;;; +physical-eq-046;DELL E7480;49;;2021-10-26;2023-06-16;Personal Computer;Missing;France;;;;MY ENTERPRISE;;365;;; +physical-eq-047;DELL E7480;18;;2021-09-25;2023-06-16;Personal Computer;In stock;France;;;;MY ENTERPRISE;;365;;; +physical-eq-048;DELL E7480;141;;2023-11-04;2023-06-16;Personal Computer;Retired;France;;;;MY ENTERPRISE;;365;;; +physical-eq-049;DELL E7480;70;;2021-09-30;2023-06-16;Personal Computer;In use;France;;;;MY ENTERPRISE;;365;;; +physical-eq-050;DELL E7480;1;;2025-06-03;2023-06-16;Personal Computer;Missing;France;;;;MY ENTERPRISE;;365;;; +physical-eq-051;DELL E7490;248;;2022-03-21;2023-06-16;Personal Computer;Retired;France;;;;MY ENTERPRISE;;365;;; +physical-eq-052;DELL E7490;52;;2021-06-17;2023-06-16;Personal Computer;In stock;France;;;;MY ENTERPRISE;;365;;; +physical-eq-053;DELL E7490;421;;2021-06-01;2023-06-16;Personal Computer;In use;France;;;;MY ENTERPRISE;;365;;; +physical-eq-054;DELL E7490;51;;2021-06-19;2023-06-16;Personal Computer;Missing;France;;;;MY ENTERPRISE;;365;;; +physical-eq-055;DELL E7490;6;;2024-04-20;2023-06-16;Personal Computer;Retired;France;;;;MY ENTERPRISE;;365;;; +physical-eq-056;DELL P1911;8;;2022-07-25;2023-06-16;Monitor;Retired;France;;;;MY ENTERPRISE;;365;;; +physical-eq-057;DELL P1911;6;;2025-01-04;2023-06-16;Monitor;Retired;France;;;;MY ENTERPRISE;;365;;; +physical-eq-058;DELL P1913;4;;2022-11-02;2023-06-16;Monitor;Retired;France;;;;MY ENTERPRISE;;365;;; +physical-eq-059;DELL P1913;2;;2024-04-12;2023-06-16;Monitor;Retired;France;;;;MY ENTERPRISE;;365;;; +physical-eq-060;HUAWEI P20;1;;2021-12-24;2023-06-16;Communication Device;Retired;France;;;;MY ENTERPRISE;;365;;; +physical-eq-061;LENOVO M73;13;;2024-01-25;2023-06-16;Personal Computer;Retired;France;;;;MY ENTERPRISE;;365;;; +physical-eq-062;LENOVO M73;13;;2022-01-25;2023-06-16;Personal Computer;In Use;France;;;;MY ENTERPRISE;;365;;; +physical-eq-063;ANTIVOL MAC;1;;2023-01-14;2023-06-16;Consumable;Missing;France;;;;MY ENTERPRISE;;365;;; +physical-eq-064;ANTIVOL MAC;1;;2023-01-14;2023-06-16;Consumable;In stock;France;;;;MY ENTERPRISE;;365;;; +physical-eq-065;ANTIVOL MAC;10;;2023-01-14;2023-06-16;Consumable;Consumed;France;;;;MY ENTERPRISE;;365;;; +physical-eq-066;AOC U2879VF;10;;2022-11-27;2023-06-16;Monitor;In use;France;;;;MY ENTERPRISE;;365;;; +physical-eq-067;DELL P2210T;1;;2022-12-29;2023-06-16;Monitor;Retired;France;;;;MY ENTERPRISE;;365;;; +physical-eq-068;DELL P2217H;286;;2021-03-13;2023-06-16;Monitor;In use;France;;;;MY ENTERPRISE;;365;;; +physical-eq-069;DELL P2217H;1;;2021-03-13;2023-06-16;Monitor;Missing;France;;;;MY ENTERPRISE;;365;;; +physical-eq-070;DELL P2217H;5;;2022-01-14;2023-06-16;Monitor;Retired;France;;;;MY ENTERPRISE;;365;;; +physical-eq-071;DELL P2217H;6;;2021-03-13;2023-06-16;Monitor;In stock;France;;;;MY ENTERPRISE;;365;;; +physical-eq-072;DELL P2217H;3;;2023-10-15;2023-06-16;Monitor;Retired;France;;;;MY ENTERPRISE;;365;;; +physical-eq-073;DELL P2319H;1;;2022-12-19;2023-06-16;Monitor;In stock;France;;;;MY ENTERPRISE;;365;;; +physical-eq-074;DELL P2319H;49;;2022-12-19;2023-06-16;Monitor;In use;France;;;;MY ENTERPRISE;;365;;; +physical-eq-075;DELL P2412H;1;;2022-03-25;2023-06-16;Monitor;Retired;France;;;;MY ENTERPRISE;;365;;; +physical-eq-076;DELL P2412H;1;;2025-06-01;2023-06-16;Monitor;Retired;France;;;;MY ENTERPRISE;;365;;; +physical-eq-077;DELL P2415Q;7;;2022-03-13;2023-06-16;Monitor;In use;France;;;;MY ENTERPRISE;;365;;; +physical-eq-078;DELL P2415Q;1;;2022-03-13;2023-06-16;Monitor;In stock;France;;;;MY ENTERPRISE;;365;;; +physical-eq-079;DELL P2417H;371;;2021-03-18;2023-06-16;Monitor;In use;France;;;;MY ENTERPRISE;;365;;; +physical-eq-080;DELL P2417H;7;;2022-07-16;2023-06-16;Monitor;Retired;France;;;;MY ENTERPRISE;;365;;; +physical-eq-081;DELL P2417H;4;;2024-07-02;2023-06-16;Monitor;Retired;France;;;;MY ENTERPRISE;;365;;; +physical-eq-082;DELL P2417H;5;;2021-03-18;2023-06-16;Monitor;In stock;France;;;;MY ENTERPRISE;;365;;; +physical-eq-083;DELL P2419H;11;;2021-09-01;2023-06-16;Monitor;In stock;France;;;;MY ENTERPRISE;;365;;; +physical-eq-084;DELL P2419H;11;;2022-10-18;2023-06-16;Monitor;Retired;France;;;;MY ENTERPRISE;;365;;; +physical-eq-085;DELL P2419H;847;;2021-09-01;2023-06-16;Monitor;In use;France;;;;MY ENTERPRISE;;365;;; +physical-eq-086;DELL P2419H;1;;2021-09-01;2023-06-16;Monitor;Missing;France;;;;MY ENTERPRISE;;365;;; +physical-eq-087;DELL P2419H;32;;2024-05-15;2023-06-16;Monitor;Retired;France;;;;MY ENTERPRISE;;365;;; +physical-eq-088;DELL P2422H;7;;2023-04-23;2023-06-16;Monitor;Retired;France;;;;MY ENTERPRISE;;365;;; +physical-eq-089;DELL P2422H;2;;2023-09-30;2023-06-16;Monitor;Retired;France;;;;MY ENTERPRISE;;365;;; +physical-eq-090;DELL P2422H;588;;2022-09-25;2023-06-16;Monitor;In use;France;;;;MY ENTERPRISE;;365;;; +physical-eq-091;DELL P2719H;1;;2022-04-04;2023-06-16;Monitor;Retired;France;;;;MY ENTERPRISE;;365;;; +physical-eq-092;DELL P2719H;43;;2021-07-29;2023-06-16;Monitor;In use;France;;;;MY ENTERPRISE;;365;;; +physical-eq-093;DELL P2719H;2;;2021-07-29;2023-06-16;Monitor;In stock;France;;;;MY ENTERPRISE;;365;;; +physical-eq-094;DELL P2721Q;2;;2023-02-24;2023-06-16;Monitor;In use;France;;;;MY ENTERPRISE;;365;;; +physical-eq-095;DELL S2216H;2;;2024-11-27;2023-06-16;Monitor;Retired;France;;;;MY ENTERPRISE;;365;;; +physical-eq-096;DELL S2216H;17;;2021-05-21;2023-06-16;Monitor;In use;France;;;;MY ENTERPRISE;;365;;; +physical-eq-097;DELL S2216H;1;;2022-11-22;2023-06-16;Monitor;Retired;France;;;;MY ENTERPRISE;;365;;; +physical-eq-098;DELL S2240L;28;;2021-06-26;2023-06-16;Monitor;In use;France;;;;MY ENTERPRISE;;365;;; +physical-eq-099;DELL S2240L;8;;2022-12-04;2023-06-16;Monitor;Retired;France;;;;MY ENTERPRISE;;365;;; +physical-eq-100;DELL S2240L;4;;2024-12-09;2023-06-16;Monitor;Retired;France;;;;MY ENTERPRISE;;365;;; diff --git a/e2e/input_template/EquipementPhysique_serveur.csv b/e2e/input_template/EquipementPhysique_serveur.csv new file mode 100644 index 00000000..21333d8a --- /dev/null +++ b/e2e/input_template/EquipementPhysique_serveur.csv @@ -0,0 +1,101 @@ +nomEquipementPhysique;modele;quantite;nomCourtDatacenter;dateAchat;dateRetrait;type;statut;paysDUtilisation;consoElecAnnuelle;utilisateur;nomSourceDonnee;nomEntite;nbCoeur;nbJourUtiliseAn;goTelecharge;modeUtilisation;tauxUtilisation +physical-eq-srv-001;blade-server--28;7;default;2016-06-17;2023-06-16;Server;In use;;;;;;;365;;; +physical-eq-srv-002;DATACENTER;1;F1;2016-06-17;2023-06-16;Server;In use;;;;;;;365;10;; +physical-eq-srv-003;DATACENTER;1;F1;2016-06-17;2023-06-16;Server;In use;;;;;;;365;20;; +physical-eq-srv-004;DATACENTER;1;F1;2016-06-17;2023-06-16;Server;In use;;;;;;;365;30;; +physical-eq-srv-005;DATACENTER;1;F1;2016-06-17;2023-06-16;Server;In use;;;;;;;365;40;; +physical-eq-srv-006;DATACENTER;1;F1;2016-06-17;2023-06-16;Server;In use;;;;;;;365;50;; +physical-eq-srv-007;DATACENTER;1;F1;2016-06-17;2023-06-16;Server;In use;;;;;;;365;;; +physical-eq-srv-008;DATACENTER;1;F1;2016-06-17;2023-06-16;Server;In use;;;;;;;365;;; +physical-eq-srv-009;DATACENTER;1;F1;2016-06-17;2023-06-16;Server;In use;;;;;;;365;;; +physical-eq-srv-010;DATACENTER;2;F1;2016-06-17;2023-06-16;Server;In use;;;;;;;365;;; +physical-eq-srv-011;DATACENTER;1;F1;2016-06-17;2023-06-16;Server;In use;;;;;;;365;;; +physical-eq-srv-012;DATACENTER;1;F1;2016-06-17;2023-06-16;Server;In use;;;;;;;365;;; +physical-eq-srv-013;DATACENTER;1;F1;2016-06-17;2023-06-16;Server;In use;;;;;;;365;;; +physical-eq-srv-014;DATACENTER;1;G1;2016-06-17;2023-06-16;Server;In use;;;;;;;365;;; +physical-eq-srv-015;DATACENTER;1;G1;2016-06-17;2023-06-16;Server;In use;;;;;;;365;;; +physical-eq-srv-016;DATACENTER;1;G1;2016-06-17;2023-06-16;Server;In use;;;;;;;365;;; +physical-eq-srv-017;DATACENTER;1;X1;2016-06-17;2023-06-16;Server;In use;;;;;;;365;;; +physical-eq-srv-018;DATACENTER;1;X1;2016-06-17;2023-06-16;Server;In use;;;;;;;365;;; +physical-eq-srv-019;DATACENTER;1;X1;2016-06-17;2023-06-16;Server;In use;;;;;;;365;;; +physical-eq-srv-020;DATACENTER;8;X1;2016-06-17;2023-06-16;Server;In use;;;;;;;365;;; +physical-eq-srv-021;DATACENTER;1;X1;2016-06-17;2023-06-16;Server;In use;;;;;;;365;;; +physical-eq-srv-022;DATACENTER;1;X1;2016-06-17;2023-06-16;Server;In use;;;;;;;365;;; +physical-eq-srv-023;DATACENTER;1;X1;2016-06-17;2023-06-16;Server;In use;;;;;;;365;;; +physical-eq-srv-024;DATACENTER;1;X1;2016-06-17;2023-06-16;Server;In use;;;;;;;365;;; +physical-eq-srv-025;DATACENTER;1;X1;2016-06-17;2023-06-16;Server;In use;;;;;;;365;;; +physical-eq-srv-026;DATACENTER;1;X1;2016-06-17;2023-06-16;Server;In use;;;;;;;365;;; +physical-eq-srv-027;DATACENTER;1;X1;2016-06-17;2023-06-16;Server;In use;;;;;;;365;;; +physical-eq-srv-028;DATACENTER;1;X1;2016-06-17;2023-06-16;Server;In use;;;;;;;365;;; +physical-eq-srv-029;DATACENTER;1;X1;2016-06-17;2023-06-16;Server;In use;;;;;;;365;;; +physical-eq-srv-030;DATACENTER;1;X1;2016-06-17;2023-06-16;Server;In use;;;;;;;365;;; +physical-eq-srv-031;DATACENTER;1;X1;2016-06-17;2023-06-16;Server;In use;;;;;;;365;;; +physical-eq-srv-032;DATACENTER;1;X1;2016-06-17;2023-06-16;Server;In use;;;;;;;365;;; +physical-eq-srv-033;DATACENTER;1;X1;2016-06-17;2023-06-16;Server;In use;;;;;;;365;;; +physical-eq-srv-034;DATACENTER;1;X1;2016-06-17;2023-06-16;Server;In use;;;;;;;365;;; +physical-eq-srv-035;DATACENTER;1;Y1;2016-06-17;2023-06-16;Server;In use;Germany;;;;;;365;;; +physical-eq-srv-036;DATACENTER;1;Y1;2016-06-17;2023-06-16;Server;In use;Germany;;;;;;365;;; +physical-eq-srv-037;DATACENTER;1;Y1;2016-06-17;2023-06-16;Server;In use;Germany;;;;;;365;;; +physical-eq-srv-038;DATACENTER;1;Y1;2016-06-17;2023-06-16;Server;In use;Germany;;;;;;365;;; +physical-eq-srv-039;DATACENTER;1;Y1;2016-06-17;2023-06-16;Server;In use;Germany;;;;;;365;;; +physical-eq-srv-040;DATACENTER;1;Y1;2016-06-17;2023-06-16;Server;In use;Germany;;;;;;365;;; +physical-eq-srv-041;DATACENTER;1;Y1;2016-06-17;2023-06-16;Server;In use;Germany;;;;;;365;;; +physical-eq-srv-042;DATACENTER;1;Y1;2016-06-17;2023-06-16;Server;In use;Germany;;;;;;365;;; +physical-eq-srv-043;DATACENTER;1;Y1;2016-06-17;2023-06-16;Server;In use;Germany;;;;;;365;;; +physical-eq-srv-044;DATACENTER;1;Y1;2016-06-17;2023-06-16;Server;In use;Germany;;;;;;365;;; +physical-eq-srv-045;DATACENTER;26;Y1;2016-06-17;2023-06-16;Server;In use;Germany;;;;;;365;;; +physical-eq-srv-046;DATACENTER;1;Y1;2016-06-17;2023-06-16;Server;In use;Germany;;;;;;365;;; +physical-eq-srv-047;DATACENTER;1;Y1;2016-06-17;2023-06-16;Server;In use;Germany;;;;;;365;;; +physical-eq-srv-048;DATACENTER;1;Y1;2016-06-17;2023-06-16;Server;In use;Germany;;;;;;365;;; +physical-eq-srv-049;DATACENTER;1;Y1;2016-06-17;2023-06-16;Server;In use;Germany;;;;;;365;;; +physical-eq-srv-050;DATACENTER;1;Y1;2016-06-17;2023-06-16;Server;In use;Germany;;;;;;365;;; +physical-eq-srv-051;DATACENTER;1;Y1;2016-06-17;2023-06-16;Server;In use;Germany;;;;;;365;;; +physical-eq-srv-052;DATACENTER;1;Y1;2016-06-17;2023-06-16;Server;In use;Germany;;;;;;365;;; +physical-eq-srv-053;DATACENTER;1;Y1;2016-06-17;2023-06-16;Server;In use;Germany;;;;;;365;;; +physical-eq-srv-054;DATACENTER;1;Y1;2016-06-17;2023-06-16;Server;In use;Germany;;;;;;365;;; +physical-eq-srv-055;DATACENTER;1;Y1;2016-06-17;2023-06-16;Server;In use;Germany;;;;;;365;;; +physical-eq-srv-056;DATACENTER;1;Y1;2016-06-17;2023-06-16;Server;In use;Germany;;;;;;365;;; +physical-eq-srv-057;DATACENTER;1;Y1;2016-06-17;2023-06-16;Server;In use;Germany;;;;;;365;;; +physical-eq-srv-058;DATACENTER;1;Y1;2016-06-17;2023-06-16;Server;In use;Germany;;;;;;365;;; +physical-eq-srv-059;DATACENTER;1;Y1;2016-06-17;2023-06-16;Server;In use;Germany;;;;;;365;;; +physical-eq-srv-060;DATACENTER;1;Y1;2016-06-17;2023-06-16;Server;In use;Germany;;;;;;365;;; +physical-eq-srv-061;DATACENTER;1;Y1;2016-06-17;2023-06-16;Server;In use;Germany;;;;;;365;;; +physical-eq-srv-062;DATACENTER;1;Y1;2016-06-17;2023-06-16;Server;In use;Germany;;;;;;365;;; +physical-eq-srv-063;DATACENTER;1;Y1;2016-06-17;2023-06-16;Server;In use;Germany;;;;;;365;;; +physical-eq-srv-064;DATACENTER;1;Y1;2016-06-17;2023-06-16;Server;In use;Germany;;;;;;365;;; +physical-eq-srv-065;DATACENTER;1;Y1;2016-06-17;2023-06-16;Server;In use;Germany;;;;;;365;;; +physical-eq-srv-066;DATACENTER;1;Y1;2016-06-17;2023-06-16;Server;In use;Germany;;;;;;365;;; +physical-eq-srv-067;DATACENTER;1;Y1;2016-06-17;2023-06-16;Server;In use;Germany;;;;;;365;;; +physical-eq-srv-068;DATACENTER;1;Y1;2016-06-17;2023-06-16;Server;In use;Germany;;;;;;365;;; +physical-eq-srv-069;DATACENTER;1;Y1;2016-06-17;2023-06-16;Server;In use;Germany;;;;;;365;;; +physical-eq-srv-070;DATACENTER;1;Y1;2016-06-17;2023-06-16;Server;In use;Germany;;;;;;365;;; +physical-eq-srv-071;DATACENTER;1;Y1;2016-06-17;2023-06-16;Server;In use;Germany;;;;;;365;;; +physical-eq-srv-072;DATACENTER;1;Y1;2016-06-17;2023-06-16;Server;In use;Germany;;;;;;365;;; +physical-eq-srv-073;DATACENTER;1;Y1;2016-06-17;2023-06-16;Server;In use;Germany;;;;;;365;;; +physical-eq-srv-074;DATACENTER;1;Z2;2016-06-17;2023-06-16;Server;In use;France;;;;;;365;;; +physical-eq-srv-075;DATACENTER;122;Z2;2016-06-17;2023-06-16;Server;In use;France;;;;;;365;;; +physical-eq-srv-076;DATACENTER;1;Z2;2016-06-17;2023-06-16;Server;In use;France;;;;;;365;;; +physical-eq-srv-077;DATACENTER;1;Z2;2016-06-17;2023-06-16;Server;In use;France;;;;;;365;;; +physical-eq-srv-078;DATACENTER;1;Z2;2016-06-17;2023-06-16;Server;In use;France;;;;;;365;;; +physical-eq-srv-079;DATACENTER;1;Z2;2016-06-17;2023-06-16;Server;In use;France;;;;;;365;;; +physical-eq-srv-080;DATACENTER;1;Z2;2016-06-17;2023-06-16;Server;In use;France;;;;;;365;;; +physical-eq-srv-081;DATACENTER;1;Z2;2016-06-17;2023-06-16;Server;In use;France;;;;;;365;;; +physical-eq-srv-082;DATACENTER;1;Z2;2016-06-17;2023-06-16;Server;In use;France;;;;;;365;;; +physical-eq-srv-083;DATACENTER;1;Z2;2016-06-17;2023-06-16;Server;In use;France;;;;;;365;;; +physical-eq-srv-084;DATACENTER;1;Z2;2016-06-17;2023-06-16;Server;In use;France;;;;;;365;;; +physical-eq-srv-085;DATACENTER;1;Z2;2016-06-17;2023-06-16;Server;In use;France;;;;;;365;;; +physical-eq-srv-086;DATACENTER;1;Z2;2016-06-17;2023-06-16;Server;In use;France;;;;;;365;;; +physical-eq-srv-087;DATACENTER;1;Z2;2016-06-17;2023-06-16;Server;In use;France;;;;;;365;;; +physical-eq-srv-088;DATACENTER;1;Z2;2016-06-17;2023-06-16;Server;In use;France;;;;;;365;;; +physical-eq-srv-089;DATACENTER;1;Z2;2016-06-17;2023-06-16;Server;In use;France;;;;;;365;;; +physical-eq-srv-090;DATACENTER;1;Z2;2016-06-17;2023-06-16;Server;In use;France;;;;;;365;;; +physical-eq-srv-091;DATACENTER;1;Z2;2016-06-17;2023-06-16;Server;In use;France;;;;;;365;;; +physical-eq-srv-092;DATACENTER;1;Z2;2016-06-17;2023-06-16;Server;In use;France;;;;;;365;;; +physical-eq-srv-093;DATACENTER;1;Z2;2016-06-17;2023-06-16;Server;In use;France;;;;;;365;;; +physical-eq-srv-094;DATACENTER;1;Z2;2016-06-17;2023-06-16;Server;In use;France;;;;;;365;;; +physical-eq-srv-095;DATACENTER;1;Z2;2016-06-17;2023-06-16;Server;In use;France;;;;;;365;;; +physical-eq-srv-096;DATACENTER;1;Z2;2016-06-17;2023-06-16;Server;In use;France;;;;;;365;;; +physical-eq-srv-097;DATACENTER;1;Z2;2016-06-17;2023-06-16;Server;In use;France;;;;;;365;;; +physical-eq-srv-098;DATACENTER;1;Z2;2016-06-17;2023-06-16;Server;In use;France;;;;;;365;;; +physical-eq-srv-099;DATACENTER;1;Z2;2016-06-17;2023-06-16;Server;In use;France;;;;;;365;;; +physical-eq-srv-100;DATACENTER;1;Z2;2016-06-17;2023-06-16;Server;In use;France;;;;;;365;;; diff --git a/e2e/input_template/EquipementVirtuel.csv b/e2e/input_template/EquipementVirtuel.csv new file mode 100644 index 00000000..c6754843 --- /dev/null +++ b/e2e/input_template/EquipementVirtuel.csv @@ -0,0 +1,2 @@ +nomEquipementVirtuel;nomEquipementPhysique;nomSourceDonneeEquipementPhysique;cleRepartition;vCPU;cluster;consoElecAnnuelle;typeEqv;capaciteStockage;nomEntite;nomSourceDonnee +virtual-eq-;physical-eq-srv-;;;##VCPU##;##TYPE_EQV##;;calcul;;;; diff --git a/e2e/performance-test.sh b/e2e/performance-test.sh new file mode 100644 index 00000000..31a9b4a4 --- /dev/null +++ b/e2e/performance-test.sh @@ -0,0 +1,62 @@ +#!/bin/bash +# Call: sh performance-test.sh +# Call: sh performance-test.sh E2E auto auto SYNC +# Call: sh performance-test.sh 1 orgX perf ASYNC + +SCENARIO=${1:-E2E} +ORGANISATION=${2:-auto} +LOT_PREFIX=${3:-auto} +MODE=${4:-SYNC} + +. ./.env + +# CONSTANTS +ARR_DATASET=(${E2E// /,} ${XS// /,} ${S// /,} ${M// /,} ${L// /,} ${XL// /,}) +ARR_MAPPING=(E2E XS S M L XL) + +if [ "${ORGANISATION}" = "auto" ];then ORGANISATION=org;fi +if [ "${LOT_PREFIX}" = "auto" ];then LOT_PREFIX=$(tr -dc A-Za-z0-9 </dev/urandom | head -c 5);fi + +if [ -d reports ]; then rm -rf reports; fi +mkdir reports +echo ${HEADER_RESULT} >reports/result.csv +echo ${HEADER_DETAIL} >reports/detail.csv + +cfg_scenario="SCENARIO_${SCENARIO}" +echo "Config scenario: $cfg_scenario = ${!cfg_scenario}" +i=0 +for nbIt in ${!cfg_scenario}; do + dataset=${ARR_MAPPING[$i]} + dataset_detail=${ARR_DATASET[$i]} + i=$((i + 1)) + if [ "${nbIt}" = "0" ]; then continue; fi + dataset_size_mb=0 + dataset_size_line=0 + ### Generate dataset ### + echo "launch dataset ($dataset), detail : $dataset_detail" + sh 2_generate_dataset.sh ${dataset_detail//,/ } + dataset_size_mb=$(cat data/sizes.csv | grep TOTAL | cut -d',' -f2) + dataset_size_line=$(cat data/sizes.csv | grep TOTAL | cut -d',' -f3) + mv data/sizes.csv reports/sizes_${dataset}.csv + start=$(date +%s) + for ((j = 1; j <= $nbIt; j++)); do + ### For each iteration load input, submit calculations and check ### + echo "*** Organisation: $ORGANISATION, NomLot: ${LOT_PREFIX}_${dataset}_${j} ***" + sh 3_load_input.sh $ORGANISATION ${LOT_PREFIX}_${dataset}_${j} $SCENARIO $MODE + start_it=$(date +%s) + sh 4_check.sh $ORGANISATION ${LOT_PREFIX}_${dataset}_${j} + end_it=$(date +%s) + elapsed_it_sec=$(($end_it - $start_it)) + mv progress.log reports/${LOT_PREFIX}_${dataset}_${j}.log + echo "$dataset,$j,$elapsed_it_sec" >>reports/detail.csv + done + end=$(date +%s) + elapsed_sec=$(($end - $start)) + echo "$dataset,$dataset_size_mb,$dataset_size_line,$nbIt,$(($elapsed_sec / $nbIt)),$elapsed_sec" >>reports/result.csv +done + +cat reports/result.csv + +if [ "${SCENARIO}" = "E2E" ]; then + sh 5_assert.sh $ORGANISATION ${LOT_PREFIX}_E2E_1 +fi diff --git a/e2e/utils.sh b/e2e/utils.sh new file mode 100644 index 00000000..f20bcb41 --- /dev/null +++ b/e2e/utils.sh @@ -0,0 +1,7 @@ +function log() { + echo "$(date +'%Y-%m-%d %H:%M:%S.%3N') - $@" +} + +function log_n() { + echo -n "$(date +'%Y-%m-%d %H:%M:%S.%3N') - $@" +} \ No newline at end of file diff --git a/services/.workspace/.idea/.gitignore b/services/.workspace/.idea/.gitignore new file mode 100644 index 00000000..9f6f15d5 --- /dev/null +++ b/services/.workspace/.idea/.gitignore @@ -0,0 +1,5 @@ +# Default ignored files +/shelf/ +/workspace.xml +sonarlint/ +libraries/ \ No newline at end of file diff --git a/services/.workspace/.idea/codeStyles/Project.xml b/services/.workspace/.idea/codeStyles/Project.xml new file mode 100644 index 00000000..919ce1f1 --- /dev/null +++ b/services/.workspace/.idea/codeStyles/Project.xml @@ -0,0 +1,7 @@ +<component name="ProjectCodeStyleConfiguration"> + <code_scheme name="Project" version="173"> + <ScalaCodeStyleSettings> + <option name="MULTILINE_STRING_CLOSING_QUOTES_ON_NEW_LINE" value="true" /> + </ScalaCodeStyleSettings> + </code_scheme> +</component> \ No newline at end of file diff --git a/services/.workspace/.idea/codeStyles/codeStyleConfig.xml b/services/.workspace/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 00000000..a55e7a17 --- /dev/null +++ b/services/.workspace/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ +<component name="ProjectCodeStyleConfiguration"> + <state> + <option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" /> + </state> +</component> \ No newline at end of file diff --git a/services/.workspace/.idea/compiler.xml b/services/.workspace/.idea/compiler.xml new file mode 100644 index 00000000..f8461598 --- /dev/null +++ b/services/.workspace/.idea/compiler.xml @@ -0,0 +1,55 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="CompilerConfiguration"> + <annotationProcessing> + <profile default="true" name="Default" enabled="true" /> + <profile name="Maven default annotation processors profile" enabled="true"> + <sourceOutputDir name="target/generated-sources/annotations" /> + <sourceTestOutputDir name="target/generated-test-sources/test-annotations" /> + <outputRelativeToContentRoot value="true" /> + </profile> + <profile name="Annotation profile for api-expositiondonneesentrees" enabled="true"> + <sourceOutputDir name="target/generated-sources/annotations" /> + <sourceTestOutputDir name="target/generated-test-sources/test-annotations" /> + <outputRelativeToContentRoot value="true" /> + <processorPath useClasspath="false"> + <entry name="$MAVEN_REPOSITORY$/org/mapstruct/mapstruct-processor/1.5.3.Final/mapstruct-processor-1.5.3.Final.jar" /> + <entry name="$MAVEN_REPOSITORY$/org/mapstruct/mapstruct/1.5.3.Final/mapstruct-1.5.3.Final.jar" /> + <entry name="$MAVEN_REPOSITORY$/org/projectlombok/lombok/1.18.26/lombok-1.18.26.jar" /> + <entry name="$MAVEN_REPOSITORY$/org/projectlombok/lombok-mapstruct-binding/0.2.0/lombok-mapstruct-binding-0.2.0.jar" /> + <entry name="$MAVEN_REPOSITORY$/com/github/therapi/therapi-runtime-javadoc-scribe/0.15.0/therapi-runtime-javadoc-scribe-0.15.0.jar" /> + <entry name="$MAVEN_REPOSITORY$/com/github/therapi/therapi-runtime-javadoc/0.15.0/therapi-runtime-javadoc-0.15.0.jar" /> + <entry name="$MAVEN_REPOSITORY$/org/instancio/instancio-processor/2.2.0/instancio-processor-2.2.0.jar" /> + <entry name="$MAVEN_REPOSITORY$/org/instancio/instancio-core/2.2.0/instancio-core-2.2.0.jar" /> + <entry name="$MAVEN_REPOSITORY$/org/jetbrains/annotations/23.1.0/annotations-23.1.0.jar" /> + </processorPath> + <module name="api-referentiel" /> + <module name="api-event-indicateurs" /> + <module name="common" /> + <module name="api-event-enrichissement" /> + <module name="api-rest-caculs" /> + <module name="calculs" /> + <module name="api-expositiondonneesentrees" /> + <module name="api-event-calculs" /> + <module name="api-event-donneesEntrees" /> + </profile> + </annotationProcessing> + <bytecodeTargetLevel> + <module name="api-event-calcul-indicateurs" target="17" /> + <module name="api-event-enrichissement" target="17" /> + <module name="api-expositionDonneesEntrees" target="17" /> + </bytecodeTargetLevel> + </component> + <component name="JavacSettings"> + <option name="ADDITIONAL_OPTIONS_OVERRIDE"> + <module name="api-event-calculs" options="-parameters" /> + <module name="api-event-donneesEntrees" options="-parameters" /> + <module name="api-expositiondonneesentrees" options="-parameters" /> + <module name="api-referentiel" options="-parameters" /> + <module name="api-rest-caculs" options="-parameters" /> + <module name="calculs" options="-parameters" /> + <module name="common" options="-parameters" /> + <module name="core" options="-parameters" /> + </option> + </component> +</project> \ No newline at end of file diff --git a/services/.workspace/.idea/encodings.xml b/services/.workspace/.idea/encodings.xml new file mode 100644 index 00000000..1351da80 --- /dev/null +++ b/services/.workspace/.idea/encodings.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="Encoding"> + <file url="file://$PROJECT_DIR$/../api-event-calculs/src/main/java" charset="UTF-8" /> + <file url="file://$PROJECT_DIR$/../api-event-calculs/target/generated-sources/src/gen/java" charset="UTF-8" /> + <file url="file://$PROJECT_DIR$/../api-event-donneesentrees/src/main/java" charset="UTF-8" /> + <file url="file://$PROJECT_DIR$/../api-expositiondonneesentrees/src/main/java" charset="UTF-8" /> + <file url="file://$PROJECT_DIR$/../api-expositiondonneesentrees/target/generated-sources/java" charset="UTF-8" /> + <file url="file://$PROJECT_DIR$/../api-expositiondonneesentrees/target/generated-sources/src/gen/java" charset="UTF-8" /> + <file url="file://$PROJECT_DIR$/../api-referentiel/src/main/java" charset="UTF-8" /> + <file url="file://$PROJECT_DIR$/../api-referentiel/src/main/resources" charset="UTF-8" /> + <file url="file://$PROJECT_DIR$/../calculs/src/main/java" charset="UTF-8" /> + <file url="file://$PROJECT_DIR$/../calculs/src/main/resources" charset="UTF-8" /> + <file url="file://$PROJECT_DIR$/../common/src/main/java" charset="UTF-8" /> + <file url="file://$PROJECT_DIR$/../common/src/main/resources" charset="UTF-8" /> + <file url="file://$PROJECT_DIR$/../common/target/generated-sources/src/gen/java" charset="UTF-8" /> + <file url="file://$PROJECT_DIR$/../core/src/main/java" charset="UTF-8" /> + <file url="file://$PROJECT_DIR$/../core/src/main/resources" charset="UTF-8" /> + </component> +</project> \ No newline at end of file diff --git a/services/.workspace/.idea/inspectionProfiles/Project_Default.xml b/services/.workspace/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 00000000..49881a89 --- /dev/null +++ b/services/.workspace/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,6 @@ +<component name="InspectionProjectProfileManager"> + <profile version="1.0"> + <option name="myName" value="Project Default" /> + <inspection_tool class="MavenCoroutinesDeprecation" enabled="false" level="ERROR" enabled_by_default="false" /> + </profile> +</component> \ No newline at end of file diff --git a/services/.workspace/.idea/jarRepositories.xml b/services/.workspace/.idea/jarRepositories.xml new file mode 100644 index 00000000..a6e9089d --- /dev/null +++ b/services/.workspace/.idea/jarRepositories.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="RemoteRepositoriesConfiguration"> + <remote-repository> + <option name="id" value="central" /> + <option name="name" value="Central Repository" /> + <option name="url" value="https://repo.maven.apache.org/maven2" /> + </remote-repository> + <remote-repository> + <option name="id" value="gitlab-maven" /> + <option name="name" value="gitlab-maven" /> + <option name="url" value="https://gitlab-forge.din.developpement-durable.gouv.fr/api/v4/groups/5389/-/packages/maven" /> + </remote-repository> + <remote-repository> + <option name="id" value="central" /> + <option name="name" value="Maven Central repository" /> + <option name="url" value="https://repo1.maven.org/maven2" /> + </remote-repository> + <remote-repository> + <option name="id" value="jboss.community" /> + <option name="name" value="JBoss Community repository" /> + <option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" /> + </remote-repository> + <remote-repository> + <option name="id" value="gitlab-maven" /> + <option name="name" value="gitlab-maven" /> + <option name="url" value="https://gitlab-forge.din.developpement-durable.gouv.fr/api/v4/projects/20519/packages/maven" /> + </remote-repository> + </component> +</project> \ No newline at end of file diff --git a/services/.workspace/.idea/misc.xml b/services/.workspace/.idea/misc.xml new file mode 100644 index 00000000..7363f2ad --- /dev/null +++ b/services/.workspace/.idea/misc.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="ExternalStorageConfigurationManager" enabled="true" /> + <component name="MavenProjectsManager"> + <option name="originalFiles"> + <list> + <option value="$PROJECT_DIR$/../api-event-calculs/pom.xml" /> + <option value="$PROJECT_DIR$/../common/pom.xml" /> + <option value="$PROJECT_DIR$/../core/pom.xml" /> + <option value="$PROJECT_DIR$/../api-event-donneesentrees/pom.xml" /> + <option value="$PROJECT_DIR$/../api-expositiondonneesentrees/pom.xml" /> + <option value="$PROJECT_DIR$/../calculs/pom.xml" /> + <option value="$PROJECT_DIR$/../api-referentiel/pom.xml" /> + </list> + </option> + <option name="workspaceImportForciblyTurnedOn" value="true" /> + </component> + <component name="ProjectRootManager" version="2" languageLevel="JDK_17" project-jdk-name="temurin-17" project-jdk-type="JavaSDK"> + <output url="file://$PROJECT_DIR$/out" /> + </component> +</project> \ No newline at end of file diff --git a/services/.workspace/.idea/modules.xml b/services/.workspace/.idea/modules.xml new file mode 100644 index 00000000..b4f71b36 --- /dev/null +++ b/services/.workspace/.idea/modules.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="ProjectModuleManager"> + <modules> + <module fileurl="file://$PROJECT_DIR$/../../e2e/e2e.iml" filepath="$PROJECT_DIR$/../../e2e/e2e.iml" /> + </modules> + </component> +</project> \ No newline at end of file diff --git a/services/.workspace/.idea/runConfigurations/ApiEventCalculsApplication.xml b/services/.workspace/.idea/runConfigurations/ApiEventCalculsApplication.xml new file mode 100644 index 00000000..91c2210a --- /dev/null +++ b/services/.workspace/.idea/runConfigurations/ApiEventCalculsApplication.xml @@ -0,0 +1,16 @@ +<component name="ProjectRunConfigurationManager"> + <configuration default="false" name="ApiEventCalculsApplication" type="Application" factoryName="Application" nameIsGenerated="true"> + <option name="MAIN_CLASS_NAME" value="org.mte.numecoeval.calculs.ApiEventCalculsApplication" /> + <module name="api-event-calculs" /> + <option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/../api-event-calculs" /> + <extension name="coverage"> + <pattern> + <option name="PATTERN" value="org.mte.numecoeval.calculs.*" /> + <option name="ENABLED" value="true" /> + </pattern> + </extension> + <method v="2"> + <option name="Make" enabled="true" /> + </method> + </configuration> +</component> \ No newline at end of file diff --git a/services/.workspace/.idea/runConfigurations/ApiEventDonneesEntreesApplication.xml b/services/.workspace/.idea/runConfigurations/ApiEventDonneesEntreesApplication.xml new file mode 100644 index 00000000..371f810d --- /dev/null +++ b/services/.workspace/.idea/runConfigurations/ApiEventDonneesEntreesApplication.xml @@ -0,0 +1,16 @@ +<component name="ProjectRunConfigurationManager"> + <configuration default="false" name="ApiEventDonneesEntreesApplication" type="Application" factoryName="Application" nameIsGenerated="true"> + <option name="MAIN_CLASS_NAME" value="org.mte.numecoeval.donneesentrees.ApiEventDonneesEntreesApplication" /> + <module name="api-event-donneesEntrees" /> + <option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/../api-event-donneesentrees" /> + <extension name="coverage"> + <pattern> + <option name="PATTERN" value="org.mte.numecoeval.donneesentrees.*" /> + <option name="ENABLED" value="true" /> + </pattern> + </extension> + <method v="2"> + <option name="Make" enabled="true" /> + </method> + </configuration> +</component> \ No newline at end of file diff --git a/services/.workspace/.idea/runConfigurations/ExpositionDonneesEntreesApplication.xml b/services/.workspace/.idea/runConfigurations/ExpositionDonneesEntreesApplication.xml new file mode 100644 index 00000000..1887099d --- /dev/null +++ b/services/.workspace/.idea/runConfigurations/ExpositionDonneesEntreesApplication.xml @@ -0,0 +1,16 @@ +<component name="ProjectRunConfigurationManager"> + <configuration default="false" name="ExpositionDonneesEntreesApplication" type="Application" factoryName="Application" nameIsGenerated="true"> + <option name="MAIN_CLASS_NAME" value="org.mte.numecoeval.expositiondonneesentrees.ExpositionDonneesEntreesApplication" /> + <module name="api-expositiondonneesentrees" /> + <option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/../api-expositiondonneesentrees" /> + <extension name="coverage"> + <pattern> + <option name="PATTERN" value="org.mte.numecoeval.expositiondonneesentrees.*" /> + <option name="ENABLED" value="true" /> + </pattern> + </extension> + <method v="2"> + <option name="Make" enabled="true" /> + </method> + </configuration> +</component> \ No newline at end of file diff --git a/services/.workspace/.idea/runConfigurations/Init.xml b/services/.workspace/.idea/runConfigurations/Init.xml new file mode 100644 index 00000000..298b23be --- /dev/null +++ b/services/.workspace/.idea/runConfigurations/Init.xml @@ -0,0 +1,10 @@ +<component name="ProjectRunConfigurationManager"> + <configuration default="false" name="Init" type="CompoundRunConfigurationType"> + <toRun name="api-event-calculs [gen-sources]" type="MavenRunConfiguration" /> + <toRun name="api-expositiondonneesentrees [gen-sources]" type="MavenRunConfiguration" /> + <toRun name="install calculs" type="MavenRunConfiguration" /> + <toRun name="install common" type="MavenRunConfiguration" /> + <toRun name="install core" type="MavenRunConfiguration" /> + <method v="2" /> + </configuration> +</component> \ No newline at end of file diff --git a/services/.workspace/.idea/runConfigurations/ReferentielApplication.xml b/services/.workspace/.idea/runConfigurations/ReferentielApplication.xml new file mode 100644 index 00000000..0ad2a37b --- /dev/null +++ b/services/.workspace/.idea/runConfigurations/ReferentielApplication.xml @@ -0,0 +1,16 @@ +<component name="ProjectRunConfigurationManager"> + <configuration default="false" name="ReferentielApplication" type="Application" factoryName="Application" nameIsGenerated="true"> + <option name="MAIN_CLASS_NAME" value="org.mte.numecoeval.referentiel.ReferentielApplication" /> + <module name="api-referentiel" /> + <option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/../api-referentiel" /> + <extension name="coverage"> + <pattern> + <option name="PATTERN" value="org.mte.numecoeval.referentiel.*" /> + <option name="ENABLED" value="true" /> + </pattern> + </extension> + <method v="2"> + <option name="Make" enabled="true" /> + </method> + </configuration> +</component> \ No newline at end of file diff --git a/services/.workspace/.idea/runConfigurations/Run.xml b/services/.workspace/.idea/runConfigurations/Run.xml new file mode 100644 index 00000000..02cda4ac --- /dev/null +++ b/services/.workspace/.idea/runConfigurations/Run.xml @@ -0,0 +1,9 @@ +<component name="ProjectRunConfigurationManager"> + <configuration default="false" name="Run" type="CompoundRunConfigurationType"> + <toRun name="ApiEventCalculsApplication" type="Application" /> + <toRun name="ApiEventDonneesEntreesApplication" type="Application" /> + <toRun name="ExpositionDonneesEntreesApplication" type="Application" /> + <toRun name="ReferentielApplication" type="Application" /> + <method v="2" /> + </configuration> +</component> \ No newline at end of file diff --git a/services/.workspace/.idea/runConfigurations/api_event_calculs__gen_sources_.xml b/services/.workspace/.idea/runConfigurations/api_event_calculs__gen_sources_.xml new file mode 100644 index 00000000..af7bfa9f --- /dev/null +++ b/services/.workspace/.idea/runConfigurations/api_event_calculs__gen_sources_.xml @@ -0,0 +1,47 @@ +<component name="ProjectRunConfigurationManager"> + <configuration default="false" name="api-event-calculs [gen-sources]" type="MavenRunConfiguration" factoryName="Maven"> + <MavenSettings> + <option name="myGeneralSettings" /> + <option name="myRunnerSettings"> + <MavenRunnerSettings> + <option name="delegateBuildToMaven" value="false" /> + <option name="environmentProperties"> + <map /> + </option> + <option name="jreName" value="temurin-17" /> + <option name="mavenProperties"> + <map /> + </option> + <option name="passParentEnv" value="true" /> + <option name="runMavenInBackground" value="true" /> + <option name="skipTests" value="false" /> + <option name="vmOptions" value="" /> + </MavenRunnerSettings> + </option> + <option name="myRunnerParameters"> + <MavenRunnerParameters> + <option name="cmdOptions" /> + <option name="profiles"> + <set /> + </option> + <option name="goals"> + <list> + <option value="generate-sources" /> + </list> + </option> + <option name="multimoduleDir" /> + <option name="pomFileName" /> + <option name="profilesMap"> + <map /> + </option> + <option name="projectsCmdOptionValues"> + <list /> + </option> + <option name="resolveToWorkspace" value="false" /> + <option name="workingDirPath" value="$PROJECT_DIR$/../api-event-calculs" /> + </MavenRunnerParameters> + </option> + </MavenSettings> + <method v="2" /> + </configuration> +</component> \ No newline at end of file diff --git a/services/.workspace/.idea/runConfigurations/api_expositiondonneesentrees__gen_sources_.xml b/services/.workspace/.idea/runConfigurations/api_expositiondonneesentrees__gen_sources_.xml new file mode 100644 index 00000000..89a394eb --- /dev/null +++ b/services/.workspace/.idea/runConfigurations/api_expositiondonneesentrees__gen_sources_.xml @@ -0,0 +1,47 @@ +<component name="ProjectRunConfigurationManager"> + <configuration default="false" name="api-expositiondonneesentrees [gen-sources]" type="MavenRunConfiguration" factoryName="Maven"> + <MavenSettings> + <option name="myGeneralSettings" /> + <option name="myRunnerSettings"> + <MavenRunnerSettings> + <option name="delegateBuildToMaven" value="false" /> + <option name="environmentProperties"> + <map /> + </option> + <option name="jreName" value="temurin-17" /> + <option name="mavenProperties"> + <map /> + </option> + <option name="passParentEnv" value="true" /> + <option name="runMavenInBackground" value="true" /> + <option name="skipTests" value="false" /> + <option name="vmOptions" value="" /> + </MavenRunnerSettings> + </option> + <option name="myRunnerParameters"> + <MavenRunnerParameters> + <option name="cmdOptions" /> + <option name="profiles"> + <set /> + </option> + <option name="goals"> + <list> + <option value="generate-sources" /> + </list> + </option> + <option name="multimoduleDir" /> + <option name="pomFileName" /> + <option name="profilesMap"> + <map /> + </option> + <option name="projectsCmdOptionValues"> + <list /> + </option> + <option name="resolveToWorkspace" value="false" /> + <option name="workingDirPath" value="$PROJECT_DIR$/../api-expositiondonneesentrees" /> + </MavenRunnerParameters> + </option> + </MavenSettings> + <method v="2" /> + </configuration> +</component> \ No newline at end of file diff --git a/services/.workspace/.idea/runConfigurations/install_calculs.xml b/services/.workspace/.idea/runConfigurations/install_calculs.xml new file mode 100644 index 00000000..d8b53028 --- /dev/null +++ b/services/.workspace/.idea/runConfigurations/install_calculs.xml @@ -0,0 +1,47 @@ +<component name="ProjectRunConfigurationManager"> + <configuration default="false" name="install calculs" type="MavenRunConfiguration" factoryName="Maven"> + <MavenSettings> + <option name="myGeneralSettings" /> + <option name="myRunnerSettings"> + <MavenRunnerSettings> + <option name="delegateBuildToMaven" value="false" /> + <option name="environmentProperties"> + <map /> + </option> + <option name="jreName" value="temurin-17" /> + <option name="mavenProperties"> + <map /> + </option> + <option name="passParentEnv" value="true" /> + <option name="runMavenInBackground" value="true" /> + <option name="skipTests" value="false" /> + <option name="vmOptions" value="" /> + </MavenRunnerSettings> + </option> + <option name="myRunnerParameters"> + <MavenRunnerParameters> + <option name="cmdOptions" /> + <option name="profiles"> + <set /> + </option> + <option name="goals"> + <list> + <option value="install" /> + </list> + </option> + <option name="multimoduleDir" /> + <option name="pomFileName" /> + <option name="profilesMap"> + <map /> + </option> + <option name="projectsCmdOptionValues"> + <list /> + </option> + <option name="resolveToWorkspace" value="false" /> + <option name="workingDirPath" value="$PROJECT_DIR$/../calculs" /> + </MavenRunnerParameters> + </option> + </MavenSettings> + <method v="2" /> + </configuration> +</component> \ No newline at end of file diff --git a/services/.workspace/.idea/runConfigurations/install_common.xml b/services/.workspace/.idea/runConfigurations/install_common.xml new file mode 100644 index 00000000..86ce6c84 --- /dev/null +++ b/services/.workspace/.idea/runConfigurations/install_common.xml @@ -0,0 +1,47 @@ +<component name="ProjectRunConfigurationManager"> + <configuration default="false" name="install common" type="MavenRunConfiguration" factoryName="Maven"> + <MavenSettings> + <option name="myGeneralSettings" /> + <option name="myRunnerSettings"> + <MavenRunnerSettings> + <option name="delegateBuildToMaven" value="false" /> + <option name="environmentProperties"> + <map /> + </option> + <option name="jreName" value="temurin-17" /> + <option name="mavenProperties"> + <map /> + </option> + <option name="passParentEnv" value="true" /> + <option name="runMavenInBackground" value="true" /> + <option name="skipTests" value="false" /> + <option name="vmOptions" value="" /> + </MavenRunnerSettings> + </option> + <option name="myRunnerParameters"> + <MavenRunnerParameters> + <option name="cmdOptions" /> + <option name="profiles"> + <set /> + </option> + <option name="goals"> + <list> + <option value="install" /> + </list> + </option> + <option name="multimoduleDir" /> + <option name="pomFileName" /> + <option name="profilesMap"> + <map /> + </option> + <option name="projectsCmdOptionValues"> + <list /> + </option> + <option name="resolveToWorkspace" value="false" /> + <option name="workingDirPath" value="$PROJECT_DIR$/../common" /> + </MavenRunnerParameters> + </option> + </MavenSettings> + <method v="2" /> + </configuration> +</component> \ No newline at end of file diff --git a/services/.workspace/.idea/runConfigurations/install_core.xml b/services/.workspace/.idea/runConfigurations/install_core.xml new file mode 100644 index 00000000..104f7ef6 --- /dev/null +++ b/services/.workspace/.idea/runConfigurations/install_core.xml @@ -0,0 +1,47 @@ +<component name="ProjectRunConfigurationManager"> + <configuration default="false" name="install core" type="MavenRunConfiguration" factoryName="Maven"> + <MavenSettings> + <option name="myGeneralSettings" /> + <option name="myRunnerSettings"> + <MavenRunnerSettings> + <option name="delegateBuildToMaven" value="false" /> + <option name="environmentProperties"> + <map /> + </option> + <option name="jreName" value="temurin-17" /> + <option name="mavenProperties"> + <map /> + </option> + <option name="passParentEnv" value="true" /> + <option name="runMavenInBackground" value="true" /> + <option name="skipTests" value="false" /> + <option name="vmOptions" value="" /> + </MavenRunnerSettings> + </option> + <option name="myRunnerParameters"> + <MavenRunnerParameters> + <option name="cmdOptions" /> + <option name="profiles"> + <set /> + </option> + <option name="goals"> + <list> + <option value="install" /> + </list> + </option> + <option name="multimoduleDir" /> + <option name="pomFileName" /> + <option name="profilesMap"> + <map /> + </option> + <option name="projectsCmdOptionValues"> + <list /> + </option> + <option name="resolveToWorkspace" value="false" /> + <option name="workingDirPath" value="$PROJECT_DIR$/../core" /> + </MavenRunnerParameters> + </option> + </MavenSettings> + <method v="2" /> + </configuration> +</component> \ No newline at end of file diff --git a/services/.workspace/.idea/saveactions_settings.xml b/services/.workspace/.idea/saveactions_settings.xml new file mode 100644 index 00000000..898dbf85 --- /dev/null +++ b/services/.workspace/.idea/saveactions_settings.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="SaveActionSettings"> + <option name="actions"> + <set> + <option value="activate" /> + <option value="organizeImports" /> + <option value="reformat" /> + </set> + </option> + <option name="configurationPath" value="" /> + <option name="exclusions"> + <set> + <option value=".*\.yaml" /> + <option value=".*\.yml" /> + </set> + </option> + </component> +</project> \ No newline at end of file diff --git a/services/.workspace/.idea/uiDesigner.xml b/services/.workspace/.idea/uiDesigner.xml new file mode 100644 index 00000000..2b63946d --- /dev/null +++ b/services/.workspace/.idea/uiDesigner.xml @@ -0,0 +1,124 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="Palette2"> + <group name="Swing"> + <item class="com.intellij.uiDesigner.HSpacer" tooltip-text="Horizontal Spacer" icon="/com/intellij/uiDesigner/icons/hspacer.svg" removable="false" auto-create-binding="false" can-attach-label="false"> + <default-constraints vsize-policy="1" hsize-policy="6" anchor="0" fill="1" /> + </item> + <item class="com.intellij.uiDesigner.VSpacer" tooltip-text="Vertical Spacer" icon="/com/intellij/uiDesigner/icons/vspacer.svg" removable="false" auto-create-binding="false" can-attach-label="false"> + <default-constraints vsize-policy="6" hsize-policy="1" anchor="0" fill="2" /> + </item> + <item class="javax.swing.JPanel" icon="/com/intellij/uiDesigner/icons/panel.svg" removable="false" auto-create-binding="false" can-attach-label="false"> + <default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3" /> + </item> + <item class="javax.swing.JScrollPane" icon="/com/intellij/uiDesigner/icons/scrollPane.svg" removable="false" auto-create-binding="false" can-attach-label="true"> + <default-constraints vsize-policy="7" hsize-policy="7" anchor="0" fill="3" /> + </item> + <item class="javax.swing.JButton" icon="/com/intellij/uiDesigner/icons/button.svg" removable="false" auto-create-binding="true" can-attach-label="false"> + <default-constraints vsize-policy="0" hsize-policy="3" anchor="0" fill="1" /> + <initial-values> + <property name="text" value="Button" /> + </initial-values> + </item> + <item class="javax.swing.JRadioButton" icon="/com/intellij/uiDesigner/icons/radioButton.svg" removable="false" auto-create-binding="true" can-attach-label="false"> + <default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" /> + <initial-values> + <property name="text" value="RadioButton" /> + </initial-values> + </item> + <item class="javax.swing.JCheckBox" icon="/com/intellij/uiDesigner/icons/checkBox.svg" removable="false" auto-create-binding="true" can-attach-label="false"> + <default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" /> + <initial-values> + <property name="text" value="CheckBox" /> + </initial-values> + </item> + <item class="javax.swing.JLabel" icon="/com/intellij/uiDesigner/icons/label.svg" removable="false" auto-create-binding="false" can-attach-label="false"> + <default-constraints vsize-policy="0" hsize-policy="0" anchor="8" fill="0" /> + <initial-values> + <property name="text" value="Label" /> + </initial-values> + </item> + <item class="javax.swing.JTextField" icon="/com/intellij/uiDesigner/icons/textField.svg" removable="false" auto-create-binding="true" can-attach-label="true"> + <default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1"> + <preferred-size width="150" height="-1" /> + </default-constraints> + </item> + <item class="javax.swing.JPasswordField" icon="/com/intellij/uiDesigner/icons/passwordField.svg" removable="false" auto-create-binding="true" can-attach-label="true"> + <default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1"> + <preferred-size width="150" height="-1" /> + </default-constraints> + </item> + <item class="javax.swing.JFormattedTextField" icon="/com/intellij/uiDesigner/icons/formattedTextField.svg" removable="false" auto-create-binding="true" can-attach-label="true"> + <default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1"> + <preferred-size width="150" height="-1" /> + </default-constraints> + </item> + <item class="javax.swing.JTextArea" icon="/com/intellij/uiDesigner/icons/textArea.svg" removable="false" auto-create-binding="true" can-attach-label="true"> + <default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3"> + <preferred-size width="150" height="50" /> + </default-constraints> + </item> + <item class="javax.swing.JTextPane" icon="/com/intellij/uiDesigner/icons/textPane.svg" removable="false" auto-create-binding="true" can-attach-label="true"> + <default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3"> + <preferred-size width="150" height="50" /> + </default-constraints> + </item> + <item class="javax.swing.JEditorPane" icon="/com/intellij/uiDesigner/icons/editorPane.svg" removable="false" auto-create-binding="true" can-attach-label="true"> + <default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3"> + <preferred-size width="150" height="50" /> + </default-constraints> + </item> + <item class="javax.swing.JComboBox" icon="/com/intellij/uiDesigner/icons/comboBox.svg" removable="false" auto-create-binding="true" can-attach-label="true"> + <default-constraints vsize-policy="0" hsize-policy="2" anchor="8" fill="1" /> + </item> + <item class="javax.swing.JTable" icon="/com/intellij/uiDesigner/icons/table.svg" removable="false" auto-create-binding="true" can-attach-label="false"> + <default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3"> + <preferred-size width="150" height="50" /> + </default-constraints> + </item> + <item class="javax.swing.JList" icon="/com/intellij/uiDesigner/icons/list.svg" removable="false" auto-create-binding="true" can-attach-label="false"> + <default-constraints vsize-policy="6" hsize-policy="2" anchor="0" fill="3"> + <preferred-size width="150" height="50" /> + </default-constraints> + </item> + <item class="javax.swing.JTree" icon="/com/intellij/uiDesigner/icons/tree.svg" removable="false" auto-create-binding="true" can-attach-label="false"> + <default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3"> + <preferred-size width="150" height="50" /> + </default-constraints> + </item> + <item class="javax.swing.JTabbedPane" icon="/com/intellij/uiDesigner/icons/tabbedPane.svg" removable="false" auto-create-binding="true" can-attach-label="false"> + <default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3"> + <preferred-size width="200" height="200" /> + </default-constraints> + </item> + <item class="javax.swing.JSplitPane" icon="/com/intellij/uiDesigner/icons/splitPane.svg" removable="false" auto-create-binding="false" can-attach-label="false"> + <default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3"> + <preferred-size width="200" height="200" /> + </default-constraints> + </item> + <item class="javax.swing.JSpinner" icon="/com/intellij/uiDesigner/icons/spinner.svg" removable="false" auto-create-binding="true" can-attach-label="true"> + <default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" /> + </item> + <item class="javax.swing.JSlider" icon="/com/intellij/uiDesigner/icons/slider.svg" removable="false" auto-create-binding="true" can-attach-label="false"> + <default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" /> + </item> + <item class="javax.swing.JSeparator" icon="/com/intellij/uiDesigner/icons/separator.svg" removable="false" auto-create-binding="false" can-attach-label="false"> + <default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3" /> + </item> + <item class="javax.swing.JProgressBar" icon="/com/intellij/uiDesigner/icons/progressbar.svg" removable="false" auto-create-binding="true" can-attach-label="false"> + <default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1" /> + </item> + <item class="javax.swing.JToolBar" icon="/com/intellij/uiDesigner/icons/toolbar.svg" removable="false" auto-create-binding="false" can-attach-label="false"> + <default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1"> + <preferred-size width="-1" height="20" /> + </default-constraints> + </item> + <item class="javax.swing.JToolBar$Separator" icon="/com/intellij/uiDesigner/icons/toolbarSeparator.svg" removable="false" auto-create-binding="false" can-attach-label="false"> + <default-constraints vsize-policy="0" hsize-policy="0" anchor="0" fill="1" /> + </item> + <item class="javax.swing.JScrollBar" icon="/com/intellij/uiDesigner/icons/scrollbar.svg" removable="false" auto-create-binding="true" can-attach-label="false"> + <default-constraints vsize-policy="6" hsize-policy="0" anchor="0" fill="2" /> + </item> + </group> + </component> +</project> \ No newline at end of file diff --git a/services/.workspace/.idea/vcs.xml b/services/.workspace/.idea/vcs.xml new file mode 100644 index 00000000..b2bdec2d --- /dev/null +++ b/services/.workspace/.idea/vcs.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="VcsDirectoryMappings"> + <mapping directory="$PROJECT_DIR$/../.." vcs="Git" /> + </component> +</project> \ No newline at end of file diff --git a/services/.workspace/settings.xml b/services/.workspace/settings.xml new file mode 100644 index 00000000..9a2cc6f3 --- /dev/null +++ b/services/.workspace/settings.xml @@ -0,0 +1,19 @@ +<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 https://maven.apache.org/xsd/settings-1.0.0.xsd"> + <localRepository/> + <interactiveMode/> + <offline/> + <pluginGroups/> + <servers/> + <mirrors/> + <proxies> + <!--<proxy> + <id>proxy</id> + <active>true</active> + <protocol>http</protocol> + <host>host</host> + <port>8080</port> + </proxy>--> + </proxies> + <activeProfiles/> +</settings> \ No newline at end of file diff --git a/services/api-event-calculs/.gitignore b/services/api-event-calculs/.gitignore new file mode 100644 index 00000000..549e00a2 --- /dev/null +++ b/services/api-event-calculs/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/services/api-event-calculs/LICENSE.txt b/services/api-event-calculs/LICENSE.txt new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/services/api-event-calculs/LICENSE.txt @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/services/api-event-calculs/README.md b/services/api-event-calculs/README.md new file mode 100644 index 00000000..09cb1504 --- /dev/null +++ b/services/api-event-calculs/README.md @@ -0,0 +1,2 @@ +# api-event-calculs + diff --git a/services/api-event-calculs/dependency_check_suppressions.xml b/services/api-event-calculs/dependency_check_suppressions.xml new file mode 100644 index 00000000..15f53bbc --- /dev/null +++ b/services/api-event-calculs/dependency_check_suppressions.xml @@ -0,0 +1,97 @@ +<?xml version="1.0" encoding="UTF-8"?> +<suppressions xmlns="https://jeremylong.github.io/DependencyCheck/dependency-suppression.1.1.xsd"> + + <suppress> + <notes><![CDATA[ + file name: spring-security-crypto-5.7.3.jar + La librairie Spring Security est en version 5.7.3. + Cette CVE est marquée uniquement jusqu'à la version 5.2.4 (exclus) + https://nvd.nist.gov/vuln/detail/CVE-2020-5408 + ]]></notes> + <cve>CVE-2020-5408</cve> + </suppress> + + <suppress> + <notes><![CDATA[ + file name: spring-web-5.3.22.jar + Dans notre contexte, nous ne recevons pas de code Java de l'extérieur et n'effectuons pas de dé-sérialisation de code Java + Côté Spring, le point est déjà remonté comme un faux-positif : https://github.com/spring-projects/spring-framework/issues/24434#issuecomment-744519525 + ]]></notes> + <cve>CVE-2016-1000027</cve> + </suppress> + + <suppress> + <notes><![CDATA[ + file name: snakeyaml-1.33.jar + Faux-positif : la version de Snakeyaml est la 1.33, la CVE existe uniquement sur les versions < 1.32 + https://nvd.nist.gov/vuln/detail/CVE-2022-38752 + ]]></notes> + <cve>CVE-2022-38752</cve> + </suppress> + + <suppress> + <notes><![CDATA[ + Faux-positif sur toutes les librairies utils: https://github.com/jeremylong/DependencyCheck/issues/5213 + Concerne : software.amazon.awssdk:utils qui n'est pas utilisé + ]]></notes> + <cve>CVE-2021-4277</cve> + </suppress> + + <suppress> + <notes><![CDATA[ + Faux-positif : matchent à tort sur tous les commons : https://github.com/jeremylong/DependencyCheck/issues/5132 + ]]></notes> + <cve>CVE-2021-37533</cve> + </suppress> + + <suppress> + <notes><![CDATA[ + Non-applicable : Snakeyaml n'est utilisé que par Spring Boot pour les fichiers application.yml + et non des fichiers externes. + ]]></notes> + <cve>CVE-2022-1471</cve> + </suppress> + + <suppress> + <notes><![CDATA[ + Non-applicable : Snakeyaml n'est utilisé que par Spring Boot pour les fichiers application.yml + et non des fichiers externes. + ]]></notes> + <cve>CVE-2022-3064</cve> + </suppress> + + <suppress> + <notes><![CDATA[ + Non-applicable : Snakeyaml n'est utilisé que par Spring Boot pour les fichiers application.yml + et non des fichiers externes. + ]]></notes> + <cve>CVE-2021-4235</cve> + </suppress> + + <suppress> + <notes><![CDATA[ + Non-applicable : json-smart est une dépendance transitive. + Les json sont limités à la taille des inventaires : la taille des listes n'a pas réellement de limite + et c'est souhaités comme cela (traitement de gros volumes de données). + ]]></notes> + <cve>CVE-2023-1370</cve> + </suppress> + + <suppress> + <notes><![CDATA[ + Faux-Positif : Concerne hutool-json et json-java qui ne sont pas utilisés. + Le faux-positif se trouve sur json-path, jackson-core, accessors-smart et json-smart. + cf. https://github.com/jeremylong/DependencyCheck/issues/5502 + ]]></notes> + <cve>CVE-2022-45688</cve> + </suppress> + + + <suppress> + <notes><![CDATA[ + Faux-Positif : Conformément au lien, nous ne sommes pas dans un des cas de vulnérabilité. + cf. cf. https://github.com/jeremylong/DependencyCheck/issues/5502 + ]]></notes> + <cve>CVE-2023-20862</cve> + </suppress> +</suppressions> diff --git a/services/api-event-calculs/pom.xml b/services/api-event-calculs/pom.xml new file mode 100644 index 00000000..a8e43c63 --- /dev/null +++ b/services/api-event-calculs/pom.xml @@ -0,0 +1,288 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.mte.numecoeval</groupId> + <artifactId>core</artifactId> + <version>1.2.3-SNAPSHOT</version> + <relativePath/> <!-- lookup parent from repository --> + </parent> + <artifactId>api-event-calculs</artifactId> + <name>api-event-calculs</name> + <version>1.2.3-SNAPSHOT</version> + <description>api-event-calculs</description> + + <repositories> + <repository> + <id>gitlab-maven</id> + <url>https://gitlab-forge.din.developpement-durable.gouv.fr/api/v4/projects/20519/packages/maven</url> + <releases> + <enabled>true</enabled> + </releases> + <snapshots> + <enabled>true</enabled> + </snapshots> + </repository> + </repositories> + + <distributionManagement> + <repository> + <id>gitlab-maven</id> + <url>https://gitlab-forge.din.developpement-durable.gouv.fr/api/v4/projects/20519/packages/maven</url> + </repository> + + <snapshotRepository> + <id>gitlab-maven</id> + <url>https://gitlab-forge.din.developpement-durable.gouv.fr/api/v4/projects/20519/packages/maven</url> + </snapshotRepository> + </distributionManagement> + + <dependencies> + <dependency> + <groupId>org.mte.numecoeval</groupId> + <artifactId>common</artifactId> + </dependency> + <dependency> + <groupId>org.mte.numecoeval</groupId> + <artifactId>calculs</artifactId> + <version>1.2.3-SNAPSHOT</version> + </dependency> + + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-jdbc</artifactId> + </dependency> + <dependency> + <groupId>org.postgresql</groupId> + <artifactId>postgresql</artifactId> + <scope>runtime</scope> + </dependency> + + <dependency> + <groupId>org.springframework.kafka</groupId> + <artifactId>spring-kafka</artifactId> + </dependency> + + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-web</artifactId> + </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-webflux</artifactId> + </dependency> + + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-cache</artifactId> + </dependency> + + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-validation</artifactId> + </dependency> + + <!-- Security --> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-security</artifactId> + </dependency> + + <!-- Mapping --> + <dependency> + <groupId>org.mapstruct</groupId> + <artifactId>mapstruct</artifactId> + </dependency> + + <!-- Utilitaire --> + <dependency> + <groupId>org.apache.commons</groupId> + <artifactId>commons-lang3</artifactId> + </dependency> + <dependency> + <groupId>org.projectlombok</groupId> + <artifactId>lombok</artifactId> + <optional>true</optional> + </dependency> + + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-actuator</artifactId> + </dependency> + <dependency> + <groupId>io.micrometer</groupId> + <artifactId>micrometer-registry-prometheus</artifactId> + <scope>runtime</scope> + </dependency> + + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-test</artifactId> + <scope>test</scope> + </dependency> + + <!-- Swagger-UI --> + <dependency> + <groupId>org.springdoc</groupId> + <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId> + </dependency> + </dependencies> + + <build> + <plugins> + <plugin> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-maven-plugin</artifactId> + <configuration> + <excludes> + <exclude> + <groupId>org.projectlombok</groupId> + <artifactId>lombok</artifactId> + </exclude> + </excludes> + </configuration> + </plugin> + + <plugin> + <groupId>org.openapitools</groupId> + <artifactId>openapi-generator-maven-plugin</artifactId> + <version>${openapi-generator-version}</version> + <executions> + <execution> + <id>generation_client_referentiel</id> + <goals> + <goal>generate</goal> + </goals> + <configuration> + <inputSpec> + ${project.basedir}/../common/src/main/resources/static/api-referentiels-openapi.yaml + </inputSpec> + <output>${project.build.directory}/generated-sources</output> + <modelPackage>org.mte.numecoeval.calculs.referentiels.generated.api.model</modelPackage> + <apiPackage>org.mte.numecoeval.calculs.referentiels.generated.api.client</apiPackage> + <invokerPackage>org.mte.numecoeval.calculs.referentiels.generated.api.invoker + </invokerPackage> + <generateApis>true</generateApis> + <generateModels>true</generateModels> + <generateModelTests>false</generateModelTests> + <generateApiTests>false</generateApiTests> + <generateSupportingFiles>true</generateSupportingFiles> + <generateModelDocumentation>false</generateModelDocumentation> + <generateApiDocumentation>false</generateApiDocumentation> + <generatorName>java</generatorName> + <library>webclient</library> + <configOptions> + <useJakartaEe>true</useJakartaEe> + <useTags>true</useTags> + <skipDefaultInterface>true</skipDefaultInterface> + <dateLibrary>java8</dateLibrary> + <useSpringBoot3>true</useSpringBoot3> + <sourceFolder>src/gen/java</sourceFolder> + <serializableModel>true</serializableModel> + <interfaceOnly>true</interfaceOnly> + <reactive>false</reactive> + <useBeanValidation>true</useBeanValidation> + <performBeanValidation>true</performBeanValidation> + <useOptional>false</useOptional> + <serviceInterface>true</serviceInterface> + <serviceImplementation>true</serviceImplementation> + <booleanGetterPrefix>is</booleanGetterPrefix> + </configOptions> + </configuration> + </execution> + <execution> + <id>generation_calculs_rest</id> + <goals> + <goal>generate</goal> + </goals> + <configuration> + <inputSpec> + ${project.basedir}/../common/src/main/resources/static/api-event-calculs-async-openapi.yaml + </inputSpec> + <output>${project.build.directory}/generated-sources</output> + <modelPackage>org.mte.numecoeval.calculs.rest.generated.api.model</modelPackage> + <apiPackage>org.mte.numecoeval.calculs.rest.generated.api.server</apiPackage> + <invokerPackage>org.mte.numecoeval.calculs.rest.generated.api.invoker</invokerPackage> + <generateApis>true</generateApis> + <generateModels>true</generateModels> + <generateModelTests>false</generateModelTests> + <generateApiTests>false</generateApiTests> + <generateSupportingFiles>false</generateSupportingFiles> + <generateModelDocumentation>false</generateModelDocumentation> + <generateApiDocumentation>false</generateApiDocumentation> + <generatorName>spring</generatorName> + <library>spring-boot</library> + <configOptions> + <useTags>true</useTags> + <skipDefaultInterface>true</skipDefaultInterface> + <dateLibrary>java8</dateLibrary> + <useSpringBoot3>true</useSpringBoot3> + <sourceFolder>src/gen/java</sourceFolder> + <serializableModel>true</serializableModel> + <interfaceOnly>true</interfaceOnly> + <dateLibrary>java8-localdatetime</dateLibrary> + <reactive>false</reactive> + <useBeanValidation>true</useBeanValidation> + <performBeanValidation>true</performBeanValidation> + <useOptional>false</useOptional> + <serviceInterface>true</serviceInterface> + <serviceImplementation>false</serviceImplementation> + <booleanGetterPrefix>is</booleanGetterPrefix> + <additionalModelTypeAnnotations> + @lombok.AllArgsConstructor;@lombok.Builder;@lombok.NoArgsConstructor + </additionalModelTypeAnnotations> + </configOptions> + </configuration> + </execution> + <execution> + <id>generation_sync_calculs_rest</id> + <goals> + <goal>generate</goal> + </goals> + <configuration> + <inputSpec> + ${project.basedir}/../common/src/main/resources/static/api-event-calculs-sync-openapi.yaml + </inputSpec> + <output>${project.build.directory}/generated-sources</output> + <modelPackage>org.mte.numecoeval.calculs.sync.generated.api.model</modelPackage> + <apiPackage>org.mte.numecoeval.calculs.sync.generated.api.server</apiPackage> + <invokerPackage>org.mte.numecoeval.calculs.sync.generated.api.invoker</invokerPackage> + <generateApis>true</generateApis> + <generateModels>true</generateModels> + <generateModelTests>false</generateModelTests> + <generateApiTests>false</generateApiTests> + <generateSupportingFiles>false</generateSupportingFiles> + <generateModelDocumentation>false</generateModelDocumentation> + <generateApiDocumentation>false</generateApiDocumentation> + <generatorName>spring</generatorName> + <library>spring-boot</library> + <configOptions> + <useTags>true</useTags> + <skipDefaultInterface>true</skipDefaultInterface> + <dateLibrary>java8</dateLibrary> + <useSpringBoot3>true</useSpringBoot3> + <sourceFolder>src/gen/java</sourceFolder> + <serializableModel>true</serializableModel> + <interfaceOnly>true</interfaceOnly> + <dateLibrary>java8-localdatetime</dateLibrary> + <reactive>false</reactive> + <useBeanValidation>true</useBeanValidation> + <performBeanValidation>true</performBeanValidation> + <useOptional>false</useOptional> + <serviceInterface>true</serviceInterface> + <serviceImplementation>false</serviceImplementation> + <booleanGetterPrefix>is</booleanGetterPrefix> + <additionalModelTypeAnnotations> + @lombok.AllArgsConstructor;@lombok.Builder;@lombok.NoArgsConstructor + </additionalModelTypeAnnotations> + </configOptions> + </configuration> + </execution> + </executions> + </plugin> + </plugins> + </build> + + +</project> diff --git a/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/ApiEventCalculsApplication.java b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/ApiEventCalculsApplication.java new file mode 100644 index 00000000..83f4e5af --- /dev/null +++ b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/ApiEventCalculsApplication.java @@ -0,0 +1,13 @@ +package org.mte.numecoeval.calculs; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class ApiEventCalculsApplication { + + public static void main(String[] args) { + SpringApplication.run(ApiEventCalculsApplication.class, args); + } + +} diff --git a/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/domain/exception/DatabaseException.java b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/domain/exception/DatabaseException.java new file mode 100644 index 00000000..76f005a7 --- /dev/null +++ b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/domain/exception/DatabaseException.java @@ -0,0 +1,18 @@ +package org.mte.numecoeval.calculs.domain.exception; + +public class DatabaseException extends RuntimeException { + + /** + * Constructor with no arg + */ + public DatabaseException() { + super(); + } + + /** + * Constructor with cause + */ + public DatabaseException(Throwable cause) { + super(cause); + } +} diff --git a/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/domain/exception/ExternalApiException.java b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/domain/exception/ExternalApiException.java new file mode 100644 index 00000000..48432418 --- /dev/null +++ b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/domain/exception/ExternalApiException.java @@ -0,0 +1,18 @@ +package org.mte.numecoeval.calculs.domain.exception; + +public class ExternalApiException extends RuntimeException { + + /** + * Constructor with no arg + */ + public ExternalApiException() { + super(); + } + + /** + * Constructor with cause + */ + public ExternalApiException(Throwable cause) { + super(cause); + } +} diff --git a/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/domain/model/CalculEquipementPhysique.java b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/domain/model/CalculEquipementPhysique.java new file mode 100644 index 00000000..170b5cd6 --- /dev/null +++ b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/domain/model/CalculEquipementPhysique.java @@ -0,0 +1,24 @@ +package org.mte.numecoeval.calculs.domain.model; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.mte.numecoeval.calculs.referentiels.generated.api.model.*; +import org.mte.numecoeval.topic.data.EquipementPhysiqueDTO; + +import java.util.List; + +@NoArgsConstructor +@Getter +@Setter +public class CalculEquipementPhysique { + private List<EtapeDTO> etapes; + private List<CritereDTO> criteres; + private List<HypotheseDTO> hypotheses; + private CorrespondanceRefEquipementDTO correspondanceRefEquipement; + private EquipementPhysiqueDTO equipementPhysique; + private TypeEquipementDTO typeEquipement; + private List<MixElectriqueDTO> mixElectriques; + private List<ImpactReseauDTO> impactsReseau; + private List<ImpactEquipementDTO> impactsEquipement; +} \ No newline at end of file diff --git a/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/domain/model/CalculMessagerie.java b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/domain/model/CalculMessagerie.java new file mode 100644 index 00000000..c4ff1d57 --- /dev/null +++ b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/domain/model/CalculMessagerie.java @@ -0,0 +1,20 @@ +package org.mte.numecoeval.calculs.domain.model; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.mte.numecoeval.calculs.referentiels.generated.api.model.CritereDTO; +import org.mte.numecoeval.calculs.referentiels.generated.api.model.ImpactMessagerieDTO; +import org.mte.numecoeval.topic.data.MessagerieDTO; + +import java.util.List; + +@NoArgsConstructor +@Getter +@Setter +public class CalculMessagerie { + private MessagerieDTO messagerie; + private List<CritereDTO> criteres; + private List<ImpactMessagerieDTO> impactsMessagerie; +} + diff --git a/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/domain/model/CalculSizes.java b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/domain/model/CalculSizes.java new file mode 100644 index 00000000..bd90d22d --- /dev/null +++ b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/domain/model/CalculSizes.java @@ -0,0 +1,16 @@ +package org.mte.numecoeval.calculs.domain.model; + +import lombok.Data; + +@Data +public class CalculSizes { + private long nbEquipementPhysique; + private long nbEquipementVirtuel; + private long nbApplication; + private long nbMessagerie; + private long nbIndicateurEquipementPhysique; + private long nbIndicateurEquipementVirtuel; + private long nbIndicateurApplication; + private long nbIndicateurReseau; + private long nbIndicateurMessagerie; +} diff --git a/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/client/ReferentielClient.java b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/client/ReferentielClient.java new file mode 100644 index 00000000..92f1d225 --- /dev/null +++ b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/client/ReferentielClient.java @@ -0,0 +1,133 @@ +package org.mte.numecoeval.calculs.infrastructure.client; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.mte.numecoeval.calculs.domain.exception.ExternalApiException; +import org.mte.numecoeval.calculs.referentiels.generated.api.client.InterneNumEcoEvalApi; +import org.mte.numecoeval.calculs.referentiels.generated.api.model.*; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.http.HttpStatusCode; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClientResponseException; + +import java.util.List; + +@Slf4j +@Service +@AllArgsConstructor +public class ReferentielClient { + + private InterneNumEcoEvalApi interneNumEcoEvalApi; + + @Cacheable("Etapes") + public List<EtapeDTO> getEtapes() { + + try { + var result = interneNumEcoEvalApi.getAllEtapes().collectList().block(); + return result == null ? List.of() : result; + } catch (WebClientResponseException e) { + if (e.getStatusCode() != HttpStatusCode.valueOf(404)) { + throw new ExternalApiException(e); + } + } + return List.of(); + } + + @Cacheable("Criteres") + public List<CritereDTO> getCriteres() { + try { + var result = interneNumEcoEvalApi.getAllCriteres().collectList().block(); + return result == null ? List.of() : result; + } catch (WebClientResponseException e) { + if (e.getStatusCode() != HttpStatusCode.valueOf(404)) { + throw new ExternalApiException(e); + } + } + return List.of(); + } + + @Cacheable(value = "Hypothese") + public HypotheseDTO getHypothese(String code) { + try { + return interneNumEcoEvalApi.getHypothese(code).block(); + } catch (WebClientResponseException e) { + if (e.getStatusCode() != HttpStatusCode.valueOf(404)) { + throw new ExternalApiException(e); + } + } + return null; + } + + @Cacheable(value = "CorrespondanceRefEquipement") + public CorrespondanceRefEquipementDTO getCorrespondanceRefEquipement(String modele) { + try { + return interneNumEcoEvalApi.getCorrespondanceRefEquipement(modele).block(); + } catch (WebClientResponseException e) { + if (e.getStatusCode() != HttpStatusCode.valueOf(404)) { + throw new ExternalApiException(e); + } + } + return null; + } + + @Cacheable(value = "TypeEquipement") + public TypeEquipementDTO getTypeEquipement(String type) { + try { + return interneNumEcoEvalApi.getTypeEquipement(type).block(); + } catch (WebClientResponseException e) { + if (e.getStatusCode() != HttpStatusCode.valueOf(404)) { + throw new ExternalApiException(e); + } + } + return null; + } + + @Cacheable(value = "ImpactEquipement") + public ImpactEquipementDTO getImpactEquipement(String refEquipement, String critere, String etape) { + try { + return interneNumEcoEvalApi.getImpactEquipement(refEquipement, critere, etape).block(); + } catch (WebClientResponseException e) { + if (e.getStatusCode() != HttpStatusCode.valueOf(404)) { + throw new ExternalApiException(e); + } + } + return null; + } + + @Cacheable(value = "ImpactReseau") + public ImpactReseauDTO getImpactReseau(String refReseau, String critere, String etape) { + try { + return interneNumEcoEvalApi.getImpactReseau(refReseau, critere, etape).block(); + } catch (WebClientResponseException e) { + if (e.getStatusCode() != HttpStatusCode.valueOf(404)) { + throw new ExternalApiException(e); + } + } + return null; + } + + @Cacheable(value = "MixElectrique") + public MixElectriqueDTO getMixElectrique(String pays, String critere) { + if (pays == null || critere == null) return null; + try { + return interneNumEcoEvalApi.getMixElectrique(pays, critere).block(); + } catch (WebClientResponseException e) { + if (e.getStatusCode() != HttpStatusCode.valueOf(404)) { + throw new ExternalApiException(e); + } + } + return null; + } + + @Cacheable(value = "ImpactMessagerie") + public ImpactMessagerieDTO getMessagerie(String critere) { + try { + return interneNumEcoEvalApi.getImpactMessagerie(critere).block(); + } catch (WebClientResponseException e) { + if (e.getStatusCode() != HttpStatusCode.valueOf(404)) { + throw new ExternalApiException(e); + } + } + return null; + } +} diff --git a/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/config/CacheConfig.java b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/config/CacheConfig.java new file mode 100644 index 00000000..ab24eb9d --- /dev/null +++ b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/config/CacheConfig.java @@ -0,0 +1,35 @@ +package org.mte.numecoeval.calculs.infrastructure.config; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.annotation.Scheduled; + +import java.util.concurrent.TimeUnit; + +@Slf4j +@EnableCaching +@EnableScheduling +@Configuration +@Profile("!test") +public class CacheConfig { + + @CacheEvict(value = { + "Etapes", + "Criteres", + "Hypothese", + "TypeEquipement", + "CorrespondanceRefEquipement", + "ImpactEquipement", + "ImpactReseau", + "MixElectrique", + "ImpactMessagerie", + }, allEntries = true) + @Scheduled(fixedRateString = "${numecoeval.cache.ttl}", timeUnit = TimeUnit.MINUTES) + public void emptyAllCaches() { + log.info("Nettoyage de tous les caches internes"); + } +} diff --git a/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/config/CommonIntegrationConfig.java b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/config/CommonIntegrationConfig.java new file mode 100644 index 00000000..13f7154b --- /dev/null +++ b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/config/CommonIntegrationConfig.java @@ -0,0 +1,31 @@ +package org.mte.numecoeval.calculs.infrastructure.config; + +import lombok.extern.slf4j.Slf4j; +import org.mte.numecoeval.calculs.referentiels.generated.api.client.InterneNumEcoEvalApi; +import org.mte.numecoeval.calculs.referentiels.generated.api.invoker.ApiClient; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.reactive.function.client.WebClient; + +@Configuration +@Slf4j +public class CommonIntegrationConfig { + + @Value("${numecoeval.referentiels.url}") + String referentielUrl; + + @Bean + public InterneNumEcoEvalApi clientAPIReferentiel() { + InterneNumEcoEvalApi interneNumEcoEvalApi = new InterneNumEcoEvalApi(); + ApiClient apiClient = new ApiClient(WebClient.builder() + .baseUrl(referentielUrl) + .build()); + apiClient.setBasePath(referentielUrl); + interneNumEcoEvalApi.setApiClient(apiClient); + + log.info("Création du client d'API Référentiel sur l'URL {}", referentielUrl); + + return interneNumEcoEvalApi; + } +} diff --git a/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/config/DomainConfiguration.java b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/config/DomainConfiguration.java new file mode 100644 index 00000000..0986a1d4 --- /dev/null +++ b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/config/DomainConfiguration.java @@ -0,0 +1,48 @@ +package org.mte.numecoeval.calculs.infrastructure.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.mte.numecoeval.calculs.domain.port.input.service.*; +import org.mte.numecoeval.calculs.domain.port.input.service.impl.*; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class DomainConfiguration { + + @Bean + public DureeDeVieEquipementPhysiqueService dureeDeVieEquipementPhysiqueService() { + return new DureeDeVieEquipementPhysiqueServiceImpl(); + } + + @Bean + public CalculImpactEquipementPhysiqueService calculImpactEquipementPhysiqueService(DureeDeVieEquipementPhysiqueService dureeDeVieEquipementPhysiqueService, ObjectMapper objectMapper) { + return new CalculImpactEquipementPhysiqueServiceImpl( + dureeDeVieEquipementPhysiqueService, + objectMapper + ); + } + + @Bean + CalculImpactEquipementVirtuelService calculImpactEquipementVirtuelService(ObjectMapper objectMapper) { + return new CalculImpactEquipementVirtuelServiceImpl(objectMapper); + } + + @Bean + CalculImpactApplicationService calculImpactApplicationService(ObjectMapper objectMapper) { + return new CalculImpactApplicationServiceImpl(objectMapper); + } + + + @Bean + public CalculImpactReseauService calculImpactReseauService(ObjectMapper objectMapper) { + return new CalculImpactReseauServiceImpl( + objectMapper + ); + } + + @Bean + CalculImpactMessagerieService calculImpactMessagerieService(ObjectMapper objectMapper) { + return new CalculImpactMessagerieServiceImpl(objectMapper); + } + +} diff --git a/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/config/KafkaConfiguration.java b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/config/KafkaConfiguration.java new file mode 100644 index 00000000..0a23a8d1 --- /dev/null +++ b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/config/KafkaConfiguration.java @@ -0,0 +1,25 @@ +package org.mte.numecoeval.calculs.infrastructure.config; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.kafka.listener.DefaultErrorHandler; +import org.springframework.util.backoff.FixedBackOff; + +@Configuration +@Slf4j +public class KafkaConfiguration { + + @Value("${spring.kafka.back-off-sec}") + private String backOffSec; + + @Bean + DefaultErrorHandler defaultErrorHandler() { + return new DefaultErrorHandler( + (rec, ex) -> log.error("Erreur kafka: {}", rec, ex), + new FixedBackOff(Long.parseLong(backOffSec), Integer.MAX_VALUE) + ); + } + +} diff --git a/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/config/OpenApiConfiguration.java b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/config/OpenApiConfiguration.java new file mode 100644 index 00000000..85e6d7f0 --- /dev/null +++ b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/config/OpenApiConfiguration.java @@ -0,0 +1,24 @@ +package org.mte.numecoeval.calculs.infrastructure.config; + +import org.springdoc.core.models.GroupedOpenApi; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class OpenApiConfiguration { + + @Bean + GroupedOpenApi calculsApi() { + return GroupedOpenApi.builder().group("calculs").pathsToMatch("/calculs/**").build(); + } + + @Bean + GroupedOpenApi syncApi() { + return GroupedOpenApi.builder().group("sync").pathsToMatch("/sync/**").build(); + } + + @Bean + GroupedOpenApi indicateurApi() { + return GroupedOpenApi.builder().group("indicateurs").pathsToMatch("/indicateur/**").build(); + } +} diff --git a/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/config/SecurityConfiguration.java b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/config/SecurityConfiguration.java new file mode 100644 index 00000000..d1ba34b5 --- /dev/null +++ b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/config/SecurityConfiguration.java @@ -0,0 +1,30 @@ +package org.mte.numecoeval.calculs.infrastructure.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +@EnableWebSecurity +public class SecurityConfiguration { + + @Bean + SecurityFilterChain globalFilterChain(HttpSecurity http) throws Exception { + http + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) + .and() + .cors() + .and() + .authorizeHttpRequests(authz -> authz + .requestMatchers("/health").permitAll() + .requestMatchers("/**").permitAll() + ) + .csrf().disable() + .formLogin().disable() + ; + return http.build(); + } +} diff --git a/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/controller/ExceptionHandler.java b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/controller/ExceptionHandler.java new file mode 100644 index 00000000..c9bd236f --- /dev/null +++ b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/controller/ExceptionHandler.java @@ -0,0 +1,65 @@ +package org.mte.numecoeval.calculs.infrastructure.controller; + +import lombok.extern.slf4j.Slf4j; +import org.mte.numecoeval.calculs.domain.exception.CalculImpactException; +import org.mte.numecoeval.calculs.domain.exception.CalculImpactRuntimeException; +import org.mte.numecoeval.calculs.rest.generated.api.model.ErreurRest; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.context.request.WebRequest; + +import java.time.LocalDateTime; + +import static org.springframework.http.HttpStatus.BAD_REQUEST; +import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR; + +@Slf4j +@RestControllerAdvice +public class ExceptionHandler { + + /** + * writer error message + * + * @param ex excepetion + * @param status le statut http + * @return l'objet erreur + */ + private static ErreurRest writeErrorResponse(Exception ex, HttpStatus status) { + return ErreurRest.builder() + .status(status.value()) + .code(status.name()) + .timestamp(LocalDateTime.now()) + .message(ex.getLocalizedMessage()).build(); + } + + @org.springframework.web.bind.annotation.ExceptionHandler(value = {CalculImpactException.class}) + @ResponseStatus(value = BAD_REQUEST) + public ErreurRest calculImpactException(Exception ex, WebRequest request) { + return writeErrorResponse(ex, BAD_REQUEST); + } + + @org.springframework.web.bind.annotation.ExceptionHandler(value = {CalculImpactRuntimeException.class}) + @ResponseStatus(value = INTERNAL_SERVER_ERROR) + public ErreurRest calculImpactRuntimeException(Exception ex, WebRequest request) { + return writeErrorResponse(ex, INTERNAL_SERVER_ERROR); + } + + @org.springframework.web.bind.annotation.ExceptionHandler(value = {RuntimeException.class}) + @ResponseStatus(value = INTERNAL_SERVER_ERROR) + public ErreurRest runtimeException(Exception ex, WebRequest request) { + log.error("RuntimeException lors d'un traitement sur l'URI {}", request.getContextPath(), ex); + log.debug("RuntimeException lors d'un traitement sur l'URI {}", request.getContextPath(), ex); + return writeErrorResponse(new Exception("Erreur interne de traitement lors du traitement de la requête"), INTERNAL_SERVER_ERROR); + } + + @org.springframework.web.bind.annotation.ExceptionHandler(value = {Exception.class}) + @ResponseStatus(value = INTERNAL_SERVER_ERROR) + public ErreurRest exception(Exception ex, WebRequest request) { + log.error("Exception lors d'un traitement sur l'URI {} : {}", request.getContextPath(), ex.getMessage()); + log.debug("Exception lors d'un traitement sur l'URI {}", request.getContextPath(), ex); + return writeErrorResponse(new Exception("Erreur interne de traitement lors du traitement de la requête"), INTERNAL_SERVER_ERROR); + } + +} + diff --git a/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/controller/export/IndicateurController.java b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/controller/export/IndicateurController.java new file mode 100644 index 00000000..f3b51d73 --- /dev/null +++ b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/controller/export/IndicateurController.java @@ -0,0 +1,159 @@ +package org.mte.numecoeval.calculs.infrastructure.controller.export; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Profile; +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 javax.sql.DataSource; +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +@RestController +@Slf4j +@Profile("!prod & !test") +@RequestMapping("indicateur") +public class IndicateurController { + + private static final int MAX_ROWS = 100_000; + + @Autowired + private DataSource dataSource; + + @GetMapping("/equipementPhysiqueCsv") + public String getEquipementPhysiqueList( + @RequestParam String nomLot, + @RequestParam String nomOrganisation, + @RequestParam String fields + ) { + + try (Connection conn = dataSource.getConnection()) { + try (var ps = conn.prepareStatement(String.format(""" + SELECT %s + FROM ind_indicateur_impact_equipement_physique + WHERE nom_lot = ? AND nom_organisation = ? + ORDER BY nom_equipement, critere, etapeacv; + """, fields))) { + + ps.setString(1, nomLot); + ps.setString(2, nomOrganisation); + return resultSetToCsv(ps.executeQuery()); + } + } catch (SQLException e) { + log.error("SQl exception", e); + throw new RuntimeException(e); + } + } + + @GetMapping("/equipementVirtuelCsv") + public String getEquipementVirtuelList( + @RequestParam String nomLot, + @RequestParam String nomOrganisation, + @RequestParam String fields + ) { + + try (Connection conn = dataSource.getConnection()) { + try (var ps = conn.prepareStatement(String.format(""" + SELECT %s + FROM ind_indicateur_impact_equipement_virtuel + WHERE nom_lot = ? AND nom_organisation = ? + ORDER BY nom_equipement, nom_equipement_virtuel, critere, etapeacv; + """, fields))) { + + ps.setString(1, nomLot); + ps.setString(2, nomOrganisation); + return resultSetToCsv(ps.executeQuery()); + } + } catch (SQLException e) { + log.error("SQl exception", e); + throw new RuntimeException(e); + } + } + + @GetMapping("/applicationCsv") + public String getApplicationList( + @RequestParam String nomLot, + @RequestParam String nomOrganisation, + @RequestParam String fields + ) { + + try (Connection conn = dataSource.getConnection()) { + try (var ps = conn.prepareStatement(String.format(""" + SELECT %s + FROM ind_indicateur_impact_application + WHERE nom_lot = ? AND nom_organisation = ? + ORDER BY nom_equipement_physique, nom_equipement_virtuel, nom_application, critere, etapeacv; + """, fields))) { + + ps.setString(1, nomLot); + ps.setString(2, nomOrganisation); + return resultSetToCsv(ps.executeQuery()); + } + } catch (SQLException e) { + log.error("SQl exception", e); + throw new RuntimeException(e); + } + } + + @GetMapping("/reseauCsv") + public String getReseauList( + @RequestParam String nomLot, + @RequestParam String nomOrganisation, + @RequestParam String fields + ) { + + try (Connection conn = dataSource.getConnection()) { + try (var ps = conn.prepareStatement(String.format(""" + SELECT %s + FROM ind_indicateur_impact_reseau + WHERE nom_lot = ? AND nom_organisation = ? + ORDER BY nom_equipement, critere, etapeacv; + """, fields))) { + + ps.setString(1, nomLot); + ps.setString(2, nomOrganisation); + return resultSetToCsv(ps.executeQuery()); + } + } catch (SQLException e) { + log.error("SQl exception", e); + throw new RuntimeException(e); + } + } + + private String resultSetToCsv(ResultSet rs) throws SQLException { + List<String> lines = new ArrayList<>(); + + ResultSetMetaData rsmd = rs.getMetaData(); + int numCols = rsmd.getColumnCount(); + + List<String> header = new ArrayList<>(); + for (int i = 1; i <= numCols; i++) header.add(rsmd.getColumnLabel(i)); + lines.add(String.join(",", header)); + + int rows = 0; + while (rs.next()) { + rows++; + List<String> values = new ArrayList<>(); + for (int i = 1; i <= numCols; i++) { + try { + var value = rs.getDouble(i); + values.add(String.valueOf(value)); + } catch (Exception e) { + var value = rs.getString(i); + if (value == null) value = ""; + values.add(value); + } + } + lines.add(String.join(",", values)); + if (rows >= MAX_ROWS) break; + } + return String.join("\n", lines); + } +} diff --git a/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/controller/rest/calculs/CalculsApplicationController.java b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/controller/rest/calculs/CalculsApplicationController.java new file mode 100644 index 00000000..f4b664a9 --- /dev/null +++ b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/controller/rest/calculs/CalculsApplicationController.java @@ -0,0 +1,42 @@ +package org.mte.numecoeval.calculs.infrastructure.controller.rest.calculs; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.AllArgsConstructor; +import org.mte.numecoeval.calculs.domain.port.input.service.impl.CalculImpactApplicationServiceImpl; +import org.mte.numecoeval.calculs.infrastructure.mapper.DomainMapper; +import org.mte.numecoeval.calculs.infrastructure.service.rest.calculs.CheckIndicateurService; +import org.mte.numecoeval.calculs.rest.generated.api.model.DemandeCalculApplicationRest; +import org.mte.numecoeval.calculs.rest.generated.api.model.IndicateurImpactApplicationRest; +import org.mte.numecoeval.calculs.rest.generated.api.server.CalculsApplicationApi; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@AllArgsConstructor +public class CalculsApplicationController implements CalculsApplicationApi { + + DomainMapper domainMapper; + + ObjectMapper objectMapper; + + CheckIndicateurService checkIndicateurService; + + /** + * POST /calculs/application + * + * @param demandeCalculApplicationRest (required) + * @return la liste d'indicateurs calcules + */ + @Override + public ResponseEntity<IndicateurImpactApplicationRest> calculerImpactApplication(DemandeCalculApplicationRest demandeCalculApplicationRest) { + var demandeCalcul = domainMapper.toDomain(demandeCalculApplicationRest); + + var indicateur = new CalculImpactApplicationServiceImpl( + objectMapper + ).calculImpactApplicatif(demandeCalcul); + + return checkIndicateurService.isOk(indicateur.getStatutIndicateur()) ? + ResponseEntity.ok(domainMapper.toRest(indicateur)) : + ResponseEntity.badRequest().body(domainMapper.toRest(indicateur)); + } +} diff --git a/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/controller/rest/calculs/CalculsEquipementPhysiqueController.java b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/controller/rest/calculs/CalculsEquipementPhysiqueController.java new file mode 100644 index 00000000..a74e53bb --- /dev/null +++ b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/controller/rest/calculs/CalculsEquipementPhysiqueController.java @@ -0,0 +1,48 @@ +package org.mte.numecoeval.calculs.infrastructure.controller.rest.calculs; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.mte.numecoeval.calculs.domain.port.input.service.DureeDeVieEquipementPhysiqueService; +import org.mte.numecoeval.calculs.domain.port.input.service.impl.CalculImpactEquipementPhysiqueServiceImpl; +import org.mte.numecoeval.calculs.infrastructure.mapper.DomainMapper; +import org.mte.numecoeval.calculs.infrastructure.service.rest.calculs.CheckIndicateurService; +import org.mte.numecoeval.calculs.rest.generated.api.model.DemandeCalculImpactEquipementPhysiqueRest; +import org.mte.numecoeval.calculs.rest.generated.api.model.IndicateurImpactEquipementPhysiqueRest; +import org.mte.numecoeval.calculs.rest.generated.api.server.CalculsEquipementPhysiqueApi; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@Slf4j +@AllArgsConstructor +public class CalculsEquipementPhysiqueController implements CalculsEquipementPhysiqueApi { + + DomainMapper domainMapper; + + ObjectMapper objectMapper; + + DureeDeVieEquipementPhysiqueService dureeDeVieEquipementPhysiqueService; + + CheckIndicateurService checkIndicateurService; + + /** + * POST /calculs/equipementPhysique + * + * @param demandeCalculImpactEquipementPhysiqueRest (required) + * @return la liste d'indicateurs calcules + */ + @Override + public ResponseEntity<IndicateurImpactEquipementPhysiqueRest> calculerImpactEquipementPhysique(DemandeCalculImpactEquipementPhysiqueRest demandeCalculImpactEquipementPhysiqueRest) { + var demandeCalcul = domainMapper.toDomain(demandeCalculImpactEquipementPhysiqueRest); + + var indicateur = new CalculImpactEquipementPhysiqueServiceImpl( + dureeDeVieEquipementPhysiqueService, + objectMapper + ).calculerImpactEquipementPhysique(demandeCalcul); + + return checkIndicateurService.isOk(indicateur.getStatutIndicateur()) ? + ResponseEntity.ok(domainMapper.toRest(indicateur)) : + ResponseEntity.badRequest().body(domainMapper.toRest(indicateur)); + } +} diff --git a/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/controller/rest/calculs/CalculsEquipementVirtuelController.java b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/controller/rest/calculs/CalculsEquipementVirtuelController.java new file mode 100644 index 00000000..66ee73ec --- /dev/null +++ b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/controller/rest/calculs/CalculsEquipementVirtuelController.java @@ -0,0 +1,42 @@ +package org.mte.numecoeval.calculs.infrastructure.controller.rest.calculs; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.AllArgsConstructor; +import org.mte.numecoeval.calculs.domain.port.input.service.impl.CalculImpactEquipementVirtuelServiceImpl; +import org.mte.numecoeval.calculs.infrastructure.mapper.DomainMapper; +import org.mte.numecoeval.calculs.infrastructure.service.rest.calculs.CheckIndicateurService; +import org.mte.numecoeval.calculs.rest.generated.api.model.DemandeCalculEquipementVirtuelRest; +import org.mte.numecoeval.calculs.rest.generated.api.model.IndicateurImpactEquipementVirtuelRest; +import org.mte.numecoeval.calculs.rest.generated.api.server.CalculsEquipementVirtuelApi; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@AllArgsConstructor +public class CalculsEquipementVirtuelController implements CalculsEquipementVirtuelApi { + + DomainMapper domainMapper; + + ObjectMapper objectMapper; + + CheckIndicateurService checkIndicateurService; + + /** + * POST /calculs/equipementVirtuel + * + * @param demandeCalculEquipementVirtuelRest (required) + * @return la liste d'indicateurs calcules + */ + @Override + public ResponseEntity<IndicateurImpactEquipementVirtuelRest> calculerImpactEquipementVirtuel(DemandeCalculEquipementVirtuelRest demandeCalculEquipementVirtuelRest) { + var demandeCalcul = domainMapper.toDomain(demandeCalculEquipementVirtuelRest); + + var indicateur = new CalculImpactEquipementVirtuelServiceImpl( + objectMapper + ).calculerImpactEquipementVirtuel(demandeCalcul); + + return checkIndicateurService.isOk(indicateur.getStatutIndicateur()) ? + ResponseEntity.ok(domainMapper.toRest(indicateur)) : + ResponseEntity.badRequest().body(domainMapper.toRest(indicateur)); + } +} diff --git a/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/controller/rest/calculs/CalculsMessagerieController.java b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/controller/rest/calculs/CalculsMessagerieController.java new file mode 100644 index 00000000..79ad2e4b --- /dev/null +++ b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/controller/rest/calculs/CalculsMessagerieController.java @@ -0,0 +1,42 @@ +package org.mte.numecoeval.calculs.infrastructure.controller.rest.calculs; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.AllArgsConstructor; +import org.mte.numecoeval.calculs.domain.port.input.service.impl.CalculImpactMessagerieServiceImpl; +import org.mte.numecoeval.calculs.infrastructure.mapper.DomainMapper; +import org.mte.numecoeval.calculs.infrastructure.service.rest.calculs.CheckIndicateurService; +import org.mte.numecoeval.calculs.rest.generated.api.model.DemandeCalculMessagerieRest; +import org.mte.numecoeval.calculs.rest.generated.api.model.IndicateurImpactMessagerieRest; +import org.mte.numecoeval.calculs.rest.generated.api.server.CalculsMessagerieApi; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@AllArgsConstructor +public class CalculsMessagerieController implements CalculsMessagerieApi { + + DomainMapper domainMapper; + + ObjectMapper objectMapper; + + CheckIndicateurService checkIndicateurService; + + /** + * POST /calculs/messagerie + * + * @param demandeCalculMessagerieRest (required) + * @return la liste d'indicateurs calcules + */ + @Override + public ResponseEntity<IndicateurImpactMessagerieRest> calculerImpactMessagerie(DemandeCalculMessagerieRest demandeCalculMessagerieRest) { + var demandeCalcul = domainMapper.toDomain(demandeCalculMessagerieRest); + + var indicateur = new CalculImpactMessagerieServiceImpl( + objectMapper + ).calculerImpactMessagerie(demandeCalcul); + + return checkIndicateurService.isOk(indicateur.getStatutIndicateur()) ? + ResponseEntity.ok(domainMapper.toRest(indicateur)) : + ResponseEntity.badRequest().body(domainMapper.toRest(indicateur)); + } +} diff --git a/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/controller/rest/calculs/CalculsReseauController.java b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/controller/rest/calculs/CalculsReseauController.java new file mode 100644 index 00000000..0e7f8f89 --- /dev/null +++ b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/controller/rest/calculs/CalculsReseauController.java @@ -0,0 +1,42 @@ +package org.mte.numecoeval.calculs.infrastructure.controller.rest.calculs; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.AllArgsConstructor; +import org.mte.numecoeval.calculs.domain.port.input.service.impl.CalculImpactReseauServiceImpl; +import org.mte.numecoeval.calculs.infrastructure.mapper.DomainMapper; +import org.mte.numecoeval.calculs.infrastructure.service.rest.calculs.CheckIndicateurService; +import org.mte.numecoeval.calculs.rest.generated.api.model.DemandeCalculImpactReseauEquipementPhysiqueRest; +import org.mte.numecoeval.calculs.rest.generated.api.model.IndicateurImpactReseauRest; +import org.mte.numecoeval.calculs.rest.generated.api.server.CalculsReseauApi; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@AllArgsConstructor +public class CalculsReseauController implements CalculsReseauApi { + + DomainMapper domainMapper; + + ObjectMapper objectMapper; + + CheckIndicateurService checkIndicateurService; + + /** + * POST /calculs/reseau/equipementPhysique + * + * @param demandeCalculImpactReseauEquipementPhysiqueRest (required) + * @return la liste d'indicateurs calcules + */ + @Override + public ResponseEntity<IndicateurImpactReseauRest> calculerImpactReseau(DemandeCalculImpactReseauEquipementPhysiqueRest demandeCalculImpactReseauEquipementPhysiqueRest) { + var demandeCalcul = domainMapper.toDomain(demandeCalculImpactReseauEquipementPhysiqueRest); + + var indicateur = new CalculImpactReseauServiceImpl( + objectMapper + ).calculerImpactReseau(demandeCalcul); + + return checkIndicateurService.isOk(indicateur.getStatutIndicateur()) ? + ResponseEntity.ok(domainMapper.toRest(indicateur)) : + ResponseEntity.badRequest().body(domainMapper.toRest(indicateur)); + } +} diff --git a/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/controller/sync/calculs/SyncCalculsController.java b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/controller/sync/calculs/SyncCalculsController.java new file mode 100644 index 00000000..4b1df009 --- /dev/null +++ b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/controller/sync/calculs/SyncCalculsController.java @@ -0,0 +1,27 @@ +package org.mte.numecoeval.calculs.infrastructure.controller.sync.calculs; + +import lombok.AllArgsConstructor; +import org.mte.numecoeval.calculs.infrastructure.service.sync.calculs.SyncCalculService; +import org.mte.numecoeval.calculs.sync.generated.api.model.ReponseCalculRest; +import org.mte.numecoeval.calculs.sync.generated.api.model.SyncCalculRest; +import org.mte.numecoeval.calculs.sync.generated.api.server.CalculsEquipementPhysiqueEtMessagerieApi; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@AllArgsConstructor +public class SyncCalculsController implements CalculsEquipementPhysiqueEtMessagerieApi { + + SyncCalculService syncCalculService; + + /** + * POST /sync/calcul + * + * @param syncCalculRest (required) + * @return la liste d'indicateurs calcules + */ + @Override + public ResponseEntity<ReponseCalculRest> syncCalculByIds(SyncCalculRest syncCalculRest) { + return ResponseEntity.ok(syncCalculService.calcul(syncCalculRest)); + } +} diff --git a/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/kafkalistener/ListenEquipementPhysique.java b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/kafkalistener/ListenEquipementPhysique.java new file mode 100644 index 00000000..3a4ed077 --- /dev/null +++ b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/kafkalistener/ListenEquipementPhysique.java @@ -0,0 +1,22 @@ +package org.mte.numecoeval.calculs.infrastructure.kafkalistener; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.mte.numecoeval.calculs.infrastructure.service.calcul.MainEquipementPhysiqueService; +import org.mte.numecoeval.topic.data.EquipementPhysiqueDTO; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.kafka.annotation.KafkaListener; +import org.springframework.stereotype.Component; + +@Component +@Slf4j +@AllArgsConstructor +public class ListenEquipementPhysique { + + private MainEquipementPhysiqueService mainEquipementPhysiqueService; + + @KafkaListener(topics = "${numecoeval.topic.equipementPhysique}", concurrency = "${numecoeval.topic.partition}") + public void consume(EquipementPhysiqueDTO equipementPhysiqueDTO) { + mainEquipementPhysiqueService.calcul(equipementPhysiqueDTO); + } +} \ No newline at end of file diff --git a/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/kafkalistener/ListenMessagerie.java b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/kafkalistener/ListenMessagerie.java new file mode 100644 index 00000000..5c94e031 --- /dev/null +++ b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/kafkalistener/ListenMessagerie.java @@ -0,0 +1,23 @@ +package org.mte.numecoeval.calculs.infrastructure.kafkalistener; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.mte.numecoeval.calculs.infrastructure.service.calcul.MainMessagerieService; +import org.mte.numecoeval.topic.data.MessagerieDTO; +import org.springframework.kafka.annotation.KafkaListener; +import org.springframework.stereotype.Component; + +@Component +@Slf4j +@AllArgsConstructor +public class ListenMessagerie { + + private MainMessagerieService mainMessagerieService; + + @KafkaListener(topics = "${numecoeval.topic.messagerie}") + public void consume(MessagerieDTO messagerieDTO) { + mainMessagerieService.calcul(messagerieDTO); + } +} + + diff --git a/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/mapper/DomainMapper.java b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/mapper/DomainMapper.java new file mode 100644 index 00000000..a5c6c049 --- /dev/null +++ b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/mapper/DomainMapper.java @@ -0,0 +1,78 @@ +package org.mte.numecoeval.calculs.infrastructure.mapper; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mte.numecoeval.calculs.domain.data.demande.*; +import org.mte.numecoeval.calculs.domain.data.entree.*; +import org.mte.numecoeval.calculs.domain.data.indicateurs.*; +import org.mte.numecoeval.calculs.domain.data.referentiel.*; +import org.mte.numecoeval.calculs.rest.generated.api.model.*; + +@Mapper(componentModel = "spring") +public interface DomainMapper { + + /*Entrées*/ + EquipementPhysique toDomain(EquipementPhysiqueRest rest); + + DataCenter toDomain(DataCenterRest rest); + + EquipementVirtuel toDomain(EquipementVirtuelRest rest); + + Application toDomain(ApplicationRest rest); + + Messagerie toDomain(MessagerieRest rest); + + /*Référentiels*/ + + ReferentielTypeEquipement toDomain(TypeEquipementRest rest); + + ReferentielCorrespondanceRefEquipement toDomain(CorrespondanceRefEquipementRest rest); + + ReferentielHypothese toDomain(HypotheseRest rest); + + ReferentielEtapeACV toDomain(EtapeRest rest); + + ReferentielCritere toDomain(CritereRest rest); + + ReferentielImpactEquipement toDomain(ImpactEquipementRest rest); + + @Mapping(source = "valeur", target = "impactReseauMobileMoyen") + ReferentielImpactReseau toDomain(ImpactReseauRest rest); + + ReferentielMixElectrique toDomain(MixElectriqueRest rest); + + ReferentielImpactMessagerie toDomain(ImpactMessagerieRest rest); + + /*Indicateurs*/ + ImpactEquipementPhysique toDomain(IndicateurImpactEquipementPhysiqueRest rest); + + ImpactEquipementVirtuel toDomain(IndicateurImpactEquipementVirtuelRest rest); + + ImpactApplication toDomain(IndicateurImpactApplicationRest rest); + + IndicateurImpactEquipementPhysiqueRest toRest(ImpactEquipementPhysique domain); + + IndicateurImpactEquipementVirtuelRest toRest(ImpactEquipementVirtuel domain); + + IndicateurImpactApplicationRest toRest(ImpactApplication domain); + + IndicateurImpactReseauRest toRest(ImpactReseau domain); + + IndicateurImpactMessagerieRest toRest(ImpactMessagerie domain); + + /*Demandes*/ + @Mapping(target = "dateCalcul", expression = "java(java.time.LocalDateTime.now())") + DemandeCalculImpactEquipementPhysique toDomain(DemandeCalculImpactEquipementPhysiqueRest rest); + + @Mapping(target = "dateCalcul", expression = "java(java.time.LocalDateTime.now())") + DemandeCalculImpactReseau toDomain(DemandeCalculImpactReseauEquipementPhysiqueRest rest); + + @Mapping(target = "dateCalcul", expression = "java(java.time.LocalDateTime.now())") + DemandeCalculImpactEquipementVirtuel toDomain(DemandeCalculEquipementVirtuelRest rest); + + @Mapping(target = "dateCalcul", expression = "java(java.time.LocalDateTime.now())") + DemandeCalculImpactApplication toDomain(DemandeCalculApplicationRest rest); + + @Mapping(target = "dateCalcul", expression = "java(java.time.LocalDateTime.now())") + DemandeCalculImpactMessagerie toDomain(DemandeCalculMessagerieRest rest); +} diff --git a/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/mapper/EntreesMapper.java b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/mapper/EntreesMapper.java new file mode 100644 index 00000000..be2c6c0b --- /dev/null +++ b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/mapper/EntreesMapper.java @@ -0,0 +1,20 @@ +package org.mte.numecoeval.calculs.infrastructure.mapper; + +import org.mapstruct.Mapper; +import org.mte.numecoeval.calculs.domain.data.entree.DataCenter; +import org.mte.numecoeval.calculs.domain.data.entree.EquipementPhysique; +import org.mte.numecoeval.calculs.domain.data.entree.Messagerie; +import org.mte.numecoeval.topic.data.DataCenterDTO; +import org.mte.numecoeval.topic.data.EquipementPhysiqueDTO; +import org.mte.numecoeval.topic.data.MessagerieDTO; + +@Mapper(componentModel = "spring") +public interface EntreesMapper { + + EquipementPhysique toDomain(EquipementPhysiqueDTO dto); + + DataCenter toDomain(DataCenterDTO dto); + + Messagerie toDomain(MessagerieDTO messagerieDTO); + +} diff --git a/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/mapper/ReferentielMapper.java b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/mapper/ReferentielMapper.java new file mode 100644 index 00000000..b6937550 --- /dev/null +++ b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/mapper/ReferentielMapper.java @@ -0,0 +1,41 @@ +package org.mte.numecoeval.calculs.infrastructure.mapper; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mte.numecoeval.calculs.domain.data.referentiel.*; +import org.mte.numecoeval.calculs.referentiels.generated.api.model.*; + +import java.util.List; + +@Mapper(componentModel = "spring") +public interface ReferentielMapper { + + ReferentielEtapeACV toEtape(EtapeDTO etapeDTO); + + ReferentielCritere toCritere(CritereDTO critereDTO); + + @Mapping(source = "valeur", target = "impactReseauMobileMoyen") + ReferentielImpactReseau toImpactReseau(ImpactReseauDTO impactReseauDTO); + + List<ReferentielImpactReseau> toListImpactReseau(List<ImpactReseauDTO> impactReseauDTO); + + ReferentielHypothese toHypothese(HypotheseDTO hypotheseDTO); + + List<ReferentielHypothese> toListHypothese(List<HypotheseDTO> hypotheseDTO); + + ReferentielImpactEquipement toImpactEquipement(ImpactEquipementDTO impactEquipementDTO); + + List<ReferentielImpactEquipement> toListImpactEquipement(List<ImpactEquipementDTO> impactEquipementDTO); + + ReferentielMixElectrique toMixElectrique(MixElectriqueDTO mixElectriqueDTO); + + List<ReferentielMixElectrique> toListMixElectrique(List<MixElectriqueDTO> mixElectriqueDTO); + + ReferentielImpactMessagerie toImpactMessagerie(ImpactMessagerieDTO impactMessagerieDTO); + + List<ReferentielImpactMessagerie> toListImpactMessagerie(List<ImpactMessagerieDTO> impactMessagerieDTO); + + ReferentielCorrespondanceRefEquipement toCorrespondanceRefEquipement(CorrespondanceRefEquipementDTO dto); + + ReferentielTypeEquipement toTypeEquipement(TypeEquipementDTO dto); +} diff --git a/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/repository/ApplicationRepository.java b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/repository/ApplicationRepository.java new file mode 100644 index 00000000..574d0576 --- /dev/null +++ b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/repository/ApplicationRepository.java @@ -0,0 +1,66 @@ +package org.mte.numecoeval.calculs.infrastructure.repository; + +import lombok.extern.slf4j.Slf4j; +import org.mte.numecoeval.calculs.domain.data.entree.Application; +import org.mte.numecoeval.calculs.domain.exception.DatabaseException; +import org.mte.numecoeval.common.utils.ResultSetUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +@Component +@Slf4j +public class ApplicationRepository { + + @Autowired + private DataSource dataSource; + + public List<Application> findApplications(String nomOrganisation, String nomLot, String nomEquipementPhysique, String nomEquipementVirtuel) { + List<Application> result = new ArrayList<>(); + + try (Connection conn = dataSource.getConnection()) { + try (var ps = conn.prepareStatement(""" + SELECT * + FROM en_application + WHERE + nullif(nom_lot, ?) is null + AND nom_equipement_virtuel = ? + AND nom_equipement_physique = ? + AND nullif(nom_organisation, ?) is null + """)) { + + ps.setString(1, nomLot); + ps.setString(2, nomEquipementVirtuel); + ps.setString(3, nomEquipementPhysique); + ps.setString(4, nomOrganisation); + + var rs = ps.executeQuery(); + while (rs.next()) { + result.add(Application.builder() + .nomLot(rs.getString("nom_lot")) + .dateLot(ResultSetUtils.getLocalDate(rs, "date_lot")) + .nomOrganisation(rs.getString("nom_organisation")) + .nomEntite(rs.getString("nom_entite")) + .nomEquipementPhysique(rs.getString("nom_equipement_physique")) + .nomEquipementVirtuel(rs.getString("nom_equipement_virtuel")) + .nomApplication(rs.getString("nom_application")) + .typeEnvironnement(rs.getString("type_environnement")) + .domaine(rs.getString("domaine")) + .sousDomaine(rs.getString("sous_domaine")) + .nomSourceDonnee(rs.getString("nom_source_donnee")) + .build()); + } + } + } catch (SQLException e) { + log.error("Une erreur s'est produite lors de la selection dans PostgreSQL. Exception: ", e); + throw new DatabaseException(e); + } + + return result; + } +} diff --git a/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/repository/EquipementPhysiqueRepository.java b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/repository/EquipementPhysiqueRepository.java new file mode 100644 index 00000000..11864627 --- /dev/null +++ b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/repository/EquipementPhysiqueRepository.java @@ -0,0 +1,110 @@ +package org.mte.numecoeval.calculs.infrastructure.repository; + +import lombok.extern.slf4j.Slf4j; +import org.mte.numecoeval.calculs.domain.exception.DatabaseException; +import org.mte.numecoeval.common.utils.ResultSetUtils; +import org.mte.numecoeval.topic.data.DataCenterDTO; +import org.mte.numecoeval.topic.data.EquipementPhysiqueDTO; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +@Component +@Slf4j +public class EquipementPhysiqueRepository { + + @Autowired + private DataSource dataSource; + + private static final String COLUMN_NAME_NOM_COURT_DATACENTER = "nom_court_datacenter"; + + public List<EquipementPhysiqueDTO> findEquipementPhysiqueDTOs(List<Long> ids) { + List<EquipementPhysiqueDTO> result = new ArrayList<>(); + + try (Connection conn = dataSource.getConnection()) { + try (var ps = conn.prepareStatement(""" + SELECT eqp.*, + dc.id as dc_id, + dc.date_creation as dc_date_creation, + dc.date_update as dc_date_update, + dc.localisation as dc_localisation, + dc.nom_long_datacenter as dc_nom_long_datacenter, + dc.pue as dc_pue, + dc.nom_entite as dc_nom_entite + FROM en_equipement_physique eqp + LEFT JOIN en_data_center dc ON dc.nom_lot = eqp.nom_lot and dc.nom_court_datacenter = eqp.nom_court_datacenter + WHERE eqp.id = ANY (?) + """)) { + + ps.setArray(1, conn.createArrayOf("long", ids.toArray(new Long[0]))); + + var rs = ps.executeQuery(); + while (rs.next()) { + result.add(EquipementPhysiqueDTO.builder() + .id(rs.getLong("id")) + .nomLot(rs.getString("nom_lot")) + .dateLot(ResultSetUtils.getLocalDate(rs, "date_lot")) + .nomOrganisation(rs.getString("nom_organisation")) + .nomEntite(rs.getString("nom_entite")) + .nomEquipementPhysique(rs.getString("nom_equipement_physique")) + .consoElecAnnuelle(ResultSetUtils.getDouble(rs, "conso_elec_annuelle")) + .dateAchat(ResultSetUtils.getLocalDate(rs, "date_achat")) + .dateRetrait(ResultSetUtils.getLocalDate(rs, "date_retrait")) + .goTelecharge(ResultSetUtils.getFloat(rs, "go_telecharge")) + .modele(rs.getString("modele")) + .nbCoeur(rs.getString("nb_coeur")) + .paysDUtilisation(rs.getString("pays_utilisation")) + .quantite(ResultSetUtils.getDouble(rs, "quantite")) + .serveur(rs.getBoolean("serveur")) + .statut(rs.getString("statut")) + .type(rs.getString("type")) + .utilisateur(rs.getString("utilisateur")) + .nomCourtDatacenter(rs.getString(COLUMN_NAME_NOM_COURT_DATACENTER)) + .nomCourtDatacenter(rs.getString(COLUMN_NAME_NOM_COURT_DATACENTER)) + .nomSourceDonnee(rs.getString("nom_source_donnee")) + .modeUtilisation(rs.getString("mode_utilisation")) + .tauxUtilisation(ResultSetUtils.getDouble(rs, "taux_utilisation")) + .dataCenter( + DataCenterDTO.builder() + .id(rs.getLong("dc_id")) + .localisation(rs.getString("dc_localisation")) + .nomLongDatacenter(rs.getString("dc_nom_long_datacenter")) + .pue(ResultSetUtils.getDouble(rs, "dc_pue")) + .nomCourtDatacenter(rs.getString(COLUMN_NAME_NOM_COURT_DATACENTER)) + .nomEntite(rs.getString("dc_nom_entite")) + .nomOrganisation(rs.getString("nom_organisation")) + .dateLot(ResultSetUtils.getLocalDate(rs, "date_lot")) + .build() + ) + .build()); + } + } + } catch (SQLException e) { + log.error("Une erreur s'est produite lors de la selection dans PostgreSQL. Exception: ", e); + throw new DatabaseException(e); + } + + return result; + } + + public void setStatutToTraite(Long id) { + try (Connection conn = dataSource.getConnection()) { + try (var ps = conn.prepareStatement(""" + UPDATE en_equipement_physique + SET statut_traitement = 'TRAITE', date_update = now() + WHERE id = ? + """)) { + ps.setLong(1, id); + ps.execute(); + } + } catch (SQLException e) { + log.error("Une erreur s'est produite lors de la selection dans PostgreSQL. Exception: ", e); + throw new DatabaseException(e); + } + } +} diff --git a/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/repository/EquipementVirtuelRepository.java b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/repository/EquipementVirtuelRepository.java new file mode 100644 index 00000000..574c2c8c --- /dev/null +++ b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/repository/EquipementVirtuelRepository.java @@ -0,0 +1,67 @@ +package org.mte.numecoeval.calculs.infrastructure.repository; + +import lombok.extern.slf4j.Slf4j; +import org.mte.numecoeval.calculs.domain.data.entree.EquipementVirtuel; +import org.mte.numecoeval.calculs.domain.exception.DatabaseException; +import org.mte.numecoeval.common.utils.ResultSetUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +@Component +@Slf4j +public class EquipementVirtuelRepository { + + @Autowired + private DataSource dataSource; + + public List<EquipementVirtuel> findEquipementVirtuels(String nomOrganisation, String nomLot, String nomEquipementPhysique) { + List<EquipementVirtuel> result = new ArrayList<>(); + + try (Connection conn = dataSource.getConnection()) { + try (var ps = conn.prepareStatement(""" + SELECT * + FROM en_equipement_virtuel + WHERE + nullif(nom_lot, ?) is null + AND nom_equipement_physique = ? + AND nullif(nom_organisation, ?) is null + """)) { + + ps.setString(1, nomLot); + ps.setString(2, nomEquipementPhysique); + ps.setString(3, nomOrganisation); + + var rs = ps.executeQuery(); + while (rs.next()) { + result.add(EquipementVirtuel.builder() + .id(rs.getLong("id")) + .nomLot(rs.getString("nom_lot")) + .dateLot(ResultSetUtils.getLocalDate(rs, "date_lot")) + .nomOrganisation(rs.getString("nom_organisation")) + .nomEntite(rs.getString("nom_entite")) + .nomEquipementPhysique(rs.getString("nom_equipement_physique")) + .nomEquipementVirtuel(rs.getString("nom_equipement_virtuel")) + .cluster(rs.getString("cluster")) + .vCPU(ResultSetUtils.getInteger(rs, "vcpu")) + .consoElecAnnuelle(ResultSetUtils.getDouble(rs, "conso_elec_annuelle")) + .typeEqv(rs.getString("type_eqv")) + .capaciteStockage(ResultSetUtils.getDouble(rs, "capacite_stockage")) + .cleRepartition(ResultSetUtils.getDouble(rs, "cle_repartition")) + .nomSourceDonnee(rs.getString("nom_source_donnee")) + .build()); + } + } + } catch (SQLException e) { + log.error("Une erreur s'est produite lors de la selection dans PostgreSQL. Exception: ", e); + throw new DatabaseException(e); + } + + return result; + } +} diff --git a/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/repository/IndicateurRepository.java b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/repository/IndicateurRepository.java new file mode 100644 index 00000000..388d866f --- /dev/null +++ b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/repository/IndicateurRepository.java @@ -0,0 +1,327 @@ +package org.mte.numecoeval.calculs.infrastructure.repository; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.mte.numecoeval.calculs.domain.data.indicateurs.*; +import org.mte.numecoeval.calculs.domain.exception.DatabaseException; +import org.mte.numecoeval.common.utils.PreparedStatementUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.SQLException; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.List; +import java.util.Optional; + +@Component +@Slf4j +public class IndicateurRepository { + + @Autowired + private DataSource dataSource; + + private static final String INSERT_EQUIPEMENT_PHYSIQUE_QUERY = """ + INSERT INTO ind_indicateur_impact_equipement_physique + (date_calcul, date_lot, date_lot_discriminator, nom_organisation, nom_organisation_discriminator, etapeacv, + critere, nom_equipement, nom_entite, nom_entite_discriminator, source, + statut_indicateur, trace, version_calcul, conso_elec_moyenne, + impact_unitaire, quantite, type_equipement, unite, + statut_equipement_physique, nom_lot, nom_source_donnee, nom_source_donnee_discriminator) + VALUES (?, ?, ?, ?, ?, ?,?, ?, ?, ?, ?,?, ?, ?, ?,?, ?, ?, ?,?, ?, ?, ?) + """; + + private static final String INSERT_EQUIPEMENT_VIRTUEL_QUERY = """ + INSERT INTO ind_indicateur_impact_equipement_virtuel + (date_calcul, date_lot, date_lot_discriminator, nom_organisation, nom_organisation_discriminator, + etapeacv, critere, nom_equipement, nom_equipement_virtuel, nom_entite, nom_entite_discriminator, + source, statut_indicateur, trace, version_calcul, impact_unitaire, unite, conso_elec_moyenne, + cluster, nom_lot, nom_source_donnee, nom_source_donnee_discriminator ) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """; + + private static final String INSERT_APPLICATION_QUERY = """ + INSERT INTO ind_indicateur_impact_application + (date_calcul, date_lot, date_lot_discriminator, etapeacv, critere, nom_organisation, + nom_organisation_discriminator, nom_application, type_environnement, nom_equipement_physique, + nom_equipement_virtuel, nom_entite, nom_entite_discriminator, source, statut_indicateur, trace, + version_calcul, domaine, sous_domaine, impact_unitaire, unite, conso_elec_moyenne, nom_lot, + nom_source_donnee, nom_source_donnee_discriminator) + VALUES (?, ?, ?, ?,?, ?, ?, ?,?, ?, ?, ?,?, ?, ?, ?,?, ?, ?, ?,?,?, ?, ?, ?) + """; + + private static final String INSERT_MESSAGERIE_QUERY = """ + INSERT INTO ind_indicateur_impact_messagerie + (date_calcul, date_lot, date_lot_discriminator, nom_organisation, nom_organisation_discriminator, + critere, mois_annee, nom_entite, nom_entite_discriminator, source, statut_indicateur, trace, + version_calcul, impact_mensuel, unite, nombre_mail_emis, volume_total_mail_emis, nom_lot, + nom_source_donnee, nom_source_donnee_discriminator) + VALUES (?, ?, ?, ?,?, ?, ?, ?,?, ?, ?, ?,?, ?, ?, ?,?, ?, ?, ?) + """; + + private static final String INSERT_RESEAU_QUERY = """ + INSERT INTO ind_indicateur_impact_reseau + (date_calcul, date_lot, date_lot_discriminator, nom_organisation, nom_organisation_discriminator, + etapeacv, critere, nom_equipement, nom_entite, nom_entite_discriminator, source, statut_indicateur, + trace, version_calcul, impact_unitaire, unite, nom_lot, nom_source_donnee, nom_source_donnee_discriminator) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """; + + /** + * Sauvegarde tous les indicateurs en base de données e + * + * @param impactsEquipementPhysique liste des impacts Eq Ph + * @param impactsEquipementVirtuels liste des impacts Eq Virt + * @param impactsApplications liste des impacts applications + */ + public void saveIndicateursEquipements( + List<ImpactEquipementPhysique> impactsEquipementPhysique, + List<ImpactEquipementVirtuel> impactsEquipementVirtuels, + List<ImpactApplication> impactsApplications, + List<ImpactReseau> impactsReseau + ) { + if (impactsEquipementPhysique.isEmpty()) return; + + try (Connection conn = dataSource.getConnection()) { + conn.setAutoCommit(false); + + try (var ps = conn.prepareStatement(INSERT_EQUIPEMENT_PHYSIQUE_QUERY)) { + for (ImpactEquipementPhysique indicateur : impactsEquipementPhysique) { + ps.setTimestamp(1, PreparedStatementUtils.getTimestampFromLocalDateTime(indicateur.getDateCalcul())); + ps.setDate(2, PreparedStatementUtils.getDateFromLocalDate(indicateur.getDateLot())); + ps.setDate(3, PreparedStatementUtils.getDateFromLocalDate(Optional.ofNullable(indicateur.getDateLot()).orElse(LocalDate.EPOCH))); + ps.setString(4, indicateur.getNomOrganisation()); + ps.setString(5, StringUtils.defaultString(indicateur.getNomOrganisation())); + ps.setString(6, indicateur.getEtapeACV()); + ps.setString(7, indicateur.getCritere()); + ps.setString(8, indicateur.getNomEquipement()); + ps.setString(9, indicateur.getNomEntite()); + ps.setString(10, StringUtils.defaultString(indicateur.getNomEntite())); + ps.setString(11, indicateur.getSource()); + ps.setString(12, indicateur.getStatutIndicateur()); + ps.setString(13, indicateur.getTrace()); + ps.setString(14, indicateur.getVersionCalcul()); + PreparedStatementUtils.setDoubleValue(ps, 15, indicateur.getConsoElecMoyenne()); + PreparedStatementUtils.setDoubleValue(ps, 16, indicateur.getImpactUnitaire()); + PreparedStatementUtils.setIntValue(ps, 17, indicateur.getQuantite().intValue()); + ps.setString(18, indicateur.getTypeEquipement()); + ps.setString(19, indicateur.getUnite()); + ps.setString(20, indicateur.getStatutEquipementPhysique()); + ps.setString(21, indicateur.getNomLot()); + ps.setString(22, indicateur.getNomSourceDonnee()); + ps.setString(23, StringUtils.defaultString(indicateur.getNomSourceDonnee())); + ps.addBatch(); + } + + ps.executeBatch(); + } + try (var ps = conn.prepareStatement(INSERT_EQUIPEMENT_VIRTUEL_QUERY)) { + for (ImpactEquipementVirtuel indicateur : impactsEquipementVirtuels) { + ps.setTimestamp(1, PreparedStatementUtils.getTimestampFromLocalDateTime(indicateur.getDateCalcul())); + ps.setDate(2, PreparedStatementUtils.getDateFromLocalDate(indicateur.getDateLot())); + ps.setDate(3, PreparedStatementUtils.getDateFromLocalDate(Optional.ofNullable(indicateur.getDateLot()).orElse(LocalDate.EPOCH))); + ps.setString(4, indicateur.getNomOrganisation()); + ps.setString(5, StringUtils.defaultString(indicateur.getNomOrganisation())); + ps.setString(6, indicateur.getEtapeACV()); + ps.setString(7, indicateur.getCritere()); + ps.setString(8, indicateur.getNomEquipement()); + ps.setString(9, indicateur.getNomEquipementVirtuel()); + ps.setString(10, indicateur.getNomEntite()); + ps.setString(11, StringUtils.defaultString(indicateur.getNomEntite())); + ps.setString(12, indicateur.getSource()); + ps.setString(13, indicateur.getStatutIndicateur()); + ps.setString(14, indicateur.getTrace()); + ps.setString(15, indicateur.getVersionCalcul()); + PreparedStatementUtils.setDoubleValue(ps, 16, indicateur.getImpactUnitaire()); + ps.setString(17, indicateur.getUnite()); + PreparedStatementUtils.setDoubleValue(ps, 18, indicateur.getConsoElecMoyenne()); + ps.setString(19, indicateur.getCluster()); + ps.setString(20, indicateur.getNomLot()); + ps.setString(21, indicateur.getNomSourceDonnee()); + ps.setString(22, StringUtils.defaultString(indicateur.getNomSourceDonnee())); + ps.addBatch(); + } + + ps.executeBatch(); + } + + try (var ps = conn.prepareStatement(INSERT_APPLICATION_QUERY)) { + for (ImpactApplication indicateur : impactsApplications) { + ps.setTimestamp(1, PreparedStatementUtils.getTimestampFromLocalDateTime(indicateur.getDateCalcul())); + ps.setDate(2, PreparedStatementUtils.getDateFromLocalDate(indicateur.getDateLot())); + ps.setDate(3, PreparedStatementUtils.getDateFromLocalDate(Optional.ofNullable(indicateur.getDateLot()).orElse(LocalDate.EPOCH))); + ps.setString(4, indicateur.getEtapeACV()); + ps.setString(5, indicateur.getCritere()); + ps.setString(6, indicateur.getNomOrganisation()); + ps.setString(7, StringUtils.defaultString(indicateur.getNomOrganisation())); + ps.setString(8, indicateur.getNomApplication()); + ps.setString(9, indicateur.getTypeEnvironnement()); + ps.setString(10, indicateur.getNomEquipementPhysique()); + ps.setString(11, indicateur.getNomEquipementVirtuel()); + ps.setString(12, indicateur.getNomEntite()); + ps.setString(13, StringUtils.defaultString(indicateur.getNomEntite())); + ps.setString(14, indicateur.getSource()); + ps.setString(15, indicateur.getStatutIndicateur()); + ps.setString(16, indicateur.getTrace()); + ps.setString(17, indicateur.getVersionCalcul()); + ps.setString(18, indicateur.getDomaine()); + ps.setString(19, indicateur.getSousDomaine()); + PreparedStatementUtils.setDoubleValue(ps, 20, indicateur.getImpactUnitaire()); + ps.setString(21, indicateur.getUnite()); + PreparedStatementUtils.setDoubleValue(ps, 22, indicateur.getConsoElecMoyenne()); + ps.setString(23, indicateur.getNomLot()); + ps.setString(24, indicateur.getNomSourceDonnee()); + ps.setString(25, StringUtils.defaultString(indicateur.getNomSourceDonnee())); + ps.addBatch(); + } + + ps.executeBatch(); + } + + try (var ps = conn.prepareStatement(INSERT_RESEAU_QUERY)) { + + for (ImpactReseau indicateur : impactsReseau) { + ps.setTimestamp(1, PreparedStatementUtils.getTimestampFromLocalDateTime(indicateur.getDateCalcul())); + ps.setDate(2, PreparedStatementUtils.getDateFromLocalDate(indicateur.getDateLot())); + ps.setDate(3, PreparedStatementUtils.getDateFromLocalDate(Optional.ofNullable(indicateur.getDateLot()).orElse(LocalDate.EPOCH))); + ps.setString(4, indicateur.getNomOrganisation()); + ps.setString(5, StringUtils.defaultString(indicateur.getNomOrganisation())); + ps.setString(6, indicateur.getEtapeACV()); + ps.setString(7, indicateur.getCritere()); + ps.setString(8, indicateur.getNomEquipement()); + ps.setString(9, indicateur.getNomEntite()); + ps.setString(10, StringUtils.defaultString(indicateur.getNomEntite())); + ps.setString(11, indicateur.getSource()); + ps.setString(12, indicateur.getStatutIndicateur()); + ps.setString(13, indicateur.getTrace()); + ps.setString(14, indicateur.getVersionCalcul()); + PreparedStatementUtils.setDoubleValue(ps, 15, indicateur.getImpactUnitaire()); + ps.setString(16, indicateur.getUnite()); + ps.setString(17, indicateur.getNomLot()); + ps.setString(18, indicateur.getNomSourceDonnee()); + ps.setString(19, StringUtils.defaultString(indicateur.getNomSourceDonnee())); + ps.addBatch(); + } + + ps.executeBatch(); + } + + conn.commit(); + } catch (SQLException e) { + log.error("Une erreur s'est produite lors de l'insertion dans PostgreSQL. Exception: ", e); + throw new DatabaseException(e); + } + } + + /** + * Sauvegarde tous les indicateurs 'messagerie' en base de données + * + * @param impactMessageries liste indicateurs messagerie + */ + public void saveIndicateursMessagerie(List<ImpactMessagerie> impactMessageries) { + if (impactMessageries.isEmpty()) return; + + try (Connection conn = dataSource.getConnection()) { + conn.setAutoCommit(false); + + try (var ps = conn.prepareStatement(INSERT_MESSAGERIE_QUERY)) { + + for (ImpactMessagerie indicateur : impactMessageries) { + ps.setTimestamp(1, PreparedStatementUtils.getTimestampFromLocalDateTime(indicateur.getDateCalcul())); + ps.setDate(2, PreparedStatementUtils.getDateFromLocalDate(indicateur.getDateLot())); + ps.setDate(3, PreparedStatementUtils.getDateFromLocalDate(Optional.ofNullable(indicateur.getDateLot()).orElse(LocalDate.EPOCH))); + ps.setString(4, indicateur.getNomOrganisation()); + ps.setString(5, StringUtils.defaultString(indicateur.getNomOrganisation())); + ps.setString(6, indicateur.getCritere()); + ps.setDate(7, PreparedStatementUtils.getDateFromLocalDate(moisAnneeToLocalDate(indicateur.getMoisAnnee()))); + ps.setString(8, indicateur.getNomEntite()); + ps.setString(9, StringUtils.defaultString(indicateur.getNomEntite())); + ps.setString(10, indicateur.getSource()); + ps.setString(11, indicateur.getStatutIndicateur()); + ps.setString(12, indicateur.getTrace()); + ps.setString(13, indicateur.getVersionCalcul()); + PreparedStatementUtils.setDoubleValue(ps, 14, indicateur.getImpactMensuel()); + ps.setString(15, indicateur.getUnite()); + PreparedStatementUtils.setDoubleValue(ps, 16, indicateur.getNombreMailEmis()); + PreparedStatementUtils.setDoubleValue(ps, 17, indicateur.getVolumeTotalMailEmis()); + ps.setString(18, indicateur.getNomLot()); + ps.setString(19, indicateur.getNomSourceDonnee()); + ps.setString(20, StringUtils.defaultString(indicateur.getNomSourceDonnee())); + ps.addBatch(); + } + + ps.executeBatch(); + } + + conn.commit(); + } catch (SQLException e) { + log.error("Une erreur s'est produite lors de l'insertion dans PostgreSQL. Exception: ", e); + throw new DatabaseException(e); + } + } + + private LocalDate moisAnneeToLocalDate(Integer moisAnnee) { + if (moisAnnee == null) return null; + try { + return LocalDate.parse(moisAnnee + "01", DateTimeFormatter.ofPattern("yyyyMMdd")); + } catch (DateTimeParseException ignored) { + return null; + } + } + + /** + * Supprime les indicateurs des equipements physiques, virtuel et applications + * + * @param nomEquipementPhysique le nom de l'equipement physique + * @param nomLot le nomLot + */ + public void clearIndicateurs(String nomEquipementPhysique, String nomLot) { + + try (Connection conn = dataSource.getConnection()) { + conn.setAutoCommit(false); + + try (var ps = conn.prepareStatement(""" + DELETE FROM ind_indicateur_impact_equipement_physique + WHERE nom_lot = ? AND nom_equipement = ? + """)) { + ps.setString(1, nomLot); + ps.setString(2, nomEquipementPhysique); + ps.execute(); + } + + try (var ps = conn.prepareStatement(""" + DELETE FROM ind_indicateur_impact_equipement_virtuel + WHERE nom_lot = ? AND nom_equipement = ? + """)) { + ps.setString(1, nomLot); + ps.setString(2, nomEquipementPhysique); + ps.execute(); + } + + try (var ps = conn.prepareStatement(""" + DELETE FROM ind_indicateur_impact_application + WHERE nom_lot = ? AND nom_equipement_physique = ? + """)) { + ps.setString(1, nomLot); + ps.setString(2, nomEquipementPhysique); + ps.execute(); + } + + try (var ps = conn.prepareStatement(""" + DELETE FROM ind_indicateur_impact_reseau + WHERE nom_lot = ? AND nom_equipement = ? + """)) { + ps.setString(1, nomLot); + ps.setString(2, nomEquipementPhysique); + ps.execute(); + } + conn.commit(); + } catch (SQLException e) { + log.error("Une erreur s'est produite lors de l'insertion dans PostgreSQL. Exception: ", e); + throw new DatabaseException(e); + } + } +} diff --git a/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/repository/MessagerieRepository.java b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/repository/MessagerieRepository.java new file mode 100644 index 00000000..eab00520 --- /dev/null +++ b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/repository/MessagerieRepository.java @@ -0,0 +1,74 @@ +package org.mte.numecoeval.calculs.infrastructure.repository; + +import lombok.extern.slf4j.Slf4j; +import org.mte.numecoeval.calculs.domain.exception.DatabaseException; +import org.mte.numecoeval.common.utils.ResultSetUtils; +import org.mte.numecoeval.topic.data.MessagerieDTO; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +@Component +@Slf4j +public class MessagerieRepository { + + @Autowired + private DataSource dataSource; + + public List<MessagerieDTO> findMessagerieDTOList(List<Long> ids) { + List<MessagerieDTO> result = new ArrayList<>(); + + try (Connection conn = dataSource.getConnection()) { + try (var ps = conn.prepareStatement(""" + SELECT * + FROM en_messagerie mes + WHERE id = ANY (?) + """)) { + + ps.setArray(1, conn.createArrayOf("long", ids.toArray(new Long[0]))); + + var rs = ps.executeQuery(); + while (rs.next()) { + result.add(MessagerieDTO.builder() + .id(rs.getLong("id")) + .nomLot(rs.getString("nom_lot")) + .dateLot(ResultSetUtils.getLocalDate(rs, "date_lot")) + .nomOrganisation(rs.getString("nom_organisation")) + .nomEntite(rs.getString("nom_entite")) + .nomSourceDonnee(rs.getString("nom_source_donnee")) + .moisAnnee(rs.getInt("mois_annee")) + .nombreMailEmis(ResultSetUtils.getInteger(rs, "nombre_mail_emis")) + .nombreMailEmisXDestinataires(ResultSetUtils.getInteger(rs, "nombre_mail_emisxdestinataires")) + .volumeTotalMailEmis(ResultSetUtils.getInteger(rs, "volume_total_mail_emis")) + .build()); + } + } + } catch (SQLException e) { + log.error("Une erreur s'est produite lors de la selection dans PostgreSQL. Exception: ", e); + throw new DatabaseException(e); + } + + return result; + } + + public void setStatutToTraite(Long id) { + try (Connection conn = dataSource.getConnection()) { + try (var ps = conn.prepareStatement(""" + UPDATE en_messagerie + SET statut_traitement = 'TRAITE', date_update = now() + WHERE id = ? + """)) { + ps.setLong(1, id); + ps.execute(); + } + } catch (SQLException e) { + log.error("Une erreur s'est produite lors de la mise a jour dans PostgreSQL. Exception: ", e); + throw new DatabaseException(e); + } + } +} diff --git a/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/service/calcul/CalculApplicationService.java b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/service/calcul/CalculApplicationService.java new file mode 100644 index 00000000..04c5481c --- /dev/null +++ b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/service/calcul/CalculApplicationService.java @@ -0,0 +1,45 @@ +package org.mte.numecoeval.calculs.infrastructure.service.calcul; + +import lombok.AllArgsConstructor; +import org.mte.numecoeval.calculs.domain.data.demande.DemandeCalculImpactApplication; +import org.mte.numecoeval.calculs.domain.data.entree.Application; +import org.mte.numecoeval.calculs.domain.data.indicateurs.ImpactApplication; +import org.mte.numecoeval.calculs.domain.data.indicateurs.ImpactEquipementVirtuel; +import org.mte.numecoeval.calculs.domain.port.input.service.CalculImpactApplicationService; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +@Service +@AllArgsConstructor +public class CalculApplicationService { + + CalculImpactApplicationService calculImpactApplicationService; + + /** + * Calcule les impacts d'une application + * Returne la liste d'impacts à partir des impacts de son equipement virtuel + * + * @param impactEquipementVirtuelList la liste d'impact de l'equipement virtuel + * @param application l'application + * @param nbApplications nombre d'application de l'équipement virtuel + * @return la liste d'impacts + */ + public List<ImpactApplication> calculImpactApplication(List<ImpactEquipementVirtuel> impactEquipementVirtuelList, Application application, Integer nbApplications) { + + if (impactEquipementVirtuelList.isEmpty()) return new ArrayList<>(); + + LocalDateTime dateCalcul = LocalDateTime.now(); + return impactEquipementVirtuelList.stream() + .map(impact -> calculImpactApplicationService.calculImpactApplicatif( + DemandeCalculImpactApplication.builder() + .dateCalcul(dateCalcul) + .application(application) + .nbApplications(nbApplications) + .impactEquipementVirtuel(impact) + .build())) + .toList(); + } +} diff --git a/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/service/calcul/CalculEquipementPhysiqueService.java b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/service/calcul/CalculEquipementPhysiqueService.java new file mode 100644 index 00000000..ee3e0b4b --- /dev/null +++ b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/service/calcul/CalculEquipementPhysiqueService.java @@ -0,0 +1,64 @@ +package org.mte.numecoeval.calculs.infrastructure.service.calcul; + +import lombok.AllArgsConstructor; +import org.mte.numecoeval.calculs.domain.data.demande.DemandeCalculImpactEquipementPhysique; +import org.mte.numecoeval.calculs.domain.data.indicateurs.ImpactEquipementPhysique; +import org.mte.numecoeval.calculs.domain.model.CalculEquipementPhysique; +import org.mte.numecoeval.calculs.domain.port.input.service.CalculImpactEquipementPhysiqueService; +import org.mte.numecoeval.calculs.infrastructure.mapper.EntreesMapper; +import org.mte.numecoeval.calculs.infrastructure.mapper.ReferentielMapper; +import org.mte.numecoeval.calculs.referentiels.generated.api.model.CritereDTO; +import org.mte.numecoeval.calculs.referentiels.generated.api.model.EtapeDTO; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +@Service +@AllArgsConstructor +public class CalculEquipementPhysiqueService { + + EntreesMapper entreesMapper; + + ReferentielMapper referentielMapper; + + CalculImpactEquipementPhysiqueService calculImpactEquipementPhysiqueService; + + /** + * Calcule les impacts d'un equipement physique + * Returne la liste d'impacts en iterant par (etape, critere) + * + * @param calculEquipementPhysique l'equipement physique enrichi + * @return la liste d'impacts + */ + public List<ImpactEquipementPhysique> calculImpactEquipementPhysique(CalculEquipementPhysique calculEquipementPhysique) { + + LocalDateTime dateCalcul = LocalDateTime.now(); + + List<ImpactEquipementPhysique> impactEquipementPhysiqueList = new ArrayList<>(); + + for (EtapeDTO etapeDTO : calculEquipementPhysique.getEtapes()) { + for (CritereDTO critereDTO : calculEquipementPhysique.getCriteres()) { + + DemandeCalculImpactEquipementPhysique demandeCalculImpactEquipementPhysique = DemandeCalculImpactEquipementPhysique.builder() + .dateCalcul(dateCalcul) + .equipementPhysique(entreesMapper.toDomain(calculEquipementPhysique.getEquipementPhysique())) + .etape(referentielMapper.toEtape(etapeDTO)) + .critere(referentielMapper.toCritere(critereDTO)) + .typeEquipement(referentielMapper.toTypeEquipement(calculEquipementPhysique.getTypeEquipement())) + .correspondanceRefEquipement(referentielMapper.toCorrespondanceRefEquipement(calculEquipementPhysique.getCorrespondanceRefEquipement())) + .hypotheses(referentielMapper.toListHypothese(calculEquipementPhysique.getHypotheses())) + .mixElectriques(referentielMapper.toListMixElectrique(calculEquipementPhysique.getMixElectriques())) + .impactEquipements(referentielMapper.toListImpactEquipement(calculEquipementPhysique.getImpactsEquipement())) + .build(); + + impactEquipementPhysiqueList.add( + calculImpactEquipementPhysiqueService.calculerImpactEquipementPhysique(demandeCalculImpactEquipementPhysique) + ); + } + } + return impactEquipementPhysiqueList; + + } +} diff --git a/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/service/calcul/CalculEquipementVirtuelService.java b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/service/calcul/CalculEquipementVirtuelService.java new file mode 100644 index 00000000..7b21fb27 --- /dev/null +++ b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/service/calcul/CalculEquipementVirtuelService.java @@ -0,0 +1,88 @@ +package org.mte.numecoeval.calculs.infrastructure.service.calcul; + +import lombok.AllArgsConstructor; +import org.mte.numecoeval.calculs.domain.data.demande.DemandeCalculImpactEquipementVirtuel; +import org.mte.numecoeval.calculs.domain.data.entree.EquipementVirtuel; +import org.mte.numecoeval.calculs.domain.data.indicateurs.ImpactEquipementPhysique; +import org.mte.numecoeval.calculs.domain.data.indicateurs.ImpactEquipementVirtuel; +import org.mte.numecoeval.calculs.domain.port.input.service.CalculImpactEquipementVirtuelService; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + + +@Service +@AllArgsConstructor +public class CalculEquipementVirtuelService { + + CalculImpactEquipementVirtuelService calculImpactEquipementVirtuelService; + + + /** + * Calcule les impacts d'un equipement virtuel + * Returne la liste d'impacts à partir des impacts de son equipement physique + * + * @param impactEquipementPhysiqueList la liste d'impact de l'equipement physique + * @param equipementVirtuel l'equipement virtuel + * @param nbEquipementsVirtuels nombre d'equipement virtuels lies a l'equipement physique + * @param nbTotalVCPU nombre total de vcpu de l'equipement physique + * @param stockageTotalVirtuel stockage total + * @return la liste d'impacts + */ + public List<ImpactEquipementVirtuel> calculImpactEquipementVirtuel( + List<ImpactEquipementPhysique> impactEquipementPhysiqueList, + EquipementVirtuel equipementVirtuel, + Integer nbEquipementsVirtuels, + Integer nbTotalVCPU, + Double stockageTotalVirtuel) { + + if (impactEquipementPhysiqueList.isEmpty()) return new ArrayList<>(); + + LocalDateTime dateCalcul = LocalDateTime.now(); + + return impactEquipementPhysiqueList.stream() + .map(impact -> calculImpactEquipementVirtuelService.calculerImpactEquipementVirtuel( + DemandeCalculImpactEquipementVirtuel.builder() + .dateCalcul(dateCalcul) + .equipementVirtuel(equipementVirtuel) + .nbEquipementsVirtuels(nbEquipementsVirtuels) + .nbTotalVCPU(nbTotalVCPU) + .stockageTotalVirtuel(stockageTotalVirtuel) + .impactEquipement(impact) + .build())) + .toList(); + } + + /** + * Calcule le totalVCPU d'une liste d'équipements virtuels + * Somme le champs VCPU + * + * @param equipementVirtuelList la liste d'equipements virtuels + * @return le total de VCPU + */ + public Integer getTotalVCPU(List<EquipementVirtuel> equipementVirtuelList) { + Integer totalVCPU = null; + if (equipementVirtuelList.stream().noneMatch(vm -> vm.getVCPU() == null || vm.getVCPU() == 0)) { + totalVCPU = equipementVirtuelList.stream().mapToInt(EquipementVirtuel::getVCPU).sum(); + } + + return totalVCPU; + } + + /** + * Calcule le totalStockage d'une liste d'équipements virtuels + * Somme le champs capaciteStockage + * + * @param equipementVirtuelList la liste d'equipements virtuels + * @return le total de capaciteStockage + */ + public Double getTotalStockage(List<EquipementVirtuel> equipementVirtuelList) { + Double totalStockage = null; + if (equipementVirtuelList.stream().noneMatch(vm -> vm.getCapaciteStockage() == null || vm.getCapaciteStockage() == 0)) { + totalStockage = equipementVirtuelList.stream().mapToDouble(EquipementVirtuel::getCapaciteStockage).sum(); + } + return totalStockage; + } +} \ No newline at end of file diff --git a/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/service/calcul/CalculMessagerieService.java b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/service/calcul/CalculMessagerieService.java new file mode 100644 index 00000000..0c082091 --- /dev/null +++ b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/service/calcul/CalculMessagerieService.java @@ -0,0 +1,48 @@ +package org.mte.numecoeval.calculs.infrastructure.service.calcul; + +import lombok.AllArgsConstructor; +import org.mte.numecoeval.calculs.domain.data.demande.DemandeCalculImpactMessagerie; +import org.mte.numecoeval.calculs.domain.data.indicateurs.ImpactMessagerie; +import org.mte.numecoeval.calculs.domain.model.CalculMessagerie; +import org.mte.numecoeval.calculs.domain.port.input.service.CalculImpactMessagerieService; +import org.mte.numecoeval.calculs.infrastructure.mapper.EntreesMapper; +import org.mte.numecoeval.calculs.infrastructure.mapper.ReferentielMapper; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +@Service +@AllArgsConstructor +public class CalculMessagerieService { + + EntreesMapper entreesMapper; + ReferentielMapper referentielMapper; + + CalculImpactMessagerieService calculImpactMessagerieService; + + /** + * Calcule les impacts d'une messagerie + * Returne la liste d'impacts + * + * @param calculMessagerie la messagerie enrichie + * @return la liste d'impacts + */ + public List<ImpactMessagerie> calculImpactEquipementVirtuel(CalculMessagerie calculMessagerie) { + + if (calculMessagerie.getCriteres() == null) return new ArrayList<>(); + + LocalDateTime dateCalcul = LocalDateTime.now(); + + return calculMessagerie.getCriteres().stream() + .map(critereDTO -> calculImpactMessagerieService.calculerImpactMessagerie(DemandeCalculImpactMessagerie.builder() + .dateCalcul(dateCalcul) + .messagerie(entreesMapper.toDomain(calculMessagerie.getMessagerie())) + .critere(referentielMapper.toCritere(critereDTO)) + .impactsMessagerie(referentielMapper.toListImpactMessagerie(calculMessagerie.getImpactsMessagerie())) + .build()) + ).toList(); + + } +} diff --git a/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/service/calcul/CalculReseauService.java b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/service/calcul/CalculReseauService.java new file mode 100644 index 00000000..307fc4a6 --- /dev/null +++ b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/service/calcul/CalculReseauService.java @@ -0,0 +1,58 @@ +package org.mte.numecoeval.calculs.infrastructure.service.calcul; + +import lombok.AllArgsConstructor; +import org.mte.numecoeval.calculs.domain.data.demande.DemandeCalculImpactReseau; +import org.mte.numecoeval.calculs.domain.data.indicateurs.ImpactReseau; +import org.mte.numecoeval.calculs.domain.model.CalculEquipementPhysique; +import org.mte.numecoeval.calculs.domain.port.input.service.CalculImpactReseauService; +import org.mte.numecoeval.calculs.infrastructure.mapper.EntreesMapper; +import org.mte.numecoeval.calculs.infrastructure.mapper.ReferentielMapper; +import org.mte.numecoeval.calculs.referentiels.generated.api.model.CritereDTO; +import org.mte.numecoeval.calculs.referentiels.generated.api.model.EtapeDTO; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +@Service +@AllArgsConstructor +public class CalculReseauService { + + EntreesMapper entreesMapper; + ReferentielMapper referentielMapper; + + CalculImpactReseauService calculImpactReseauService; + + /** + * Calcule les impacts réseau d'un équipement physique + * Returne la liste d'impacts + * + * @param calculEquipementPhysique l'equipement physique + * @return la liste d'impacts + */ + public List<ImpactReseau> calculImpactReseau(CalculEquipementPhysique calculEquipementPhysique) { + + List<ImpactReseau> impactReseauList = new ArrayList<>(); + + if (calculEquipementPhysique == null || + calculEquipementPhysique.getEquipementPhysique() == null || + calculEquipementPhysique.getEquipementPhysique().getGoTelecharge() == null) return impactReseauList; + + LocalDateTime dateCalcul = LocalDateTime.now(); + + for (EtapeDTO etape : calculEquipementPhysique.getEtapes()) { + for (CritereDTO critere : calculEquipementPhysique.getCriteres()) { + impactReseauList.add(calculImpactReseauService.calculerImpactReseau(DemandeCalculImpactReseau.builder() + .dateCalcul(dateCalcul) + .equipementPhysique(entreesMapper.toDomain(calculEquipementPhysique.getEquipementPhysique())) + .etape(referentielMapper.toEtape(etape)) + .critere(referentielMapper.toCritere(critere)) + .impactsReseau(referentielMapper.toListImpactReseau(calculEquipementPhysique.getImpactsReseau())) + .build())); + } + } + + return impactReseauList; + } +} \ No newline at end of file diff --git a/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/service/calcul/IntegrationCalculEquipementsService.java b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/service/calcul/IntegrationCalculEquipementsService.java new file mode 100644 index 00000000..28a5d72b --- /dev/null +++ b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/service/calcul/IntegrationCalculEquipementsService.java @@ -0,0 +1,130 @@ +package org.mte.numecoeval.calculs.infrastructure.service.calcul; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.mte.numecoeval.calculs.domain.data.entree.Application; +import org.mte.numecoeval.calculs.domain.data.entree.EquipementVirtuel; +import org.mte.numecoeval.calculs.domain.data.indicateurs.ImpactApplication; +import org.mte.numecoeval.calculs.domain.data.indicateurs.ImpactEquipementPhysique; +import org.mte.numecoeval.calculs.domain.data.indicateurs.ImpactEquipementVirtuel; +import org.mte.numecoeval.calculs.domain.data.indicateurs.ImpactReseau; +import org.mte.numecoeval.calculs.domain.model.CalculEquipementPhysique; +import org.mte.numecoeval.calculs.domain.model.CalculSizes; +import org.mte.numecoeval.calculs.infrastructure.repository.ApplicationRepository; +import org.mte.numecoeval.calculs.infrastructure.repository.EquipementPhysiqueRepository; +import org.mte.numecoeval.calculs.infrastructure.repository.EquipementVirtuelRepository; +import org.mte.numecoeval.calculs.infrastructure.repository.IndicateurRepository; +import org.mte.numecoeval.topic.data.EquipementPhysiqueDTO; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; + +@Slf4j +@Service +@AllArgsConstructor +public class IntegrationCalculEquipementsService { + + IndicateurRepository indicateurRepository; + + EquipementPhysiqueRepository equipementPhysiqueRepository; + EquipementVirtuelRepository equipementVirtuelRepository; + ApplicationRepository applicationRepository; + CalculEquipementPhysiqueService calculEquipementPhysiqueService; + CalculEquipementVirtuelService calculEquipementVirtuelService; + CalculApplicationService calculApplicationService; + CalculReseauService calculReseauService; + + /** + * Calcul les impacts d'un équipement physique enrichi, liste des impacts: + * - equipement physique + * - ses équipements virtuels + * - les applications sur ses équipements virtuels + * - réseaux + * + * @param calculEquipementPhysique l'équipement physique enrichi + * @return le nombre d'elements traites sous forme CalculSizes + */ + public CalculSizes calculImpactEquipementPhysique(CalculEquipementPhysique calculEquipementPhysique) { + + if (calculEquipementPhysique == null) return null; + + var ttStart = System.currentTimeMillis(); + + EquipementPhysiqueDTO equipementPhysiqueDTO = calculEquipementPhysique.getEquipementPhysique(); + String nomLot = equipementPhysiqueDTO.getNomLot(); + String nomOrganisation = equipementPhysiqueDTO.getNomOrganisation(); + String nomEquipementPhysique = equipementPhysiqueDTO.getNomEquipementPhysique(); + + // delete existing indicateurs + indicateurRepository.clearIndicateurs(nomEquipementPhysique, nomLot); + + var ttAfterClear = System.currentTimeMillis(); + + List<ImpactEquipementPhysique> impactEquipementPhysiqueList = calculEquipementPhysiqueService.calculImpactEquipementPhysique(calculEquipementPhysique); + List<ImpactReseau> impactReseauList = calculReseauService.calculImpactReseau(calculEquipementPhysique); + + // get equipements virtuels + List<EquipementVirtuel> equipementVirtuelList = equipementVirtuelRepository.findEquipementVirtuels(nomOrganisation, nomLot, nomEquipementPhysique); + var ttAfterFindEqV = System.currentTimeMillis(); + + List<ImpactEquipementVirtuel> allImpactVirtuel = new ArrayList<>(); + List<ImpactApplication> allImpactApplication = new ArrayList<>(); + var totalNbApplication = 0; + + var totalVCPU = calculEquipementVirtuelService.getTotalVCPU(equipementVirtuelList); + var totalStockage = calculEquipementVirtuelService.getTotalStockage(equipementVirtuelList); + + for (EquipementVirtuel equipementVirtuel : equipementVirtuelList) { + List<ImpactEquipementVirtuel> impactEquipementVirtuelList = calculEquipementVirtuelService.calculImpactEquipementVirtuel( + impactEquipementPhysiqueList, equipementVirtuel, + equipementVirtuelList.size(), totalVCPU, totalStockage + ); + + allImpactVirtuel.addAll(impactEquipementVirtuelList); + + List<Application> applicationList = applicationRepository.findApplications(nomOrganisation, nomLot, nomEquipementPhysique, equipementVirtuel.getNomEquipementVirtuel()); + + totalNbApplication += applicationList.size(); + for (Application application : applicationList) { + List<ImpactApplication> impactApplicationList = calculApplicationService.calculImpactApplication(impactEquipementVirtuelList, application, applicationList.size()); + allImpactApplication.addAll(impactApplicationList); + } + } + + var ttAfterEqVAndApp = System.currentTimeMillis(); + + // save impacts into db + indicateurRepository.saveIndicateursEquipements(impactEquipementPhysiqueList, allImpactVirtuel, allImpactApplication, impactReseauList); + + var ttAfterSave = System.currentTimeMillis(); + + // set statut of equipement physique to TRAITE + equipementPhysiqueRepository.setStatutToTraite(equipementPhysiqueDTO.getId()); + + var ttAfterSaveStatus = System.currentTimeMillis(); + + log.info("{} - {} - {} : Calcul impacts equipement physique: {} [{}, {}]. Temps (ms): total={}, clear={}, eqV={} finEqVApp={}, saveInd={}, saveStatus={}", + nomOrganisation, nomLot, equipementPhysiqueDTO.getDateLot(), + nomEquipementPhysique, + equipementVirtuelList.size(), + totalNbApplication, + ttAfterSaveStatus - ttStart, + ttAfterClear - ttStart, + ttAfterFindEqV -ttAfterClear, + ttAfterEqVAndApp - ttAfterFindEqV, + ttAfterSave - ttAfterEqVAndApp, + ttAfterSaveStatus - ttAfterSave + ); + + CalculSizes result = new CalculSizes(); + result.setNbEquipementPhysique(1); + result.setNbEquipementVirtuel(equipementVirtuelList.size()); + result.setNbApplication(totalNbApplication); + result.setNbIndicateurEquipementPhysique(impactEquipementPhysiqueList.size()); + result.setNbIndicateurEquipementVirtuel(allImpactVirtuel.size()); + result.setNbIndicateurApplication(allImpactApplication.size()); + result.setNbIndicateurReseau(impactReseauList.size()); + return result; + } +} \ No newline at end of file diff --git a/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/service/calcul/IntegrationCalculMessagerieService.java b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/service/calcul/IntegrationCalculMessagerieService.java new file mode 100644 index 00000000..73897447 --- /dev/null +++ b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/service/calcul/IntegrationCalculMessagerieService.java @@ -0,0 +1,42 @@ +package org.mte.numecoeval.calculs.infrastructure.service.calcul; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.mte.numecoeval.calculs.domain.model.CalculMessagerie; +import org.mte.numecoeval.calculs.domain.model.CalculSizes; +import org.mte.numecoeval.calculs.infrastructure.repository.IndicateurRepository; +import org.mte.numecoeval.calculs.infrastructure.repository.MessagerieRepository; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +@AllArgsConstructor +public class IntegrationCalculMessagerieService { + + IndicateurRepository indicateurRepository; + + CalculMessagerieService calculMessagerieService; + + MessagerieRepository messagerieRepository; + + public CalculSizes calculImpactMessagerie(CalculMessagerie calculMessagerie) { + if (calculMessagerie == null) return null; + + var messagerie = calculMessagerie.getMessagerie(); + + log.debug("{} - {} - {} : Calcul impact messagerie : {}, Nombre de Critère : {}", + messagerie.getNomOrganisation(), messagerie.getNomLot(), messagerie.getDateLot(), + messagerie.getId(), + calculMessagerie.getCriteres().size() + ); + + var impactsMessagerie = calculMessagerieService.calculImpactEquipementVirtuel(calculMessagerie); + indicateurRepository.saveIndicateursMessagerie(impactsMessagerie); + messagerieRepository.setStatutToTraite(messagerie.getId()); + + var result = new CalculSizes(); + result.setNbMessagerie(1); + result.setNbIndicateurMessagerie(impactsMessagerie.size()); + return result; + } +} diff --git a/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/service/calcul/MainEquipementPhysiqueService.java b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/service/calcul/MainEquipementPhysiqueService.java new file mode 100644 index 00000000..3a070d3a --- /dev/null +++ b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/service/calcul/MainEquipementPhysiqueService.java @@ -0,0 +1,23 @@ +package org.mte.numecoeval.calculs.infrastructure.service.calcul; + +import lombok.AllArgsConstructor; +import org.mte.numecoeval.calculs.domain.model.CalculSizes; +import org.mte.numecoeval.calculs.infrastructure.service.enrichissement.EnrichissementEquipementPhysiqueService; +import org.mte.numecoeval.topic.data.EquipementPhysiqueDTO; +import org.springframework.stereotype.Service; + +@Service +@AllArgsConstructor +public class MainEquipementPhysiqueService { + + private EnrichissementEquipementPhysiqueService enrichissementEquipementPhysiqueService; + private IntegrationCalculEquipementsService integrationCalculEquipementsService; + + public CalculSizes calcul(EquipementPhysiqueDTO equipementPhysiqueDTO) { + + var eqPhEnrichi = enrichissementEquipementPhysiqueService.serviceEnrichissementEquipementPhysique(equipementPhysiqueDTO); + return integrationCalculEquipementsService.calculImpactEquipementPhysique(eqPhEnrichi); + + } + +} diff --git a/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/service/calcul/MainMessagerieService.java b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/service/calcul/MainMessagerieService.java new file mode 100644 index 00000000..fda3ab9c --- /dev/null +++ b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/service/calcul/MainMessagerieService.java @@ -0,0 +1,23 @@ +package org.mte.numecoeval.calculs.infrastructure.service.calcul; + +import lombok.AllArgsConstructor; +import org.mte.numecoeval.calculs.domain.model.CalculSizes; +import org.mte.numecoeval.calculs.infrastructure.service.enrichissement.EnrichissementMessagerieService; +import org.mte.numecoeval.topic.data.MessagerieDTO; +import org.springframework.stereotype.Service; + +@Service +@AllArgsConstructor +public class MainMessagerieService { + + private EnrichissementMessagerieService enrichissementMessagerieService; + private IntegrationCalculMessagerieService integrationCalculMessagerieService; + + public CalculSizes calcul(MessagerieDTO messagerieDTO) { + + var messagerieEnrichie = enrichissementMessagerieService.serviceEnrichissementMessagerie(messagerieDTO); + return integrationCalculMessagerieService.calculImpactMessagerie(messagerieEnrichie); + + } + +} diff --git a/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/service/enrichissement/EnrichissementEquipementPhysiqueService.java b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/service/enrichissement/EnrichissementEquipementPhysiqueService.java new file mode 100644 index 00000000..1a23d86f --- /dev/null +++ b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/service/enrichissement/EnrichissementEquipementPhysiqueService.java @@ -0,0 +1,97 @@ +package org.mte.numecoeval.calculs.infrastructure.service.enrichissement; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.mte.numecoeval.calculs.domain.model.CalculEquipementPhysique; +import org.mte.numecoeval.calculs.infrastructure.client.ReferentielClient; +import org.mte.numecoeval.calculs.referentiels.generated.api.model.HypotheseDTO; +import org.mte.numecoeval.topic.data.EquipementPhysiqueDTO; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; + +@Slf4j +@Service +@AllArgsConstructor +public class EnrichissementEquipementPhysiqueService { + + private ReferentielClient referentielClient; + + public CalculEquipementPhysique serviceEnrichissementEquipementPhysique(EquipementPhysiqueDTO equipementPhysiqueDTO) { + if (equipementPhysiqueDTO == null) return null; + + log.debug("{} - {} - {} : Enrichissement d'un équipement physique : Nom Equipement: {}, Type: {}, Modele: {}", + equipementPhysiqueDTO.getNomOrganisation(), equipementPhysiqueDTO.getNomLot(), equipementPhysiqueDTO.getDateLot(), + equipementPhysiqueDTO.getNomEquipementPhysique(), equipementPhysiqueDTO.getType(), equipementPhysiqueDTO.getModele()); + + CalculEquipementPhysique calculEquipementPhysique = new CalculEquipementPhysique(); + + calculEquipementPhysique.setEquipementPhysique(equipementPhysiqueDTO); + + calculEquipementPhysique.setEtapes(referentielClient.getEtapes()); + calculEquipementPhysique.setCriteres(referentielClient.getCriteres()); + + var hypotheses = new ArrayList<HypotheseDTO>(); + var hDuree = referentielClient.getHypothese("dureeVieParDefaut"); + var hPue = referentielClient.getHypothese("PUEParDefaut"); + var tuBYOD = referentielClient.getHypothese("taux_utilisation_BYOD"); + var tuCOPE = referentielClient.getHypothese("taux_utilisation_COPE"); + + if (hDuree != null) hypotheses.add(hDuree); + if (hPue != null) hypotheses.add(hPue); + if (tuBYOD != null) hypotheses.add(tuBYOD); + if (tuCOPE != null) hypotheses.add(tuCOPE); + + calculEquipementPhysique.setHypotheses(hypotheses); + + calculEquipementPhysique.setCorrespondanceRefEquipement(referentielClient.getCorrespondanceRefEquipement(equipementPhysiqueDTO.getModele())); + calculEquipementPhysique.setTypeEquipement(referentielClient.getTypeEquipement(equipementPhysiqueDTO.getType())); + + calculEquipementPhysique.setImpactsEquipement(new ArrayList<>()); + calculEquipementPhysique.setImpactsReseau(new ArrayList<>()); + calculEquipementPhysique.setMixElectriques(new ArrayList<>()); + for (var critere : calculEquipementPhysique.getCriteres()) { + for (var etape : calculEquipementPhysique.getEtapes()) { + if (calculEquipementPhysique.getCorrespondanceRefEquipement() != null) { + calculEquipementPhysique.getImpactsEquipement().add( + referentielClient.getImpactEquipement( + calculEquipementPhysique.getCorrespondanceRefEquipement().getRefEquipementCible(), + critere.getNomCritere(), + etape.getCode() + ) + ); + } else if (calculEquipementPhysique.getTypeEquipement() != null + && StringUtils.isNotBlank(calculEquipementPhysique.getTypeEquipement().getRefEquipementParDefaut())) { + calculEquipementPhysique.getImpactsEquipement().add( + referentielClient.getImpactEquipement( + calculEquipementPhysique.getTypeEquipement().getRefEquipementParDefaut(), + critere.getNomCritere(), + etape.getCode() + ) + ); + } + calculEquipementPhysique.getImpactsReseau().add( + referentielClient.getImpactReseau( + "impactReseauMobileMoyen", + critere.getNomCritere(), + etape.getCode() + ) + ); + } + + var mixElec = referentielClient.getMixElectrique(equipementPhysiqueDTO.getPaysDUtilisation(), critere.getNomCritere()); + if (mixElec != null) calculEquipementPhysique.getMixElectriques().add(mixElec); + + if (equipementPhysiqueDTO.getDataCenter() != null + && StringUtils.isNotBlank(equipementPhysiqueDTO.getDataCenter().getLocalisation()) + && !StringUtils.equals(equipementPhysiqueDTO.getPaysDUtilisation(), equipementPhysiqueDTO.getDataCenter().getLocalisation())) { + + var mixElecDataCenter = referentielClient.getMixElectrique(equipementPhysiqueDTO.getDataCenter().getLocalisation(), critere.getNomCritere()); + if (mixElecDataCenter != null) calculEquipementPhysique.getMixElectriques().add(mixElecDataCenter); + } + } + + return calculEquipementPhysique; + } +} diff --git a/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/service/enrichissement/EnrichissementMessagerieService.java b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/service/enrichissement/EnrichissementMessagerieService.java new file mode 100644 index 00000000..a4c823dd --- /dev/null +++ b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/service/enrichissement/EnrichissementMessagerieService.java @@ -0,0 +1,35 @@ +package org.mte.numecoeval.calculs.infrastructure.service.enrichissement; + +import lombok.AllArgsConstructor; +import org.mte.numecoeval.calculs.domain.model.CalculMessagerie; +import org.mte.numecoeval.calculs.infrastructure.client.ReferentielClient; +import org.mte.numecoeval.topic.data.MessagerieDTO; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.Objects; + +@Service +@AllArgsConstructor +public class EnrichissementMessagerieService { + + private ReferentielClient referentielClient; + + public CalculMessagerie serviceEnrichissementMessagerie(MessagerieDTO messagerieDTO) { + + if (Objects.isNull(messagerieDTO)) return null; + + CalculMessagerie calculMessagerie = new CalculMessagerie(); + calculMessagerie.setMessagerie(messagerieDTO); + + calculMessagerie.setCriteres(referentielClient.getCriteres()); + calculMessagerie.setImpactsMessagerie(new ArrayList<>()); + + calculMessagerie.setImpactsMessagerie(calculMessagerie.getCriteres().stream() + .map(critereDTO -> referentielClient.getMessagerie(critereDTO.getNomCritere())) + .filter(Objects::nonNull) + .toList()); + + return calculMessagerie; + } +} diff --git a/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/service/rest/calculs/CheckIndicateurService.java b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/service/rest/calculs/CheckIndicateurService.java new file mode 100644 index 00000000..37a48364 --- /dev/null +++ b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/service/rest/calculs/CheckIndicateurService.java @@ -0,0 +1,11 @@ +package org.mte.numecoeval.calculs.infrastructure.service.rest.calculs; + +import org.springframework.stereotype.Service; + +@Service +public class CheckIndicateurService { + + public boolean isOk(String statutIndicateur) { + return "OK".equals(statutIndicateur); + } +} diff --git a/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/service/sync/calculs/SyncCalculService.java b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/service/sync/calculs/SyncCalculService.java new file mode 100644 index 00000000..7afd3534 --- /dev/null +++ b/services/api-event-calculs/src/main/java/org/mte/numecoeval/calculs/infrastructure/service/sync/calculs/SyncCalculService.java @@ -0,0 +1,51 @@ +package org.mte.numecoeval.calculs.infrastructure.service.sync.calculs; + +import lombok.AllArgsConstructor; +import org.mte.numecoeval.calculs.domain.model.CalculSizes; +import org.mte.numecoeval.calculs.infrastructure.repository.EquipementPhysiqueRepository; +import org.mte.numecoeval.calculs.infrastructure.repository.MessagerieRepository; +import org.mte.numecoeval.calculs.infrastructure.service.calcul.MainEquipementPhysiqueService; +import org.mte.numecoeval.calculs.infrastructure.service.calcul.MainMessagerieService; +import org.mte.numecoeval.calculs.sync.generated.api.model.ReponseCalculRest; +import org.mte.numecoeval.calculs.sync.generated.api.model.SyncCalculRest; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Objects; + +@Service +@AllArgsConstructor +public class SyncCalculService { + + private EquipementPhysiqueRepository equipementPhysiqueRepository; + private MessagerieRepository messagerieRepository; + private MainEquipementPhysiqueService mainEquipementPhysiqueService; + private MainMessagerieService mainMessagerieService; + + + public ReponseCalculRest calcul(SyncCalculRest syncCalculRest) { + var result = new ReponseCalculRest(); + + if (syncCalculRest.getEquipementPhysiqueIds() != null && !syncCalculRest.getEquipementPhysiqueIds().isEmpty()) { + List<CalculSizes> calculSizesList = equipementPhysiqueRepository.findEquipementPhysiqueDTOs(syncCalculRest.getEquipementPhysiqueIds()).stream() + .map(equipementPhysiqueDTO -> mainEquipementPhysiqueService.calcul(equipementPhysiqueDTO)) + .filter(Objects::nonNull) + .toList(); + + result.setNbrEquipementPhysique(calculSizesList.stream().map(CalculSizes::getNbEquipementPhysique).reduce(0L, Long::sum)); + result.setNbrEquipementVirtuel(calculSizesList.stream().map(CalculSizes::getNbEquipementVirtuel).reduce(0L, Long::sum)); + result.setNbrApplication(calculSizesList.stream().map(CalculSizes::getNbApplication).reduce(0L, Long::sum)); + } + + if (syncCalculRest.getMessagerieIds() != null && !syncCalculRest.getMessagerieIds().isEmpty()) { + List<CalculSizes> calculSizesMessagerieList = messagerieRepository.findMessagerieDTOList(syncCalculRest.getMessagerieIds()).stream() + .map(messagerieDTO -> mainMessagerieService.calcul(messagerieDTO)) + .filter(Objects::nonNull) + .toList(); + + result.setNbrMessagerie(calculSizesMessagerieList.stream().map(CalculSizes::getNbMessagerie).reduce(0L, Long::sum)); + } + + return result; + } +} diff --git a/services/api-event-calculs/src/main/resources/application.yaml b/services/api-event-calculs/src/main/resources/application.yaml new file mode 100644 index 00000000..9be26cc4 --- /dev/null +++ b/services/api-event-calculs/src/main/resources/application.yaml @@ -0,0 +1,59 @@ +spring: + sql: + init: + mode: always + # Base de données + datasource: + generate-unique-name: true + url: "jdbc:postgresql://localhost:5432/postgres?reWriteBatchedInserts=true" + username: postgres + password: postgres + driver-class-name: org.postgresql.Driver + tomcat: + test-on-borrow: false + jmx-enabled: false + max-active: 100 + # Kafka + kafka: + bootstrap-servers: localhost:9092 + back-off-sec: 10 + consumer: + group-id: api-event-calculs + auto-offset-reset: earliest + enable-auto-commit: false + max-poll-records: 50 + isolation.level: read_committed + value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer + key-deserializer: org.apache.kafka.common.serialization.StringDeserializer + properties: + spring.json.trusted.packages: "org.mte.numecoeval.*" + +# Application +numecoeval: + topic: + partition: "4" + equipementPhysique: "entree_equipementPhysique" + messagerie: "entree_messagerie" + referentiels: + url: "http://localhost:18080" + cache: + ttl: "20" + +server: + port: 18085 + shutdown: graceful + +# Actuator +management: + server: + port: 18085 + security: + user: + name: "actuator" + password: "actuator" + roles: ACTUATOR_ADMIN + endpoints: + web: + base-path: / + exposure: + include: health,prometheus,httptrace,info,metrics,mappings \ No newline at end of file diff --git a/services/api-event-calculs/src/main/resources/logback.xml b/services/api-event-calculs/src/main/resources/logback.xml new file mode 100644 index 00000000..f10f3276 --- /dev/null +++ b/services/api-event-calculs/src/main/resources/logback.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="UTF-8"?> +<configuration> + <!-- Spring default.xml --> + <conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter"/> + <conversionRule conversionWord="wex" + converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter"/> + <conversionRule conversionWord="wEx" + converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter"/> + + <property name="CONSOLE_LOG_PATTERN" + value="${CONSOLE_LOG_PATTERN:-%clr(%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%t]){faint} %clr(%-42.42logger{0}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/> + <property name="CONSOLE_LOG_CHARSET" value="${CONSOLE_LOG_CHARSET:-${file.encoding:-UTF-8}}"/> + + <statusListener class="ch.qos.logback.core.status.NopStatusListener"/> + + <!-- console-appender.xml--> + <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> + <encoder> + <pattern>${CONSOLE_LOG_PATTERN}</pattern> + <charset>${CONSOLE_LOG_CHARSET}</charset> + </encoder> + </appender> + + <root level="${APP_LOGGING_LEVEL:-INFO}"> + <appender-ref ref="CONSOLE"/> + </root> + <logger name="org.springframework.web" level="INFO"/> + <logger name="org.springframework.integration" level="INFO"/> + <logger name="org.springframework.kafka" level="WARN"/> + <logger name="org.apache.kafka" level="WARN"/> +</configuration> \ No newline at end of file diff --git a/services/api-event-calculs/src/main/resources/schema.sql b/services/api-event-calculs/src/main/resources/schema.sql new file mode 100644 index 00000000..c5b6cb8e --- /dev/null +++ b/services/api-event-calculs/src/main/resources/schema.sql @@ -0,0 +1,164 @@ +-- Creation des tables indicateurs +CREATE TABLE IF NOT EXISTS ind_indicateur_impact_equipement_physique +( + date_calcul timestamp NULL, + date_lot date NULL, + nom_lot varchar(255) NULL, + etapeacv varchar(255) NULL, + critere varchar(255) NULL, + "source" varchar(255) NULL, + statut_indicateur varchar(255) NULL, + trace text NULL, + version_calcul varchar(255) NULL, + conso_elec_moyenne float8 NULL, + impact_unitaire float8 NULL, + quantite int4 NULL, + statut_equipement_physique varchar(255) NULL, + type_equipement varchar(255) NULL, + unite varchar(255) NULL, + nom_entite varchar(255) NULL, + nom_organisation varchar(255) NULL, + nom_source_donnee varchar(255) NULL, + nom_equipement varchar(255) NULL +); + +CREATE TABLE IF NOT EXISTS ind_indicateur_impact_equipement_virtuel +( + date_calcul timestamp NULL, + date_lot date NULL, + nom_lot varchar(255) NULL, + etapeacv varchar(255) NULL, + critere varchar(255) NULL, + nom_organisation varchar(255) NULL, + nom_source_donnee varchar(255) NULL, + nom_equipement varchar(255) NULL, + nom_equipement_virtuel varchar(255) NULL, + nom_entite varchar(255) NULL, + "source" varchar(255) NULL, + statut_indicateur varchar(255) NULL, + trace text NULL, + version_calcul varchar(255) NULL, + impact_unitaire float8 NULL, + unite varchar(255) NULL, + conso_elec_moyenne float8 NULL, + "cluster" varchar(255) NULL +); + +CREATE TABLE IF NOT EXISTS ind_indicateur_impact_application +( + date_calcul timestamp NULL, + date_lot date NULL, + nom_lot varchar(255) NULL, + etapeacv varchar(255) NULL, + critere varchar(255) NULL, + nom_organisation varchar(255) NULL, + nom_source_donnee varchar(255) NULL, + nom_application varchar(255) NULL, + type_environnement varchar(255) NULL, + nom_equipement_physique varchar(255) NULL, + nom_equipement_virtuel varchar(255) NULL, + nom_entite varchar(255) NULL, + "source" varchar(255) NULL, + statut_indicateur varchar(255) NULL, + trace text NULL, + version_calcul varchar(255) NULL, + domaine varchar(255) NULL, + sous_domaine varchar(255) NULL, + impact_unitaire float8 NULL, + unite varchar(255) NULL, + conso_elec_moyenne float8 NULL +); + +CREATE TABLE IF NOT EXISTS ind_indicateur_impact_messagerie +( + date_calcul timestamp NULL, + date_lot date NULL, + nom_lot varchar(255) NULL, + nom_organisation varchar(255) NULL, + nom_source_donnee varchar(255) NULL, + critere varchar(255) NULL, + mois_annee date NULL, + nom_entite varchar(255) NULL, + "source" varchar(255) NULL, + statut_indicateur varchar(255) NULL, + trace text NULL, + version_calcul varchar(255) NULL, + impact_mensuel float8 NULL, + unite varchar(255) NULL, + nombre_mail_emis float8 NULL, + volume_total_mail_emis float8 NULL +); + +CREATE TABLE IF NOT EXISTS ind_indicateur_impact_reseau +( + date_calcul timestamp NULL, + date_lot date NULL, + nom_lot varchar(255) NULL, + etapeacv varchar(255) NULL, + critere varchar(255) NULL, + "source" varchar(255) NULL, + statut_indicateur varchar(255) NULL, + trace text NULL, + version_calcul varchar(255) NULL, + impact_unitaire float8 NULL, + unite varchar(255) NULL, + nom_entite varchar(255) NULL, + nom_organisation varchar(255) NULL, + nom_source_donnee varchar(255) NULL, + nom_equipement varchar(255) NULL +); + +ALTER TABLE IF EXISTS ind_indicateur_impact_equipement_physique ADD COLUMN IF NOT EXISTS date_lot_discriminator DATE NOT NULL DEFAULT '1970-01-01'; +UPDATE ind_indicateur_impact_equipement_physique SET date_lot_discriminator = coalesce(date_lot, '1970-01-01') where date_lot_discriminator is null; +ALTER TABLE IF EXISTS ind_indicateur_impact_reseau ADD COLUMN IF NOT EXISTS date_lot_discriminator DATE NOT NULL DEFAULT '1970-01-01'; +UPDATE ind_indicateur_impact_reseau SET date_lot_discriminator = coalesce(date_lot, '1970-01-01') where date_lot_discriminator is null; +ALTER TABLE IF EXISTS ind_indicateur_impact_equipement_virtuel ADD COLUMN IF NOT EXISTS date_lot_discriminator DATE NOT NULL DEFAULT '1970-01-01'; +UPDATE ind_indicateur_impact_equipement_virtuel SET date_lot_discriminator = coalesce(date_lot, '1970-01-01') where date_lot_discriminator is null; +ALTER TABLE IF EXISTS ind_indicateur_impact_application ADD COLUMN IF NOT EXISTS date_lot_discriminator DATE NOT NULL DEFAULT '1970-01-01'; +UPDATE ind_indicateur_impact_application SET date_lot_discriminator = coalesce(date_lot, '1970-01-01') where date_lot_discriminator is null; +ALTER TABLE IF EXISTS ind_indicateur_impact_messagerie ADD COLUMN IF NOT EXISTS date_lot_discriminator DATE NOT NULL DEFAULT '1970-01-01'; +UPDATE ind_indicateur_impact_messagerie SET date_lot_discriminator = coalesce(date_lot, '1970-01-01') where date_lot_discriminator is null; + +ALTER TABLE IF EXISTS ind_indicateur_impact_equipement_physique ADD COLUMN IF NOT EXISTS nom_organisation_discriminator varchar(255) NOT NULL DEFAULT ''; +ALTER TABLE IF EXISTS ind_indicateur_impact_equipement_physique ALTER COLUMN nom_organisation_discriminator SET NOT NULL; +ALTER TABLE IF EXISTS ind_indicateur_impact_reseau ADD COLUMN IF NOT EXISTS nom_organisation_discriminator varchar(255) NOT NULL DEFAULT ''; +ALTER TABLE IF EXISTS ind_indicateur_impact_reseau ALTER COLUMN nom_organisation_discriminator SET NOT NULL; +ALTER TABLE IF EXISTS ind_indicateur_impact_equipement_virtuel ADD COLUMN IF NOT EXISTS nom_organisation_discriminator varchar(255) NOT NULL DEFAULT ''; +ALTER TABLE IF EXISTS ind_indicateur_impact_equipement_virtuel ALTER COLUMN nom_organisation_discriminator SET NOT NULL; +ALTER TABLE IF EXISTS ind_indicateur_impact_application ADD COLUMN IF NOT EXISTS nom_organisation_discriminator varchar(255) NOT NULL DEFAULT ''; +ALTER TABLE IF EXISTS ind_indicateur_impact_application ALTER COLUMN nom_organisation_discriminator SET NOT NULL; +ALTER TABLE IF EXISTS ind_indicateur_impact_messagerie ADD COLUMN IF NOT EXISTS nom_organisation_discriminator varchar(255) NOT NULL DEFAULT ''; +ALTER TABLE IF EXISTS ind_indicateur_impact_messagerie ALTER COLUMN nom_organisation_discriminator SET NOT NULL; + +ALTER TABLE IF EXISTS ind_indicateur_impact_equipement_physique ADD COLUMN IF NOT EXISTS nom_entite_discriminator varchar(255) NOT NULL DEFAULT ''; +UPDATE ind_indicateur_impact_equipement_physique SET nom_entite_discriminator = coalesce(nom_entite, '') where nom_entite_discriminator is null; +ALTER TABLE IF EXISTS ind_indicateur_impact_reseau ADD COLUMN IF NOT EXISTS nom_entite_discriminator varchar(255) NOT NULL DEFAULT ''; +UPDATE ind_indicateur_impact_reseau SET nom_entite_discriminator = coalesce(nom_entite, '') where nom_entite_discriminator is null; +ALTER TABLE IF EXISTS ind_indicateur_impact_equipement_virtuel ADD COLUMN IF NOT EXISTS nom_entite_discriminator varchar(255) NOT NULL DEFAULT ''; +UPDATE ind_indicateur_impact_equipement_virtuel SET nom_entite_discriminator = coalesce(nom_entite, '') where nom_entite_discriminator is null; +ALTER TABLE IF EXISTS ind_indicateur_impact_application ADD COLUMN IF NOT EXISTS nom_entite_discriminator varchar(255) NOT NULL DEFAULT ''; +UPDATE ind_indicateur_impact_application SET nom_entite_discriminator = coalesce(nom_entite, '') where nom_entite_discriminator is null; +ALTER TABLE IF EXISTS ind_indicateur_impact_messagerie ADD COLUMN IF NOT EXISTS nom_entite_discriminator varchar(255) NOT NULL DEFAULT ''; +UPDATE ind_indicateur_impact_messagerie SET nom_entite_discriminator = coalesce(nom_entite, '') where nom_entite_discriminator is null; + +ALTER TABLE IF EXISTS ind_indicateur_impact_equipement_physique ADD COLUMN IF NOT EXISTS nom_source_donnee_discriminator varchar(255) NOT NULL DEFAULT ''; +UPDATE ind_indicateur_impact_equipement_physique SET nom_source_donnee_discriminator = coalesce(nom_source_donnee, '') where nom_source_donnee_discriminator is null; +ALTER TABLE IF EXISTS ind_indicateur_impact_reseau ADD COLUMN IF NOT EXISTS nom_source_donnee_discriminator varchar(255) NOT NULL DEFAULT ''; +UPDATE ind_indicateur_impact_reseau SET nom_source_donnee_discriminator = coalesce(nom_source_donnee, '') where nom_source_donnee_discriminator is null; +ALTER TABLE IF EXISTS ind_indicateur_impact_equipement_virtuel ADD COLUMN IF NOT EXISTS nom_source_donnee_discriminator varchar(255) NOT NULL DEFAULT ''; +UPDATE ind_indicateur_impact_equipement_virtuel SET nom_source_donnee_discriminator = coalesce(nom_source_donnee, '') where nom_source_donnee_discriminator is null; +ALTER TABLE IF EXISTS ind_indicateur_impact_application ADD COLUMN IF NOT EXISTS nom_source_donnee_discriminator varchar(255) NOT NULL DEFAULT ''; +UPDATE ind_indicateur_impact_application SET nom_source_donnee_discriminator = coalesce(nom_source_donnee, '') where nom_source_donnee_discriminator is null; +ALTER TABLE IF EXISTS ind_indicateur_impact_messagerie ADD COLUMN IF NOT EXISTS nom_source_donnee_discriminator varchar(255) NOT NULL DEFAULT ''; +UPDATE ind_indicateur_impact_messagerie SET nom_source_donnee_discriminator = coalesce(nom_source_donnee, '') where nom_source_donnee_discriminator is null; + + +CREATE INDEX IF NOT EXISTS idx_ind_eq_p__nom_lot_nom_equipement ON ind_indicateur_impact_equipement_physique (nom_lot, nom_equipement); +CREATE INDEX IF NOT EXISTS idx_ind_eq_v__nom_lot_nom_equipement ON ind_indicateur_impact_equipement_virtuel (nom_lot, nom_equipement); +CREATE INDEX IF NOT EXISTS idx_ind_app__nom_lot_nom_equipement ON ind_indicateur_impact_application (nom_lot, nom_equipement_physique); +CREATE INDEX IF NOT EXISTS idx_ind_reseau__nom_lot_nom_equipement ON ind_indicateur_impact_reseau (nom_lot, nom_equipement); + +-- Lignes a supprimer dans les futures versions +ALTER TABLE ind_indicateur_impact_equipement_virtuel ADD COLUMN IF NOT EXISTS nom_equipement_virtuel VARCHAR(255); +ALTER TABLE ind_indicateur_impact_application ADD COLUMN IF NOT EXISTS nom_equipement_virtuel VARCHAR(255); + diff --git a/services/api-event-calculs/src/test/java/org/mte/numecoeval/calculs/ApiEventCalculsApplicationTests.java b/services/api-event-calculs/src/test/java/org/mte/numecoeval/calculs/ApiEventCalculsApplicationTests.java new file mode 100644 index 00000000..9c4949fd --- /dev/null +++ b/services/api-event-calculs/src/test/java/org/mte/numecoeval/calculs/ApiEventCalculsApplicationTests.java @@ -0,0 +1,165 @@ +package org.mte.numecoeval.calculs; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; +import org.mte.numecoeval.calculs.domain.data.entree.Application; +import org.mte.numecoeval.calculs.domain.data.entree.EquipementVirtuel; +import org.mte.numecoeval.calculs.domain.data.indicateurs.*; +import org.mte.numecoeval.calculs.domain.model.CalculEquipementPhysique; +import org.mte.numecoeval.calculs.domain.model.CalculMessagerie; +import org.mte.numecoeval.calculs.infrastructure.repository.*; +import org.mte.numecoeval.calculs.infrastructure.service.calcul.IntegrationCalculEquipementsService; +import org.mte.numecoeval.calculs.infrastructure.service.calcul.IntegrationCalculMessagerieService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration; +import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.ActiveProfiles; + +import javax.sql.DataSource; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.LocalDate; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; + +@SpringBootTest +@ActiveProfiles(profiles = {"test"}) +@Slf4j +@EnableAutoConfiguration(exclude = {SecurityAutoConfiguration.class, DataSourceAutoConfiguration.class, KafkaAutoConfiguration.class}) +class ApiEventCalculsApplicationTests { + + + @Autowired + IntegrationCalculEquipementsService integrationCalculEquipementsService; + @Autowired + IntegrationCalculMessagerieService integrationCalculMessagerieService; + + @MockBean + EquipementPhysiqueRepository equipementPhysiqueRepository; + @MockBean + EquipementVirtuelRepository equipementVirtuelRepository; + @MockBean + ApplicationRepository applicationRepository; + @MockBean + MessagerieRepository messagerieRepository; + @MockBean + DataSource dataSource; + + private static final String RESOURCES = "src/test/resources"; + private static final ObjectMapper mapper = new ObjectMapper().registerModule(new JavaTimeModule()); + + @MockBean + IndicateurRepository indicateurRepository; + + ArgumentCaptor<List<ImpactEquipementPhysique>> captorImpactEqPh = ArgumentCaptor.forClass((Class) List.class); + ArgumentCaptor<List<ImpactEquipementVirtuel>> captorImpactEqV = ArgumentCaptor.forClass((Class) List.class); + ArgumentCaptor<List<ImpactApplication>> captorImpactApp = ArgumentCaptor.forClass((Class) List.class); + ArgumentCaptor<List<ImpactReseau>> captorImpactReseau = ArgumentCaptor.forClass((Class) List.class); + + ArgumentCaptor<List<ImpactMessagerie>> captorImpactMsg = ArgumentCaptor.forClass((Class) List.class); + + @Test + void testIntegrationCalculEquipementsService() throws IOException { + + assertNotNull(ApiEventCalculsApplication.class); + + // check null case + integrationCalculEquipementsService.calculImpactEquipementPhysique(null); + + /* DEFINE INPUT DATA */ + var calculEquipementPhysique = mapper.readValue( + Files.readString(Path.of(RESOURCES).resolve("input").resolve("equipementPhysique.json")), + CalculEquipementPhysique.class + ); + + // mock equipements virtuels + List<EquipementVirtuel> equipementVirtuelList = List.of(EquipementVirtuel.builder() + .id(79302L) + .nomEquipementVirtuel("virtual-eq-1001-1") + .nomEquipementPhysique("physical-eq-srv-1001") + .cluster("PY1ORA01") + .nomLot("lot1") + .dateLot(LocalDate.of(2023, 10, 26)) + .nomOrganisation("org") + .typeEqv("calcul") + .vCPU(4) + .build()); + Mockito.when(equipementVirtuelRepository.findEquipementVirtuels(any(), any(), any())).thenReturn(equipementVirtuelList); + + // mock applications + List<Application> applicationList = List.of(Application.builder() + .nomApplication("application-1001-1-1") + .typeEnvironnement("Production") + .nomEquipementVirtuel("virtual-eq-1001-1") + .nomEquipementPhysique("physical-eq-srv-1001") + .domaine("Domain 1") + .sousDomaine("Sub Domain 1") + .nomLot("lot1") + .dateLot(LocalDate.of(2023, 10, 26)) + .nomOrganisation("org") + .nomEntite("Entite de test") + .build()); + Mockito.when(applicationRepository.findApplications(any(), any(), any(), any())).thenReturn(applicationList); + + /* EXECUTE */ + integrationCalculEquipementsService.calculImpactEquipementPhysique(calculEquipementPhysique); + + /* CAPTURE Impacts lists */ + Mockito.verify(indicateurRepository, times(1)).saveIndicateursEquipements(captorImpactEqPh.capture(), captorImpactEqV.capture(), captorImpactApp.capture(), captorImpactReseau.capture()); + + List<ImpactEquipementPhysique> actualImpactEqPh = captorImpactEqPh.getValue(); + List<ImpactEquipementVirtuel> actualImpactEqV = captorImpactEqV.getValue(); + List<ImpactApplication> actualImpactApp = captorImpactApp.getValue(); + List<ImpactReseau> actualImpactReseau = captorImpactReseau.getValue(); + + /* Check EXPECTED */ + Assertions.assertEquals(4, actualImpactEqPh.size()); + Assertions.assertEquals(4, actualImpactEqV.size()); + Assertions.assertEquals(4, actualImpactApp.size()); + Assertions.assertEquals(4, actualImpactReseau.size()); + + /* Check all statutIndicateur = OK */ + // Assertions.assertTrue(actualImpactEqPh.stream().allMatch(i -> "OK".equals(i.getStatutIndicateur()))); + // Assertions.assertTrue(actualImpactEqV.stream().allMatch(i -> "OK".equals(i.getStatutIndicateur()))); + // Assertions.assertTrue(actualImpactApp.stream().allMatch(i -> "OK".equals(i.getStatutIndicateur()))); + // Assertions.assertTrue(actualImpactReseau.stream().allMatch(i -> "OK".equals(i.getStatutIndicateur()))); + } + + + @Test + void testIntegrationCalculMessagerieService() throws IOException { + // check null case + integrationCalculMessagerieService.calculImpactMessagerie(null); + + /* DEFINE INPUT DATA */ + var calculMessagerie = mapper.readValue( + Files.readString(Path.of(RESOURCES).resolve("input").resolve("messagerie.json")), + CalculMessagerie.class + ); + + integrationCalculMessagerieService.calculImpactMessagerie(calculMessagerie); + + /* CAPTURE Impacts lists */ + Mockito.verify(indicateurRepository, times(1)).saveIndicateursMessagerie(captorImpactMsg.capture()); + List<ImpactMessagerie> actualImpactMsg = captorImpactMsg.getValue(); + + /* Check EXPECTED */ + Assertions.assertEquals(2, actualImpactMsg.size()); + + /* Check all statutIndicateur = ERREUR */ + Assertions.assertTrue(actualImpactMsg.stream().allMatch(i -> "ERREUR".equals(i.getStatutIndicateur()))); + } +} diff --git a/services/api-event-calculs/src/test/java/org/mte/numecoeval/calculs/infrastructure/service/calcul/CalculEquipementVirtuelServiceTest.java b/services/api-event-calculs/src/test/java/org/mte/numecoeval/calculs/infrastructure/service/calcul/CalculEquipementVirtuelServiceTest.java new file mode 100644 index 00000000..84b9dced --- /dev/null +++ b/services/api-event-calculs/src/test/java/org/mte/numecoeval/calculs/infrastructure/service/calcul/CalculEquipementVirtuelServiceTest.java @@ -0,0 +1,68 @@ +package org.mte.numecoeval.calculs.infrastructure.service.calcul; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mte.numecoeval.calculs.domain.data.entree.EquipementVirtuel; + +import java.util.List; + +@ExtendWith(MockitoExtension.class) +class CalculEquipementVirtuelServiceTest { + + @InjectMocks + CalculEquipementVirtuelService calculEquipementVirtuelService; + + @Test + void testCalculEquipementVirtuelService_getTotalVCPU_nominal() { + List<EquipementVirtuel> equipementVirtuelList = List.of( + EquipementVirtuel.builder().vCPU(2).build(), + EquipementVirtuel.builder().vCPU(6).build() + ); + + Assertions.assertEquals(8, calculEquipementVirtuelService.getTotalVCPU(equipementVirtuelList)); + } + + @Test + void testCalculEquipementVirtuelService_getTotalVCPU_null() { + List<EquipementVirtuel> equipementVirtuelList = List.of( + EquipementVirtuel.builder().vCPU(2).build(), + EquipementVirtuel.builder().vCPU(0).build() + ); + Assertions.assertNull(calculEquipementVirtuelService.getTotalVCPU(equipementVirtuelList)); + + equipementVirtuelList = List.of( + EquipementVirtuel.builder().vCPU(2).build(), + EquipementVirtuel.builder().build() + ); + Assertions.assertNull(calculEquipementVirtuelService.getTotalVCPU(equipementVirtuelList)); + } + + + @Test + void testCalculEquipementVirtuelService_getTotalStockage_nominal() { + List<EquipementVirtuel> equipementVirtuelList = List.of( + EquipementVirtuel.builder().capaciteStockage(2.1).build(), + EquipementVirtuel.builder().capaciteStockage(6.2).build() + ); + + Assertions.assertEquals(8.3, calculEquipementVirtuelService.getTotalStockage(equipementVirtuelList)); + } + + @Test + void testCalculEquipementVirtuelService_getTotalStockage_null() { + List<EquipementVirtuel> equipementVirtuelList = List.of( + EquipementVirtuel.builder().capaciteStockage(2.1).build(), + EquipementVirtuel.builder().capaciteStockage(0.0).build() + ); + Assertions.assertNull(calculEquipementVirtuelService.getTotalVCPU(equipementVirtuelList)); + + equipementVirtuelList = List.of( + EquipementVirtuel.builder().capaciteStockage(2.0).build(), + EquipementVirtuel.builder().build() + ); + Assertions.assertNull(calculEquipementVirtuelService.getTotalVCPU(equipementVirtuelList)); + } +} diff --git a/services/api-event-calculs/src/test/java/org/mte/numecoeval/calculs/infrastructure/service/calcul/CalculReseauServiceTest.java b/services/api-event-calculs/src/test/java/org/mte/numecoeval/calculs/infrastructure/service/calcul/CalculReseauServiceTest.java new file mode 100644 index 00000000..836abbc1 --- /dev/null +++ b/services/api-event-calculs/src/test/java/org/mte/numecoeval/calculs/infrastructure/service/calcul/CalculReseauServiceTest.java @@ -0,0 +1,42 @@ +package org.mte.numecoeval.calculs.infrastructure.service.calcul; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mte.numecoeval.calculs.domain.model.CalculEquipementPhysique; + +import java.util.List; + +@ExtendWith(MockitoExtension.class) +class CalculReseauServiceTest { + + @InjectMocks + CalculReseauService calculReseauService; + + private static final ObjectMapper mapper = new ObjectMapper().registerModule(new JavaTimeModule()); + + @Test + void testCalculImpactReseau_nullCases() throws JsonProcessingException { + Assertions.assertEquals(List.of(), calculReseauService.calculImpactReseau(null)); + + Assertions.assertEquals(List.of(), calculReseauService.calculImpactReseau(new CalculEquipementPhysique())); + + Assertions.assertEquals(List.of(), calculReseauService.calculImpactReseau(mapper.readValue(""" + { "equipementPhysique": {} } + """, CalculEquipementPhysique.class))); + + Assertions.assertEquals(List.of(), calculReseauService.calculImpactReseau(mapper.readValue(""" + { + "equipementPhysique": { + "goTelecharge": null + } + } + """, CalculEquipementPhysique.class))); + } + +} \ No newline at end of file diff --git a/services/api-event-calculs/src/test/java/org/mte/numecoeval/calculs/infrastructure/service/calcul/EnrichissementEquipementPhysiqueServiceTest.java b/services/api-event-calculs/src/test/java/org/mte/numecoeval/calculs/infrastructure/service/calcul/EnrichissementEquipementPhysiqueServiceTest.java new file mode 100644 index 00000000..9751e082 --- /dev/null +++ b/services/api-event-calculs/src/test/java/org/mte/numecoeval/calculs/infrastructure/service/calcul/EnrichissementEquipementPhysiqueServiceTest.java @@ -0,0 +1,164 @@ +package org.mte.numecoeval.calculs.infrastructure.service.calcul; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +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; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mte.numecoeval.calculs.infrastructure.client.ReferentielClient; +import org.mte.numecoeval.calculs.infrastructure.service.enrichissement.EnrichissementEquipementPhysiqueService; +import org.mte.numecoeval.calculs.referentiels.generated.api.model.*; +import org.mte.numecoeval.topic.data.EquipementPhysiqueDTO; + +import java.util.Arrays; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; + +@ExtendWith(MockitoExtension.class) +class EnrichissementEquipementPhysiqueServiceTest { + + private static final ObjectMapper mapper = new ObjectMapper().registerModule(new JavaTimeModule()); + + @InjectMocks + EnrichissementEquipementPhysiqueService enrichissementEquipementPhysiqueService; + + @Mock + ReferentielClient referentielClient; + + EquipementPhysiqueDTO equipementPhysiqueDTO = mapper.readValue(""" + { + "id": 43702, + "nomEquipementPhysique": "physical-eq-srv-1001", + "modele": "blade-server--28", + "type": "Server", + "statut": "In use", + "paysDUtilisation": null, + "dateAchat": "2016-06-17", + "dateRetrait": "2023-06-16", + "nomCourtDatacenter": "default", + "nbJourUtiliseAn": null, + "goTelecharge": 0.0, + "quantite": 7.0, + "consoElecAnnuelle": null, + "serveur": true, + "nomLot": "lot1", + "dateLot": "2023-10-26", + "nomOrganisation": "org", + "nbEquipementsVirtuels": 1, + "nbTotalVCPU": 4, + "dataCenter": { + "id": 5026, + "nomCourtDatacenter": "default", + "nomLongDatacenter": "Default", + "pue": 1.75, + "localisation": "France", + "dateLot": "2023-10-26", + "nomOrganisation": "org" + } + } + """, EquipementPhysiqueDTO.class); + + EnrichissementEquipementPhysiqueServiceTest() throws JsonProcessingException { + } + + @BeforeEach + void initMocksReferentiel() throws JsonProcessingException { + /* MOCK REFERENTIEL : Etapes */ + Mockito.lenient().when(referentielClient.getEtapes()).thenReturn(Arrays.asList(mapper.readValue(""" + [{ "code": "UTILISATION", "libelle": "Using" }] + """, EtapeDTO[].class))); + + /* MOCK REFERENTIEL : Criteres */ + Mockito.lenient().when(referentielClient.getCriteres()).thenReturn(Arrays.asList(mapper.readValue(""" + [{ + "nomCritere": "Climate change", + "unite": "kg CO2 eq", + "description": "Greenhouse gases (GHG)" + }] + """, CritereDTO[].class))); + + /* MOCK REFERENTIEL : Hypothese */ + Mockito.lenient().when(referentielClient.getHypothese(any())).thenReturn(mapper.readValue(""" + { + "code": "hyp code", + "valeur": "val", + "source": "test" + } + """, HypotheseDTO.class)); + + /* MOCK REFERENTIEL : ImpactReseau */ + Mockito.lenient().when(referentielClient.getImpactReseau(any(), any(), any())).thenReturn(new ImpactReseauDTO()); + + /* MOCK REFERENTIEL : MixElectrique */ + var mixFr = new MixElectriqueDTO(); + mixFr.setPays("FR"); + Mockito.lenient().when(referentielClient.getMixElectrique("France", "Climate change")).thenReturn(mixFr); + Mockito.lenient().when(referentielClient.getMixElectrique(null, "Climate change")).thenReturn(null); + } + + @Test + void testServiceEnrichissementEquipementPhysqique_null() { + Assertions.assertNull(enrichissementEquipementPhysiqueService.serviceEnrichissementEquipementPhysique(null)); + } + + @Test + void testServiceEnrichissementEquipementPhysqique_correspondanceRefEquipement() throws JsonProcessingException { + + /* MOCK REFERENTIEL : CorrespondanceRefEquipement */ + Mockito.when(referentielClient.getCorrespondanceRefEquipement("blade-server--28")).thenReturn(mapper.readValue(""" + { + "modeleEquipementSource": "srv", + "refEquipementCible": "cible" + } + """, CorrespondanceRefEquipementDTO.class)); + + /* MOCK REFERENTIEL : ImpactEquipement */ + var impactEquipementDTO = new ImpactEquipementDTO(); + impactEquipementDTO.setValeur(1.1); + + Mockito.when(referentielClient.getImpactEquipement(eq("cible"), any(), any())).thenReturn(impactEquipementDTO); + + var actual = enrichissementEquipementPhysiqueService.serviceEnrichissementEquipementPhysique(equipementPhysiqueDTO); + + Assertions.assertEquals(1, actual.getEtapes().size()); + Assertions.assertEquals(1, actual.getCriteres().size()); + Assertions.assertEquals(4, actual.getHypotheses().size()); + Assertions.assertEquals(1, actual.getMixElectriques().size()); + Assertions.assertEquals("FR", actual.getMixElectriques().get(0).getPays()); + Assertions.assertEquals(1.1, actual.getImpactsEquipement().get(0).getValeur()); + } + + + @Test + void testServiceEnrichissementEquipementPhysqique_typeEquipement() throws JsonProcessingException { + + /* MOCK REFERENTIEL : CorrespondanceRefEquipement */ + Mockito.when(referentielClient.getTypeEquipement("Server")).thenReturn(mapper.readValue(""" + { + "refEquipementParDefaut": "default-ref-server" + } + """, TypeEquipementDTO.class)); + + /* MOCK REFERENTIEL : ImpactEquipement */ + var impactEquipementDTO = new ImpactEquipementDTO(); + impactEquipementDTO.setValeur(2.2); + + Mockito.when(referentielClient.getImpactEquipement(eq("default-ref-server"), any(), any())).thenReturn(impactEquipementDTO); + + var actual = enrichissementEquipementPhysiqueService.serviceEnrichissementEquipementPhysique(equipementPhysiqueDTO); + + Assertions.assertEquals(1, actual.getEtapes().size()); + Assertions.assertEquals(1, actual.getCriteres().size()); + Assertions.assertEquals(4, actual.getHypotheses().size()); + Assertions.assertEquals(1, actual.getMixElectriques().size()); + Assertions.assertEquals("FR", actual.getMixElectriques().get(0).getPays()); + Assertions.assertEquals(2.2, actual.getImpactsEquipement().get(0).getValeur()); + } +} diff --git a/services/api-event-calculs/src/test/java/org/mte/numecoeval/calculs/infrastructure/service/calcul/EnrichissementMessagerieServiceTest.java b/services/api-event-calculs/src/test/java/org/mte/numecoeval/calculs/infrastructure/service/calcul/EnrichissementMessagerieServiceTest.java new file mode 100644 index 00000000..c4ffe6ff --- /dev/null +++ b/services/api-event-calculs/src/test/java/org/mte/numecoeval/calculs/infrastructure/service/calcul/EnrichissementMessagerieServiceTest.java @@ -0,0 +1,60 @@ +package org.mte.numecoeval.calculs.infrastructure.service.calcul; + +import org.junit.jupiter.api.Assertions; +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; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mte.numecoeval.calculs.infrastructure.client.ReferentielClient; +import org.mte.numecoeval.calculs.infrastructure.service.enrichissement.EnrichissementMessagerieService; +import org.mte.numecoeval.calculs.referentiels.generated.api.model.CritereDTO; +import org.mte.numecoeval.calculs.referentiels.generated.api.model.ImpactMessagerieDTO; +import org.mte.numecoeval.topic.data.MessagerieDTO; + +import java.util.List; + +@ExtendWith(MockitoExtension.class) +class EnrichissementMessagerieServiceTest { + + @InjectMocks + EnrichissementMessagerieService enrichissementMessagerieService; + + @Mock + ReferentielClient referentielClient; + + @Test + void testServiceEnrichissementMessagerie_null() { + Assertions.assertNull(enrichissementMessagerieService.serviceEnrichissementMessagerie(null)); + } + + @Test + void testServiceEnrichissementMessagerie() { + + /* INPUT DATA */ + var messagerieDTO = new MessagerieDTO(); + messagerieDTO.setId(1L); + + var critereDTO = new CritereDTO(); + critereDTO.setNomCritere("critere"); + + Mockito.when(referentielClient.getCriteres()).thenReturn(List.of(critereDTO)); + + var impactMessagerieDTO = new ImpactMessagerieDTO(); + impactMessagerieDTO.setCritere("critere"); + impactMessagerieDTO.setConstanteCoefficientDirecteur(1.1); + + Mockito.when(referentielClient.getMessagerie("critere")).thenReturn(impactMessagerieDTO); + + /* EXECUTE */ + var actual = enrichissementMessagerieService.serviceEnrichissementMessagerie(messagerieDTO); + Assertions.assertEquals(1, actual.getCriteres().size()); + Assertions.assertEquals(1, actual.getImpactsMessagerie().size()); + Assertions.assertEquals(1L, actual.getMessagerie().getId()); + + Assertions.assertEquals(1.1, actual.getImpactsMessagerie().get(0).getConstanteCoefficientDirecteur()); + Assertions.assertEquals("critere", actual.getImpactsMessagerie().get(0).getCritere()); + } + +} diff --git a/services/api-event-calculs/src/test/java/org/mte/numecoeval/calculs/infrastructure/service/rest/calculs/CheckIndicateurServiceTest.java b/services/api-event-calculs/src/test/java/org/mte/numecoeval/calculs/infrastructure/service/rest/calculs/CheckIndicateurServiceTest.java new file mode 100644 index 00000000..bb674a42 --- /dev/null +++ b/services/api-event-calculs/src/test/java/org/mte/numecoeval/calculs/infrastructure/service/rest/calculs/CheckIndicateurServiceTest.java @@ -0,0 +1,21 @@ +package org.mte.numecoeval.calculs.infrastructure.service.rest.calculs; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class CheckIndicateurServiceTest { + + @InjectMocks + CheckIndicateurService checkIndicateurService; + + @Test + void testCheckIndicateurService_isOk() { + Assertions.assertTrue(checkIndicateurService.isOk("OK")); + Assertions.assertFalse(checkIndicateurService.isOk("ERREUR")); + } + +} diff --git a/services/api-event-calculs/src/test/java/org/mte/numecoeval/calculs/infrastructure/service/sync/calculs/SyncCalculServiceTest.java b/services/api-event-calculs/src/test/java/org/mte/numecoeval/calculs/infrastructure/service/sync/calculs/SyncCalculServiceTest.java new file mode 100644 index 00000000..ee05e985 --- /dev/null +++ b/services/api-event-calculs/src/test/java/org/mte/numecoeval/calculs/infrastructure/service/sync/calculs/SyncCalculServiceTest.java @@ -0,0 +1,100 @@ +package org.mte.numecoeval.calculs.infrastructure.service.sync.calculs; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import org.junit.jupiter.api.Assertions; +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; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mte.numecoeval.calculs.domain.model.CalculSizes; +import org.mte.numecoeval.calculs.infrastructure.repository.EquipementPhysiqueRepository; +import org.mte.numecoeval.calculs.infrastructure.repository.MessagerieRepository; +import org.mte.numecoeval.calculs.infrastructure.service.calcul.MainEquipementPhysiqueService; +import org.mte.numecoeval.calculs.infrastructure.service.calcul.MainMessagerieService; +import org.mte.numecoeval.calculs.sync.generated.api.model.SyncCalculRest; +import org.mte.numecoeval.topic.data.EquipementPhysiqueDTO; +import org.mte.numecoeval.topic.data.MessagerieDTO; + +import java.util.List; + +import static org.mockito.ArgumentMatchers.any; + +@ExtendWith(MockitoExtension.class) +class SyncCalculServiceTest { + + @InjectMocks + SyncCalculService syncCalculService; + + @Mock + EquipementPhysiqueRepository equipementPhysiqueRepository; + @Mock + MainEquipementPhysiqueService mainEquipementPhysiqueService; + + @Mock + MessagerieRepository messagerieRepository; + @Mock + MainMessagerieService mainMessagerieService; + + private static final ObjectMapper mapper = new ObjectMapper().registerModule(new JavaTimeModule()); + + @Test + void testSyncCalculService_withListEquipementPhysique() throws JsonProcessingException { + /* MOCKS */ + Mockito.when(equipementPhysiqueRepository.findEquipementPhysiqueDTOs(any())).thenReturn(List.of( + new EquipementPhysiqueDTO(), + new EquipementPhysiqueDTO() + )); + + var calculSizes = new CalculSizes(); + calculSizes.setNbEquipementPhysique(1); + calculSizes.setNbEquipementVirtuel(2); + calculSizes.setNbApplication(3); + + Mockito.when(mainEquipementPhysiqueService.calcul(any())).thenReturn(calculSizes); + + var syncCalculRest = mapper.readValue(""" + { + "equipementPhysiqueIds": [1,2] + } + """, SyncCalculRest.class); + + /* EXECUTE */ + var actual = syncCalculService.calcul(syncCalculRest); + + /* ASSERT */ + Assertions.assertEquals(2, actual.getNbrEquipementPhysique()); + Assertions.assertEquals(4, actual.getNbrEquipementVirtuel()); + Assertions.assertEquals(6, actual.getNbrApplication()); + } + + @Test + void testSyncCalculService_withListMessagerie() throws JsonProcessingException { + /* MOCKS */ + Mockito.when(messagerieRepository.findMessagerieDTOList(any())).thenReturn(List.of( + new MessagerieDTO(), + new MessagerieDTO(), + new MessagerieDTO() + )); + + var calculSizes = new CalculSizes(); + calculSizes.setNbMessagerie(1); + + Mockito.when(mainMessagerieService.calcul(any())).thenReturn(calculSizes); + + var syncCalculRest = mapper.readValue(""" + { + "messagerieIds": [10,20,30] + } + """, SyncCalculRest.class); + + /* EXECUTE */ + var actual = syncCalculService.calcul(syncCalculRest); + + /* ASSERT */ + Assertions.assertEquals(3, actual.getNbrMessagerie()); + } +} diff --git a/services/api-event-calculs/src/test/resources/application-test.yaml b/services/api-event-calculs/src/test/resources/application-test.yaml new file mode 100644 index 00000000..843fe39b --- /dev/null +++ b/services/api-event-calculs/src/test/resources/application-test.yaml @@ -0,0 +1,2 @@ +spring: + jackson.serialization.write-dates-as-timestamps: false diff --git a/services/api-event-calculs/src/test/resources/input/equipementPhysique.json b/services/api-event-calculs/src/test/resources/input/equipementPhysique.json new file mode 100644 index 00000000..30d28311 --- /dev/null +++ b/services/api-event-calculs/src/test/resources/input/equipementPhysique.json @@ -0,0 +1,194 @@ +{ + "etapes": [ + { + "code": "FABRICATION", + "libelle": "Manufacturing" + }, + { + "code": "UTILISATION", + "libelle": "Using" + } + ], + "criteres": [ + { + "nomCritere": "Climate change", + "unite": "kg CO2 eq", + "description": "Greenhouse gases (GHG) are gaseous components which absorb the infrared radiation emitted by the earth's surface 1 and thus contribute to the greenhouse effect." + }, + { + "nomCritere": "Particulate matter and respiratory inorganics", + "unite": "Disease incidence", + "description": "The presence of small-diameter fine particles in the air - especially those with a diameter of less than 10 microns - represents a human health problem, since their inhalation can cause respiratory and cardiovascular problems." + } + ], + "hypotheses": [], + "correspondanceRefEquipement": null, + "equipementPhysique": { + "id": 43702, + "nomEquipementPhysique": "physical-eq-srv-1001", + "modele": "blade-server--28", + "type": "Server", + "statut": "In use", + "paysDUtilisation": null, + "utilisateur": null, + "dateAchat": "2016-06-17", + "dateRetrait": "2023-06-16", + "nbCoeur": null, + "nomCourtDatacenter": "default", + "nbJourUtiliseAn": null, + "goTelecharge": 0.0, + "quantite": 7.0, + "consoElecAnnuelle": null, + "serveur": true, + "nomEntite": null, + "nomSourceDonnee": null, + "nomLot": "lot1", + "dateLot": "2023-10-26", + "nomOrganisation": "org", + "nbEquipementsVirtuels": 1, + "nbTotalVCPU": 4, + "dataCenter": { + "id": 5026, + "nomCourtDatacenter": "default", + "nomLongDatacenter": "Default", + "pue": 1.75, + "localisation": "France", + "nomEntite": null, + "nomSourceDonnee": null, + "nomLot": null, + "dateLot": "2023-10-26", + "nomOrganisation": "org" + }, + "stockageTotalVirtuel": null + }, + "typeEquipement": { + "type": "Server", + "serveur": true, + "commentaire": "Lifetime par defaut Sopra Steria", + "dureeVieDefaut": 7.0, + "source": "", + "refEquipementParDefaut": "blade-server--28" + }, + "mixElectriques": [ + null, + { + "pays": "France", + "raccourcisAnglais": "FR", + "critere": "Climate change", + "valeur": 0.0813225, + "source": "Base IMPACTS®2.02" + }, + null, + { + "pays": "France", + "raccourcisAnglais": "FR", + "critere": "Particulate matter and respiratory inorganics", + "valeur": 4.15844e-9, + "source": "Base IMPACTS®2.02" + }, + null, + { + "pays": "France", + "raccourcisAnglais": "FR", + "critere": "Ionising radiation", + "valeur": 3.23443, + "source": "Base IMPACTS®2.02" + }, + null, + { + "pays": "France", + "raccourcisAnglais": "FR", + "critere": "Acidification", + "valeur": 0.000209809, + "source": "Base IMPACTS®2.02" + }, + null, + { + "pays": "France", + "raccourcisAnglais": "FR", + "critere": "Resource use (minerals and metals)", + "valeur": 4.85798e-8, + "source": "Base IMPACTS®2.02" + } + ], + "impactsReseau": [ + { + "refReseau": "impactReseauMobileMoyen", + "etapeACV": "FABRICATION", + "critere": "Climate change", + "unite": null, + "source": "Test V1.00", + "valeur": 518.28, + "consoElecMoyenne": null + }, + { + "refReseau": "impactReseauMobileMoyen", + "etapeACV": "UTILISATION", + "critere": "Climate change", + "unite": null, + "source": "Test V1.00", + "valeur": 76.28, + "consoElecMoyenne": null + }, + { + "refReseau": "impactReseauMobileMoyen", + "etapeACV": "FABRICATION", + "critere": "Particulate matter and respiratory inorganics", + "unite": null, + "source": "Test V1.00", + "valeur": 148.28, + "consoElecMoyenne": null + }, + { + "refReseau": "impactReseauMobileMoyen", + "etapeACV": "UTILISATION", + "critere": "Particulate matter and respiratory inorganics", + "unite": null, + "source": "Test V1.00", + "valeur": 18.8, + "consoElecMoyenne": null + } + ], + "impactsEquipement": [ + { + "refEquipement": "blade-server--28", + "etape": "FABRICATION", + "critere": "Climate change", + "source": "test", + "type": "", + "valeur": 2935.5392, + "consoElecMoyenne": 3504.0, + "description": "Blade server 2 processor high-end 1 SSD: 1024 GB each 0 HDD 16 RAM, 16 GB each 0 GPU" + }, + { + "refEquipement": "blade-server--28", + "etape": "UTILISATION", + "critere": "Climate change", + "source": "test", + "type": "", + "valeur": 234.62784, + "consoElecMoyenne": 3504.0, + "description": "Blade server 2 processor high-end 1 SSD: 1024 GB each 0 HDD 16 RAM, 16 GB each 0 GPU" + }, + { + "refEquipement": "blade-server--28", + "etape": "FABRICATION", + "critere": "Particulate matter and respiratory inorganics", + "source": "test", + "type": "", + "valeur": 0.0000917808, + "consoElecMoyenne": 3504.0, + "description": "Blade server 2 processor high-end 1 SSD: 1024 GB each 0 HDD 16 RAM, 16 GB each 0 GPU" + }, + { + "refEquipement": "blade-server--28", + "etape": "UTILISATION", + "critere": "Particulate matter and respiratory inorganics", + "source": "test", + "type": "", + "valeur": 0.000052475904, + "consoElecMoyenne": 3504.0, + "description": "Blade server 2 processor high-end 1 SSD: 1024 GB each 0 HDD 16 RAM, 16 GB each 0 GPU" + } + ] +} \ No newline at end of file diff --git a/services/api-event-calculs/src/test/resources/input/messagerie.json b/services/api-event-calculs/src/test/resources/input/messagerie.json new file mode 100644 index 00000000..188e9f9b --- /dev/null +++ b/services/api-event-calculs/src/test/resources/input/messagerie.json @@ -0,0 +1,16 @@ +{ + "criteres": [ + { + "nomCritere": "Climate change", + "unite": "kg CO2 eq", + "description": "Greenhouse gases (GHG) are gaseous components which absorb the infrared radiation emitted by the earth's surface 1 and thus contribute to the greenhouse effect." + }, + { + "nomCritere": "Particulate matter and respiratory inorganics", + "unite": "Disease incidence", + "description": "The presence of small-diameter fine particles in the air - especially those with a diameter of less than 10 microns - represents a human health problem, since their inhalation can cause respiratory and cardiovascular problems." + } + ], + "messagerie": {}, + "impactsMessagerie": [] +} \ No newline at end of file diff --git a/services/api-event-calculs/src/test/resources/logback-test.xml b/services/api-event-calculs/src/test/resources/logback-test.xml new file mode 100644 index 00000000..e264d815 --- /dev/null +++ b/services/api-event-calculs/src/test/resources/logback-test.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> +<configuration> + <!-- Spring default.xml --> + <conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter"/> + <conversionRule conversionWord="wex" + converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter"/> + <conversionRule conversionWord="wEx" + converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter"/> + + <property name="CONSOLE_LOG_PATTERN" + value="${CONSOLE_LOG_PATTERN:-%clr(%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%t]){faint} %clr(%-42.42logger{0}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/> + + <statusListener class="ch.qos.logback.core.status.NopStatusListener"/> + + <!-- console-appender.xml--> + <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> + <encoder> + <pattern>${CONSOLE_LOG_PATTERN}</pattern> + </encoder> + </appender> + + <root level="${APP_LOGGING_LEVEL:-INFO}"> + <appender-ref ref="CONSOLE"/> + </root> + <logger name="org.springframework.web" level="INFO"/> +</configuration> \ No newline at end of file diff --git a/services/api-event-donneesentrees/.gitignore b/services/api-event-donneesentrees/.gitignore new file mode 100644 index 00000000..549e00a2 --- /dev/null +++ b/services/api-event-donneesentrees/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/services/api-event-donneesentrees/README.md b/services/api-event-donneesentrees/README.md new file mode 100644 index 00000000..d12766d1 --- /dev/null +++ b/services/api-event-donneesentrees/README.md @@ -0,0 +1,3 @@ +# api-event-donneesentrees + +Application de gestion d'événement vers kafka diff --git a/services/api-event-donneesentrees/dependency_check_suppressions.xml b/services/api-event-donneesentrees/dependency_check_suppressions.xml new file mode 100644 index 00000000..15f53bbc --- /dev/null +++ b/services/api-event-donneesentrees/dependency_check_suppressions.xml @@ -0,0 +1,97 @@ +<?xml version="1.0" encoding="UTF-8"?> +<suppressions xmlns="https://jeremylong.github.io/DependencyCheck/dependency-suppression.1.1.xsd"> + + <suppress> + <notes><![CDATA[ + file name: spring-security-crypto-5.7.3.jar + La librairie Spring Security est en version 5.7.3. + Cette CVE est marquée uniquement jusqu'à la version 5.2.4 (exclus) + https://nvd.nist.gov/vuln/detail/CVE-2020-5408 + ]]></notes> + <cve>CVE-2020-5408</cve> + </suppress> + + <suppress> + <notes><![CDATA[ + file name: spring-web-5.3.22.jar + Dans notre contexte, nous ne recevons pas de code Java de l'extérieur et n'effectuons pas de dé-sérialisation de code Java + Côté Spring, le point est déjà remonté comme un faux-positif : https://github.com/spring-projects/spring-framework/issues/24434#issuecomment-744519525 + ]]></notes> + <cve>CVE-2016-1000027</cve> + </suppress> + + <suppress> + <notes><![CDATA[ + file name: snakeyaml-1.33.jar + Faux-positif : la version de Snakeyaml est la 1.33, la CVE existe uniquement sur les versions < 1.32 + https://nvd.nist.gov/vuln/detail/CVE-2022-38752 + ]]></notes> + <cve>CVE-2022-38752</cve> + </suppress> + + <suppress> + <notes><![CDATA[ + Faux-positif sur toutes les librairies utils: https://github.com/jeremylong/DependencyCheck/issues/5213 + Concerne : software.amazon.awssdk:utils qui n'est pas utilisé + ]]></notes> + <cve>CVE-2021-4277</cve> + </suppress> + + <suppress> + <notes><![CDATA[ + Faux-positif : matchent à tort sur tous les commons : https://github.com/jeremylong/DependencyCheck/issues/5132 + ]]></notes> + <cve>CVE-2021-37533</cve> + </suppress> + + <suppress> + <notes><![CDATA[ + Non-applicable : Snakeyaml n'est utilisé que par Spring Boot pour les fichiers application.yml + et non des fichiers externes. + ]]></notes> + <cve>CVE-2022-1471</cve> + </suppress> + + <suppress> + <notes><![CDATA[ + Non-applicable : Snakeyaml n'est utilisé que par Spring Boot pour les fichiers application.yml + et non des fichiers externes. + ]]></notes> + <cve>CVE-2022-3064</cve> + </suppress> + + <suppress> + <notes><![CDATA[ + Non-applicable : Snakeyaml n'est utilisé que par Spring Boot pour les fichiers application.yml + et non des fichiers externes. + ]]></notes> + <cve>CVE-2021-4235</cve> + </suppress> + + <suppress> + <notes><![CDATA[ + Non-applicable : json-smart est une dépendance transitive. + Les json sont limités à la taille des inventaires : la taille des listes n'a pas réellement de limite + et c'est souhaités comme cela (traitement de gros volumes de données). + ]]></notes> + <cve>CVE-2023-1370</cve> + </suppress> + + <suppress> + <notes><![CDATA[ + Faux-Positif : Concerne hutool-json et json-java qui ne sont pas utilisés. + Le faux-positif se trouve sur json-path, jackson-core, accessors-smart et json-smart. + cf. https://github.com/jeremylong/DependencyCheck/issues/5502 + ]]></notes> + <cve>CVE-2022-45688</cve> + </suppress> + + + <suppress> + <notes><![CDATA[ + Faux-Positif : Conformément au lien, nous ne sommes pas dans un des cas de vulnérabilité. + cf. cf. https://github.com/jeremylong/DependencyCheck/issues/5502 + ]]></notes> + <cve>CVE-2023-20862</cve> + </suppress> +</suppressions> diff --git a/services/api-event-donneesentrees/pom.xml b/services/api-event-donneesentrees/pom.xml new file mode 100644 index 00000000..a6921d7b --- /dev/null +++ b/services/api-event-donneesentrees/pom.xml @@ -0,0 +1,161 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.mte.numecoeval</groupId> + <artifactId>core</artifactId> + <version>1.2.3-SNAPSHOT</version> + <relativePath/> <!-- lookup parent from repository --> + </parent> + <artifactId>api-event-donneesEntrees</artifactId> + <name>api-event-donneesEntrees</name> + <version>1.2.3-SNAPSHOT</version> + <description>api-event-donneesEntrees</description> + + <repositories> + <repository> + <id>gitlab-maven</id> + <url>https://gitlab-forge.din.developpement-durable.gouv.fr/api/v4/projects/20519/packages/maven</url> + </repository> + </repositories> + + <distributionManagement> + <repository> + <id>gitlab-maven</id> + <url>https://gitlab-forge.din.developpement-durable.gouv.fr/api/v4/projects/20519/packages/maven</url> + </repository> + + <snapshotRepository> + <id>gitlab-maven</id> + <url>https://gitlab-forge.din.developpement-durable.gouv.fr/api/v4/projects/20519/packages/maven</url> + </snapshotRepository> + </distributionManagement> + + <dependencies> + <dependency> + <groupId>org.mte.numecoeval</groupId> + <artifactId>common</artifactId> + </dependency> + + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-actuator</artifactId> + </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-integration</artifactId> + </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-jdbc</artifactId> + </dependency> + + <!-- JPA --> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-data-jpa</artifactId> + </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-web</artifactId> + </dependency> + <dependency> + <groupId>org.springframework.integration</groupId> + <artifactId>spring-integration-jdbc</artifactId> + </dependency> + <dependency> + <groupId>org.springframework.integration</groupId> + <artifactId>spring-integration-kafka</artifactId> + </dependency> + <dependency> + <groupId>org.springframework.kafka</groupId> + <artifactId>spring-kafka</artifactId> + </dependency> + + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-devtools</artifactId> + <scope>runtime</scope> + <optional>true</optional> + </dependency> + <dependency> + <groupId>io.micrometer</groupId> + <artifactId>micrometer-registry-prometheus</artifactId> + <scope>runtime</scope> + </dependency> + <dependency> + <groupId>org.postgresql</groupId> + <artifactId>postgresql</artifactId> + <scope>runtime</scope> + </dependency> + <dependency> + <groupId>org.projectlombok</groupId> + <artifactId>lombok</artifactId> + <optional>true</optional> + </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-test</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.springframework.integration</groupId> + <artifactId>spring-integration-test</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.springframework.kafka</groupId> + <artifactId>spring-kafka-test</artifactId> + <scope>test</scope> + </dependency> + + <!-- Base de données de tests --> + <dependency> + <groupId>io.zonky.test</groupId> + <artifactId>embedded-database-spring-test</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>io.zonky.test</groupId> + <artifactId>embedded-postgres</artifactId> + <scope>test</scope> + </dependency> + + <!-- Données de tests --> + <dependency> + <groupId>org.instancio</groupId> + <artifactId>instancio-junit</artifactId> + <scope>test</scope> + </dependency> + <!-- Tests avec attente minimum --> + <dependency> + <groupId>org.awaitility</groupId> + <artifactId>awaitility</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.mte.numecoeval</groupId> + <artifactId>common</artifactId> + </dependency> + </dependencies> + + <build> + <plugins> + <plugin> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-maven-plugin</artifactId> + <configuration> + <excludes> + <exclude> + <groupId>org.projectlombok</groupId> + <artifactId>lombok</artifactId> + </exclude> + </excludes> + </configuration> + </plugin> + </plugins> + </build> + + +</project> diff --git a/services/api-event-donneesentrees/src/main/java/org/mte/numecoeval/donneesentrees/ApiEventDonneesEntreesApplication.java b/services/api-event-donneesentrees/src/main/java/org/mte/numecoeval/donneesentrees/ApiEventDonneesEntreesApplication.java new file mode 100644 index 00000000..7164ee93 --- /dev/null +++ b/services/api-event-donneesentrees/src/main/java/org/mte/numecoeval/donneesentrees/ApiEventDonneesEntreesApplication.java @@ -0,0 +1,17 @@ +package org.mte.numecoeval.donneesentrees; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.integration.config.EnableIntegration; +import org.springframework.kafka.annotation.EnableKafka; + +@SpringBootApplication +@EnableIntegration +@EnableKafka +public class ApiEventDonneesEntreesApplication { + + public static void main(String[] args) { + SpringApplication.run(ApiEventDonneesEntreesApplication.class, args); + } + +} diff --git a/services/api-event-donneesentrees/src/main/java/org/mte/numecoeval/donneesentrees/infrastructure/config/EquipementPhysiqueIntegrationConfig.java b/services/api-event-donneesentrees/src/main/java/org/mte/numecoeval/donneesentrees/infrastructure/config/EquipementPhysiqueIntegrationConfig.java new file mode 100644 index 00000000..7b83082f --- /dev/null +++ b/services/api-event-donneesentrees/src/main/java/org/mte/numecoeval/donneesentrees/infrastructure/config/EquipementPhysiqueIntegrationConfig.java @@ -0,0 +1,167 @@ +package org.mte.numecoeval.donneesentrees.infrastructure.config; + +import lombok.extern.slf4j.Slf4j; +import org.apache.kafka.clients.admin.NewTopic; +import org.mte.numecoeval.common.utils.ResultSetUtils; +import org.mte.numecoeval.donneesentrees.infrastructure.utils.Constants; +import org.mte.numecoeval.topic.data.DataCenterDTO; +import org.mte.numecoeval.topic.data.EquipementPhysiqueDTO; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.expression.common.LiteralExpression; +import org.springframework.integration.annotation.InboundChannelAdapter; +import org.springframework.integration.annotation.Poller; +import org.springframework.integration.annotation.ServiceActivator; +import org.springframework.integration.annotation.Splitter; +import org.springframework.integration.channel.DirectChannel; +import org.springframework.integration.core.MessageSource; +import org.springframework.integration.expression.FunctionExpression; +import org.springframework.integration.expression.ValueExpression; +import org.springframework.integration.jdbc.JdbcPollingChannelAdapter; +import org.springframework.integration.kafka.outbound.KafkaProducerMessageHandler; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.MessageHandler; +import org.springframework.messaging.support.GenericMessage; + +import javax.sql.DataSource; +import java.util.HashMap; +import java.util.List; +import java.util.Objects; +import java.util.UUID; +import java.util.function.Function; + +@Slf4j +@Configuration +@ConditionalOnProperty( + value = "numecoeval.features.eqp" +) +public class EquipementPhysiqueIntegrationConfig { + + private static final String COLUMN_NAME_NOM_COURT_DATACENTER = "nom_court_datacenter"; + + @Value("${numecoeval.topic.entree.equipementPhysique}") + String topicEntreeEquipementPhysique; + + @Value("${numecoeval.topic.partition}") + Integer topicPartition; + + /** + * Topic Kafka pour les équipements physiques + * + * @return Topic à créer dans Kafka + */ + @Bean + public NewTopic topicEntreeEquipementPhysique() { + return new NewTopic(topicEntreeEquipementPhysique, topicPartition, (short) 1); + } + + @Bean + public MessageChannel entreeEquipementPhysique() { + return new DirectChannel(); + } + + @Bean + public MessageChannel entreeEquipementPhysiqueSplitter() { + return new DirectChannel(); + } + + @Bean + @ServiceActivator(inputChannel = "entreeEquipementPhysique") + public MessageHandler equipementPhysiqueHandler(KafkaTemplate<String, EquipementPhysiqueDTO> kafkaTemplate) { + KafkaProducerMessageHandler<String, EquipementPhysiqueDTO> handler = + new KafkaProducerMessageHandler<>(kafkaTemplate); + handler.setMessageKeyExpression(new LiteralExpression(UUID.randomUUID().toString())); + handler.setTopicExpression(new ValueExpression<>(topicEntreeEquipementPhysique)); + Function<Message<?>, Long> partitionIdRandomFn = (m) -> (long) (Math.random() * topicPartition); + handler.setPartitionIdExpression(new FunctionExpression<>(partitionIdRandomFn)); + return handler; + } + + @Bean + @InboundChannelAdapter( + value = "entreeEquipementPhysiqueSplitter", + poller = @Poller(fixedDelay = "5000") + ) + @SuppressWarnings("java:S1452") // La classe JdbcPollingChannelAdapter n'est pas compatible avec une classe fixe + public MessageSource<?> getEquipementPhysiqueToProcess(DataSource dataSource) { + JdbcPollingChannelAdapter adapter = new JdbcPollingChannelAdapter(dataSource, + """ + SELECT eqp.*, + dc.id as dc_id, + dc.date_creation as dc_date_creation, + dc.date_update as dc_date_update, + dc.localisation as dc_localisation, + dc.nom_long_datacenter as dc_nom_long_datacenter, + dc.pue as dc_pue, + dc.nom_entite as dc_nom_entite + FROM en_equipement_physique eqp + LEFT JOIN en_data_center dc ON dc.nom_lot = eqp.nom_lot and dc.nom_court_datacenter = eqp.nom_court_datacenter + WHERE eqp.statut_traitement = 'A_INGERER' + ORDER BY nom_lot ASC, date_lot ASC, nom_organisation ASC + LIMIT 1000 + """ + ); + adapter.setUpdateSql("UPDATE en_equipement_physique SET statut_traitement = 'INGERE', date_update = now() WHERE id in (:id)"); + adapter.setRowMapper((rs, index) -> + EquipementPhysiqueDTO.builder() + .id(rs.getLong("id")) + .nomLot(rs.getString("nom_lot")) + .dateLot(ResultSetUtils.getLocalDate(rs, "date_lot")) + .nomOrganisation(rs.getString("nom_organisation")) + .nomEntite(rs.getString("nom_entite")) + .nomEquipementPhysique(rs.getString("nom_equipement_physique")) + .consoElecAnnuelle(ResultSetUtils.getDouble(rs, "conso_elec_annuelle")) + .dateAchat(ResultSetUtils.getLocalDate(rs, "date_achat")) + .dateRetrait(ResultSetUtils.getLocalDate(rs, "date_retrait")) + .goTelecharge(ResultSetUtils.getFloat(rs, "go_telecharge")) + .modele(rs.getString("modele")) + .nbCoeur(rs.getString("nb_coeur")) + .modeUtilisation(rs.getString("mode_utilisation")) + .tauxUtilisation(rs.getDouble("taux_utilisation")) + .paysDUtilisation(rs.getString("pays_utilisation")) + .quantite(ResultSetUtils.getDouble(rs, "quantite")) + .serveur(rs.getBoolean("serveur")) + .statut(rs.getString("statut")) + .type(rs.getString("type")) + .utilisateur(rs.getString("utilisateur")) + .nomCourtDatacenter(rs.getString(COLUMN_NAME_NOM_COURT_DATACENTER)) + .nomSourceDonnee(rs.getString("nom_source_donnee")) + .dataCenter( + DataCenterDTO.builder() + .id(rs.getLong("dc_id")) + .localisation(rs.getString("dc_localisation")) + .nomLongDatacenter(rs.getString("dc_nom_long_datacenter")) + .pue(ResultSetUtils.getDouble(rs, "dc_pue")) + .nomCourtDatacenter(rs.getString(COLUMN_NAME_NOM_COURT_DATACENTER)) + .nomEntite(rs.getString("dc_nom_entite")) + .nomOrganisation(rs.getString("nom_organisation")) + .dateLot(ResultSetUtils.getLocalDate(rs, "date_lot")) + .build() + ) + .build() + ); + return adapter; + } + + @Splitter( + inputChannel = "entreeEquipementPhysiqueSplitter", + outputChannel = "entreeEquipementPhysique" + ) + public List<Message<EquipementPhysiqueDTO>> splitListEquipementPhysique(Message<List<EquipementPhysiqueDTO>> messageList) { + if (messageList == null) return List.of(); + + return messageList.getPayload().stream() + .map((EquipementPhysiqueDTO equipementPhysiqueDTO) -> { + var headers = new HashMap<String, Object>(); + headers.put(Constants.NOM_LOT, equipementPhysiqueDTO.getNomLot()); + headers.put(Constants.DATELOT, Objects.toString(equipementPhysiqueDTO.getDateLot(), "")); + headers.put(Constants.NOM_ORGANISATION, equipementPhysiqueDTO.getNomOrganisation()); + return (Message<EquipementPhysiqueDTO>) new GenericMessage<>(equipementPhysiqueDTO, headers); + }) + .toList(); + } +} \ No newline at end of file diff --git a/services/api-event-donneesentrees/src/main/java/org/mte/numecoeval/donneesentrees/infrastructure/config/MessagerieIntegrationConfig.java b/services/api-event-donneesentrees/src/main/java/org/mte/numecoeval/donneesentrees/infrastructure/config/MessagerieIntegrationConfig.java new file mode 100644 index 00000000..371fa3d9 --- /dev/null +++ b/services/api-event-donneesentrees/src/main/java/org/mte/numecoeval/donneesentrees/infrastructure/config/MessagerieIntegrationConfig.java @@ -0,0 +1,121 @@ +package org.mte.numecoeval.donneesentrees.infrastructure.config; + +import lombok.extern.slf4j.Slf4j; +import org.apache.kafka.clients.admin.NewTopic; +import org.mte.numecoeval.common.utils.ResultSetUtils; +import org.mte.numecoeval.donneesentrees.infrastructure.utils.Constants; +import org.mte.numecoeval.topic.data.MessagerieDTO; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.expression.common.LiteralExpression; +import org.springframework.integration.annotation.InboundChannelAdapter; +import org.springframework.integration.annotation.Poller; +import org.springframework.integration.annotation.ServiceActivator; +import org.springframework.integration.annotation.Splitter; +import org.springframework.integration.channel.DirectChannel; +import org.springframework.integration.core.MessageSource; +import org.springframework.integration.expression.ValueExpression; +import org.springframework.integration.jdbc.JdbcPollingChannelAdapter; +import org.springframework.integration.kafka.outbound.KafkaProducerMessageHandler; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.MessageHandler; +import org.springframework.messaging.support.GenericMessage; + +import javax.sql.DataSource; +import java.util.HashMap; +import java.util.List; +import java.util.Objects; +import java.util.UUID; + +@Slf4j +@Configuration +@ConditionalOnProperty( + value = "numecoeval.features.mes" +) +public class MessagerieIntegrationConfig { + + @Value("${numecoeval.topic.entree.messagerie}") + String topicEntreeMessagerie; + + @Bean + public NewTopic topicEntreeMessagerie() { + return new NewTopic(topicEntreeMessagerie, 1, (short) 1); + } + + @Bean + public MessageChannel entreeMessagerie() { + return new DirectChannel(); + } + + @Bean + public MessageChannel entreeMessagerieSplitter() { + return new DirectChannel(); + } + + @Bean + @ServiceActivator(inputChannel = "entreeMessagerie") + public MessageHandler messagerieHandler(KafkaTemplate<String, MessagerieDTO> kafkaTemplate) { + KafkaProducerMessageHandler<String, MessagerieDTO> handler = + new KafkaProducerMessageHandler<>(kafkaTemplate); + handler.setMessageKeyExpression(new LiteralExpression(UUID.randomUUID().toString())); + handler.setTopicExpression(new ValueExpression<>(topicEntreeMessagerie)); + return handler; + } + + @Bean + @InboundChannelAdapter( + value = "entreeMessagerieSplitter", + poller = @Poller(fixedDelay = "5000") + ) + @SuppressWarnings("java:S1452") // La classe JdbcPollingChannelAdapter n'est pas compatible avec une classe fixe + public MessageSource<?> getMessagerieToProcess(DataSource dataSource) { + JdbcPollingChannelAdapter adapter = new JdbcPollingChannelAdapter(dataSource, + """ + SELECT * + FROM en_messagerie mes + WHERE mes.statut_traitement = 'A_INGERER' + ORDER BY nom_lot ASC, date_lot ASC, nom_organisation ASC + LIMIT 1000 + """ + ); + adapter.setUpdateSql("UPDATE en_messagerie SET statut_traitement = 'INGERE', date_update = now() WHERE id in (:id)"); + adapter.setRowMapper((rs, index) -> + MessagerieDTO.builder() + .id(rs.getLong("id")) + .nomLot(rs.getString("nom_lot")) + .dateLot(ResultSetUtils.getLocalDate(rs, "date_lot")) + .nomOrganisation(rs.getString("nom_organisation")) + .nomEntite(rs.getString("nom_entite")) + .nomSourceDonnee(rs.getString("nom_source_donnee")) + .moisAnnee(rs.getInt("mois_annee")) + .nombreMailEmis(ResultSetUtils.getInteger(rs, "nombre_mail_emis")) + .nombreMailEmisXDestinataires(ResultSetUtils.getInteger(rs, "nombre_mail_emisxdestinataires")) + .volumeTotalMailEmis(ResultSetUtils.getInteger(rs, "volume_total_mail_emis")) + .build() + ); + return adapter; + } + + @Splitter( + inputChannel = "entreeMessagerieSplitter", + outputChannel = "entreeMessagerie" + ) + public List<Message<MessagerieDTO>> splitListMessagerie(Message<List<MessagerieDTO>> messageList) { + if (messageList == null) return List.of(); + + return messageList.getPayload().stream() + .map(messagerieDTO -> { + var headers = new HashMap<String, Object>(); + headers.put(Constants.NOM_LOT, messagerieDTO.getNomLot()); + headers.put(Constants.DATELOT, Objects.toString(messagerieDTO.getDateLot(), "")); + headers.put(Constants.NOM_ORGANISATION, messagerieDTO.getNomOrganisation()); + return (Message<MessagerieDTO>) new GenericMessage<>(messagerieDTO, headers); + }) + .toList(); + } + +} diff --git a/services/api-event-donneesentrees/src/main/java/org/mte/numecoeval/donneesentrees/infrastructure/utils/Constants.java b/services/api-event-donneesentrees/src/main/java/org/mte/numecoeval/donneesentrees/infrastructure/utils/Constants.java new file mode 100644 index 00000000..6544cd6e --- /dev/null +++ b/services/api-event-donneesentrees/src/main/java/org/mte/numecoeval/donneesentrees/infrastructure/utils/Constants.java @@ -0,0 +1,7 @@ +package org.mte.numecoeval.donneesentrees.infrastructure.utils; + +public class Constants { + public static final String NOM_LOT = "nomLot"; + public static final String DATELOT = "dateLot"; + public static final String NOM_ORGANISATION = "nomOrganisation"; +} diff --git a/services/api-event-donneesentrees/src/main/resources/application.yaml b/services/api-event-donneesentrees/src/main/resources/application.yaml new file mode 100644 index 00000000..ab6e3fc6 --- /dev/null +++ b/services/api-event-donneesentrees/src/main/resources/application.yaml @@ -0,0 +1,55 @@ +spring: + sql: + init: + mode: always + # Base de données + datasource: + generate-unique-name: true + url: "jdbc:postgresql://localhost:5432/postgres?reWriteBatchedInserts=true" + username: postgres + password: postgres + driver-class-name: org.postgresql.Driver + tomcat: + test-on-borrow: false + jmx-enabled: false + max-active: 100 + # Kafka + kafka: + bootstrap-servers: localhost:9092 + producer: + batch-size: 16384 + buffer-memory: 33554432 + retries: 0 + key-serializer: org.apache.kafka.common.serialization.StringSerializer + value-serializer: org.springframework.kafka.support.serializer.JsonSerializer + +# Application +numecoeval: + topic: + partition: "4" + entree: + equipementPhysique: "entree_equipementPhysique" + messagerie: "entree_messagerie" + features: + eqp: true + mes: true + + +server: + port: 18090 + shutdown: graceful + +# Actuator +management: + server: + port: 18090 + security: + user: + name: "actuator" + password: "actuator" + roles: ACTUATOR_ADMIN + endpoints: + web: + base-path: / + exposure: + include: health,prometheus,httptrace,info,metrics,mappings \ No newline at end of file diff --git a/services/api-event-donneesentrees/src/main/resources/logback.xml b/services/api-event-donneesentrees/src/main/resources/logback.xml new file mode 100644 index 00000000..1171d0e2 --- /dev/null +++ b/services/api-event-donneesentrees/src/main/resources/logback.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<configuration> + <!-- Spring default.xml --> + <conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter" /> + <conversionRule conversionWord="wex" converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter" /> + <conversionRule conversionWord="wEx" converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter" /> + + <property name="CONSOLE_LOG_PATTERN" value="${CONSOLE_LOG_PATTERN:-%clr(%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%t]){faint} %clr(%-42.42logger{0}){cyan} %clr(:){faint} %clr(%X{nomLot} - %X{dateLot} - %X{nomOrganisation}){magenta} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/> + <property name="CONSOLE_LOG_CHARSET" value="${CONSOLE_LOG_CHARSET:-${file.encoding:-UTF-8}}"/> + + <!-- console-appender.xml--> + <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> + <encoder> + <pattern>${CONSOLE_LOG_PATTERN}</pattern> + <charset>${CONSOLE_LOG_CHARSET}</charset> + </encoder> + </appender> + + <root level="${APP_LOGGING_LEVEL:-INFO}"> + <appender-ref ref="CONSOLE" /> + </root> + <logger name="org.springframework.web" level="INFO"/> + <logger name="org.springframework.kafka" level="WARN"/> + <logger name="org.apache.kafka" level="WARN"/> +</configuration> \ No newline at end of file diff --git a/services/api-event-donneesentrees/src/test/java/org/mte/numecoeval/donneesentrees/IntegrationEquipementPhysiqueTest.java b/services/api-event-donneesentrees/src/test/java/org/mte/numecoeval/donneesentrees/IntegrationEquipementPhysiqueTest.java new file mode 100644 index 00000000..50171dc7 --- /dev/null +++ b/services/api-event-donneesentrees/src/test/java/org/mte/numecoeval/donneesentrees/IntegrationEquipementPhysiqueTest.java @@ -0,0 +1,71 @@ +package org.mte.numecoeval.donneesentrees; + +import io.zonky.test.db.AutoConfigureEmbeddedDatabase; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mte.numecoeval.donneesentrees.test.jdbc.ScriptUtils; +import org.mte.numecoeval.donneesentrees.test.kafka.KafkaConsumerEquipementPhysique; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.kafka.test.context.EmbeddedKafka; +import org.springframework.test.context.ActiveProfiles; + +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +import static org.awaitility.Awaitility.await; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@ActiveProfiles(profiles = {"test"}) +@AutoConfigureEmbeddedDatabase +@EmbeddedKafka( + bootstrapServersProperty = "spring.kafka.bootstrap-servers", + partitions = 1 +) +class IntegrationEquipementPhysiqueTest { + + @Autowired + JdbcTemplate jdbcTemplate; + + @Autowired + KafkaConsumerEquipementPhysique kafkaConsumer; + + @BeforeEach + void setup() { + ScriptUtils.loadScript(jdbcTemplate.getDataSource(), "sql/schema.sql"); + jdbcTemplate.batchUpdate("DELETE FROM en_data_center"); + jdbcTemplate.batchUpdate("DELETE FROM en_equipement_physique"); + } + + @Test + void whenDataAvailable_shouldUpdateStatusAndSendMessage() { + ScriptUtils.loadScript(jdbcTemplate.getDataSource(), "sql/equipment_physique.sql"); + + // Given + var queryResultCountEntrees = jdbcTemplate.query( + "SELECT count(*) as nbr from en_equipement_physique", + (rs, rowNum) -> rs.getInt("nbr") + ); + var nbrEntrees = queryResultCountEntrees.get(0); + + // When + jdbcTemplate.batchUpdate("UPDATE en_equipement_physique SET statut_traitement = 'A_INGERER'"); + + // Then + await().atMost(10, TimeUnit.SECONDS) + .until(() -> { + var queryResultCountEntreesIngerees = jdbcTemplate.query( + "SELECT count(*) as nbr from en_equipement_physique WHERE statut_traitement = 'INGERE'", + (rs, rowNum) -> rs.getInt("nbr") + ); + + return !queryResultCountEntreesIngerees.isEmpty() && Objects.equals(nbrEntrees, queryResultCountEntreesIngerees.get(0)); + }); + + await().atMost(5, TimeUnit.SECONDS) + .until(() -> nbrEntrees == kafkaConsumer.getPayloads().size()); + + } + +} diff --git a/services/api-event-donneesentrees/src/test/java/org/mte/numecoeval/donneesentrees/IntegrationMessagerieTest.java b/services/api-event-donneesentrees/src/test/java/org/mte/numecoeval/donneesentrees/IntegrationMessagerieTest.java new file mode 100644 index 00000000..f9dad59a --- /dev/null +++ b/services/api-event-donneesentrees/src/test/java/org/mte/numecoeval/donneesentrees/IntegrationMessagerieTest.java @@ -0,0 +1,71 @@ +package org.mte.numecoeval.donneesentrees; + +import io.zonky.test.db.AutoConfigureEmbeddedDatabase; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mte.numecoeval.donneesentrees.test.jdbc.ScriptUtils; +import org.mte.numecoeval.donneesentrees.test.kafka.KafkaConsumerMessagerie; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.kafka.test.context.EmbeddedKafka; +import org.springframework.test.context.ActiveProfiles; + +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +import static org.awaitility.Awaitility.await; +import static org.junit.jupiter.api.Assertions.assertEquals; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@ActiveProfiles(profiles = {"test"}) +@AutoConfigureEmbeddedDatabase +@EmbeddedKafka( + bootstrapServersProperty = "spring.kafka.bootstrap-servers", + partitions = 1 +) +class IntegrationMessagerieTest { + + @Autowired + JdbcTemplate jdbcTemplate; + + @Autowired + KafkaConsumerMessagerie kafkaConsumer; + + @BeforeEach + void setup() { + ScriptUtils.loadScript(jdbcTemplate.getDataSource(), "sql/schema.sql"); + jdbcTemplate.batchUpdate("DELETE FROM en_messagerie"); + } + + @Test + void whenDataAvailable_shouldUpdateStatusAndSendMessage() { + ScriptUtils.loadScript(jdbcTemplate.getDataSource(), "sql/messagerie.sql"); + + // Given + var queryResultCountEntrees = jdbcTemplate.query( + "SELECT count(*) as nbr from en_messagerie", + (rs, rowNum) -> rs.getInt("nbr") + ); + var nbrEntrees = queryResultCountEntrees.get(0); + + // When + jdbcTemplate.batchUpdate("UPDATE en_messagerie SET statut_traitement = 'A_INGERER'"); + + // Then + assertEquals(1, nbrEntrees); + await().atMost(5, TimeUnit.SECONDS) + .until(() -> { + var queryResultCountEntreesIngerees = jdbcTemplate.query( + "SELECT count(*) as nbr from en_messagerie WHERE statut_traitement = 'INGERE'", + (rs, rowNum) -> rs.getInt("nbr") + ); + + return !queryResultCountEntreesIngerees.isEmpty() && Objects.equals(nbrEntrees, queryResultCountEntreesIngerees.get(0)); + }); + + await().atMost(5, TimeUnit.SECONDS) + .until(() -> nbrEntrees == kafkaConsumer.getPayloads().size()); + } + +} diff --git a/services/api-event-donneesentrees/src/test/java/org/mte/numecoeval/donneesentrees/test/jdbc/ScriptUtils.java b/services/api-event-donneesentrees/src/test/java/org/mte/numecoeval/donneesentrees/test/jdbc/ScriptUtils.java new file mode 100644 index 00000000..d45a48b1 --- /dev/null +++ b/services/api-event-donneesentrees/src/test/java/org/mte/numecoeval/donneesentrees/test/jdbc/ScriptUtils.java @@ -0,0 +1,29 @@ +package org.mte.numecoeval.donneesentrees.test.jdbc; + +import org.springframework.core.io.ClassPathResource; +import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator; + +import javax.sql.DataSource; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +public class ScriptUtils { + + public static void loadScript(DataSource dataSource, String scriptSql) { + loadScripts(dataSource, Collections.singletonList(scriptSql)); + } + + public static void loadScripts(DataSource dataSource, List<String> scriptsSqls) { + assertNotNull(dataSource); + + var populator = new ResourceDatabasePopulator(); + for(var scriptSQL : scriptsSqls) { + populator.addScripts( + new ClassPathResource(scriptSQL) + ); + } + populator.execute(dataSource); + } +} diff --git a/services/api-event-donneesentrees/src/test/java/org/mte/numecoeval/donneesentrees/test/kafka/KafkaConsumer.java b/services/api-event-donneesentrees/src/test/java/org/mte/numecoeval/donneesentrees/test/kafka/KafkaConsumer.java new file mode 100644 index 00000000..c76ac1d7 --- /dev/null +++ b/services/api-event-donneesentrees/src/test/java/org/mte/numecoeval/donneesentrees/test/kafka/KafkaConsumer.java @@ -0,0 +1,32 @@ +package org.mte.numecoeval.donneesentrees.test.kafka; + +import lombok.extern.slf4j.Slf4j; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; + +@Slf4j +public abstract class KafkaConsumer<T> { + List<T> payloads = new ArrayList<>(); + protected CountDownLatch latch = new CountDownLatch(1); + + abstract public void consumeMessage(T message); + + public void reset() { + payloads = new ArrayList<>(); + resetLatch(); + } + + public CountDownLatch getLatch() { + return latch; + } + + public void resetLatch() { + latch = new CountDownLatch(1); + } + + public List<T> getPayloads() { + return payloads; + } +} diff --git a/services/api-event-donneesentrees/src/test/java/org/mte/numecoeval/donneesentrees/test/kafka/KafkaConsumerConfig.java b/services/api-event-donneesentrees/src/test/java/org/mte/numecoeval/donneesentrees/test/kafka/KafkaConsumerConfig.java new file mode 100644 index 00000000..546cbec0 --- /dev/null +++ b/services/api-event-donneesentrees/src/test/java/org/mte/numecoeval/donneesentrees/test/kafka/KafkaConsumerConfig.java @@ -0,0 +1,39 @@ +package org.mte.numecoeval.donneesentrees.test.kafka; + +import org.mte.numecoeval.topic.data.EquipementPhysiqueDTO; +import org.mte.numecoeval.topic.data.MessagerieDTO; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.kafka.annotation.EnableKafka; +import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory; +import org.springframework.kafka.core.ConsumerFactory; + +@Configuration +@EnableKafka +public class KafkaConsumerConfig { + + @Bean + public ConcurrentKafkaListenerContainerFactory<String, EquipementPhysiqueDTO> kafkaListenerContainerFactoryEquipementPhysique(ConsumerFactory<String, EquipementPhysiqueDTO> consumerFactory) { + ConcurrentKafkaListenerContainerFactory<String, EquipementPhysiqueDTO> factory = new ConcurrentKafkaListenerContainerFactory<>(); + factory.setConsumerFactory(consumerFactory); + return factory; + } + + @Bean + public KafkaConsumer<EquipementPhysiqueDTO> kafkaConsumerEquipementPhysique() { + return new KafkaConsumerEquipementPhysique(); + } + + @Bean + public ConcurrentKafkaListenerContainerFactory<String, MessagerieDTO> kafkaListenerContainerFactoryMessagerie(ConsumerFactory<String, MessagerieDTO> consumerFactory) { + ConcurrentKafkaListenerContainerFactory<String, MessagerieDTO> factory = new ConcurrentKafkaListenerContainerFactory<>(); + factory.setConsumerFactory(consumerFactory); + return factory; + } + + @Bean + public KafkaConsumer<MessagerieDTO> kafkaConsumerMessagerie() { + return new KafkaConsumerMessagerie(); + } + +} diff --git a/services/api-event-donneesentrees/src/test/java/org/mte/numecoeval/donneesentrees/test/kafka/KafkaConsumerEquipementPhysique.java b/services/api-event-donneesentrees/src/test/java/org/mte/numecoeval/donneesentrees/test/kafka/KafkaConsumerEquipementPhysique.java new file mode 100644 index 00000000..c19382c7 --- /dev/null +++ b/services/api-event-donneesentrees/src/test/java/org/mte/numecoeval/donneesentrees/test/kafka/KafkaConsumerEquipementPhysique.java @@ -0,0 +1,18 @@ +package org.mte.numecoeval.donneesentrees.test.kafka; + +import lombok.extern.slf4j.Slf4j; +import org.mte.numecoeval.topic.data.EquipementPhysiqueDTO; +import org.springframework.kafka.annotation.KafkaListener; + +@Slf4j +public class KafkaConsumerEquipementPhysique extends KafkaConsumer<EquipementPhysiqueDTO> { + + @Override + @KafkaListener(topics = "${numecoeval.topic.entree.equipementPhysique}") + public void consumeMessage(EquipementPhysiqueDTO message) { + log.info("############# Playload = {}", message); + payloads.add(message); + latch.countDown(); + + } +} diff --git a/services/api-event-donneesentrees/src/test/java/org/mte/numecoeval/donneesentrees/test/kafka/KafkaConsumerMessagerie.java b/services/api-event-donneesentrees/src/test/java/org/mte/numecoeval/donneesentrees/test/kafka/KafkaConsumerMessagerie.java new file mode 100644 index 00000000..63e21045 --- /dev/null +++ b/services/api-event-donneesentrees/src/test/java/org/mte/numecoeval/donneesentrees/test/kafka/KafkaConsumerMessagerie.java @@ -0,0 +1,18 @@ +package org.mte.numecoeval.donneesentrees.test.kafka; + +import lombok.extern.slf4j.Slf4j; +import org.mte.numecoeval.topic.data.MessagerieDTO; +import org.springframework.kafka.annotation.KafkaListener; + +@Slf4j +public class KafkaConsumerMessagerie extends KafkaConsumer<MessagerieDTO> { + + @Override + @KafkaListener(topics = "${numecoeval.topic.entree.messagerie}") + public void consumeMessage(MessagerieDTO message) { + log.info("############# Playload = {}", message); + payloads.add(message); + latch.countDown(); + + } +} diff --git a/services/api-event-donneesentrees/src/test/resources/application-test.yaml b/services/api-event-donneesentrees/src/test/resources/application-test.yaml new file mode 100644 index 00000000..16c24052 --- /dev/null +++ b/services/api-event-donneesentrees/src/test/resources/application-test.yaml @@ -0,0 +1,33 @@ +spring: + kafka: + bootstrap-servers: localhost:9092 + consumer: + group-id: api-event-donneesentrees + auto-offset-reset: earliest + enable-auto-commit: false + value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer + key-deserializer: org.apache.kafka.common.serialization.StringDeserializer + properties: + spring.json.trusted.packages: org.mte.numecoeval.* + producer: + batch-size: 16384 + buffer-memory: 33554432 + retries: 0 + key-serializer: org.apache.kafka.common.serialization.StringSerializer + value-serializer: org.springframework.kafka.support.serializer.JsonSerializer + +# Application +numecoeval: + features: + eqp: true + mes: true + +#CONFIGURATION BASES +zonky: + test: + database: + postgres: + server: + properties: + max_connections: 100 + provider: zonky \ No newline at end of file diff --git a/services/api-event-donneesentrees/src/test/resources/logback-test.xml b/services/api-event-donneesentrees/src/test/resources/logback-test.xml new file mode 100644 index 00000000..d01f3999 --- /dev/null +++ b/services/api-event-donneesentrees/src/test/resources/logback-test.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> +<configuration> + <!-- Spring default.xml --> + <conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter"/> + <conversionRule conversionWord="wex" + converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter"/> + <conversionRule conversionWord="wEx" + converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter"/> + + <property name="CONSOLE_LOG_PATTERN" + value="${CONSOLE_LOG_PATTERN:-%clr(%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%t]){faint} %clr(%-42.42logger{0}){cyan} %clr(:){faint} %clr(%X{nomLot} - %X{dateLot} - %X{nomOrganisation}){magenta} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/> + + <!-- console-appender.xml--> + <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> + <encoder> + <pattern>${CONSOLE_LOG_PATTERN}</pattern> + </encoder> + </appender> + + <root level="${APP_LOGGING_LEVEL:-INFO}"> + <appender-ref ref="CONSOLE"/> + </root> + <logger name="org.springframework.web" level="INFO"/> + <logger name="org.springframework.kafka" level="ERROR"/> + <logger name="org.apache.kafka" level="ERROR"/> +</configuration> \ No newline at end of file diff --git a/services/api-event-donneesentrees/src/test/resources/sql/equipment_physique.sql b/services/api-event-donneesentrees/src/test/resources/sql/equipment_physique.sql new file mode 100644 index 00000000..4d532f98 --- /dev/null +++ b/services/api-event-donneesentrees/src/test/resources/sql/equipment_physique.sql @@ -0,0 +1,14 @@ +INSERT INTO en_data_center (id, date_creation, nom_lot, date_lot, nom_organisation, localisation, nom_court_datacenter, + nom_entite, nom_long_datacenter, pue, statut_traitement, date_update, nom_source_donnee) +VALUES (6002, '2023-03-08 19:45:39.484539', 'ENTITE|2022-01-01', '2022-01-01', 'ENTITE', 'France', 'TestDataCenter', 'ENTITE', 'TEST', 1.44, + 'EN_ATTENTE', NULL, 'SOURCE_A'); + +INSERT INTO en_equipement_physique (id, date_creation, nom_lot, date_lot, nom_organisation, conso_elec_annuelle, + date_achat, date_retrait, duree_vie_defaut, go_telecharge, modele, nb_coeur, mode_utilisation, taux_utilisation, + nb_jour_utilise_an, nom_court_datacenter, nom_entite, + nom_equipement_physique, pays_utilisation, quantite, serveur, statut, "type", + utilisateur, statut_traitement, date_update, nom_source_donnee) +VALUES (376826, '2023-03-23 15:53:51.179031', 'ENTITE|2022-01-01', '2022-01-01', 'ENTITE', NULL, '2022-01-01', NULL, 8.0, 0.0, + 'computer monitor-01-55', '','BYOD',0.4, 365.0, '', 'ENTITE', '2023-03-09-Ecran-105', 'France', 13.0, false, 'actif', 'Ecran', + '', 'EN_ATTENTE', NULL, 'SOURCE_A'); + diff --git a/services/api-event-donneesentrees/src/test/resources/sql/messagerie.sql b/services/api-event-donneesentrees/src/test/resources/sql/messagerie.sql new file mode 100644 index 00000000..4d5957a7 --- /dev/null +++ b/services/api-event-donneesentrees/src/test/resources/sql/messagerie.sql @@ -0,0 +1,6 @@ + + +INSERT INTO en_messagerie (id, date_creation, nom_lot, date_lot, nom_organisation, mois_annee, nom_entite, nombre_mail_emis, + nombre_mail_emisxdestinataires, volume_total_mail_emis, statut_traitement, date_update, nom_source_donnee) +VALUES (11002, '2023-03-23 15:53:37.073851', 'ENTITE|01-01-2022', '2022-01-01', 'ENTITE', 201901, 'ENTITE', 123456, 513, 84351385, + 'EN_ATTENTE', NULL, 'SOURCE_F'); \ No newline at end of file diff --git a/services/api-event-donneesentrees/src/test/resources/sql/schema.sql b/services/api-event-donneesentrees/src/test/resources/sql/schema.sql new file mode 100644 index 00000000..32f7442b --- /dev/null +++ b/services/api-event-donneesentrees/src/test/resources/sql/schema.sql @@ -0,0 +1,138 @@ +CREATE TABLE IF NOT EXISTS en_donnees_entrees +( + id int8 NOT NULL, + date_update timestamp NULL, + date_creation timestamp NULL, + date_lot date NULL, + nom_organisation varchar(255) NULL, + nom_lot varchar(255) NULL, + nbr_applications int8 NULL, + nbr_data_center int8 NULL, + nbr_equipements_physiques int8 NULL, + nbr_equipements_virtuels int8 NULL, + nbr_messageries int8 NULL, + CONSTRAINT en_donnees_entrees_pkey PRIMARY KEY (id) +); + +CREATE TABLE IF NOT EXISTS en_data_center +( + id int8 NOT NULL, + date_creation timestamp NULL, + date_update timestamp NULL, + date_lot date NULL, + nom_lot varchar(255) NULL, + nom_organisation varchar(255) NULL, + nom_source_donnee varchar(255) NULL, + localisation varchar(255) NULL, + nom_court_datacenter varchar(255) NULL, + nom_entite varchar(255) NULL, + nom_long_datacenter varchar(255) NULL, + pue float8 NULL, + CONSTRAINT en_data_center_pkey PRIMARY KEY (id) +); + +CREATE TABLE IF NOT EXISTS en_equipement_physique +( + id int8 NOT NULL, + date_creation timestamp NULL, + date_update timestamp NULL, + date_lot date NULL, + nom_lot varchar(255) NULL, + nom_organisation varchar(255) NULL, + nom_source_donnee varchar(255) NULL, + conso_elec_annuelle float8 NULL, + date_achat date NULL, + date_retrait date NULL, + duree_vie_defaut float8 NULL, + go_telecharge float4 NULL, + modele varchar(255) NULL, + nb_coeur varchar(255) NULL, + nb_jour_utilise_an float8 NULL, + nom_court_datacenter varchar(255) NULL, + nom_entite varchar(255) NULL, + nom_equipement_physique varchar(255) NULL, + pays_utilisation varchar(255) NULL, + quantite float8 NULL, + serveur bool NOT NULL, + statut varchar(255) NULL, + mode_utilisation varchar(255) NULL, + taux_utilisation float8 NULL, + "type" varchar(255) NULL, + utilisateur varchar(255) NULL, + CONSTRAINT en_equipement_physique_pkey PRIMARY KEY (id) +); + +CREATE TABLE IF NOT EXISTS en_equipement_virtuel +( + id int8 NOT NULL, + date_creation timestamp NULL, + date_lot date NULL, + nom_lot varchar(255) NULL, + nom_organisation varchar(255) NULL, + nom_source_donnee varchar(255) NULL, + "cluster" varchar(255) NULL, + nom_entite varchar(255) NULL, + nom_equipement_physique varchar(255) NULL, + nom_equipement_virtuel varchar(255) NULL, + vcpu int4 NULL, + CONSTRAINT en_equipement_virtuel_pkey PRIMARY KEY (id) +); + +CREATE TABLE IF NOT EXISTS en_application +( + id int8 NOT NULL, + date_creation timestamp NULL, + date_lot date NULL, + nom_lot varchar(255) NULL, + nom_organisation varchar(255) NULL, + nom_source_donnee varchar(255) NULL, + domaine varchar(255) NULL, + nom_application varchar(255) NULL, + nom_entite varchar(255) NULL, + nom_equipement_virtuel varchar(255) NULL, + nom_equipement_physique varchar(255) NULL, + sous_domaine varchar(255) NULL, + type_environnement varchar(255) NULL, + CONSTRAINT en_application_pkey PRIMARY KEY (id) +); + + +CREATE TABLE IF NOT EXISTS en_entite +( + id int8 NOT NULL, + date_creation timestamp NULL, + date_update timestamp NULL, + date_lot date NULL, + nom_lot varchar(255) NULL, + nom_organisation varchar(255) NULL, + nom_source_donnee varchar(255) NULL, + nom_entite varchar(255) NULL, + nb_collaborateurs int4 NULL, + responsable_entite varchar(255) NULL, + responsable_numerique_durable varchar(255) NULL, + CONSTRAINT en_entite_pkey PRIMARY KEY (id) +); + +CREATE TABLE IF NOT EXISTS en_messagerie +( + id int8 NOT NULL, + date_creation timestamp NULL, + date_update timestamp NULL, + date_lot date NULL, + nom_lot varchar(255) NULL, + nom_organisation varchar(255) NULL, + nom_source_donnee varchar(255) NULL, + mois_annee int4 NULL, + nom_entite varchar(255) NULL, + nombre_mail_emis int4 NULL, + nombre_mail_emisxdestinataires int4 NULL, + volume_total_mail_emis int4 NULL, + CONSTRAINT en_messagerie_pkey PRIMARY KEY (id) +); + +ALTER TABLE IF EXISTS en_donnees_entrees ADD COLUMN IF NOT EXISTS statut_traitement varchar(255); +ALTER TABLE IF EXISTS en_data_center ADD COLUMN IF NOT EXISTS statut_traitement varchar(255); +ALTER TABLE IF EXISTS en_equipement_physique ADD COLUMN IF NOT EXISTS statut_traitement varchar(255); +ALTER TABLE IF EXISTS en_equipement_virtuel ADD COLUMN IF NOT EXISTS statut_traitement varchar(255); +ALTER TABLE IF EXISTS en_application ADD COLUMN IF NOT EXISTS statut_traitement varchar(255); +ALTER TABLE IF EXISTS en_messagerie ADD COLUMN IF NOT EXISTS statut_traitement varchar(255); diff --git a/services/api-expositiondonneesentrees/.gitignore b/services/api-expositiondonneesentrees/.gitignore new file mode 100644 index 00000000..549e00a2 --- /dev/null +++ b/services/api-expositiondonneesentrees/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/services/api-expositiondonneesentrees/LICENSE.txt b/services/api-expositiondonneesentrees/LICENSE.txt new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/services/api-expositiondonneesentrees/LICENSE.txt @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/services/api-expositiondonneesentrees/README.md b/services/api-expositiondonneesentrees/README.md new file mode 100644 index 00000000..628853ac --- /dev/null +++ b/services/api-expositiondonneesentrees/README.md @@ -0,0 +1,64 @@ +# api-expositiondonneesentrees + +API permettant d'envoyer des données d'entrées dans le système NumEcoEval. +L'import de données peut se faire via des fichiers CSV respectant un format spécifique ou au format JSON. +Le contrat d'interface est disponible au format [OpenAPI 3](src/main/resources/static/openapi.yaml). + +## Pré-requis + +- JDK 17 + - [Open JDK](https://jdk.java.net/java-se-ri/17) + - [Coretto](https://docs.aws.amazon.com/corretto/latest/corretto-17-ug/downloads-list.html) +- [Maven 3](https://maven.apache.org/download.cgi) +- Un IDE compatible avec Maven 3 et JDK 17 + +## Build + +Pour compiler l'application, utilisez la commande suivante : + +```bash +mvn clean install +``` + +### API REST et Contrat OpenAPI + +Le contrat OpenAPI 3 est disponible dans les ressources du +projet : [openapi.yaml](src/main/resources/static/openapi.yaml) + +Ce contrat est utilisé pour générer les interfaces et POJOs utilisés dans les communications REST via le plugin +Maven [openapi-generator-maven-plugin](https://github.com/OpenAPITools/openapi-generator/tree/master/modules/openapi-generator-maven-plugin) + +#### Profiles Maven + +- DEPENDENCY-CHECK : Un profile permettant un scan local des dépendances Maven avec le dependency-check Maven plugin + d'OWASP. + +### Configuration de l'application + +Editer le fichier [application.yml](src/main/resources/application.yaml) + +### Démarrer l'application + +Pour démarrer l'application avec la configuration actuelle, utilisez la commande suivante: + +```bash +mvn spring-boot:run +``` + +Avec la configuration par défaut, l'API REST sera disponible sur l'URL +suivante : [http://localhost:18081](http://localhost:18081) + +L'application utilise Springdoc-openapi permettant un affichage via Swagger-UI sur l' +URL [http://localhost:18081/api/swagger-ui/index.html](http://localhost:18081/api/swagger-ui/index.html). + +* Par défaut, le contrat d'interface OpenAPI est également disponible sur l' + URL [http://localhost:18081/openapi.yaml](http://localhost:18081/openapi.yaml). + +## Licences + +[Apache 2.0](LICENSE.txt) + +## Liens utiles + +- [Spring Documentation (version courante)](https://docs.spring.io/spring-framework/docs/current/reference/html/) +- [Spring Boot Documentation (version courante)](https://docs.spring.io/spring-boot/docs/current/reference/html/index.html) \ No newline at end of file diff --git a/services/api-expositiondonneesentrees/dependency_check_suppressions.xml b/services/api-expositiondonneesentrees/dependency_check_suppressions.xml new file mode 100644 index 00000000..7937ced3 --- /dev/null +++ b/services/api-expositiondonneesentrees/dependency_check_suppressions.xml @@ -0,0 +1,99 @@ +<?xml version="1.0" encoding="UTF-8"?> +<suppressions xmlns="https://jeremylong.github.io/DependencyCheck/dependency-suppression.1.1.xsd"> + + <suppress> + <notes><![CDATA[ + file name: spring-security-crypto-5.7.3.jar + La librairie Spring Security est en version 5.7.3. + Cette CVE est marquée uniquement jusqu'à la version 5.2.4 (exclus) + https://nvd.nist.gov/vuln/detail/CVE-2020-5408 + ]]></notes> + <cve>CVE-2020-5408</cve> + </suppress> + + <suppress> + <notes><![CDATA[ + file name: spring-web-5.3.22.jar + Dans notre contexte, nous ne recevons pas de code Java de l'extérieur et n'effectuons pas de dé-sérialisation de code Java + Côté Spring, le point est déjà remonté comme un faux-positif : https://github.com/spring-projects/spring-framework/issues/24434#issuecomment-744519525 + ]]></notes> + <cve>CVE-2016-1000027</cve> + </suppress> + + <suppress> + <notes><![CDATA[ + file name: snakeyaml-1.33.jar + Faux-positif : la version de Snakeyaml est la 1.33, la CVE existe uniquement sur les versions < 1.32 + https://nvd.nist.gov/vuln/detail/CVE-2022-38752 + ]]></notes> + <cve>CVE-2022-38752</cve> + </suppress> + + <suppress> + <notes><![CDATA[ + Faux-positif sur toutes les librairies utils: https://github.com/jeremylong/DependencyCheck/issues/5213 + Concerne : software.amazon.awssdk:utils qui n'est pas utilisé + ]]></notes> + <cve>CVE-2021-4277</cve> + </suppress> + + <suppress> + <notes><![CDATA[ + Faux-positif : matchent à tort sur tous les commons : https://github.com/jeremylong/DependencyCheck/issues/5132 + ]]></notes> + <cve>CVE-2021-37533</cve> + </suppress> + + <suppress> + <notes><![CDATA[ + Non-applicable : Snakeyaml n'est utilisé que par Spring Boot pour les fichiers application.yml + et non des fichiers externes. + ]]></notes> + <cve>CVE-2022-1471</cve> + </suppress> + + <suppress> + <notes><![CDATA[ + Non-applicable : Snakeyaml n'est utilisé que par Spring Boot pour les fichiers application.yml + et non des fichiers externes. + ]]></notes> + <cve>CVE-2022-3064</cve> + </suppress> + + <suppress> + <notes><![CDATA[ + Non-applicable : Snakeyaml n'est utilisé que par Spring Boot pour les fichiers application.yml + et non des fichiers externes. + ]]></notes> + <cve>CVE-2021-4235</cve> + </suppress> + + <suppress> + <notes><![CDATA[ + Non-applicable : json-smart est une dépendance transitive. + Les json sont limités à la taille des inventaires : la taille des listes n'a pas réellement de limite + et c'est souhaités comme cela (traitement de gros volumes de données). + ]]></notes> + <cve>CVE-2023-1370</cve> + </suppress> + + <suppress> + <notes><![CDATA[ + Faux-Positif : Concerne hutool-json et json-java qui ne sont pas utilisés. + Le faux-positif se trouve sur json-path, jackson-core, accessors-smart et json-smart. + cf. https://github.com/jeremylong/DependencyCheck/issues/5502 + ]]></notes> + <cve>CVE-2022-45688</cve> + </suppress> + + + <suppress> + <notes><![CDATA[ + Faux-Positif : Conformément au lien, nous ne sommes pas dans un des cas de vulnérabilité. + cf. cf. https://github.com/jeremylong/DependencyCheck/issues/5502 + ]]></notes> + <cve>CVE-2023-20862</cve> + </suppress> + + +</suppressions> diff --git a/services/api-expositiondonneesentrees/pom.xml b/services/api-expositiondonneesentrees/pom.xml new file mode 100644 index 00000000..353bd2a1 --- /dev/null +++ b/services/api-expositiondonneesentrees/pom.xml @@ -0,0 +1,401 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.mte.numecoeval</groupId> + <artifactId>core</artifactId> + <version>1.2.3-SNAPSHOT</version> + <relativePath/> <!-- lookup parent from repository --> + </parent> + <artifactId>api-expositiondonneesentrees</artifactId> + <version>1.2.3-SNAPSHOT</version> + <name>api-expositiondonneesentrees</name> + <description>API Exposition des données d'entrées - Exposition par API pour injecter des données d'entrées + </description> + + <repositories> + <repository> + <id>gitlab-maven</id> + <url>https://gitlab-forge.din.developpement-durable.gouv.fr/api/v4/projects/20519/packages/maven</url> + </repository> + </repositories> + + <distributionManagement> + <repository> + <id>gitlab-maven</id> + <url>https://gitlab-forge.din.developpement-durable.gouv.fr/api/v4/projects/20519/packages/maven</url> + </repository> + + <snapshotRepository> + <id>gitlab-maven</id> + <url>https://gitlab-forge.din.developpement-durable.gouv.fr/api/v4/projects/20519/packages/maven</url> + </snapshotRepository> + </distributionManagement> + + <properties> + </properties> + <dependencies> + <dependency> + <groupId>org.mte.numecoeval</groupId> + <artifactId>common</artifactId> + </dependency> + + <!-- REST --> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-web</artifactId> + </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-webflux</artifactId> + </dependency> + + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-cache</artifactId> + </dependency> + + <!-- JPA --> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-data-jpa</artifactId> + </dependency> + + <!-- Driver pour base de données --> + <dependency> + <groupId>org.postgresql</groupId> + <artifactId>postgresql</artifactId> + <scope>runtime</scope> + </dependency> + + <!-- Nécessaire avec Spring Boot 3 pour Hibernate --> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-validation</artifactId> + </dependency> + + <dependency> + <groupId>org.springdoc</groupId> + <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId> + </dependency> + + <!-- Security --> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-security</artifactId> + </dependency> + + <!-- Mapping --> + <dependency> + <groupId>org.mapstruct</groupId> + <artifactId>mapstruct</artifactId> + </dependency> + + <dependency> + <groupId>org.apache.commons</groupId> + <artifactId>commons-csv</artifactId> + </dependency> + + <!-- Utilitaire --> + <dependency> + <groupId>org.apache.commons</groupId> + <artifactId>commons-lang3</artifactId> + </dependency> + <dependency> + <groupId>org.apache.commons</groupId> + <artifactId>commons-collections4</artifactId> + </dependency> + + <!-- Monitoring --> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-actuator</artifactId> + </dependency> + <dependency> + <groupId>io.micrometer</groupId> + <artifactId>micrometer-registry-prometheus</artifactId> + <scope>runtime</scope> + </dependency> + + <!-- Lombok --> + <dependency> + <groupId>org.projectlombok</groupId> + <artifactId>lombok</artifactId> + <optional>true</optional> + </dependency> + + <!-- Code généré--> + <dependency> + <groupId>org.openapitools</groupId> + <artifactId>jackson-databind-nullable</artifactId> + </dependency> + + <!-- TU/TI --> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-test</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.springframework.security</groupId> + <artifactId>spring-security-test</artifactId> + <scope>test</scope> + </dependency> + + <dependency> + <groupId>org.junit.platform</groupId> + <artifactId>junit-platform-suite</artifactId> + <scope>test</scope> + </dependency> + + <dependency> + <groupId>org.junit.platform</groupId> + <artifactId>junit-platform-engine</artifactId> + <scope>test</scope> + </dependency> + + <dependency> + <groupId>org.meanbean</groupId> + <artifactId>meanbean</artifactId> + <scope>test</scope> + </dependency> + + <dependency> + <groupId>nl.jqno.equalsverifier</groupId> + <artifactId>equalsverifier</artifactId> + <scope>test</scope> + </dependency> + + <!-- Tests d'API REST --> + <dependency> + <groupId>io.rest-assured</groupId> + <artifactId>rest-assured</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>io.rest-assured</groupId> + <artifactId>rest-assured-all</artifactId> + <scope>test</scope> + </dependency> + + <!-- Tests avec attente minimum --> + <dependency> + <groupId>org.awaitility</groupId> + <artifactId>awaitility</artifactId> + <scope>test</scope> + </dependency> + + <!-- Base de données de tests --> + <dependency> + <groupId>io.zonky.test</groupId> + <artifactId>embedded-database-spring-test</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>io.zonky.test</groupId> + <artifactId>embedded-postgres</artifactId> + <scope>test</scope> + </dependency> + + <!-- Données de tests --> + <dependency> + <groupId>org.instancio</groupId> + <artifactId>instancio-junit</artifactId> + <scope>test</scope> + </dependency> + + <!-- Tests fonctionnels --> + <dependency> + <groupId>io.cucumber</groupId> + <artifactId>cucumber-core</artifactId> + <scope>test</scope> + </dependency> + + <dependency> + <groupId>io.cucumber</groupId> + <artifactId>cucumber-java</artifactId> + <scope>test</scope> + </dependency> + + <dependency> + <groupId>io.cucumber</groupId> + <artifactId>cucumber-spring</artifactId> + <scope>test</scope> + </dependency> + + <dependency> + <groupId>io.cucumber</groupId> + <artifactId>cucumber-junit-platform-engine</artifactId> + <scope>test</scope> + </dependency> + + <dependency> + <groupId>com.github.tomakehurst</groupId> + <artifactId>wiremock-jre8-standalone</artifactId> + <version>2.33.2</version> + <scope>test</scope> + </dependency> + </dependencies> + + <build> + <plugins> + <plugin> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-maven-plugin</artifactId> + <configuration> + <excludes> + <exclude> + <groupId>org.projectlombok</groupId> + <artifactId>lombok</artifactId> + </exclude> + </excludes> + </configuration> + </plugin> + + <plugin> + <groupId>org.openapitools</groupId> + <artifactId>openapi-generator-maven-plugin</artifactId> + <version>${openapi-generator-version}</version> + <executions> + <execution> + <id>generation_serveur</id> + <goals> + <goal>generate</goal> + </goals> + <configuration> + <inputSpec> + ${project.basedir}/src/main/resources/static/openapi.yaml + </inputSpec> + <output>${project.build.directory}/generated-sources</output> + <modelPackage>org.mte.numecoeval.expositiondonneesentrees.generated.api.model</modelPackage> + <apiPackage>org.mte.numecoeval.expositiondonneesentrees.generated.api.server</apiPackage> + <invokerPackage>org.mte.numecoeval.expositiondonneesentrees.generated.api.invoker + </invokerPackage> + <generateApis>true</generateApis> + <generateModels>true</generateModels> + <generateModelTests>false</generateModelTests> + <generateApiTests>false</generateApiTests> + <generateSupportingFiles>false</generateSupportingFiles> + <generateModelDocumentation>false</generateModelDocumentation> + <generateApiDocumentation>false</generateApiDocumentation> + <generatorName>spring</generatorName> + <library>spring-boot</library> + <configOptions> + <useTags>true</useTags> + <skipDefaultInterface>true</skipDefaultInterface> + <dateLibrary>java8</dateLibrary> + <useSpringBoot3>true</useSpringBoot3> + <sourceFolder>src/gen/java</sourceFolder> + <serializableModel>true</serializableModel> + <interfaceOnly>true</interfaceOnly> + <reactive>false</reactive> + <useBeanValidation>true</useBeanValidation> + <performBeanValidation>true</performBeanValidation> + <useOptional>false</useOptional> + <serviceInterface>true</serviceInterface> + <serviceImplementation>false</serviceImplementation> + <booleanGetterPrefix>is</booleanGetterPrefix> + <additionalModelTypeAnnotations> + @lombok.AllArgsConstructor;@lombok.Builder;@lombok.NoArgsConstructor + </additionalModelTypeAnnotations> + </configOptions> + </configuration> + </execution> + <execution> + <id>generation_client_sync_calculs</id> + <goals> + <goal>generate</goal> + </goals> + <configuration> + <inputSpec> + ${project.basedir}/../common/src/main/resources/static/api-event-calculs-sync-openapi.yaml + </inputSpec> + <output>${project.build.directory}/generated-sources</output> + <modelPackage>org.mte.numecoeval.expositiondonneesentrees.sync.calculs.generated.api.model + </modelPackage> + <apiPackage>org.mte.numecoeval.expositiondonneesentrees.sync.calculs.generated.api.client + </apiPackage> + <invokerPackage> + org.mte.numecoeval.expositiondonneesentrees.sync.calculs.generated.api.invoker + </invokerPackage> + <generateApis>true</generateApis> + <generateModels>true</generateModels> + <generateModelTests>false</generateModelTests> + <generateApiTests>false</generateApiTests> + <generateSupportingFiles>true</generateSupportingFiles> + <generateModelDocumentation>false</generateModelDocumentation> + <generateApiDocumentation>false</generateApiDocumentation> + <generatorName>java</generatorName> + <library>webclient</library> + <configOptions> + <useJakartaEe>true</useJakartaEe> + <useTags>true</useTags> + <skipDefaultInterface>true</skipDefaultInterface> + <dateLibrary>java8</dateLibrary> + <useSpringBoot3>true</useSpringBoot3> + <sourceFolder>src/gen/java</sourceFolder> + <serializableModel>true</serializableModel> + <interfaceOnly>true</interfaceOnly> + <reactive>false</reactive> + <useBeanValidation>true</useBeanValidation> + <performBeanValidation>true</performBeanValidation> + <useOptional>false</useOptional> + <serviceInterface>true</serviceInterface> + <serviceImplementation>true</serviceImplementation> + <booleanGetterPrefix>is</booleanGetterPrefix> + </configOptions> + </configuration> + </execution> + <execution> + <id>generation_client_referentiel</id> + <goals> + <goal>generate</goal> + </goals> + <configuration> + <inputSpec> + ${project.basedir}/../common/src/main/resources/static/api-referentiels-openapi.yaml + </inputSpec> + <output>${project.build.directory}/generated-sources</output> + <modelPackage>org.mte.numecoeval.expositiondonneesentrees.referentiels.generated.api.model + </modelPackage> + <apiPackage>org.mte.numecoeval.expositiondonneesentrees.referentiels.generated.api.client + </apiPackage> + <invokerPackage> + org.mte.numecoeval.expositiondonneesentrees.referentiels.generated.api.invoker + </invokerPackage> + <generateApis>true</generateApis> + <generateModels>true</generateModels> + <generateModelTests>false</generateModelTests> + <generateApiTests>false</generateApiTests> + <generateSupportingFiles>true</generateSupportingFiles> + <generateModelDocumentation>false</generateModelDocumentation> + <generateApiDocumentation>false</generateApiDocumentation> + <generatorName>java</generatorName> + <library>webclient</library> + <configOptions> + <useJakartaEe>true</useJakartaEe> + <useTags>true</useTags> + <skipDefaultInterface>true</skipDefaultInterface> + <dateLibrary>java8</dateLibrary> + <useSpringBoot3>true</useSpringBoot3> + <sourceFolder>src/gen/java</sourceFolder> + <serializableModel>true</serializableModel> + <interfaceOnly>true</interfaceOnly> + <reactive>false</reactive> + <useBeanValidation>true</useBeanValidation> + <performBeanValidation>true</performBeanValidation> + <useOptional>false</useOptional> + <serviceInterface>true</serviceInterface> + <serviceImplementation>true</serviceImplementation> + <booleanGetterPrefix>is</booleanGetterPrefix> + </configOptions> + </configuration> + </execution> + </executions> + + </plugin> + </plugins> + </build> + +</project> diff --git a/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/ExpositionDonneesEntreesApplication.java b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/ExpositionDonneesEntreesApplication.java new file mode 100644 index 00000000..5ee08d0d --- /dev/null +++ b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/ExpositionDonneesEntreesApplication.java @@ -0,0 +1,15 @@ +package org.mte.numecoeval.expositiondonneesentrees; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableScheduling; + +@SpringBootApplication +@EnableScheduling +public class ExpositionDonneesEntreesApplication { + + public static void main(String[] args) { + SpringApplication.run(ExpositionDonneesEntreesApplication.class, args); + } + +} diff --git a/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/exception/NotFoundException.java b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/exception/NotFoundException.java new file mode 100644 index 00000000..51b50b6f --- /dev/null +++ b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/exception/NotFoundException.java @@ -0,0 +1,13 @@ +package org.mte.numecoeval.expositiondonneesentrees.domain.exception; + +public class NotFoundException extends RuntimeException { + + /** + * Constructor with no arg + */ + public NotFoundException() { + super(); + } + +} + diff --git a/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/exception/ReferentielRuntimeException.java b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/exception/ReferentielRuntimeException.java new file mode 100644 index 00000000..b81b3654 --- /dev/null +++ b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/exception/ReferentielRuntimeException.java @@ -0,0 +1,21 @@ +package org.mte.numecoeval.expositiondonneesentrees.domain.exception; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ReferentielRuntimeException extends RuntimeException{ + + private final String errorType; + + public ReferentielRuntimeException(String errorType, String message ) { + super(message); + this.errorType = errorType; + + Logger logger = LoggerFactory.getLogger(ReferentielRuntimeException.class); + logger.error("{}: [ {} ]",errorType,message); + } + + public String getErrorType() { + return errorType; + } +} diff --git a/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/exception/RestException.java b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/exception/RestException.java new file mode 100644 index 00000000..eba76eb4 --- /dev/null +++ b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/exception/RestException.java @@ -0,0 +1,8 @@ +package org.mte.numecoeval.expositiondonneesentrees.domain.exception; + +public class RestException extends RuntimeException { + + public RestException(Throwable cause) { + super(cause); + } +} diff --git a/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/exception/ValidationException.java b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/exception/ValidationException.java new file mode 100644 index 00000000..555e056f --- /dev/null +++ b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/exception/ValidationException.java @@ -0,0 +1,12 @@ +package org.mte.numecoeval.expositiondonneesentrees.domain.exception; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class ValidationException extends RuntimeException { + + final String erreur; + +} diff --git a/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/model/AbstractEntree.java b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/model/AbstractEntree.java new file mode 100644 index 00000000..9ca54951 --- /dev/null +++ b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/model/AbstractEntree.java @@ -0,0 +1,53 @@ +package org.mte.numecoeval.expositiondonneesentrees.domain.model; + +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.SuperBuilder; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +@Getter +@Setter +@EqualsAndHashCode +@AllArgsConstructor +@SuperBuilder +@NoArgsConstructor +public abstract class AbstractEntree { + + protected String idLot; + protected Long id; + /** + * Nom du lot de données + */ + protected String nomLot; + /** + * Nom de l'organisation liée aux données - Metadata + */ + protected String nomOrganisation; + /** + * Nom de la source de données - Metadata + */ + protected String nomSourceDonnee; + /** + * La date du lot permet d’agréger les données provenant de différentes sources et d'en faire un suivi temporel + */ + protected LocalDate dateLot; + /** + * Date de création + */ + protected LocalDateTime dateCreation; + /** + * Date de dernière mise à jour + */ + protected LocalDateTime dateUpdate; + /** + * Statut du traitement de la donnée dans NumEcoEval + */ + protected String statutTraitement; + + +} diff --git a/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/model/Application.java b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/model/Application.java new file mode 100644 index 00000000..cc7bb1c3 --- /dev/null +++ b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/model/Application.java @@ -0,0 +1,25 @@ +package org.mte.numecoeval.expositiondonneesentrees.domain.model; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.Accessors; +import lombok.experimental.SuperBuilder; + +@Getter +@Setter +@EqualsAndHashCode(callSuper = true) +@NoArgsConstructor +@SuperBuilder +@Accessors(chain = true) +public class Application extends AbstractEntree { + String nomApplication; + String typeEnvironnement; + String nomEquipementVirtuel; + String nomSourceDonneeEquipementVirtuel; + String domaine; + String sousDomaine; + String nomEntite; + String nomEquipementPhysique; +} diff --git a/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/model/DataCenter.java b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/model/DataCenter.java new file mode 100644 index 00000000..cc5a9929 --- /dev/null +++ b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/model/DataCenter.java @@ -0,0 +1,24 @@ +package org.mte.numecoeval.expositiondonneesentrees.domain.model; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.Accessors; +import lombok.experimental.SuperBuilder; + +@Getter +@Setter +@EqualsAndHashCode(callSuper = true) +@NoArgsConstructor +@SuperBuilder +@Accessors(chain = true) +public class DataCenter extends AbstractEntree { + + String nomCourtDatacenter; + String nomLongDatacenter; + Double pue; + String localisation; + String nomEntite; + +} diff --git a/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/model/DemandeCalcul.java b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/model/DemandeCalcul.java new file mode 100644 index 00000000..97babf49 --- /dev/null +++ b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/model/DemandeCalcul.java @@ -0,0 +1,20 @@ +package org.mte.numecoeval.expositiondonneesentrees.domain.model; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.Accessors; +import lombok.experimental.SuperBuilder; + +@Getter +@Setter +@EqualsAndHashCode +@NoArgsConstructor +@SuperBuilder +@Accessors(chain = true) +public class DemandeCalcul { + String nomLot; + String dateLot; + String nomOrganisation; +} diff --git a/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/model/DonneesEntree.java b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/model/DonneesEntree.java new file mode 100644 index 00000000..b1fc6b30 --- /dev/null +++ b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/model/DonneesEntree.java @@ -0,0 +1,30 @@ +package org.mte.numecoeval.expositiondonneesentrees.domain.model; + +import lombok.*; +import lombok.experimental.Accessors; + +import java.util.ArrayList; +import java.util.List; + +@Getter +@Setter +@EqualsAndHashCode(callSuper = true) +@NoArgsConstructor +@AllArgsConstructor +@Builder +@Accessors(chain = true) +public class DonneesEntree extends AbstractEntree { + + List<DataCenter> dataCenters = new ArrayList<>(); + + List<EquipementPhysique> equipementsPhysiques = new ArrayList<>(); + + List<EquipementVirtuel> equipementsVirtuels = new ArrayList<>(); + + List<Application> applications = new ArrayList<>(); + + List<Messagerie> messageries = new ArrayList<>(); + + List<Entite> entites = new ArrayList<>(); + +} diff --git a/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/model/Entite.java b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/model/Entite.java new file mode 100644 index 00000000..899c0ad7 --- /dev/null +++ b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/model/Entite.java @@ -0,0 +1,21 @@ +package org.mte.numecoeval.expositiondonneesentrees.domain.model; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.Accessors; +import lombok.experimental.SuperBuilder; + +@Getter +@Setter +@EqualsAndHashCode(callSuper = true) +@NoArgsConstructor +@SuperBuilder +@Accessors(chain = true) +public class Entite extends AbstractEntree{ + String nomEntite; + Integer nbCollaborateurs; + String responsableEntite; + String responsableNumeriqueDurable; +} diff --git a/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/model/EquipementPhysique.java b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/model/EquipementPhysique.java new file mode 100644 index 00000000..cab21acc --- /dev/null +++ b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/model/EquipementPhysique.java @@ -0,0 +1,37 @@ +package org.mte.numecoeval.expositiondonneesentrees.domain.model; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.Accessors; +import lombok.experimental.SuperBuilder; + +import java.time.LocalDate; + +@Getter +@Setter +@EqualsAndHashCode(callSuper = true) +@NoArgsConstructor +@SuperBuilder +@Accessors(chain = true) +public class EquipementPhysique extends AbstractEntree { + String nomEquipementPhysique; + String modele; + String type; + String statut; + String paysDUtilisation; + String utilisateur; + LocalDate dateAchat; + LocalDate dateRetrait; + String nbCoeur; + String nomCourtDatacenter; + Double nbJourUtiliseAn; + Float goTelecharge; + Double consoElecAnnuelle; + boolean serveur; + String nomEntite; + Double quantite; + String modeUtilisation; + Double tauxUtilisation; +} diff --git a/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/model/EquipementVirtuel.java b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/model/EquipementVirtuel.java new file mode 100644 index 00000000..e93006d0 --- /dev/null +++ b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/model/EquipementVirtuel.java @@ -0,0 +1,27 @@ +package org.mte.numecoeval.expositiondonneesentrees.domain.model; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.Accessors; +import lombok.experimental.SuperBuilder; + +@Getter +@Setter +@EqualsAndHashCode(callSuper = true) +@NoArgsConstructor +@SuperBuilder +@Accessors(chain = true) +public class EquipementVirtuel extends AbstractEntree { + String nomEquipementVirtuel; + String nomEquipementPhysique; + String nomSourceDonneeEquipementPhysique; + Integer vCPU; + String cluster; + String nomEntite; + Double consoElecAnnuelle; + String typeEqv; + Double capaciteStockage; + Double cleRepartition; +} diff --git a/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/model/Messagerie.java b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/model/Messagerie.java new file mode 100644 index 00000000..d6389591 --- /dev/null +++ b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/model/Messagerie.java @@ -0,0 +1,23 @@ +package org.mte.numecoeval.expositiondonneesentrees.domain.model; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.Accessors; +import lombok.experimental.SuperBuilder; + +@Getter +@Setter +@EqualsAndHashCode(callSuper = true) +@NoArgsConstructor +@SuperBuilder +@Accessors(chain = true) +public class Messagerie extends AbstractEntree { + Integer nombreMailEmis; + Integer nombreMailEmisXDestinataires; + Integer volumeTotalMailEmis; + Integer moisAnnee; + String nomEntite; + +} diff --git a/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/model/RapportDemandeCalcul.java b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/model/RapportDemandeCalcul.java new file mode 100644 index 00000000..34dcdd17 --- /dev/null +++ b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/model/RapportDemandeCalcul.java @@ -0,0 +1,25 @@ +package org.mte.numecoeval.expositiondonneesentrees.domain.model; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.Accessors; +import lombok.experimental.SuperBuilder; + +@Getter +@Setter +@EqualsAndHashCode +@NoArgsConstructor +@SuperBuilder +@Accessors(chain = true) +public class RapportDemandeCalcul { + String nomLot; + String dateLot; + String nomOrganisation; + Integer nbrDataCenter; + Integer nbrEquipementPhysique; + Integer nbrEquipementVirtuel; + Integer nbrApplication; + Integer nbrMessagerie; +} diff --git a/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/model/RapportImport.java b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/model/RapportImport.java new file mode 100644 index 00000000..a35d0635 --- /dev/null +++ b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/model/RapportImport.java @@ -0,0 +1,26 @@ +package org.mte.numecoeval.expositiondonneesentrees.domain.model; + +import lombok.*; +import lombok.experimental.SuperBuilder; + +import java.util.ArrayList; +import java.util.List; + +@Getter +@Setter +@EqualsAndHashCode +@AllArgsConstructor +@SuperBuilder +@NoArgsConstructor +public class RapportImport { + + String fichier; + + String type; + + List<String> erreurs = new ArrayList<>(); + + List<String> avertissements = new ArrayList<>(); + + long nbrLignesImportees = 0; +} diff --git a/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/model/ResultatImport.java b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/model/ResultatImport.java new file mode 100644 index 00000000..ef4b0c80 --- /dev/null +++ b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/model/ResultatImport.java @@ -0,0 +1,24 @@ +package org.mte.numecoeval.expositiondonneesentrees.domain.model; + +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.SuperBuilder; + +import java.util.ArrayList; +import java.util.List; + +@Getter +@Setter +@EqualsAndHashCode +@AllArgsConstructor +@SuperBuilder +@NoArgsConstructor +public class ResultatImport { + + List<RapportImport> rapports = new ArrayList<>(); + + DonneesEntree donneesEntree = new DonneesEntree(); +} diff --git a/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/model/Volume.java b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/model/Volume.java new file mode 100644 index 00000000..a3395501 --- /dev/null +++ b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/model/Volume.java @@ -0,0 +1,4 @@ +package org.mte.numecoeval.expositiondonneesentrees.domain.model; + +public record Volume(long nbEnCours, long nbTraite) { +} diff --git a/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/ports/input/ImportDonneesEntreePort.java b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/ports/input/ImportDonneesEntreePort.java new file mode 100644 index 00000000..311996e6 --- /dev/null +++ b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/ports/input/ImportDonneesEntreePort.java @@ -0,0 +1,52 @@ +package org.mte.numecoeval.expositiondonneesentrees.domain.ports.input; + +import org.apache.commons.csv.CSVRecord; +import org.apache.commons.lang3.tuple.Pair; +import org.mte.numecoeval.expositiondonneesentrees.domain.model.*; +import org.springframework.web.multipart.MultipartFile; + +import java.time.LocalDate; +import java.util.List; + +@SuppressWarnings("java:S107") +// Obligatoire à cause de la gestion des fichiers différents au niveau contrat d'interface +public interface ImportDonneesEntreePort { + String EQUIPEMENT_PHYSIQUE_CSV_HEADER = "modele;quantite;nomEquipementPhysique;type;statut;paysDUtilisation;utilisateur;dateAchat;dateRetrait;nbCoeur;nomCourtDatacenter;goTelecharge;nbJourUtiliseAn;consoElecAnnuelle"; + String CSV_SEPARATOR = ";"; + String DATA_CENTER_CSV_HEADER = "nomCourtDatacenter;nomLongDatacenter;pue;localisation"; + + String EQUIPEMENT_VIRTUEL_CSV_HEADER = "nomEquipementPhysique;vCPU;cluster"; + + String APPLICATION_CSV_HEADER = "nomApplication;typeEnvironnement;nomEquipementPhysique;domaine;sousDomaine"; + String MESSAGERIE_CSV_HEADER = "nombreMailEmis;nombreMailEmisXDestinataires;volumeTotalMailEmis;MoisAnnee"; + String ENTITE_CSV_HEADER = "nomEntite;nbCollaborateurs;responsableEntite;responsableNumeriqueDurable"; + + ResultatImport importCsv(MultipartFile csvDataCenter, MultipartFile csvEquipementPhysique, MultipartFile csvEquipementVirtuel, + MultipartFile csvApplication, MultipartFile csvMessagerie, MultipartFile csvEntite, + String dateLot, String nomOrganisation, String nomLot); + + Pair<RapportImport, List<DataCenter>> importDataCenter(String nomLot, LocalDate dateLot, String nomOrganisation, MultipartFile csvDataCenter); + + Pair<RapportImport, List<EquipementPhysique>> importEquipementsPhysiques(String nomLot, LocalDate dateLot, String nomOrganisation, MultipartFile csvEquipementPhysique); + + Pair<RapportImport, List<EquipementVirtuel>> importEquipementsVirtuels(String nomLot, LocalDate dateLot, String nomOrganisation, MultipartFile csvEquipementVirtuel); + + Pair<RapportImport, List<Application>> importApplications(String nomLot, LocalDate dateLot, String nomOrganisation, MultipartFile csvApplication); + + Pair<RapportImport, List<Messagerie>> importMessageries(String nomLot, LocalDate dateLot, String nomOrganisation, MultipartFile csvMessagerie); + + Pair<RapportImport, List<Entite>> importEntite(String nomLot, LocalDate dateLot, String nomOrganisation, MultipartFile csvMessagerie); + + default boolean isFieldIsMappedAtLeastOnce(CSVRecord csvRecord, String mainName, String... alternativeNames) { + if (csvRecord.isMapped(mainName)) { + return true; + } + + for (String alternativeName : alternativeNames) { + if (csvRecord.isMapped(alternativeName)) { + return true; + } + } + return false; + } +} diff --git a/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/ports/input/SoumissionCalculPort.java b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/ports/input/SoumissionCalculPort.java new file mode 100644 index 00000000..79354f2c --- /dev/null +++ b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/ports/input/SoumissionCalculPort.java @@ -0,0 +1,41 @@ +package org.mte.numecoeval.expositiondonneesentrees.domain.ports.input; + +import org.mte.numecoeval.expositiondonneesentrees.domain.exception.ValidationException; +import org.mte.numecoeval.expositiondonneesentrees.domain.model.DemandeCalcul; +import org.mte.numecoeval.expositiondonneesentrees.domain.model.RapportDemandeCalcul; + +import java.util.Objects; + +public interface SoumissionCalculPort { + + /** + * Envoie la demande de calcul pour traitement. + * Met à jour le statut de tous les objets d'entrées correspondant et en attente (statut = "EN_ATTENTE") au statut "A_INGERER". + * @param demandeCalcul {@link DemandeCalcul} à traiter + * @return {@link RapportDemandeCalcul} traçant les modifications effectuées + */ + RapportDemandeCalcul soumissionCalcul(DemandeCalcul demandeCalcul); + + /** + * Mets à jour les statuts de données d'entrées pour un rejeu de calcul. + * Fait passer tous les objets d'entrées correspondant au statut "A_INGERER" indépendamment de leur statut actuel. + * @param demandeCalcul {@link DemandeCalcul} à traiter + * @return {@link RapportDemandeCalcul} traçant les modifications effectuées + */ + RapportDemandeCalcul rejeuCalcul(DemandeCalcul demandeCalcul); + + /** + * Validation de la demande de calcul + * @param demandeCalcul {@link DemandeCalcul} à traiter + * @throws ValidationException en cas d'erreur de validation de l'objet {@link DemandeCalcul} + */ + default void validate(DemandeCalcul demandeCalcul) throws ValidationException { + if(Objects.isNull(demandeCalcul)) { + throw new ValidationException("Corps de la demande obligatoire"); + } + + if(Objects.isNull(demandeCalcul.getNomLot())) { + throw new ValidationException("Nom de lot obligatoire"); + } + } +} diff --git a/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/ports/input/SoumissionCalculSyncPort.java b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/ports/input/SoumissionCalculSyncPort.java new file mode 100644 index 00000000..d80a3b20 --- /dev/null +++ b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/ports/input/SoumissionCalculSyncPort.java @@ -0,0 +1,35 @@ +package org.mte.numecoeval.expositiondonneesentrees.domain.ports.input; + +import org.mte.numecoeval.expositiondonneesentrees.domain.exception.ValidationException; +import org.mte.numecoeval.expositiondonneesentrees.domain.model.DemandeCalcul; +import org.mte.numecoeval.expositiondonneesentrees.domain.model.RapportDemandeCalcul; + +import java.util.Objects; + +public interface SoumissionCalculSyncPort { + + /** + * Envoie la demande de calcul pour traitement. + * Met à jour le statut de tous les objets d'entrées correspondant et en attente (statut = "EN_ATTENTE") au statut "A_INGERER". + * + * @param demandeCalcul {@link DemandeCalcul} à traiter + * @return {@link RapportDemandeCalcul} traçant les modifications effectuées + */ + RapportDemandeCalcul soumissionCalcul(DemandeCalcul demandeCalcul); + + /** + * Validation de la demande de calcul + * + * @param demandeCalcul {@link DemandeCalcul} à traiter + * @throws ValidationException en cas d'erreur de validation de l'objet {@link DemandeCalcul} + */ + default void validate(DemandeCalcul demandeCalcul) throws ValidationException { + if (Objects.isNull(demandeCalcul)) { + throw new ValidationException("Corps de la demande obligatoire"); + } + + if (Objects.isNull(demandeCalcul.getNomLot())) { + throw new ValidationException("Nom de lot obligatoire"); + } + } +} diff --git a/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/ports/input/StatutPourCalculPort.java b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/ports/input/StatutPourCalculPort.java new file mode 100644 index 00000000..181c022a --- /dev/null +++ b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/ports/input/StatutPourCalculPort.java @@ -0,0 +1,16 @@ +package org.mte.numecoeval.expositiondonneesentrees.domain.ports.input; + +import org.mte.numecoeval.expositiondonneesentrees.generated.api.model.StatutCalculRest; + +public interface StatutPourCalculPort { + + /** + * Génère le statut global des calculs + * + * @param nomLot nom du lot + * @param nomOrganisation nom organisation + * @return le StatutCalculRest + */ + StatutCalculRest statutDesCalculs(String nomLot, String nomOrganisation); + +} diff --git a/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/ports/input/impl/ImportDonneesEntreePortImpl.java b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/ports/input/impl/ImportDonneesEntreePortImpl.java new file mode 100644 index 00000000..9a5978c9 --- /dev/null +++ b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/ports/input/impl/ImportDonneesEntreePortImpl.java @@ -0,0 +1,510 @@ +package org.mte.numecoeval.expositiondonneesentrees.domain.ports.input.impl; + +import lombok.AllArgsConstructor; +import org.apache.commons.csv.CSVFormat; +import org.apache.commons.csv.CSVRecord; +import org.apache.commons.lang3.math.NumberUtils; +import org.apache.commons.lang3.tuple.Pair; +import org.mte.numecoeval.expositiondonneesentrees.domain.model.*; +import org.mte.numecoeval.expositiondonneesentrees.domain.ports.input.ImportDonneesEntreePort; +import org.mte.numecoeval.expositiondonneesentrees.domain.ports.output.ReferentielServicePort; +import org.mte.numecoeval.expositiondonneesentrees.generated.api.model.StatutTraitement; +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.helper.CSVHelper; +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.service.ErrorManagementService; +import org.mte.numecoeval.expositiondonneesentrees.referentiels.generated.api.model.TypeEquipementDTO; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.multipart.MultipartFile; + +import java.io.FileNotFoundException; +import java.io.InputStreamReader; +import java.io.Reader; +import java.time.LocalDate; +import java.util.*; + +@AllArgsConstructor +public class ImportDonneesEntreePortImpl implements ImportDonneesEntreePort { + + private static final String LABEL_NOM_EQUIPEMENT_PHYSIQUE = "nomEquipementPhysique"; + private static final String LABEL_NOM_VM = "nomVM"; + private static final String LABEL_NOM_EQUIPEMENT_VIRTUEL = "nomEquipementVirtuel"; + + private static final String[] EQUIPEMENT_PHYSIQUE_HEADER = EQUIPEMENT_PHYSIQUE_CSV_HEADER.split(CSV_SEPARATOR); + + private static final String[] DATA_CENTER_HEADER = DATA_CENTER_CSV_HEADER.split(CSV_SEPARATOR); + + private static final String[] EQUIPEMENT_VIRTUEL_HEADER = EQUIPEMENT_VIRTUEL_CSV_HEADER.split(CSV_SEPARATOR); + + private static final String[] APPLICATION_HEADER = APPLICATION_CSV_HEADER.split(CSV_SEPARATOR); + private static final String[] MESSAGERIE_HEADER = MESSAGERIE_CSV_HEADER.split(CSV_SEPARATOR); + private static final String[] ENTITE_HEADER = ENTITE_CSV_HEADER.split(CSV_SEPARATOR); + private static final String HEADER_NOM_ENTITE = "nomEntite"; + private static final String HEADER_NOM_SOURCE_DONNEE = "nomSourceDonnee"; + private static final String LIGNE_INCONSISTENTE = "LIGNE_INCONSISTENTE"; + + + private static final Logger LOGGER = LoggerFactory.getLogger(ImportDonneesEntreePortImpl.class); + private static final String STATUT_TRAITEMENT_EN_ATTENTE = StatutTraitement.EN_ATTENTE.getValue(); + + final ReferentielServicePort referentielServicePort; + + final ErrorManagementService errorManagementService; + + final Map<String, String> errorMessages; + + @Override + public ResultatImport importCsv(MultipartFile csvDataCenter, + MultipartFile csvEquipementPhysique, + MultipartFile csvEquipementVirtuel, + MultipartFile csvApplication, + MultipartFile csvMessagerie, + MultipartFile csvEntite, + String dateLot, String nomOrganisation, String nomLot) { + + LocalDate dateLotAsDate = dateLot == null || "".equals(dateLot) ? null : LocalDate.parse(dateLot); + ResultatImport resultatImport = new ResultatImport(); + resultatImport.getDonneesEntree().setStatutTraitement(STATUT_TRAITEMENT_EN_ATTENTE); + resultatImport.getDonneesEntree().setDateLot(dateLotAsDate); + resultatImport.getDonneesEntree().setNomOrganisation(nomOrganisation); + resultatImport.getDonneesEntree().setNomLot(nomLot); + + if (csvDataCenter != null) { + Pair<RapportImport, List<DataCenter>> importDataCenter = importDataCenter(nomLot, dateLotAsDate, nomOrganisation, csvDataCenter); + resultatImport.getRapports().add(importDataCenter.getKey()); + resultatImport.getDonneesEntree().setDataCenters(importDataCenter.getValue()); + } + + if (csvEquipementPhysique != null) { + Pair<RapportImport, List<EquipementPhysique>> importEquipementsPhysiques = importEquipementsPhysiques(nomLot, dateLotAsDate, nomOrganisation, csvEquipementPhysique); + resultatImport.getRapports().add(importEquipementsPhysiques.getKey()); + resultatImport.getDonneesEntree().setEquipementsPhysiques(importEquipementsPhysiques.getValue()); + } + + if (csvEquipementVirtuel != null) { + Pair<RapportImport, List<EquipementVirtuel>> importEquipementVirtuel = importEquipementsVirtuels(nomLot, dateLotAsDate, nomOrganisation, csvEquipementVirtuel); + resultatImport.getRapports().add(importEquipementVirtuel.getKey()); + resultatImport.getDonneesEntree().getEquipementsVirtuels().addAll(importEquipementVirtuel.getValue()); + } + + if (csvApplication != null) { + Pair<RapportImport, List<Application>> importApplications = importApplications(nomLot, dateLotAsDate, nomOrganisation, csvApplication); + resultatImport.getRapports().add(importApplications.getKey()); + resultatImport.getDonneesEntree().getApplications().addAll(importApplications.getValue()); + } + + if (csvMessagerie != null) { + Pair<RapportImport, List<Messagerie>> importMessagerie = importMessageries(nomLot, dateLotAsDate, nomOrganisation, csvMessagerie); + resultatImport.getRapports().add(importMessagerie.getKey()); + resultatImport.getDonneesEntree().setMessageries(importMessagerie.getValue()); + } + + if (csvEntite != null) { + Pair<RapportImport, List<Entite>> importEntites = importEntite(nomLot, dateLotAsDate, nomOrganisation, csvEntite); + resultatImport.getRapports().add(importEntites.getKey()); + resultatImport.getDonneesEntree().setEntites(importEntites.getValue()); + } + return resultatImport; + } + + @Override + public Pair<RapportImport, List<DataCenter>> importDataCenter(String nomLot, LocalDate dateLot, String nomOrganisation, MultipartFile csvDataCenter) { + List<DataCenter> domainModels = new ArrayList<>(); + RapportImport rapportImport = new RapportImport(); + rapportImport.setFichier(csvDataCenter.getOriginalFilename()); + rapportImport.setType("datacenter"); + + try (Reader reader = new InputStreamReader(csvDataCenter.getInputStream())) { + Iterable<CSVRecord> records = CSVFormat.DEFAULT.builder() + .setHeader() + .setDelimiter(CSV_SEPARATOR) + .setTrim(true) + .setAllowMissingColumnNames(true) + .setSkipHeaderRecord(true) + .build().parse(reader); + records.forEach(csvRecord -> { + if (!Arrays.stream(DATA_CENTER_HEADER).allMatch(csvRecord::isMapped)) { + rapportImport.getErreurs().add(errorMessages.get(LIGNE_INCONSISTENTE).formatted(csvDataCenter.getOriginalFilename(), csvRecord.getRecordNumber() + 1)); + } else { + String localisation = CSVHelper.safeString(csvRecord, "localisation"); + String nomLongDataCenter = CSVHelper.safeString(csvRecord, "nomLongDatacenter"); + var dataCenter = DataCenter.builder() + .statutTraitement(STATUT_TRAITEMENT_EN_ATTENTE) + .nomLot(nomLot) + .dateLot(dateLot) + .nomOrganisation(nomOrganisation) + .nomCourtDatacenter(CSVHelper.safeString(csvRecord, "nomCourtDatacenter")) + .nomLongDatacenter(nomLongDataCenter) + .pue(CSVHelper.safeDouble(csvRecord, "pue")) + .localisation(localisation) + .nomEntite(CSVHelper.safeString(csvRecord, HEADER_NOM_ENTITE)) + .nomSourceDonnee(CSVHelper.safeString(csvRecord, HEADER_NOM_SOURCE_DONNEE)) + .build(); + + domainModels.add(dataCenter); + rapportImport.getErreurs().addAll(errorManagementService.checkDataCenter(dataCenter)); + + } + }); + + } catch (FileNotFoundException e) { + LOGGER.error("Erreur CSV pour des DataCenters introuvable", e); + return Pair.of(RapportImport.builder() + .erreurs(Collections.singletonList("Le fichier CSV des DataCenters n'est pas trouvable.")) + .build() + , Collections.emptyList() + ); + } catch (Exception e) { + LOGGER.error("Erreur durant la lecture d'un CSV pour des DataCenters", e); + + return Pair.of(RapportImport.builder() + .erreurs(Collections.singletonList("Le fichier CSV des DataCenter n'est pas lisible par le système.")) + .build() + , Collections.emptyList() + ); + } + + rapportImport.setNbrLignesImportees(domainModels.size()); + + return Pair.of(rapportImport, domainModels); + } + + @Override + public Pair<RapportImport, List<EquipementPhysique>> importEquipementsPhysiques(String nomLot, LocalDate dateLot, String nomOrganisation, MultipartFile csvEquipementPhysique) { + List<EquipementPhysique> domainModels = new ArrayList<>(); + RapportImport rapportImport = new RapportImport(); + rapportImport.setFichier(csvEquipementPhysique.getOriginalFilename()); + rapportImport.setType("equipement_physique"); + List<TypeEquipementDTO> typesEquipement = referentielServicePort.getAllTypesEquipement(); + Set<String> avertissements = new HashSet<>(); + + try (Reader reader = new InputStreamReader(csvEquipementPhysique.getInputStream())) { + Iterable<CSVRecord> records = CSVFormat.DEFAULT.builder() + .setHeader() + .setAllowMissingColumnNames(true) + .setDelimiter(CSV_SEPARATOR) + .setTrim(true) + .setSkipHeaderRecord(true) + .build().parse(reader); + records.forEach(csvRecord -> + { + String typeEquipement = csvRecord.get("type"); + var refTypeEquipementOpt = typesEquipement.stream() + .filter(refType -> refType.getType().equals(typeEquipement)) + .findFirst(); + + if (!Arrays.stream(EQUIPEMENT_PHYSIQUE_HEADER).allMatch(csvRecord::isMapped)) { + rapportImport.getErreurs().add(errorMessages.get(LIGNE_INCONSISTENTE).formatted(csvEquipementPhysique.getOriginalFilename(), csvRecord.getRecordNumber() + 1)); + } else if (refTypeEquipementOpt.isEmpty()) { + // CA 1.1 + rapportImport.getErreurs().add(errorMessages.get("TYPE_EQUIPEMENT_INCONNU").formatted(csvRecord.get("type"), CSVHelper.safeString(csvRecord, LABEL_NOM_EQUIPEMENT_PHYSIQUE, "equipement"))); + } else { + var goTelechargeStr = CSVHelper.safeString(csvRecord, "goTelecharge"); + Float goTelecharge = goTelechargeStr == null ? null : NumberUtils.toFloat(goTelechargeStr); + var tauxUtilisationStr = CSVHelper.safeString(csvRecord, "tauxUtilisation"); + Double tauxUtilisation = tauxUtilisationStr == null ? null : NumberUtils.toDouble(tauxUtilisationStr); + + + EquipementPhysique equipementToAdd = EquipementPhysique.builder() + .statutTraitement(STATUT_TRAITEMENT_EN_ATTENTE) + .nomLot(nomLot) + .dateLot(dateLot) + .nomOrganisation(nomOrganisation) + .modele(CSVHelper.safeString(csvRecord, "modele", "refEquipement")) + .type(csvRecord.get("type")) + .quantite(CSVHelper.safeDouble(csvRecord, "quantite")) + .nomEquipementPhysique(CSVHelper.safeString(csvRecord, LABEL_NOM_EQUIPEMENT_PHYSIQUE, "equipement")) + .statut(CSVHelper.safeString(csvRecord, "statut")) + .paysDUtilisation(CSVHelper.safeString(csvRecord, "paysDUtilisation")) + .utilisateur(CSVHelper.safeString(csvRecord, "utilisateur")) + .dateAchat(CSVHelper.safeParseLocalDate(csvRecord, "dateAchat")) + .dateRetrait(CSVHelper.safeParseLocalDate(csvRecord, "dateRetrait")) + .nbCoeur(CSVHelper.safeString(csvRecord, "nbCoeur")) + .nomCourtDatacenter(CSVHelper.safeString(csvRecord, "nomCourtDatacenter", "refDatacenter")) + .goTelecharge(goTelecharge) + .modeUtilisation(CSVHelper.safeString(csvRecord, "modeUtilisation")) + .tauxUtilisation(tauxUtilisation) + .nbJourUtiliseAn(CSVHelper.safeDouble(csvRecord, "nbJourUtiliseAn")) + .consoElecAnnuelle(CSVHelper.safeDouble(csvRecord, "consoElecAnnuelle")) + .serveur(refTypeEquipementOpt.get().isServeur()) + .nomEntite(CSVHelper.safeString(csvRecord, HEADER_NOM_ENTITE)) + .nomSourceDonnee(CSVHelper.safeString(csvRecord, HEADER_NOM_SOURCE_DONNEE)) + .build(); + + var erreurs = errorManagementService.checkEquipementPhysique(equipementToAdd, refTypeEquipementOpt.get().getRefEquipementParDefaut()); + rapportImport.getErreurs().addAll(erreurs.getKey()); + avertissements.addAll(erreurs.getValue()); + + domainModels.add(equipementToAdd); + } + }); + + } catch (FileNotFoundException e) { + LOGGER.error("Erreur CSV pour des équipements physiques introuvable", e); + return Pair.of(RapportImport.builder() + .erreurs(Collections.singletonList("Le fichier CSV des équipements physiques n'est pas trouvable.")) + .build() + , Collections.emptyList() + ); + } catch (Exception e) { + LOGGER.error("Erreur durant la lecture d'un CSV pour des équipements physiques", e); + + return Pair.of(RapportImport.builder() + .erreurs(Collections.singletonList("Le fichier CSV des équipements physiques n'est pas lisible par le système.")) + .build() + , Collections.emptyList() + ); + } + + rapportImport.setNbrLignesImportees(domainModels.size()); + rapportImport.setAvertissements(avertissements.stream().toList()); + + return Pair.of(rapportImport, domainModels); + } + + @Override + public Pair<RapportImport, List<EquipementVirtuel>> importEquipementsVirtuels(String nomLot, LocalDate dateLot, String nomOrganisation, MultipartFile csvEquipementVirtuel) { + List<EquipementVirtuel> domainModels = new ArrayList<>(); + RapportImport rapportImport = new RapportImport(); + rapportImport.setFichier(csvEquipementVirtuel.getOriginalFilename()); + rapportImport.setType("equipement_virtuel"); + + try (Reader reader = new InputStreamReader(csvEquipementVirtuel.getInputStream())) { + Iterable<CSVRecord> records = CSVFormat.DEFAULT.builder() + .setHeader() + .setAllowMissingColumnNames(true) + .setDelimiter(CSV_SEPARATOR) + .setTrim(true) + .setSkipHeaderRecord(true) + .build().parse(reader); + records.forEach(csvRecord -> + { + boolean isRecordOK = isFieldIsMappedAtLeastOnce(csvRecord, LABEL_NOM_EQUIPEMENT_VIRTUEL, LABEL_NOM_VM) + && Arrays.stream(EQUIPEMENT_VIRTUEL_HEADER).allMatch(csvRecord::isMapped); + if (!isRecordOK) { + rapportImport.getErreurs().add(errorMessages.get(LIGNE_INCONSISTENTE).formatted(csvEquipementVirtuel.getOriginalFilename(), csvRecord.getRecordNumber() + 1)); + } else { + EquipementVirtuel equipementVirtuel = EquipementVirtuel.builder() + .statutTraitement(STATUT_TRAITEMENT_EN_ATTENTE) + .nomLot(nomLot) + .dateLot(dateLot) + .nomOrganisation(nomOrganisation) + .nomEquipementVirtuel(CSVHelper.safeString(csvRecord, LABEL_NOM_EQUIPEMENT_VIRTUEL, LABEL_NOM_VM)) + .nomEquipementPhysique(CSVHelper.safeString(csvRecord, LABEL_NOM_EQUIPEMENT_PHYSIQUE)) + .nomSourceDonneeEquipementPhysique(CSVHelper.safeString(csvRecord, "nomSourceDonneeEquipementPhysique")) + .vCPU(CSVHelper.safeInteger(csvRecord, "vCPU")) + .cluster(CSVHelper.safeString(csvRecord, "cluster")) + .consoElecAnnuelle(CSVHelper.safeDouble(csvRecord, "consoElecAnnuelle")) + .typeEqv(CSVHelper.safeString(csvRecord, "typeEqv")) + .capaciteStockage(CSVHelper.safeDouble(csvRecord, "capaciteStockage")) + .cleRepartition(CSVHelper.safeDouble(csvRecord, "cleRepartition")) + .nomEntite(CSVHelper.safeString(csvRecord, HEADER_NOM_ENTITE)) + .nomSourceDonnee(CSVHelper.safeString(csvRecord, HEADER_NOM_SOURCE_DONNEE)) + .build(); + + domainModels.add(equipementVirtuel); + } + }); + + } catch (FileNotFoundException e) { + LOGGER.error("Erreur CSV pour des équipements virtuels introuvable", e); + return Pair.of(RapportImport.builder() + .erreurs(Collections.singletonList("Le fichier CSV des équipements virtuels n'est pas trouvable.")) + .build() + , Collections.emptyList() + ); + } catch (Exception e) { + LOGGER.error("Erreur durant la lecture d'un CSV pour des équipements virtuels", e); + + return Pair.of(RapportImport.builder() + .erreurs(Collections.singletonList("Le fichier CSV des équipements virtuels n'est pas lisible par le système.")) + .build() + , Collections.emptyList() + ); + } + + rapportImport.setNbrLignesImportees(domainModels.size()); + + return Pair.of(rapportImport, domainModels); + + } + + @Override + public Pair<RapportImport, List<Application>> importApplications(String nomLot, LocalDate dateLot, String nomOrganisation, MultipartFile csvApplication) { + List<Application> domainModels = new ArrayList<>(); + RapportImport rapportImport = new RapportImport(); + rapportImport.setFichier(csvApplication.getOriginalFilename()); + rapportImport.setType("application"); + + try (Reader reader = new InputStreamReader(csvApplication.getInputStream())) { + Iterable<CSVRecord> records = CSVFormat.DEFAULT.builder() + .setHeader() + .setAllowMissingColumnNames(true) + .setDelimiter(CSV_SEPARATOR) + .setTrim(true) + .setSkipHeaderRecord(true) + .build().parse(reader); + records.forEach(csvRecord -> + { + boolean isRecordOK = isFieldIsMappedAtLeastOnce(csvRecord, LABEL_NOM_EQUIPEMENT_VIRTUEL, LABEL_NOM_VM) + && Arrays.stream(APPLICATION_HEADER).allMatch(csvRecord::isMapped); + if (!isRecordOK) { + rapportImport.getErreurs().add(errorMessages.get(LIGNE_INCONSISTENTE).formatted(csvApplication.getOriginalFilename(), csvRecord.getRecordNumber() + 1)); + } else { + Application application = Application.builder() + .statutTraitement(STATUT_TRAITEMENT_EN_ATTENTE) + .nomLot(nomLot) + .dateLot(dateLot) + .nomOrganisation(nomOrganisation) + .nomApplication(CSVHelper.safeString(csvRecord, "nomApplication")) + .typeEnvironnement(CSVHelper.safeString(csvRecord, "typeEnvironnement")) + .nomEquipementVirtuel(CSVHelper.safeString(csvRecord, LABEL_NOM_EQUIPEMENT_VIRTUEL, LABEL_NOM_VM)) + .nomSourceDonneeEquipementVirtuel(CSVHelper.safeString(csvRecord, "nomSourceDonneeEquipementVirtuel")) + .domaine(CSVHelper.safeString(csvRecord, "domaine")) + .sousDomaine(CSVHelper.safeString(csvRecord, "sousDomaine")) + .nomEntite(CSVHelper.safeString(csvRecord, HEADER_NOM_ENTITE)) + .nomEquipementPhysique(CSVHelper.safeString(csvRecord, LABEL_NOM_EQUIPEMENT_PHYSIQUE)) + .nomSourceDonnee(CSVHelper.safeString(csvRecord, HEADER_NOM_SOURCE_DONNEE)) + .build(); + + domainModels.add(application); + } + }); + + } catch (FileNotFoundException e) { + LOGGER.error("Erreur CSV pour des applications introuvable", e); + return Pair.of(RapportImport.builder() + .erreurs(Collections.singletonList("Le fichier CSV des Applications n'est pas trouvable.")) + .build() + , Collections.emptyList() + ); + } catch (Exception e) { + LOGGER.error("Erreur durant la lecture d'un CSV pour des Applications", e); + + return Pair.of(RapportImport.builder() + .erreurs(Collections.singletonList("Le fichier CSV des Applications n'est pas lisible par le système.")) + .build() + , Collections.emptyList() + ); + } + + rapportImport.setNbrLignesImportees(domainModels.size()); + + return Pair.of(rapportImport, domainModels); + } + + @Override + public Pair<RapportImport, List<Messagerie>> importMessageries(String nomLot, LocalDate dateLot, String nomOrganisation, MultipartFile csvMessagerie) { + List<Messagerie> domainModels = new ArrayList<>(); + RapportImport rapportImport = new RapportImport(); + rapportImport.setFichier(csvMessagerie.getOriginalFilename()); + rapportImport.setType("messagerie"); + + try (Reader reader = new InputStreamReader(csvMessagerie.getInputStream())) { + Iterable<CSVRecord> records = CSVFormat.DEFAULT.builder() + .setHeader() + .setAllowMissingColumnNames(true) + .setDelimiter(CSV_SEPARATOR) + .setTrim(true) + .setSkipHeaderRecord(true) + .build().parse(reader); + records.forEach(csvRecord -> + { + if (!Arrays.stream(MESSAGERIE_HEADER).allMatch(csvRecord::isMapped)) { + rapportImport.getErreurs().add(errorMessages.get(LIGNE_INCONSISTENTE).formatted(csvMessagerie.getOriginalFilename(), csvRecord.getRecordNumber() + 1)); + } else { + domainModels.add( + Messagerie.builder() + .statutTraitement(STATUT_TRAITEMENT_EN_ATTENTE) + .nomLot(nomLot) + .dateLot(dateLot) + .nomOrganisation(nomOrganisation) + .nombreMailEmis(CSVHelper.safeInteger(csvRecord, "nombreMailEmis")) + .nombreMailEmisXDestinataires(CSVHelper.safeInteger(csvRecord, "nombreMailEmisXDestinataires")) + .volumeTotalMailEmis(CSVHelper.safeInteger(csvRecord, "volumeTotalMailEmis")) + .moisAnnee(CSVHelper.safeInteger(csvRecord, "MoisAnnee")) + .nomEntite(CSVHelper.safeString(csvRecord, HEADER_NOM_ENTITE)) + .nomSourceDonnee(CSVHelper.safeString(csvRecord, HEADER_NOM_SOURCE_DONNEE)) + .build() + ); + } + }); + + } catch (FileNotFoundException e) { + LOGGER.error("Erreur CSV pour de la messagerie introuvable", e); + return Pair.of(RapportImport.builder() + .erreurs(Collections.singletonList("Le fichier CSV de la messagerie n'est pas trouvable.")) + .build() + , Collections.emptyList() + ); + } catch (Exception e) { + LOGGER.error("Erreur durant la lecture d'un CSV pour de la messagerie", e); + + return Pair.of(RapportImport.builder() + .erreurs(Collections.singletonList("Le fichier CSV de la messagerie n'est pas lisible par le système.")) + .build() + , Collections.emptyList() + ); + } + + rapportImport.setNbrLignesImportees(domainModels.size()); + + return Pair.of(rapportImport, domainModels); + } + + @Override + public Pair<RapportImport, List<Entite>> importEntite(String nomLot, LocalDate dateLot, String nomOrganisation, MultipartFile csvEntite) { + List<Entite> domainModels = new ArrayList<>(); + RapportImport rapportImport = new RapportImport(); + rapportImport.setFichier(csvEntite.getOriginalFilename()); + rapportImport.setType("entite"); + + try (Reader reader = new InputStreamReader(csvEntite.getInputStream())) { + Iterable<CSVRecord> records = CSVFormat.DEFAULT.builder() + .setHeader() + .setAllowMissingColumnNames(true) + .setDelimiter(CSV_SEPARATOR) + .setTrim(true) + .setSkipHeaderRecord(true) + .build().parse(reader); + records.forEach(csvRecord -> + { + if (!Arrays.stream(ENTITE_HEADER).allMatch(csvRecord::isMapped)) { + rapportImport.getErreurs().add(errorMessages.get(LIGNE_INCONSISTENTE).formatted(csvEntite.getOriginalFilename(), csvRecord.getRecordNumber() + 1)); + } else { + domainModels.add( + Entite.builder() + .nomLot(nomLot) + .dateLot(dateLot) + .nomOrganisation(nomOrganisation) + .nomEntite(CSVHelper.safeString(csvRecord, HEADER_NOM_ENTITE)) + .responsableEntite(CSVHelper.safeString(csvRecord, "responsableEntite")) + .responsableNumeriqueDurable(CSVHelper.safeString(csvRecord, "responsableNumeriqueDurable")) + .nbCollaborateurs(CSVHelper.safeInteger(csvRecord, "nbCollaborateurs")) + .nomSourceDonnee(CSVHelper.safeString(csvRecord, HEADER_NOM_SOURCE_DONNEE)) + .build() + ); + } + }); + + } catch (FileNotFoundException e) { + LOGGER.error("Erreur CSV pour des entités introuvable", e); + return Pair.of(RapportImport.builder() + .erreurs(Collections.singletonList("Le fichier CSV des entités n'est pas trouvable.")) + .build() + , Collections.emptyList() + ); + } catch (Exception e) { + LOGGER.error("Erreur durant la lecture d'un CSV pour les entités", e); + + return Pair.of(RapportImport.builder() + .erreurs(Collections.singletonList("Le fichier CSV des entités n'est pas lisible par le système.")) + .build() + , Collections.emptyList() + ); + } + + rapportImport.setNbrLignesImportees(domainModels.size()); + + return Pair.of(rapportImport, domainModels); + } +} diff --git a/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/ports/input/impl/SoumissionCalculSyncPortImpl.java b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/ports/input/impl/SoumissionCalculSyncPortImpl.java new file mode 100644 index 00000000..c0f73260 --- /dev/null +++ b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/ports/input/impl/SoumissionCalculSyncPortImpl.java @@ -0,0 +1,52 @@ +package org.mte.numecoeval.expositiondonneesentrees.domain.ports.input.impl; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.mte.numecoeval.expositiondonneesentrees.domain.model.DemandeCalcul; +import org.mte.numecoeval.expositiondonneesentrees.domain.model.RapportDemandeCalcul; +import org.mte.numecoeval.expositiondonneesentrees.domain.ports.input.SoumissionCalculSyncPort; +import org.mte.numecoeval.expositiondonneesentrees.generated.api.model.StatutTraitement; +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.adapters.CalculsRestClient; +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.jpa.repository.DataCenterRepository; +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.jpa.repository.EquipementPhysiqueRepository; +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.jpa.repository.MessagerieRepository; +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.mapper.CalculRestMapper; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@Slf4j +@AllArgsConstructor +public class SoumissionCalculSyncPortImpl implements SoumissionCalculSyncPort { + + EquipementPhysiqueRepository equipementPhysiqueRepository; + MessagerieRepository messagerieRepository; + + DataCenterRepository dataCenterRepository; + CalculsRestClient calculsRestClient; + + CalculRestMapper calculRestMapper; + + @Override + public RapportDemandeCalcul soumissionCalcul(DemandeCalcul demandeCalcul) { + validate(demandeCalcul); + + // find equipements physiques à partir de nomLot et nomOrganisation + List<Long> equipementPhysiqueIds = equipementPhysiqueRepository.getIdsByNomLotAndStatutTraitement(demandeCalcul.getNomLot(), StatutTraitement.EN_ATTENTE.getValue()); + List<Long> messagerieEntityIds = messagerieRepository.getIdsByNomLotAndStatutTraitement(demandeCalcul.getNomLot(), StatutTraitement.EN_ATTENTE.getValue()); + + // map to Rest + var reponseCalculRest = calculsRestClient.postSyncCalcul( + equipementPhysiqueIds, + messagerieEntityIds + ); + + var rapport = calculRestMapper.toRest(reponseCalculRest); + rapport.setNomLot(demandeCalcul.getNomLot()); + rapport.setDateLot(demandeCalcul.getDateLot()); + rapport.setNomOrganisation(demandeCalcul.getNomOrganisation()); + return rapport; + } + +} diff --git a/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/ports/input/impl/StatutPourCalculPortImpl.java b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/ports/input/impl/StatutPourCalculPortImpl.java new file mode 100644 index 00000000..6832764f --- /dev/null +++ b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/ports/input/impl/StatutPourCalculPortImpl.java @@ -0,0 +1,61 @@ +package org.mte.numecoeval.expositiondonneesentrees.domain.ports.input.impl; + +import lombok.extern.slf4j.Slf4j; +import org.mte.numecoeval.expositiondonneesentrees.domain.exception.NotFoundException; +import org.mte.numecoeval.expositiondonneesentrees.domain.model.Volume; +import org.mte.numecoeval.expositiondonneesentrees.domain.ports.input.StatutPourCalculPort; +import org.mte.numecoeval.expositiondonneesentrees.generated.api.model.StatutCalculRest; +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.helper.Constants; +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.jdbc.VolumeJdbc; +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.mapper.VolumeMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; + + +@Service +@Slf4j +public class StatutPourCalculPortImpl implements StatutPourCalculPort { + + @Autowired + private VolumeJdbc volumeJdbc; + + @Autowired + private VolumeMapper volumeMapper; + + @Cacheable("statutDesCalculs") + @Override + public StatutCalculRest statutDesCalculs(String nomLot, String nomOrganisation) { + + var volumeEqPh = volumeJdbc.getVolumeBy(nomLot, nomOrganisation, Constants.TABLE_EQUIPEMENT_PHYSIQUE); + var volumeMessagerie = volumeJdbc.getVolumeBy(nomLot, nomOrganisation, Constants.TABLE_MESSAGERIE); + return statutCalculs(volumeEqPh, volumeMessagerie); + } + + /** + * Statut des calculs en cours pour des volumes recuperes de la bdd + * + * @param volumeEqPh volume des equipements physiques + * @param volumeMessagerie volume de la messagerie + * @return le StatutCalculRest + */ + public StatutCalculRest statutCalculs(Volume volumeEqPh, Volume volumeMessagerie) { + var totalTraite = volumeEqPh.nbTraite() + volumeMessagerie.nbTraite(); + var totalEnCours = volumeEqPh.nbEnCours() + volumeMessagerie.nbEnCours(); + + if (totalTraite + totalEnCours == 0) { + throw new NotFoundException(); + } + + var etat = totalTraite * 100 / (totalTraite + totalEnCours); + var statut = StatutCalculRest.StatutEnum.EN_COURS; + if (etat == 100) statut = StatutCalculRest.StatutEnum.TERMINE; + + var result = new StatutCalculRest(); + result.setStatut(statut); + result.setEtat(etat + "%"); + result.setEquipementPhysique(volumeMapper.toRest(volumeEqPh)); + result.setMessagerie(volumeMapper.toRest(volumeMessagerie)); + return result; + } +} diff --git a/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/ports/output/CalculsServicePort.java b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/ports/output/CalculsServicePort.java new file mode 100644 index 00000000..3e0a9714 --- /dev/null +++ b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/ports/output/CalculsServicePort.java @@ -0,0 +1,7 @@ +package org.mte.numecoeval.expositiondonneesentrees.domain.ports.output; + +import java.util.List; + +public interface CalculsServicePort { + void postSyncCalcul(List<Long> equipementPhysiqueIds, List<Long> messagerieIds); +} diff --git a/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/ports/output/EntreePersistencePort.java b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/ports/output/EntreePersistencePort.java new file mode 100644 index 00000000..2429a96f --- /dev/null +++ b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/ports/output/EntreePersistencePort.java @@ -0,0 +1,16 @@ +package org.mte.numecoeval.expositiondonneesentrees.domain.ports.output; + +import org.mte.numecoeval.expositiondonneesentrees.domain.model.AbstractEntree; + +import java.util.List; +import java.util.Set; + +public interface EntreePersistencePort<T extends AbstractEntree> { + void save(T entree); + + void saveAll(List<T> entrees); + + default int updateStatutInList(String statut, String nomLot, String nomOrganisation, Set<String> noms) { + return 0; + } +} diff --git a/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/ports/output/ReferentielServicePort.java b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/ports/output/ReferentielServicePort.java new file mode 100644 index 00000000..7fc8e1aa --- /dev/null +++ b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/ports/output/ReferentielServicePort.java @@ -0,0 +1,20 @@ +package org.mte.numecoeval.expositiondonneesentrees.domain.ports.output; + +import org.mte.numecoeval.expositiondonneesentrees.referentiels.generated.api.model.*; + +import java.util.List; + +public interface ReferentielServicePort { + boolean hasMixElec(String pays); + + List<TypeEquipementDTO> getAllTypesEquipement(); + + CorrespondanceRefEquipementDTO getCorrespondance(String modele); + + ImpactEquipementDTO getImpactEquipement(String refEquipement, String critere, String etape); + + List<EtapeDTO> getAllEtapes(); + + List<CritereDTO> getAllCriteres(); + +} diff --git a/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/ports/output/SaveDonneesEntreePort.java b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/ports/output/SaveDonneesEntreePort.java new file mode 100644 index 00000000..7624aba2 --- /dev/null +++ b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/domain/ports/output/SaveDonneesEntreePort.java @@ -0,0 +1,14 @@ +package org.mte.numecoeval.expositiondonneesentrees.domain.ports.output; + +import org.mte.numecoeval.expositiondonneesentrees.domain.model.DonneesEntree; + +public interface SaveDonneesEntreePort { + + /** + * Sauvegarde de toutes les données d'entrées + * + * @param donneesEntree {@link DonneesEntree} données à traiter + */ + void save(DonneesEntree donneesEntree); + +} diff --git a/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/adapters/CalculsRestClient.java b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/adapters/CalculsRestClient.java new file mode 100644 index 00000000..c1da6ce1 --- /dev/null +++ b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/adapters/CalculsRestClient.java @@ -0,0 +1,36 @@ +package org.mte.numecoeval.expositiondonneesentrees.infrastructure.adapters; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.mte.numecoeval.expositiondonneesentrees.domain.exception.RestException; +import org.mte.numecoeval.expositiondonneesentrees.sync.calculs.generated.api.client.CalculsEquipementPhysiqueEtMessagerieApi; +import org.mte.numecoeval.expositiondonneesentrees.sync.calculs.generated.api.model.ReponseCalculRest; +import org.mte.numecoeval.expositiondonneesentrees.sync.calculs.generated.api.model.SyncCalculRest; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClientResponseException; + +import java.util.List; + +@Service +@Slf4j +@AllArgsConstructor +public class CalculsRestClient { + + private CalculsEquipementPhysiqueEtMessagerieApi calculsEquipementPhysiqueEtMessagerieApi; + + public ReponseCalculRest postSyncCalcul(List<Long> equipementPhysiqueIds, List<Long> messagerieIds) { + try { + + var syncCalculRest = new SyncCalculRest(); + syncCalculRest.setEquipementPhysiqueIds(equipementPhysiqueIds); + syncCalculRest.setMessagerieIds(messagerieIds); + + return calculsEquipementPhysiqueEtMessagerieApi.syncCalculByIds(syncCalculRest).block(); + + } catch (WebClientResponseException e) { + throw new RestException(e); + } + + } + +} diff --git a/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/adapters/ReferentielRestClient.java b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/adapters/ReferentielRestClient.java new file mode 100644 index 00000000..b101173d --- /dev/null +++ b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/adapters/ReferentielRestClient.java @@ -0,0 +1,86 @@ +package org.mte.numecoeval.expositiondonneesentrees.infrastructure.adapters; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.mte.numecoeval.expositiondonneesentrees.domain.exception.RestException; +import org.mte.numecoeval.expositiondonneesentrees.domain.ports.output.ReferentielServicePort; +import org.mte.numecoeval.expositiondonneesentrees.referentiels.generated.api.client.InterneNumEcoEvalApi; +import org.mte.numecoeval.expositiondonneesentrees.referentiels.generated.api.model.*; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClientResponseException; + +import java.util.List; + +@Service +@Slf4j +@AllArgsConstructor +public class ReferentielRestClient implements ReferentielServicePort { + + private InterneNumEcoEvalApi interneNumEcoEvalApi; + + @Cacheable("hasMixElec") + @Override + public boolean hasMixElec(String pays) { + + boolean result = false; + try { + var res = interneNumEcoEvalApi.getMixElectriqueParPaysWithHttpInfo(pays).block(); + if (res == null || res.getBody() == null) return result; + return !res.getBody().isEmpty(); + } catch (Exception e) { + log.error("Une erreur est survenue lors de l'appel au référentiel mix electrique", e); + } + return result; + } + + @Override + public List<TypeEquipementDTO> getAllTypesEquipement() { + var result = interneNumEcoEvalApi.getAllTypeEquipementWithHttpInfo().block(); + return result.getBody(); + } + + @Cacheable("CorrespondanceRefEquipement") + @Override + public CorrespondanceRefEquipementDTO getCorrespondance(String modele) { + try { + return interneNumEcoEvalApi.getCorrespondanceRefEquipement(modele).block(); + } catch (WebClientResponseException e) { + if (e.getStatusCode() == HttpStatus.NOT_FOUND) { + return null; + } + throw new RestException(e); + } + } + + @Cacheable("ImpactEquipement") + @Override + public ImpactEquipementDTO getImpactEquipement(String refEquipement, String critere, String etape) { + + try { + var res = interneNumEcoEvalApi.getImpactEquipement(refEquipement, critere, etape).block(); + if (res != null) { + return res; + } + } catch (WebClientResponseException e) { + if (e.getStatusCode() != HttpStatus.NOT_FOUND) { + log.error("Une erreur est survenue lors de l'appel au référentiel impactequipement", e); + return null; + } + } + return null; + } + + @Cacheable("Etapes") + @Override + public List<EtapeDTO> getAllEtapes() { + return interneNumEcoEvalApi.getAllEtapesWithHttpInfo().block().getBody(); + } + + @Cacheable("Criteres") + @Override + public List<CritereDTO> getAllCriteres() { + return interneNumEcoEvalApi.getAllCriteresWithHttpInfo().block().getBody(); + } +} diff --git a/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/cache/SchedulerEvictCache.java b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/cache/SchedulerEvictCache.java new file mode 100644 index 00000000..c3c4ffc9 --- /dev/null +++ b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/cache/SchedulerEvictCache.java @@ -0,0 +1,25 @@ +package org.mte.numecoeval.expositiondonneesentrees.infrastructure.cache; + +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +@Component +public class SchedulerEvictCache { + + @CacheEvict(value = "statutDesCalculs", allEntries = true) + @Scheduled(fixedRateString = "${caching.spring.statutDesCalculs}") + public void emptyCacheStatutDesCalculs() { + } + + @CacheEvict(value = { + "hasMixElec", + "Etapes", + "Criteres", + "CorrespondanceRefEquipement", + "ImpactEquipement" + }, allEntries = true) + @Scheduled(fixedRateString = "${caching.spring.referentiels}") + public void emptyCacheReferentiel() { + } +} diff --git a/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/config/ApplicationPortConfig.java b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/config/ApplicationPortConfig.java new file mode 100644 index 00000000..ecc79a18 --- /dev/null +++ b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/config/ApplicationPortConfig.java @@ -0,0 +1,26 @@ +package org.mte.numecoeval.expositiondonneesentrees.infrastructure.config; + +import lombok.AllArgsConstructor; +import org.mte.numecoeval.expositiondonneesentrees.domain.ports.input.ImportDonneesEntreePort; +import org.mte.numecoeval.expositiondonneesentrees.domain.ports.input.impl.ImportDonneesEntreePortImpl; +import org.mte.numecoeval.expositiondonneesentrees.domain.ports.output.ReferentielServicePort; +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.service.ErrorManagementService; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +@Configuration +@AllArgsConstructor +@ComponentScan(basePackages = "org.mte.numecoeval.expositiondonneesentrees.infrastructure.adapters") +public class ApplicationPortConfig { + + private ReferentielServicePort referentielServicePort; + + private ErrorManagementService errorManagementService; + private MessageProperties messageProperties; + + @Bean + public ImportDonneesEntreePort importDonneesEntreePort() { + return new ImportDonneesEntreePortImpl(referentielServicePort, errorManagementService, messageProperties.getMessages()); + } +} diff --git a/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/config/CommonIntegrationConfig.java b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/config/CommonIntegrationConfig.java new file mode 100644 index 00000000..608aefe0 --- /dev/null +++ b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/config/CommonIntegrationConfig.java @@ -0,0 +1,50 @@ +package org.mte.numecoeval.expositiondonneesentrees.infrastructure.config; + +import lombok.extern.slf4j.Slf4j; +import org.mte.numecoeval.expositiondonneesentrees.referentiels.generated.api.client.InterneNumEcoEvalApi; +import org.mte.numecoeval.expositiondonneesentrees.sync.calculs.generated.api.client.CalculsEquipementPhysiqueEtMessagerieApi; +import org.mte.numecoeval.expositiondonneesentrees.sync.calculs.generated.api.invoker.ApiClient; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.reactive.function.client.WebClient; + +@Configuration +@Slf4j +public class CommonIntegrationConfig { + + @Value("${numecoeval.calculs.server.url}") + String calculUrl; + + @Value("${numecoeval.referentiel.server.url}") + String referentielUrl; + + @Bean + public CalculsEquipementPhysiqueEtMessagerieApi clientAPISyncCalculs() { + CalculsEquipementPhysiqueEtMessagerieApi calculsApi = new CalculsEquipementPhysiqueEtMessagerieApi(); + var apiClient = new ApiClient(WebClient.builder() + .baseUrl(calculUrl) + .build()); + apiClient.setBasePath(calculUrl); + calculsApi.setApiClient(apiClient); + + log.info("Création du client d'API Sync Calcul sur l'URL {}", calculUrl); + + return calculsApi; + } + + @Bean + public InterneNumEcoEvalApi clientAPIReferentiel() { + var interneNumEcoEvalApi = new InterneNumEcoEvalApi(); + var apiClient = new org.mte.numecoeval.expositiondonneesentrees.referentiels.generated.api.invoker.ApiClient(WebClient.builder() + .baseUrl(referentielUrl) + .build()); + apiClient.setBasePath(referentielUrl); + interneNumEcoEvalApi.setApiClient(apiClient); + + log.info("Création du client d'API Referentiel sur l'URL {}", referentielUrl); + + return interneNumEcoEvalApi; + } + +} diff --git a/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/config/GeneralConfig.java b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/config/GeneralConfig.java new file mode 100644 index 00000000..3f6f4c05 --- /dev/null +++ b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/config/GeneralConfig.java @@ -0,0 +1,32 @@ +package org.mte.numecoeval.expositiondonneesentrees.infrastructure.config; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@EnableCaching +public class GeneralConfig { + + @Bean + public ObjectMapper getObjectMapper() { + ObjectMapper objectMapper = new ObjectMapper() + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .disable(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES) + .disable(DeserializationFeature.FAIL_ON_UNRESOLVED_OBJECT_IDS) + .enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY) + .disable(SerializationFeature.WRITE_SINGLE_ELEM_ARRAYS_UNWRAPPED) + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + ; + + // Nécessaire pour lire les objets LocalDate/LocalDateTime + objectMapper.registerModule(new JavaTimeModule()); + + return objectMapper; + } + +} diff --git a/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/config/MessageProperties.java b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/config/MessageProperties.java new file mode 100644 index 00000000..2b155075 --- /dev/null +++ b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/config/MessageProperties.java @@ -0,0 +1,20 @@ +package org.mte.numecoeval.expositiondonneesentrees.infrastructure.config; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.util.HashMap; + +@Component +@EnableConfigurationProperties +@ConfigurationProperties +@Getter +@Setter +public class MessageProperties { + + private HashMap<String, String> messages; + +} diff --git a/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/config/OpenApiConfig.java b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/config/OpenApiConfig.java new file mode 100644 index 00000000..5620670c --- /dev/null +++ b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/config/OpenApiConfig.java @@ -0,0 +1,47 @@ +package org.mte.numecoeval.expositiondonneesentrees.infrastructure.config; + +import org.apache.commons.lang3.ArrayUtils; +import org.springdoc.core.models.GroupedOpenApi; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class OpenApiConfig { + + private static final String[] IMPORT_PATHS = { + "/entrees/json", + "/entrees/csv" + }; + private static final String[] CALCUL_PATHS = { + "/entrees/calculs/*" + }; + + @Bean + public GroupedOpenApi importGroupApi() { + return GroupedOpenApi.builder() + .group("Imports") + .displayName("NumEcoEval Imports") + .pathsToMatch(IMPORT_PATHS) + .build(); + } + + @Bean + public GroupedOpenApi calculsGroupApi() { + return GroupedOpenApi.builder() + .group("Calculs") + .displayName("NumEcoEval Calculs") + .pathsToMatch(CALCUL_PATHS) + .build(); + } + + @Bean + public GroupedOpenApi allGroupApi() { + return GroupedOpenApi.builder() + .group("All") + .displayName("NumEcoEval Toutes les APIs") + .pathsToMatch(ArrayUtils.addAll(IMPORT_PATHS, CALCUL_PATHS)) + .build(); + } + +} + diff --git a/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/config/security/SecurityConfig.java b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/config/security/SecurityConfig.java new file mode 100644 index 00000000..6d89f6db --- /dev/null +++ b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/config/security/SecurityConfig.java @@ -0,0 +1,63 @@ +package org.mte.numecoeval.expositiondonneesentrees.infrastructure.config.security; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +import java.util.Arrays; + +/** + * Configuration Spring Security + */ +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + @Value("${numecoeval.urls.allowed:}") + private String[] urlsAllowed; + + /** + * Configuration pour les endpoints avec une session DNC. + */ + @Bean + SecurityFilterChain globalFilterChain(HttpSecurity http) throws Exception { + http + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) + .and() + .cors() + .and() + .authorizeHttpRequests(authz -> authz + // Healthcheck Actuator + .requestMatchers("/health").permitAll() + // application + .requestMatchers("/entrees/**").permitAll() + // Springdoc-openapi + .requestMatchers("/v3/api-docs/**").permitAll() + .requestMatchers("/swagger-ui/**").permitAll() + .requestMatchers("/swagger-ui.html").permitAll() + .requestMatchers("/openapi.yaml").permitAll() + ) + .csrf().disable() + .formLogin().disable() + ; + return http.build(); + } + + @Bean + CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + configuration.setAllowedOrigins(Arrays.asList(urlsAllowed)); + configuration.setAllowedMethods(Arrays.asList("GET","POST","OPTION")); + configuration.setAllowCredentials(true); + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + return source; + } +} diff --git a/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/controller/CalculController.java b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/controller/CalculController.java new file mode 100644 index 00000000..9727613e --- /dev/null +++ b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/controller/CalculController.java @@ -0,0 +1,59 @@ +package org.mte.numecoeval.expositiondonneesentrees.infrastructure.controller; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.mte.numecoeval.expositiondonneesentrees.domain.ports.input.SoumissionCalculPort; +import org.mte.numecoeval.expositiondonneesentrees.domain.ports.input.SoumissionCalculSyncPort; +import org.mte.numecoeval.expositiondonneesentrees.domain.ports.input.StatutPourCalculPort; +import org.mte.numecoeval.expositiondonneesentrees.generated.api.model.DemandeCalculRest; +import org.mte.numecoeval.expositiondonneesentrees.generated.api.model.ModeRest; +import org.mte.numecoeval.expositiondonneesentrees.generated.api.model.RapportDemandeCalculRest; +import org.mte.numecoeval.expositiondonneesentrees.generated.api.model.StatutCalculRest; +import org.mte.numecoeval.expositiondonneesentrees.generated.api.server.CalculsApi; +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.mapper.CalculRestMapper; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@AllArgsConstructor +@Slf4j +public class CalculController implements CalculsApi { + + CalculRestMapper calculRestMapper; + + SoumissionCalculPort soumissionCalculPort; + + SoumissionCalculSyncPort soumissionCalculSyncPort; + StatutPourCalculPort statutPourCalculPort; + + @Override + public ResponseEntity<StatutCalculRest> statutPourCalcul(String nomLot, String nomOrganisation) { + var statut = statutPourCalculPort.statutDesCalculs(nomLot, nomOrganisation); + log.info("Statut global des calculs, nomOrganisation: {}, nomLot: {}. Avancement: {}", nomOrganisation, nomLot, statut.getEtat()); + return ResponseEntity.ok(statut); + } + + @Override + public ResponseEntity<RapportDemandeCalculRest> soumissionPourCalcul(DemandeCalculRest demandeCalculRest, ModeRest mode) { + log.info("Soumission de calcul pour nom_lot: {}, mode: {}", demandeCalculRest.getNomLot(), mode); + var demandeCalcul = calculRestMapper.toDomain(demandeCalculRest); + + var soumission = ModeRest.ASYNC == mode ? + soumissionCalculPort.soumissionCalcul(demandeCalcul) : + soumissionCalculSyncPort.soumissionCalcul(demandeCalcul); + + return ResponseEntity.ok(calculRestMapper.toRest(soumission)); + } + + @Override + public ResponseEntity<RapportDemandeCalculRest> rejeuCalcul(DemandeCalculRest demandeCalculRest) { + log.info("Rejeu de calcul, nom_lot: {}", demandeCalculRest.getNomLot()); + var demandeCalcul = calculRestMapper.toDomain(demandeCalculRest); + + var soumission = soumissionCalculPort.rejeuCalcul(demandeCalcul); + + var responseBody = calculRestMapper.toRest(soumission); + + return ResponseEntity.ok(responseBody); + } +} diff --git a/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/controller/ImportCSVController.java b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/controller/ImportCSVController.java new file mode 100644 index 00000000..f8923656 --- /dev/null +++ b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/controller/ImportCSVController.java @@ -0,0 +1,172 @@ +package org.mte.numecoeval.expositiondonneesentrees.infrastructure.controller; + +import lombok.AllArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.mte.numecoeval.expositiondonneesentrees.domain.exception.ValidationException; +import org.mte.numecoeval.expositiondonneesentrees.domain.model.ResultatImport; +import org.mte.numecoeval.expositiondonneesentrees.domain.ports.input.ImportDonneesEntreePort; +import org.mte.numecoeval.expositiondonneesentrees.domain.ports.output.SaveDonneesEntreePort; +import org.mte.numecoeval.expositiondonneesentrees.generated.api.model.DonneesEntreeRest; +import org.mte.numecoeval.expositiondonneesentrees.generated.api.model.RapportImportRest; +import org.mte.numecoeval.expositiondonneesentrees.generated.api.server.ImportsApi; +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.helper.CSVHelper; +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.mapper.DonneesEntreeRestMapper; +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.service.ErrorManagementPostSaveService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.server.ResponseStatusException; + +import java.time.LocalDate; +import java.time.format.DateTimeParseException; +import java.util.List; + +@RestController +@AllArgsConstructor +@SuppressWarnings("java:S107") +// Obligatoire à cause de la gestion des fichiers différents au niveau contrat d'interface +public class ImportCSVController implements ImportsApi { + + private static final Logger LOGGER = LoggerFactory.getLogger(ImportCSVController.class); + + DonneesEntreeRestMapper donneesEntreeMapper; + + ImportDonneesEntreePort importDonneesEntreePort; + + SaveDonneesEntreePort saveDonneesEntreePort; + + ErrorManagementPostSaveService errorManagementPostSaveService; + + @Override + public ResponseEntity<List<RapportImportRest>> importCSV(String nomLot, MultipartFile csvDataCenter, MultipartFile csvEquipementPhysique, MultipartFile csvEquipementVirtuel, MultipartFile csvApplication, MultipartFile csvMessagerie, MultipartFile csvEntite, String dateLot, String nomOrganisation) { + return importInterneCSV(nomLot, dateLot, nomOrganisation, csvDataCenter, csvEquipementPhysique, csvEquipementVirtuel, csvApplication, csvMessagerie, csvEntite); + } + + public ResponseEntity<List<RapportImportRest>> importInterneCSV(String nomLot, String dateLot, String nomOrganisation, MultipartFile csvDataCenter, MultipartFile csvEquipementPhysique, MultipartFile csvEquipementVirtuel, MultipartFile csvApplication, MultipartFile csvMessagerie, MultipartFile csvEntite) { + return ResponseEntity.ok( + importDonneesFromCSV( + nomLot, dateLot, nomOrganisation, + csvDataCenter, + csvEquipementPhysique, + csvEquipementVirtuel, + csvApplication, + csvMessagerie, + csvEntite + ) + ); + } + + @Override + public ResponseEntity<List<RapportImportRest>> importJson(DonneesEntreeRest donneesEntreeRest) { + throw new ResponseStatusException( + HttpStatus.NOT_IMPLEMENTED, + "Cette méthode d'import n'a pas encore été implémenté." + ); + } + + public List<RapportImportRest> importDonneesFromCSV( + String nomLot, String dateLot, String nomOrganisation, + MultipartFile csvDataCenter, + MultipartFile csvEquipementPhysique, + MultipartFile csvEquipementVirtuel, + MultipartFile csvApplication, + MultipartFile csvMessagerie, + MultipartFile csvEntite) throws ValidationException { + LOGGER.info("Reception de fichiers pour imports de données d'entrées : Nom de Lot : {}, Date de lot : {}, Nom Organisation : {}", nomLot, dateLot, nomOrganisation); + + validateRequestParametersForImportCSV(nomLot, dateLot, csvDataCenter, csvEquipementPhysique, csvEquipementVirtuel, csvApplication, csvMessagerie, csvEntite); + + return importAllCsv(csvDataCenter, csvEquipementPhysique, csvEquipementVirtuel, csvApplication, csvMessagerie, csvEntite, dateLot, nomOrganisation, nomLot); + } + + /** + * Validation des entrées lors d'un import d'un ou plusieurs CSV. + * + * @param nomLot Nom de lot + * @param dateLot date du lot + * @param csvDataCenter Fichier CSV des data centers + * @param csvEquipementPhysique Fichier CSV des équipements physiques + * @param csvEquipementVirtuel Fichier CSV des équipements virtuels + * @param csvApplication Fichier CSV des applications + * @param csvMessagerie Fichier CSV de la messagerie + * @param csvEntite Fichier CSV des entités + * @throws ResponseStatusException avec le statut 400 lorsque les entrées ne sont pas cohérentes. + */ + private void validateRequestParametersForImportCSV(String nomLot, String dateLot, MultipartFile csvDataCenter, MultipartFile csvEquipementPhysique, MultipartFile csvEquipementVirtuel, MultipartFile csvApplication, MultipartFile csvMessagerie, MultipartFile csvEntite) { + if (CSVHelper.fileIsNullOrEmpty(csvDataCenter) + && CSVHelper.fileIsNullOrEmpty(csvEquipementPhysique) + && CSVHelper.fileIsNullOrEmpty(csvEquipementVirtuel) + && CSVHelper.fileIsNullOrEmpty(csvApplication) + && CSVHelper.fileIsNullOrEmpty(csvMessagerie) + && CSVHelper.fileIsNullOrEmpty(csvEntite) + ) { + LOGGER.error("Import: Erreur contrôle : Tous les fichiers ne peuvent être vides en même temps"); + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Tous les fichiers ne peuvent être vides en même temps"); + } + + if (StringUtils.isBlank(nomLot)) { + LOGGER.error("Import: Erreur contrôle : Le nom du Lot ne peut être pas vide"); + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Le nom du Lot ne peut être pas vide"); + } + + if (dateLot != null && !"".equals(dateLot)) { + try { + LocalDate.parse(dateLot); + } catch (DateTimeParseException e) { + LOGGER.error("Import: Erreur contrôle : La date du lot doit avoir le format yyyy-MM-dd"); + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "La date du lot doit avoir le format yyyy-MM-dd"); + } + } + + } + + /** + * Importe tous les fichiers CSV en paramètre. + * + * @param csvDataCenter Fichier CSV des data centers + * @param csvEquipementPhysique Fichier CSV des équipements physiques + * @param csvEquipementVirtuel Fichier CSV des équipements virtuels + * @param csvApplication Fichier CSV des applications + * @param csvMessagerie Fichier CSV de la messagerie + * @param csvEntite Fichier CSV des entités + * @param dateLot Date du lot associée aux fichiers + * @param nomOrganisation Nom de l'organisation + * @param nomLot Nom du lot + * @return {@link List} des {@link RapportImportRest} correspondant à l'import + * @throws ValidationException en cas d'absence de donner à pousser dans le système. + */ + private List<RapportImportRest> importAllCsv(MultipartFile csvDataCenter, MultipartFile csvEquipementPhysique, MultipartFile csvEquipementVirtuel, MultipartFile csvApplication, MultipartFile csvMessagerie, MultipartFile csvEntite, String dateLot, String nomOrganisation, String nomLot) throws ValidationException { + // Lecture & conversion + var resultatImport = importDonneesEntreePort.importCsv(csvDataCenter, csvEquipementPhysique, csvEquipementVirtuel, csvApplication, csvMessagerie, csvEntite, dateLot, nomOrganisation, nomLot); + + saveDonneesEntreePort.save(resultatImport.getDonneesEntree()); + + // verifications apres sauvegarde en bdd + updateResultatImport(resultatImport, "equipement_physique", errorManagementPostSaveService.checkEquipementPhysiques(nomLot)); + updateResultatImport(resultatImport, "equipement_virtuel", errorManagementPostSaveService.checkEquipementVirtuels(nomLot)); + updateResultatImport(resultatImport, "application", errorManagementPostSaveService.checkApplications(nomLot)); + + return resultatImport.getRapports().stream() + .map(donneesEntreeMapper::toRestDTO) + .toList(); + } + + /** + * Mets a jour l'objet ResultatImport en y ajoutant la liste d'erreurs si non vide + * + * @param resultatImport l'objet resultatImport + * @param type le type d'element + * @param erreurs la liste d'erreur + */ + private void updateResultatImport(ResultatImport resultatImport, String type, List<String> erreurs) { + if (erreurs.isEmpty()) return; + resultatImport.getRapports().stream() + .filter(res -> type.equals(res.getType())) + .findFirst() + .ifPresent(o -> o.getErreurs().addAll(erreurs)); + } + +} diff --git a/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/controller/RestExceptionHandler.java b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/controller/RestExceptionHandler.java new file mode 100644 index 00000000..034c34c7 --- /dev/null +++ b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/controller/RestExceptionHandler.java @@ -0,0 +1,80 @@ +package org.mte.numecoeval.expositiondonneesentrees.infrastructure.controller; + +import org.mte.numecoeval.expositiondonneesentrees.domain.exception.NotFoundException; +import org.mte.numecoeval.expositiondonneesentrees.domain.exception.RestException; +import org.mte.numecoeval.expositiondonneesentrees.domain.exception.ValidationException; +import org.mte.numecoeval.expositiondonneesentrees.generated.api.model.ErreurRest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + +import java.time.OffsetDateTime; + +@ControllerAdvice +public class RestExceptionHandler extends ResponseEntityExceptionHandler { + + private static final Logger LOG = LoggerFactory.getLogger(RestExceptionHandler.class); + + @ExceptionHandler(value = {ValidationException.class}) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ResponseEntity<ErreurRest> handleValidationException(ValidationException ex, WebRequest request) { + LOG.error("Exception de validation survenue lors de la requête {}, Exception : {}", request.getContextPath(), ex.getErreur()); + return new ResponseEntity<>( + ErreurRest.builder() + .code("400") + .message(ex.getErreur()) + .timestamp(OffsetDateTime.now()) + .status(400) + .build(), + HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(value = {NotFoundException.class}) + @ResponseStatus(HttpStatus.NOT_FOUND) + public ResponseEntity<ErreurRest> handleNotFoundException(NotFoundException ex, WebRequest request) { + return new ResponseEntity<>( + ErreurRest.builder() + .code("404") + .message(ex.getMessage()) + .timestamp(OffsetDateTime.now()) + .status(404) + .build(), + HttpStatus.NOT_FOUND); + } + + @ExceptionHandler(value = {RuntimeException.class, RestException.class}) + @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR) + public ResponseEntity<ErreurRest> runtimeException(Exception ex, WebRequest request) { + LOG.error("RuntimeException lors d'un traitement sur l'URI {} : {}", request.getContextPath(), ex.getMessage()); + LOG.debug("RuntimeException lors d'un traitement sur l'URI {}", request.getContextPath(), ex); + return new ResponseEntity<>( + ErreurRest.builder() + .code("500") + .message("Erreur interne de traitement lors du traitement de la requête") + .timestamp(OffsetDateTime.now()) + .status(500) + .build(), + HttpStatus.INTERNAL_SERVER_ERROR); + } + + @ExceptionHandler(value = {Exception.class}) + @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR) + public ResponseEntity<ErreurRest> exception(Exception ex, WebRequest request) { + LOG.error("Exception lors d'un traitement sur l'URI {} : {}", request.getContextPath(), ex.getMessage()); + LOG.debug("Exception lors d'un traitement sur l'URI {}", request.getContextPath(), ex); + return new ResponseEntity<>( + ErreurRest.builder() + .code("500") + .message("Erreur interne de traitement lors du traitement de la requête") + .timestamp(OffsetDateTime.now()) + .status(500) + .build(), + HttpStatus.INTERNAL_SERVER_ERROR); + } +} diff --git a/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/handler/RestClientResponseErrorHandler.java b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/handler/RestClientResponseErrorHandler.java new file mode 100644 index 00000000..44925c4d --- /dev/null +++ b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/handler/RestClientResponseErrorHandler.java @@ -0,0 +1,40 @@ +package org.mte.numecoeval.expositiondonneesentrees.infrastructure.handler; + +import org.mte.numecoeval.expositiondonneesentrees.domain.exception.ReferentielRuntimeException; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.web.client.ResponseErrorHandler; + +import java.io.IOException; + +public class RestClientResponseErrorHandler implements ResponseErrorHandler { + + private String errorType; + private String errorMessage; + + public RestClientResponseErrorHandler(String errorType, String errorMessage) { + this.errorType = errorType; + this.errorMessage = errorMessage; + } + + + @Override + public boolean hasError(ClientHttpResponse httpResponse) throws IOException { + return (httpResponse.getStatusCode().is4xxClientError() || + httpResponse.getStatusCode().is5xxServerError()); + } + + + @Override + public void handleError(ClientHttpResponse httpResponse) throws IOException { + if (httpResponse.getStatusCode().is5xxServerError()) { + //Handle SERVER_ERROR + throw new ReferentielRuntimeException("ErrCalcTech","erreur serveur : "+httpResponse.getStatusText()); + + } + else if (httpResponse.getStatusCode().is4xxClientError()) { + //Handle CLIENT_ERROR + throw new ReferentielRuntimeException(errorType,errorMessage); + } + + } +} diff --git a/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/helper/CSVHelper.java b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/helper/CSVHelper.java new file mode 100644 index 00000000..976b68b2 --- /dev/null +++ b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/helper/CSVHelper.java @@ -0,0 +1,75 @@ +package org.mte.numecoeval.expositiondonneesentrees.infrastructure.helper; + +import org.apache.commons.csv.CSVRecord; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.math.NumberUtils; +import org.springframework.web.multipart.MultipartFile; + +import java.time.LocalDate; + +public class CSVHelper { + + private CSVHelper() { + } + + /** + * Renvoie la valeur de la colonne {@param mainName} dans le {@param csvRecord}. + * Si le {@param mainName} n'est pas mappé dans le {@link CSVRecord}, la liste des noms alternatifs est utilisée. + * Si aucune colonne n'est mappée ou que la liste alternative est vide, la valeur {@code null} est renvoyée. + * @param csvRecord La ligne de CSV à traiter + * @param mainName Le nom de la colonne souhaitée en 1er + * @param alternativeNames Les noms de colonnes alternatifs à utiliser, peut être vide + * @return La valeur de la 1er colonne correctement mappée sur la ligne de CSV, {@code null} en absence de mapping ou de contenue + */ + public static String safeString(CSVRecord csvRecord, String mainName, String... alternativeNames) { + if(csvRecord.isMapped(mainName)) { + return StringUtils.defaultIfBlank(StringUtils.trim(csvRecord.get(mainName)), null); + } + + for (String alternativeName : alternativeNames) { + if(csvRecord.isMapped(alternativeName)) { + return StringUtils.trim(csvRecord.get(alternativeName)); + } + } + return null; + } + + public static LocalDate safeParseLocalDate(CSVRecord csvRecord, String field) { + if(!csvRecord.isMapped(field)) { + return null; + } + if(StringUtils.isEmpty(StringUtils.trim(csvRecord.get(field)))) { + return null; + } + return LocalDate.parse(StringUtils.trim(csvRecord.get(field))); + } + + public static Double safeDouble(CSVRecord csvRecord, String field) { + if(!csvRecord.isMapped(field)) { + return null; + } + if(!NumberUtils.isCreatable(StringUtils.trim(csvRecord.get(field)))) { + return null; + } + return NumberUtils.toDouble(StringUtils.trim(csvRecord.get(field))); + } + + public static Integer safeInteger(CSVRecord csvRecord, String field) { + if(!csvRecord.isMapped(field)) { + return null; + } + if(!NumberUtils.isCreatable(StringUtils.trim(csvRecord.get(field)))) { + return null; + } + return NumberUtils.toInt(StringUtils.trim(csvRecord.get(field))); + } + + /** + * Vérifie si le fichier en paramètre est {@code null} ou vide ({@see MultipartFile#isEmpty()}). + * @param fichierATester fichier à tester + * @return {@code true} si le fichier est vide ou null sinon {@code false} + */ + public static boolean fileIsNullOrEmpty(MultipartFile fichierATester) { + return fichierATester == null || fichierATester.isEmpty(); + } +} diff --git a/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/helper/Constants.java b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/helper/Constants.java new file mode 100644 index 00000000..9c448a70 --- /dev/null +++ b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/helper/Constants.java @@ -0,0 +1,11 @@ +package org.mte.numecoeval.expositiondonneesentrees.infrastructure.helper; + +public class Constants { + + public static final String TABLE_DONNEES_ENTREES = "en_donnees_entrees"; + public static final String TABLE_DATA_CENTER = "en_data_center"; + public static final String TABLE_EQUIPEMENT_PHYSIQUE = "en_equipement_physique"; + public static final String TABLE_EQUIPEMENT_VIRTUEL = "en_equipement_virtuel"; + public static final String TABLE_APPLICATION = "en_application"; + public static final String TABLE_MESSAGERIE = "en_messagerie"; +} diff --git a/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jdbc/SoumissionCalculPortJdbcImpl.java b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jdbc/SoumissionCalculPortJdbcImpl.java new file mode 100644 index 00000000..4e6095f9 --- /dev/null +++ b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jdbc/SoumissionCalculPortJdbcImpl.java @@ -0,0 +1,67 @@ +package org.mte.numecoeval.expositiondonneesentrees.infrastructure.jdbc; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.mte.numecoeval.expositiondonneesentrees.domain.model.DemandeCalcul; +import org.mte.numecoeval.expositiondonneesentrees.domain.model.RapportDemandeCalcul; +import org.mte.numecoeval.expositiondonneesentrees.domain.ports.input.SoumissionCalculPort; +import org.mte.numecoeval.expositiondonneesentrees.generated.api.model.StatutTraitement; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import static org.mte.numecoeval.expositiondonneesentrees.infrastructure.helper.Constants.*; + +@Service +@Slf4j +@AllArgsConstructor +public class SoumissionCalculPortJdbcImpl implements SoumissionCalculPort { + + private static final String STATUT_TRAITEMENT_A_INGERER = StatutTraitement.A_INGERER.getValue(); + + private static final String STATUT_TRAITEMENT_EN_ATTENTE = StatutTraitement.EN_ATTENTE.getValue(); + + JdbcTemplate jdbcTemplate; + + @Transactional + @Override + public RapportDemandeCalcul soumissionCalcul(DemandeCalcul demandeCalcul) { + validate(demandeCalcul); + + var rapport = new RapportDemandeCalcul(); + rapport.setNomLot(demandeCalcul.getNomLot()); + rapport.setDateLot(demandeCalcul.getDateLot()); + rapport.setNomOrganisation(demandeCalcul.getNomOrganisation()); + jdbcTemplate.update(getUpdateStatementForTable(TABLE_DONNEES_ENTREES), STATUT_TRAITEMENT_A_INGERER, demandeCalcul.getNomLot(), STATUT_TRAITEMENT_EN_ATTENTE); + rapport.setNbrDataCenter(jdbcTemplate.update(getUpdateStatementForTable(TABLE_DATA_CENTER), STATUT_TRAITEMENT_A_INGERER, demandeCalcul.getNomLot(), STATUT_TRAITEMENT_EN_ATTENTE)); + rapport.setNbrEquipementPhysique(jdbcTemplate.update(getUpdateStatementForTable(TABLE_EQUIPEMENT_PHYSIQUE), STATUT_TRAITEMENT_A_INGERER, demandeCalcul.getNomLot(), STATUT_TRAITEMENT_EN_ATTENTE)); + rapport.setNbrMessagerie(jdbcTemplate.update(getUpdateStatementForTable(TABLE_MESSAGERIE), STATUT_TRAITEMENT_A_INGERER, demandeCalcul.getNomLot(), STATUT_TRAITEMENT_EN_ATTENTE)); + return rapport; + } + + @Override + public RapportDemandeCalcul rejeuCalcul(DemandeCalcul demandeCalcul) { + validate(demandeCalcul); + + var rapport = new RapportDemandeCalcul(); + rapport.setNomLot(demandeCalcul.getNomLot()); + rapport.setDateLot(demandeCalcul.getDateLot()); + rapport.setNomOrganisation(demandeCalcul.getNomOrganisation()); + + jdbcTemplate.update(getUpdateForRejeuStatementForTable(TABLE_DONNEES_ENTREES), STATUT_TRAITEMENT_A_INGERER, demandeCalcul.getNomLot()); + rapport.setNbrDataCenter(jdbcTemplate.update(getUpdateForRejeuStatementForTable(TABLE_DATA_CENTER), STATUT_TRAITEMENT_A_INGERER, demandeCalcul.getNomLot())); + rapport.setNbrEquipementPhysique(jdbcTemplate.update(getUpdateForRejeuStatementForTable(TABLE_EQUIPEMENT_PHYSIQUE), STATUT_TRAITEMENT_A_INGERER, demandeCalcul.getNomLot())); + rapport.setNbrMessagerie(jdbcTemplate.update(getUpdateForRejeuStatementForTable(TABLE_MESSAGERIE), STATUT_TRAITEMENT_A_INGERER, demandeCalcul.getNomLot())); + return rapport; + } + + private String getUpdateStatementForTable(String table) { + return "UPDATE " + table + " SET date_update = NOW(), statut_traitement = ? " + + "WHERE nom_lot = ? and statut_traitement = ?"; + } + + private String getUpdateForRejeuStatementForTable(String table) { + return "UPDATE " + table + " SET date_update = NOW(), statut_traitement = ? " + + "WHERE nom_lot = ?"; + } +} diff --git a/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jdbc/VolumeJdbc.java b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jdbc/VolumeJdbc.java new file mode 100644 index 00000000..11275a13 --- /dev/null +++ b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jdbc/VolumeJdbc.java @@ -0,0 +1,75 @@ +package org.mte.numecoeval.expositiondonneesentrees.infrastructure.jdbc; + +import lombok.extern.slf4j.Slf4j; +import org.mte.numecoeval.expositiondonneesentrees.domain.exception.RestException; +import org.mte.numecoeval.expositiondonneesentrees.domain.model.Volume; +import org.mte.numecoeval.expositiondonneesentrees.generated.api.model.StatutTraitement; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.sql.DataSource; +import java.sql.Array; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +@Slf4j +@Service +public class VolumeJdbc { + + @Autowired + private DataSource dataSource; + + private static final String SQL_QUERY_VOLUME = """ + SELECT count(*) as count from %s + WHERE nom_lot = ? AND nom_organisation = ? AND statut_traitement = ANY (?) + """; + + /** + * @param nomLot nom lot + * @param nomOrganisation nom organisation + * @param table entree table + * @return le volume enCours, traite + */ + public Volume getVolumeBy(String nomLot, String nomOrganisation, String table) { + long countTermine = 0; + long countEnCours = 0; + + try (Connection conn = dataSource.getConnection()) { + + try (PreparedStatement statement = conn.prepareStatement(String.format(SQL_QUERY_VOLUME, table))) { + + Array statuts = conn.createArrayOf("varchar", new String[]{ + StatutTraitement.A_INGERER.getValue(), + StatutTraitement.EN_ATTENTE.getValue(), + StatutTraitement.INGERE.getValue() + }); + + statement.setString(1, nomLot); + statement.setString(2, nomOrganisation); + statement.setArray(3, statuts); + + var resultSet = statement.executeQuery(); + while (resultSet.next()) { + countEnCours = resultSet.getLong("count"); + } + + statuts = conn.createArrayOf("varchar", new String[]{ + StatutTraitement.TRAITE.getValue(), + StatutTraitement.EN_ERREUR.getValue() + }); + + statement.setArray(3, statuts); + resultSet = statement.executeQuery(); + while (resultSet.next()) { + countTermine = resultSet.getLong("count"); + } + } + } catch (SQLException e) { + log.error("Cannot get volume of table {}", table); + throw new RestException(e); + } + + return new Volume(countEnCours, countTermine); + } +} diff --git a/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/adapter/ApplicationJpaAdapter.java b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/adapter/ApplicationJpaAdapter.java new file mode 100644 index 00000000..93c80e91 --- /dev/null +++ b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/adapter/ApplicationJpaAdapter.java @@ -0,0 +1,27 @@ +package org.mte.numecoeval.expositiondonneesentrees.infrastructure.jpa.adapter; + +import lombok.AllArgsConstructor; +import org.mte.numecoeval.expositiondonneesentrees.domain.model.Application; +import org.mte.numecoeval.expositiondonneesentrees.domain.ports.output.EntreePersistencePort; +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.jpa.repository.ApplicationRepository; +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.mapper.EntreeEntityMapper; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@AllArgsConstructor +public class ApplicationJpaAdapter implements EntreePersistencePort<Application> { + private ApplicationRepository repository; + EntreeEntityMapper entreeEntityMapper; + + @Override + public void save(Application entree) { + repository.save(entreeEntityMapper.toEntity(entree)); + } + + @Override + public void saveAll(List<Application> entrees) { + repository.saveAll(entreeEntityMapper.toEntityListApplication(entrees)); + } +} \ No newline at end of file diff --git a/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/adapter/DataCenterJpaAdapter.java b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/adapter/DataCenterJpaAdapter.java new file mode 100644 index 00000000..7a9e6aed --- /dev/null +++ b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/adapter/DataCenterJpaAdapter.java @@ -0,0 +1,29 @@ +package org.mte.numecoeval.expositiondonneesentrees.infrastructure.jpa.adapter; + +import lombok.AllArgsConstructor; +import org.mte.numecoeval.expositiondonneesentrees.domain.model.DataCenter; +import org.mte.numecoeval.expositiondonneesentrees.domain.ports.output.EntreePersistencePort; +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.jpa.repository.DataCenterRepository; +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.mapper.EntreeEntityMapper; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@AllArgsConstructor +public class DataCenterJpaAdapter implements EntreePersistencePort<DataCenter> { + + DataCenterRepository repository; + + EntreeEntityMapper entreeEntityMapper; + + @Override + public void save(DataCenter entree) { + repository.save(entreeEntityMapper.toEntity(entree)); + } + + @Override + public void saveAll(List<DataCenter> entrees) { + repository.saveAll(entreeEntityMapper.toEntityListDataCenter(entrees)); + } +} diff --git a/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/adapter/DonneesEntreesJpaAdapter.java b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/adapter/DonneesEntreesJpaAdapter.java new file mode 100644 index 00000000..f97b0c05 --- /dev/null +++ b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/adapter/DonneesEntreesJpaAdapter.java @@ -0,0 +1,30 @@ +package org.mte.numecoeval.expositiondonneesentrees.infrastructure.jpa.adapter; + +import lombok.AllArgsConstructor; +import org.apache.commons.collections4.CollectionUtils; +import org.mte.numecoeval.expositiondonneesentrees.domain.model.DonneesEntree; +import org.mte.numecoeval.expositiondonneesentrees.domain.ports.output.EntreePersistencePort; +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.jpa.repository.DonneesEntreesRepository; +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.mapper.EntreeEntityMapper; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@AllArgsConstructor +public class DonneesEntreesJpaAdapter implements EntreePersistencePort<DonneesEntree> { + + DonneesEntreesRepository repository; + + EntreeEntityMapper entreeEntityMapper; + + @Override + public void save(DonneesEntree entree) { + repository.save(entreeEntityMapper.toEntity(entree)); + } + + @Override + public void saveAll(List<DonneesEntree> entrees) { + CollectionUtils.emptyIfNull(entrees).forEach(this::save); + } +} diff --git a/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/adapter/EntiteJpaAdapter.java b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/adapter/EntiteJpaAdapter.java new file mode 100644 index 00000000..c49757ae --- /dev/null +++ b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/adapter/EntiteJpaAdapter.java @@ -0,0 +1,29 @@ +package org.mte.numecoeval.expositiondonneesentrees.infrastructure.jpa.adapter; + +import lombok.AllArgsConstructor; +import org.mte.numecoeval.expositiondonneesentrees.domain.model.Entite; +import org.mte.numecoeval.expositiondonneesentrees.domain.ports.output.EntreePersistencePort; +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.jpa.repository.EntiteRepository; +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.mapper.EntreeEntityMapper; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@AllArgsConstructor +public class EntiteJpaAdapter implements EntreePersistencePort<Entite> { + + EntiteRepository repository; + + EntreeEntityMapper entreeEntityMapper; + + @Override + public void save(Entite entree) { + repository.save(entreeEntityMapper.toEntity(entree)); + } + + @Override + public void saveAll(List<Entite> entrees) { + repository.saveAll(entreeEntityMapper.toEntityListEntite(entrees)); + } +} diff --git a/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/adapter/EquipementPhysiqueJpaAdapter.java b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/adapter/EquipementPhysiqueJpaAdapter.java new file mode 100644 index 00000000..c7256a14 --- /dev/null +++ b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/adapter/EquipementPhysiqueJpaAdapter.java @@ -0,0 +1,34 @@ +package org.mte.numecoeval.expositiondonneesentrees.infrastructure.jpa.adapter; + +import lombok.AllArgsConstructor; +import org.mte.numecoeval.expositiondonneesentrees.domain.model.EquipementPhysique; +import org.mte.numecoeval.expositiondonneesentrees.domain.ports.output.EntreePersistencePort; +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.jpa.repository.EquipementPhysiqueRepository; +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.mapper.EntreeEntityMapper; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Set; + +@Service +@AllArgsConstructor +public class EquipementPhysiqueJpaAdapter implements EntreePersistencePort<EquipementPhysique> { + + EquipementPhysiqueRepository repository; + + EntreeEntityMapper entreeEntityMapper; + + @Override + public void save(EquipementPhysique entree) { + repository.save(entreeEntityMapper.toEntity(entree)); + } + + @Override + public void saveAll(List<EquipementPhysique> entrees) { + repository.saveAll(entreeEntityMapper.toEntityListEquipementPhysique(entrees)); + } + + public int updateStatutInList(String statut, String nomLot, String nomOrganisation, Set<String> nomsEquipementsPhysiques) { + return repository.updateEquipementPhysiqueStatutTraitement(statut, nomLot, nomOrganisation, nomsEquipementsPhysiques); + } +} diff --git a/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/adapter/EquipementVirtuelJpaAdapter.java b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/adapter/EquipementVirtuelJpaAdapter.java new file mode 100644 index 00000000..99989e13 --- /dev/null +++ b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/adapter/EquipementVirtuelJpaAdapter.java @@ -0,0 +1,29 @@ +package org.mte.numecoeval.expositiondonneesentrees.infrastructure.jpa.adapter; + +import lombok.AllArgsConstructor; +import org.mte.numecoeval.expositiondonneesentrees.domain.model.EquipementVirtuel; +import org.mte.numecoeval.expositiondonneesentrees.domain.ports.output.EntreePersistencePort; +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.jpa.repository.EquipementVirtuelRepository; +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.mapper.EntreeEntityMapper; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@AllArgsConstructor +public class EquipementVirtuelJpaAdapter implements EntreePersistencePort<EquipementVirtuel> { + + EquipementVirtuelRepository repository; + + EntreeEntityMapper entreeEntityMapper; + + @Override + public void save(EquipementVirtuel entree) { + repository.save(entreeEntityMapper.toEntity(entree)); + } + + @Override + public void saveAll(List<EquipementVirtuel> entrees) { + repository.saveAll(entreeEntityMapper.toEntityListEquipementVirtuel(entrees) ); + } +} diff --git a/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/adapter/MessagerieJpaAdapter.java b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/adapter/MessagerieJpaAdapter.java new file mode 100644 index 00000000..a1edae99 --- /dev/null +++ b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/adapter/MessagerieJpaAdapter.java @@ -0,0 +1,29 @@ +package org.mte.numecoeval.expositiondonneesentrees.infrastructure.jpa.adapter; + +import lombok.AllArgsConstructor; +import org.mte.numecoeval.expositiondonneesentrees.domain.model.Messagerie; +import org.mte.numecoeval.expositiondonneesentrees.domain.ports.output.EntreePersistencePort; +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.jpa.repository.MessagerieRepository; +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.mapper.EntreeEntityMapper; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@AllArgsConstructor +public class MessagerieJpaAdapter implements EntreePersistencePort<Messagerie> { + + MessagerieRepository repository; + + EntreeEntityMapper entreeEntityMapper; + + @Override + public void save(Messagerie entree) { + repository.save(entreeEntityMapper.toEntity(entree)); + } + + @Override + public void saveAll(List<Messagerie> entrees) { + repository.saveAll(entreeEntityMapper.toEntityListMessagerie(entrees)); + } +} diff --git a/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/adapter/SaveDonneesEntreeAdapter.java b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/adapter/SaveDonneesEntreeAdapter.java new file mode 100644 index 00000000..446463b9 --- /dev/null +++ b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/adapter/SaveDonneesEntreeAdapter.java @@ -0,0 +1,145 @@ +package org.mte.numecoeval.expositiondonneesentrees.infrastructure.jpa.adapter; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.time.StopWatch; +import org.mte.numecoeval.expositiondonneesentrees.domain.model.*; +import org.mte.numecoeval.expositiondonneesentrees.domain.ports.output.EntreePersistencePort; +import org.mte.numecoeval.expositiondonneesentrees.domain.ports.output.SaveDonneesEntreePort; +import org.mte.numecoeval.expositiondonneesentrees.generated.api.model.StatutTraitement; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.stereotype.Service; + +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +@Slf4j +@AllArgsConstructor +@Service +public class SaveDonneesEntreeAdapter implements SaveDonneesEntreePort { + + private EntreePersistencePort<DonneesEntree> donneesEntreesEntreePersistencePort; + private EntreePersistencePort<DataCenter> dataCenterEntreePersistencePort; + private EntreePersistencePort<EquipementPhysique> equipementPhysiqueEntreePersistencePort; + private EntreePersistencePort<EquipementVirtuel> equipementVirtuelEntreePersistencePort; + private EntreePersistencePort<Application> applicationEntreePersistencePort; + private EntreePersistencePort<Messagerie> messagerieEntreePersistencePort; + private EntreePersistencePort<Entite> entiteEntreePersistencePort; + + @Override + @CacheEvict(cacheNames = "correspondanceRefEquipement", allEntries = true) + public void save(DonneesEntree donneesEntree) { + + if (donneesEntree == null) { + log.warn("Données null reçue"); + return; + } + + log.info("Données reçues : Nom Lot : {}, Date de lot : {} - Nom Organisation : {}", + donneesEntree.getNomLot(), donneesEntree.getDateLot(), donneesEntree.getNomOrganisation() + ); + + + Set<String> equipementsPhysiquesImpactes = new HashSet<>(); + Set<String> csvEquipementPhysiques = new HashSet<>(); + + StopWatch globalStopWatch = StopWatch.createStarted(); + + StopWatch stopWatch = StopWatch.createStarted(); + donneesEntreesEntreePersistencePort.save(donneesEntree); + stopWatch.stop(); + log.info("Fin du traitement de l'objet DonneesEntree reçue en {} secondes", stopWatch.getTime(TimeUnit.SECONDS)); + + if (!CollectionUtils.isEmpty(donneesEntree.getDataCenters())) { + stopWatch = StopWatch.createStarted(); + dataCenterEntreePersistencePort.saveAll(donneesEntree.getDataCenters()); + stopWatch.stop(); + log.info("Fin du traitement des {} objets DataCenter reçus en {} secondes", + donneesEntree.getDataCenters().size(), + stopWatch.getTime(TimeUnit.SECONDS) + ); + } + + if (!CollectionUtils.isEmpty(donneesEntree.getEquipementsPhysiques())) { + stopWatch = StopWatch.createStarted(); + equipementPhysiqueEntreePersistencePort.saveAll(donneesEntree.getEquipementsPhysiques()); + stopWatch.stop(); + log.info("Fin du traitement des {} objets EquipementPhysique reçus en {} secondes", + donneesEntree.getEquipementsPhysiques().size(), + stopWatch.getTime(TimeUnit.SECONDS) + ); + csvEquipementPhysiques = donneesEntree.getEquipementsPhysiques().stream() + .map(EquipementPhysique::getNomEquipementPhysique) + .collect(Collectors.toSet()); + } + + if (!CollectionUtils.isEmpty(donneesEntree.getEquipementsVirtuels())) { + stopWatch = StopWatch.createStarted(); + equipementVirtuelEntreePersistencePort.saveAll(donneesEntree.getEquipementsVirtuels()); + stopWatch.stop(); + log.info("Fin du traitement des {} objets EquipementVirtuel reçus en {} secondes", + donneesEntree.getEquipementsVirtuels().size(), + stopWatch.getTime(TimeUnit.SECONDS) + ); + + equipementsPhysiquesImpactes.addAll(donneesEntree.getEquipementsVirtuels().stream() + .map(EquipementVirtuel::getNomEquipementPhysique) + .collect(Collectors.toSet())); + } + + if (!CollectionUtils.isEmpty(donneesEntree.getApplications())) { + stopWatch = StopWatch.createStarted(); + applicationEntreePersistencePort.saveAll(donneesEntree.getApplications()); + stopWatch.stop(); + log.info("Fin du traitement des {} objets Applications reçus en {} secondes", + donneesEntree.getApplications().size(), + stopWatch.getTime(TimeUnit.SECONDS) + ); + + equipementsPhysiquesImpactes.addAll(donneesEntree.getApplications().stream() + .map(Application::getNomEquipementPhysique) + .collect(Collectors.toSet())); + } + + if (!CollectionUtils.isEmpty(donneesEntree.getMessageries())) { + stopWatch = StopWatch.createStarted(); + messagerieEntreePersistencePort.saveAll(donneesEntree.getMessageries()); + stopWatch.stop(); + log.info("Fin du traitement des {} objets Messagerie reçus en {} secondes", + donneesEntree.getMessageries().size(), + stopWatch.getTime(TimeUnit.SECONDS) + ); + } + + if (!CollectionUtils.isEmpty(donneesEntree.getEntites())) { + stopWatch = StopWatch.createStarted(); + entiteEntreePersistencePort.saveAll(donneesEntree.getEntites()); + stopWatch.stop(); + log.info("Fin du traitement des {} objets Entites reçus en {} secondes", + donneesEntree.getEntites(), + stopWatch.getTime(TimeUnit.SECONDS) + ); + } + + + equipementsPhysiquesImpactes.removeAll(csvEquipementPhysiques); + + if (!equipementsPhysiquesImpactes.isEmpty()) { + // mise a jour du statut_traitement à EN_ATTENTE + int nbUpdated = equipementPhysiqueEntreePersistencePort.updateStatutInList(StatutTraitement.EN_ATTENTE.getValue(), donneesEntree.getNomLot(), donneesEntree.getNomOrganisation(), equipementsPhysiquesImpactes); + log.info("Traitement en mode delta: Nom de Lot : {} - Date de lot : {} - Nom Organisation : {}. Nombre d'équipements physiques à rejouer : {}, réellement rejoués: {}", + donneesEntree.getNomLot(), donneesEntree.getDateLot(), donneesEntree.getNomOrganisation(), + equipementsPhysiquesImpactes.size(), nbUpdated); + } + + globalStopWatch.stop(); + log.info("Fin du traitement des données reçues : Nom de Lot : {} - Date de lot : {} - Nom Organisation : {}, en {} secondes", + donneesEntree.getNomLot(), donneesEntree.getDateLot(), donneesEntree.getNomOrganisation(), + globalStopWatch.getTime(TimeUnit.SECONDS) + ); + + } +} diff --git a/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/entity/AbstractEntreeEntity.java b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/entity/AbstractEntreeEntity.java new file mode 100644 index 00000000..56f47c67 --- /dev/null +++ b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/entity/AbstractEntreeEntity.java @@ -0,0 +1,53 @@ +package org.mte.numecoeval.expositiondonneesentrees.infrastructure.jpa.entity; + +import jakarta.persistence.MappedSuperclass; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.SuperBuilder; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@SuperBuilder +@MappedSuperclass +public abstract class AbstractEntreeEntity { + + @CreationTimestamp + protected LocalDateTime dateCreation; + + @UpdateTimestamp + protected LocalDateTime dateUpdate; + + /** + * Nom de l'organisation liée aux données - Metadata + */ + protected String nomOrganisation; + + /** + * Nom de la source de données - Metadata + */ + protected String nomSourceDonnee; + + /** + * Nom du lot de données + */ + protected String nomLot; + + /** + * La date du lot permet d’agréger les données provenant de différentes sources et d'en faire un suivi temporel + */ + protected LocalDate dateLot; + + /** + * Statut du traitement de la donnée dans NumEcoEval + */ + protected String statutTraitement; +} diff --git a/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/entity/ApplicationEntity.java b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/entity/ApplicationEntity.java new file mode 100644 index 00000000..687361b3 --- /dev/null +++ b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/entity/ApplicationEntity.java @@ -0,0 +1,74 @@ +package org.mte.numecoeval.expositiondonneesentrees.infrastructure.jpa.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.SequenceGenerator; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.SuperBuilder; + +/** + * Entité représentant une application dans les données d'entrées. + */ +@Getter +@Setter +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode +@Table(name = "EN_APPLICATION") +@Entity +public class ApplicationEntity extends AbstractEntreeEntity{ + @Id + @GeneratedValue(generator = "SEQ_EN_APPLICATION", strategy = GenerationType.SEQUENCE) + @SequenceGenerator(name = "SEQ_EN_APPLICATION", sequenceName="SEQ_EN_APPLICATION",allocationSize=1000) + @Column(nullable = false) + private Long id; + + /** + * Nom de l'application + */ + private String nomApplication; + + /** + * Type d'environnement de l'instance de l'application + */ + private String typeEnvironnement; + + /** + * Référence de l'équipement virtuel rattaché + */ + private String nomEquipementVirtuel; + + /** + * Référence de l'équipement physique rattaché + */ + private String nomEquipementPhysique; + + /** + * Nom de la source de données pour l'équipement physique rattaché + */ + private String nomSourceDonneeEquipementVirtuel; + + /** + * Domaine ou catégorie principale de l'application + */ + private String domaine; + + /** + * Domaine ou catégorie secondaire de l'application + */ + private String sousDomaine; + + /** + * Nom de l'entité rattachée à l'application + */ + private String nomEntite; +} diff --git a/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/entity/DataCenterEntity.java b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/entity/DataCenterEntity.java new file mode 100644 index 00000000..6f9a6f4b --- /dev/null +++ b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/entity/DataCenterEntity.java @@ -0,0 +1,38 @@ +package org.mte.numecoeval.expositiondonneesentrees.infrastructure.jpa.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.SequenceGenerator; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.SuperBuilder; + +@Getter +@Setter +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@Table(name = "EN_DATA_CENTER") +@Entity +public class DataCenterEntity extends AbstractEntreeEntity { + + @Id + @GeneratedValue(generator = "SEQ_EN_DATA_CENTER", strategy = GenerationType.SEQUENCE) + @SequenceGenerator(name = "SEQ_EN_DATA_CENTER", sequenceName="SEQ_EN_DATA_CENTER",allocationSize=1000) + @Column(nullable = false) + protected Long id; + + private String nomCourtDatacenter; + private String nomLongDatacenter; + private Double pue; + private String localisation; + private String nomEntite; + + +} diff --git a/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/entity/DonneesEntreesEntity.java b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/entity/DonneesEntreesEntity.java new file mode 100644 index 00000000..277304f7 --- /dev/null +++ b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/entity/DonneesEntreesEntity.java @@ -0,0 +1,58 @@ +package org.mte.numecoeval.expositiondonneesentrees.infrastructure.jpa.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.SequenceGenerator; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.SuperBuilder; + +/** + * Entité représentant un message global d'arrivée de Données d'entrées. + */ +@Getter +@Setter +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@Table(name = "EN_DONNEES_ENTREES") +@Entity +public class DonneesEntreesEntity extends AbstractEntreeEntity { + @Id + @GeneratedValue(generator = "SEQ_EN_DONNEES_ENTREES", strategy = GenerationType.SEQUENCE) + @SequenceGenerator(name = "SEQ_EN_DONNEES_ENTREES", sequenceName = "SEQ_EN_DONNEES_ENTREES", allocationSize = 1000) + @Column(nullable = false) + protected Long id; + + /** + * Nombre de Data Centers rattachés à ce message de données d'entrées + */ + private Long nbrDataCenter; + + /** + * Nombre d'équipments physiques rattachés à ce message de données d'entrées + */ + private Long nbrEquipementsPhysiques; + + /** + * Nombre d'équipements virtuels rattachés à ce message de données d'entrées + */ + private Long nbrEquipementsVirtuels; + + /** + * Nombre d'applications rattachés à ce message de données d'entrées + */ + private Long nbrApplications; + + /** + * Nombre d'éléments de messageries rattachés à ce message de données d'entrées + */ + private Long nbrMessageries; + +} diff --git a/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/entity/EntiteEntity.java b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/entity/EntiteEntity.java new file mode 100644 index 00000000..cc0b05ac --- /dev/null +++ b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/entity/EntiteEntity.java @@ -0,0 +1,37 @@ +package org.mte.numecoeval.expositiondonneesentrees.infrastructure.jpa.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.SequenceGenerator; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.SuperBuilder; + +@Getter +@Setter +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@Table(name = "EN_ENTITE") +@Entity +public class EntiteEntity extends AbstractEntreeEntity +{ + @Id + @GeneratedValue(generator = "SEQ_EN_ENTITE", strategy = GenerationType.SEQUENCE) + @SequenceGenerator(name = "SEQ_EN_ENTITE", sequenceName="SEQ_EN_ENTITE",allocationSize=1000) + @Column(nullable = false) + private Long id; + + private String nomEntite; + private Integer nbCollaborateurs; + private String responsableEntite; + private String responsableNumeriqueDurable; + + +} diff --git a/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/entity/EquipementPhysiqueEntity.java b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/entity/EquipementPhysiqueEntity.java new file mode 100644 index 00000000..75cd3f41 --- /dev/null +++ b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/entity/EquipementPhysiqueEntity.java @@ -0,0 +1,52 @@ +package org.mte.numecoeval.expositiondonneesentrees.infrastructure.jpa.entity; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.SuperBuilder; + +import java.time.LocalDate; + +@Getter +@Setter +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@Table(name = "EN_EQUIPEMENT_PHYSIQUE") +@Entity +public class EquipementPhysiqueEntity extends AbstractEntreeEntity { + + @Id + @GeneratedValue(generator = "SEQ_EN_EQUIPEMENT_PHYSIQUE", strategy = GenerationType.SEQUENCE) + @SequenceGenerator(name = "SEQ_EN_EQUIPEMENT_PHYSIQUE", sequenceName = "SEQ_EN_EQUIPEMENT_PHYSIQUE", allocationSize = 1000) + @Column(nullable = false) + private Long id; + + private String nomEquipementPhysique; + private String type; + private String modele; + private String statut; + + @Column(name = "pays_utilisation") + private String paysDUtilisation; + private String utilisateur; + private LocalDate dateAchat; + private LocalDate dateRetrait; + private String nbCoeur; + private String nomCourtDatacenter; + private Double nbJourUtiliseAn; + private Float goTelecharge; + private Double consoElecAnnuelle; + private boolean serveur; + private Double dureeVieDefaut; + // référence d'équipement par défaut, propre au traitement + private String refEquipementParDefaut; + // référence d'équipement retenu via Correspondance dans le référentiel, propre au traitement + String refEquipementRetenu; + private String nomEntite; + private Double quantite; + private String modeUtilisation; + private Double tauxUtilisation; +} diff --git a/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/entity/EquipementVirtuelEntity.java b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/entity/EquipementVirtuelEntity.java new file mode 100644 index 00000000..3fb7ec06 --- /dev/null +++ b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/entity/EquipementVirtuelEntity.java @@ -0,0 +1,33 @@ +package org.mte.numecoeval.expositiondonneesentrees.infrastructure.jpa.entity; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.SuperBuilder; + +@Getter +@Setter +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@Table(name = "EN_EQUIPEMENT_VIRTUEL") +@Entity +public class EquipementVirtuelEntity extends AbstractEntreeEntity { + @Id + @GeneratedValue(generator = "SEQ_EN_EQUIPEMENT_VIRTUEL", strategy = GenerationType.SEQUENCE) + @SequenceGenerator(name = "SEQ_EN_EQUIPEMENT_VIRTUEL", sequenceName = "SEQ_EN_EQUIPEMENT_VIRTUEL", allocationSize = 1000) + @Column(nullable = false) + private Long id; + private String nomEquipementVirtuel; + private String nomEquipementPhysique; + private String nomSourceDonneeEquipementPhysique; + private Integer vCPU; + private String cluster; + private String nomEntite; + private Double consoElecAnnuelle; + private String typeEqv; + private Double capaciteStockage; + private Double cleRepartition; +} diff --git a/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/entity/MessagerieEntity.java b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/entity/MessagerieEntity.java new file mode 100644 index 00000000..8682ad72 --- /dev/null +++ b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/entity/MessagerieEntity.java @@ -0,0 +1,37 @@ +package org.mte.numecoeval.expositiondonneesentrees.infrastructure.jpa.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.SequenceGenerator; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.SuperBuilder; + +@Getter +@Setter +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@Table(name = "EN_MESSAGERIE") +@Entity +public class MessagerieEntity extends AbstractEntreeEntity +{ + @Id + @GeneratedValue(generator = "SEQ_EN_MESSAGERIE", strategy = GenerationType.SEQUENCE) + @SequenceGenerator(name = "SEQ_EN_MESSAGERIE", sequenceName="SEQ_EN_MESSAGERIE",allocationSize=1000) + @Column(nullable = false) + private Long id; + private Integer volumeTotalMailEmis; + private Integer nombreMailEmis; + private Integer nombreMailEmisXDestinataires; + private Integer moisAnnee; + private String nomEntite; + + +} diff --git a/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/repository/ApplicationRepository.java b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/repository/ApplicationRepository.java new file mode 100644 index 00000000..e56779c1 --- /dev/null +++ b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/repository/ApplicationRepository.java @@ -0,0 +1,31 @@ +package org.mte.numecoeval.expositiondonneesentrees.infrastructure.jpa.repository; + +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.jpa.entity.ApplicationEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface ApplicationRepository extends JpaRepository<ApplicationEntity, Long> { + @Query(value = """ + SELECT nom_application FROM en_application ea + LEFT join en_equipement_physique eep ON ea.nom_equipement_physique = eep.nom_equipement_physique + WHERE ea.nom_lot = ?1 + AND (ea.nom_equipement_physique is null + OR eep.nom_equipement_physique is null) + ORDER BY nom_application + """, nativeQuery = true) + List<String> getApplicationSansEquipementPhysique(String nomLot); + + @Query(value = """ + SELECT nom_application FROM en_application ea + LEFT join en_equipement_virtuel eev ON ea.nom_equipement_virtuel = eev.nom_equipement_virtuel + WHERE ea.nom_lot = ?1 + AND (ea.nom_equipement_virtuel is null + OR eev.nom_equipement_virtuel is null) + ORDER BY nom_application + """, nativeQuery = true) + List<String> getApplicationSansEquipementVirtuel(String nomLot); +} diff --git a/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/repository/DataCenterRepository.java b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/repository/DataCenterRepository.java new file mode 100644 index 00000000..a7619180 --- /dev/null +++ b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/repository/DataCenterRepository.java @@ -0,0 +1,11 @@ +package org.mte.numecoeval.expositiondonneesentrees.infrastructure.jpa.repository; + +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.jpa.entity.DataCenterEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface DataCenterRepository extends JpaRepository<DataCenterEntity, Long> { + + long countByNomLot(String nomLot); +} diff --git a/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/repository/DonneesEntreesRepository.java b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/repository/DonneesEntreesRepository.java new file mode 100644 index 00000000..1e08aeec --- /dev/null +++ b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/repository/DonneesEntreesRepository.java @@ -0,0 +1,10 @@ +package org.mte.numecoeval.expositiondonneesentrees.infrastructure.jpa.repository; + +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.jpa.entity.DonneesEntreesEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface DonneesEntreesRepository extends JpaRepository<DonneesEntreesEntity,Long> +{ +} diff --git a/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/repository/EntiteRepository.java b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/repository/EntiteRepository.java new file mode 100644 index 00000000..25a62cf4 --- /dev/null +++ b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/repository/EntiteRepository.java @@ -0,0 +1,10 @@ +package org.mte.numecoeval.expositiondonneesentrees.infrastructure.jpa.repository; + +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.jpa.entity.EntiteEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface EntiteRepository extends JpaRepository<EntiteEntity,Long> +{ +} diff --git a/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/repository/EquipementPhysiqueRepository.java b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/repository/EquipementPhysiqueRepository.java new file mode 100644 index 00000000..b3d870be --- /dev/null +++ b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/repository/EquipementPhysiqueRepository.java @@ -0,0 +1,49 @@ +package org.mte.numecoeval.expositiondonneesentrees.infrastructure.jpa.repository; + +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.jpa.entity.EquipementPhysiqueEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Set; + +@Repository +public interface EquipementPhysiqueRepository extends JpaRepository<EquipementPhysiqueEntity, Long> { + + @Transactional + @Modifying + @Query(""" + UPDATE EquipementPhysiqueEntity eqp + SET eqp.statutTraitement = :statut + WHERE eqp.nomLot = :nomLot + AND eqp.nomOrganisation = :nomOrganisation + AND eqp.nomEquipementPhysique IN :noms + """) + int updateEquipementPhysiqueStatutTraitement( + @Param("statut") String statut, + @Param("nomLot") String nomLot, + @Param("nomOrganisation") String nomOrganisation, + @Param("noms") Set<String> noms + ); + + @Query("select id from #{#entityName} where nomLot = ?1 and statutTraitement = ?2") + List<Long> getIdsByNomLotAndStatutTraitement(String nomLot, String statutTraitement); + + @Query(value = """ + WITH datacenters AS ( + SELECT nom_court_datacenter FROM en_data_center WHERE nom_lot = ?1 + ) + SELECT nom_equipement_physique FROM en_equipement_physique eep + WHERE serveur = true + AND nom_lot = ?1 + AND (nom_court_datacenter is null OR + nom_court_datacenter NOT IN (SELECT nom_court_datacenter FROM datacenters)) + ORDER BY nom_equipement_physique + """, nativeQuery = true) + List<String> getEquipementPhysiqueSansDatacenter(String nomLot); + +} \ No newline at end of file diff --git a/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/repository/EquipementVirtuelRepository.java b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/repository/EquipementVirtuelRepository.java new file mode 100644 index 00000000..20fbe3a8 --- /dev/null +++ b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/repository/EquipementVirtuelRepository.java @@ -0,0 +1,22 @@ +package org.mte.numecoeval.expositiondonneesentrees.infrastructure.jpa.repository; + +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.jpa.entity.EquipementVirtuelEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface EquipementVirtuelRepository extends JpaRepository<EquipementVirtuelEntity, Long> { + + @Query(value = """ + SELECT nom_equipement_virtuel FROM en_equipement_virtuel eev + LEFT join en_equipement_physique eep ON eev.nom_equipement_physique = eep.nom_equipement_physique + WHERE eev.nom_lot = ?1 + AND (eev.nom_equipement_physique is null + OR eep.nom_equipement_physique is null) + ORDER BY nom_equipement_virtuel + """, nativeQuery = true) + List<String> getEquipementVirtuelSansEquipementPhysique(String nomLot); +} diff --git a/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/repository/MessagerieRepository.java b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/repository/MessagerieRepository.java new file mode 100644 index 00000000..f48d59d2 --- /dev/null +++ b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/repository/MessagerieRepository.java @@ -0,0 +1,14 @@ +package org.mte.numecoeval.expositiondonneesentrees.infrastructure.jpa.repository; + +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.jpa.entity.MessagerieEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface MessagerieRepository extends JpaRepository<MessagerieEntity, Long> { + @Query("select id from #{#entityName} where nomLot = ?1 and statutTraitement = ?2") + List<Long> getIdsByNomLotAndStatutTraitement(String nomLot, String statutTraitement); +} diff --git a/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/mapper/CalculRestMapper.java b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/mapper/CalculRestMapper.java new file mode 100644 index 00000000..9ccfe007 --- /dev/null +++ b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/mapper/CalculRestMapper.java @@ -0,0 +1,18 @@ +package org.mte.numecoeval.expositiondonneesentrees.infrastructure.mapper; + +import org.mapstruct.Mapper; +import org.mte.numecoeval.expositiondonneesentrees.domain.model.DemandeCalcul; +import org.mte.numecoeval.expositiondonneesentrees.domain.model.RapportDemandeCalcul; +import org.mte.numecoeval.expositiondonneesentrees.generated.api.model.DemandeCalculRest; +import org.mte.numecoeval.expositiondonneesentrees.generated.api.model.RapportDemandeCalculRest; +import org.mte.numecoeval.expositiondonneesentrees.sync.calculs.generated.api.model.ReponseCalculRest; + +@Mapper(componentModel = "spring") +public interface CalculRestMapper { + + DemandeCalcul toDomain(DemandeCalculRest restDTO); + + RapportDemandeCalculRest toRest(RapportDemandeCalcul domain); + + RapportDemandeCalcul toRest(ReponseCalculRest reponseCalculRest); +} diff --git a/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/mapper/DonneesEntreeRestMapper.java b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/mapper/DonneesEntreeRestMapper.java new file mode 100644 index 00000000..07d8cf9c --- /dev/null +++ b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/mapper/DonneesEntreeRestMapper.java @@ -0,0 +1,12 @@ +package org.mte.numecoeval.expositiondonneesentrees.infrastructure.mapper; + +import org.mapstruct.Mapper; +import org.mte.numecoeval.expositiondonneesentrees.domain.model.RapportImport; +import org.mte.numecoeval.expositiondonneesentrees.generated.api.model.RapportImportRest; + +@Mapper(componentModel = "spring") +public interface DonneesEntreeRestMapper { + + RapportImportRest toRestDTO(RapportImport rapportImport); + +} diff --git a/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/mapper/EntreeEntityMapper.java b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/mapper/EntreeEntityMapper.java new file mode 100644 index 00000000..4e2ee607 --- /dev/null +++ b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/mapper/EntreeEntityMapper.java @@ -0,0 +1,53 @@ +package org.mte.numecoeval.expositiondonneesentrees.infrastructure.mapper; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mte.numecoeval.expositiondonneesentrees.domain.model.Application; +import org.mte.numecoeval.expositiondonneesentrees.domain.model.DataCenter; +import org.mte.numecoeval.expositiondonneesentrees.domain.model.DonneesEntree; +import org.mte.numecoeval.expositiondonneesentrees.domain.model.Entite; +import org.mte.numecoeval.expositiondonneesentrees.domain.model.EquipementPhysique; +import org.mte.numecoeval.expositiondonneesentrees.domain.model.EquipementVirtuel; +import org.mte.numecoeval.expositiondonneesentrees.domain.model.Messagerie; +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.jpa.entity.ApplicationEntity; +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.jpa.entity.DataCenterEntity; +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.jpa.entity.DonneesEntreesEntity; +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.jpa.entity.EntiteEntity; +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.jpa.entity.EquipementPhysiqueEntity; +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.jpa.entity.EquipementVirtuelEntity; +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.jpa.entity.MessagerieEntity; + +import java.util.List; + +@Mapper(componentModel = "spring") +public interface EntreeEntityMapper { + + DonneesEntreesEntity toEntity(DonneesEntree domain); + DataCenterEntity toEntity(DataCenter domain); + EquipementPhysiqueEntity toEntity(EquipementPhysique domain); + @Mapping(target = "vCPU", source = "VCPU") + EquipementVirtuelEntity toEntity(EquipementVirtuel domain); + ApplicationEntity toEntity(Application domain); + MessagerieEntity toEntity(Messagerie domain); + EntiteEntity toEntity(Entite domain); + List<DataCenterEntity> toEntityListDataCenter(List<DataCenter> domains); + List<EquipementPhysiqueEntity> toEntityListEquipementPhysique(List<EquipementPhysique> domains); + List<EquipementVirtuelEntity> toEntityListEquipementVirtuel(List<EquipementVirtuel> domains); + List<ApplicationEntity> toEntityListApplication(List<Application> domains); + List<MessagerieEntity> toEntityListMessagerie(List<Messagerie> domains); + List<EntiteEntity> toEntityListEntite(List<Entite> domains); + + DonneesEntree toDomain(DonneesEntreesEntity entity); + DataCenter toDomain(DataCenterEntity entity); + EquipementPhysique toDomain(EquipementPhysiqueEntity entity); + EquipementVirtuel toDomain(EquipementVirtuelEntity entity); + Application toDomain(ApplicationEntity entity); + Messagerie toDomain(MessagerieEntity entity); + Entite toDomain(Entite entity); + List<DataCenter> toDomainListDataCenter(List<DataCenterEntity> entities); + List<EquipementPhysique> toDomainListEquipementPhysique(List<EquipementPhysiqueEntity> entities); + List<EquipementVirtuel> toDomainListEquipementVirtuel(List<EquipementVirtuelEntity> entities); + List<Application> toDomainListApplication(List<ApplicationEntity> entities); + List<Messagerie> toDomainListMessagerie(List<MessagerieEntity> entities); + List<Entite> toDomainListEntite(List<EntiteEntity> entities); +} diff --git a/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/mapper/VolumeMapper.java b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/mapper/VolumeMapper.java new file mode 100644 index 00000000..99854023 --- /dev/null +++ b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/mapper/VolumeMapper.java @@ -0,0 +1,11 @@ +package org.mte.numecoeval.expositiondonneesentrees.infrastructure.mapper; + +import org.mapstruct.Mapper; +import org.mte.numecoeval.expositiondonneesentrees.domain.model.Volume; +import org.mte.numecoeval.expositiondonneesentrees.generated.api.model.VolumeRest; + +@Mapper(componentModel = "spring") +public interface VolumeMapper { + + VolumeRest toRest(Volume volume); +} diff --git a/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/service/ErrorManagementPostSaveService.java b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/service/ErrorManagementPostSaveService.java new file mode 100644 index 00000000..e10fe962 --- /dev/null +++ b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/service/ErrorManagementPostSaveService.java @@ -0,0 +1,54 @@ +package org.mte.numecoeval.expositiondonneesentrees.infrastructure.service; + +import lombok.AllArgsConstructor; +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.config.MessageProperties; +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.jpa.repository.ApplicationRepository; +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.jpa.repository.EquipementPhysiqueRepository; +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.jpa.repository.EquipementVirtuelRepository; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; + +@Service +@AllArgsConstructor +public class ErrorManagementPostSaveService { + + final MessageProperties messageProperties; + + final EquipementPhysiqueRepository equipementPhysiqueRepository; + final EquipementVirtuelRepository equipementVirtuelRepository; + final ApplicationRepository applicationRepository; + + public List<String> checkEquipementPhysiques(String nomLot) { + // CA 3.2 + return equipementPhysiqueRepository.getEquipementPhysiqueSansDatacenter(nomLot).stream() + .map(equipementPhysique -> messageProperties.getMessages().get("EQUIPEMENT_DATACENTER_INCONNU").formatted(equipementPhysique)) + .toList(); + } + + public List<String> checkEquipementVirtuels(String nomLot) { + // CA 3.3 + return equipementVirtuelRepository.getEquipementVirtuelSansEquipementPhysique(nomLot).stream() + .map(equipementVirtuel -> messageProperties.getMessages().get("EQUIPEMENT_VIRTUEL_INCONNU").formatted(equipementVirtuel)) + .toList(); + } + + public List<String> checkApplications(String nomLot) { + var result = new ArrayList<String>(); + + // CA 3.4.1 + result.addAll(applicationRepository.getApplicationSansEquipementPhysique(nomLot).stream() + .map(equipementVirtuel -> messageProperties.getMessages().get("APPLICATION_AVEC_EQUIPEMENT_PHYSIQUE_INCONNU").formatted(equipementVirtuel)) + .toList()); + + // CA 3.4.2 + result.addAll(applicationRepository.getApplicationSansEquipementPhysique(nomLot).stream() + .map(equipementVirtuel -> messageProperties.getMessages().get("APPLICATION_AVEC_EQUIPEMENT_VIRTUEL_INCONNU").formatted(equipementVirtuel)) + .toList()); + + return result; + } + + +} diff --git a/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/service/ErrorManagementService.java b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/service/ErrorManagementService.java new file mode 100644 index 00000000..952379b7 --- /dev/null +++ b/services/api-expositiondonneesentrees/src/main/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/service/ErrorManagementService.java @@ -0,0 +1,114 @@ +package org.mte.numecoeval.expositiondonneesentrees.infrastructure.service; + +import lombok.AllArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.Pair; +import org.mte.numecoeval.expositiondonneesentrees.domain.model.DataCenter; +import org.mte.numecoeval.expositiondonneesentrees.domain.model.EquipementPhysique; +import org.mte.numecoeval.expositiondonneesentrees.domain.ports.output.ReferentielServicePort; +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.config.MessageProperties; +import org.mte.numecoeval.expositiondonneesentrees.referentiels.generated.api.model.CorrespondanceRefEquipementDTO; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; + + +@Service +@AllArgsConstructor +public class ErrorManagementService { + + final MessageProperties messageProperties; + + final ReferentielServicePort referentielServicePort; + + @Value("#{'${constraints.mode-utilisation}'.split(',')}") + private List<String> modeUtilisationList; + + /** + * Vérifie si la localisation du datacenter existe dans la table ref_MixElec.pays + * + * @param dataCenter le datacenter + * @return la liste d'erreur + */ + public List<String> checkDataCenter(DataCenter dataCenter) { + var result = new ArrayList<String>(); + + // CA 1.3 + if (!referentielServicePort.hasMixElec(dataCenter.getLocalisation())) { + result.add(messageProperties.getMessages().get("DATACENTER_MIX_ELEC_LOCALISATION_INCONNUE").formatted(dataCenter.getLocalisation(), dataCenter.getNomLongDatacenter())); + } + + return result; + } + + /** + * Vérifie un equipement physique + * + * @param equipementPhysique l'equipement physique + * @param refEquipementParDefaut la reference d'equipement par default + * @return la paire (erreurs, avertissements) + */ + public Pair<List<String>, List<String>> checkEquipementPhysique(EquipementPhysique equipementPhysique, String refEquipementParDefaut) { + var erreurs = new ArrayList<String>(); + + // CA 1.2 + // L'ajout d'un équipement physique dont le pays d'utilisation n'existe pas dans la table ref_MixElec.pays sort l'erreur suivante dans le rapport de contrôle + if (StringUtils.isNotBlank(equipementPhysique.getPaysDUtilisation()) && + !referentielServicePort.hasMixElec(equipementPhysique.getPaysDUtilisation())) { + + erreurs.add(messageProperties.getMessages().get("EQUIPEMENT_MIX_ELEC_LOCALISATION_INCONNUE").formatted(equipementPhysique.getPaysDUtilisation(), equipementPhysique.getNomEquipementPhysique())); + } + + String refEquipement = refEquipementParDefaut; + + // CA 1.5 + // L'ajout d'un équipement physique dont le type d'équipement ne possède pas de refEquipementParDefaut et dont le Modele n'est pas présent dans la table des ref_CorrespondanceRefEqP sort l'erreur suivante dans le rapport de contrôle + if (StringUtils.isBlank(refEquipementParDefaut)) { + CorrespondanceRefEquipementDTO refCorrespondance = referentielServicePort.getCorrespondance(equipementPhysique.getModele()); + if (refCorrespondance == null) { + erreurs.add(messageProperties.getMessages().get("EQUIPEMENT_CORRESPONDANCE_INCONNUE").formatted(equipementPhysique.getNomEquipementPhysique(), equipementPhysique.getType())); + } else { + refEquipement = refCorrespondance.getRefEquipementCible(); + } + } + + // CA 2.1 + // L'ajout d'un équipement physique dont la référence d'impact (déterminée à partir de la table de correspondance ou à partir de la table type équipement) est nulle sort un warning + var avertissements = new ArrayList<String>(); + + var etapes = referentielServicePort.getAllEtapes(); + var criteres = referentielServicePort.getAllCriteres(); + for (var critere : criteres) { + for (var etape : etapes) { + var impact = referentielServicePort.getImpactEquipement(refEquipement, critere.getNomCritere(), etape.getCode()); + if (impact == null) { + avertissements.add(messageProperties.getMessages().get("EQUIPEMENT_IMPACT_INCONNU").formatted(refEquipement, etape.getCode(), critere.getNomCritere())); + } + } + } + + // CA 3.1 + // L'ajout d'un équipement dont le date de retrait (equipementPhysique.DateRetrait) précède la date d'achat (equipementPhysique.DateAchat) + if (equipementPhysique.getDateAchat() != null && equipementPhysique.getDateRetrait() != null && + equipementPhysique.getDateAchat().isBefore(equipementPhysique.getDateRetrait())) { + + erreurs.add(messageProperties.getMessages().get("EQUIPEMENT_DATE_INCOHERENTE").formatted(equipementPhysique.getNomEquipementPhysique())); + } + //CA 4.1 + //L'ajout d'un équipement dont le mode d'utilisation est autre que COPE, BYOD ou null + if (equipementPhysique.getModeUtilisation() != null && !modeUtilisationList.contains(equipementPhysique.getModeUtilisation())) { + avertissements.add(messageProperties.getMessages().get("EQUIPEMENT_MODE_UTILISATION_INCONNU").formatted(equipementPhysique.getModeUtilisation())); + } + //CA 5.1 + //L'ajout d'un équipement dont le taux d'utilisation n'est pas compris entre 0 et 1 + Double taux = equipementPhysique.getTauxUtilisation(); + if (taux != null && (taux < 0 || taux > 1)) { + avertissements.add(messageProperties.getMessages().get("EQUIPEMENT_TAUX_UTILISATION_INVALIDE").formatted(taux)); + } + + return Pair.of(erreurs, avertissements); + } + +} diff --git a/services/api-expositiondonneesentrees/src/main/resources/application.yaml b/services/api-expositiondonneesentrees/src/main/resources/application.yaml new file mode 100644 index 00000000..c41b811a --- /dev/null +++ b/services/api-expositiondonneesentrees/src/main/resources/application.yaml @@ -0,0 +1,92 @@ +# Application +numecoeval: + urls: + allowed: "http://localhost" + referentiel: + server: + url: "http://localhost:18080" + calculs: + server: + url: "http://localhost:18085" + +# Serveur Web +server: + port: 18081 + shutdown: graceful + tomcat: + mbeanregistry: + enabled: true + +# Actuator +management: + server: + port: 18081 + security: + user: + name: "actuator" + password: "NotReally@GoodPasswordForActuator" + roles: ACTUATOR_ADMIN + endpoints: + web: + base-path: / + exposure: + include: health,prometheus,httptrace,info,metrics,mappings + +#CONFIGURATION BASES +spring: + sql: + init: + mode: always + # Base de données + datasource: + generate-unique-name: true + url: "jdbc:postgresql://localhost:5432/postgres?reWriteBatchedInserts=true" + username: postgres + password: postgres + driver-class-name: org.postgresql.Driver + tomcat: + test-on-borrow: false + jmx-enabled: false + max-active: 100 + jpa: + # POSTGRES + generate-ddl: true + database-platform: org.hibernate.dialect.PostgreSQLDialect + hibernate: + ddl-auto: none + show-sql: false + properties: + hibernate: + order_inserts: true + jdbc: + batch_size: 1000 + generate_statistics: false + open-in-view: true + # Taille des uploads et des requêtes + servlet: + multipart: + max-request-size: "200MB" + max-file-size: "50MB" + +caching: + spring: + statutDesCalculs: "PT10S" + referentiels: "PT10M" + +messages: + LIGNE_INCONSISTENTE: "Fichier %s : La ligne n°%d n'est pas consistente avec les headers du fichier" + LIGNE_INCORRECTE: "Fichier %s : La ligne n°%d est incorrecte pour l'objet %s requise" + TYPE_EQUIPEMENT_INCONNU: "Le type d'équipement %s correspondant à l'équipement %s n'existe pas dans la table des références ref_TypeEquipement" + EQUIPEMENT_MIX_ELEC_LOCALISATION_INCONNUE: "Le pays %s correspondant à l'équipement physique %s n'existe pas dans la table des mix électriques ref_MixElec.pays" + DATACENTER_MIX_ELEC_LOCALISATION_INCONNUE: "La localisation %s du centre de données %s n'existe pas dans la table des mix électriques ref_MixElec.pays" + EQUIPEMENT_CORRESPONDANCE_INCONNUE: "L'équipement %s de type %s ne possède pas de référence d'équipement par défaut dans la table ref_typeEquipement et pas de correspondance dans la table ref_CorrespondanceRefEqP" + EQUIPEMENT_IMPACT_INCONNU: "L'impact de l'équipement de référence %s pour l'étape %s et le critère %s ne pourra pas être calculé en raison de l'absence de facteur d'impact sur l'équipement" + EQUIPEMENT_DATE_INCOHERENTE: "L'âge de l'équipement %s ne peut être calculé car sa date de retrait précède sa date d'achat" + EQUIPEMENT_MODE_UTILISATION_INCONNU: "Le mode d'utilisation renseigné '%s' est inconnu du référentiel" + EQUIPEMENT_TAUX_UTILISATION_INVALIDE: "Le taux d'utilisation renseigné '%s' n'est pas valide, il doit être compris entre 0 et 1, le taux par défaut sera appliqué." + EQUIPEMENT_DATACENTER_INCONNU: "L'équipement %s n'est lié à aucun datacenter" + EQUIPEMENT_VIRTUEL_INCONNU: "L'équipement virtuel %s n'est supporté par aucun équipement physique" + APPLICATION_AVEC_EQUIPEMENT_PHYSIQUE_INCONNU: "L'application %s n'est supporté par aucun équipement physique" + APPLICATION_AVEC_EQUIPEMENT_VIRTUEL_INCONNU: "L'application %s n'est supporté par aucun équipement virtuel" +constraints: + mode-utilisation: "BYOD,COPE" diff --git a/services/api-expositiondonneesentrees/src/main/resources/logback.xml b/services/api-expositiondonneesentrees/src/main/resources/logback.xml new file mode 100644 index 00000000..fea45667 --- /dev/null +++ b/services/api-expositiondonneesentrees/src/main/resources/logback.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> +<configuration> + <!-- Spring default.xml --> + <conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter"/> + <conversionRule conversionWord="wex" + converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter"/> + <conversionRule conversionWord="wEx" + converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter"/> + + <property name="CONSOLE_LOG_PATTERN" + value="${CONSOLE_LOG_PATTERN:-%clr(%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/> + <property name="CONSOLE_LOG_CHARSET" value="${CONSOLE_LOG_CHARSET:-${file.encoding:-UTF-8}}"/> + + <!-- console-appender.xml--> + <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> + <encoder> + <pattern>${CONSOLE_LOG_PATTERN}</pattern> + <charset>${CONSOLE_LOG_CHARSET}</charset> + </encoder> + </appender> + <statusListener class="ch.qos.logback.core.status.NopStatusListener"/> + <root level="${APP_LOGGING_LEVEL:-INFO}"> + <appender-ref ref="CONSOLE"/> + </root> + <logger name="org.springframework.web" level="INFO"/> +</configuration> \ No newline at end of file diff --git a/services/api-expositiondonneesentrees/src/main/resources/schema.sql b/services/api-expositiondonneesentrees/src/main/resources/schema.sql new file mode 100644 index 00000000..a6b627e6 --- /dev/null +++ b/services/api-expositiondonneesentrees/src/main/resources/schema.sql @@ -0,0 +1,176 @@ +CREATE TABLE IF NOT EXISTS en_donnees_entrees +( + id int8 NOT NULL, + date_update timestamp NULL, + date_creation timestamp NULL, + date_lot date NULL, + nom_organisation varchar(255) NULL, + nom_lot varchar(255) NULL, + nbr_applications int8 NULL, + nbr_data_center int8 NULL, + nbr_equipements_physiques int8 NULL, + nbr_equipements_virtuels int8 NULL, + nbr_messageries int8 NULL, + CONSTRAINT en_donnees_entrees_pkey PRIMARY KEY (id) +); + +CREATE TABLE IF NOT EXISTS en_data_center +( + id int8 NOT NULL, + date_creation timestamp NULL, + date_update timestamp NULL, + date_lot date NULL, + nom_lot varchar(255) NULL, + nom_organisation varchar(255) NULL, + nom_source_donnee varchar(255) NULL, + localisation varchar(255) NULL, + nom_court_datacenter varchar(255) NULL, + nom_entite varchar(255) NULL, + nom_long_datacenter varchar(255) NULL, + pue float8 NULL, + CONSTRAINT en_data_center_pkey PRIMARY KEY (id) +); + +CREATE TABLE IF NOT EXISTS en_equipement_physique +( + id int8 NOT NULL, + date_creation timestamp NULL, + date_update timestamp NULL, + date_lot date NULL, + nom_lot varchar(255) NULL, + nom_organisation varchar(255) NULL, + nom_source_donnee varchar(255) NULL, + conso_elec_annuelle float8 NULL, + date_achat date NULL, + date_retrait date NULL, + duree_vie_defaut float8 NULL, + go_telecharge float4 NULL, + modele varchar(255) NULL, + nb_coeur varchar(255) NULL, + nb_jour_utilise_an float8 NULL, + nom_court_datacenter varchar(255) NULL, + nom_entite varchar(255) NULL, + nom_equipement_physique varchar(255) NULL, + pays_utilisation varchar(255) NULL, + quantite float8 NULL, + serveur bool NOT NULL, + statut varchar(255) NULL, + "type" varchar(255) NULL, + utilisateur varchar(255) NULL, + mode_utilisation varchar(255) NULL, + taux_utilisation float8 NULL, + CONSTRAINT en_equipement_physique_pkey PRIMARY KEY (id) +); + +CREATE TABLE IF NOT EXISTS en_equipement_virtuel +( + id int8 NOT NULL, + date_creation timestamp NULL, + date_lot date NULL, + nom_lot varchar(255) NULL, + nom_organisation varchar(255) NULL, + nom_source_donnee varchar(255) NULL, + "cluster" varchar(255) NULL, + nom_entite varchar(255) NULL, + nom_equipement_physique varchar(255) NULL, + nom_equipement_virtuel varchar(255) NULL, + vcpu int4 NULL, + CONSTRAINT en_equipement_virtuel_pkey PRIMARY KEY (id) +); + +CREATE TABLE IF NOT EXISTS en_application +( + id int8 NOT NULL, + date_creation timestamp NULL, + date_lot date NULL, + nom_lot varchar(255) NULL, + nom_organisation varchar(255) NULL, + nom_source_donnee varchar(255) NULL, + domaine varchar(255) NULL, + nom_application varchar(255) NULL, + nom_entite varchar(255) NULL, + nom_equipement_virtuel varchar(255) NULL, + nom_equipement_physique varchar(255) NULL, + sous_domaine varchar(255) NULL, + type_environnement varchar(255) NULL, + CONSTRAINT en_application_pkey PRIMARY KEY (id) +); + +CREATE TABLE IF NOT EXISTS en_messagerie +( + id int8 NOT NULL, + date_creation timestamp NULL, + date_update timestamp NULL, + date_lot date NULL, + nom_lot varchar(255) NULL, + nom_organisation varchar(255) NULL, + nom_source_donnee varchar(255) NULL, + mois_annee int4 NULL, + nom_entite varchar(255) NULL, + nombre_mail_emis int4 NULL, + nombre_mail_emisxdestinataires int4 NULL, + volume_total_mail_emis int4 NULL, + CONSTRAINT en_messagerie_pkey PRIMARY KEY (id) +); + +CREATE TABLE IF NOT EXISTS en_entite +( + id int8 NOT NULL, + date_creation timestamp NULL, + date_update timestamp NULL, + date_lot date NULL, + nom_lot varchar(255) NULL, + nom_organisation varchar(255) NULL, + nom_source_donnee varchar(255) NULL, + nom_entite varchar(255) NULL, + nb_collaborateurs int4 NULL, + responsable_entite varchar(255) NULL, + responsable_numerique_durable varchar(255) NULL, + CONSTRAINT en_entite_pkey PRIMARY KEY (id) +); + +CREATE SEQUENCE IF NOT EXISTS seq_en_entite INCREMENT BY 1000; + +ALTER TABLE IF EXISTS en_donnees_entrees ADD COLUMN IF NOT EXISTS statut_traitement varchar(255); +ALTER TABLE IF EXISTS en_data_center ADD COLUMN IF NOT EXISTS statut_traitement varchar(255); +ALTER TABLE IF EXISTS en_equipement_physique ADD COLUMN IF NOT EXISTS statut_traitement varchar(255); +ALTER TABLE IF EXISTS en_equipement_virtuel ADD COLUMN IF NOT EXISTS statut_traitement varchar(255); +ALTER TABLE IF EXISTS en_application ADD COLUMN IF NOT EXISTS statut_traitement varchar(255); +ALTER TABLE IF EXISTS en_messagerie ADD COLUMN IF NOT EXISTS statut_traitement varchar(255); + +ALTER TABLE IF EXISTS en_donnees_entrees ADD COLUMN IF NOT EXISTS date_update timestamp; +ALTER TABLE IF EXISTS en_data_center ADD COLUMN IF NOT EXISTS date_update timestamp; +ALTER TABLE IF EXISTS en_equipement_physique ADD COLUMN IF NOT EXISTS date_update timestamp; +ALTER TABLE IF EXISTS en_equipement_virtuel ADD COLUMN IF NOT EXISTS date_update timestamp; +ALTER TABLE IF EXISTS en_application ADD COLUMN IF NOT EXISTS date_update timestamp; +ALTER TABLE IF EXISTS en_messagerie ADD COLUMN IF NOT EXISTS date_update timestamp; + +-- Taiga#937 - Impacts - La source de l'équipement virtuel peut être différente de celle de l'équipement physique +ALTER TABLE IF EXISTS en_equipement_virtuel + ADD COLUMN IF NOT EXISTS nom_source_donnee_equipement_physique varchar(255); + +-- Taiga#937 - Impacts - La source de l'instance d'application peut être différente de celle de l'équipement virtuel +ALTER TABLE IF EXISTS en_application + ADD COLUMN IF NOT EXISTS nom_source_donnee_equipement_virtuel varchar(255); + +-- Taiga#457 - Impacts - Ajout des colonnes conso_elec_annuelle, type_eqv, capacite_stockage et cle_repartition +ALTER TABLE IF EXISTS en_equipement_virtuel ADD COLUMN IF NOT EXISTS conso_elec_annuelle float8; +ALTER TABLE IF EXISTS en_equipement_virtuel ADD COLUMN IF NOT EXISTS type_eqv varchar(255); +ALTER TABLE IF EXISTS en_equipement_virtuel ADD COLUMN IF NOT EXISTS capacite_stockage float8; +ALTER TABLE IF EXISTS en_equipement_virtuel ADD COLUMN IF NOT EXISTS cle_repartition float8; +-- Taiga#457 - Impacts - Ajout de la colonne pour la capacité totale de stockage +ALTER TABLE IF EXISTS en_equipement_physique ADD COLUMN IF NOT EXISTS stockage_total_virtuel float8; +ALTER TABLE IF EXISTS en_equipement_physique ADD COLUMN IF NOT EXISTS mode_utilisation varchar(255); +ALTER TABLE IF EXISTS en_equipement_physique ADD COLUMN IF NOT EXISTS taux_utilisation float8; + +-- Creation indexes +-- Accelere la recuperation des donnees depuis EquipementPhysiqueIntegrationConfig.java +CREATE INDEX IF NOT EXISTS idx_en_equipement_physique_statut_traitement ON en_equipement_physique (statut_traitement); +-- Accelere le count du volume des calculs - api-expositiondonneesentrees +CREATE INDEX IF NOT EXISTS idx_en_equipement_physique_nom_lot ON en_equipement_physique (nom_lot); + +-- Indexes pour api-event-calculs +-- Accelere la recuperation des equipements virtuels d'un equipement physique +CREATE INDEX IF NOT EXISTS idx_en_equipement_virtuel_nom_equipement_physique ON en_equipement_virtuel (nom_equipement_physique); +-- Accelere la recuperation des applications d'un equipement virtuel +CREATE INDEX IF NOT EXISTS idx_en_application_nom_equipement_physique_et_virtuel ON en_application (nom_equipement_physique, nom_equipement_virtuel); diff --git a/services/api-expositiondonneesentrees/src/main/resources/static/openapi.yaml b/services/api-expositiondonneesentrees/src/main/resources/static/openapi.yaml new file mode 100644 index 00000000..9cd38d0d --- /dev/null +++ b/services/api-expositiondonneesentrees/src/main/resources/static/openapi.yaml @@ -0,0 +1,575 @@ +openapi: 3.0.1 +info: + title: NumEcoEval - API données d'entrées + version: v0 + license: + name: Apache 2.0 + description: | + API permettant l'import de données dans le système NumEcoEval. \ + L'import de données seul ne permet pas d'importer et impose de soumettre la demande de calcul après l'import des données. +tags: + - name: Imports + description: Endpoints liés à l'import de données dans NumEcoEval + - name: Calculs + description: Endpoints liés au déclenchement de calcul dans NumEcoEval +paths: + /entrees/calculs/statut: + get: + summary: Endpoint de récupération du statut des calculs. + description: | + Endpoint de récupération du statut des calculs. + tags: + - Calculs + operationId: statutPourCalcul + parameters: + - in: query + name: nomLot + schema: + type: string + required: true + description: Nom lot + - in: query + name: nomOrganisation + schema: + type: string + required: true + description: Nom Organisation + responses: + "500": + description: Erreur interne du service + content: + 'application/json': + schema: + $ref: "#/components/schemas/ErreurRest" + "400": + description: Contenu de la soumission incorrecte + content: + 'application/json': + schema: + $ref: "#/components/schemas/ErreurRest" + "200": + description: Rapport d'import des données d'entrées + content: + 'application/json': + schema: + $ref: "#/components/schemas/StatutCalculRest" + /entrees/calculs/soumission: + post: + summary: Endpoint de soumission de données d'entrées pour lancer les calculs dans NumEcoEval. + description: | + Endpoint de soumission de données d'entrées pour lancer les calculs dans NumEcoEval. + tags: + - Calculs + operationId: soumissionPourCalcul + parameters: + - in: query + name: mode + schema: + $ref: "#/components/schemas/ModeRest" + required: false + description: Mode de traitement, SYNC ou ASYNC + requestBody: + required: true + content: + 'application/json': + schema: + $ref: "#/components/schemas/DemandeCalculRest" + responses: + "500": + description: Erreur interne du service + content: + 'application/json': + schema: + $ref: "#/components/schemas/ErreurRest" + "400": + description: Contenu de la soumission incorrecte + content: + 'application/json': + schema: + $ref: "#/components/schemas/ErreurRest" + "200": + description: Rapport d'import des données d'entrées + content: + 'application/json': + schema: + $ref: "#/components/schemas/RapportDemandeCalculRest" + /entrees/calculs/rejeu: + post: + summary: Endpoint de rejeu des calculs à partir des données d'entrées pour relancer les calculs dans NumEcoEval. + description: | + Endpoint de rejeu des calculs à partir des données d'entrées pour relancer les calculs dans NumEcoEval. + tags: + - Calculs + operationId: rejeuCalcul + requestBody: + required: true + content: + 'application/json': + schema: + $ref: "#/components/schemas/DemandeCalculRest" + responses: + "500": + description: Erreur interne du service + content: + 'application/json': + schema: + $ref: "#/components/schemas/ErreurRest" + "400": + description: Contenu de la soumission incorrecte + content: + 'application/json': + schema: + $ref: "#/components/schemas/ErreurRest" + "200": + description: Rapport d'import des données d'entrées + content: + 'application/json': + schema: + $ref: "#/components/schemas/RapportDemandeCalculRest" + /entrees/json: + post: + tags: + - Imports + summary: Soumission de données d'entrées au format CSV pour calcul d'indicateurs + description: | + Endpoint de soumission des données d'entrées au format JSON pour le calcul d'indicateurs. \ + La taille totale de tout le contenu JSON ne doit pas dépasser 10 Mo. + operationId: importJson + requestBody: + required: true + content: + 'application/json': + schema: + $ref: "#/components/schemas/DonneesEntreeRest" + responses: + "500": + description: Erreur interne du service + content: + 'application/json': + schema: + $ref: "#/components/schemas/ErreurRest" + "400": + description: Contenu de la soumission incorrecte + content: + 'application/json': + schema: + $ref: "#/components/schemas/ErreurRest" + "200": + description: Rapport d'import des données d'entrées + content: + 'application/json': + schema: + type: array + items: + $ref: "#/components/schemas/RapportImportRest" + /entrees/csv: + post: + tags: + - Imports + summary: Soumission de données d'entrées au format CSV pour calcul d'indicateurs + description: | + Endpoint de soumission des données d'entrées au format CSV pour le calcul d'indicateurs. \ + La taille totale de tous les fichiers ne doit pas dépasser 10 Mo. \ + Le séparateur des fichiers CSV est le point-virgule (;). \ + Le Header du CSV des data centers est : nomCourtDatacenter;nomLongDatacenter;pue;localisation;nomSourceDonnee \ + Le Header du CSV des équipements physiques est : modele;quantite;nomEquipementPhysique;type;statut;paysDUtilisation;utilisateur;dateAchat;dateRetrait;nbCoeur;nomCourtDatacenter;goTelecharge;modeUtilisation;tauxUtilisation;nbJourUtiliseAn;consoElecAnnuelle;nomSourceDonnee \ + Le Header du CSV des équipements virtuels est : nomEquipementVirtuel;nomEquipementPhysique;vCPU;cluster;nomSourceDonnee;nomSourceEquipementPhysique;consoElecAnnuelle;typeEqv;capaciteStockage;cleRepartition \ + Le Header du CSV des application est : nomApplication;typeEnvironnement;nomEquipementVirtuel;nomEquipementPhysique;domaine;sousDomaine;nomSourceDonnee;typeEqv;nomSourceEquipementVirtuel;capaciteStockage;cleRepartition \ + Le Header du CSV de la messagerie est : nombreMailEmis;nombreMailEmisXDestinataires;volumeTotalMailEmis;MoisAnnee;nomSourceDonnee \ + Le Header du CSV des entités est : nomEntite;nbCollaborateurs;responsableEntite;responsableNumeriqueResponsable;nomSourceDonnee + + operationId: importCSV + requestBody: + content: + multipart/form-data: + schema: + type: object + required: + - nomLot + properties: + csvDataCenter: + type: string + format: binary + csvEquipementPhysique: + type: string + format: binary + csvEquipementVirtuel: + type: string + format: binary + csvApplication: + type: string + format: binary + csvMessagerie: + type: string + format: binary + csvEntite: + type: string + format: binary + dateLot: + type: string + nomOrganisation: + type: string + nomLot: + type: string + responses: + "500": + description: Erreur interne du service + content: + 'application/json': + schema: + $ref: "#/components/schemas/ErreurRest" + "400": + description: Contenu de la soumission incorrecte + content: + 'application/json': + schema: + $ref: "#/components/schemas/ErreurRest" + "200": + description: Rapport d'import des données d'entrées + content: + 'application/json': + schema: + type: array + items: + $ref: "#/components/schemas/RapportImportRest" +components: + schemas: + StatutTraitement: + description: Statut de traitement des données d'entrées dans NumEcoEval + type: string + enum: + - EN_ATTENTE + - A_INGERER + - INGERE + - TRAITE + - A_REJOUER + - EN_ERREUR + DemandeCalculRest: + description: Objet à soumettre pour une demande de calcul + required: + - nomLot + properties: + nomLot: + description: "Nom du lot rattaché" + type: string + ModeRest: + type: string + enum: + - ASYNC + - SYNC + default: ASYNC + StatutCalculRest: + description: Statut des calculs. + properties: + statut: + description: "Statut global des calculs" + type: string + enum: + - TERMINE + - EN_COURS + etat: + description: "Etat des calculs" + type: string + equipementPhysique: + description: "Bloc equipement physique" + $ref: '#/components/schemas/VolumeRest' + messagerie: + description: "Bloc messagerie" + $ref: '#/components/schemas/VolumeRest' + VolumeRest: + description: Volume + type: object + properties: + nbEnCours: + type: integer + nbTraite: + type: integer + RapportDemandeCalculRest: + description: Rapport de la demande de calcul. + properties: + nomLot: + description: "Nom du lot rattaché" + type: string + nbrDataCenter: + description: Nombre de Data Center concernés + type: integer + nbrEquipementPhysique: + description: Nombre d'équipements physiques concernés + type: integer + nbrEquipementVirtuel: + description: Nombre d'équipements physiques concernés + type: integer + nbrApplication: + description: Nombre d'application concernées + type: integer + nbrMessagerie: + description: Nombre d'éléments de messagerie concernés + type: integer + ErreurRest: + description: Objet standard pour les réponses en cas d'erreur d'API + type: object + properties: + code: + description: Code de l'erreur + type: string + message: + description: Message de l'erreur + type: string + status: + description: Code Statut HTTP de la réponse + type: integer + timestamp: + description: Date & Heure de l'erreur + type: string + format: date-time + RapportImportRest: + description: Rapport d'import pour un fichier + type: object + properties: + fichier: + description: Fichier concerné par le rapport + type: string + erreurs: + description: Erreurs du fichier + type: array + items: + type: string + avertissements: + description: Avertissements du fichier + type: array + items: + type: string + nbrLignesImportees: + description: Nombre de lignes importées + type: integer + DonneesEntreeRest: + description: Données d'entres de NumEcoEval + type: object + required: + - dateLot + - nomOrganisation + properties: + nomOrganisation: + description: Nom de l'organisation rattachée au données + type: string + nomLot: + description: "Nom du lot rattaché" + type: string + dateLot: + description: Date de lot rattachée au données + type: string + dataCenters: + type: array + items: + $ref: "#/components/schemas/DataCenterRest" + equipementsPhysiques: + type: array + items: + $ref: "#/components/schemas/EquipementPhysiqueRest" + messageries: + type: array + items: + $ref: "#/components/schemas/MessagerieRest" + entites: + type: array + items: + $ref: "#/components/schemas/EntiteRest" + DataCenterRest: + description: Représentation d'un DataCenter dans NumEcoEval + properties: + nomCourtDatacenter: + description: "" + type: string + nomLongDatacenter: + description: "" + type: string + pue: + description: "" + type: number + localisation: + description: "" + type: string + nomEntite: + description: "" + type: string + nomSourceDonnee: + description: "Nom de la source de la donnée" + type: string + EquipementPhysiqueRest: + description: Représentation d'un équipement physique dans NumEcoEval + properties: + nomEquipementPhysique: + description: "" + type: string + modele: + description: "" + type: string + type: + description: "" + type: string + statut: + description: "" + type: string + paysDUtilisation: + description: "" + type: string + utilisateur: + description: "" + type: string + dateAchat: + description: "" + type: string + format: date + dateRetrait: + description: "" + type: string + format: date + nbCoeur: + description: "" + type: string + modeUtilisation: + description: "" + type: string + tauxUtilisation: + description: "" + type: number + nomCourtDatacenter: + description: "" + type: string + nbJourUtiliseAn: + description: "" + type: number + goTelecharge: + description: "" + type: number + quantite: + description: "" + type: number + serveur: + description: "" + type: boolean + nomEntite: + description: "" + type: string + nomSourceDonnee: + description: "Nom de la source de la donnée" + type: string + equipementsVirtuels: + description: "" + items: + $ref: "#/components/schemas/EquipementVirtuelRest" + EquipementVirtuelRest: + description: Représentation d'un équipement virtuel dans NumEcoEval + properties: + nomEquipementVirtuel: + description: "" + type: string + nomEquipementPhysique: + description: "" + type: string + vCPU: + description: "" + type: integer + cluster: + description: "" + type: string + nomEntite: + description: "" + type: string + nomSourceDonnee: + description: "Nom de la source de la donnée" + type: string + nomSourceDonneeEquipementPhysique: + description: "Nom de la source de la donnée pour l'équipement physique" + type: string + applications: + description: "" + type: array + items: + $ref: "#/components/schemas/ApplicationRest" + typeEqv: + description: | + Le type d'équipement virtuel contient "calcul", "stockage" ou null. + Peut être omis entièrement si ce n'est pas applicable. + type: string + capaciteStockage: + description: | + Capacité de stockage de l'équipement virtuel en To. + type: number + format: double + cleRepartition: + description: | + La clé de repartition est exprimée comme une fraction. + type: number + format: double + consoElecAnnuelle: + description: | + Consommation électrique annuelle de l'équipement virtuel. + type: number + format: double + ApplicationRest: + description: Représentation d'une application dans NumEcoEval + properties: + nomApplication: + description: "" + type: string + typeEnvironnement: + description: "" + type: string + nomEquipementVirtuel: + description: "" + type: string + domaine: + description: "" + type: string + sousDomaine: + description: "" + type: string + nomEntite: + description: "" + type: string + nomSourceDonnee: + description: "Nom de la source de la donnée" + type: string + nomSourceDonneeEquipementVirtuel: + description: "Nom de la source de la donnée pour l'équipement virtuel" + type: string + MessagerieRest: + description: Représentation d'éléments de messagerie dans NumEcoEval + properties: + nombreMailEmis: + description: "" + type: integer + nombreMailEmisXDestinataires: + description: "" + type: integer + volumeTotalMailEmis: + description: "" + type: integer + moisAnnee: + description: "Mois et Année rattachés au données, format MMAAAA" + type: integer + nomEntite: + description: "" + type: string + nomSourceDonnee: + description: "Nom de la source de la donnée" + type: string + EntiteRest: + description: Représentation d'une entité dans NumEcoEval + properties: + nomEntite: + description: "Nom de l'entité" + type: string + nbCollaborateurs: + description: "Nombre de collaborateur de l'entité" + type: integer + responsableEntite: + description: "Nom du responsable de l'entité" + type: string + responsableNumeriqueDurable: + description: "Nom du responsable du numérique durable dans l'entité" + type: string + nomSourceDonnee: + description: "Nom de la source de la donnée" + type: string \ No newline at end of file diff --git a/services/api-expositiondonneesentrees/src/test/java/org/mte/numecoeval/expositiondonneesentrees/domain/port/input/ImportDonneesEntreePortImplTest.java b/services/api-expositiondonneesentrees/src/test/java/org/mte/numecoeval/expositiondonneesentrees/domain/port/input/ImportDonneesEntreePortImplTest.java new file mode 100644 index 00000000..0bdb213e --- /dev/null +++ b/services/api-expositiondonneesentrees/src/test/java/org/mte/numecoeval/expositiondonneesentrees/domain/port/input/ImportDonneesEntreePortImplTest.java @@ -0,0 +1,160 @@ +package org.mte.numecoeval.expositiondonneesentrees.domain.port.input; + +import org.apache.commons.lang3.tuple.Pair; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.mte.numecoeval.expositiondonneesentrees.domain.model.*; +import org.mte.numecoeval.expositiondonneesentrees.domain.ports.input.impl.ImportDonneesEntreePortImpl; +import org.mte.numecoeval.expositiondonneesentrees.domain.ports.output.ReferentielServicePort; +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.service.ErrorManagementService; +import org.springframework.web.multipart.MultipartFile; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ImportDonneesEntreePortImplTest { + @Mock + MultipartFile fileToRead; + + @Mock + ReferentielServicePort referentielServicePort; + + Map<String, String> errorMessages = Map.of(); + + ImportDonneesEntreePortImpl importDonneesEntreePort; + ErrorManagementService errorManagementService; + + @BeforeEach + public void init() { + MockitoAnnotations.openMocks(this); + importDonneesEntreePort = new ImportDonneesEntreePortImpl(referentielServicePort, errorManagementService, errorMessages); + } + + @Test + void importDataCenter_onIOException_shouldReturnReportWith1Error() throws IOException { + Mockito.when(fileToRead.getInputStream()).thenThrow(new IOException("Test")); + + Pair<RapportImport, List<DataCenter>> resultImport = importDonneesEntreePort.importDataCenter(null, null, null, fileToRead); + + assertTrue(resultImport.getValue().isEmpty()); + assertEquals("Le fichier CSV des DataCenter n'est pas lisible par le système.", resultImport.getKey().getErreurs().get(0)); + } + + @Test + void importDataCenter_onFileNotFoundException_shouldReturnReportWith1Error() throws IOException { + Mockito.when(fileToRead.getInputStream()).thenThrow(new FileNotFoundException("Test")); + + Pair<RapportImport, List<DataCenter>> resultImport = importDonneesEntreePort.importDataCenter(null, null, null, fileToRead); + + assertTrue(resultImport.getValue().isEmpty()); + assertEquals("Le fichier CSV des DataCenters n'est pas trouvable.", resultImport.getKey().getErreurs().get(0)); + } + + @Test + void importEquipementsPhysiques_onIOException_shouldReturnReportWith1Error() throws IOException { + Mockito.when(fileToRead.getInputStream()).thenThrow(new IOException("Test")); + + Pair<RapportImport, List<EquipementPhysique>> resultImport = importDonneesEntreePort.importEquipementsPhysiques(null, null, null, fileToRead); + + assertTrue(resultImport.getValue().isEmpty()); + assertEquals("Le fichier CSV des équipements physiques n'est pas lisible par le système.", resultImport.getKey().getErreurs().get(0)); + } + + @Test + void importEquipementsPhysiques_onFileNotFoundException_shouldReturnReportWith1Error() throws IOException { + Mockito.when(fileToRead.getInputStream()).thenThrow(new FileNotFoundException("Test")); + + Pair<RapportImport, List<EquipementPhysique>> resultImport = importDonneesEntreePort.importEquipementsPhysiques(null, null, null, fileToRead); + + assertTrue(resultImport.getValue().isEmpty()); + assertEquals("Le fichier CSV des équipements physiques n'est pas trouvable.", resultImport.getKey().getErreurs().get(0)); + } + + @Test + void importEquipementsVirtuels_onIOException_shouldReturnReportWith1Error() throws IOException { + Mockito.when(fileToRead.getInputStream()).thenThrow(new IOException("Test")); + + Pair<RapportImport, List<EquipementVirtuel>> resultImport = importDonneesEntreePort.importEquipementsVirtuels(null, null, null, fileToRead); + + assertTrue(resultImport.getValue().isEmpty()); + assertEquals("Le fichier CSV des équipements virtuels n'est pas lisible par le système.", resultImport.getKey().getErreurs().get(0)); + } + + @Test + void importEquipementsVirtuels_onFileNotFoundException_shouldReturnReportWith1Error() throws IOException { + Mockito.when(fileToRead.getInputStream()).thenThrow(new FileNotFoundException("Test")); + + Pair<RapportImport, List<EquipementVirtuel>> resultImport = importDonneesEntreePort.importEquipementsVirtuels(null, null, null, fileToRead); + + assertTrue(resultImport.getValue().isEmpty()); + assertEquals("Le fichier CSV des équipements virtuels n'est pas trouvable.", resultImport.getKey().getErreurs().get(0)); + } + + @Test + void importApplications_onIOException_shouldReturnReportWith1Error() throws IOException { + Mockito.when(fileToRead.getInputStream()).thenThrow(new IOException("Test")); + + Pair<RapportImport, List<Application>> resultImport = importDonneesEntreePort.importApplications(null, null, null, fileToRead); + + assertTrue(resultImport.getValue().isEmpty()); + assertEquals("Le fichier CSV des Applications n'est pas lisible par le système.", resultImport.getKey().getErreurs().get(0)); + } + + @Test + void importApplications_onFileNotFoundException_shouldReturnReportWith1Error() throws IOException { + Mockito.when(fileToRead.getInputStream()).thenThrow(new FileNotFoundException("Test")); + + Pair<RapportImport, List<Application>> resultImport = importDonneesEntreePort.importApplications(null, null, null, fileToRead); + + assertTrue(resultImport.getValue().isEmpty()); + assertEquals("Le fichier CSV des Applications n'est pas trouvable.", resultImport.getKey().getErreurs().get(0)); + } + + @Test + void importMessageries_onIOException_shouldReturnReportWith1Error() throws IOException { + Mockito.when(fileToRead.getInputStream()).thenThrow(new IOException("Test")); + + Pair<RapportImport, List<Messagerie>> resultImport = importDonneesEntreePort.importMessageries(null, null, null, fileToRead); + + assertTrue(resultImport.getValue().isEmpty()); + assertEquals("Le fichier CSV de la messagerie n'est pas lisible par le système.", resultImport.getKey().getErreurs().get(0)); + } + + @Test + void importMessageries_onFileNotFoundException_shouldReturnReportWith1Error() throws IOException { + Mockito.when(fileToRead.getInputStream()).thenThrow(new FileNotFoundException("Test")); + + Pair<RapportImport, List<Messagerie>> resultImport = importDonneesEntreePort.importMessageries(null, null, null, fileToRead); + + assertTrue(resultImport.getValue().isEmpty()); + assertEquals("Le fichier CSV de la messagerie n'est pas trouvable.", resultImport.getKey().getErreurs().get(0)); + } + + @Test + void importEntite_onIOException_shouldReturnReportWith1Error() throws IOException { + Mockito.when(fileToRead.getInputStream()).thenThrow(new IOException("Test")); + + var resultImport = importDonneesEntreePort.importEntite(null, null, null, fileToRead); + + assertTrue(resultImport.getValue().isEmpty()); + assertEquals("Le fichier CSV des entités n'est pas lisible par le système.", resultImport.getKey().getErreurs().get(0)); + } + + @Test + void importEntite_onFileNotFoundException_shouldReturnReportWith1Error() throws IOException { + Mockito.when(fileToRead.getInputStream()).thenThrow(new FileNotFoundException("Test")); + + var resultImport = importDonneesEntreePort.importEntite(null, null, null, fileToRead); + + assertTrue(resultImport.getValue().isEmpty()); + assertEquals("Le fichier CSV des entités n'est pas trouvable.", resultImport.getKey().getErreurs().get(0)); + } +} diff --git a/services/api-expositiondonneesentrees/src/test/java/org/mte/numecoeval/expositiondonneesentrees/domain/port/input/SoumissionCalculPortImplTest.java b/services/api-expositiondonneesentrees/src/test/java/org/mte/numecoeval/expositiondonneesentrees/domain/port/input/SoumissionCalculPortImplTest.java new file mode 100644 index 00000000..3df6461c --- /dev/null +++ b/services/api-expositiondonneesentrees/src/test/java/org/mte/numecoeval/expositiondonneesentrees/domain/port/input/SoumissionCalculPortImplTest.java @@ -0,0 +1,86 @@ +package org.mte.numecoeval.expositiondonneesentrees.domain.port.input; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.mte.numecoeval.expositiondonneesentrees.domain.exception.ValidationException; +import org.mte.numecoeval.expositiondonneesentrees.domain.model.DemandeCalcul; +import org.mte.numecoeval.expositiondonneesentrees.domain.ports.input.SoumissionCalculPort; +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.jdbc.SoumissionCalculPortJdbcImpl; +import org.springframework.jdbc.core.JdbcTemplate; + +import static org.junit.jupiter.api.Assertions.*; + +class SoumissionCalculPortImplTest { + + @Mock + JdbcTemplate jdbcTemplate; + + SoumissionCalculPort soumissionCalculPort; + + @BeforeEach + public void init() { + MockitoAnnotations.openMocks(this); + soumissionCalculPort = new SoumissionCalculPortJdbcImpl(jdbcTemplate); + } + + @Test + void whenDemandeIsNull_validateShouldThrowException() { + var exception = assertThrows(ValidationException.class, () -> soumissionCalculPort.validate(null)); + assertEquals("Corps de la demande obligatoire", exception.getErreur()); + } + + @Test + void whenDateLotIsNull_validateShouldThrowException() { + var demande = DemandeCalcul.builder() + .dateLot(null) + .nomOrganisation(null) + .nomLot(null) + .build(); + var exception = assertThrows(ValidationException.class, () -> soumissionCalculPort.validate(demande)); + assertEquals("Nom de lot obligatoire", exception.getErreur()); + } + + @Test + void whenDemandeIsValid_soumissionShouldBeOK() { + var demande = DemandeCalcul.builder() + .nomLot("TEST|2023-01-01") + .build(); + Mockito.when(jdbcTemplate.update(Mockito.contains("en_donnees_entrees"), Mockito.anyString(), Mockito.anyString(), Mockito.anyString())).thenReturn(1); + Mockito.when(jdbcTemplate.update(Mockito.contains("en_data_center"), Mockito.anyString(), Mockito.anyString(), Mockito.anyString())).thenReturn(2); + Mockito.when(jdbcTemplate.update(Mockito.contains("en_equipement_physique"), Mockito.anyString(), Mockito.anyString(), Mockito.anyString())).thenReturn(5); + Mockito.when(jdbcTemplate.update(Mockito.contains("en_messagerie"), Mockito.anyString(), Mockito.anyString(), Mockito.anyString())).thenReturn(20); + + var resultat = soumissionCalculPort.soumissionCalcul(demande); + + assertNotNull(resultat); + assertEquals(2, resultat.getNbrDataCenter()); + assertEquals(5, resultat.getNbrEquipementPhysique()); + assertNull(resultat.getNbrEquipementVirtuel()); + assertNull(resultat.getNbrApplication()); + assertEquals(20, resultat.getNbrMessagerie()); + } + + @Test + void whenDemandeIsValid_rejeuShouldBeOK() { + var demande = DemandeCalcul.builder() + .nomLot("TEST|2023-01-01") + .build(); + Mockito.when(jdbcTemplate.update(Mockito.contains("en_donnees_entrees"), Mockito.anyString(), Mockito.anyString())).thenReturn(1); + Mockito.when(jdbcTemplate.update(Mockito.contains("en_data_center"), Mockito.anyString(), Mockito.anyString())).thenReturn(2); + Mockito.when(jdbcTemplate.update(Mockito.contains("en_equipement_physique"), Mockito.anyString(), Mockito.anyString())).thenReturn(5); + Mockito.when(jdbcTemplate.update(Mockito.contains("en_messagerie"), Mockito.anyString(), Mockito.anyString())).thenReturn(20); + + var resultat = soumissionCalculPort.rejeuCalcul(demande); + + assertNotNull(resultat); + assertEquals(2, resultat.getNbrDataCenter()); + assertEquals(5, resultat.getNbrEquipementPhysique()); + assertNull(resultat.getNbrEquipementVirtuel()); + assertNull(resultat.getNbrApplication()); + assertEquals(20, resultat.getNbrMessagerie()); + } + +} diff --git a/services/api-expositiondonneesentrees/src/test/java/org/mte/numecoeval/expositiondonneesentrees/domain/port/input/StatutPourCalculPortImplTest.java b/services/api-expositiondonneesentrees/src/test/java/org/mte/numecoeval/expositiondonneesentrees/domain/port/input/StatutPourCalculPortImplTest.java new file mode 100644 index 00000000..e5162cec --- /dev/null +++ b/services/api-expositiondonneesentrees/src/test/java/org/mte/numecoeval/expositiondonneesentrees/domain/port/input/StatutPourCalculPortImplTest.java @@ -0,0 +1,106 @@ +package org.mte.numecoeval.expositiondonneesentrees.domain.port.input; + +import com.fasterxml.jackson.annotation.JsonInclude; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mapstruct.factory.Mappers; +import org.mockito.InjectMocks; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mte.numecoeval.expositiondonneesentrees.domain.exception.NotFoundException; +import org.mte.numecoeval.expositiondonneesentrees.domain.model.Volume; +import org.mte.numecoeval.expositiondonneesentrees.domain.ports.input.impl.StatutPourCalculPortImpl; +import org.mte.numecoeval.expositiondonneesentrees.generated.api.model.StatutCalculRest; +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.mapper.VolumeMapper; +import org.springframework.test.util.ReflectionTestUtils; +import org.testcontainers.shaded.com.fasterxml.jackson.databind.ObjectMapper; +import org.testcontainers.shaded.com.fasterxml.jackson.databind.SerializationFeature; + +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@ExtendWith(MockitoExtension.class) +class StatutPourCalculPortImplTest { + + @InjectMocks + StatutPourCalculPortImpl statutPourCalculPort; + + private static final ObjectMapper mapper = new ObjectMapper() + .setSerializationInclusion(JsonInclude.Include.NON_NULL) + .configure(SerializationFeature.INDENT_OUTPUT, true); + + @BeforeEach + public void setup() { + VolumeMapper volumeMapper = Mappers.getMapper(VolumeMapper.class); // Initialization of the mapper + ReflectionTestUtils.setField(statutPourCalculPort, "volumeMapper", volumeMapper); + } + + @Test + void testStatutDesCalculs_Nominal() throws IOException { + + /* INPUT VOLUME MOCKED */ + var volumeEqPhysique = new Volume(10L, 100L); + var volumeMessagerie = new Volume(50L, 5000L); + + /* EXECUTE : params does not matter in the test */ + var actual = statutPourCalculPort.statutCalculs(volumeEqPhysique, volumeMessagerie); + + var expected = mapper.readValue(""" + { + "statut" : "EN_COURS", + "etat" : "98%", + "equipementPhysique" : { + "nbEnCours" : 10, + "nbTraite" : 100 + }, + "messagerie" : { + "nbEnCours" : 50, + "nbTraite" : 5000 + } + } + """, StatutCalculRest.class); + + /* ASSERT */ + assertEquals(mapper.writeValueAsString(expected), mapper.writeValueAsString(actual)); + + } + + @Test + void testStatutDesCalculs_Termine() throws IOException { + + /* INPUT VOLUME MOCKED */ + var volumeEqPhysique = new Volume(0L, 100L); + var volumeMessagerie = new Volume(0L, 5000L); + + /* EXECUTE : params does not matter in the test */ + var actual = statutPourCalculPort.statutCalculs(volumeEqPhysique, volumeMessagerie); + + var expected = mapper.readValue(""" + { + "statut" : "TERMINE", + "etat" : "100%", + "equipementPhysique" : { + "nbEnCours" : 0, + "nbTraite" : 100 + }, + "messagerie" : { + "nbEnCours" : 0, + "nbTraite" : 5000 + } + } + """, StatutCalculRest.class); + + /* ASSERT */ + assertEquals(mapper.writeValueAsString(expected), mapper.writeValueAsString(actual)); + + } + + @Test + void testStatutDesCalculs_NotFound() { + var volume0 = new Volume(0L, 0L); + assertThrows(NotFoundException.class, () -> statutPourCalculPort.statutCalculs(volume0, volume0)); + } + +} diff --git a/services/api-expositiondonneesentrees/src/test/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/controller/ImportCSVControllerTest.java b/services/api-expositiondonneesentrees/src/test/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/controller/ImportCSVControllerTest.java new file mode 100644 index 00000000..e8ca5bc9 --- /dev/null +++ b/services/api-expositiondonneesentrees/src/test/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/controller/ImportCSVControllerTest.java @@ -0,0 +1,115 @@ +package org.mte.numecoeval.expositiondonneesentrees.infrastructure.controller; + +import org.instancio.Instancio; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mte.numecoeval.expositiondonneesentrees.domain.ports.input.ImportDonneesEntreePort; +import org.mte.numecoeval.expositiondonneesentrees.generated.api.model.DonneesEntreeRest; +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.jpa.adapter.SaveDonneesEntreeAdapter; +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.mapper.DonneesEntreeRestMapper; +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.mapper.DonneesEntreeRestMapperImpl; +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.service.ErrorManagementPostSaveService; +import org.springframework.http.HttpStatus; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.util.ResourceUtils; +import org.springframework.web.server.ResponseStatusException; + +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class ImportCSVControllerTest { + + @InjectMocks + private ImportCSVController importCSVController; + + @Mock + ImportDonneesEntreePort importDonneesEntreePort; + + @Mock + SaveDonneesEntreeAdapter saveDonneesEntreeAdapter; + + DonneesEntreeRestMapper donneesEntreeMapper = new DonneesEntreeRestMapperImpl(); + + @Mock + ErrorManagementPostSaveService errorManagementPostSaveService; + + @BeforeEach + public void init() { + MockitoAnnotations.openMocks(this); + importCSVController = new ImportCSVController(donneesEntreeMapper, importDonneesEntreePort, saveDonneesEntreeAdapter, errorManagementPostSaveService); + } + + @Test + void importJson_shouldThrowExceptionWithNotImplemented() { + var donneesTests = Instancio.of(DonneesEntreeRest.class).create(); + var exception = assertThrows(ResponseStatusException.class, () -> importCSVController.importJson(donneesTests)); + + assertEquals(HttpStatus.NOT_IMPLEMENTED, exception.getStatusCode()); + assertEquals("Cette méthode d'import n'a pas encore été implémenté.", exception.getReason()); + } + + @Test + void importCSV_onNullFiles_shouldThrowResponseStatusExceptionWithBadRequest() { + var exception = assertThrows(ResponseStatusException.class, () -> importCSVController.importInterneCSV("nomLot", "2023-01-01", "TEST", null, null, null, null, null, null)); + + assertEquals(HttpStatus.BAD_REQUEST, exception.getStatusCode()); + assertEquals("Tous les fichiers ne peuvent être vides en même temps", exception.getReason()); + } + + @Test + void importCSV_onEmptyFiles_shouldThrowResponseStatusExceptionWithBadRequest() { + MockMultipartFile csvDataCenters = new MockMultipartFile("csvDataCenterVide.csv", new byte[]{}); + MockMultipartFile csvEquipements = new MockMultipartFile("csvEquipementsVide.csv", new byte[]{}); + MockMultipartFile csvEquipementsVirtuels = new MockMultipartFile("csvEquipementVirtuelVide.csv", new byte[]{}); + MockMultipartFile csvApplications = new MockMultipartFile("csvApplicationVide.csv", new byte[]{}); + MockMultipartFile csvMessagerie = new MockMultipartFile("csvMessagerieVide.csv", new byte[]{}); + MockMultipartFile csvEntite = new MockMultipartFile("csvEntiteVide.csv", new byte[]{}); + + var exception = assertThrows(ResponseStatusException.class, () -> importCSVController.importInterneCSV("nomLot", "2023-01-01", "TEST", csvDataCenters, csvEquipements, csvEquipementsVirtuels, csvApplications, csvMessagerie, csvEntite)); + + assertEquals(HttpStatus.BAD_REQUEST, exception.getStatusCode()); + assertEquals("Tous les fichiers ne peuvent être vides en même temps", exception.getReason()); + } + + @ParameterizedTest + @NullAndEmptySource + void importCSV_whenNomLotIsNullOrBlank_shouldThrowResponseStatusExceptionWithBadRequest(String nomLot) throws IOException { + MockMultipartFile csvDataCenters = null; + InputStream fileEquipement = new FileInputStream(ResourceUtils.getFile("target/test-classes/equipementPhysique.csv")); + MockMultipartFile csvEquipements = new MockMultipartFile("csvEquipementEquipementNonVide.csv", fileEquipement); + MockMultipartFile csvEquipementsVirtuels = null; + MockMultipartFile csvApplications = null; + MockMultipartFile csvMessagerie = null; + MockMultipartFile csvEntite = null; + + var exception = assertThrows(ResponseStatusException.class, () -> importCSVController.importInterneCSV(nomLot, "2023-01-01", "TEST", csvDataCenters, csvEquipements, csvEquipementsVirtuels, csvApplications, csvMessagerie, csvEntite)); + + assertEquals(HttpStatus.BAD_REQUEST, exception.getStatusCode()); + assertEquals("Le nom du Lot ne peut être pas vide", exception.getReason()); + } + + @Test + void importCSV_whenDateLotBadFormat_shouldThrowResponseStatusExceptionWithBadRequest() throws IOException { + MockMultipartFile csvDataCenters = null; + InputStream fileEquipement = new FileInputStream(ResourceUtils.getFile("target/test-classes/equipementPhysique.csv")); + MockMultipartFile csvEquipements = new MockMultipartFile("csvEquipementEquipementNonVide.csv", fileEquipement); + MockMultipartFile csvEquipementsVirtuels = null; + MockMultipartFile csvApplications = null; + MockMultipartFile csvMessagerie = null; + MockMultipartFile csvEntite = null; + + var exception = assertThrows(ResponseStatusException.class, () -> importCSVController.importInterneCSV("test", "20230101", "TEST", csvDataCenters, csvEquipements, csvEquipementsVirtuels, csvApplications, csvMessagerie, csvEntite)); + + assertEquals(HttpStatus.BAD_REQUEST, exception.getStatusCode()); + assertEquals("La date du lot doit avoir le format yyyy-MM-dd", exception.getReason()); + } +} diff --git a/services/api-expositiondonneesentrees/src/test/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/controller/RestExceptionHandlerTest.java b/services/api-expositiondonneesentrees/src/test/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/controller/RestExceptionHandlerTest.java new file mode 100644 index 00000000..331c5ac5 --- /dev/null +++ b/services/api-expositiondonneesentrees/src/test/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/controller/RestExceptionHandlerTest.java @@ -0,0 +1,64 @@ +package org.mte.numecoeval.expositiondonneesentrees.infrastructure.controller; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mte.numecoeval.expositiondonneesentrees.domain.exception.ValidationException; +import org.springframework.web.context.request.WebRequest; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.when; + +class RestExceptionHandlerTest { + + RestExceptionHandler restExceptionHandler = new RestExceptionHandler(); + + @Mock + WebRequest webRequest; + + @BeforeEach + public void setup() { + MockitoAnnotations.openMocks(this); + } + + @Test + void onException_shouldReturnGenericMessage() { + Exception erreur = new Exception(); + when(webRequest.getContextPath()).thenReturn("/test"); + + var response = restExceptionHandler.exception(erreur, webRequest); + assertNotNull(response.getBody(), "Le corps de la réponse en erreur ne peut être null"); + assertEquals("500", response.getBody().getCode()); + assertEquals(500, response.getBody().getStatus()); + assertEquals("Erreur interne de traitement lors du traitement de la requête", response.getBody().getMessage()); + } + + + @Test + void onRuntimeException_shouldReturnGenericMessage() { + var erreur = new NullPointerException("test"); + when(webRequest.getContextPath()).thenReturn("/test"); + + var response = restExceptionHandler.runtimeException(erreur, webRequest); + assertNotNull(response.getBody(), "Le corps de la réponse en erreur ne peut être null"); + assertEquals("500", response.getBody().getCode()); + assertEquals(500, response.getBody().getStatus()); + assertEquals("Erreur interne de traitement lors du traitement de la requête", response.getBody().getMessage()); + } + + + @Test + void onValidationException_shouldReturnGenericMessage() { + var erreur = new ValidationException("Erreur lors de la validation des paramètres"); + when(webRequest.getContextPath()).thenReturn("/test"); + + var response = restExceptionHandler.handleValidationException(erreur, webRequest); + assertNotNull(response.getBody(), "Le corps de la réponse en erreur ne peut être null"); + assertEquals("400", response.getBody().getCode()); + assertEquals(400, response.getBody().getStatus()); + assertEquals(erreur.getErreur(), response.getBody().getMessage()); + } + +} diff --git a/services/api-expositiondonneesentrees/src/test/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/adapter/ApplicationJpaAdapterTest.java b/services/api-expositiondonneesentrees/src/test/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/adapter/ApplicationJpaAdapterTest.java new file mode 100644 index 00000000..5eeeb038 --- /dev/null +++ b/services/api-expositiondonneesentrees/src/test/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/adapter/ApplicationJpaAdapterTest.java @@ -0,0 +1,67 @@ +package org.mte.numecoeval.expositiondonneesentrees.infrastructure.jpa.adapter; + +import org.instancio.Instancio; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mte.numecoeval.expositiondonneesentrees.domain.model.Application; +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.jpa.entity.ApplicationEntity; +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.jpa.repository.ApplicationRepository; +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.mapper.EntreeEntityMapper; +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.mapper.EntreeEntityMapperImpl; + +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.times; + +@ExtendWith(MockitoExtension.class) +class ApplicationJpaAdapterTest { + @InjectMocks + private ApplicationJpaAdapter jpaAdapter; + + @Mock + ApplicationRepository repository; + + EntreeEntityMapper entreeEntityMapper = new EntreeEntityMapperImpl(); + + @BeforeEach + void setup(){ + jpaAdapter = new ApplicationJpaAdapter(repository, entreeEntityMapper); + } + + @Test + void saveShouldConvertAndCallSave() { + Application domain = Instancio.of(Application.class).create(); + ArgumentCaptor<ApplicationEntity> valueCapture = ArgumentCaptor.forClass(ApplicationEntity.class); + + jpaAdapter.save(domain); + + Mockito.verify(repository, times(1)).save(valueCapture.capture()); + assertNotNull(valueCapture.getValue()); + } + + @Test + void saveAllShouldConvertAndCallSaveAll() { + Application domain1 = Instancio.of(Application.class).create(); + Application domain2 = Instancio.of(Application.class).create(); + ArgumentCaptor<List<ApplicationEntity>> valueCapture = ArgumentCaptor.forClass(List.class); + List<Application> entrees = Arrays.asList( + domain1, + domain2 + ); + + jpaAdapter.saveAll(entrees); + + Mockito.verify(repository, times(1)).saveAll(valueCapture.capture()); + assertNotNull(valueCapture.getValue()); + assertEquals(2, valueCapture.getValue().size()); + } +} diff --git a/services/api-expositiondonneesentrees/src/test/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/adapter/DataCenterJpaAdapterTest.java b/services/api-expositiondonneesentrees/src/test/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/adapter/DataCenterJpaAdapterTest.java new file mode 100644 index 00000000..d2a6853a --- /dev/null +++ b/services/api-expositiondonneesentrees/src/test/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/adapter/DataCenterJpaAdapterTest.java @@ -0,0 +1,67 @@ +package org.mte.numecoeval.expositiondonneesentrees.infrastructure.jpa.adapter; + +import org.instancio.Instancio; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mte.numecoeval.expositiondonneesentrees.domain.model.DataCenter; +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.jpa.entity.DataCenterEntity; +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.jpa.repository.DataCenterRepository; +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.mapper.EntreeEntityMapper; +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.mapper.EntreeEntityMapperImpl; + +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.times; + +@ExtendWith(MockitoExtension.class) +class DataCenterJpaAdapterTest { + @InjectMocks + private DataCenterJpaAdapter jpaAdapter; + + @Mock + DataCenterRepository repository; + + EntreeEntityMapper entreeEntityMapper = new EntreeEntityMapperImpl(); + + @BeforeEach + void setup(){ + jpaAdapter = new DataCenterJpaAdapter(repository, entreeEntityMapper); + } + + @Test + void saveShouldConvertAndCallSave() { + DataCenter domain = Instancio.of(DataCenter.class).create(); + ArgumentCaptor<DataCenterEntity> valueCapture = ArgumentCaptor.forClass(DataCenterEntity.class); + + jpaAdapter.save(domain); + + Mockito.verify(repository, times(1)).save(valueCapture.capture()); + assertNotNull(valueCapture.getValue()); + } + + @Test + void saveAllShouldConvertAndCallSaveAll() { + DataCenter domain1 = Instancio.of(DataCenter.class).create(); + DataCenter domain2 = Instancio.of(DataCenter.class).create(); + ArgumentCaptor<List<DataCenterEntity>> valueCapture = ArgumentCaptor.forClass(List.class); + List<DataCenter> entrees = Arrays.asList( + domain1, + domain2 + ); + + jpaAdapter.saveAll(entrees); + + Mockito.verify(repository, times(1)).saveAll(valueCapture.capture()); + assertNotNull(valueCapture.getValue()); + assertEquals(2, valueCapture.getValue().size()); + } +} diff --git a/services/api-expositiondonneesentrees/src/test/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/adapter/DonneesEntreesJpaAdapterTest.java b/services/api-expositiondonneesentrees/src/test/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/adapter/DonneesEntreesJpaAdapterTest.java new file mode 100644 index 00000000..1b20b581 --- /dev/null +++ b/services/api-expositiondonneesentrees/src/test/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/adapter/DonneesEntreesJpaAdapterTest.java @@ -0,0 +1,63 @@ +package org.mte.numecoeval.expositiondonneesentrees.infrastructure.jpa.adapter; + +import org.instancio.Instancio; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mte.numecoeval.expositiondonneesentrees.domain.model.DonneesEntree; +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.jpa.entity.DonneesEntreesEntity; +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.jpa.repository.DonneesEntreesRepository; +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.mapper.EntreeEntityMapper; +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.mapper.EntreeEntityMapperImpl; + +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.times; + +@ExtendWith(MockitoExtension.class) +class DonneesEntreesJpaAdapterTest { + @InjectMocks + private DonneesEntreesJpaAdapter jpaAdapter; + + @Mock + DonneesEntreesRepository repository; + + EntreeEntityMapper entreeEntityMapper = new EntreeEntityMapperImpl(); + + @BeforeEach + void setup(){ + jpaAdapter = new DonneesEntreesJpaAdapter(repository, entreeEntityMapper); + } + + @Test + void saveShouldConvertAndCallSave() { + DonneesEntree domain = Instancio.of(DonneesEntree.class).create(); + ArgumentCaptor<DonneesEntreesEntity> valueCapture = ArgumentCaptor.forClass(DonneesEntreesEntity.class); + + jpaAdapter.save(domain); + + Mockito.verify(repository, times(1)).save(valueCapture.capture()); + assertNotNull(valueCapture.getValue()); + } + + @Test + void saveAllShouldConvertAndCallSaveForEachObject() { + DonneesEntree domain1 = Instancio.of(DonneesEntree.class).create(); + DonneesEntree domain2 = Instancio.of(DonneesEntree.class).create(); + List<DonneesEntree> entrees = Arrays.asList( + domain1, + domain2 + ); + + jpaAdapter.saveAll(entrees); + + Mockito.verify(repository, times(entrees.size())).save(Mockito.any()); + } +} diff --git a/services/api-expositiondonneesentrees/src/test/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/adapter/EntiteJpaAdapterTest.java b/services/api-expositiondonneesentrees/src/test/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/adapter/EntiteJpaAdapterTest.java new file mode 100644 index 00000000..14592ff8 --- /dev/null +++ b/services/api-expositiondonneesentrees/src/test/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/adapter/EntiteJpaAdapterTest.java @@ -0,0 +1,67 @@ +package org.mte.numecoeval.expositiondonneesentrees.infrastructure.jpa.adapter; + +import org.instancio.Instancio; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mte.numecoeval.expositiondonneesentrees.domain.model.Entite; +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.jpa.entity.EntiteEntity; +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.jpa.repository.EntiteRepository; +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.mapper.EntreeEntityMapper; +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.mapper.EntreeEntityMapperImpl; + +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.times; + +@ExtendWith(MockitoExtension.class) +class EntiteJpaAdapterTest { + @InjectMocks + private EntiteJpaAdapter jpaAdapter; + + @Mock + EntiteRepository repository; + + EntreeEntityMapper entreeEntityMapper = new EntreeEntityMapperImpl(); + + @BeforeEach + void setup(){ + jpaAdapter = new EntiteJpaAdapter(repository, entreeEntityMapper); + } + + @Test + void saveShouldConvertAndCallSave() { + Entite domain = Instancio.of(Entite.class).create(); + ArgumentCaptor<EntiteEntity> valueCapture = ArgumentCaptor.forClass(EntiteEntity.class); + + jpaAdapter.save(domain); + + Mockito.verify(repository, times(1)).save(valueCapture.capture()); + assertNotNull(valueCapture.getValue()); + } + + @Test + void saveAllShouldConvertAndCallSaveAll() { + Entite domain1 = Instancio.of(Entite.class).create(); + Entite domain2 = Instancio.of(Entite.class).create(); + ArgumentCaptor<List<EntiteEntity>> valueCapture = ArgumentCaptor.forClass(List.class); + List<Entite> entrees = Arrays.asList( + domain1, + domain2 + ); + + jpaAdapter.saveAll(entrees); + + Mockito.verify(repository, times(1)).saveAll(valueCapture.capture()); + assertNotNull(valueCapture.getValue()); + assertEquals(2, valueCapture.getValue().size()); + } +} diff --git a/services/api-expositiondonneesentrees/src/test/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/adapter/EquipementPhysiqueJpaAdapterTest.java b/services/api-expositiondonneesentrees/src/test/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/adapter/EquipementPhysiqueJpaAdapterTest.java new file mode 100644 index 00000000..66b5e07b --- /dev/null +++ b/services/api-expositiondonneesentrees/src/test/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/adapter/EquipementPhysiqueJpaAdapterTest.java @@ -0,0 +1,67 @@ +package org.mte.numecoeval.expositiondonneesentrees.infrastructure.jpa.adapter; + +import org.instancio.Instancio; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mte.numecoeval.expositiondonneesentrees.domain.model.EquipementPhysique; +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.jpa.entity.EquipementPhysiqueEntity; +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.jpa.repository.EquipementPhysiqueRepository; +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.mapper.EntreeEntityMapper; +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.mapper.EntreeEntityMapperImpl; + +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.times; + +@ExtendWith(MockitoExtension.class) +class EquipementPhysiqueJpaAdapterTest { + @InjectMocks + private EquipementPhysiqueJpaAdapter jpaAdapter; + + @Mock + EquipementPhysiqueRepository repository; + + EntreeEntityMapper entreeEntityMapper = new EntreeEntityMapperImpl(); + + @BeforeEach + void setup(){ + jpaAdapter = new EquipementPhysiqueJpaAdapter(repository, entreeEntityMapper); + } + + @Test + void saveShouldConvertAndCallSave() { + EquipementPhysique domain = Instancio.of(EquipementPhysique.class).create(); + ArgumentCaptor<EquipementPhysiqueEntity> valueCapture = ArgumentCaptor.forClass(EquipementPhysiqueEntity.class); + + jpaAdapter.save(domain); + + Mockito.verify(repository, times(1)).save(valueCapture.capture()); + assertNotNull(valueCapture.getValue()); + } + + @Test + void saveAllShouldConvertAndCallSaveAll() { + EquipementPhysique domain1 = Instancio.of(EquipementPhysique.class).create(); + EquipementPhysique domain2 = Instancio.of(EquipementPhysique.class).create(); + ArgumentCaptor<List<EquipementPhysiqueEntity>> valueCapture = ArgumentCaptor.forClass(List.class); + List<EquipementPhysique> entrees = Arrays.asList( + domain1, + domain2 + ); + + jpaAdapter.saveAll(entrees); + + Mockito.verify(repository, times(1)).saveAll(valueCapture.capture()); + assertNotNull(valueCapture.getValue()); + assertEquals(2, valueCapture.getValue().size()); + } +} diff --git a/services/api-expositiondonneesentrees/src/test/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/adapter/EquipementVirtuelJpaAdapterTest.java b/services/api-expositiondonneesentrees/src/test/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/adapter/EquipementVirtuelJpaAdapterTest.java new file mode 100644 index 00000000..5f12f313 --- /dev/null +++ b/services/api-expositiondonneesentrees/src/test/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/adapter/EquipementVirtuelJpaAdapterTest.java @@ -0,0 +1,67 @@ +package org.mte.numecoeval.expositiondonneesentrees.infrastructure.jpa.adapter; + +import org.instancio.Instancio; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mte.numecoeval.expositiondonneesentrees.domain.model.EquipementVirtuel; +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.jpa.entity.EquipementVirtuelEntity; +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.jpa.repository.EquipementVirtuelRepository; +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.mapper.EntreeEntityMapper; +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.mapper.EntreeEntityMapperImpl; + +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.times; + +@ExtendWith(MockitoExtension.class) +class EquipementVirtuelJpaAdapterTest { + @InjectMocks + private EquipementVirtuelJpaAdapter jpaAdapter; + + @Mock + EquipementVirtuelRepository repository; + + EntreeEntityMapper entreeEntityMapper = new EntreeEntityMapperImpl(); + + @BeforeEach + void setup(){ + jpaAdapter = new EquipementVirtuelJpaAdapter(repository, entreeEntityMapper); + } + + @Test + void saveShouldConvertAndCallSave() { + EquipementVirtuel domain = Instancio.of(EquipementVirtuel.class).create(); + ArgumentCaptor<EquipementVirtuelEntity> valueCapture = ArgumentCaptor.forClass(EquipementVirtuelEntity.class); + + jpaAdapter.save(domain); + + Mockito.verify(repository, times(1)).save(valueCapture.capture()); + assertNotNull(valueCapture.getValue()); + } + + @Test + void saveAllShouldConvertAndCallSaveAll() { + EquipementVirtuel domain1 = Instancio.of(EquipementVirtuel.class).create(); + EquipementVirtuel domain2 = Instancio.of(EquipementVirtuel.class).create(); + ArgumentCaptor<List<EquipementVirtuelEntity>> valueCapture = ArgumentCaptor.forClass(List.class); + List<EquipementVirtuel> entrees = Arrays.asList( + domain1, + domain2 + ); + + jpaAdapter.saveAll(entrees); + + Mockito.verify(repository, times(1)).saveAll(valueCapture.capture()); + assertNotNull(valueCapture.getValue()); + assertEquals(2, valueCapture.getValue().size()); + } +} diff --git a/services/api-expositiondonneesentrees/src/test/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/adapter/MessagerieJpaAdapterTest.java b/services/api-expositiondonneesentrees/src/test/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/adapter/MessagerieJpaAdapterTest.java new file mode 100644 index 00000000..9bed6a8f --- /dev/null +++ b/services/api-expositiondonneesentrees/src/test/java/org/mte/numecoeval/expositiondonneesentrees/infrastructure/jpa/adapter/MessagerieJpaAdapterTest.java @@ -0,0 +1,67 @@ +package org.mte.numecoeval.expositiondonneesentrees.infrastructure.jpa.adapter; + +import org.instancio.Instancio; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mte.numecoeval.expositiondonneesentrees.domain.model.Messagerie; +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.jpa.entity.MessagerieEntity; +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.jpa.repository.MessagerieRepository; +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.mapper.EntreeEntityMapper; +import org.mte.numecoeval.expositiondonneesentrees.infrastructure.mapper.EntreeEntityMapperImpl; + +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.times; + +@ExtendWith(MockitoExtension.class) +class MessagerieJpaAdapterTest { + @InjectMocks + private MessagerieJpaAdapter jpaAdapter; + + @Mock + MessagerieRepository repository; + + EntreeEntityMapper entreeEntityMapper = new EntreeEntityMapperImpl(); + + @BeforeEach + void setup(){ + jpaAdapter = new MessagerieJpaAdapter(repository, entreeEntityMapper); + } + + @Test + void saveShouldConvertAndCallSave() { + Messagerie domain = Instancio.of(Messagerie.class).create(); + ArgumentCaptor<MessagerieEntity> valueCapture = ArgumentCaptor.forClass(MessagerieEntity.class); + + jpaAdapter.save(domain); + + Mockito.verify(repository, times(1)).save(valueCapture.capture()); + assertNotNull(valueCapture.getValue()); + } + + @Test + void saveAllShouldConvertAndCallSaveAll() { + Messagerie domain1 = Instancio.of(Messagerie.class).create(); + Messagerie domain2 = Instancio.of(Messagerie.class).create(); + ArgumentCaptor<List<MessagerieEntity>> valueCapture = ArgumentCaptor.forClass(List.class); + List<Messagerie> entrees = Arrays.asList( + domain1, + domain2 + ); + + jpaAdapter.saveAll(entrees); + + Mockito.verify(repository, times(1)).saveAll(valueCapture.capture()); + assertNotNull(valueCapture.getValue()); + assertEquals(2, valueCapture.getValue().size()); + } +} diff --git a/services/api-expositiondonneesentrees/src/test/java/org/mte/numecoeval/expositiondonneesentrees/test/DataTableUtils.java b/services/api-expositiondonneesentrees/src/test/java/org/mte/numecoeval/expositiondonneesentrees/test/DataTableUtils.java new file mode 100644 index 00000000..f33acb89 --- /dev/null +++ b/services/api-expositiondonneesentrees/src/test/java/org/mte/numecoeval/expositiondonneesentrees/test/DataTableUtils.java @@ -0,0 +1,58 @@ +package org.mte.numecoeval.expositiondonneesentrees.test; + +import org.apache.commons.lang3.StringUtils; + +import java.time.LocalDate; +import java.util.Map; + +/** + * Classe utilitaire pour les DataTable et la récupération de champ dans des types particuliers à partir de String. + */ +public class DataTableUtils { + + public static String safeString(Map<String, String> rowDatatable, String key) { + if(StringUtils.isBlank(rowDatatable.get(key))) { + return null; + } + return rowDatatable.get(key); + } + + public static LocalDate safeLocalDate(Map<String, String> rowDatatable, String key) { + if(StringUtils.isBlank(rowDatatable.get(key))) { + return null; + } + return LocalDate.parse(rowDatatable.get(key)); + } + + public static Double safeDouble(Map<String, String> rowDatatable, String key) { + if(StringUtils.isBlank(rowDatatable.get(key))) { + return null; + } + return Double.parseDouble(rowDatatable.get(key)); + } + + public static Float safeFloat(Map<String, String> rowDatatable, String key) { + if(StringUtils.isBlank(rowDatatable.get(key))) { + return null; + } + return Float.parseFloat(rowDatatable.get(key)); + } + + public static Integer safeInteger(Map<String, String> rowDatatable, String key) { + if(StringUtils.isBlank(rowDatatable.get(key))) { + return null; + } + return Integer.parseInt(rowDatatable.get(key)); + } + + public static Boolean safeBoolean(Map<String, String> rowDatatable, String key) { + if(StringUtils.isBlank(rowDatatable.get(key))) { + return false; + } + return Boolean.parseBoolean(rowDatatable.get(key)); + } + + private DataTableUtils() { + // Private constructor + } +} diff --git a/services/api-expositiondonneesentrees/src/test/resources/application-test.yaml b/services/api-expositiondonneesentrees/src/test/resources/application-test.yaml new file mode 100644 index 00000000..d4439711 --- /dev/null +++ b/services/api-expositiondonneesentrees/src/test/resources/application-test.yaml @@ -0,0 +1,36 @@ +# Application +numecoeval: + referentiel: + server: + url: "http://localhost:19090" + +# Serveur Web +server: + port: 18080 + tomcat: + mbeanregistry: + enabled: true + +# Actuator +management: + server: + port: 18080 + security: + user: + name: "test" + password: "test" + roles: ACTUATOR_ADMIN + +#CONFIGURATION BASES +zonky: + test: + database: + postgres: + server: + properties: + max_connections: 100 + provider: zonky + +spring: + jpa: + show-sql: true diff --git a/services/api-expositiondonneesentrees/src/test/resources/equipementPhysique.csv b/services/api-expositiondonneesentrees/src/test/resources/equipementPhysique.csv new file mode 100644 index 00000000..dec2cbe1 --- /dev/null +++ b/services/api-expositiondonneesentrees/src/test/resources/equipementPhysique.csv @@ -0,0 +1,2 @@ +test +test diff --git a/services/api-expositiondonneesentrees/src/test/resources/logback-test.xml b/services/api-expositiondonneesentrees/src/test/resources/logback-test.xml new file mode 100644 index 00000000..276e8663 --- /dev/null +++ b/services/api-expositiondonneesentrees/src/test/resources/logback-test.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="UTF-8"?> +<configuration> + <!-- Spring default.xml --> + <conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter"/> + <conversionRule conversionWord="wex" + converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter"/> + <conversionRule conversionWord="wEx" + converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter"/> + + <property name="CONSOLE_LOG_PATTERN" + value="${CONSOLE_LOG_PATTERN:-%clr(%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/> + <property name="CONSOLE_LOG_CHARSET" value="${CONSOLE_LOG_CHARSET:-${file.encoding:-UTF-8}}"/> + <property name="FILE_LOG_PATTERN" + value="${FILE_LOG_PATTERN:-%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}} ${LOG_LEVEL_PATTERN:-%5p} ${PID:- } --- [%t] %-40.40logger{39} : %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/> + <property name="FILE_LOG_CHARSET" value="${FILE_LOG_CHARSET:-${file.encoding:-UTF-8}}"/> + + <!-- console-appender.xml--> + <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> + <encoder> + <pattern>${CONSOLE_LOG_PATTERN}</pattern> + <charset>${CONSOLE_LOG_CHARSET}</charset> + </encoder> + </appender> + <statusListener class="ch.qos.logback.core.status.NopStatusListener"/> + <root level="INFO"> + <appender-ref ref="CONSOLE"/> + </root> + <logger name="org.springframework.web" level="INFO"/> +</configuration> \ No newline at end of file diff --git a/services/api-referentiel/.gitignore b/services/api-referentiel/.gitignore new file mode 100644 index 00000000..20c8d182 --- /dev/null +++ b/services/api-referentiel/.gitignore @@ -0,0 +1,32 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/services/api-referentiel/README.md b/services/api-referentiel/README.md new file mode 100644 index 00000000..88057897 --- /dev/null +++ b/services/api-referentiel/README.md @@ -0,0 +1,4 @@ +# api-referentiel + +Application API Référentiel portant le chargement et l'exposition des différents référentiels utilisés par les calculs + diff --git a/services/api-referentiel/dependency_check_suppressions.xml b/services/api-referentiel/dependency_check_suppressions.xml new file mode 100644 index 00000000..15f53bbc --- /dev/null +++ b/services/api-referentiel/dependency_check_suppressions.xml @@ -0,0 +1,97 @@ +<?xml version="1.0" encoding="UTF-8"?> +<suppressions xmlns="https://jeremylong.github.io/DependencyCheck/dependency-suppression.1.1.xsd"> + + <suppress> + <notes><![CDATA[ + file name: spring-security-crypto-5.7.3.jar + La librairie Spring Security est en version 5.7.3. + Cette CVE est marquée uniquement jusqu'à la version 5.2.4 (exclus) + https://nvd.nist.gov/vuln/detail/CVE-2020-5408 + ]]></notes> + <cve>CVE-2020-5408</cve> + </suppress> + + <suppress> + <notes><![CDATA[ + file name: spring-web-5.3.22.jar + Dans notre contexte, nous ne recevons pas de code Java de l'extérieur et n'effectuons pas de dé-sérialisation de code Java + Côté Spring, le point est déjà remonté comme un faux-positif : https://github.com/spring-projects/spring-framework/issues/24434#issuecomment-744519525 + ]]></notes> + <cve>CVE-2016-1000027</cve> + </suppress> + + <suppress> + <notes><![CDATA[ + file name: snakeyaml-1.33.jar + Faux-positif : la version de Snakeyaml est la 1.33, la CVE existe uniquement sur les versions < 1.32 + https://nvd.nist.gov/vuln/detail/CVE-2022-38752 + ]]></notes> + <cve>CVE-2022-38752</cve> + </suppress> + + <suppress> + <notes><![CDATA[ + Faux-positif sur toutes les librairies utils: https://github.com/jeremylong/DependencyCheck/issues/5213 + Concerne : software.amazon.awssdk:utils qui n'est pas utilisé + ]]></notes> + <cve>CVE-2021-4277</cve> + </suppress> + + <suppress> + <notes><![CDATA[ + Faux-positif : matchent à tort sur tous les commons : https://github.com/jeremylong/DependencyCheck/issues/5132 + ]]></notes> + <cve>CVE-2021-37533</cve> + </suppress> + + <suppress> + <notes><![CDATA[ + Non-applicable : Snakeyaml n'est utilisé que par Spring Boot pour les fichiers application.yml + et non des fichiers externes. + ]]></notes> + <cve>CVE-2022-1471</cve> + </suppress> + + <suppress> + <notes><![CDATA[ + Non-applicable : Snakeyaml n'est utilisé que par Spring Boot pour les fichiers application.yml + et non des fichiers externes. + ]]></notes> + <cve>CVE-2022-3064</cve> + </suppress> + + <suppress> + <notes><![CDATA[ + Non-applicable : Snakeyaml n'est utilisé que par Spring Boot pour les fichiers application.yml + et non des fichiers externes. + ]]></notes> + <cve>CVE-2021-4235</cve> + </suppress> + + <suppress> + <notes><![CDATA[ + Non-applicable : json-smart est une dépendance transitive. + Les json sont limités à la taille des inventaires : la taille des listes n'a pas réellement de limite + et c'est souhaités comme cela (traitement de gros volumes de données). + ]]></notes> + <cve>CVE-2023-1370</cve> + </suppress> + + <suppress> + <notes><![CDATA[ + Faux-Positif : Concerne hutool-json et json-java qui ne sont pas utilisés. + Le faux-positif se trouve sur json-path, jackson-core, accessors-smart et json-smart. + cf. https://github.com/jeremylong/DependencyCheck/issues/5502 + ]]></notes> + <cve>CVE-2022-45688</cve> + </suppress> + + + <suppress> + <notes><![CDATA[ + Faux-Positif : Conformément au lien, nous ne sommes pas dans un des cas de vulnérabilité. + cf. cf. https://github.com/jeremylong/DependencyCheck/issues/5502 + ]]></notes> + <cve>CVE-2023-20862</cve> + </suppress> +</suppressions> diff --git a/services/api-referentiel/pom.xml b/services/api-referentiel/pom.xml new file mode 100644 index 00000000..183894cc --- /dev/null +++ b/services/api-referentiel/pom.xml @@ -0,0 +1,206 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.mte.numecoeval</groupId> + <artifactId>core</artifactId> + <version>1.2.3-SNAPSHOT</version> + <relativePath/> <!-- lookup parent from repository --> + </parent> + + <artifactId>api-referentiel</artifactId> + <version>1.2.3-SNAPSHOT</version> + <name>api-referentiel</name> + <description>API Referentiel - Lecture, chargement et exposition par API</description> + + <repositories> + <repository> + <id>gitlab-maven</id> + <url>https://gitlab-forge.din.developpement-durable.gouv.fr/api/v4/projects/20519/packages/maven</url> + </repository> + </repositories> + + <distributionManagement> + <repository> + <id>gitlab-maven</id> + <url>https://gitlab-forge.din.developpement-durable.gouv.fr/api/v4/projects/20519/packages/maven</url> + </repository> + + <snapshotRepository> + <id>gitlab-maven</id> + <url>https://gitlab-forge.din.developpement-durable.gouv.fr/api/v4/projects/20519/packages/maven</url> + </snapshotRepository> + </distributionManagement> + + <properties> + </properties> + <dependencies> + <dependency> + <groupId>org.mte.numecoeval</groupId> + <artifactId>common</artifactId> + </dependency> + + <!-- open api docs provisoire --> + <dependency> + <groupId>org.springdoc</groupId> + <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId> + </dependency> + + <dependency> + <groupId>org.springdoc</groupId> + <artifactId>springdoc-openapi-starter-common</artifactId> + </dependency> + + <!-- Security --> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-security</artifactId> + </dependency> + + <!-- Mapping --> + <dependency> + <groupId>org.mapstruct</groupId> + <artifactId>mapstruct</artifactId> + </dependency> + <!-- Monitoring --> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-actuator</artifactId> + </dependency> + <dependency> + <groupId>io.micrometer</groupId> + <artifactId>micrometer-registry-prometheus</artifactId> + <scope>runtime</scope> + </dependency> + + <!-- JPA --> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-data-jpa</artifactId> + </dependency> + + <!-- Nécessaire avec Spring Boot 3 pour Hibernate --> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-validation</artifactId> + </dependency> + + <!-- REST --> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-web</artifactId> + </dependency> + + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-data-rest</artifactId> + </dependency> + + + <!-- Utilitaire --> + <dependency> + <groupId>org.apache.commons</groupId> + <artifactId>commons-csv</artifactId> + </dependency> + <dependency> + <groupId>org.apache.commons</groupId> + <artifactId>commons-collections4</artifactId> + </dependency> + + <!-- Driver pour base de données --> + <dependency> + <groupId>org.postgresql</groupId> + <artifactId>postgresql</artifactId> + <scope>runtime</scope> + </dependency> + + <!-- Lombok --> + <dependency> + <groupId>org.projectlombok</groupId> + <artifactId>lombok</artifactId> + <optional>true</optional> + </dependency> + + <!-- TU/TI --> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-test</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.springframework.security</groupId> + <artifactId>spring-security-test</artifactId> + <scope>test</scope> + </dependency> + + <dependency> + <groupId>org.junit.platform</groupId> + <artifactId>junit-platform-suite</artifactId> + <scope>test</scope> + </dependency> + + <!-- Base de données de tests --> + <dependency> + <groupId>org.hsqldb</groupId> + <artifactId>hsqldb</artifactId> + <scope>test</scope> + </dependency> + + <!-- Tests fonctionnels --> + <dependency> + <groupId>io.cucumber</groupId> + <artifactId>cucumber-core</artifactId> + <scope>test</scope> + </dependency> + + <dependency> + <groupId>io.cucumber</groupId> + <artifactId>cucumber-java</artifactId> + <scope>test</scope> + </dependency> + + <dependency> + <groupId>io.cucumber</groupId> + <artifactId>cucumber-spring</artifactId> + <scope>test</scope> + </dependency> + + <dependency> + <groupId>io.cucumber</groupId> + <artifactId>cucumber-junit-platform-engine</artifactId> + <scope>test</scope> + </dependency> + + <!-- Tests d'API REST --> + <dependency> + <groupId>io.rest-assured</groupId> + <artifactId>rest-assured</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>io.rest-assured</groupId> + <artifactId>rest-assured-all</artifactId> + <scope>test</scope> + </dependency> + + </dependencies> + + <build> + <plugins> + <plugin> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-maven-plugin</artifactId> + <configuration> + <excludes> + <exclude> + <groupId>org.projectlombok</groupId> + <artifactId>lombok</artifactId> + </exclude> + </excludes> + </configuration> + </plugin> + </plugins> + </build> + +</project> diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/ReferentielApplication.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/ReferentielApplication.java new file mode 100644 index 00000000..5e1f8798 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/ReferentielApplication.java @@ -0,0 +1,13 @@ +package org.mte.numecoeval.referentiel; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class ReferentielApplication { + + public static void main(String[] args) { + SpringApplication.run(ReferentielApplication.class, args); + } + +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/data/ResultatImport.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/data/ResultatImport.java new file mode 100644 index 00000000..56a091d4 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/data/ResultatImport.java @@ -0,0 +1,20 @@ +package org.mte.numecoeval.referentiel.domain.data; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.ArrayList; +import java.util.List; + +@Getter +@Setter +@NoArgsConstructor +public class ResultatImport<T> { + + List<String> erreurs = new ArrayList<>(); + + long nbrLignesImportees = 0; + + List<T> objects; +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/exception/NotFoundException.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/exception/NotFoundException.java new file mode 100644 index 00000000..ae14c1ab --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/exception/NotFoundException.java @@ -0,0 +1,8 @@ +package org.mte.numecoeval.referentiel.domain.exception; + +public class NotFoundException extends RuntimeException { + + public NotFoundException(String message) { + super(message); + } +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/exception/ReferentielException.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/exception/ReferentielException.java new file mode 100644 index 00000000..18c1c925 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/exception/ReferentielException.java @@ -0,0 +1,8 @@ +package org.mte.numecoeval.referentiel.domain.exception; + +public class ReferentielException extends Exception { + + public ReferentielException(String message) { + super(message); + } +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/exception/ReferentielRuntimeException.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/exception/ReferentielRuntimeException.java new file mode 100644 index 00000000..79e8ed19 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/exception/ReferentielRuntimeException.java @@ -0,0 +1,7 @@ +package org.mte.numecoeval.referentiel.domain.exception; + +public class ReferentielRuntimeException extends RuntimeException { + public ReferentielRuntimeException(String message) { + super(message); + } +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/model/AbstractReferentiel.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/model/AbstractReferentiel.java new file mode 100644 index 00000000..e28b6a86 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/model/AbstractReferentiel.java @@ -0,0 +1,7 @@ +package org.mte.numecoeval.referentiel.domain.model; + +import java.io.Serializable; + +public interface AbstractReferentiel extends Serializable { + // Actuellement l'interface n'a pas de comportement par défaut ni de champ partagé +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/model/CorrespondanceRefEquipement.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/model/CorrespondanceRefEquipement.java new file mode 100644 index 00000000..7bfc53c1 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/model/CorrespondanceRefEquipement.java @@ -0,0 +1,16 @@ +package org.mte.numecoeval.referentiel.domain.model; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Data; +import lombok.experimental.FieldDefaults; + +@Builder +@Data +@FieldDefaults(level = AccessLevel.PRIVATE) +public class CorrespondanceRefEquipement implements AbstractReferentiel { + + String modeleEquipementSource; + String refEquipementCible; + +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/model/Critere.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/model/Critere.java new file mode 100644 index 00000000..05fb3cd2 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/model/Critere.java @@ -0,0 +1,22 @@ +package org.mte.numecoeval.referentiel.domain.model; + +import lombok.AccessLevel; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.Accessors; +import lombok.experimental.FieldDefaults; + +@Getter +@Setter +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +@FieldDefaults(level = AccessLevel.PRIVATE) +@NoArgsConstructor +public class Critere implements AbstractReferentiel { + String nomCritere; + String unite; + String description; + +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/model/Etape.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/model/Etape.java new file mode 100644 index 00000000..301f82e7 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/model/Etape.java @@ -0,0 +1,21 @@ +package org.mte.numecoeval.referentiel.domain.model; + +import lombok.AccessLevel; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.Accessors; +import lombok.experimental.FieldDefaults; + +@Getter +@Setter +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +@FieldDefaults(level = AccessLevel.PRIVATE) +@NoArgsConstructor +public class Etape implements AbstractReferentiel { + String code; + String libelle; + +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/model/Hypothese.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/model/Hypothese.java new file mode 100644 index 00000000..1c55577c --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/model/Hypothese.java @@ -0,0 +1,22 @@ +package org.mte.numecoeval.referentiel.domain.model; + +import lombok.AccessLevel; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.Accessors; +import lombok.experimental.FieldDefaults; + +@Getter +@Setter +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +@FieldDefaults(level = AccessLevel.PRIVATE) +@NoArgsConstructor +public class Hypothese implements AbstractReferentiel { + String code; + String valeur; + String source; + +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/model/ImpactEquipement.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/model/ImpactEquipement.java new file mode 100644 index 00000000..df20f22f --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/model/ImpactEquipement.java @@ -0,0 +1,29 @@ +package org.mte.numecoeval.referentiel.domain.model; + +import lombok.AccessLevel; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.Accessors; +import lombok.experimental.FieldDefaults; + +@Getter +@Setter +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +@FieldDefaults(level = AccessLevel.PRIVATE) +@NoArgsConstructor +public class ImpactEquipement implements AbstractReferentiel { + + String refEquipement; + String etape; + String critere; + String source; + String type; + Double valeur; + Double consoElecMoyenne; + String description; + + +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/model/ImpactMessagerie.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/model/ImpactMessagerie.java new file mode 100644 index 00000000..1da3e59c --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/model/ImpactMessagerie.java @@ -0,0 +1,24 @@ +package org.mte.numecoeval.referentiel.domain.model; + +import lombok.AccessLevel; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.Accessors; +import lombok.experimental.FieldDefaults; + +@Getter +@Setter +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +@FieldDefaults(level = AccessLevel.PRIVATE) +@NoArgsConstructor +public class ImpactMessagerie implements AbstractReferentiel { + + String critere; + Double constanteCoefficientDirecteur; + Double constanteOrdonneeOrigine; + String source; + +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/model/ImpactReseau.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/model/ImpactReseau.java new file mode 100644 index 00000000..c3a6672a --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/model/ImpactReseau.java @@ -0,0 +1,30 @@ +package org.mte.numecoeval.referentiel.domain.model; + + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.Accessors; +import lombok.experimental.FieldDefaults; + +@Getter +@Setter +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +@FieldDefaults(level = AccessLevel.PRIVATE) +@NoArgsConstructor +@AllArgsConstructor +public class ImpactReseau implements AbstractReferentiel { + + String refReseau; + String etape; + String critere; + String source; + Double valeur; + Double consoElecMoyenne; + + +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/model/MixElectrique.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/model/MixElectrique.java new file mode 100644 index 00000000..498d5038 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/model/MixElectrique.java @@ -0,0 +1,25 @@ +package org.mte.numecoeval.referentiel.domain.model; + +import lombok.AccessLevel; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.Accessors; +import lombok.experimental.FieldDefaults; + +@Getter +@Setter +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +@FieldDefaults(level = AccessLevel.PRIVATE) +@NoArgsConstructor +public class MixElectrique implements AbstractReferentiel { + + String pays; + String raccourcisAnglais; + String critere; + Double valeur; + String source; + +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/model/TypeEquipement.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/model/TypeEquipement.java new file mode 100644 index 00000000..e32a429c --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/model/TypeEquipement.java @@ -0,0 +1,21 @@ +package org.mte.numecoeval.referentiel.domain.model; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Data; +import lombok.experimental.FieldDefaults; + +@Builder +@Data +@FieldDefaults(level = AccessLevel.PRIVATE) +public class TypeEquipement implements AbstractReferentiel { + + String type; + boolean serveur; + String commentaire; + Double dureeVieDefaut; + String source; + // Référence de l'équipement par défaut, permet des correspondances en cas d'absence de correspondance direct. + String refEquipementParDefaut; + +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/model/id/CritereId.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/model/id/CritereId.java new file mode 100644 index 00000000..692a5fb7 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/model/id/CritereId.java @@ -0,0 +1,23 @@ +package org.mte.numecoeval.referentiel.domain.model.id; + +import lombok.AccessLevel; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.Accessors; +import lombok.experimental.FieldDefaults; + +import java.io.Serializable; + +@Getter +@Setter +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +@FieldDefaults(level = AccessLevel.PRIVATE) +@NoArgsConstructor +public class CritereId implements Serializable { + + String nomCritere; + +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/model/id/EtapeId.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/model/id/EtapeId.java new file mode 100644 index 00000000..b795f786 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/model/id/EtapeId.java @@ -0,0 +1,23 @@ +package org.mte.numecoeval.referentiel.domain.model.id; + +import lombok.AccessLevel; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.Accessors; +import lombok.experimental.FieldDefaults; + +import java.io.Serializable; + +@Getter +@Setter +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +@FieldDefaults(level = AccessLevel.PRIVATE) +@NoArgsConstructor +public class EtapeId implements Serializable { + + String code; + +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/model/id/HypotheseId.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/model/id/HypotheseId.java new file mode 100644 index 00000000..2c95cf39 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/model/id/HypotheseId.java @@ -0,0 +1,25 @@ +package org.mte.numecoeval.referentiel.domain.model.id; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.Accessors; +import lombok.experimental.FieldDefaults; + +import java.io.Serializable; + +@Getter +@Setter +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +@FieldDefaults(level = AccessLevel.PRIVATE) +@NoArgsConstructor +@AllArgsConstructor +public class HypotheseId implements Serializable { + + String code; + +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/model/id/ImpactEquipementId.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/model/id/ImpactEquipementId.java new file mode 100644 index 00000000..1bb9cb75 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/model/id/ImpactEquipementId.java @@ -0,0 +1,24 @@ +package org.mte.numecoeval.referentiel.domain.model.id; + +import lombok.AccessLevel; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.Accessors; +import lombok.experimental.FieldDefaults; + +import java.io.Serializable; + +@Getter +@Setter +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +@FieldDefaults(level = AccessLevel.PRIVATE) +@NoArgsConstructor +public class ImpactEquipementId implements Serializable { + String refEquipement; + String etape; + String critere; + +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/model/id/ImpactReseauId.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/model/id/ImpactReseauId.java new file mode 100644 index 00000000..22a3511d --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/model/id/ImpactReseauId.java @@ -0,0 +1,27 @@ +package org.mte.numecoeval.referentiel.domain.model.id; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.Accessors; +import lombok.experimental.FieldDefaults; + +import java.io.Serializable; + +@Getter +@Setter +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +@FieldDefaults(level = AccessLevel.PRIVATE) +@NoArgsConstructor +@AllArgsConstructor +public class ImpactReseauId implements Serializable { + + String refReseau; + String etape; + String critere; + +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/model/id/MixElectriqueId.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/model/id/MixElectriqueId.java new file mode 100644 index 00000000..b0f006f9 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/model/id/MixElectriqueId.java @@ -0,0 +1,22 @@ +package org.mte.numecoeval.referentiel.domain.model.id; + +import lombok.*; +import lombok.experimental.Accessors; +import lombok.experimental.FieldDefaults; + +import java.io.Serializable; + +@Getter +@Setter +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +@FieldDefaults(level = AccessLevel.PRIVATE) +@NoArgsConstructor +@AllArgsConstructor +@ToString +public class MixElectriqueId implements Serializable { + + String pays; + String critere; + +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/ports/input/ImportCSVReferentielPort.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/ports/input/ImportCSVReferentielPort.java new file mode 100644 index 00000000..85affa82 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/ports/input/ImportCSVReferentielPort.java @@ -0,0 +1,70 @@ +package org.mte.numecoeval.referentiel.domain.ports.input; + +import org.apache.commons.csv.CSVRecord; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.math.NumberUtils; +import org.mte.numecoeval.referentiel.domain.data.ResultatImport; +import org.mte.numecoeval.referentiel.domain.exception.ReferentielException; + +import java.io.InputStream; +import java.util.Arrays; + +public interface ImportCSVReferentielPort<T> { + + String CSV_SEPARATOR = ";"; + + String MESSAGE_AVERTISSEMENT_LIGNE_INCONSISTENTE = "Fichier %s : La ligne n°%d n'est pas consistente avec les headers du fichier"; + + String MESSAGE_LIGNE_INVALIDE = "La ligne n°%d est invalide : %s"; + + void checkCSVRecord(CSVRecord csvRecord) throws ReferentielException; + + default void checkAllHeadersAreMapped(CSVRecord csvRecord, String[] headers) throws ReferentielException{ + if(!Arrays.stream(headers).allMatch(csvRecord::isMapped)) { + throw new ReferentielException(MESSAGE_LIGNE_INVALIDE.formatted(csvRecord.getRecordNumber()+1, "Entêtes incohérentes")); + } + } + + default void checkFieldIsMappedInCSVRecord(CSVRecord csvRecord, String field) throws ReferentielException{ + if(!csvRecord.isMapped(field)) { + throw new ReferentielException(MESSAGE_LIGNE_INVALIDE.formatted(csvRecord.getRecordNumber()+1, "La colonne "+field+" doit être présente")); + } + } + + default void checkFieldIsMappedAndNotBlankInCSVRecord(CSVRecord csvRecord, String field) throws ReferentielException{ + checkFieldIsMappedInCSVRecord(csvRecord, field); + if(StringUtils.isBlank(csvRecord.get(field))) { + throw new ReferentielException(MESSAGE_LIGNE_INVALIDE.formatted(csvRecord.getRecordNumber()+1, "La colonne "+field+" ne peut être vide")); + } + } + + ResultatImport<T> importCSV(InputStream csvInputStream); + default Double getDoubleValueFromRecord(CSVRecord csvRecord, String field, Double defaultValue) { + if(!NumberUtils.isCreatable(StringUtils.trim(csvRecord.get(field)))) { + return defaultValue; + } + return NumberUtils.toDouble(csvRecord.get(field)); + } + + /** + * Renvoie la valeur de la colonne {@param mainName} dans le {@param csvRecord}. + * Si le {@param mainName} n'est pas mappé dans le {@link CSVRecord}, la liste des noms alternatifs est utilisée. + * Si aucune colonne n'est mappée ou que la liste alternative est vide, la valeur {@code null} est renvoyée. + * @param csvRecord La ligne de CSV à traiter + * @param mainName Le nom de la colonne souhaitée en 1er + * @param alternativeNames Les noms de colonnes alternatifs à utiliser, peut être vide + * @return La valeur de la 1er colonne correctement mappée sur la ligne de CSV, {@code null} en absence de mapping + */ + default String getStringValueFromRecord(CSVRecord csvRecord, String mainName, String... alternativeNames) { + if(csvRecord.isMapped(mainName)) { + return StringUtils.trim(csvRecord.get(mainName)); + } + + for (String alternativeName : alternativeNames) { + if(csvRecord.isMapped(alternativeName)) { + return StringUtils.trim(csvRecord.get(alternativeName)); + } + } + return null; + } +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/ports/input/impl/ImportCorrespondanceRefEquipementPortImpl.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/ports/input/impl/ImportCorrespondanceRefEquipementPortImpl.java new file mode 100644 index 00000000..ae62e8d4 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/ports/input/impl/ImportCorrespondanceRefEquipementPortImpl.java @@ -0,0 +1,75 @@ +package org.mte.numecoeval.referentiel.domain.ports.input.impl; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.csv.CSVFormat; +import org.apache.commons.csv.CSVRecord; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.CorrespondanceRefEquipementDTO; +import org.mte.numecoeval.referentiel.domain.data.ResultatImport; +import org.mte.numecoeval.referentiel.domain.exception.ReferentielException; +import org.mte.numecoeval.referentiel.domain.ports.input.ImportCSVReferentielPort; + +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +@Slf4j +public class ImportCorrespondanceRefEquipementPortImpl implements ImportCSVReferentielPort<CorrespondanceRefEquipementDTO> { + public static final String MODELE_EQUIPEMENT_SOURCE = "modeleEquipementSource"; + public static final String REF_EQUIPEMENT_CIBLE = "refEquipementCible"; + private static final String[] HEADERS = new String[]{MODELE_EQUIPEMENT_SOURCE, REF_EQUIPEMENT_CIBLE}; + + public static String[] getHeaders() { + return HEADERS; + } + + public void checkCSVRecord(CSVRecord csvRecord) throws ReferentielException { + checkAllHeadersAreMapped(csvRecord, HEADERS); + checkFieldIsMappedAndNotBlankInCSVRecord(csvRecord, MODELE_EQUIPEMENT_SOURCE); + checkFieldIsMappedAndNotBlankInCSVRecord(csvRecord, REF_EQUIPEMENT_CIBLE); + } + + @Override + public ResultatImport<CorrespondanceRefEquipementDTO> importCSV(InputStream csvInputStream) { + ResultatImport<CorrespondanceRefEquipementDTO> resultatImport = new ResultatImport<>(); + List<CorrespondanceRefEquipementDTO> dtos = new ArrayList<>(); + + try (Reader reader = new InputStreamReader(csvInputStream)) { + Iterable<CSVRecord> records = CSVFormat.DEFAULT.builder() + .setHeader() + .setDelimiter(CSV_SEPARATOR) + .setTrim(true) + .setAllowMissingColumnNames(true) + .setSkipHeaderRecord(true) + .build().parse(reader); + records.forEach(csvRecord -> { + try { + checkCSVRecord(csvRecord); + dtos.add(CorrespondanceRefEquipementDTO.builder() + .modeleEquipementSource(csvRecord.get(MODELE_EQUIPEMENT_SOURCE).trim()) + .refEquipementCible(csvRecord.get(REF_EQUIPEMENT_CIBLE).trim()) + .build()); + } + catch (Exception e) { + log.error("Erreur prévue lors de la lecture de la ligne {} : {}", csvRecord.getRecordNumber()+1, e.getMessage()); + resultatImport.getErreurs().add( e.getMessage() ); + } + }); + + } catch (Exception e) { + log.error("Erreur de traitement du fichier", e); + + resultatImport.setErreurs(Collections.singletonList("Le fichier CSV n'a pas pu être lu.")); + resultatImport.setNbrLignesImportees(0); + resultatImport.setObjects(null); + return resultatImport; + } + + resultatImport.setObjects(dtos); + resultatImport.setNbrLignesImportees(dtos.size()); + + return resultatImport; + } +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/ports/input/impl/ImportCriterePortImpl.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/ports/input/impl/ImportCriterePortImpl.java new file mode 100644 index 00000000..c622f8b4 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/ports/input/impl/ImportCriterePortImpl.java @@ -0,0 +1,74 @@ +package org.mte.numecoeval.referentiel.domain.ports.input.impl; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.csv.CSVFormat; +import org.apache.commons.csv.CSVRecord; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.CritereDTO; +import org.mte.numecoeval.referentiel.domain.data.ResultatImport; +import org.mte.numecoeval.referentiel.domain.exception.ReferentielException; +import org.mte.numecoeval.referentiel.domain.ports.input.ImportCSVReferentielPort; + +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +@Slf4j +public class ImportCriterePortImpl implements ImportCSVReferentielPort<CritereDTO> { + private static final String HEADER_NOM_CRITERE = "nomCritere"; + private static final String[] HEADERS = new String[]{HEADER_NOM_CRITERE, "description", "unite"}; + + public static String[] getHeaders() { + return HEADERS; + } + + public void checkCSVRecord(CSVRecord csvRecord) throws ReferentielException { + checkAllHeadersAreMapped(csvRecord, HEADERS); + checkFieldIsMappedAndNotBlankInCSVRecord(csvRecord, HEADER_NOM_CRITERE); + } + + @Override + public ResultatImport<CritereDTO> importCSV(InputStream csvInputStream) { + ResultatImport<CritereDTO> resultatImport = new ResultatImport<>(); + List<CritereDTO> dtos = new ArrayList<>(); + + try (Reader reader = new InputStreamReader(csvInputStream)) { + Iterable<CSVRecord> records = CSVFormat.DEFAULT.builder() + .setHeader() + .setDelimiter(CSV_SEPARATOR) + .setTrim(true) + .setAllowMissingColumnNames(true) + .setSkipHeaderRecord(true) + .build().parse(reader); + records.forEach(csvRecord -> { + try { + checkCSVRecord(csvRecord); + dtos.add(CritereDTO.builder() + .nomCritere(csvRecord.get(HEADER_NOM_CRITERE).trim()) + .description(csvRecord.get("description")) + .unite(csvRecord.get("unite")) + .build()); + } + catch (Exception e) { + log.error("Erreur prévue lors de la lecture de la ligne {} : {}", csvRecord.getRecordNumber()+1, e.getMessage()); + resultatImport.getErreurs().add( e.getMessage() ); + } + }); + + } catch (Exception e) { + log.error("Erreur de traitement du fichier", e); + + resultatImport.setErreurs(Collections.singletonList("Le fichier CSV n'a pas pu être lu.")); + resultatImport.setNbrLignesImportees(0); + resultatImport.setObjects(null); + return resultatImport; + } + + resultatImport.setObjects(dtos); + resultatImport.setNbrLignesImportees(dtos.size()); + + return resultatImport; + } +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/ports/input/impl/ImportEtapePortImpl.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/ports/input/impl/ImportEtapePortImpl.java new file mode 100644 index 00000000..6358dcd6 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/ports/input/impl/ImportEtapePortImpl.java @@ -0,0 +1,73 @@ +package org.mte.numecoeval.referentiel.domain.ports.input.impl; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.csv.CSVFormat; +import org.apache.commons.csv.CSVRecord; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.EtapeDTO; +import org.mte.numecoeval.referentiel.domain.data.ResultatImport; +import org.mte.numecoeval.referentiel.domain.exception.ReferentielException; +import org.mte.numecoeval.referentiel.domain.ports.input.ImportCSVReferentielPort; + +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +@Slf4j +public class ImportEtapePortImpl implements ImportCSVReferentielPort<EtapeDTO> { + private static final String[] HEADERS = new String[]{"code", "libelle"}; + + public static String[] getHeaders() { + return HEADERS; + } + + @Override + public void checkCSVRecord(CSVRecord csvRecord) throws ReferentielException { + checkAllHeadersAreMapped(csvRecord, HEADERS); + checkFieldIsMappedAndNotBlankInCSVRecord(csvRecord, "code"); + } + + @Override + public ResultatImport<EtapeDTO> importCSV(InputStream csvInputStream) { + ResultatImport<EtapeDTO> resultatImport = new ResultatImport<>(); + List<EtapeDTO> etapeDTOS = new ArrayList<>(); + + try (Reader reader = new InputStreamReader(csvInputStream)) { + Iterable<CSVRecord> records = CSVFormat.DEFAULT.builder() + .setHeader() + .setDelimiter(CSV_SEPARATOR) + .setTrim(true) + .setAllowMissingColumnNames(true) + .setSkipHeaderRecord(true) + .build().parse(reader); + records.forEach(csvRecord -> { + try { + checkCSVRecord(csvRecord); + etapeDTOS.add(EtapeDTO.builder() + .code(csvRecord.get("code").trim()) + .libelle(csvRecord.get("libelle").trim()) + .build()); + } + catch (Exception e) { + log.error("Erreur prévue lors de la lecture de la ligne {} : {}", csvRecord.getRecordNumber()+1, e.getMessage()); + resultatImport.getErreurs().add( e.getMessage() ); + } + }); + + } catch (Exception e) { + log.error("Erreur de traitement du fichier", e); + + resultatImport.setErreurs(Collections.singletonList("Le fichier CSV n'a pas pu être lu.")); + resultatImport.setNbrLignesImportees(0); + resultatImport.setObjects(null); + return resultatImport; + } + + resultatImport.setObjects(etapeDTOS); + resultatImport.setNbrLignesImportees(etapeDTOS.size()); + + return resultatImport; + } +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/ports/input/impl/ImportHypothesePortImpl.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/ports/input/impl/ImportHypothesePortImpl.java new file mode 100644 index 00000000..a14c212b --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/ports/input/impl/ImportHypothesePortImpl.java @@ -0,0 +1,75 @@ +package org.mte.numecoeval.referentiel.domain.ports.input.impl; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.csv.CSVFormat; +import org.apache.commons.csv.CSVRecord; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.HypotheseDTO; +import org.mte.numecoeval.referentiel.domain.data.ResultatImport; +import org.mte.numecoeval.referentiel.domain.exception.ReferentielException; +import org.mte.numecoeval.referentiel.domain.ports.input.ImportCSVReferentielPort; + +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +@Slf4j +public class ImportHypothesePortImpl implements ImportCSVReferentielPort<HypotheseDTO> { + private static final String HEADER_VALEUR = "valeur"; + private static final String[] HEADERS = new String[]{"cle", HEADER_VALEUR, "source"}; + + public static String[] getHeaders() { + return HEADERS; + } + + public void checkCSVRecord(CSVRecord csvRecord) throws ReferentielException { + checkAllHeadersAreMapped(csvRecord, HEADERS); + checkFieldIsMappedAndNotBlankInCSVRecord(csvRecord, "cle"); + checkFieldIsMappedAndNotBlankInCSVRecord(csvRecord, HEADER_VALEUR); + } + + @Override + public ResultatImport<HypotheseDTO> importCSV(InputStream csvInputStream) { + ResultatImport<HypotheseDTO> resultatImport = new ResultatImport<>(); + List<HypotheseDTO> dtos = new ArrayList<>(); + + try (Reader reader = new InputStreamReader(csvInputStream)) { + Iterable<CSVRecord> records = CSVFormat.DEFAULT.builder() + .setHeader() + .setDelimiter(CSV_SEPARATOR) + .setTrim(true) + .setAllowMissingColumnNames(true) + .setSkipHeaderRecord(true) + .build().parse(reader); + records.forEach(csvRecord -> { + try { + checkCSVRecord(csvRecord); + dtos.add(HypotheseDTO.builder() + .valeur(csvRecord.get(HEADER_VALEUR).trim()) + .code(csvRecord.get("cle").trim()) + .source(csvRecord.get("source").trim()) + .build()); + } + catch (Exception e) { + log.error("Erreur prévue lors de la lecture de la ligne {} : {}", csvRecord.getRecordNumber()+1, e.getMessage()); + resultatImport.getErreurs().add( e.getMessage() ); + } + }); + + } catch (Exception e) { + log.error("Erreur de traitement du fichier", e); + + resultatImport.setErreurs(Collections.singletonList("Le fichier CSV n'a pas pu être lu.")); + resultatImport.setNbrLignesImportees(0); + resultatImport.setObjects(null); + return resultatImport; + } + + resultatImport.setObjects(dtos); + resultatImport.setNbrLignesImportees(dtos.size()); + + return resultatImport; + } +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/ports/input/impl/ImportImpactEquipementPortImpl.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/ports/input/impl/ImportImpactEquipementPortImpl.java new file mode 100644 index 00000000..5b44f79b --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/ports/input/impl/ImportImpactEquipementPortImpl.java @@ -0,0 +1,90 @@ +package org.mte.numecoeval.referentiel.domain.ports.input.impl; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.csv.CSVFormat; +import org.apache.commons.csv.CSVRecord; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.ImpactEquipementDTO; +import org.mte.numecoeval.referentiel.domain.data.ResultatImport; +import org.mte.numecoeval.referentiel.domain.exception.ReferentielException; +import org.mte.numecoeval.referentiel.domain.ports.input.ImportCSVReferentielPort; + +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +@Slf4j +public class ImportImpactEquipementPortImpl implements ImportCSVReferentielPort<ImpactEquipementDTO> { + private static final String HEADER_REF_EQUIPEMENT = "refEquipement"; + private static final String HEADER_ETAPEACV = "etapeacv"; + private static final String HEADER_CRITERE = "critere"; + private static final String HEADER_CONSO_ELEC_MOYENNE = "consoElecMoyenne"; + private static final String HEADER_VALEUR = "valeur"; + private static final String[] HEADERS = new String[]{ + HEADER_REF_EQUIPEMENT, HEADER_ETAPEACV, HEADER_CRITERE, + HEADER_CONSO_ELEC_MOYENNE, HEADER_VALEUR, "source", "type" + }; + + public static String[] getHeaders() { + return HEADERS; + } + + public void checkCSVRecord(CSVRecord csvRecord) throws ReferentielException { + checkAllHeadersAreMapped(csvRecord, HEADERS); + checkFieldIsMappedAndNotBlankInCSVRecord(csvRecord, HEADER_REF_EQUIPEMENT); + checkFieldIsMappedAndNotBlankInCSVRecord(csvRecord, HEADER_ETAPEACV); + checkFieldIsMappedAndNotBlankInCSVRecord(csvRecord, HEADER_CRITERE); + checkFieldIsMappedInCSVRecord(csvRecord, HEADER_CONSO_ELEC_MOYENNE); + checkFieldIsMappedInCSVRecord(csvRecord, HEADER_VALEUR); + } + + @Override + public ResultatImport<ImpactEquipementDTO> importCSV(InputStream csvInputStream) { + ResultatImport<ImpactEquipementDTO> resultatImport = new ResultatImport<>(); + List<ImpactEquipementDTO> dtos = new ArrayList<>(); + + try (Reader reader = new InputStreamReader(csvInputStream)) { + Iterable<CSVRecord> records = CSVFormat.DEFAULT.builder() + .setHeader() + .setDelimiter(CSV_SEPARATOR) + .setTrim(true) + .setAllowMissingColumnNames(true) + .setSkipHeaderRecord(true) + .build().parse(reader); + records.forEach(csvRecord -> { + try { + checkCSVRecord(csvRecord); + dtos.add(ImpactEquipementDTO.builder() + .refEquipement(csvRecord.get(HEADER_REF_EQUIPEMENT).trim()) + .etape(csvRecord.get(HEADER_ETAPEACV).trim()) + .critere(csvRecord.get(HEADER_CRITERE).trim()) + .consoElecMoyenne(getDoubleValueFromRecord(csvRecord, HEADER_CONSO_ELEC_MOYENNE, null)) + .valeur(getDoubleValueFromRecord(csvRecord, HEADER_VALEUR, null)) + .source(getStringValueFromRecord(csvRecord, "source")) + .type(getStringValueFromRecord(csvRecord,"type")) + .description(getStringValueFromRecord(csvRecord,"description")) + .build()); + } + catch (Exception e) { + log.error("Erreur prévue lors de la lecture de la ligne {} : {}", csvRecord.getRecordNumber()+1, e.getMessage()); + resultatImport.getErreurs().add( e.getMessage() ); + } + }); + + } catch (Exception e) { + log.error("Erreur de traitement du fichier", e); + + resultatImport.setErreurs(Collections.singletonList("Le fichier CSV n'a pas pu être lu.")); + resultatImport.setNbrLignesImportees(0); + resultatImport.setObjects(null); + return resultatImport; + } + + resultatImport.setObjects(dtos); + resultatImport.setNbrLignesImportees(dtos.size()); + + return resultatImport; + } +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/ports/input/impl/ImportImpactMessageriePortImpl.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/ports/input/impl/ImportImpactMessageriePortImpl.java new file mode 100644 index 00000000..53d24fc0 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/ports/input/impl/ImportImpactMessageriePortImpl.java @@ -0,0 +1,74 @@ +package org.mte.numecoeval.referentiel.domain.ports.input.impl; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.csv.CSVFormat; +import org.apache.commons.csv.CSVRecord; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.ImpactMessagerieDTO; +import org.mte.numecoeval.referentiel.domain.data.ResultatImport; +import org.mte.numecoeval.referentiel.domain.exception.ReferentielException; +import org.mte.numecoeval.referentiel.domain.ports.input.ImportCSVReferentielPort; + +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +@Slf4j +public class ImportImpactMessageriePortImpl implements ImportCSVReferentielPort<ImpactMessagerieDTO> { + private static final String HEADER_CRITERE = "critere"; + private static final String[] HEADERS = new String[]{HEADER_CRITERE,"constanteCoefficientDirecteur","constanteOrdonneeOrigine","source"}; + + public static String[] getHeaders() { + return HEADERS; + } + + public void checkCSVRecord(CSVRecord csvRecord) throws ReferentielException { + checkAllHeadersAreMapped(csvRecord, HEADERS); + checkFieldIsMappedAndNotBlankInCSVRecord(csvRecord, HEADER_CRITERE); + } + + @Override + public ResultatImport<ImpactMessagerieDTO> importCSV(InputStream csvInputStream) { + ResultatImport<ImpactMessagerieDTO> resultatImport = new ResultatImport<>(); + List<ImpactMessagerieDTO> dtos = new ArrayList<>(); + + try (Reader reader = new InputStreamReader(csvInputStream)) { + Iterable<CSVRecord> records = CSVFormat.DEFAULT.builder() + .setHeader() + .setDelimiter(CSV_SEPARATOR) + .setTrim(true) + .setAllowMissingColumnNames(true) + .setSkipHeaderRecord(true) + .build().parse(reader); + records.forEach(csvRecord -> { + try { + checkCSVRecord(csvRecord); + dtos.add(ImpactMessagerieDTO.builder() + .critere(csvRecord.get(HEADER_CRITERE).trim()) + .constanteCoefficientDirecteur(getDoubleValueFromRecord(csvRecord,"constanteCoefficientDirecteur", null)) + .constanteOrdonneeOrigine(getDoubleValueFromRecord(csvRecord,"constanteOrdonneeOrigine", null)) + .source(csvRecord.get("source").trim()) + .build()); + } + catch (Exception e) { + log.error("Erreur prévue lors de la lecture de la ligne {} : {}", csvRecord.getRecordNumber()+1, e.getMessage()); + resultatImport.getErreurs().add( e.getMessage() ); + } + }); + + } catch (Exception e) { + log.error("Erreur de traitement du fichier", e); + + resultatImport.setErreurs(Collections.singletonList("Le fichier CSV n'a pas pu être lu.")); + resultatImport.setNbrLignesImportees(0); + resultatImport.setObjects(null); + return resultatImport; + } + resultatImport.setObjects(dtos); + resultatImport.setNbrLignesImportees(dtos.size()); + return resultatImport; + } + +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/ports/input/impl/ImportImpactReseauPortImpl.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/ports/input/impl/ImportImpactReseauPortImpl.java new file mode 100644 index 00000000..c4d6e0d7 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/ports/input/impl/ImportImpactReseauPortImpl.java @@ -0,0 +1,87 @@ +package org.mte.numecoeval.referentiel.domain.ports.input.impl; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.csv.CSVFormat; +import org.apache.commons.csv.CSVRecord; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.ImpactReseauDTO; +import org.mte.numecoeval.referentiel.domain.data.ResultatImport; +import org.mte.numecoeval.referentiel.domain.exception.ReferentielException; +import org.mte.numecoeval.referentiel.domain.ports.input.ImportCSVReferentielPort; + +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +@Slf4j +public class ImportImpactReseauPortImpl implements ImportCSVReferentielPort<ImpactReseauDTO> { + private static final String HEADER_REF_RESEAU = "refReseau"; + private static final String HEADER_ETAPE_ACV = "etapeACV"; + private static final String HEADER_CRITERE = "critere"; + private static final String HEADER_VALEUR = "valeur"; + private static final String HEADER_CONSO_ELEC_MOYENNE = "consoElecMoyenne"; + private static final String[] HEADERS = new String[]{ + HEADER_REF_RESEAU, HEADER_ETAPE_ACV, HEADER_CRITERE, HEADER_VALEUR, + HEADER_CONSO_ELEC_MOYENNE, "source" + }; + + public static String[] getHeaders() { + return HEADERS; + } + + public void checkCSVRecord(CSVRecord csvRecord) throws ReferentielException { + checkAllHeadersAreMapped(csvRecord, HEADERS); + checkFieldIsMappedAndNotBlankInCSVRecord(csvRecord, HEADER_REF_RESEAU); + checkFieldIsMappedAndNotBlankInCSVRecord(csvRecord, HEADER_ETAPE_ACV); + checkFieldIsMappedAndNotBlankInCSVRecord(csvRecord, HEADER_CRITERE); + checkFieldIsMappedInCSVRecord(csvRecord, HEADER_CONSO_ELEC_MOYENNE); + checkFieldIsMappedInCSVRecord(csvRecord, HEADER_VALEUR); + } + + @Override + public ResultatImport<ImpactReseauDTO> importCSV(InputStream csvInputStream) { + ResultatImport<ImpactReseauDTO> resultatImport = new ResultatImport<>(); + List<ImpactReseauDTO> dtos = new ArrayList<>(); + + try (Reader reader = new InputStreamReader(csvInputStream)) { + Iterable<CSVRecord> records = CSVFormat.DEFAULT.builder() + .setHeader() + .setDelimiter(CSV_SEPARATOR) + .setTrim(true) + .setAllowMissingColumnNames(true) + .setSkipHeaderRecord(true) + .build().parse(reader); + records.forEach(csvRecord -> { + try { + checkCSVRecord(csvRecord); + dtos.add(ImpactReseauDTO.builder() + .critere(csvRecord.get(HEADER_CRITERE).trim()) + .refReseau(csvRecord.get(HEADER_REF_RESEAU).trim()) + .consoElecMoyenne(getDoubleValueFromRecord(csvRecord, HEADER_CONSO_ELEC_MOYENNE, null)) + .valeur(getDoubleValueFromRecord(csvRecord, HEADER_VALEUR, null)) + .source(csvRecord.get("source").trim()) + .etapeACV(csvRecord.get(HEADER_ETAPE_ACV).trim()) + .build()); + } catch (Exception e) { + log.error("Erreur prévue lors de la lecture de la ligne {} : {}", csvRecord.getRecordNumber() + 1, e.getMessage()); + resultatImport.getErreurs().add(e.getMessage()); + } + }); + + } catch (Exception e) { + log.error("Erreur de traitement du fichier", e); + + resultatImport.setErreurs(Collections.singletonList("Le fichier CSV n'a pas pu être lu.")); + resultatImport.setNbrLignesImportees(0); + resultatImport.setObjects(null); + return resultatImport; + } + + resultatImport.setObjects(dtos); + resultatImport.setNbrLignesImportees(dtos.size()); + + return resultatImport; + } +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/ports/input/impl/ImportMixElectriquePortImpl.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/ports/input/impl/ImportMixElectriquePortImpl.java new file mode 100644 index 00000000..78978f4d --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/ports/input/impl/ImportMixElectriquePortImpl.java @@ -0,0 +1,80 @@ +package org.mte.numecoeval.referentiel.domain.ports.input.impl; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.csv.CSVFormat; +import org.apache.commons.csv.CSVRecord; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.MixElectriqueDTO; +import org.mte.numecoeval.referentiel.domain.data.ResultatImport; +import org.mte.numecoeval.referentiel.domain.exception.ReferentielException; +import org.mte.numecoeval.referentiel.domain.ports.input.ImportCSVReferentielPort; + +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +@Slf4j +public class ImportMixElectriquePortImpl implements ImportCSVReferentielPort<MixElectriqueDTO> { + private static final String HEADER_PAYS = "pays"; + private static final String HEADER_RACCOURCIS_ANGLAIS = "raccourcisAnglais"; + private static final String HEADER_CRITERE = "critere"; + private static final String[] HEADERS = new String[]{HEADER_PAYS, HEADER_RACCOURCIS_ANGLAIS, HEADER_CRITERE, "valeur", "source"}; + + public static String[] getHeaders() { + return HEADERS; + } + + public void checkCSVRecord(CSVRecord csvRecord) throws ReferentielException { + checkAllHeadersAreMapped(csvRecord, HEADERS); + checkFieldIsMappedAndNotBlankInCSVRecord(csvRecord, HEADER_PAYS); + checkFieldIsMappedAndNotBlankInCSVRecord(csvRecord, HEADER_RACCOURCIS_ANGLAIS); + checkFieldIsMappedAndNotBlankInCSVRecord(csvRecord, HEADER_CRITERE); + } + + @Override + public ResultatImport<MixElectriqueDTO> importCSV(InputStream csvInputStream) { + ResultatImport<MixElectriqueDTO> resultatImport = new ResultatImport<>(); + List<MixElectriqueDTO> dtos = new ArrayList<>(); + + try (Reader reader = new InputStreamReader(csvInputStream)) { + Iterable<CSVRecord> records = CSVFormat.DEFAULT.builder() + .setHeader() + .setDelimiter(CSV_SEPARATOR) + .setAllowMissingColumnNames(true) + .setTrim(true) + .setSkipHeaderRecord(true) + .build().parse(reader); + records.forEach(csvRecord -> { + try { + checkCSVRecord(csvRecord); + dtos.add(MixElectriqueDTO.builder() + .pays(csvRecord.get(HEADER_PAYS).trim()) + .raccourcisAnglais(csvRecord.get(HEADER_RACCOURCIS_ANGLAIS).trim()) + .critere(csvRecord.get(HEADER_CRITERE).trim()) + .valeur(getDoubleValueFromRecord(csvRecord,"valeur",null)) + .source(csvRecord.get("source")) + .build()); + } + catch (Exception e) { + log.error("Erreur prévue lors de la lecture de la ligne {} : {}", csvRecord.getRecordNumber()+1, e.getMessage()); + resultatImport.getErreurs().add( e.getMessage() ); + } + }); + + } catch (Exception e) { + log.error("Erreur de traitement du fichier", e); + + resultatImport.setErreurs(Collections.singletonList("Le fichier CSV n'a pas pu être lu.")); + resultatImport.setNbrLignesImportees(0); + resultatImport.setObjects(null); + return resultatImport; + } + + resultatImport.setObjects(dtos); + resultatImport.setNbrLignesImportees(dtos.size()); + + return resultatImport; + } +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/ports/input/impl/ImportTypeEquipementPortImpl.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/ports/input/impl/ImportTypeEquipementPortImpl.java new file mode 100644 index 00000000..493c2535 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/ports/input/impl/ImportTypeEquipementPortImpl.java @@ -0,0 +1,77 @@ +package org.mte.numecoeval.referentiel.domain.ports.input.impl; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.csv.CSVFormat; +import org.apache.commons.csv.CSVRecord; +import org.apache.commons.lang3.BooleanUtils; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.TypeEquipementDTO; +import org.mte.numecoeval.referentiel.domain.data.ResultatImport; +import org.mte.numecoeval.referentiel.domain.exception.ReferentielException; +import org.mte.numecoeval.referentiel.domain.ports.input.ImportCSVReferentielPort; + +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +@Slf4j +public class ImportTypeEquipementPortImpl implements ImportCSVReferentielPort<TypeEquipementDTO> { + private static final String HEADER_TYPE = "type"; + private static final String[] HEADERS = new String[]{HEADER_TYPE,"serveur","commentaire","dureeVieDefaut","source","refEquipementParDefaut"}; + + public static String[] getHeaders() { + return HEADERS; + } + + public void checkCSVRecord(CSVRecord csvRecord) throws ReferentielException { + checkAllHeadersAreMapped(csvRecord, HEADERS); + checkFieldIsMappedAndNotBlankInCSVRecord(csvRecord, HEADER_TYPE); + } + + @Override + public ResultatImport<TypeEquipementDTO> importCSV(InputStream csvInputStream) { + ResultatImport<TypeEquipementDTO> resultatImport = new ResultatImport<>(); + List<TypeEquipementDTO> dtos = new ArrayList<>(); + + try (Reader reader = new InputStreamReader(csvInputStream)) { + Iterable<CSVRecord> records = CSVFormat.DEFAULT.builder() + .setHeader() + .setDelimiter(CSV_SEPARATOR) + .setTrim(true) + .setAllowMissingColumnNames(true) + .setSkipHeaderRecord(true) + .build().parse(reader); + records.forEach(csvRecord -> { + try { + checkCSVRecord(csvRecord); + dtos.add(TypeEquipementDTO.builder() + .type(csvRecord.get(HEADER_TYPE).trim()) + .serveur(BooleanUtils.toBoolean(csvRecord.get("serveur").trim())) + .commentaire(csvRecord.get("commentaire").trim()) + .dureeVieDefaut(getDoubleValueFromRecord(csvRecord,"dureeVieDefaut",null)) + .source(csvRecord.get("source").trim()) + .refEquipementParDefaut(getStringValueFromRecord(csvRecord, "refEquipementParDefaut")) + .build()); + } + catch (Exception e) { + log.error("Erreur prévue lors de la lecture de la ligne {} : {}", csvRecord.getRecordNumber()+1, e.getMessage()); + resultatImport.getErreurs().add( e.getMessage() ); + } + }); + + } catch (Exception e) { + log.error("Erreur de traitement du fichier", e); + + resultatImport.setErreurs(Collections.singletonList("Le fichier CSV n'a pas pu être lu.")); + resultatImport.setNbrLignesImportees(0); + resultatImport.setObjects(null); + return resultatImport; + } + resultatImport.setObjects(dtos); + resultatImport.setNbrLignesImportees(dtos.size()); + return resultatImport; + } + +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/ports/output/ReferentielCsvExportService.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/ports/output/ReferentielCsvExportService.java new file mode 100644 index 00000000..45902d67 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/ports/output/ReferentielCsvExportService.java @@ -0,0 +1,111 @@ +package org.mte.numecoeval.referentiel.domain.ports.output; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.csv.CSVFormat; +import org.apache.commons.csv.CSVPrinter; +import org.apache.commons.lang3.StringUtils; + +import java.io.IOException; +import java.io.Writer; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.util.List; +import java.util.Locale; +import java.util.Objects; + +@Slf4j +public abstract class ReferentielCsvExportService<T> { + static final String DELIMITER = ";"; + + final Class<T> typeParameterClass; + + protected ReferentielCsvExportService(Class<T> typeParameterClass) { + this.typeParameterClass = typeParameterClass; + } + + /** + * Headers du fichier CSV. + * @return Tableau des headers du fichier CSV généré. + */ + protected abstract String[] getHeaders(); + + /** + * Ecriture d'une ligne dans le fichier CSV. + * @param csvPrinter {@link CSVPrinter} à utiliser + * @param objectToWrite objet à traiter + * @throws IOException levée en cas d'erreur de la ligne viw le {@link CSVPrinter} + */ + protected abstract void printRecord(CSVPrinter csvPrinter, T objectToWrite) throws IOException; + + /** + * Renvoie la liste des objets à traiter pour le CSV. + * @return Liste des objets à traiter, peut être {@code null} + */ + protected abstract List<T> getObjectsToWrite(); + + /** + * Méthode appelé pour identifier un objet à traiter dans les logs d'erreur spécifique. + * @param object objet à traiter, ne peut être {@code null} + * @return {@link String} perettant d'identifier l'objet + */ + protected abstract String getObjectId(T object); + + /** + * Renvoie la chaine de caractères correctement formatté pour un {@link Double} + * @param doubleValue valeur à traiter + * @return {@link String} du représentant le double au bon format + */ + protected String formatDouble(Double doubleValue) { + if(Objects.isNull(doubleValue)) { + return StringUtils.EMPTY; + } + DecimalFormat df = new DecimalFormat("0", DecimalFormatSymbols.getInstance(Locale.ENGLISH)); + df.setMaximumFractionDigits(340); + return df.format(doubleValue); + } + + /** + * Log d'une erreur sur l'écriture d'une ligne du fichier CSV. + * @param objectToWrite objet à traiter + * @param exception {@link Exception} levée lors de l'erreur sur l'écriture de la ligne + */ + public void logRecordError(T objectToWrite, Exception exception) { + var logMessage = "Erreur d'écriture d'une ligne d'objet "+ typeParameterClass.getName(); + if(Objects.nonNull(objectToWrite)) { + logMessage = "Erreur d'écriture d'une ligne d'objet %s : %s".formatted(typeParameterClass.getName(), getObjectId(objectToWrite)); + } + log.error(logMessage, exception); + } + + /** + * Logs d'une erreur en cas de problème à l'écriture via {@link Writer} + * @param exception {@link Exception} levée lors de l'erreur sur l'écriture dans le Writer + */ + public void logWriterError(Exception exception) { + log.error("Erreur d'écriture du fichier CSV des objets {}", typeParameterClass.getName(), exception); + } + + /** + * Ecriture du fichier CSV via {@link Writer}. + * @see #printRecord(CSVPrinter, Object) + * @see #logRecordError(Object, Exception) + * @see #logWriterError(Exception) + * @param writer {@link Writer} à utiliser + */ + public void writeToCsv(Writer writer) { + List<T> objects = getObjectsToWrite(); + CSVFormat csvFormat = CSVFormat.Builder.create().setHeader(getHeaders()).setDelimiter(DELIMITER).build(); + try (CSVPrinter csvPrinter = new CSVPrinter(writer, csvFormat)) { + CollectionUtils.emptyIfNull(objects).forEach(objectToWrite -> { + try { + printRecord(csvPrinter, objectToWrite); + } catch (Exception e) { + logRecordError(objectToWrite, e); + } + }); + } catch (Exception e) { + logWriterError(e); + } + } +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/ports/output/ReferentielPersistencePort.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/ports/output/ReferentielPersistencePort.java new file mode 100644 index 00000000..8ea16327 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/domain/ports/output/ReferentielPersistencePort.java @@ -0,0 +1,49 @@ +package org.mte.numecoeval.referentiel.domain.ports.output; + +import org.mte.numecoeval.referentiel.domain.exception.ReferentielException; +import org.mte.numecoeval.referentiel.domain.model.AbstractReferentiel; + +import java.util.Collection; +import java.util.List; + + +public interface ReferentielPersistencePort<T extends AbstractReferentiel, P extends Object> { + + /** + * Sauvegarde et mise à jour + * + * @param referentiel + * @return + */ + T save(T referentiel) throws ReferentielException; + + /** + * Ajout en masse d'une liste + * + * @param referentiel + * @throws ReferentielException + */ + void saveAll(Collection<T> referentiel) throws ReferentielException; + + /** + * Recherche par id + * + * @param id + * @return + */ + T get(P id) throws ReferentielException; + + /** + * purge entity + */ + void purge(); + + /** + * liste des elements d'une table + * + * @return + */ + List<T> getAll(); + + +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/adapter/export/CorrespondanceRefEquipemenetCsvExportService.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/adapter/export/CorrespondanceRefEquipemenetCsvExportService.java new file mode 100644 index 00000000..c580f5be --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/adapter/export/CorrespondanceRefEquipemenetCsvExportService.java @@ -0,0 +1,43 @@ +package org.mte.numecoeval.referentiel.infrastructure.adapter.export; + +import org.apache.commons.csv.CSVPrinter; +import org.mte.numecoeval.referentiel.domain.ports.input.impl.ImportHypothesePortImpl; +import org.mte.numecoeval.referentiel.domain.ports.output.ReferentielCsvExportService; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.CorrespondanceRefEquipementEntity; +import org.mte.numecoeval.referentiel.infrastructure.jpa.repository.CorrespondanceRefEquipementRepository; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.util.List; + +@Service +public class CorrespondanceRefEquipemenetCsvExportService extends ReferentielCsvExportService<CorrespondanceRefEquipementEntity> { + + private final CorrespondanceRefEquipementRepository repository; + + public CorrespondanceRefEquipemenetCsvExportService(CorrespondanceRefEquipementRepository repository) { + super(CorrespondanceRefEquipementEntity.class); + this.repository = repository; + } + + @Override + public String[] getHeaders() { + return ImportHypothesePortImpl.getHeaders(); + } + + @Override + public List<CorrespondanceRefEquipementEntity> getObjectsToWrite() { + return repository.findAll(); + } + + @Override + protected String getObjectId(CorrespondanceRefEquipementEntity object) { + return object.getModeleEquipementSource(); + } + + @Override + public void printRecord(CSVPrinter csvPrinter, CorrespondanceRefEquipementEntity objectToWrite) throws IOException { + csvPrinter.printRecord(objectToWrite.getModeleEquipementSource(),objectToWrite.getRefEquipementCible()); + } + +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/adapter/export/CritereCsvExportService.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/adapter/export/CritereCsvExportService.java new file mode 100644 index 00000000..0836535d --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/adapter/export/CritereCsvExportService.java @@ -0,0 +1,43 @@ +package org.mte.numecoeval.referentiel.infrastructure.adapter.export; + +import org.apache.commons.csv.CSVPrinter; +import org.mte.numecoeval.referentiel.domain.ports.input.impl.ImportCriterePortImpl; +import org.mte.numecoeval.referentiel.domain.ports.output.ReferentielCsvExportService; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.CritereEntity; +import org.mte.numecoeval.referentiel.infrastructure.jpa.repository.CritereRepository; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.util.List; + +@Service +public class CritereCsvExportService extends ReferentielCsvExportService<CritereEntity> { + + private final CritereRepository repository; + + public CritereCsvExportService(CritereRepository repository) { + super(CritereEntity.class); + this.repository = repository; + } + + @Override + public String[] getHeaders() { + return ImportCriterePortImpl.getHeaders(); + } + + @Override + public List<CritereEntity> getObjectsToWrite() { + return repository.findAll(); + } + + @Override + protected String getObjectId(CritereEntity object) { + return object.getNomCritere(); + } + + @Override + public void printRecord(CSVPrinter csvPrinter, CritereEntity objectToWrite) throws IOException { + csvPrinter.printRecord(objectToWrite.getNomCritere(),objectToWrite.getDescription(),objectToWrite.getUnite()); + } + +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/adapter/export/EtapeCsvExportService.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/adapter/export/EtapeCsvExportService.java new file mode 100644 index 00000000..d6bfabf1 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/adapter/export/EtapeCsvExportService.java @@ -0,0 +1,41 @@ +package org.mte.numecoeval.referentiel.infrastructure.adapter.export; + +import org.apache.commons.csv.CSVPrinter; +import org.mte.numecoeval.referentiel.domain.ports.input.impl.ImportEtapePortImpl; +import org.mte.numecoeval.referentiel.domain.ports.output.ReferentielCsvExportService; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.EtapeEntity; +import org.mte.numecoeval.referentiel.infrastructure.jpa.repository.EtapeRepository; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.util.List; + +@Service +public class EtapeCsvExportService extends ReferentielCsvExportService<EtapeEntity> { + private final EtapeRepository repository; + + public EtapeCsvExportService(EtapeRepository repository) { + super(EtapeEntity.class); + this.repository = repository; + } + + @Override + public String[] getHeaders() { + return ImportEtapePortImpl.getHeaders(); + } + + @Override + public List<EtapeEntity> getObjectsToWrite() { + return repository.findAll(); + } + + @Override + protected String getObjectId(EtapeEntity object) { + return object.getCode(); + } + + @Override + public void printRecord(CSVPrinter csvPrinter, EtapeEntity objectToWrite) throws IOException { + csvPrinter.printRecord(objectToWrite.getCode(),objectToWrite.getLibelle()); + } +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/adapter/export/HypotheseCsvExportService.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/adapter/export/HypotheseCsvExportService.java new file mode 100644 index 00000000..68480243 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/adapter/export/HypotheseCsvExportService.java @@ -0,0 +1,43 @@ +package org.mte.numecoeval.referentiel.infrastructure.adapter.export; + +import org.apache.commons.csv.CSVPrinter; +import org.mte.numecoeval.referentiel.domain.ports.input.impl.ImportHypothesePortImpl; +import org.mte.numecoeval.referentiel.domain.ports.output.ReferentielCsvExportService; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.HypotheseEntity; +import org.mte.numecoeval.referentiel.infrastructure.jpa.repository.HypotheseRepository; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.util.List; + +@Service +public class HypotheseCsvExportService extends ReferentielCsvExportService<HypotheseEntity> { + + private final HypotheseRepository repository; + + public HypotheseCsvExportService(HypotheseRepository repository) { + super(HypotheseEntity.class); + this.repository = repository; + } + + @Override + public String[] getHeaders() { + return ImportHypothesePortImpl.getHeaders(); + } + + @Override + public List<HypotheseEntity> getObjectsToWrite() { + return repository.findAll(); + } + + @Override + protected String getObjectId(HypotheseEntity object) { + return object.getCode(); + } + + @Override + public void printRecord(CSVPrinter csvPrinter, HypotheseEntity objectToWrite) throws IOException { + csvPrinter.printRecord(objectToWrite.getCode(),objectToWrite.getValeur(),objectToWrite.getSource()); + } + +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/adapter/export/ImpactEquipementCsvExportService.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/adapter/export/ImpactEquipementCsvExportService.java new file mode 100644 index 00000000..ea7030a8 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/adapter/export/ImpactEquipementCsvExportService.java @@ -0,0 +1,47 @@ +package org.mte.numecoeval.referentiel.infrastructure.adapter.export; + +import org.apache.commons.csv.CSVPrinter; +import org.mte.numecoeval.referentiel.domain.ports.input.impl.ImportImpactEquipementPortImpl; +import org.mte.numecoeval.referentiel.domain.ports.output.ReferentielCsvExportService; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.ImpactEquipementEntity; +import org.mte.numecoeval.referentiel.infrastructure.jpa.repository.ImpactEquipementRepository; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.util.List; + +@Service +public class ImpactEquipementCsvExportService extends ReferentielCsvExportService<ImpactEquipementEntity> { + + private final ImpactEquipementRepository repository; + + public ImpactEquipementCsvExportService(ImpactEquipementRepository repository) { + super(ImpactEquipementEntity.class); + this.repository = repository; + } + + @Override + public String[] getHeaders() { + return ImportImpactEquipementPortImpl.getHeaders(); + } + + @Override + public List<ImpactEquipementEntity> getObjectsToWrite() { + return repository.findAll(); + } + + @Override + protected String getObjectId(ImpactEquipementEntity object) { + return String.join("-", object.getRefEquipement(),object.getEtape(),object.getCritere()); + } + + @Override + public void printRecord(CSVPrinter csvPrinter, ImpactEquipementEntity objectToWrite) throws IOException { + csvPrinter.printRecord(objectToWrite.getRefEquipement(), + objectToWrite.getEtape(),objectToWrite.getCritere(), + formatDouble(objectToWrite.getConsoElecMoyenne()),formatDouble(objectToWrite.getValeur()), + objectToWrite.getSource(), objectToWrite.getType() + ); + } + +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/adapter/export/ImpactMessagerieCsvExportService.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/adapter/export/ImpactMessagerieCsvExportService.java new file mode 100644 index 00000000..6c0ab188 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/adapter/export/ImpactMessagerieCsvExportService.java @@ -0,0 +1,48 @@ +package org.mte.numecoeval.referentiel.infrastructure.adapter.export; + +import org.apache.commons.csv.CSVPrinter; +import org.mte.numecoeval.referentiel.domain.ports.input.impl.ImportImpactMessageriePortImpl; +import org.mte.numecoeval.referentiel.domain.ports.output.ReferentielCsvExportService; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.ImpactMessagerieEntity; +import org.mte.numecoeval.referentiel.infrastructure.jpa.repository.ImpactMessagerieRepository; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.util.List; + +@Service +public class ImpactMessagerieCsvExportService extends ReferentielCsvExportService<ImpactMessagerieEntity> { + + private final ImpactMessagerieRepository repository; + + public ImpactMessagerieCsvExportService(ImpactMessagerieRepository repository) { + super(ImpactMessagerieEntity.class); + this.repository = repository; + } + + @Override + public String[] getHeaders() { + return ImportImpactMessageriePortImpl.getHeaders(); + } + + @Override + public List<ImpactMessagerieEntity> getObjectsToWrite() { + return repository.findAll(); + } + + @Override + protected String getObjectId(ImpactMessagerieEntity object) { + return object.getCritere(); + } + + @Override + public void printRecord(CSVPrinter csvPrinter, ImpactMessagerieEntity objectToWrite) throws IOException { + csvPrinter.printRecord( + objectToWrite.getCritere(), + formatDouble(objectToWrite.getConstanteCoefficientDirecteur()), + formatDouble(objectToWrite.getConstanteOrdonneeOrigine()), + objectToWrite.getSource() + ); + } + +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/adapter/export/ImpactReseauCsvExportService.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/adapter/export/ImpactReseauCsvExportService.java new file mode 100644 index 00000000..fcc0937f --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/adapter/export/ImpactReseauCsvExportService.java @@ -0,0 +1,48 @@ +package org.mte.numecoeval.referentiel.infrastructure.adapter.export; + +import org.apache.commons.csv.CSVPrinter; +import org.mte.numecoeval.referentiel.domain.ports.input.impl.ImportImpactReseauPortImpl; +import org.mte.numecoeval.referentiel.domain.ports.output.ReferentielCsvExportService; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.ImpactReseauEntity; +import org.mte.numecoeval.referentiel.infrastructure.jpa.repository.ImpactReseauRepository; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.util.List; + +@Service +public class ImpactReseauCsvExportService extends ReferentielCsvExportService<ImpactReseauEntity> { + + private final ImpactReseauRepository repository; + + public ImpactReseauCsvExportService(ImpactReseauRepository repository) { + super(ImpactReseauEntity.class); + this.repository = repository; + } + + @Override + public String[] getHeaders() { + return ImportImpactReseauPortImpl.getHeaders(); + } + + @Override + public List<ImpactReseauEntity> getObjectsToWrite() { + return repository.findAll(); + } + + @Override + protected String getObjectId(ImpactReseauEntity object) { + return String.join("-", object.getRefReseau(),object.getEtape(),object.getCritere()); + } + + @Override + public void printRecord(CSVPrinter csvPrinter, ImpactReseauEntity objectToWrite) throws IOException { + csvPrinter.printRecord(objectToWrite.getRefReseau(), + objectToWrite.getEtape(),objectToWrite.getCritere(), + formatDouble(objectToWrite.getValeur()), + formatDouble(objectToWrite.getConsoElecMoyenne()), + objectToWrite.getSource() + ); + } + +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/adapter/export/MixElectriqueCsvExportService.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/adapter/export/MixElectriqueCsvExportService.java new file mode 100644 index 00000000..af99d634 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/adapter/export/MixElectriqueCsvExportService.java @@ -0,0 +1,49 @@ +package org.mte.numecoeval.referentiel.infrastructure.adapter.export; + +import org.apache.commons.csv.CSVPrinter; +import org.mte.numecoeval.referentiel.domain.ports.input.impl.ImportMixElectriquePortImpl; +import org.mte.numecoeval.referentiel.domain.ports.output.ReferentielCsvExportService; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.MixElectriqueEntity; +import org.mte.numecoeval.referentiel.infrastructure.jpa.repository.MixElectriqueRepository; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.util.List; + +@Service +public class MixElectriqueCsvExportService extends ReferentielCsvExportService<MixElectriqueEntity> { + + private final MixElectriqueRepository repository; + + public MixElectriqueCsvExportService(MixElectriqueRepository repository) { + super(MixElectriqueEntity.class); + this.repository = repository; + } + + @Override + public String[] getHeaders() { + return ImportMixElectriquePortImpl.getHeaders(); + } + + @Override + public List<MixElectriqueEntity> getObjectsToWrite() { + return repository.findAll(); + } + + @Override + protected String getObjectId(MixElectriqueEntity object) { + return String.join("-", object.getPays(), object.getCritere()); + } + + @Override + public void printRecord(CSVPrinter csvPrinter, MixElectriqueEntity objectToWrite) throws IOException { + csvPrinter.printRecord( + objectToWrite.getPays(), + objectToWrite.getRaccourcisAnglais(), + objectToWrite.getCritere(), + formatDouble(objectToWrite.getValeur()), + objectToWrite.getSource() + ); + } + +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/adapter/export/TypeEquipementCsvExportService.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/adapter/export/TypeEquipementCsvExportService.java new file mode 100644 index 00000000..adee3de7 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/adapter/export/TypeEquipementCsvExportService.java @@ -0,0 +1,47 @@ +package org.mte.numecoeval.referentiel.infrastructure.adapter.export; + +import org.apache.commons.csv.CSVPrinter; +import org.mte.numecoeval.referentiel.domain.ports.input.impl.ImportTypeEquipementPortImpl; +import org.mte.numecoeval.referentiel.domain.ports.output.ReferentielCsvExportService; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.TypeEquipementEntity; +import org.mte.numecoeval.referentiel.infrastructure.jpa.repository.TypeEquipementRepository; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.util.List; + +@Service +public class TypeEquipementCsvExportService extends ReferentielCsvExportService<TypeEquipementEntity> { + + private final TypeEquipementRepository repository; + + public TypeEquipementCsvExportService(TypeEquipementRepository repository) { + super(TypeEquipementEntity.class); + this.repository = repository; + } + + @Override + public String[] getHeaders() { + return ImportTypeEquipementPortImpl.getHeaders(); + } + + @Override + public List<TypeEquipementEntity> getObjectsToWrite() { + return repository.findAll(); + } + + @Override + protected String getObjectId(TypeEquipementEntity object) { + return object.getType(); + } + + @Override + public void printRecord(CSVPrinter csvPrinter, TypeEquipementEntity objectToWrite) throws IOException { + csvPrinter.printRecord(objectToWrite.getType(), + objectToWrite.isServeur(), + objectToWrite.getCommentaire(), + formatDouble(objectToWrite.getDureeVieDefaut()), + objectToWrite.getSource(), objectToWrite.getRefEquipementParDefaut()); + } + +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/configuration/ReferentielConfig.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/configuration/ReferentielConfig.java new file mode 100644 index 00000000..b5f32c70 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/configuration/ReferentielConfig.java @@ -0,0 +1,11 @@ +package org.mte.numecoeval.referentiel.infrastructure.configuration; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +@Configuration +@EnableWebMvc +public class ReferentielConfig { + + +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/configuration/openapi/ReferentielOpenApiConfig.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/configuration/openapi/ReferentielOpenApiConfig.java new file mode 100644 index 00000000..cd928e39 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/configuration/openapi/ReferentielOpenApiConfig.java @@ -0,0 +1,105 @@ +package org.mte.numecoeval.referentiel.infrastructure.configuration.openapi; + +import io.swagger.v3.oas.models.ExternalDocumentation; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.info.License; +import org.springdoc.core.models.GroupedOpenApi; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class ReferentielOpenApiConfig { + + @Bean + public OpenAPI configOpenAPI() { + return new OpenAPI() + .info(new Info().title("API des référentiels de NumEcoEval") + .description(""" + Endpoints permettant de manipuler les référentiels de NumEcoEval. + Les endpoints CRUD sont générés via Spring DataRest. + + Les endpoints d'export CSV permettent d'exporter l'intégralité d'un référentiel + sous forme de fichier CSV ré-importable via les endpoints d'imports. + + Les endpoints d'import fonctionnent en annule & remplace et supprimeront l'intégralité + du référentiel et utiliseront le contenu du CSV pour peupler le référentiel. + + Les endpoints internes sont utilisés par les différents modules de NumEcoEval. + """) + .version("v0.0.1") + .license(new License() + .name("Apache 2.0") + .url("https://gitlab-forge.din.developpement-durable.gouv.fr/pub/numeco/m4g/api-referentiel/-/blob/main/LICENSE.txt") + ) + ) + .externalDocs(new ExternalDocumentation() + .description("NumEcoEval Documentation") + .url("https://gitlab-forge.din.developpement-durable.gouv.fr/pub/numeco/m4g/docs")); + } + + @Bean + public GroupedOpenApi springDataRestOpenApiGroup() { + String[] paths = { + "/CorrespondanceRefEquipement/**", + "/Critere/**", + "/Etape/**", + "/Hypothèses/**", + "/ImpactEquipement/**", + "/ImpactMessagerie/**", + "/ImpactReseau/**", + "/MixElectrique/**", + "/TypeEquipement/**", + "/profile/**", + "/version" + }; + return GroupedOpenApi.builder() + .group("spring-data-rest") + .displayName("Spring Data Rest - Auto-Générés") + .pathsToMatch(paths) + .build(); + } + + @Bean + public GroupedOpenApi numEcoEvalGlobalOpenApiGroup() { + String[] paths = { + "/referentiel/**", + "/version" + }; + return GroupedOpenApi.builder() + .group("NumEcoEval-Global") + .displayName("NumEcoEval Global") + .pathsToMatch(paths) + .build(); + } + + @Bean + public GroupedOpenApi numEcoEvalOpenApiGroup() { + String[] paths = { + "/referentiel/*", + "/referentiel/mixelecs/{pays}", + "/referentiel/typesEquipement/{type}", + "/referentiel/impactsMessagerie/{critere}", + }; + return GroupedOpenApi.builder() + .group("NumEcoEval") + .displayName("NumEcoEval Internes") + .pathsToMatch(paths) + .pathsToExclude("/referentiel/*/csv") + .build(); + } + + @Bean + public GroupedOpenApi numEcoEvalAdministrationOpenApiGroup() { + String[] paths = { + "/referentiel/*/csv" + }; + return GroupedOpenApi.builder() + .group("NumEcoEval-administration") + .displayName("Administration NumEcoEval") + .pathsToMatch(paths) + .build(); + } + +} + diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/configuration/security/SecurityConfig.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/configuration/security/SecurityConfig.java new file mode 100644 index 00000000..48d35c8b --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/configuration/security/SecurityConfig.java @@ -0,0 +1,56 @@ +package org.mte.numecoeval.referentiel.infrastructure.configuration.security; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +import java.util.Arrays; + +/** + * Configuration Spring Security + */ +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + @Value("${numecoeval.urls.allowed:}") + private String[] urlsAllowed; + + /** + * Configuration pour les endpoints avec une session DNC. + */ + @Bean + SecurityFilterChain globalFilterChain(HttpSecurity http) throws Exception { + http + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) + .and() + .cors() + .and() + .authorizeHttpRequests(authz -> authz + .requestMatchers("/health").permitAll() + .requestMatchers("/**").permitAll() + ) + .csrf().disable() + .formLogin().disable() + ; + return http.build(); + } + + @Bean + CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + configuration.setAllowedOrigins(Arrays.asList(urlsAllowed)); + configuration.setAllowedMethods(Arrays.asList("GET","POST","OPTION")); + configuration.setAllowCredentials(true); + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + return source; + } +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/adapter/CorrespondanceRefEquipementJpaAdapter.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/adapter/CorrespondanceRefEquipementJpaAdapter.java new file mode 100644 index 00000000..f57cd54e --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/adapter/CorrespondanceRefEquipementJpaAdapter.java @@ -0,0 +1,66 @@ +package org.mte.numecoeval.referentiel.infrastructure.jpa.adapter; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.ListUtils; +import org.mte.numecoeval.referentiel.domain.exception.ReferentielException; +import org.mte.numecoeval.referentiel.domain.model.CorrespondanceRefEquipement; +import org.mte.numecoeval.referentiel.domain.ports.output.ReferentielPersistencePort; +import org.mte.numecoeval.referentiel.infrastructure.jpa.repository.CorrespondanceRefEquipementRepository; +import org.mte.numecoeval.referentiel.infrastructure.mapper.CorrespondanceRefEquipementMapper; +import org.springframework.stereotype.Service; + +import java.util.Collection; +import java.util.List; + +@Service +@Slf4j +@AllArgsConstructor +public class CorrespondanceRefEquipementJpaAdapter implements ReferentielPersistencePort<CorrespondanceRefEquipement,String> { + + CorrespondanceRefEquipementMapper mapper; + CorrespondanceRefEquipementRepository repository; + + @Override + public CorrespondanceRefEquipement save(CorrespondanceRefEquipement referentiel) throws ReferentielException { + var entityToSave = mapper.toEntity(referentiel); + if(entityToSave != null) { + var entitySaved = repository.save(entityToSave); + return mapper.toDomain(entitySaved); + } + return null; + } + + @Override + public void saveAll(Collection<CorrespondanceRefEquipement> referentiels) throws ReferentielException { + repository.saveAll(ListUtils.emptyIfNull(referentiels + .stream() + .map(typeEquipement -> mapper.toEntity(typeEquipement)) + .toList())); + } + + @Override + public CorrespondanceRefEquipement get(String id) throws ReferentielException { + if(id != null) { + var entityOpt= repository.findById(id); + return entityOpt + .map(entity -> mapper.toDomain(entity)) + .orElseThrow(() -> new ReferentielException("Correspondance au RefEquipement "+ id +" non trouvé")); + } + throw new ReferentielException("Correspondance au RefEquipement (id null) non trouvé"); + } + + @Override + public void purge() { + repository.deleteAll(); + } + + @Override + public List<CorrespondanceRefEquipement> getAll() { + + return ListUtils.emptyIfNull(repository.findAll()) + .stream() + .map(typeEquipementEntity -> mapper.toDomain(typeEquipementEntity)) + .toList(); + } +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/adapter/CritereJpaAdapter.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/adapter/CritereJpaAdapter.java new file mode 100644 index 00000000..e92b9a7a --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/adapter/CritereJpaAdapter.java @@ -0,0 +1,59 @@ +package org.mte.numecoeval.referentiel.infrastructure.jpa.adapter; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.mte.numecoeval.referentiel.domain.exception.ReferentielException; +import org.mte.numecoeval.referentiel.domain.model.Critere; +import org.mte.numecoeval.referentiel.domain.model.id.CritereId; +import org.mte.numecoeval.referentiel.domain.ports.output.ReferentielPersistencePort; +import org.mte.numecoeval.referentiel.infrastructure.jpa.repository.CritereRepository; +import org.mte.numecoeval.referentiel.infrastructure.mapper.CritereMapper; +import org.springframework.stereotype.Service; + +import java.util.Collection; +import java.util.List; + + +@Service +@Slf4j +@AllArgsConstructor +public class CritereJpaAdapter implements ReferentielPersistencePort<Critere, CritereId> { + + CritereRepository critereRepository; + + // Pour les purges des données filles + ImpactEquipementJpaAdapter impactEquipementJpaAdapter; + ImpactReseauJpaAdapter impactReseauJpaAdapter; + MixElectriqueJpaAdapter mixElectriqueJpaAdapter; + + ImpactMessagerieJpaAdapter impactMessagerieJpaAdapter; + + CritereMapper critereMapper; + + + @Override + public Critere save(Critere referentiel) throws ReferentielException { + return null; + } + + @Override + public void saveAll(Collection<Critere> referentiel) throws ReferentielException { + critereRepository.saveAll(critereMapper.toEntities(referentiel)); + } + + @Override + public Critere get(CritereId id) throws ReferentielException { + return null; + } + + @Override + public void purge() { + // Purge des données parentes + critereRepository.deleteAll(); + } + + @Override + public List<Critere> getAll() { + return critereMapper.toDomains(critereRepository.findAll()); + } +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/adapter/EtapeJpaAdapter.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/adapter/EtapeJpaAdapter.java new file mode 100644 index 00000000..f4108268 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/adapter/EtapeJpaAdapter.java @@ -0,0 +1,55 @@ +package org.mte.numecoeval.referentiel.infrastructure.jpa.adapter; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.mte.numecoeval.referentiel.domain.exception.ReferentielException; +import org.mte.numecoeval.referentiel.domain.model.Etape; +import org.mte.numecoeval.referentiel.domain.model.id.EtapeId; +import org.mte.numecoeval.referentiel.domain.ports.output.ReferentielPersistencePort; +import org.mte.numecoeval.referentiel.infrastructure.jpa.repository.EtapeRepository; +import org.mte.numecoeval.referentiel.infrastructure.mapper.EtapeMapper; +import org.springframework.stereotype.Service; + +import java.util.Collection; +import java.util.List; + + +@Service +@Slf4j +@AllArgsConstructor +public class EtapeJpaAdapter implements ReferentielPersistencePort<Etape, EtapeId> { + EtapeRepository etapeRepository; + + EtapeMapper etapeMapper; + + // Pour les purges des données filles + ImpactReseauJpaAdapter impactReseauJpaAdapter; + ImpactEquipementJpaAdapter impactEquipementJpaAdapter; + + + @Override + public Etape save(Etape referentiel) throws ReferentielException { + return null; + } + + @Override + public void saveAll(Collection<Etape> referentiel) throws ReferentielException { + etapeRepository.saveAll(etapeMapper.toEntities(referentiel)); + } + + @Override + public Etape get(EtapeId id) throws ReferentielException { + return null; + } + + @Override + public void purge() { + // Purge des données parentes + etapeRepository.deleteAll(); + } + + @Override + public List<Etape> getAll() { + return etapeMapper.toDomains(etapeRepository.findAll()); + } +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/adapter/HypotheseJpaAdapter.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/adapter/HypotheseJpaAdapter.java new file mode 100644 index 00000000..ec82b030 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/adapter/HypotheseJpaAdapter.java @@ -0,0 +1,60 @@ +package org.mte.numecoeval.referentiel.infrastructure.jpa.adapter; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.mte.numecoeval.referentiel.domain.exception.ReferentielException; +import org.mte.numecoeval.referentiel.domain.model.Hypothese; +import org.mte.numecoeval.referentiel.domain.model.id.HypotheseId; +import org.mte.numecoeval.referentiel.domain.ports.output.ReferentielPersistencePort; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.HypotheseEntity; +import org.mte.numecoeval.referentiel.infrastructure.jpa.repository.HypotheseRepository; +import org.mte.numecoeval.referentiel.infrastructure.mapper.HypotheseMapper; +import org.springframework.stereotype.Service; + +import java.util.Collection; +import java.util.List; +import java.util.Optional; + + +@Service +@Slf4j +@AllArgsConstructor +public class HypotheseJpaAdapter implements ReferentielPersistencePort<Hypothese, HypotheseId> { + HypotheseRepository hypotheseRepository; + + HypotheseMapper hypotheseMapper; + + + @Override + public Hypothese save(Hypothese referentiel) throws ReferentielException { + var entityToSave = hypotheseMapper.toEntity(referentiel); + if(entityToSave != null) { + var entitySaved = hypotheseRepository.save(entityToSave); + return hypotheseMapper.toDomain(entitySaved); + } + return null; + } + + @Override + public void saveAll(Collection<Hypothese> referentiel) throws ReferentielException { + hypotheseRepository.saveAll(hypotheseMapper.toEntities(referentiel)); + } + + @Override + public Hypothese get(HypotheseId id) throws ReferentielException { + Optional<HypotheseEntity> hypotheseOpt = hypotheseRepository.findById(hypotheseMapper.toEntityId(id)); + HypotheseEntity hypotheseEntity = hypotheseOpt.orElseThrow(() -> new ReferentielException("Hypothèse non trouvé")); + return hypotheseMapper.toDomain(hypotheseEntity); + } + + @Override + public void purge() { + hypotheseRepository.deleteAll(); + } + + @Override + public List<Hypothese> getAll() { + return hypotheseMapper.fromEntities(hypotheseRepository.findAll()); + } + +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/adapter/ImpactEquipementJpaAdapter.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/adapter/ImpactEquipementJpaAdapter.java new file mode 100644 index 00000000..59da314f --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/adapter/ImpactEquipementJpaAdapter.java @@ -0,0 +1,69 @@ +package org.mte.numecoeval.referentiel.infrastructure.jpa.adapter; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.ListUtils; +import org.mte.numecoeval.referentiel.domain.exception.ReferentielException; +import org.mte.numecoeval.referentiel.domain.model.ImpactEquipement; +import org.mte.numecoeval.referentiel.domain.model.id.ImpactEquipementId; +import org.mte.numecoeval.referentiel.domain.ports.output.ReferentielPersistencePort; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.ImpactEquipementEntity; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.id.ImpactEquipementIdEntity; +import org.mte.numecoeval.referentiel.infrastructure.jpa.repository.ImpactEquipementRepository; +import org.mte.numecoeval.referentiel.infrastructure.mapper.ImpactEquipementMapper; +import org.springframework.stereotype.Service; + +import java.util.Collection; +import java.util.List; +import java.util.Optional; + + +@Service +@Slf4j +@AllArgsConstructor +public class ImpactEquipementJpaAdapter implements ReferentielPersistencePort<ImpactEquipement, ImpactEquipementId> { + + private ImpactEquipementRepository repository; + + private ImpactEquipementMapper mapper; + + + @Override + public ImpactEquipement save(ImpactEquipement referentiel) throws ReferentielException { + var entityToSave = mapper.toEntity(referentiel); + if(entityToSave != null) { + var entitySaved = repository.save(entityToSave); + return mapper.toDomain(entitySaved); + } + return null; + } + + @Override + public void saveAll(Collection<ImpactEquipement> referentiel) throws ReferentielException { + purge(); + repository.saveAll(mapper.toEntities(referentiel)); + } + + @Override + public ImpactEquipement get(ImpactEquipementId id) throws ReferentielException { + if(id != null) { + ImpactEquipementIdEntity ieIdEntity = mapper.toEntityId(id); + Optional<ImpactEquipementEntity> optionalEntity = repository.findById(ieIdEntity); + return mapper.toDomain( + optionalEntity + .orElseThrow(() -> new ReferentielException("Impact Equipement non trouvé")) + ); + } + throw new ReferentielException("Impact Equipement non trouvé"); + } + + @Override + public void purge() { + repository.deleteAll(); + } + + @Override + public List<ImpactEquipement> getAll() { + return ListUtils.emptyIfNull(mapper.toDomains(repository.findAll())); + } +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/adapter/ImpactMessagerieJpaAdapter.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/adapter/ImpactMessagerieJpaAdapter.java new file mode 100644 index 00000000..585544e8 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/adapter/ImpactMessagerieJpaAdapter.java @@ -0,0 +1,57 @@ +package org.mte.numecoeval.referentiel.infrastructure.jpa.adapter; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.ListUtils; +import org.mte.numecoeval.referentiel.domain.exception.ReferentielException; +import org.mte.numecoeval.referentiel.domain.model.ImpactMessagerie; +import org.mte.numecoeval.referentiel.domain.ports.output.ReferentielPersistencePort; +import org.mte.numecoeval.referentiel.infrastructure.jpa.repository.CritereRepository; +import org.mte.numecoeval.referentiel.infrastructure.jpa.repository.ImpactMessagerieRepository; +import org.mte.numecoeval.referentiel.infrastructure.mapper.ImpactMessagerieMapper; +import org.springframework.stereotype.Service; + +import java.util.Collection; +import java.util.List; + +@Service +@Slf4j +@AllArgsConstructor +public class ImpactMessagerieJpaAdapter implements ReferentielPersistencePort<ImpactMessagerie,String> { + + ImpactMessagerieMapper impactMessagerieMapper; + ImpactMessagerieRepository impactMessagerieRepository; + CritereRepository critereRepository; + + @Override + public ImpactMessagerie save(ImpactMessagerie referentiel) throws ReferentielException { + var entity = impactMessagerieRepository.save(impactMessagerieMapper.toEntity(referentiel)); + return impactMessagerieMapper.toDomain(entity); + } + + @Override + public void saveAll(Collection<ImpactMessagerie> domains) throws ReferentielException { + impactMessagerieRepository.saveAll(impactMessagerieMapper.toEntities(domains)); + } + + @Override + public ImpactMessagerie get(String critere) throws ReferentielException { + if(critere != null) { + var entityOpt= impactMessagerieRepository.findById(critere); + return entityOpt + .map(impactMessagerieEntity -> impactMessagerieMapper.toDomain(impactMessagerieEntity)) + .orElseThrow(() -> new ReferentielException("ImpactMessagerie non trouvé")); + } + throw new ReferentielException("ImpactMessagerie non trouvé"); + } + + @Override + public void purge() { + impactMessagerieRepository.deleteAll(); + } + + @Override + public List<ImpactMessagerie> getAll() { + return impactMessagerieMapper.toDomains(ListUtils.emptyIfNull(impactMessagerieRepository.findAll())); + } +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/adapter/ImpactReseauJpaAdapter.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/adapter/ImpactReseauJpaAdapter.java new file mode 100644 index 00000000..f7019303 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/adapter/ImpactReseauJpaAdapter.java @@ -0,0 +1,62 @@ +package org.mte.numecoeval.referentiel.infrastructure.jpa.adapter; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.ListUtils; +import org.mte.numecoeval.referentiel.domain.exception.ReferentielException; +import org.mte.numecoeval.referentiel.domain.model.ImpactReseau; +import org.mte.numecoeval.referentiel.domain.model.id.ImpactReseauId; +import org.mte.numecoeval.referentiel.domain.ports.output.ReferentielPersistencePort; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.ImpactReseauEntity; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.id.ImpactReseauIdEntity; +import org.mte.numecoeval.referentiel.infrastructure.jpa.repository.ImpactReseauRepository; +import org.mte.numecoeval.referentiel.infrastructure.mapper.ImpactReseauMapper; +import org.springframework.stereotype.Service; + +import java.util.Collection; +import java.util.List; +import java.util.Optional; + + +@Service +@Slf4j +@AllArgsConstructor +public class ImpactReseauJpaAdapter implements ReferentielPersistencePort<ImpactReseau, ImpactReseauId> { + ImpactReseauRepository reseauRepository; + + ImpactReseauMapper reseauMapper; + + @Override + public ImpactReseau save(ImpactReseau facteurImpact) throws ReferentielException { + ImpactReseauEntity impactReseauEntity = reseauMapper.toEntity(facteurImpact); + ImpactReseauEntity save = reseauRepository.save(impactReseauEntity); + return reseauMapper.toDomain(save); + } + + @Override + public void saveAll(Collection<ImpactReseau> facteursImpacts) throws ReferentielException { + List<ImpactReseauEntity> impactReseauEntities = reseauMapper.toEntity(facteursImpacts); + reseauRepository.saveAll(impactReseauEntities); + } + + + @Override + public ImpactReseau get(ImpactReseauId id) throws ReferentielException { + ImpactReseauIdEntity reseauId = reseauMapper.toEntityId(id); + Optional<ImpactReseauEntity> reseauEntityOptional = reseauRepository.findById(reseauId); + return reseauMapper.toDomain( + reseauEntityOptional.orElseThrow(() -> new ReferentielException("Impact Réseau non trouvé")) + ); + } + + @Override + public void purge() { + reseauRepository.deleteAll(); + } + + @Override + public List<ImpactReseau> getAll() { + return ListUtils.emptyIfNull(reseauMapper.toDomains(reseauRepository.findAll())); + } + +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/adapter/MixElectriqueJpaAdapter.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/adapter/MixElectriqueJpaAdapter.java new file mode 100644 index 00000000..48337d78 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/adapter/MixElectriqueJpaAdapter.java @@ -0,0 +1,69 @@ +package org.mte.numecoeval.referentiel.infrastructure.jpa.adapter; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.ListUtils; +import org.mte.numecoeval.referentiel.domain.exception.ReferentielException; +import org.mte.numecoeval.referentiel.domain.model.MixElectrique; +import org.mte.numecoeval.referentiel.domain.model.id.MixElectriqueId; +import org.mte.numecoeval.referentiel.domain.ports.output.ReferentielPersistencePort; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.MixElectriqueEntity; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.id.MixElectriqueIdEntity; +import org.mte.numecoeval.referentiel.infrastructure.jpa.repository.MixElectriqueRepository; +import org.mte.numecoeval.referentiel.infrastructure.mapper.MixElectriqueMapper; +import org.springframework.stereotype.Service; + +import java.util.Collection; +import java.util.List; +import java.util.Optional; + + +@Service +@Slf4j +@AllArgsConstructor +public class MixElectriqueJpaAdapter implements ReferentielPersistencePort<MixElectrique, MixElectriqueId> { + MixElectriqueRepository mixElectriqueRepository; + MixElectriqueMapper mixElectriqueMapper; + + + @Override + public MixElectrique save(MixElectrique referentiel) throws ReferentielException { + var entityToSave = mixElectriqueMapper.toEntity(referentiel); + if (entityToSave != null) { + var entitySaved = mixElectriqueRepository.save(entityToSave); + return mixElectriqueMapper.toDomain(entitySaved); + } + return null; + } + + @Override + public void saveAll(Collection<MixElectrique> mixElecs) throws ReferentielException { + List<MixElectriqueEntity> mixElecsEntities = mixElectriqueMapper.toEntities(mixElecs); + mixElectriqueRepository.saveAll(mixElecsEntities); + } + + + @Override + public MixElectrique get(MixElectriqueId id) throws ReferentielException { + if (id != null) { + MixElectriqueIdEntity meIdEntity = mixElectriqueMapper.toEntityId(id); + Optional<MixElectriqueEntity> reseauEntityOptional = mixElectriqueRepository.findById(meIdEntity); + return mixElectriqueMapper.toDomain( + reseauEntityOptional + .orElseThrow(() -> new ReferentielException("Mix Electrique non trouvé pour l'id " + id)) + ); + } + throw new ReferentielException("Mix Electrique non trouvé pour l'id null"); + } + + @Override + public void purge() { + mixElectriqueRepository.deleteAll(); + } + + @Override + public List<MixElectrique> getAll() { + return ListUtils.emptyIfNull(mixElectriqueMapper.toDomains(mixElectriqueRepository.findAll())); + } + +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/adapter/TypeEquipementJpaAdapter.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/adapter/TypeEquipementJpaAdapter.java new file mode 100644 index 00000000..e0c2e7d9 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/adapter/TypeEquipementJpaAdapter.java @@ -0,0 +1,60 @@ +package org.mte.numecoeval.referentiel.infrastructure.jpa.adapter; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.ListUtils; +import org.mte.numecoeval.referentiel.domain.exception.ReferentielException; +import org.mte.numecoeval.referentiel.domain.model.TypeEquipement; +import org.mte.numecoeval.referentiel.domain.ports.output.ReferentielPersistencePort; +import org.mte.numecoeval.referentiel.infrastructure.jpa.repository.TypeEquipementRepository; +import org.mte.numecoeval.referentiel.infrastructure.mapper.TypeEquipementMapper; +import org.springframework.stereotype.Service; + +import java.util.Collection; +import java.util.List; + +@Service +@Slf4j +@AllArgsConstructor +public class TypeEquipementJpaAdapter implements ReferentielPersistencePort<TypeEquipement,String> { + + TypeEquipementMapper typeEquipementMapper; + TypeEquipementRepository typeEquipementRepository; + + @Override + public TypeEquipement save(TypeEquipement referentiel) throws ReferentielException { + typeEquipementRepository.save(typeEquipementMapper.toEntity(referentiel)); + return null; + } + + @Override + public void saveAll(Collection<TypeEquipement> referentiels) throws ReferentielException { + typeEquipementRepository.saveAll(ListUtils.emptyIfNull(referentiels + .stream() + .map(typeEquipement -> typeEquipementMapper.toEntity(typeEquipement)) + .toList())); + } + + @Override + public TypeEquipement get(String id) throws ReferentielException { + var entityOpt= typeEquipementRepository.findById(id); + if(entityOpt.isPresent()){ + return typeEquipementMapper.toDomaine(entityOpt.get()); + } + return null; + } + + @Override + public void purge() { + typeEquipementRepository.deleteAll(); + } + + @Override + public List<TypeEquipement> getAll() { + + return ListUtils.emptyIfNull(typeEquipementRepository.findAll()) + .stream() + .map(typeEquipementEntity -> typeEquipementMapper.toDomaine(typeEquipementEntity)) + .toList(); + } +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/entity/AbstractReferentielEntity.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/entity/AbstractReferentielEntity.java new file mode 100644 index 00000000..933be59b --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/entity/AbstractReferentielEntity.java @@ -0,0 +1,7 @@ +package org.mte.numecoeval.referentiel.infrastructure.jpa.entity; + +import java.io.Serializable; + +public interface AbstractReferentielEntity extends Serializable { + // Actuellement l'interface n'a pas de comportement par défaut ni de champ partagé +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/entity/CorrespondanceRefEquipementEntity.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/entity/CorrespondanceRefEquipementEntity.java new file mode 100644 index 00000000..c1c533dc --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/entity/CorrespondanceRefEquipementEntity.java @@ -0,0 +1,49 @@ +package org.mte.numecoeval.referentiel.infrastructure.jpa.entity; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.Accessors; +import lombok.experimental.FieldDefaults; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; + +@Getter +@Setter +@Accessors(chain = true) +@FieldDefaults(level = AccessLevel.PRIVATE) +@NoArgsConstructor +@Entity +@Table(name = "REF_CORRESPONDANCE_REF_EQP") +public class CorrespondanceRefEquipementEntity implements AbstractReferentielEntity { + @Id + String modeleEquipementSource; + + String refEquipementCible; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + + if (o == null || getClass() != o.getClass()) return false; + + CorrespondanceRefEquipementEntity that = (CorrespondanceRefEquipementEntity) o; + + return new EqualsBuilder() + .append(modeleEquipementSource, that.modeleEquipementSource) + .append(refEquipementCible, that.refEquipementCible) + .isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(17, 37) + .append(modeleEquipementSource) + .append(refEquipementCible) + .toHashCode(); + } +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/entity/CritereEntity.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/entity/CritereEntity.java new file mode 100644 index 00000000..9a8b3799 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/entity/CritereEntity.java @@ -0,0 +1,44 @@ +package org.mte.numecoeval.referentiel.infrastructure.jpa.entity; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.IdClass; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; +import lombok.experimental.FieldDefaults; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.id.CritereIdEntity; + +@Getter +@Setter +@Accessors(chain = true) +@FieldDefaults(level = AccessLevel.PRIVATE) +@Entity +@IdClass(CritereIdEntity.class) +@Table(name = "REF_CRITERE") +public class CritereEntity implements AbstractReferentielEntity { + @Id + String nomCritere; + String unite; + String description; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + + if (o == null || getClass() != o.getClass()) return false; + + CritereEntity that = (CritereEntity) o; + + return new EqualsBuilder().append(nomCritere, that.nomCritere).append(unite, that.unite).append(description, that.description).isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(17, 37).append(nomCritere).append(unite).append(description).toHashCode(); + } +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/entity/EtapeEntity.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/entity/EtapeEntity.java new file mode 100644 index 00000000..e24f78e5 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/entity/EtapeEntity.java @@ -0,0 +1,45 @@ +package org.mte.numecoeval.referentiel.infrastructure.jpa.entity; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.IdClass; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.Accessors; +import lombok.experimental.FieldDefaults; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.id.EtapeIdEntity; + +@Getter +@Setter +@Accessors(chain = true) +@FieldDefaults(level = AccessLevel.PRIVATE) +@NoArgsConstructor +@Entity +@IdClass(EtapeIdEntity.class) +@Table(name = "REF_ETAPEACV") +public class EtapeEntity implements AbstractReferentielEntity { + @Id + String code; + String libelle; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + + if (o == null || getClass() != o.getClass()) return false; + + EtapeEntity that = (EtapeEntity) o; + + return new EqualsBuilder().append(code, that.code).append(libelle, that.libelle).isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(17, 37).append(code).append(libelle).toHashCode(); + } +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/entity/HypotheseEntity.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/entity/HypotheseEntity.java new file mode 100644 index 00000000..ab8fc01d --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/entity/HypotheseEntity.java @@ -0,0 +1,46 @@ +package org.mte.numecoeval.referentiel.infrastructure.jpa.entity; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.IdClass; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.Accessors; +import lombok.experimental.FieldDefaults; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.id.HypotheseIdEntity; + +@Getter +@Setter +@Accessors(chain = true) +@FieldDefaults(level = AccessLevel.PRIVATE) +@NoArgsConstructor +@Entity +@IdClass(HypotheseIdEntity.class) +@Table(name = "REF_HYPOTHESE") +public class HypotheseEntity implements AbstractReferentielEntity { + @Id + String code; + String valeur; + String source; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + + if (o == null || getClass() != o.getClass()) return false; + + HypotheseEntity that = (HypotheseEntity) o; + + return new EqualsBuilder().append(code, that.code).append(valeur, that.valeur).append(source, that.source).isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(17, 37).append(code).append(valeur).append(source).toHashCode(); + } +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/entity/ImpactEquipementEntity.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/entity/ImpactEquipementEntity.java new file mode 100644 index 00000000..2f90d6a8 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/entity/ImpactEquipementEntity.java @@ -0,0 +1,74 @@ +package org.mte.numecoeval.referentiel.infrastructure.jpa.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.IdClass; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; +import lombok.experimental.FieldDefaults; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.id.ImpactEquipementIdEntity; + +@Getter +@Setter +@Accessors(chain = true) +@FieldDefaults(level = AccessLevel.PRIVATE) +@Entity +@IdClass(ImpactEquipementIdEntity.class) +@Table(name = "REF_IMPACTEQUIPEMENT") +public class ImpactEquipementEntity implements AbstractReferentielEntity { + @Id + @Column(name = "REFEQUIPEMENT") + String refEquipement; + @Id + @Column(name = "ETAPEACV") + String etape; + @Id + @Column(name = "NOMCRITERE") + String critere; + String source; + String type; + Double valeur; + Double consoElecMoyenne; + + String description; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + + if (o == null || getClass() != o.getClass()) return false; + + ImpactEquipementEntity that = (ImpactEquipementEntity) o; + + return new EqualsBuilder() + .append(refEquipement, that.refEquipement) + .append(etape, that.etape) + .append(critere, that.critere) + .append(source, that.source) + .append(type, that.type) + .append(valeur, that.valeur) + .append(consoElecMoyenne, that.consoElecMoyenne) + .append(description, that.description) + .isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(17, 37) + .append(refEquipement) + .append(etape) + .append(critere) + .append(source) + .append(type) + .append(valeur) + .append(consoElecMoyenne) + .append(description) + .toHashCode(); + } +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/entity/ImpactMessagerieEntity.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/entity/ImpactMessagerieEntity.java new file mode 100644 index 00000000..d461f065 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/entity/ImpactMessagerieEntity.java @@ -0,0 +1,46 @@ +package org.mte.numecoeval.referentiel.infrastructure.jpa.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; +import lombok.experimental.FieldDefaults; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; + +@Getter +@Setter +@Accessors(chain = true) +@FieldDefaults(level = AccessLevel.PRIVATE) +@Entity +@Table(name = "REF_IMPACT_MESSAGERIE") +public class ImpactMessagerieEntity implements AbstractReferentielEntity { + + @Id + @Column(name = "NOM_CRITERE") + String critere; + + Double constanteCoefficientDirecteur; + Double constanteOrdonneeOrigine; + String source; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + + if (o == null || getClass() != o.getClass()) return false; + + ImpactMessagerieEntity that = (ImpactMessagerieEntity) o; + + return new EqualsBuilder().append(critere, that.critere).append(constanteCoefficientDirecteur, that.constanteCoefficientDirecteur).append(constanteOrdonneeOrigine, that.constanteOrdonneeOrigine).append(source, that.source).isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(17, 37).append(critere).append(constanteCoefficientDirecteur).append(constanteOrdonneeOrigine).append(source).toHashCode(); + } +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/entity/ImpactReseauEntity.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/entity/ImpactReseauEntity.java new file mode 100644 index 00000000..2b202ed3 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/entity/ImpactReseauEntity.java @@ -0,0 +1,54 @@ +package org.mte.numecoeval.referentiel.infrastructure.jpa.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.IdClass; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; +import lombok.experimental.FieldDefaults; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.id.ImpactReseauIdEntity; + +@Getter +@Setter +@Accessors(chain = true) +@FieldDefaults(level = AccessLevel.PRIVATE) +@Entity +@IdClass(ImpactReseauIdEntity.class) +@Table(name = "REF_IMPACTRESEAU") +public class ImpactReseauEntity implements AbstractReferentielEntity { + @Id + @Column(name = "REFRESEAU", nullable = false) + String refReseau; + @Id + @Column(name = "ETAPEACV", nullable = false) + String etape; + @Id + @Column(name = "NOMCRITERE", nullable = false) + String critere; + String source; + Double valeur; + @Column(name = "CONSOELECMOYENNE") + Double consoElecMoyenne; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + + if (o == null || getClass() != o.getClass()) return false; + + ImpactReseauEntity that = (ImpactReseauEntity) o; + + return new EqualsBuilder().append(refReseau, that.refReseau).append(etape, that.etape).append(critere, that.critere).append(source, that.source).append(valeur, that.valeur).append(consoElecMoyenne, that.consoElecMoyenne).isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(17, 37).append(refReseau).append(etape).append(critere).append(source).append(valeur).append(consoElecMoyenne).toHashCode(); + } +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/entity/MixElectriqueEntity.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/entity/MixElectriqueEntity.java new file mode 100644 index 00000000..c42caef2 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/entity/MixElectriqueEntity.java @@ -0,0 +1,51 @@ +package org.mte.numecoeval.referentiel.infrastructure.jpa.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.IdClass; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; +import lombok.experimental.FieldDefaults; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.id.MixElectriqueIdEntity; + +@Getter +@Setter +@Accessors(chain = true) +@FieldDefaults(level = AccessLevel.PRIVATE) +@Entity +@IdClass(MixElectriqueIdEntity.class) +@Table(name = "REF_MIXELEC") +public class MixElectriqueEntity implements AbstractReferentielEntity { + + @Id + String pays; + @Id + @Column(name = "NOMCRITERE") + String critere; + @Column(name = "RACCOURCISANGLAIS") + String raccourcisAnglais; + String source; + Double valeur; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + + if (o == null || getClass() != o.getClass()) return false; + + MixElectriqueEntity that = (MixElectriqueEntity) o; + + return new EqualsBuilder().append(pays, that.pays).append(critere, that.critere).append(raccourcisAnglais, that.raccourcisAnglais).append(source, that.source).append(valeur, that.valeur).isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(17, 37).append(pays).append(critere).append(raccourcisAnglais).append(source).append(valeur).toHashCode(); + } +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/entity/TypeEquipementEntity.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/entity/TypeEquipementEntity.java new file mode 100644 index 00000000..dced599b --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/entity/TypeEquipementEntity.java @@ -0,0 +1,47 @@ +package org.mte.numecoeval.referentiel.infrastructure.jpa.entity; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.Accessors; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; + +@Builder +@Getter +@Setter +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Entity(name = "REF_TYPE_EQUIPEMENT") +public class TypeEquipementEntity implements AbstractReferentielEntity { + @Id + String type; + boolean serveur; + String commentaire; + Double dureeVieDefaut; + String source; + + // Référence de l'équipement par défaut, permet des correspondances en cas d'absence de correspondance direct. + String refEquipementParDefaut; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + + if (o == null || getClass() != o.getClass()) return false; + + TypeEquipementEntity that = (TypeEquipementEntity) o; + + return new EqualsBuilder().append(serveur, that.serveur).append(type, that.type).append(commentaire, that.commentaire).append(dureeVieDefaut, that.dureeVieDefaut).append(source, that.source).isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(17, 37).append(type).append(serveur).append(commentaire).append(dureeVieDefaut).append(source).toHashCode(); + } +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/entity/id/AbstractReferentieIdEntity.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/entity/id/AbstractReferentieIdEntity.java new file mode 100644 index 00000000..1ba9d175 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/entity/id/AbstractReferentieIdEntity.java @@ -0,0 +1,7 @@ +package org.mte.numecoeval.referentiel.infrastructure.jpa.entity.id; + +import java.io.Serializable; + +public interface AbstractReferentieIdEntity extends Serializable { + // Actuellement l'interface n'a pas de comportement par défaut ni de champ partagé +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/entity/id/CritereIdEntity.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/entity/id/CritereIdEntity.java new file mode 100644 index 00000000..5c1083cb --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/entity/id/CritereIdEntity.java @@ -0,0 +1,35 @@ +package org.mte.numecoeval.referentiel.infrastructure.jpa.entity.id; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.Accessors; +import lombok.experimental.FieldDefaults; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; + +@Getter +@Setter +@Accessors(chain = true) +@FieldDefaults(level = AccessLevel.PRIVATE) +@NoArgsConstructor +public class CritereIdEntity implements AbstractReferentieIdEntity { + String nomCritere; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + + if (o == null || getClass() != o.getClass()) return false; + + CritereIdEntity that = (CritereIdEntity) o; + + return new EqualsBuilder().append(nomCritere, that.nomCritere).isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(17, 37).append(nomCritere).toHashCode(); + } +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/entity/id/EtapeIdEntity.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/entity/id/EtapeIdEntity.java new file mode 100644 index 00000000..b6560bd3 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/entity/id/EtapeIdEntity.java @@ -0,0 +1,36 @@ +package org.mte.numecoeval.referentiel.infrastructure.jpa.entity.id; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.Accessors; +import lombok.experimental.FieldDefaults; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; + + +@Getter +@Setter +@Accessors(chain = true) +@FieldDefaults(level = AccessLevel.PRIVATE) +@NoArgsConstructor +public class EtapeIdEntity implements AbstractReferentieIdEntity { + String code; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + + if (o == null || getClass() != o.getClass()) return false; + + EtapeIdEntity that = (EtapeIdEntity) o; + + return new EqualsBuilder().append(code, that.code).isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(17, 37).append(code).toHashCode(); + } +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/entity/id/HypotheseIdEntity.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/entity/id/HypotheseIdEntity.java new file mode 100644 index 00000000..9ee5948e --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/entity/id/HypotheseIdEntity.java @@ -0,0 +1,36 @@ +package org.mte.numecoeval.referentiel.infrastructure.jpa.entity.id; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.Accessors; +import lombok.experimental.FieldDefaults; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; + +@Getter +@Setter +@Accessors(chain = true) +@FieldDefaults(level = AccessLevel.PRIVATE) +@NoArgsConstructor +public class HypotheseIdEntity implements AbstractReferentieIdEntity { + + String code; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + + if (o == null || getClass() != o.getClass()) return false; + + HypotheseIdEntity that = (HypotheseIdEntity) o; + + return new EqualsBuilder().append(code, that.code).isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(17, 37).append(code).toHashCode(); + } +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/entity/id/ImpactEquipementIdEntity.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/entity/id/ImpactEquipementIdEntity.java new file mode 100644 index 00000000..2cec43b7 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/entity/id/ImpactEquipementIdEntity.java @@ -0,0 +1,38 @@ +package org.mte.numecoeval.referentiel.infrastructure.jpa.entity.id; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.Accessors; +import lombok.experimental.FieldDefaults; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; + +@Getter +@Setter +@Accessors(chain = true) +@FieldDefaults(level = AccessLevel.PRIVATE) +@NoArgsConstructor + +public class ImpactEquipementIdEntity implements AbstractReferentieIdEntity { + String refEquipement; + String etape; + String critere; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + + if (o == null || getClass() != o.getClass()) return false; + + ImpactEquipementIdEntity that = (ImpactEquipementIdEntity) o; + + return new EqualsBuilder().append(refEquipement, that.refEquipement).append(etape, that.etape).append(critere, that.critere).isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(17, 37).append(refEquipement).append(etape).append(critere).toHashCode(); + } +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/entity/id/ImpactReseauIdEntity.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/entity/id/ImpactReseauIdEntity.java new file mode 100644 index 00000000..e43e7251 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/entity/id/ImpactReseauIdEntity.java @@ -0,0 +1,37 @@ +package org.mte.numecoeval.referentiel.infrastructure.jpa.entity.id; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.Accessors; +import lombok.experimental.FieldDefaults; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; + +@Getter +@Setter +@Accessors(chain = true) +@FieldDefaults(level = AccessLevel.PRIVATE) +@NoArgsConstructor +public class ImpactReseauIdEntity implements AbstractReferentieIdEntity { + String refReseau; + String etape; + String critere; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + + if (o == null || getClass() != o.getClass()) return false; + + ImpactReseauIdEntity that = (ImpactReseauIdEntity) o; + + return new EqualsBuilder().append(refReseau, that.refReseau).append(etape, that.etape).append(critere, that.critere).isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(17, 37).append(refReseau).append(etape).append(critere).toHashCode(); + } +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/entity/id/MixElectriqueIdEntity.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/entity/id/MixElectriqueIdEntity.java new file mode 100644 index 00000000..feae6b5e --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/entity/id/MixElectriqueIdEntity.java @@ -0,0 +1,38 @@ +package org.mte.numecoeval.referentiel.infrastructure.jpa.entity.id; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.Accessors; +import lombok.experimental.FieldDefaults; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; + +@Getter +@Setter +@Accessors(chain = true) +@FieldDefaults(level = AccessLevel.PRIVATE) +@NoArgsConstructor + +public class MixElectriqueIdEntity implements AbstractReferentieIdEntity { + + String pays; + String critere; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + + if (o == null || getClass() != o.getClass()) return false; + + MixElectriqueIdEntity that = (MixElectriqueIdEntity) o; + + return new EqualsBuilder().append(pays, that.pays).append(critere, that.critere).isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(17, 37).append(pays).append(critere).toHashCode(); + } +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/repository/CorrespondanceRefEquipementRepository.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/repository/CorrespondanceRefEquipementRepository.java new file mode 100644 index 00000000..119bd61c --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/repository/CorrespondanceRefEquipementRepository.java @@ -0,0 +1,11 @@ +package org.mte.numecoeval.referentiel.infrastructure.jpa.repository; + +import io.swagger.v3.oas.annotations.tags.Tag; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.CorrespondanceRefEquipementEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.rest.core.annotation.RepositoryRestResource; + +@RepositoryRestResource(path = "CorrespondanceRefEquipement" , itemResourceRel = "CorrespondancesRefEquipement") +@Tag(name = "CorrespondanceRefEquipement") +public interface CorrespondanceRefEquipementRepository extends JpaRepository<CorrespondanceRefEquipementEntity,String> { +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/repository/CritereRepository.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/repository/CritereRepository.java new file mode 100644 index 00000000..1c75dc90 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/repository/CritereRepository.java @@ -0,0 +1,22 @@ +package org.mte.numecoeval.referentiel.infrastructure.jpa.repository; + +import io.swagger.v3.oas.annotations.tags.Tag; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.CritereEntity; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.id.CritereIdEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.rest.core.annotation.Description; +import org.springframework.data.rest.core.annotation.RepositoryRestResource; + +@RepositoryRestResource( + path = "Critere", + collectionResourceDescription = @Description(""" + Endpoints CRUD généré par Spring Data REST pour la récupération de plusieurs critères d'impact écologique. + """), + itemResourceDescription = @Description(""" + Endpoints CRUD généré par Spring Data REST pour la récupération d'un critère d'impact écologique. + """) +) +@Tag(name = "Critères - CRUD/Spring Data REST") +public interface CritereRepository extends JpaRepository<CritereEntity, CritereIdEntity> { + +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/repository/EtapeRepository.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/repository/EtapeRepository.java new file mode 100644 index 00000000..0027d7c1 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/repository/EtapeRepository.java @@ -0,0 +1,13 @@ +package org.mte.numecoeval.referentiel.infrastructure.jpa.repository; + +import io.swagger.v3.oas.annotations.tags.Tag; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.EtapeEntity; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.id.EtapeIdEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.rest.core.annotation.RepositoryRestResource; + +@RepositoryRestResource(path = "Etape" , itemResourceRel = "Etapes") +@Tag(name = "Etapes - CRUD/Spring Data REST") +public interface EtapeRepository extends JpaRepository<EtapeEntity, EtapeIdEntity> { + +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/repository/HypotheseRepository.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/repository/HypotheseRepository.java new file mode 100644 index 00000000..250786e9 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/repository/HypotheseRepository.java @@ -0,0 +1,17 @@ +package org.mte.numecoeval.referentiel.infrastructure.jpa.repository; + +import io.swagger.v3.oas.annotations.tags.Tag; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.HypotheseEntity; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.id.HypotheseIdEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.rest.core.annotation.RepositoryRestResource; + +import java.util.Optional; + + +@RepositoryRestResource(path = "Hypothèses" , itemResourceRel = "Hypothèses") +@Tag(name = "Hypothèse - CRUD/Spring Data REST") +public interface HypotheseRepository extends JpaRepository<HypotheseEntity, HypotheseIdEntity> { + + Optional<HypotheseEntity> findById(HypotheseIdEntity id); +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/repository/ImpactEquipementRepository.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/repository/ImpactEquipementRepository.java new file mode 100644 index 00000000..af84c79c --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/repository/ImpactEquipementRepository.java @@ -0,0 +1,13 @@ +package org.mte.numecoeval.referentiel.infrastructure.jpa.repository; + +import io.swagger.v3.oas.annotations.tags.Tag; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.ImpactEquipementEntity; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.id.ImpactEquipementIdEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.rest.core.annotation.RepositoryRestResource; + +@RepositoryRestResource(path = "ImpactEquipement" , itemResourceRel = "ImpactEquipements") +@Tag(name = "ImpactEquipement - CRUD/Spring Data REST") +public interface ImpactEquipementRepository extends JpaRepository<ImpactEquipementEntity, ImpactEquipementIdEntity> { + +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/repository/ImpactMessagerieRepository.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/repository/ImpactMessagerieRepository.java new file mode 100644 index 00000000..ecb9a4e8 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/repository/ImpactMessagerieRepository.java @@ -0,0 +1,11 @@ +package org.mte.numecoeval.referentiel.infrastructure.jpa.repository; + +import io.swagger.v3.oas.annotations.tags.Tag; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.ImpactMessagerieEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.rest.core.annotation.RepositoryRestResource; + +@RepositoryRestResource(path = "ImpactMessagerie" , itemResourceRel = "ImpactMessageries") +@Tag(name = "ImpactMessagerie - CRUD/Spring Data REST") +public interface ImpactMessagerieRepository extends JpaRepository<ImpactMessagerieEntity, String> { +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/repository/ImpactReseauRepository.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/repository/ImpactReseauRepository.java new file mode 100644 index 00000000..f37f3c3a --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/repository/ImpactReseauRepository.java @@ -0,0 +1,13 @@ +package org.mte.numecoeval.referentiel.infrastructure.jpa.repository; + +import io.swagger.v3.oas.annotations.tags.Tag; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.ImpactReseauEntity; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.id.ImpactReseauIdEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.rest.core.annotation.RepositoryRestResource; + +@RepositoryRestResource(path = "ImpactReseau" , itemResourceRel = "ImpactReseaux") +@Tag(name = "ImpactReseau - CRUD/Spring Data REST") +public interface ImpactReseauRepository extends JpaRepository<ImpactReseauEntity, ImpactReseauIdEntity> { + +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/repository/MixElectriqueRepository.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/repository/MixElectriqueRepository.java new file mode 100644 index 00000000..448a8b3b --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/repository/MixElectriqueRepository.java @@ -0,0 +1,13 @@ +package org.mte.numecoeval.referentiel.infrastructure.jpa.repository; + +import io.swagger.v3.oas.annotations.tags.Tag; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.MixElectriqueEntity; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.id.MixElectriqueIdEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.rest.core.annotation.RepositoryRestResource; + +@RepositoryRestResource(path = "MixElectrique" , itemResourceRel = "MixElectriques") +@Tag(name = "MixElectrique - CRUD/Spring Data REST") +public interface MixElectriqueRepository extends JpaRepository<MixElectriqueEntity, MixElectriqueIdEntity> { + +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/repository/TypeEquipementRepository.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/repository/TypeEquipementRepository.java new file mode 100644 index 00000000..c924a861 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/jpa/repository/TypeEquipementRepository.java @@ -0,0 +1,11 @@ +package org.mte.numecoeval.referentiel.infrastructure.jpa.repository; + +import io.swagger.v3.oas.annotations.tags.Tag; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.TypeEquipementEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.rest.core.annotation.RepositoryRestResource; + +@RepositoryRestResource(path = "TypeEquipement" , itemResourceRel = "TypesEquipements") +@Tag(name = "TypeEquipement - CRUD/Spring Data REST") +public interface TypeEquipementRepository extends JpaRepository<TypeEquipementEntity,String> { +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/mapper/CorrespondanceRefEquipementMapper.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/mapper/CorrespondanceRefEquipementMapper.java new file mode 100644 index 00000000..6501c7b5 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/mapper/CorrespondanceRefEquipementMapper.java @@ -0,0 +1,25 @@ +package org.mte.numecoeval.referentiel.infrastructure.mapper; + +import org.mapstruct.Mapper; +import org.mte.numecoeval.referentiel.domain.model.CorrespondanceRefEquipement; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.CorrespondanceRefEquipementEntity; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.CorrespondanceRefEquipementDTO; + +import java.util.Collection; +import java.util.List; + +@Mapper(componentModel = "spring") +public interface CorrespondanceRefEquipementMapper { + + CorrespondanceRefEquipement toDomain(CorrespondanceRefEquipementEntity entity); + + CorrespondanceRefEquipement toDomain(CorrespondanceRefEquipementDTO dto); + + CorrespondanceRefEquipementEntity toEntity(CorrespondanceRefEquipement domain); + + List<CorrespondanceRefEquipementEntity> toEntities(Collection<CorrespondanceRefEquipement> domains); + + CorrespondanceRefEquipementDTO toDto(CorrespondanceRefEquipement domain); + + +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/mapper/CritereMapper.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/mapper/CritereMapper.java new file mode 100644 index 00000000..b898dcdf --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/mapper/CritereMapper.java @@ -0,0 +1,20 @@ +package org.mte.numecoeval.referentiel.infrastructure.mapper; + +import org.mapstruct.Mapper; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.CritereDTO; +import org.mte.numecoeval.referentiel.domain.model.Critere; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.CritereEntity; + +import java.util.Collection; +import java.util.List; + +@Mapper(componentModel = "spring") +public interface CritereMapper { + List<Critere> toDomainsFromDTO(List<CritereDTO> criteresDTO); + + List<CritereEntity> toEntities(Collection<Critere> referentiel); + + List<CritereDTO> toDTO(List<Critere> all); + + List<Critere> toDomains(List<CritereEntity> all); +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/mapper/EtapeMapper.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/mapper/EtapeMapper.java new file mode 100644 index 00000000..7a46e490 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/mapper/EtapeMapper.java @@ -0,0 +1,22 @@ +package org.mte.numecoeval.referentiel.infrastructure.mapper; + +import org.mapstruct.Mapper; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.EtapeDTO; +import org.mte.numecoeval.referentiel.domain.model.Etape; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.EtapeEntity; + +import java.util.Collection; +import java.util.List; + +@Mapper(componentModel = "spring") +public interface EtapeMapper { + List<Etape> toDomain(List<EtapeDTO> etapesDTO); + + List<Etape> toDomains(List<EtapeEntity> etapesEntities); + + EtapeEntity toEntity(Etape referentiel); + + List<EtapeEntity> toEntities(Collection<Etape> referentiel); + + List<EtapeDTO> toDTO(List<Etape> etapes); +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/mapper/HypotheseMapper.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/mapper/HypotheseMapper.java new file mode 100644 index 00000000..95817320 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/mapper/HypotheseMapper.java @@ -0,0 +1,37 @@ +package org.mte.numecoeval.referentiel.infrastructure.mapper; + +import org.mapstruct.Mapper; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.HypotheseDTO; +import org.mte.numecoeval.referentiel.domain.model.Hypothese; +import org.mte.numecoeval.referentiel.domain.model.id.HypotheseId; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.HypotheseEntity; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.id.HypotheseIdEntity; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.id.HypotheseIdDTO; + +import java.util.Collection; +import java.util.List; + +@Mapper(componentModel = "spring") +public interface HypotheseMapper { + + HypotheseEntity toEntity(Hypothese referentiel); + + List<HypotheseEntity> toEntities(Collection<Hypothese> referentiel); + + List<Hypothese> fromEntities(List<HypotheseEntity> all); + + + Collection<Hypothese> toDomains(List<HypotheseDTO> hypotheses); + + List<HypotheseDTO> toDtos(List<Hypothese> all); + + + HypotheseId toDomain(HypotheseIdDTO hypotheseIdDTO); + + HypotheseDTO toDTO(Hypothese hypothese); + + + HypotheseIdEntity toEntityId(HypotheseId id); + + Hypothese toDomain(HypotheseEntity hypothese); +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/mapper/ImpactEquipementMapper.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/mapper/ImpactEquipementMapper.java new file mode 100644 index 00000000..d129f74b --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/mapper/ImpactEquipementMapper.java @@ -0,0 +1,33 @@ +package org.mte.numecoeval.referentiel.infrastructure.mapper; + +import org.mapstruct.Mapper; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.ImpactEquipementDTO; +import org.mte.numecoeval.referentiel.domain.model.ImpactEquipement; +import org.mte.numecoeval.referentiel.domain.model.id.ImpactEquipementId; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.ImpactEquipementEntity; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.id.ImpactEquipementIdEntity; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.id.ImpactEquipementIdDTO; + +import java.util.Collection; +import java.util.List; + +@Mapper(componentModel = "spring") +public interface ImpactEquipementMapper { + ImpactEquipementId toDomainId(ImpactEquipementIdDTO id); + + ImpactEquipementEntity toEntity(ImpactEquipement referentiel); + + List<ImpactEquipementEntity> toEntities(Collection<ImpactEquipement> referentiel); + + ImpactEquipementIdEntity toEntityId(ImpactEquipementId id); + + ImpactEquipement toDomain(ImpactEquipementEntity entity); + + List<ImpactEquipement> toDomains(List<ImpactEquipementEntity> entities); + + ImpactEquipement toDomain(ImpactEquipementDTO dto); + + List<ImpactEquipement> toDomainsFromDTO(List<ImpactEquipementDTO> iesDTO); + + ImpactEquipementDTO toDTO(ImpactEquipement impactEquipement); +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/mapper/ImpactMessagerieMapper.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/mapper/ImpactMessagerieMapper.java new file mode 100644 index 00000000..f30310e7 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/mapper/ImpactMessagerieMapper.java @@ -0,0 +1,24 @@ +package org.mte.numecoeval.referentiel.infrastructure.mapper; + +import org.mapstruct.Mapper; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.ImpactMessagerieDTO; +import org.mte.numecoeval.referentiel.domain.model.ImpactMessagerie; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.ImpactMessagerieEntity; + +import java.util.Collection; +import java.util.List; + +@Mapper(componentModel = "spring") +public interface ImpactMessagerieMapper { + + ImpactMessagerieEntity toEntity(ImpactMessagerie impactMessagerie); + List<ImpactMessagerieEntity> toEntities(Collection<ImpactMessagerie> impactMessageries); + + ImpactMessagerie toDomain(ImpactMessagerieEntity impactMessagerieEntity); + ImpactMessagerie toDomain(ImpactMessagerieDTO dto); + List<ImpactMessagerie> toDomains(List<ImpactMessagerieEntity> entities); + List<ImpactMessagerie> toDomainsFromDTO(List<ImpactMessagerieDTO> dtos); + + List<ImpactMessagerieDTO> toDTOs(List<ImpactMessagerie> impactMessageries); + ImpactMessagerieDTO toDTO(ImpactMessagerie impactMessagerie); +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/mapper/ImpactReseauMapper.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/mapper/ImpactReseauMapper.java new file mode 100644 index 00000000..3411ffc6 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/mapper/ImpactReseauMapper.java @@ -0,0 +1,39 @@ +package org.mte.numecoeval.referentiel.infrastructure.mapper; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.ImpactReseauDTO; +import org.mte.numecoeval.referentiel.domain.model.ImpactReseau; +import org.mte.numecoeval.referentiel.domain.model.id.ImpactReseauId; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.ImpactReseauEntity; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.id.ImpactReseauIdEntity; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.id.ImpactReseauIdDTO; + +import java.util.Collection; +import java.util.List; + +@Mapper(componentModel = "spring") + +public interface ImpactReseauMapper { + + ImpactReseauEntity toEntity(ImpactReseau impactReseau); + + @Mapping(source = "etape", target = "etapeACV") + ImpactReseauDTO toDTO(ImpactReseau impactReseau); + + ImpactReseauIdEntity toEntityId(ImpactReseauId reseauId); + + ImpactReseau toDomain(ImpactReseauEntity reseauEntity); + + @Mapping(source = "etapeACV", target = "etape") + ImpactReseau toDomain(ImpactReseauDTO impactReseauDTO); + + @Mapping(source = "etapeACV", target = "etape") + ImpactReseauId toDomainId(ImpactReseauIdDTO idImpactReseauDTO); + + List<ImpactReseauEntity> toEntity(Collection<ImpactReseau> facteursImpacts); + + List<ImpactReseau> toDomains(List<ImpactReseauEntity> entities); + + List<ImpactReseau> toDomainsFromDTO(List<ImpactReseauDTO> dtos); +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/mapper/MixElectriqueMapper.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/mapper/MixElectriqueMapper.java new file mode 100644 index 00000000..c204d9f4 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/mapper/MixElectriqueMapper.java @@ -0,0 +1,32 @@ +package org.mte.numecoeval.referentiel.infrastructure.mapper; + +import org.mapstruct.Mapper; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.MixElectriqueDTO; +import org.mte.numecoeval.referentiel.domain.model.MixElectrique; +import org.mte.numecoeval.referentiel.domain.model.id.MixElectriqueId; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.MixElectriqueEntity; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.id.MixElectriqueIdEntity; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.id.MixElectriqueIdDTO; + +import java.util.Collection; +import java.util.List; + +@Mapper(componentModel = "spring") +public interface MixElectriqueMapper { + + MixElectriqueId toDomainId(MixElectriqueIdDTO id); + + MixElectriqueEntity toEntity(MixElectrique mixElecs); + + List<MixElectriqueEntity> toEntities(Collection<MixElectrique> mixElecs); + + MixElectriqueIdEntity toEntityId(MixElectriqueId id); + + MixElectrique toDomain(MixElectriqueEntity mixElec); + + List<MixElectrique> toDomains(List<MixElectriqueEntity> mixElec); + + List<MixElectrique> toDomainsFromDTO(List<MixElectriqueDTO> mixElecs); + + MixElectriqueDTO toDTO(MixElectrique mixElectrique); +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/mapper/TypeEquipementMapper.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/mapper/TypeEquipementMapper.java new file mode 100644 index 00000000..aef63367 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/mapper/TypeEquipementMapper.java @@ -0,0 +1,23 @@ +package org.mte.numecoeval.referentiel.infrastructure.mapper; + +import org.mapstruct.Mapper; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.TypeEquipementDTO; +import org.mte.numecoeval.referentiel.domain.model.TypeEquipement; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.TypeEquipementEntity; + +import java.util.Collection; +import java.util.List; + +@Mapper(componentModel = "spring") +public interface TypeEquipementMapper { + + TypeEquipement toDomaine(TypeEquipementEntity typeEquipementEntity); + TypeEquipement toDomaine(TypeEquipementDTO typeEquipementDTO); + + TypeEquipementEntity toEntity(TypeEquipement typeEquipement); + List<TypeEquipementEntity> toEntities(Collection<TypeEquipement> typeEquipements); + + TypeEquipementDTO toDto(TypeEquipement typeEquipement); + + +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/BaseExportReferentiel.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/BaseExportReferentiel.java new file mode 100644 index 00000000..118bcbfc --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/BaseExportReferentiel.java @@ -0,0 +1,20 @@ +package org.mte.numecoeval.referentiel.infrastructure.restapi.controller; + +import jakarta.servlet.http.HttpServletResponse; +import org.mte.numecoeval.referentiel.domain.ports.output.ReferentielCsvExportService; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +public interface BaseExportReferentiel<T> { + + default void exportCSV(HttpServletResponse servletResponse, ReferentielCsvExportService<T> csvExportService, String baseFilename) throws IOException { + servletResponse.setStatus(200); + servletResponse.setContentType("text/csv"); + servletResponse.setCharacterEncoding("UTF-8"); + servletResponse.addHeader("Content-Disposition","attachment; filename="+baseFilename+"-"+ + DateTimeFormatter.ofPattern("yyyy-MM-dd_hhmmss").format(LocalDateTime.now()) +".csv"); + csvExportService.writeToCsv(servletResponse.getWriter()); + } +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/correspondance/ReferentielAdministrationCorrespondanceRefEquipementRestApi.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/correspondance/ReferentielAdministrationCorrespondanceRefEquipementRestApi.java new file mode 100644 index 00000000..6db611f8 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/correspondance/ReferentielAdministrationCorrespondanceRefEquipementRestApi.java @@ -0,0 +1,43 @@ +package org.mte.numecoeval.referentiel.infrastructure.restapi.controller.correspondance; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +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 jakarta.servlet.http.HttpServletResponse; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.ErrorResponseDTO; +import org.mte.numecoeval.referentiel.domain.exception.ReferentielException; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.RapportImportDTO; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; + +public interface ReferentielAdministrationCorrespondanceRefEquipementRestApi { + + @Operation( + summary = "Alimentation du référentiel des correspondances de RefEquipement par csv : annule et remplace.", + tags = {"Import Référentiels"}, + operationId = "importCorrespondanceRefEquipementCSV" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Rapport d'import du fichier CSV"), + @ApiResponse(responseCode = "400", description = "Invalid request", + content = {@Content(mediaType = "application/json", + schema = @Schema(implementation = ErrorResponseDTO.class))})}) + @PostMapping(path = "/referentiel/correspondanceRefEquipement/csv", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + RapportImportDTO importCSV(@RequestPart("file") MultipartFile file) throws IOException, ReferentielException; + + + @Operation( + summary = "Exporter les correspondances de RefEquipement sous format csv", + tags = {"Export Référentiels"}, + operationId = "exportCorrespondanceRefEquipementCSV" + ) + @GetMapping("/referentiel/correspondanceRefEquipement/csv") + void exportCSV(HttpServletResponse servletResponse) throws IOException; +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/correspondance/ReferentielCorrespondanceRefEquipementRestApi.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/correspondance/ReferentielCorrespondanceRefEquipementRestApi.java new file mode 100644 index 00000000..2255fe5d --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/correspondance/ReferentielCorrespondanceRefEquipementRestApi.java @@ -0,0 +1,35 @@ +package org.mte.numecoeval.referentiel.infrastructure.restapi.controller.correspondance; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +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 org.mte.numecoeval.referentiel.infrastructure.restapi.dto.CorrespondanceRefEquipementDTO; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.ErrorResponseDTO; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; + +public interface ReferentielCorrespondanceRefEquipementRestApi { + + @Operation( + summary = "Endpoint interne à NumEcoEval - Récupération d'une correspondance de refEquipement à partir d'un modèle d'équipement.", + description = """ + Endpoint interne utilisé dans l'import de données d'entrées dans NumEcoEval + pour déterminer la référence d'équipement à utiliser dans les référentiels'. + """, + tags = "Interne NumEcoEval", + operationId = "getCorrespondanceRefEquipement" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Correspondance trouvée", + content = {@Content(mediaType = "application/json", schema = @Schema(implementation = CorrespondanceRefEquipementDTO.class))}), + @ApiResponse(responseCode = "400", description = "Invalid request", content = {@Content(mediaType = "application/json", + schema = @Schema(implementation = ErrorResponseDTO.class))}), + @ApiResponse(responseCode = "404", description = "Correspondance non trouvée", content = {@Content(mediaType = "application/json", + schema = @Schema(implementation = ErrorResponseDTO.class))})}) + @GetMapping(path = "/referentiel/correspondanceRefEquipement", produces = MediaType.APPLICATION_JSON_VALUE) + CorrespondanceRefEquipementDTO get(@RequestParam(name = "modele") final String modele); + +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/correspondance/ReferentielCorrespondanceRefEquipementRestApiImpl.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/correspondance/ReferentielCorrespondanceRefEquipementRestApiImpl.java new file mode 100644 index 00000000..a552e528 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/correspondance/ReferentielCorrespondanceRefEquipementRestApiImpl.java @@ -0,0 +1,55 @@ +package org.mte.numecoeval.referentiel.infrastructure.restapi.controller.correspondance; + +import jakarta.servlet.http.HttpServletResponse; +import lombok.AllArgsConstructor; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.mte.numecoeval.referentiel.domain.exception.ReferentielException; +import org.mte.numecoeval.referentiel.domain.ports.input.impl.ImportCorrespondanceRefEquipementPortImpl; +import org.mte.numecoeval.referentiel.infrastructure.adapter.export.CorrespondanceRefEquipemenetCsvExportService; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.CorrespondanceRefEquipementEntity; +import org.mte.numecoeval.referentiel.infrastructure.restapi.controller.BaseExportReferentiel; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.CorrespondanceRefEquipementDTO; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.RapportImportDTO; +import org.mte.numecoeval.referentiel.infrastructure.restapi.facade.CorrespondanceRefEquipementFacade; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.server.ResponseStatusException; + +import java.io.IOException; + +@RestController +@Slf4j +@AllArgsConstructor +public class ReferentielCorrespondanceRefEquipementRestApiImpl implements BaseExportReferentiel<CorrespondanceRefEquipementEntity>, ReferentielCorrespondanceRefEquipementRestApi, ReferentielAdministrationCorrespondanceRefEquipementRestApi { + private CorrespondanceRefEquipementFacade referentielFacade; + + private CorrespondanceRefEquipemenetCsvExportService csvExportService; + + @Override + public RapportImportDTO importCSV(MultipartFile fichier) throws IOException, ReferentielException { + if (fichier == null || fichier.isEmpty()) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Le fichier n'existe pas ou alors il est vide"); + } + var rapportImport = new ImportCorrespondanceRefEquipementPortImpl().importCSV(fichier.getInputStream()); + referentielFacade.purgeAndAddAll(rapportImport.getObjects()); + + return new RapportImportDTO( + fichier.getOriginalFilename(), + rapportImport.getErreurs(), + rapportImport.getNbrLignesImportees() + ); + } + + @SneakyThrows + @Override + public CorrespondanceRefEquipementDTO get(String modele) { + return referentielFacade.get(modele); + } + + @Override + public void exportCSV(HttpServletResponse servletResponse) throws IOException { + exportCSV(servletResponse, csvExportService, "correspondancesRefEquipement"); + } +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/critere/ReferentielAdministrationCritereRestApi.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/critere/ReferentielAdministrationCritereRestApi.java new file mode 100644 index 00000000..8e1b8382 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/critere/ReferentielAdministrationCritereRestApi.java @@ -0,0 +1,44 @@ +package org.mte.numecoeval.referentiel.infrastructure.restapi.controller.critere; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +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 jakarta.servlet.http.HttpServletResponse; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.ErrorResponseDTO; +import org.mte.numecoeval.referentiel.domain.exception.ReferentielException; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.RapportImportDTO; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; + +public interface ReferentielAdministrationCritereRestApi { + + @Operation( + summary = "Alimentation des Criteres par csv (annule et remplace).", + description = "Alimentation des Criteres par csv (annule et remplace).", + tags = {"Import Référentiels"}, + operationId = "importCriteresCSV" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Rapport d'import du fichier CSV"), + @ApiResponse(responseCode = "400", description = "Invalid request", content = {@Content(mediaType = "application/json", + schema = @Schema(implementation = ErrorResponseDTO.class))})}) + @PostMapping(path = "/referentiel/criteres/csv", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + RapportImportDTO importCSV(@RequestPart("file") MultipartFile file) throws IOException, ReferentielException; + + + @Operation( + summary = "Exporter les criteres sous format csv", + tags = {"Export Référentiels"}, + operationId = "exportCriteresCSV" + ) + @GetMapping("/referentiel/criteres/csv") + void exportCSV(HttpServletResponse servletResponse) throws IOException; + +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/critere/ReferentielCritereRestApi.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/critere/ReferentielCritereRestApi.java new file mode 100644 index 00000000..f1c5ab8e --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/critere/ReferentielCritereRestApi.java @@ -0,0 +1,36 @@ +package org.mte.numecoeval.referentiel.infrastructure.restapi.controller.critere; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Content; +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 org.mte.numecoeval.referentiel.infrastructure.restapi.dto.CritereDTO; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.ErrorResponseDTO; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; + +import java.util.List; + +public interface ReferentielCritereRestApi { + + @Operation( + summary = "Endpoint interne à NumEcoEval - Récupération de tous les critères d'impacts écologiques", + description = """ + Endpoint interne utilisé dans la génération des indicateurs par le module api-calcul de NumEcoEval. + """, + tags = "Interne NumEcoEval", + operationId = "getAllCriteres" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "impact reseau trouvé", + content = {@Content(mediaType = "application/json", array = @ArraySchema(schema = @Schema(implementation = CritereDTO.class)))}), + @ApiResponse(responseCode = "400", description = "Invalid request", content = {@Content(mediaType = "application/json", + schema = @Schema(implementation = ErrorResponseDTO.class))}), + @ApiResponse(responseCode = "404", description = "Impact Reseau non trouvé", content = {@Content(mediaType = "application/json", + schema = @Schema(implementation = ErrorResponseDTO.class))})}) + @GetMapping(path = "/referentiel/criteres", produces = MediaType.APPLICATION_JSON_VALUE) + List<CritereDTO> getAll(); + +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/critere/ReferentielCritereRestApiImpl.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/critere/ReferentielCritereRestApiImpl.java new file mode 100644 index 00000000..72fe0a6c --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/critere/ReferentielCritereRestApiImpl.java @@ -0,0 +1,56 @@ +package org.mte.numecoeval.referentiel.infrastructure.restapi.controller.critere; + +import jakarta.servlet.http.HttpServletResponse; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.CritereDTO; +import org.mte.numecoeval.referentiel.domain.exception.ReferentielException; +import org.mte.numecoeval.referentiel.domain.ports.input.impl.ImportCriterePortImpl; +import org.mte.numecoeval.referentiel.infrastructure.adapter.export.CritereCsvExportService; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.CritereEntity; +import org.mte.numecoeval.referentiel.infrastructure.restapi.controller.BaseExportReferentiel; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.RapportImportDTO; +import org.mte.numecoeval.referentiel.infrastructure.restapi.facade.CritereFacade; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.server.ResponseStatusException; + +import java.io.IOException; +import java.util.List; + +@RestController +@Slf4j +@AllArgsConstructor +public class ReferentielCritereRestApiImpl implements BaseExportReferentiel<CritereEntity>, ReferentielCritereRestApi, ReferentielAdministrationCritereRestApi { + private CritereFacade referentielFacade; + private CritereCsvExportService csvExportService; + + + @Override + public List<CritereDTO> getAll() { + return referentielFacade.getAll(); + } + + @Override + public RapportImportDTO importCSV(MultipartFile fichier) throws IOException, ReferentielException { + + if (fichier == null || fichier.isEmpty()) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Le fichier n'existe pas ou alors il est vide"); + } + var rapportImport = new ImportCriterePortImpl().importCSV(fichier.getInputStream()); + referentielFacade.purgeAndAddAll(rapportImport.getObjects()); + + return new RapportImportDTO( + fichier.getOriginalFilename(), + rapportImport.getErreurs(), + rapportImport.getNbrLignesImportees() + ); + + } + + @Override + public void exportCSV(HttpServletResponse servletResponse) throws IOException { + exportCSV(servletResponse, csvExportService, "criteres"); + } +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/etape/ReferentielAdministrationEtapeRestApi.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/etape/ReferentielAdministrationEtapeRestApi.java new file mode 100644 index 00000000..dd10619b --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/etape/ReferentielAdministrationEtapeRestApi.java @@ -0,0 +1,44 @@ +package org.mte.numecoeval.referentiel.infrastructure.restapi.controller.etape; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +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 jakarta.servlet.http.HttpServletResponse; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.ErrorResponseDTO; +import org.mte.numecoeval.referentiel.domain.exception.ReferentielException; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.RapportImportDTO; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; + +public interface ReferentielAdministrationEtapeRestApi { + + @Operation( + summary = "Alimentation Etape ACV par csv : annule et remplace.", + description = "Alimentation Etape ACV par csv : annule et remplace.", + tags = {"Import Référentiels"}, + operationId = "importEtapesCSV" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Rapport d'import du fichier CSV"), + @ApiResponse(responseCode = "400", description = "Invalid request", content = {@Content(mediaType = "application/json", + schema = @Schema(implementation = ErrorResponseDTO.class))})}) + @PostMapping(path = "/referentiel/etapes/csv", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + RapportImportDTO importCSV(@RequestPart("file") MultipartFile file) throws IOException, ReferentielException; + + @Operation( + summary = "Exporter les etapes sous format csv", + tags = {"Export Référentiels"}, + operationId = "exportEtapesCSV" + ) + @GetMapping("/referentiel/etapes/csv") + void exportCSV(HttpServletResponse servletResponse) throws IOException; + + +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/etape/ReferentielEtapeRestApi.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/etape/ReferentielEtapeRestApi.java new file mode 100644 index 00000000..8fc7bcd7 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/etape/ReferentielEtapeRestApi.java @@ -0,0 +1,37 @@ +package org.mte.numecoeval.referentiel.infrastructure.restapi.controller.etape; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Content; +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 org.mte.numecoeval.referentiel.infrastructure.restapi.dto.ErrorResponseDTO; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.EtapeDTO; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; + +import java.util.List; + +public interface ReferentielEtapeRestApi { + + @Operation( + summary = "Endpoint interne à NumEcoEval - Récupération de toutes les étapes ACV", + description = """ + Endpoint interne utilisé dans la génération des indicateurs par le module api-calcul de NumEcoEval. + Renvoie l'intégralité des étapes du cycle de vie (étapes ACV) des équipements. + """, + tags = "Interne NumEcoEval", + operationId = "getAllEtapes" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "etape acv non trouvé", + content = {@Content(mediaType = "application/json", array = @ArraySchema(schema = @Schema(implementation = EtapeDTO.class)))}), + @ApiResponse(responseCode = "400", description = "Invalid request", content = {@Content(mediaType = "application/json", + schema = @Schema(implementation = ErrorResponseDTO.class))}), + @ApiResponse(responseCode = "404", description = "Impact Reseau non trouvé", content = {@Content(mediaType = "application/json", + schema = @Schema(implementation = ErrorResponseDTO.class))})}) + @GetMapping(path = "/referentiel/etapes", produces = MediaType.APPLICATION_JSON_VALUE) + List<EtapeDTO> getAll(); + +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/etape/ReferentielEtapeRestApiImpl.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/etape/ReferentielEtapeRestApiImpl.java new file mode 100644 index 00000000..2335d8f0 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/etape/ReferentielEtapeRestApiImpl.java @@ -0,0 +1,56 @@ +package org.mte.numecoeval.referentiel.infrastructure.restapi.controller.etape; + +import jakarta.servlet.http.HttpServletResponse; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.EtapeDTO; +import org.mte.numecoeval.referentiel.domain.exception.ReferentielException; +import org.mte.numecoeval.referentiel.domain.ports.input.impl.ImportEtapePortImpl; +import org.mte.numecoeval.referentiel.infrastructure.adapter.export.EtapeCsvExportService; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.EtapeEntity; +import org.mte.numecoeval.referentiel.infrastructure.restapi.controller.BaseExportReferentiel; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.RapportImportDTO; +import org.mte.numecoeval.referentiel.infrastructure.restapi.facade.EtapeFacade; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.server.ResponseStatusException; + +import java.io.IOException; +import java.util.List; + +@RestController +@Slf4j +@AllArgsConstructor +public class ReferentielEtapeRestApiImpl implements BaseExportReferentiel<EtapeEntity>,ReferentielEtapeRestApi, ReferentielAdministrationEtapeRestApi { + private EtapeFacade referentielFacade; + private EtapeCsvExportService csvExportService; + + + + @Override + public List<EtapeDTO> getAll() { + return referentielFacade.getAll(); + } + + @Override + public RapportImportDTO importCSV(MultipartFile fichier) throws IOException, ReferentielException { + if (fichier == null || fichier.isEmpty()) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Le fichier n'existe pas ou alors il est vide"); + } + var rapportImport = new ImportEtapePortImpl().importCSV(fichier.getInputStream()); + + referentielFacade.purgeAndAddAll(rapportImport.getObjects()); + + return new RapportImportDTO( + fichier.getOriginalFilename(), + rapportImport.getErreurs(), + rapportImport.getNbrLignesImportees() + ); + } + + @Override + public void exportCSV(HttpServletResponse servletResponse) throws IOException { + exportCSV(servletResponse, csvExportService, "etapes"); + } +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/exception/ReferentelExceptionHandler.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/exception/ReferentelExceptionHandler.java new file mode 100644 index 00000000..245a9e7a --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/exception/ReferentelExceptionHandler.java @@ -0,0 +1,79 @@ +package org.mte.numecoeval.referentiel.infrastructure.restapi.controller.exception; + +import lombok.extern.slf4j.Slf4j; +import org.mte.numecoeval.referentiel.domain.exception.NotFoundException; +import org.mte.numecoeval.referentiel.domain.exception.ReferentielException; +import org.mte.numecoeval.referentiel.domain.exception.ReferentielRuntimeException; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.ErrorResponseDTO; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.context.request.WebRequest; + +import java.time.LocalDateTime; + +import static org.springframework.http.HttpStatus.*; + +@Slf4j +@RestControllerAdvice +public class ReferentelExceptionHandler { + + /** + * writer error message + * + * @param ex excepetion + * @param status le statut http + * @return l'objet erreur + */ + private static ErrorResponseDTO writeErrorResponse(Exception ex, HttpStatus status) { + return ErrorResponseDTO.builder() + .status(status) + .code(status.value()) + .timestamp(LocalDateTime.now()) + .message(ex.getLocalizedMessage()).build(); + } + + @ExceptionHandler(value = {NotFoundException.class}) + public ErrorResponseDTO notFoundException(Exception ex, WebRequest request) { + return writeErrorResponse(ex, NOT_FOUND); + } + + @ExceptionHandler(value = {ReferentielException.class}) + @ResponseStatus(value = NOT_FOUND) + public ErrorResponseDTO referentielException(Exception ex, WebRequest request) { + return writeErrorResponse(ex, NOT_FOUND); + } + + @ExceptionHandler(value = {ReferentielRuntimeException.class}) + @ResponseStatus(value = INTERNAL_SERVER_ERROR) + public ErrorResponseDTO referentielPersistenceException(Exception ex, WebRequest request) { + return writeErrorResponse(ex, INTERNAL_SERVER_ERROR); + } + + @ExceptionHandler(value = {DataIntegrityViolationException.class}) + @ResponseStatus(BAD_REQUEST) + public ErrorResponseDTO dataIntegrityViolationException(DataIntegrityViolationException ex, WebRequest request) { + log.debug("DataIntegrityViolationException lors d'un traitement sur l'URI {}", request.getContextPath(), ex); + return writeErrorResponse(new Exception("Erreur d'intégrité lors de la persistence des données."), BAD_REQUEST); + } + + @ExceptionHandler(value = {RuntimeException.class}) + @ResponseStatus(value = INTERNAL_SERVER_ERROR) + public ErrorResponseDTO runtimeException(Exception ex, WebRequest request) { + log.error("RuntimeException lors d'un traitement sur l'URI {}", request.getContextPath(), ex); + log.debug("RuntimeException lors d'un traitement sur l'URI {}", request.getContextPath(), ex); + return writeErrorResponse(new Exception("Erreur interne de traitement lors du traitement de la requête"), INTERNAL_SERVER_ERROR); + } + + @ExceptionHandler(value = {Exception.class}) + @ResponseStatus(value = INTERNAL_SERVER_ERROR) + public ErrorResponseDTO exception(Exception ex, WebRequest request) { + log.error("Exception lors d'un traitement sur l'URI {} : {}", request.getContextPath(), ex.getMessage()); + log.debug("Exception lors d'un traitement sur l'URI {}", request.getContextPath(), ex); + return writeErrorResponse(new Exception("Erreur interne de traitement lors du traitement de la requête"), INTERNAL_SERVER_ERROR); + } + +} + diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/hypothese/ReferentielAdministrationHypotheseRestApi.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/hypothese/ReferentielAdministrationHypotheseRestApi.java new file mode 100644 index 00000000..34bb5683 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/hypothese/ReferentielAdministrationHypotheseRestApi.java @@ -0,0 +1,44 @@ +package org.mte.numecoeval.referentiel.infrastructure.restapi.controller.hypothese; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +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 jakarta.servlet.http.HttpServletResponse; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.ErrorResponseDTO; +import org.mte.numecoeval.referentiel.domain.exception.ReferentielException; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.RapportImportDTO; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; + +public interface ReferentielAdministrationHypotheseRestApi { + + @Operation( + summary = "Alimentation des Hypothèses par csv : annule et remplace.", + description = "Alimentation des Hypothèses par csv : annule et remplace.", + tags = {"Import Référentiels"}, + operationId = "importHypothesesCSV" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Rapport d'import du fichier CSV"), + @ApiResponse(responseCode = "400", description = "Invalid request", content = {@Content(mediaType = "application/json", + schema = @Schema(implementation = ErrorResponseDTO.class))})}) + @PostMapping(path = "/referentiel/hypotheses/csv", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + RapportImportDTO importCSV(@RequestPart("file") MultipartFile file) throws IOException, ReferentielException; + + + @Operation( + summary = "Exporter les hypothèses sous format csv", + tags = {"Export Référentiels"}, + operationId = "exportHypothesesCSV" + ) + @GetMapping("/referentiel/hypotheses/csv") + void exportCSV(HttpServletResponse servletResponse) throws IOException; + +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/hypothese/ReferentielHypotheseRestApi.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/hypothese/ReferentielHypotheseRestApi.java new file mode 100644 index 00000000..dc18fd82 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/hypothese/ReferentielHypotheseRestApi.java @@ -0,0 +1,35 @@ +package org.mte.numecoeval.referentiel.infrastructure.restapi.controller.hypothese; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +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 org.mte.numecoeval.referentiel.infrastructure.restapi.dto.ErrorResponseDTO; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.HypotheseDTO; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; + +public interface ReferentielHypotheseRestApi { + + @Operation( + summary = "Endpoint interne à NumEcoEval - Récupération d'une hypothèse par son code", + description = """ + Endpoint interne utilisé dans la génération des indicateurs par le module api-calcul de NumEcoEval. + Renvoie une hypothèse en fonction de sa clé. + """, + tags = "Interne NumEcoEval", + operationId = "getHypothese" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Hypothèse trouvé", + content = {@Content(mediaType = "application/json", schema = @Schema(implementation = HypotheseDTO.class))}), + @ApiResponse(responseCode = "400", description = "Invalid request", content = {@Content(mediaType = "application/json", + schema = @Schema(implementation = ErrorResponseDTO.class))}), + @ApiResponse(responseCode = "404", description = "Hypothèse non trouvé", content = {@Content(mediaType = "application/json", + schema = @Schema(implementation = ErrorResponseDTO.class))})}) + @GetMapping(path = "/referentiel/hypothese", produces = MediaType.APPLICATION_JSON_VALUE) + HypotheseDTO get(@RequestParam @Schema(description = "Clé de l'hypothèse") final String cle); + +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/hypothese/ReferentielHypotheseRestApiImpl.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/hypothese/ReferentielHypotheseRestApiImpl.java new file mode 100644 index 00000000..9a021fb0 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/hypothese/ReferentielHypotheseRestApiImpl.java @@ -0,0 +1,58 @@ +package org.mte.numecoeval.referentiel.infrastructure.restapi.controller.hypothese; + +import jakarta.servlet.http.HttpServletResponse; +import lombok.AllArgsConstructor; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.mte.numecoeval.referentiel.domain.exception.ReferentielException; +import org.mte.numecoeval.referentiel.domain.ports.input.impl.ImportHypothesePortImpl; +import org.mte.numecoeval.referentiel.infrastructure.adapter.export.HypotheseCsvExportService; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.HypotheseEntity; +import org.mte.numecoeval.referentiel.infrastructure.restapi.controller.BaseExportReferentiel; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.HypotheseDTO; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.RapportImportDTO; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.id.HypotheseIdDTO; +import org.mte.numecoeval.referentiel.infrastructure.restapi.facade.HypotheseFacade; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.server.ResponseStatusException; + +import java.io.IOException; + +@RestController +@Slf4j +@AllArgsConstructor +public class ReferentielHypotheseRestApiImpl implements BaseExportReferentiel<HypotheseEntity>, ReferentielAdministrationHypotheseRestApi, ReferentielHypotheseRestApi { + private HypotheseFacade referentielFacade; + + private HypotheseCsvExportService csvExportService; + + @Override + public RapportImportDTO importCSV(MultipartFile fichier) throws IOException, ReferentielException { + if (fichier == null || fichier.isEmpty()) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Le fichier n'existe pas ou alors il est vide"); + } + var rapportImport = new ImportHypothesePortImpl().importCSV(fichier.getInputStream()); + referentielFacade.purgeAndAddAll(rapportImport.getObjects()); + + return new RapportImportDTO( + fichier.getOriginalFilename(), + rapportImport.getErreurs(), + rapportImport.getNbrLignesImportees() + ); + } + + @SneakyThrows + @Override + public HypotheseDTO get(String cle) { + HypotheseIdDTO hypotheseIdDTO = new HypotheseIdDTO(); + hypotheseIdDTO.setCode(cle); + return referentielFacade.get(hypotheseIdDTO); + } + + @Override + public void exportCSV(HttpServletResponse servletResponse) throws IOException { + exportCSV(servletResponse, csvExportService, "hypotheses"); + } +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/impactequipement/ReferentielAdministrationImpactEquipementRestApi.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/impactequipement/ReferentielAdministrationImpactEquipementRestApi.java new file mode 100644 index 00000000..acef1066 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/impactequipement/ReferentielAdministrationImpactEquipementRestApi.java @@ -0,0 +1,44 @@ +package org.mte.numecoeval.referentiel.infrastructure.restapi.controller.impactequipement; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +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 jakarta.servlet.http.HttpServletResponse; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.ErrorResponseDTO; +import org.mte.numecoeval.referentiel.domain.exception.ReferentielException; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.RapportImportDTO; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; + +public interface ReferentielAdministrationImpactEquipementRestApi { + + @Operation( + summary = "Alimentation du référentiel ImpactEquipement par csv : annule et remplace.", + description = "Alimentation du référentiel ImpactEquipement par csv : annule et remplace.", + tags = {"Import Référentiels"}, + operationId = "importImpactEquipementCSV" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Rapport d'import du fichier CSV"), + @ApiResponse(responseCode = "400", description = "Invalid request", content = {@Content(mediaType = "application/json", + schema = @Schema(implementation = ErrorResponseDTO.class))})}) + @PostMapping(path = "/referentiel/impactequipements/csv", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + RapportImportDTO importCSV(@RequestPart("file") MultipartFile file) throws IOException, ReferentielException; + + + @Operation( + summary = "Exporter les impacts d'équipements sous format csv", + tags = {"Export Référentiels"}, + operationId = "exportImpactEquipementCSV" + ) + @GetMapping("/referentiel/impactequipements/csv") + void exportCSV(HttpServletResponse servletResponse) throws IOException; + +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/impactequipement/ReferentielImpactEquipementRestApi.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/impactequipement/ReferentielImpactEquipementRestApi.java new file mode 100644 index 00000000..7f642a9b --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/impactequipement/ReferentielImpactEquipementRestApi.java @@ -0,0 +1,46 @@ +package org.mte.numecoeval.referentiel.infrastructure.restapi.controller.impactequipement; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +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 org.mte.numecoeval.referentiel.infrastructure.restapi.dto.ErrorResponseDTO; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.ImpactEquipementDTO; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; + +public interface ReferentielImpactEquipementRestApi { + + @Operation(summary = "Endpoint interne à NumEcoEval - Récupération d'un Impact Equipement", + description = """ + Endpoint interne utilisé dans la génération des indicateurs par le module api-calcul de NumEcoEval. + Récupération d'un impact équipement en fonction de 3 paramètres: + <ul> + <li>La référence d'équipement: refEquipement</li> + <li>Le critère d'impact: critere</li> + <li>L'étape ACV: etapeACV</li> + </ul> + . + """, + tags = "Interne NumEcoEval", + operationId = "getImpactEquipement") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "impact equipement trouvé", + content = {@Content(mediaType = "application/json", schema = @Schema(implementation = ImpactEquipementDTO.class))}), + @ApiResponse(responseCode = "400", description = "Invalid request", content = {@Content(mediaType = "application/json", + schema = @Schema(implementation = ErrorResponseDTO.class))}), + @ApiResponse(responseCode = "404", description = "Impact Equipement non trouvé", content = {@Content(mediaType = "application/json", + schema = @Schema(implementation = ErrorResponseDTO.class))})}) + @GetMapping(path = "/referentiel/impactequipements", produces = MediaType.APPLICATION_JSON_VALUE) + ImpactEquipementDTO get( + @RequestParam + @Schema(description = "Référence de l'équipement recherché") final String refEquipement, + @RequestParam + @Schema(description = "Nom du critère d'impact écologique") final String critere, + @RequestParam + @Schema(description = "Code de l'étape ACV") final String etapeacv + ); + +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/impactequipement/ReferentielImpactEquipementRestApiImpl.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/impactequipement/ReferentielImpactEquipementRestApiImpl.java new file mode 100644 index 00000000..27e9300d --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/impactequipement/ReferentielImpactEquipementRestApiImpl.java @@ -0,0 +1,66 @@ +package org.mte.numecoeval.referentiel.infrastructure.restapi.controller.impactequipement; + +import jakarta.servlet.http.HttpServletResponse; +import lombok.AllArgsConstructor; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.mte.numecoeval.referentiel.domain.exception.ReferentielException; +import org.mte.numecoeval.referentiel.domain.ports.input.impl.ImportImpactEquipementPortImpl; +import org.mte.numecoeval.referentiel.infrastructure.adapter.export.ImpactEquipementCsvExportService; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.ImpactEquipementEntity; +import org.mte.numecoeval.referentiel.infrastructure.restapi.controller.BaseExportReferentiel; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.ImpactEquipementDTO; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.RapportImportDTO; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.id.ImpactEquipementIdDTO; +import org.mte.numecoeval.referentiel.infrastructure.restapi.facade.ImpactEquipementFacade; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.server.ResponseStatusException; + +import java.io.IOException; + +@RestController +@Slf4j +@AllArgsConstructor +public class ReferentielImpactEquipementRestApiImpl implements BaseExportReferentiel<ImpactEquipementEntity>, ReferentielImpactEquipementRestApi, ReferentielAdministrationImpactEquipementRestApi { + private ImpactEquipementFacade referentielFacade; + + private ImpactEquipementCsvExportService csvExportService; + + @SneakyThrows + @Override + public ImpactEquipementDTO get(String refEquipement, String critere, String etapeacv) { + + ImpactEquipementIdDTO id = ImpactEquipementIdDTO.builder() + .refEquipement(StringUtils.trim(refEquipement)) + .critere(critere) + .etape(etapeacv) + .build(); + + return referentielFacade.get(id); + } + + @Override + public RapportImportDTO importCSV(MultipartFile fichier) throws IOException, ReferentielException { + + if (fichier == null || fichier.isEmpty()) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Le fichier n'existe pas ou alors il est vide"); + } + var rapportImport = new ImportImpactEquipementPortImpl().importCSV(fichier.getInputStream()); + referentielFacade.purgeAndAddAll(rapportImport.getObjects()); + + return new RapportImportDTO( + fichier.getOriginalFilename(), + rapportImport.getErreurs(), + rapportImport.getNbrLignesImportees() + ); + + } + + @Override + public void exportCSV(HttpServletResponse servletResponse) throws IOException { + exportCSV(servletResponse, csvExportService, "impactEquipement"); + } +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/impactmessagerie/ReferentielImpactMessagerieRestApi.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/impactmessagerie/ReferentielImpactMessagerieRestApi.java new file mode 100644 index 00000000..df45d627 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/impactmessagerie/ReferentielImpactMessagerieRestApi.java @@ -0,0 +1,61 @@ +package org.mte.numecoeval.referentiel.infrastructure.restapi.controller.impactmessagerie; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Content; +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 org.mte.numecoeval.referentiel.infrastructure.restapi.dto.ErrorResponseDTO; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.ImpactMessagerieDTO; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; + +import java.util.List; + +public interface ReferentielImpactMessagerieRestApi { + + + @Operation(summary = "Endpoint interne à NumEcoEval - Récupération d'un impact de messagerie", + description = """ + Endpoint interne utilisé dans l'enrichissement des données pour un calcul dans le module api-enrichissement de NumEcoEval. + Renvoie un élément du référentiel des impacts de messagerie. + """, + tags = "Interne NumEcoEval", + operationId = "getImpactMessagerie" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = " Référentiel Impact Messagerie", + content = {@Content(mediaType = "application/json", + schema = @Schema(implementation = ImpactMessagerieDTO.class))}), + @ApiResponse(responseCode = "400", description = "Invalid request", + content = {@Content(mediaType = "application/json", + schema = @Schema(implementation = ErrorResponseDTO.class))}), + @ApiResponse(responseCode = "404", description = " Référentiel Impact Messagerie non trouvé", content = {@Content(mediaType = "application/json", + schema = @Schema(implementation = ErrorResponseDTO.class))})}) + @GetMapping(path = "/referentiel/impactsMessagerie/{critere}", produces = MediaType.APPLICATION_JSON_VALUE) + ImpactMessagerieDTO getImpactMessagerie(@PathVariable + @Schema(description = "Critère recherché") String critere); + + + @Operation(summary = "Endpoint interne à NumEcoEval - Récupération des impacts de messagerie", + description = """ + Endpoint interne utilisé dans la génération des indicateurs par le module api-calcul de NumEcoEval. + Renvoie l'intégralité des impacts de messagerie. + """, + tags = "Interne NumEcoEval", + operationId = "getAllImpactMessagerie" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = " Référentiel Impact Messagerie", + content = {@Content(mediaType = "application/json", + array = @ArraySchema(schema = @Schema(implementation = ImpactMessagerieDTO.class)))}), + @ApiResponse(responseCode = "400", description = "Invalid request", + content = {@Content(mediaType = "application/json", + schema = @Schema(implementation = ErrorResponseDTO.class))}), + @ApiResponse(responseCode = "404", description = " Référentiel Impact Messagerie non trouvé", content = {@Content(mediaType = "application/json", + schema = @Schema(implementation = ErrorResponseDTO.class))})}) + @GetMapping(path = "/referentiel/impactsMessagerie", produces = MediaType.APPLICATION_JSON_VALUE) + List<ImpactMessagerieDTO> getAllImpactMessagerie(); +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/impactmessagerie/ReferentielImpactMessagerieRestApiImpl.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/impactmessagerie/ReferentielImpactMessagerieRestApiImpl.java new file mode 100644 index 00000000..0ee0ff2e --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/impactmessagerie/ReferentielImpactMessagerieRestApiImpl.java @@ -0,0 +1,59 @@ +package org.mte.numecoeval.referentiel.infrastructure.restapi.controller.impactmessagerie; + +import jakarta.servlet.http.HttpServletResponse; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.mte.numecoeval.referentiel.domain.exception.ReferentielException; +import org.mte.numecoeval.referentiel.domain.ports.input.impl.ImportImpactMessageriePortImpl; +import org.mte.numecoeval.referentiel.infrastructure.adapter.export.ImpactMessagerieCsvExportService; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.ImpactMessagerieEntity; +import org.mte.numecoeval.referentiel.infrastructure.restapi.controller.BaseExportReferentiel; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.ImpactMessagerieDTO; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.RapportImportDTO; +import org.mte.numecoeval.referentiel.infrastructure.restapi.facade.ImpactMessagerieFacade; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.server.ResponseStatusException; + +import java.io.IOException; +import java.util.List; + +@RestController +@Slf4j +@AllArgsConstructor +public class ReferentielImpactMessagerieRestApiImpl implements BaseExportReferentiel<ImpactMessagerieEntity>, ReferentielInterneImpactMessagerieRestApi, ReferentielImpactMessagerieRestApi { + + private ImpactMessagerieFacade impactMessagerieFacade; + + private ImpactMessagerieCsvExportService csvExportService; + + @Override + public RapportImportDTO importCSV(MultipartFile fichier) throws IOException, ReferentielException { + if (fichier == null || fichier.isEmpty()) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Le fichier n'existe pas ou alors il est vide"); + } + var rapportImport = new ImportImpactMessageriePortImpl().importCSV(fichier.getInputStream()); + impactMessagerieFacade.purgeAndAddAll(rapportImport.getObjects()); + return new RapportImportDTO( + fichier.getOriginalFilename(), + rapportImport.getErreurs(), + rapportImport.getNbrLignesImportees() + ); + } + + @Override + public ImpactMessagerieDTO getImpactMessagerie(String critere) { + return impactMessagerieFacade.getImpactMessagerieForCritere(critere); + } + + @Override + public List<ImpactMessagerieDTO> getAllImpactMessagerie() { + return impactMessagerieFacade.getAllImpactMessagerie(); + } + + @Override + public void exportCSV(HttpServletResponse servletResponse) throws IOException { + exportCSV(servletResponse, csvExportService, "impactMessagerie"); + } +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/impactmessagerie/ReferentielInterneImpactMessagerieRestApi.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/impactmessagerie/ReferentielInterneImpactMessagerieRestApi.java new file mode 100644 index 00000000..6272030f --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/impactmessagerie/ReferentielInterneImpactMessagerieRestApi.java @@ -0,0 +1,44 @@ +package org.mte.numecoeval.referentiel.infrastructure.restapi.controller.impactmessagerie; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +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 jakarta.servlet.http.HttpServletResponse; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.ErrorResponseDTO; +import org.mte.numecoeval.referentiel.domain.exception.ReferentielException; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.RapportImportDTO; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; + +public interface ReferentielInterneImpactMessagerieRestApi { + + @Operation( + summary = "Alimentation du référentiel Impact Messagerie par csv : annule et remplace.", + description = "Alimentation du référentiel Impact Messagerie par csv : annule et remplace.", + tags = {"Import Référentiels"}, + operationId = "importImpactMessagerieCSV" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Rapport d'import du fichier CSV"), + @ApiResponse(responseCode = "400", description = "Invalid request", + content = {@Content(mediaType = "application/json", + schema = @Schema(implementation = ErrorResponseDTO.class))})}) + @PostMapping(path = "/referentiel/impactMessagerie/csv", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + RapportImportDTO importCSV(@RequestPart("file") MultipartFile file) throws IOException, ReferentielException; + + + @Operation( + summary = "Exporter les impacts de messagerie sous format csv", + tags = {"Export Référentiels"}, + operationId = "exportImpactMessagerieCSV" + ) + @GetMapping("/referentiel/impactMessagerie/csv") + void exportCSV(HttpServletResponse servletResponse) throws IOException; +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/impactreseau/ReferentielAdministrationImpactReseauRestApi.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/impactreseau/ReferentielAdministrationImpactReseauRestApi.java new file mode 100644 index 00000000..0d2340e1 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/impactreseau/ReferentielAdministrationImpactReseauRestApi.java @@ -0,0 +1,79 @@ +package org.mte.numecoeval.referentiel.infrastructure.restapi.controller.impactreseau; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +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 jakarta.servlet.http.HttpServletResponse; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.ErrorResponseDTO; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.ImpactReseauDTO; +import org.mte.numecoeval.referentiel.domain.exception.ReferentielException; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.RapportImportDTO; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; + +public interface ReferentielAdministrationImpactReseauRestApi { + + @Operation( + summary = "Exposition ressource ajout d' un Impact Reseau", + tags = {"Administration détaillée"}, + hidden = true, + description = "Exposition ressource ajout d' un Impact Reseau" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "impact reseau trouvé", + content = {@Content(mediaType = "application/json", schema = @Schema(implementation = ImpactReseauDTO.class))}), + @ApiResponse(responseCode = "400", description = "Invalid request", content = {@Content(mediaType = "application/json", + schema = @Schema(implementation = ErrorResponseDTO.class))}), + @ApiResponse(responseCode = "404", description = "Impact Réseau non trouvé", content = {@Content(mediaType = "application/json", + schema = @Schema(implementation = ErrorResponseDTO.class))})}) + @PostMapping(path = "/referentiel/impactreseaux", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) + ResponseEntity<ImpactReseauDTO> add(@RequestBody final ImpactReseauDTO impactReseauDTO) throws ReferentielException; + + @Operation( + summary = "Exposition ressource update Impact Reseau ", + tags = {"Administration détaillée"},hidden = true + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Modication d'un impact reseau : les champs refReseau, critere & etapeACV ne peuvent pas être modifié", + content = {@Content(mediaType = "application/json", schema = @Schema(implementation = ImpactReseauDTO.class))}), + @ApiResponse(responseCode = "400", description = "Invalid request", content = {@Content(mediaType = "application/json", + schema = @Schema(implementation = ErrorResponseDTO.class))}), + @ApiResponse(responseCode = "404", description = "Impact Réseau non trouvé", content = {@Content(mediaType = "application/json", + schema = @Schema(implementation = ErrorResponseDTO.class))})}) + @PutMapping(path = "/referentiel/impactreseaux", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) + ResponseEntity<ImpactReseauDTO> update(@RequestBody final ImpactReseauDTO impactReseauDTO) throws ReferentielException; + + + @Operation( + summary = "Alimentation du référentiel ImpactReseau par csv : annule et remplace.", + tags = {"Import Référentiels"}, + operationId = "importImpactReseauxCSV" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Rapport d'import du fichier CSV"), + @ApiResponse(responseCode = "400", description = "Invalid request", content = {@Content(mediaType = "application/json", + schema = @Schema(implementation = ErrorResponseDTO.class))})}) + @PostMapping(path = "/referentiel/impactreseaux/csv", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + RapportImportDTO importCSV(@RequestPart("file") MultipartFile file) throws IOException, ReferentielException; + + + @Operation( + summary = "Exporter les impacts réseaux sous format csv", + tags = {"Export Référentiels"}, + operationId = "exportImpactReseauxCSV" + ) + @GetMapping("/referentiel/impactreseaux/csv") + void exportCSV(HttpServletResponse servletResponse) throws IOException; + + +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/impactreseau/ReferentielImpactReseauRestApi.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/impactreseau/ReferentielImpactReseauRestApi.java new file mode 100644 index 00000000..3f657002 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/impactreseau/ReferentielImpactReseauRestApi.java @@ -0,0 +1,46 @@ +package org.mte.numecoeval.referentiel.infrastructure.restapi.controller.impactreseau; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +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 org.mte.numecoeval.referentiel.infrastructure.restapi.dto.ErrorResponseDTO; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.ImpactReseauDTO; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; + +public interface ReferentielImpactReseauRestApi { + + @Operation(summary = "Endpoint interne à NumEcoEval - Récupération d'un Impact Réseau", + description = """ + Endpoint interne utilisé dans la génération des indicateurs par le module api-calcul de NumEcoEval. + Récupération d'un impact écologique vis à vis de l'usage du réseau en fonction de 3 paramètres: + <ul> + <li>La référence d'impact réseau: refReseau</li> + <li>Le critère d'impact: critere</li> + <li>L'étape ACV: etapeACV</li> + </ul> + . + """, + tags = "Interne NumEcoEval", + operationId = "getImpactReseau") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "impact reseau trouvé", + content = {@Content(mediaType = "application/json", schema = @Schema(implementation = ImpactReseauDTO.class))}), + @ApiResponse(responseCode = "400", description = "Invalid request", content = {@Content(mediaType = "application/json", + schema = @Schema(implementation = ErrorResponseDTO.class))}), + @ApiResponse(responseCode = "404", description = "Impact Reseau non trouvé", content = {@Content(mediaType = "application/json", + schema = @Schema(implementation = ErrorResponseDTO.class))})}) + @GetMapping(path = "/referentiel/impactreseaux", produces = MediaType.APPLICATION_JSON_VALUE) + ImpactReseauDTO get( + @RequestParam + @Schema(description = "Référence de réseau recherché") final String refReseau, + @RequestParam + @Schema(description = "Nom du critère d'impact écologique") final String critere, + @RequestParam + @Schema(description = "Code de l'étape ACV") final String etapeacv + ); + +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/impactreseau/ReferentielImpactReseauRestApiImpl.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/impactreseau/ReferentielImpactReseauRestApiImpl.java new file mode 100644 index 00000000..bdd40ff1 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/impactreseau/ReferentielImpactReseauRestApiImpl.java @@ -0,0 +1,93 @@ +package org.mte.numecoeval.referentiel.infrastructure.restapi.controller.impactreseau; + +import jakarta.servlet.http.HttpServletResponse; +import lombok.AllArgsConstructor; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.mte.numecoeval.referentiel.domain.exception.ReferentielException; +import org.mte.numecoeval.referentiel.domain.ports.input.impl.ImportImpactReseauPortImpl; +import org.mte.numecoeval.referentiel.infrastructure.adapter.export.ImpactReseauCsvExportService; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.ImpactReseauEntity; +import org.mte.numecoeval.referentiel.infrastructure.restapi.controller.BaseExportReferentiel; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.ImpactReseauDTO; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.RapportImportDTO; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.id.ImpactReseauIdDTO; +import org.mte.numecoeval.referentiel.infrastructure.restapi.facade.ImpactReseauFacade; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.server.ResponseStatusException; + +import java.io.IOException; +import java.util.Objects; + +@RestController +@Slf4j +@AllArgsConstructor +public class ReferentielImpactReseauRestApiImpl implements BaseExportReferentiel<ImpactReseauEntity>, ReferentielImpactReseauRestApi, ReferentielAdministrationImpactReseauRestApi { + private ImpactReseauFacade referentielFacade; + + private ImpactReseauCsvExportService csvExportService; + + + @SneakyThrows + @Override + public ImpactReseauDTO get(String refReseau, String critere, String etapeacv) { + ImpactReseauIdDTO idImpactReseauDTO = ImpactReseauIdDTO.builder() + .critere(critere) + .etapeACV(etapeacv) + .refReseau(refReseau).build(); + return referentielFacade.get(idImpactReseauDTO); + } + + + @Override + public ResponseEntity<ImpactReseauDTO> add(ImpactReseauDTO impactReseauDTO) throws ReferentielException { + if (Objects.isNull(impactReseauDTO)) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Le corps de la requête ne peut être null"); + } + if (StringUtils.isBlank(impactReseauDTO.getSource())) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "La source est obligatoire"); + } + return ResponseEntity.ok().body(referentielFacade.addOrUpdate(impactReseauDTO)); + } + + @Override + @PutMapping(path = "/referentiel/impactreseaux", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity<ImpactReseauDTO> update(ImpactReseauDTO impactReseauDTO) throws ReferentielException { + if (Objects.isNull(impactReseauDTO)) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Le corps de la requête ne peut être null"); + } + if (StringUtils.isBlank(impactReseauDTO.getSource())) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "La source est obligatoire"); + } + return ResponseEntity.ok().body(referentielFacade.addOrUpdate(impactReseauDTO)); + } + + @Override + @PostMapping(path = "/referentiel/impactreseaux/csv", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public RapportImportDTO importCSV(MultipartFile fichier) throws IOException, ReferentielException { + if (fichier == null || fichier.isEmpty()) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Le fichier n'existe pas ou alors il est vide"); + } + var rapportImport = new ImportImpactReseauPortImpl().importCSV(fichier.getInputStream()); + referentielFacade.purgeAndAddAll(rapportImport.getObjects()); + + return new RapportImportDTO( + fichier.getOriginalFilename(), + rapportImport.getErreurs(), + rapportImport.getNbrLignesImportees() + ); + } + + @Override + public void exportCSV(HttpServletResponse servletResponse) throws IOException { + exportCSV(servletResponse, csvExportService, "impactReseaux"); + } + +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/mixelectrique/ReferentielAdministrationMixElectriqueRestApi.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/mixelectrique/ReferentielAdministrationMixElectriqueRestApi.java new file mode 100644 index 00000000..a9830845 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/mixelectrique/ReferentielAdministrationMixElectriqueRestApi.java @@ -0,0 +1,45 @@ +package org.mte.numecoeval.referentiel.infrastructure.restapi.controller.mixelectrique; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +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 jakarta.servlet.http.HttpServletResponse; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.ErrorResponseDTO; +import org.mte.numecoeval.referentiel.domain.exception.ReferentielException; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.RapportImportDTO; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; + +public interface ReferentielAdministrationMixElectriqueRestApi { + + + @Operation( + summary = "Alimentation du référentiel ImpactEquipement par csv : annule et remplace.", + tags = {"Import Référentiels"}, + operationId = "importImpactEquipementCSV" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Rapport d'import du fichier CSV"), + @ApiResponse(responseCode = "400", description = "Invalid request", content = {@Content(mediaType = "application/json", + schema = @Schema(implementation = ErrorResponseDTO.class))})}) + @PostMapping(path = "/referentiel/mixelecs/csv", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + RapportImportDTO importCSV(@RequestPart("file") MultipartFile file) throws IOException, ReferentielException; + + + @Operation( + summary = "Exporter les impacts des mix électrique sous format csv", + tags = {"Export Référentiels"}, + operationId = "exportImpactEquipementCSV" + ) + @GetMapping("/referentiel/mixelecs/csv") + void exportCSV(HttpServletResponse servletResponse) throws IOException; + + +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/mixelectrique/ReferentielMixElecRestApiImpl.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/mixelectrique/ReferentielMixElecRestApiImpl.java new file mode 100644 index 00000000..770a4e60 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/mixelectrique/ReferentielMixElecRestApiImpl.java @@ -0,0 +1,75 @@ +package org.mte.numecoeval.referentiel.infrastructure.restapi.controller.mixelectrique; + +import jakarta.servlet.http.HttpServletResponse; +import lombok.AllArgsConstructor; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.mte.numecoeval.referentiel.domain.exception.ReferentielException; +import org.mte.numecoeval.referentiel.domain.ports.input.impl.ImportMixElectriquePortImpl; +import org.mte.numecoeval.referentiel.infrastructure.adapter.export.MixElectriqueCsvExportService; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.MixElectriqueEntity; +import org.mte.numecoeval.referentiel.infrastructure.restapi.controller.BaseExportReferentiel; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.MixElectriqueDTO; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.RapportImportDTO; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.id.MixElectriqueIdDTO; +import org.mte.numecoeval.referentiel.infrastructure.restapi.facade.MixElectriqueFacade; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.server.ResponseStatusException; + +import java.io.IOException; +import java.util.List; + +@RestController +@Slf4j +@AllArgsConstructor +public class ReferentielMixElecRestApiImpl implements BaseExportReferentiel<MixElectriqueEntity>, ReferentielMixElectriqueRestApi, ReferentielAdministrationMixElectriqueRestApi { + private MixElectriqueFacade referentielFacade; + + private MixElectriqueCsvExportService csvExportService; + + @Override + public RapportImportDTO importCSV(MultipartFile fichier) throws IOException, ReferentielException { + if (fichier == null || fichier.isEmpty()) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Le fichier n'existe pas ou alors il est vide"); + } + var rapportImport = new ImportMixElectriquePortImpl().importCSV(fichier.getInputStream()); + referentielFacade.purgeAndAddAll(rapportImport.getObjects()); + + return new RapportImportDTO( + fichier.getOriginalFilename(), + rapportImport.getErreurs(), + rapportImport.getNbrLignesImportees() + ); + } + + @SneakyThrows + @Override + public List<MixElectriqueDTO> get(String pays) { + + if ("_all".equals(pays)) { + return referentielFacade.getAll(); + } else { + return referentielFacade.getByPays(pays); + } + + } + + @SneakyThrows + @Override + public MixElectriqueDTO get(String pays, String critere) { + MixElectriqueIdDTO id = MixElectriqueIdDTO.builder() + .pays(pays) + .critere(critere) + .build(); + + return referentielFacade.get(id); + } + + @Override + public void exportCSV(HttpServletResponse servletResponse) throws IOException { + exportCSV(servletResponse, csvExportService, "mixElec"); + } + +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/mixelectrique/ReferentielMixElectriqueRestApi.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/mixelectrique/ReferentielMixElectriqueRestApi.java new file mode 100644 index 00000000..d29e7e28 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/mixelectrique/ReferentielMixElectriqueRestApi.java @@ -0,0 +1,70 @@ +package org.mte.numecoeval.referentiel.infrastructure.restapi.controller.mixelectrique; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Content; +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 org.mte.numecoeval.referentiel.infrastructure.restapi.dto.ErrorResponseDTO; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.MixElectriqueDTO; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestParam; + +import java.util.List; + +public interface ReferentielMixElectriqueRestApi { + + @Operation(summary = "Endpoint interne à NumEcoEval - Récupération d'un Mix électrique", + description = """ + Endpoint interne utilisé dans la génération des indicateurs par le module api-calcul de NumEcoEval. + Récupération d'un mix électrique en fonction de paramètres: + <ul> + <li>Le pays de l'équipement: pays</li> + <li>Le critère d'impact: critere</li> + </ul> + . + """, + tags = "Interne NumEcoEval", + operationId = "getMixElectrique") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "mix Electrique trouvé", + content = {@Content(mediaType = "application/json", schema = @Schema(implementation = MixElectriqueDTO.class))}), + @ApiResponse(responseCode = "400", description = "Invalid request", content = {@Content(mediaType = "application/json", + schema = @Schema(implementation = ErrorResponseDTO.class))}), + @ApiResponse(responseCode = "404", description = "mix Electrique non trouvé", content = {@Content(mediaType = "application/json", + schema = @Schema(implementation = ErrorResponseDTO.class))})}) + @GetMapping(path = "/referentiel/mixelecs", produces = MediaType.APPLICATION_JSON_VALUE) + MixElectriqueDTO get( + @RequestParam + @Schema(description = "Pays recherché") final String pays, + @RequestParam + @Schema(description = "Nom du critère d'impact écologique") final String critere + ); + + @Operation(summary = "Endpoint interne à NumEcoEval - Récupération de Mix électriques par pays", + description = """ + Endpoint interne utilisé dans la génération des indicateurs par le module api-calcul de NumEcoEval. + Récupération de mix électriques en fonction de paramètres: + <ul> + <li>Le pays de l'équipement: pays</li> + </ul> + Cas spécifique avec pays = _all pour retourner tous les Mix electriques. + """, + tags = "Interne NumEcoEval", + operationId = "getMixElectriqueParPays") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "mix Electrique trouvé", + content = {@Content(mediaType = "application/json", array = @ArraySchema(schema = @Schema(implementation = MixElectriqueDTO.class)))}), + @ApiResponse(responseCode = "400", description = "Invalid request", content = {@Content(mediaType = "application/json", + schema = @Schema(implementation = ErrorResponseDTO.class))}), + @ApiResponse(responseCode = "404", description = "mix Electrique non trouvé", content = {@Content(mediaType = "application/json", + schema = @Schema(implementation = ErrorResponseDTO.class))})}) + @GetMapping(path = "/referentiel/mixelecs/{pays}", produces = MediaType.APPLICATION_JSON_VALUE) + List<MixElectriqueDTO> get( + @PathVariable + @Schema(description = "Pays recherché") final String pays + ); +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/typeequipement/ReferentielAdministrationTypeEquipementRestApi.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/typeequipement/ReferentielAdministrationTypeEquipementRestApi.java new file mode 100644 index 00000000..b0daa761 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/typeequipement/ReferentielAdministrationTypeEquipementRestApi.java @@ -0,0 +1,43 @@ +package org.mte.numecoeval.referentiel.infrastructure.restapi.controller.typeequipement; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +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 jakarta.servlet.http.HttpServletResponse; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.ErrorResponseDTO; +import org.mte.numecoeval.referentiel.domain.exception.ReferentielException; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.RapportImportDTO; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; + +public interface ReferentielAdministrationTypeEquipementRestApi { + + @Operation( + summary = "Alimentation du référentiel Type Equipement par csv : annule et remplace.", + tags = {"Import Référentiels"}, + operationId = "importTypeEquipementCSV" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Rapport d'import du fichier CSV"), + @ApiResponse(responseCode = "400", description = "Invalid request", + content = {@Content(mediaType = "application/json", + schema = @Schema(implementation = ErrorResponseDTO.class))})}) + @PostMapping(path = "/referentiel/typeEquipement/csv", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + RapportImportDTO importCSV(@RequestPart("file") MultipartFile file) throws IOException, ReferentielException; + + + @Operation( + summary = "Exporter les impacts des types d'équipements sous format csv", + tags = {"Export Référentiels"}, + operationId = "exportTypeEquipementCSV" + ) + @GetMapping("/referentiel/typeEquipement/csv") + void exportCSV(HttpServletResponse servletResponse) throws IOException; +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/typeequipement/ReferentielTypeEquipementRestApi.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/typeequipement/ReferentielTypeEquipementRestApi.java new file mode 100644 index 00000000..0bbc6c19 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/typeequipement/ReferentielTypeEquipementRestApi.java @@ -0,0 +1,63 @@ +package org.mte.numecoeval.referentiel.infrastructure.restapi.controller.typeequipement; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Content; +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 org.mte.numecoeval.referentiel.infrastructure.restapi.dto.ErrorResponseDTO; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.TypeEquipementDTO; +import org.mte.numecoeval.referentiel.domain.exception.ReferentielException; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; + +import java.util.List; + +public interface ReferentielTypeEquipementRestApi { + + @Operation( + summary = "Endpoint interne à NumEcoEval - Récupération de tous les types d'équipement", + description = """ + Endpoint interne utilisé à la réception de données d'entrées par le module api-expositiondonneesentrees de NumEcoEval. + Renvoie l'intégralité des types d'équipements utilisables par NumEcoEval. + + Les types d'équipement servent notamment à alimenter la durée de vie par défaut des équipements + reçues. + """, + tags = "Interne NumEcoEval", + operationId = "getAllTypeEquipement" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Types Equipement", + content = {@Content(mediaType = "application/json", + array = @ArraySchema(schema = @Schema(implementation = TypeEquipementDTO.class)))}), + @ApiResponse(responseCode = "400", description = "Invalid request", + content = {@Content(mediaType = "application/json", + schema = @Schema(implementation = ErrorResponseDTO.class))}), + @ApiResponse(responseCode = "404", description = "Types Equipement non trouvé", content = {@Content(mediaType = "application/json", + schema = @Schema(implementation = ErrorResponseDTO.class))})}) + @GetMapping(path = "/referentiel/typesEquipement", produces = MediaType.APPLICATION_JSON_VALUE) + List<TypeEquipementDTO> getTypesEquipement(); + + @Operation( + summary = "Endpoint interne à NumEcoEval - Récupération d'un type d'équipement via son type", + description = """ + V2 - Endpoint interne utilisé à l'enrichissement données pour un équipement physique. + """, + tags = "Interne NumEcoEval", + operationId = "getTypeEquipement" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Types Equipement", + content = {@Content(mediaType = "application/json", + schema = @Schema(implementation = TypeEquipementDTO.class))}), + @ApiResponse(responseCode = "400", description = "Invalid request", + content = {@Content(mediaType = "application/json", + schema = @Schema(implementation = ErrorResponseDTO.class))}), + @ApiResponse(responseCode = "404", description = "Types Equipement non trouvé", content = {@Content(mediaType = "application/json", + schema = @Schema(implementation = ErrorResponseDTO.class))})}) + @GetMapping(path = "/referentiel/typesEquipement/{type}", produces = MediaType.APPLICATION_JSON_VALUE) + TypeEquipementDTO getTypeEquipement(@PathVariable @Schema(description = "type recherché") String type) throws ReferentielException; +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/typeequipement/ReferentielTypeEquipementRestApiImpl.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/typeequipement/ReferentielTypeEquipementRestApiImpl.java new file mode 100644 index 00000000..13b16bec --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/typeequipement/ReferentielTypeEquipementRestApiImpl.java @@ -0,0 +1,60 @@ +package org.mte.numecoeval.referentiel.infrastructure.restapi.controller.typeequipement; + +import jakarta.servlet.http.HttpServletResponse; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.TypeEquipementDTO; +import org.mte.numecoeval.referentiel.domain.exception.ReferentielException; +import org.mte.numecoeval.referentiel.domain.ports.input.impl.ImportTypeEquipementPortImpl; +import org.mte.numecoeval.referentiel.infrastructure.adapter.export.TypeEquipementCsvExportService; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.TypeEquipementEntity; +import org.mte.numecoeval.referentiel.infrastructure.restapi.controller.BaseExportReferentiel; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.RapportImportDTO; +import org.mte.numecoeval.referentiel.infrastructure.restapi.facade.TypeEquipementFacade; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.server.ResponseStatusException; + +import java.io.IOException; +import java.util.List; + +@RestController +@Slf4j +@AllArgsConstructor +public class ReferentielTypeEquipementRestApiImpl implements BaseExportReferentiel<TypeEquipementEntity>, ReferentielTypeEquipementRestApi, ReferentielAdministrationTypeEquipementRestApi { + + private TypeEquipementFacade typeEquipementFacade; + + private TypeEquipementCsvExportService csvExportService; + + @Override + public RapportImportDTO importCSV(MultipartFile fichier) throws IOException, ReferentielException { + if (fichier == null || fichier.isEmpty()) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Le fichier n'existe pas ou alors il est vide"); + } + var rapportImport = new ImportTypeEquipementPortImpl().importCSV(fichier.getInputStream()); + typeEquipementFacade.purgeAndAddAll(rapportImport.getObjects()); + return new RapportImportDTO( + fichier.getOriginalFilename(), + rapportImport.getErreurs(), + rapportImport.getNbrLignesImportees() + ); + } + + @Override + public TypeEquipementDTO getTypeEquipement(String type) throws ReferentielException { + return typeEquipementFacade.getTypeEquipementForType(type); + } + + @Override + public List<TypeEquipementDTO> getTypesEquipement() { + return typeEquipementFacade.getAllTypesEquipement(); + } + + @Override + public void exportCSV(HttpServletResponse servletResponse) throws IOException { + exportCSV(servletResponse, csvExportService, "typeEquipement"); + } + +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/version/VersionRestApi.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/version/VersionRestApi.java new file mode 100644 index 00000000..68f3d076 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/version/VersionRestApi.java @@ -0,0 +1,30 @@ +package org.mte.numecoeval.referentiel.infrastructure.restapi.controller.version; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +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 org.mte.numecoeval.referentiel.infrastructure.restapi.dto.VersionDTO; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; + +public interface VersionRestApi { + + @Operation( + summary = "Endpoint interne à NumEcoEval - Récupération de la version courante", + description = """ + Endpoint interne utilisé pour connaître la version installée + """, + tags = "Interne NumEcoEval", + operationId = "getVersion" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Version", + content = {@Content(mediaType = "application/json", + schema = @Schema(implementation = VersionDTO.class))}) + }) + @GetMapping(path = "/version", produces = MediaType.APPLICATION_JSON_VALUE) + VersionDTO getVersion(); + +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/version/VersionRestApiImpl.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/version/VersionRestApiImpl.java new file mode 100644 index 00000000..5d153946 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/version/VersionRestApiImpl.java @@ -0,0 +1,18 @@ +package org.mte.numecoeval.referentiel.infrastructure.restapi.controller.version; + +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.VersionDTO; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class VersionRestApiImpl implements VersionRestApi { + + @Value("${version}") + private String version; + + @Override + public VersionDTO getVersion() { + return new VersionDTO(version); + } + +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/dto/CorrespondanceRefEquipementDTO.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/dto/CorrespondanceRefEquipementDTO.java new file mode 100644 index 00000000..3101e193 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/dto/CorrespondanceRefEquipementDTO.java @@ -0,0 +1,30 @@ +package org.mte.numecoeval.referentiel.infrastructure.restapi.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import lombok.experimental.Accessors; +import lombok.experimental.FieldDefaults; + +import java.io.Serializable; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +@Accessors(chain = true) +@FieldDefaults(level = AccessLevel.PRIVATE) +@EqualsAndHashCode +@Builder +@Schema( + description = "Référentiel de correspondance entre un modèle d'équipement physique et une référence d'équipement dans les référentiels." +) +public class CorrespondanceRefEquipementDTO implements Serializable { + @Schema( + description = "Modèle de l'équipement, clé du référentiel" + ) + String modeleEquipementSource; + @Schema( + description = "Référence d'équipement correspondant au modèle de l'équipement" + ) + String refEquipementCible; +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/dto/CritereDTO.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/dto/CritereDTO.java new file mode 100644 index 00000000..e955fdd2 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/dto/CritereDTO.java @@ -0,0 +1,33 @@ +package org.mte.numecoeval.referentiel.infrastructure.restapi.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import lombok.experimental.Accessors; +import lombok.experimental.FieldDefaults; + +import java.io.Serializable; + +@Getter +@Setter +@AllArgsConstructor +@Accessors(chain = true) +@FieldDefaults(level = AccessLevel.PRIVATE) +@EqualsAndHashCode +@Builder +@Schema( + description = "Référentiel de critère d'impact écologique" +) +public class CritereDTO implements Serializable { + @Schema( + description = "Nom du critère d'impact écologique, clé du référentiel" + ) + String nomCritere; + @Schema( + description = "Unité du critère d'impact écologique" + ) + String unite; + @Schema( + description = "Description du critère d'impact écologique" + ) + String description; +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/dto/ErrorResponseDTO.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/dto/ErrorResponseDTO.java new file mode 100644 index 00000000..a7ab3877 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/dto/ErrorResponseDTO.java @@ -0,0 +1,42 @@ +package org.mte.numecoeval.referentiel.infrastructure.restapi.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import lombok.experimental.FieldDefaults; +import org.springframework.http.HttpStatus; + +import java.io.Serializable; +import java.time.LocalDateTime; + + +@Setter +@Getter +@AllArgsConstructor +@NoArgsConstructor +@Builder +@FieldDefaults(level = AccessLevel.PRIVATE) +@EqualsAndHashCode +@Schema( + description = "Objet standard pour les réponses en cas d'erreur d'API" +) +public class ErrorResponseDTO implements Serializable { + @Schema( + description = "Code de l'erreur" + ) + int code; + @Schema( + description = "Message de l'erreur" + ) + String message; + @Schema( + description = "Statut HTTP de la réponse" + ) + HttpStatus status; + @Schema( + description = "Date & Heure de l'erreur" + ) + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "dd-MM-yyyy HH:mm:ss") + LocalDateTime timestamp; + +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/dto/EtapeDTO.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/dto/EtapeDTO.java new file mode 100644 index 00000000..bf027f17 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/dto/EtapeDTO.java @@ -0,0 +1,30 @@ +package org.mte.numecoeval.referentiel.infrastructure.restapi.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import lombok.experimental.Accessors; +import lombok.experimental.FieldDefaults; + +import java.io.Serializable; + +@Getter +@Setter +@AllArgsConstructor +@EqualsAndHashCode +@Accessors(chain = true) +@FieldDefaults(level = AccessLevel.PRIVATE) +@Builder +@Schema( + description = "Référentiel d'étape dans le cycle de vie d'un équipement (Etape ACV)" +) +public class EtapeDTO implements Serializable { + @Schema( + description = "Code de l'étape. Ne contient que des majuscules, clé du référentiel", + pattern = "[A-Z]+" + ) + String code; + @Schema( + description = "Libellé de l'étape" + ) + String libelle; +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/dto/HypotheseDTO.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/dto/HypotheseDTO.java new file mode 100644 index 00000000..cfb8931a --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/dto/HypotheseDTO.java @@ -0,0 +1,33 @@ +package org.mte.numecoeval.referentiel.infrastructure.restapi.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import lombok.experimental.Accessors; +import lombok.experimental.FieldDefaults; + +import java.io.Serializable; + +@Getter +@Setter +@AllArgsConstructor +@Accessors(chain = true) +@FieldDefaults(level = AccessLevel.PRIVATE) +@EqualsAndHashCode +@Builder +@Schema( + description = "Référentiel des hypothèses utilisées pour les calculs" +) +public class HypotheseDTO implements Serializable { + @Schema( + description = "Code de l'hypothèse, clé du référentiel" + ) + String code; + @Schema( + description = "Valeur de l'hypothèse" + ) + String valeur; + @Schema( + description = "Source de l'hypothèse" + ) + String source; +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/dto/ImpactEquipementDTO.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/dto/ImpactEquipementDTO.java new file mode 100644 index 00000000..18c0d471 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/dto/ImpactEquipementDTO.java @@ -0,0 +1,52 @@ +package org.mte.numecoeval.referentiel.infrastructure.restapi.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import lombok.experimental.FieldDefaults; + +import java.io.Serializable; + +@Setter +@Getter +@AllArgsConstructor +@NoArgsConstructor +@Builder +@FieldDefaults(level = AccessLevel.PRIVATE) +@EqualsAndHashCode +@Schema( + description = "Référentiel de l'impact écologique d'un équipement physique dans les référentiels. La clé est composé des champs refEquipement, etape et critere." +) +public class ImpactEquipementDTO implements Serializable { + @Schema( + description = "Référence de l'équipement physique, fait partie de la clé dans le référentiel" + ) + String refEquipement; + @Schema( + description = "Étape ACV concernée, fait partie de la clé dans le référentiel" + ) + String etape; + @Schema( + description = "Critère d'impact écologique concerné, fait partie de la clé dans le référentiel" + ) + String critere; + @Schema( + description = "Source de l'impact écologique pour cette équipement physique" + ) + String source; + @Schema( + description = "Type de l'équipement physique concerné" + ) + String type; + @Schema( + description = "Valeur de l'impact écologique" + ) + Double valeur; + @Schema( + description = "Consommation électrique moyenne" + ) + Double consoElecMoyenne; + @Schema( + description = "Description de l'entrée dans le référentiel" + ) + String description; +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/dto/ImpactMessagerieDTO.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/dto/ImpactMessagerieDTO.java new file mode 100644 index 00000000..2964d2a7 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/dto/ImpactMessagerieDTO.java @@ -0,0 +1,38 @@ +package org.mte.numecoeval.referentiel.infrastructure.restapi.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import lombok.experimental.Accessors; +import lombok.experimental.FieldDefaults; + +import java.io.Serializable; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +@Accessors(chain = true) +@FieldDefaults(level = AccessLevel.PRIVATE) +@EqualsAndHashCode +@Builder +@Schema( + description = "Référentiel de l'impact écologique d'une messagerie. La clé est le champ critere. Chaque entrée représente les composants d'une fonction affine (Ax+b)." +) +public class ImpactMessagerieDTO implements Serializable { + @Schema( + description = "Coefficient directeur de la fonction affine" + ) + Double constanteCoefficientDirecteur; + @Schema( + description = "Constante de la fonction affine" + ) + Double constanteOrdonneeOrigine; + @Schema( + description = "Critère de l'impact écologique d'une messagerie, clé du référentiel" + ) + String critere; + @Schema( + description = "Source de l'impact écologique d'une messagerie" + ) + String source; +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/dto/ImpactReseauDTO.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/dto/ImpactReseauDTO.java new file mode 100644 index 00000000..a6ea17bb --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/dto/ImpactReseauDTO.java @@ -0,0 +1,58 @@ +package org.mte.numecoeval.referentiel.infrastructure.restapi.dto; + + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import lombok.experimental.FieldDefaults; + +import java.io.Serializable; + +@Setter +@Getter +@AllArgsConstructor +@NoArgsConstructor +@Builder +@FieldDefaults(level = AccessLevel.PRIVATE) +@EqualsAndHashCode +@Schema( + description = "Référentiel de l'impact écologique d'un équipement physique vis à vis de l'usage du réseau dans les référentiels. La clé est composé des champs refReseau, etapeACV et critere." +) +public class ImpactReseauDTO implements Serializable { + @Schema( + description = "Référence de l'usage du réseau, fait partie de la clé du référentiel" + ) + @JsonProperty("refReseau") + String refReseau; + @Schema( + description = "Étape ACV concerné pour l'usage du réseau, fait partie de la clé du référentiel" + ) + @JsonProperty("etapeACV") + String etapeACV; + @Schema( + description = "Critère d'impact écologique concerné pour l'usage du réseau, fait partie de la clé du référentiel" + ) + @JsonProperty("critere") + String critere; + @Schema( + description = "Unité de l'impact écologique concerné pour l'usage du réseau. Champ Déprécié", + deprecated = true + ) + @JsonProperty("unite") + String unite; + @Schema( + description = "Source de l'impact écologique" + ) + @JsonProperty("source") + String source; + @Schema( + description = "Valeur de l'impact écologique" + ) + @JsonProperty("valeur") + Double valeur; + @Schema( + description = "Consommation électrique moyenne de l'impact écologique" + ) + @JsonProperty("consoElecMoyenne") + Double consoElecMoyenne; +} \ No newline at end of file diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/dto/MixElectriqueDTO.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/dto/MixElectriqueDTO.java new file mode 100644 index 00000000..0e12ab01 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/dto/MixElectriqueDTO.java @@ -0,0 +1,41 @@ +package org.mte.numecoeval.referentiel.infrastructure.restapi.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import lombok.experimental.FieldDefaults; + +import java.io.Serializable; + +@Setter +@Getter +@AllArgsConstructor +@NoArgsConstructor +@Builder +@FieldDefaults(level = AccessLevel.PRIVATE) +@EqualsAndHashCode +@Schema( + description = "Référentiel des mix électrique couvrant l'usage de l'électricité vis à vis du pays d'utilisation de l'équipement. La clé du référentiel est composé des champs pays et critere." +) +public class MixElectriqueDTO implements Serializable { + + @Schema( + description = "Pays concerné, fait partie de la clé du référentiel" + ) + String pays; + @Schema( + description = "Code du pays concerné en anglais" + ) + String raccourcisAnglais; + @Schema( + description = "Critère d'impact écologique concerné, fait partie de la clé du référentiel" + ) + String critere; + @Schema( + description = "Valeur du référentiel" + ) + Double valeur; + @Schema( + description = "Source de la valeur du référentiel" + ) + String source; +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/dto/RapportImportDTO.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/dto/RapportImportDTO.java new file mode 100644 index 00000000..129a03b2 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/dto/RapportImportDTO.java @@ -0,0 +1,18 @@ +package org.mte.numecoeval.referentiel.infrastructure.restapi.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.ArrayList; +import java.util.List; + +@Getter +@AllArgsConstructor +public class RapportImportDTO { + + String fichier; + + List<String> erreurs = new ArrayList<>(); + + long nbrLignesImportees = 0; +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/dto/TypeEquipementDTO.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/dto/TypeEquipementDTO.java new file mode 100644 index 00000000..ac9e33db --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/dto/TypeEquipementDTO.java @@ -0,0 +1,46 @@ +package org.mte.numecoeval.referentiel.infrastructure.restapi.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import lombok.experimental.Accessors; +import lombok.experimental.FieldDefaults; + +import java.io.Serializable; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +@Accessors(chain = true) +@FieldDefaults(level = AccessLevel.PRIVATE) +@EqualsAndHashCode +@Builder +@Schema( + description = "Référentiel des types d'équipements physiques utilisables dans le système. La clé du référentiel est le champ type." +) +public class TypeEquipementDTO implements Serializable { + @Schema( + description = "Type de l'équipment physique, clé du référentiel" + ) + String type; + @Schema( + description = "Flag indiquant si l'équipement physique est un serveur" + ) + boolean serveur; + @Schema( + description = "Commentaire de l'entrée dans le référentiel" + ) + String commentaire; + @Schema( + description = "Durée de vie par défaut de ce type d'équipement physique" + ) + Double dureeVieDefaut; + @Schema( + description = "Source de l'information du référentiel" + ) + String source; + @Schema( + description = "Référence de l'équipement par défaut, permet des correspondances en cas d'absence de correspondance direct" + ) + String refEquipementParDefaut; +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/dto/VersionDTO.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/dto/VersionDTO.java new file mode 100644 index 00000000..69168dbf --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/dto/VersionDTO.java @@ -0,0 +1,27 @@ +package org.mte.numecoeval.referentiel.infrastructure.restapi.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import lombok.experimental.Accessors; +import lombok.experimental.FieldDefaults; + +import java.io.Serializable; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +@Accessors(chain = true) +@FieldDefaults(level = AccessLevel.PRIVATE) +@EqualsAndHashCode +@Builder +@Schema( + description = "Version applicative" +) +public class VersionDTO implements Serializable { + @Schema( + description = "La version" + ) + String version; + +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/dto/id/CritereIdDTO.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/dto/id/CritereIdDTO.java new file mode 100644 index 00000000..8e9e53d1 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/dto/id/CritereIdDTO.java @@ -0,0 +1,26 @@ +package org.mte.numecoeval.referentiel.infrastructure.restapi.dto.id; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.FieldDefaults; + +import java.io.Serializable; + +@Setter +@Getter +@AllArgsConstructor +@NoArgsConstructor +@Builder +@FieldDefaults(level = AccessLevel.PRIVATE) +@EqualsAndHashCode +public class CritereIdDTO implements Serializable { + + String nomCritere; + + +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/dto/id/EtapeIdDTO.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/dto/id/EtapeIdDTO.java new file mode 100644 index 00000000..ef396485 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/dto/id/EtapeIdDTO.java @@ -0,0 +1,25 @@ +package org.mte.numecoeval.referentiel.infrastructure.restapi.dto.id; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.FieldDefaults; + +import java.io.Serializable; + +@Setter +@Getter +@AllArgsConstructor +@NoArgsConstructor +@Builder +@FieldDefaults(level = AccessLevel.PRIVATE) +@EqualsAndHashCode +public class EtapeIdDTO implements Serializable { + + String code; + +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/dto/id/HypotheseIdDTO.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/dto/id/HypotheseIdDTO.java new file mode 100644 index 00000000..ec20ee6b --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/dto/id/HypotheseIdDTO.java @@ -0,0 +1,21 @@ +package org.mte.numecoeval.referentiel.infrastructure.restapi.dto.id; + +import lombok.AccessLevel; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.Accessors; +import lombok.experimental.FieldDefaults; + +import java.io.Serializable; + +@Getter +@Setter +@EqualsAndHashCode +@Accessors(chain = true) +@FieldDefaults(level = AccessLevel.PRIVATE) +@NoArgsConstructor +public class HypotheseIdDTO implements Serializable { + String code; +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/dto/id/ImpactEquipementIdDTO.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/dto/id/ImpactEquipementIdDTO.java new file mode 100644 index 00000000..7800d19e --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/dto/id/ImpactEquipementIdDTO.java @@ -0,0 +1,26 @@ +package org.mte.numecoeval.referentiel.infrastructure.restapi.dto.id; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.FieldDefaults; + +import java.io.Serializable; + +@Setter +@Getter +@AllArgsConstructor +@NoArgsConstructor +@Builder +@FieldDefaults(level = AccessLevel.PRIVATE) +@EqualsAndHashCode +public class ImpactEquipementIdDTO implements Serializable { + String refEquipement; + String etape; + String critere; + +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/dto/id/ImpactReseauIdDTO.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/dto/id/ImpactReseauIdDTO.java new file mode 100644 index 00000000..0fc29e59 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/dto/id/ImpactReseauIdDTO.java @@ -0,0 +1,29 @@ +package org.mte.numecoeval.referentiel.infrastructure.restapi.dto.id; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.FieldDefaults; + +import java.io.Serializable; + +@Setter +@Getter +@AllArgsConstructor +@NoArgsConstructor +@Builder +@FieldDefaults(level = AccessLevel.PRIVATE) +@EqualsAndHashCode +public class ImpactReseauIdDTO implements Serializable { + @JsonProperty("refReseau") + String refReseau; + @JsonProperty("etapeACV") + String etapeACV; + @JsonProperty("critere") + String critere; +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/dto/id/MixElectriqueIdDTO.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/dto/id/MixElectriqueIdDTO.java new file mode 100644 index 00000000..de5aad79 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/dto/id/MixElectriqueIdDTO.java @@ -0,0 +1,25 @@ +package org.mte.numecoeval.referentiel.infrastructure.restapi.dto.id; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.FieldDefaults; + +import java.io.Serializable; + +@Setter +@Getter +@AllArgsConstructor +@NoArgsConstructor +@Builder +@FieldDefaults(level = AccessLevel.PRIVATE) +@EqualsAndHashCode +public class MixElectriqueIdDTO implements Serializable { + String pays; + String critere; + +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/facade/CorrespondanceRefEquipementFacade.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/facade/CorrespondanceRefEquipementFacade.java new file mode 100644 index 00000000..007e5271 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/facade/CorrespondanceRefEquipementFacade.java @@ -0,0 +1,42 @@ +package org.mte.numecoeval.referentiel.infrastructure.restapi.facade; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.mte.numecoeval.referentiel.domain.exception.ReferentielException; +import org.mte.numecoeval.referentiel.domain.model.CorrespondanceRefEquipement; +import org.mte.numecoeval.referentiel.domain.ports.output.ReferentielPersistencePort; +import org.mte.numecoeval.referentiel.infrastructure.mapper.CorrespondanceRefEquipementMapper; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.CorrespondanceRefEquipementDTO; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@Slf4j +@AllArgsConstructor +public class CorrespondanceRefEquipementFacade { + private ReferentielPersistencePort<CorrespondanceRefEquipement, String> persistencePort; + private CorrespondanceRefEquipementMapper mapper; + + public CorrespondanceRefEquipementDTO get(String modeleEquipementSource) throws ReferentielException { + var correspondanceRefEquipement = persistencePort.get(modeleEquipementSource); + if (correspondanceRefEquipement != null) { + return mapper.toDto(correspondanceRefEquipement); + } + return null; + } + + public List<CorrespondanceRefEquipementDTO> getAll() { + List<CorrespondanceRefEquipement> domains = persistencePort.getAll(); + return CollectionUtils.emptyIfNull(domains).stream().map(domain -> mapper.toDto(domain)).toList(); + } + + public void purgeAndAddAll(List<CorrespondanceRefEquipementDTO> correspondances) throws ReferentielException { + persistencePort.purge(); + persistencePort.saveAll(CollectionUtils.emptyIfNull(correspondances).stream() + .map(dto -> mapper.toDomain(dto)) + .toList()); + } + +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/facade/CritereFacade.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/facade/CritereFacade.java new file mode 100644 index 00000000..fafbfa46 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/facade/CritereFacade.java @@ -0,0 +1,42 @@ +package org.mte.numecoeval.referentiel.infrastructure.restapi.facade; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.CritereDTO; +import org.mte.numecoeval.referentiel.domain.exception.ReferentielException; +import org.mte.numecoeval.referentiel.domain.model.Critere; +import org.mte.numecoeval.referentiel.domain.model.id.CritereId; +import org.mte.numecoeval.referentiel.domain.ports.output.ReferentielPersistencePort; +import org.mte.numecoeval.referentiel.infrastructure.mapper.CritereMapper; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@Slf4j +@AllArgsConstructor +public class CritereFacade { + + private ReferentielPersistencePort<Critere, CritereId> persistencePort; + + private CritereMapper mapper; + + /** + * creation liste etapes + * + * @param criteresDTO + */ + public void purgeAndAddAll(List<CritereDTO> criteresDTO) throws ReferentielException { + persistencePort.purge(); + persistencePort.saveAll(mapper.toDomainsFromDTO(criteresDTO)); + } + + /** + * Recuperation de la liste des hypotheses + * + * @return + */ + public List<CritereDTO> getAll() { + return mapper.toDTO(persistencePort.getAll()); + } +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/facade/EtapeFacade.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/facade/EtapeFacade.java new file mode 100644 index 00000000..e5f03d23 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/facade/EtapeFacade.java @@ -0,0 +1,41 @@ +package org.mte.numecoeval.referentiel.infrastructure.restapi.facade; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.EtapeDTO; +import org.mte.numecoeval.referentiel.domain.exception.ReferentielException; +import org.mte.numecoeval.referentiel.domain.model.Etape; +import org.mte.numecoeval.referentiel.domain.model.id.EtapeId; +import org.mte.numecoeval.referentiel.domain.ports.output.ReferentielPersistencePort; +import org.mte.numecoeval.referentiel.infrastructure.mapper.EtapeMapper; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@Slf4j +@AllArgsConstructor +public class EtapeFacade { + + private ReferentielPersistencePort<Etape, EtapeId> persistencePort; + + private EtapeMapper mapper; + + + /** + * Ajout en masse des etapes + * + * @param etapesDTO + */ + public void purgeAndAddAll(List<EtapeDTO> etapesDTO) throws ReferentielException { + persistencePort.purge(); + persistencePort.saveAll(mapper.toDomain(etapesDTO)); + } + + /** + * @return + */ + public List<EtapeDTO> getAll() { + return mapper.toDTO(persistencePort.getAll()); + } +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/facade/HypotheseFacade.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/facade/HypotheseFacade.java new file mode 100644 index 00000000..ceeb6491 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/facade/HypotheseFacade.java @@ -0,0 +1,50 @@ +package org.mte.numecoeval.referentiel.infrastructure.restapi.facade; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.HypotheseDTO; +import org.mte.numecoeval.referentiel.domain.exception.ReferentielException; +import org.mte.numecoeval.referentiel.domain.model.Hypothese; +import org.mte.numecoeval.referentiel.domain.model.id.HypotheseId; +import org.mte.numecoeval.referentiel.domain.ports.output.ReferentielPersistencePort; +import org.mte.numecoeval.referentiel.infrastructure.mapper.HypotheseMapper; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.id.HypotheseIdDTO; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@Slf4j +@AllArgsConstructor +public class HypotheseFacade { + + private ReferentielPersistencePort<Hypothese, HypotheseId> persistencePort; + + + private HypotheseMapper mapper; + + /** + * Ajout des hypotheses depuis un fichier csv + * + * @param hypotheses + */ + public void purgeAndAddAll(List<HypotheseDTO> hypotheses) throws ReferentielException { + persistencePort.purge(); + persistencePort.saveAll(mapper.toDomains(hypotheses)); + } + + /** + * Recuperation de la liste des hypotheses + * + * @return + */ + public List<HypotheseDTO> getAll() { + return mapper.toDtos(persistencePort.getAll()); + } + + + public HypotheseDTO get(HypotheseIdDTO hypotheseIdDTO) throws ReferentielException { + Hypothese hypothese = persistencePort.get(mapper.toDomain(hypotheseIdDTO)); + return mapper.toDTO(hypothese); + } +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/facade/ImpactEquipementFacade.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/facade/ImpactEquipementFacade.java new file mode 100644 index 00000000..e94e380f --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/facade/ImpactEquipementFacade.java @@ -0,0 +1,37 @@ +package org.mte.numecoeval.referentiel.infrastructure.restapi.facade; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.ImpactEquipementDTO; +import org.mte.numecoeval.referentiel.domain.exception.ReferentielException; +import org.mte.numecoeval.referentiel.domain.model.ImpactEquipement; +import org.mte.numecoeval.referentiel.domain.model.id.ImpactEquipementId; +import org.mte.numecoeval.referentiel.domain.ports.output.ReferentielPersistencePort; +import org.mte.numecoeval.referentiel.infrastructure.mapper.ImpactEquipementMapper; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.id.ImpactEquipementIdDTO; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@Slf4j +@AllArgsConstructor +public class ImpactEquipementFacade { + + private ReferentielPersistencePort<ImpactEquipement, ImpactEquipementId> persistencePort; + + + private ImpactEquipementMapper mapper; + + + public ImpactEquipementDTO get(ImpactEquipementIdDTO id) throws ReferentielException { + ImpactEquipementId equipementId = mapper.toDomainId(id); + ImpactEquipement impactEquipement = persistencePort.get(equipementId); + return mapper.toDTO(impactEquipement); + } + + public void purgeAndAddAll(List<ImpactEquipementDTO> iesDTO) throws ReferentielException { + persistencePort.purge(); + persistencePort.saveAll(mapper.toDomainsFromDTO(iesDTO)); + } +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/facade/ImpactMessagerieFacade.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/facade/ImpactMessagerieFacade.java new file mode 100644 index 00000000..8ab3067a --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/facade/ImpactMessagerieFacade.java @@ -0,0 +1,46 @@ +package org.mte.numecoeval.referentiel.infrastructure.restapi.facade; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.ListUtils; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.ImpactMessagerieDTO; +import org.mte.numecoeval.referentiel.domain.exception.ReferentielException; +import org.mte.numecoeval.referentiel.domain.exception.ReferentielRuntimeException; +import org.mte.numecoeval.referentiel.domain.model.ImpactMessagerie; +import org.mte.numecoeval.referentiel.domain.ports.output.ReferentielPersistencePort; +import org.mte.numecoeval.referentiel.infrastructure.mapper.ImpactMessagerieMapper; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@Slf4j +public class ImpactMessagerieFacade { + private ReferentielPersistencePort<ImpactMessagerie, String> persistencePort; + private ImpactMessagerieMapper mapper; + + public ImpactMessagerieFacade(ReferentielPersistencePort<ImpactMessagerie, String> persistencePort, ImpactMessagerieMapper mapper) { + this.persistencePort = persistencePort; + this.mapper = mapper; + } + + public ImpactMessagerieDTO getImpactMessagerieForCritere(String critere) { + try { + var impactMessagerie = persistencePort.get(critere); + return mapper.toDTO(impactMessagerie); + } catch (Exception e) { + log.error("Erreur lors de l'accès à l'impact Messagerie : Critère : {} : {}", critere, e.getMessage()); + throw new ReferentielRuntimeException(e.getMessage()); + } + } + + public List<ImpactMessagerieDTO> getAllImpactMessagerie() { + List<ImpactMessagerie> domains = persistencePort.getAll(); + return ListUtils.emptyIfNull(mapper.toDTOs(domains)); + } + + public void purgeAndAddAll(List<ImpactMessagerieDTO> dtos) throws ReferentielException { + persistencePort.purge(); + persistencePort.saveAll(mapper.toDomainsFromDTO(dtos)); + } + +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/facade/ImpactReseauFacade.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/facade/ImpactReseauFacade.java new file mode 100644 index 00000000..2c286783 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/facade/ImpactReseauFacade.java @@ -0,0 +1,40 @@ +package org.mte.numecoeval.referentiel.infrastructure.restapi.facade; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.ImpactReseauDTO; +import org.mte.numecoeval.referentiel.domain.exception.ReferentielException; +import org.mte.numecoeval.referentiel.domain.model.ImpactReseau; +import org.mte.numecoeval.referentiel.domain.model.id.ImpactReseauId; +import org.mte.numecoeval.referentiel.domain.ports.output.ReferentielPersistencePort; +import org.mte.numecoeval.referentiel.infrastructure.mapper.ImpactReseauMapper; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.id.ImpactReseauIdDTO; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@Slf4j +@AllArgsConstructor +public class ImpactReseauFacade { + + private ReferentielPersistencePort<ImpactReseau, ImpactReseauId> persistencePort; + + private ImpactReseauMapper mapper; + + public ImpactReseauDTO get(ImpactReseauIdDTO idImpactReseauDTO) throws ReferentielException { + ImpactReseau impactReseau = persistencePort.get(mapper.toDomainId(idImpactReseauDTO)); + return mapper.toDTO(impactReseau); + } + + public ImpactReseauDTO addOrUpdate(ImpactReseauDTO impactReseauDTO) throws ReferentielException { + ImpactReseau save = persistencePort.save(mapper.toDomain(impactReseauDTO)); + return mapper.toDTO(save); + } + + public void purgeAndAddAll(List<ImpactReseauDTO> impactesReseaux) throws ReferentielException { + persistencePort.purge(); + List<ImpactReseau> impactReseauList = mapper.toDomainsFromDTO(impactesReseaux); + persistencePort.saveAll(impactReseauList); + } +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/facade/MixElectriqueFacade.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/facade/MixElectriqueFacade.java new file mode 100644 index 00000000..921e0253 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/facade/MixElectriqueFacade.java @@ -0,0 +1,48 @@ +package org.mte.numecoeval.referentiel.infrastructure.restapi.facade; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.mte.numecoeval.referentiel.domain.exception.ReferentielException; +import org.mte.numecoeval.referentiel.domain.model.MixElectrique; +import org.mte.numecoeval.referentiel.domain.model.id.MixElectriqueId; +import org.mte.numecoeval.referentiel.domain.ports.output.ReferentielPersistencePort; +import org.mte.numecoeval.referentiel.infrastructure.mapper.MixElectriqueMapper; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.MixElectriqueDTO; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.id.MixElectriqueIdDTO; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@Slf4j +@AllArgsConstructor +public class MixElectriqueFacade { + + private ReferentielPersistencePort<MixElectrique, MixElectriqueId> persistencePort; + + private MixElectriqueMapper mapper; + + public MixElectriqueDTO get(MixElectriqueIdDTO id) throws ReferentielException { + MixElectriqueId electriqueId = mapper.toDomainId(id); + MixElectrique mixElectrique = persistencePort.get(electriqueId); + return mapper.toDTO(mixElectrique); + } + + public List<MixElectriqueDTO> getAll() { + return persistencePort.getAll().stream() + .map(mix -> mapper.toDTO(mix)) + .toList(); + } + + public List<MixElectriqueDTO> getByPays(String pays) { + return persistencePort.getAll().stream() + .filter(mix -> pays.equals(mix.getPays()) || pays.equals(mix.getRaccourcisAnglais())) + .map(mix -> mapper.toDTO(mix)) + .toList(); + } + + public void purgeAndAddAll(List<MixElectriqueDTO> mixElecs) throws ReferentielException { + persistencePort.purge(); + persistencePort.saveAll(mapper.toDomainsFromDTO(mixElecs)); + } +} diff --git a/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/facade/TypeEquipementFacade.java b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/facade/TypeEquipementFacade.java new file mode 100644 index 00000000..927baa14 --- /dev/null +++ b/services/api-referentiel/src/main/java/org/mte/numecoeval/referentiel/infrastructure/restapi/facade/TypeEquipementFacade.java @@ -0,0 +1,45 @@ +package org.mte.numecoeval.referentiel.infrastructure.restapi.facade; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.TypeEquipementDTO; +import org.mte.numecoeval.referentiel.domain.exception.ReferentielException; +import org.mte.numecoeval.referentiel.domain.model.TypeEquipement; +import org.mte.numecoeval.referentiel.domain.ports.output.ReferentielPersistencePort; +import org.mte.numecoeval.referentiel.infrastructure.mapper.TypeEquipementMapper; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@Slf4j +public class TypeEquipementFacade { + private ReferentielPersistencePort<TypeEquipement, String> persistencePort; + private TypeEquipementMapper mapper; + + public TypeEquipementFacade(ReferentielPersistencePort<TypeEquipement, String> persistencePort, TypeEquipementMapper mapper) { + this.persistencePort = persistencePort; + this.mapper = mapper; + } + + public TypeEquipementDTO getTypeEquipementForType(String type) throws ReferentielException { + var typeEquipement = persistencePort.get(type); + if (typeEquipement != null) { + return mapper.toDto(typeEquipement); + } + return null; + } + + public List<TypeEquipementDTO> getAllTypesEquipement() { + List<TypeEquipement> typeEquipements = persistencePort.getAll(); + return CollectionUtils.emptyIfNull(typeEquipements).stream().map(typeEquipement -> mapper.toDto(typeEquipement)).toList(); + } + + public void purgeAndAddAll(List<TypeEquipementDTO> types) throws ReferentielException { + persistencePort.purge(); + persistencePort.saveAll(CollectionUtils.emptyIfNull(types).stream() + .map(typeEquipementDTO -> mapper.toDomaine(typeEquipementDTO)) + .toList()); + } + +} diff --git a/services/api-referentiel/src/main/resources/application.yaml b/services/api-referentiel/src/main/resources/application.yaml new file mode 100644 index 00000000..ef3b2fa5 --- /dev/null +++ b/services/api-referentiel/src/main/resources/application.yaml @@ -0,0 +1,67 @@ +# Replaced by CI/CD +version: "1.0.0" + +# Application +numecoeval: + urls: + allowed: "http://localhost" + +#CONFIGURATION BASES +spring: + sql: + init: + mode: always + # Base de données + datasource: + generate-unique-name: true + url: "jdbc:postgresql://localhost:5432/postgres" + username: postgres + password: postgres + driver-class-name: org.postgresql.Driver + tomcat: + test-on-borrow: false + jmx-enabled: false + max-active: 100 + jpa: + # POSTGRES + generate-ddl: true + database-platform: org.hibernate.dialect.PostgreSQLDialect + hibernate: + ddl-auto: none + show-sql: false + properties: + hibernate: + order_inserts: true + jdbc: + batch_size: 1000 + generate_statistics: false + open-in-view: true + + # Taille des uploads et des requêtes + servlet: + multipart: + max-request-size: "12MB" + max-file-size: "10MB" + +# Serveur Web +server: + port: 18080 + shutdown: graceful + tomcat: + mbeanregistry: + enabled: true + +# Actuator +management: + server: + port: 18080 + security: + user: + name: "test" + password: "test" + roles: ACTUATOR_ADMIN + endpoints: + web: + base-path: / + exposure: + include: health,prometheus,httptrace,info,metrics,mappings \ No newline at end of file diff --git a/services/api-referentiel/src/main/resources/logback.xml b/services/api-referentiel/src/main/resources/logback.xml new file mode 100644 index 00000000..7f5ba818 --- /dev/null +++ b/services/api-referentiel/src/main/resources/logback.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<configuration> +<!-- Spring default.xml --> + <conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter" /> + <conversionRule conversionWord="wex" converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter" /> + <conversionRule conversionWord="wEx" converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter" /> + + <property name="CONSOLE_LOG_PATTERN" value="${CONSOLE_LOG_PATTERN:-%clr(%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/> + <property name="CONSOLE_LOG_CHARSET" value="${CONSOLE_LOG_CHARSET:-${file.encoding:-UTF-8}}"/> + +<!-- console-appender.xml--> + <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> + <encoder> + <pattern>${CONSOLE_LOG_PATTERN}</pattern> + <charset>${CONSOLE_LOG_CHARSET}</charset> + </encoder> + </appender> + + <root level="${APP_LOGGING_LEVEL:-INFO}"> + <appender-ref ref="CONSOLE" /> + </root> + <logger name="org.springframework.web" level="INFO"/> +</configuration> \ No newline at end of file diff --git a/services/api-referentiel/src/main/resources/schema.sql b/services/api-referentiel/src/main/resources/schema.sql new file mode 100644 index 00000000..e3949260 --- /dev/null +++ b/services/api-referentiel/src/main/resources/schema.sql @@ -0,0 +1,91 @@ +CREATE TABLE IF NOT EXISTS ref_etapeacv +( + code varchar(255) NOT NULL, + libelle varchar(255) NULL, + CONSTRAINT ref_etapeacv_pkey PRIMARY KEY (code) +); + +CREATE TABLE IF NOT EXISTS ref_critere +( + nom_critere varchar(255) NOT NULL, + description varchar(255) NULL, + unite varchar(255) NULL, + CONSTRAINT ref_critere_pkey PRIMARY KEY (nom_critere) +); + +CREATE TABLE IF NOT EXISTS ref_hypothese +( + code varchar(255) NOT NULL, + "source" varchar(255) NULL, + valeur varchar(255) NULL, + CONSTRAINT ref_hypothese_pkey PRIMARY KEY (code) +); + +CREATE TABLE IF NOT EXISTS ref_type_equipement +( + "type" varchar(255) NOT NULL, + commentaire varchar(255) NULL, + duree_vie_defaut float8 NULL, + serveur bool NOT NULL, + "source" varchar(255) NULL, + ref_equipement_par_defaut varchar(255) NULL, + CONSTRAINT ref_type_equipement_pkey PRIMARY KEY (type) +); + +CREATE TABLE IF NOT EXISTS ref_impact_messagerie +( + constante_coefficient_directeur float8 NULL, + constante_ordonnee_origine float8 NULL, + "source" varchar(255) NULL, + nom_critere varchar(255) NOT NULL, + CONSTRAINT ref_impact_messagerie_pkey PRIMARY KEY (nom_critere) +); + +CREATE TABLE IF NOT EXISTS ref_correspondance_ref_eqp +( + modele_equipement_source varchar(255) NOT NULL, + ref_equipement_cible varchar(255) NULL, + CONSTRAINT ref_correspondance_ref_eqp_pkey PRIMARY KEY (modele_equipement_source) +); + +CREATE TABLE IF NOT EXISTS ref_impactequipement +( + refequipement varchar(255) NOT NULL, + conso_elec_moyenne float8 NULL, + "source" varchar(255) NULL, + "type" varchar(255) NULL, + valeur float8 NULL, + etapeacv varchar(255) NOT NULL, + nomcritere varchar(255) NOT NULL, + description varchar(255) NULL, + CONSTRAINT ref_impactequipement_pkey PRIMARY KEY (nomcritere, etapeacv, refequipement) +); + +CREATE TABLE IF NOT EXISTS ref_impactreseau +( + refreseau varchar(255) NOT NULL, + consoelecmoyenne float8 NULL, + "source" varchar(255) NULL, + valeur float8 NULL, + etapeacv varchar(255) NOT NULL, + nomcritere varchar(255) NOT NULL, + CONSTRAINT ref_impactreseau_pkey PRIMARY KEY (nomcritere, etapeacv, refreseau) +); + +CREATE TABLE IF NOT EXISTS ref_mixelec +( + pays varchar(255) NOT NULL, + raccourcisanglais varchar(255) NULL, + "source" varchar(255) NULL, + valeur float8 NULL, + nomcritere varchar(255) NOT NULL, + CONSTRAINT ref_mixelec_pkey PRIMARY KEY (nomcritere, pays) +); + +-- suppression des contraintes de clés étrangères +ALTER TABLE ref_impactequipement DROP CONSTRAINT IF EXISTS fk5iuiwnk7rymtob1fku71uuj52; +ALTER TABLE ref_impactequipement DROP CONSTRAINT IF EXISTS fksfjum8kagn7q6vsv5uqn6kimx; +ALTER TABLE ref_impactreseau DROP CONSTRAINT IF EXISTS fk31ykp7xtj41win3ptqlr3us9s; +ALTER TABLE ref_impactreseau DROP CONSTRAINT IF EXISTS fkb8tkreu8c8s8pqqnft6vr4pnf; +ALTER TABLE ref_mixelec DROP CONSTRAINT IF EXISTS fkdncd4m2je6fbno7pkn850u1fs; +ALTER TABLE ref_impact_messagerie DROP CONSTRAINT IF EXISTS fkohnlpwfp0ebk7dswmfbe5l3k0; \ No newline at end of file diff --git a/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/CucumberIntegrationTest.java b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/CucumberIntegrationTest.java new file mode 100644 index 00000000..6a41ee15 --- /dev/null +++ b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/CucumberIntegrationTest.java @@ -0,0 +1,30 @@ +package org.mte.numecoeval.referentiel; + +import io.cucumber.spring.CucumberContextConfiguration; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.platform.suite.api.ConfigurationParameter; +import org.junit.platform.suite.api.IncludeEngines; +import org.junit.platform.suite.api.SelectClasspathResource; +import org.junit.platform.suite.api.Suite; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +import static io.cucumber.junit.platform.engine.Constants.GLUE_PROPERTY_NAME; +import static io.cucumber.junit.platform.engine.Constants.PLUGIN_PROPERTY_NAME; + +@CucumberContextConfiguration +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) +@ActiveProfiles(profiles = {"test"}) +@Suite +@IncludeEngines("cucumber") +@SelectClasspathResource("org/mte/numecoeval/referentiel") +@ConfigurationParameter(key = PLUGIN_PROPERTY_NAME, value = "pretty,html:target/cucumber-reports.html") +@ConfigurationParameter(key = GLUE_PROPERTY_NAME, value = "org.mte.numecoeval.referentiel") +class CucumberIntegrationTest { + + @Test + void loadCucumber() { + Assertions.assertNotNull(CucumberIntegrationTest.class); + } +} diff --git a/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/ReferentielApplicationTests.java b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/ReferentielApplicationTests.java new file mode 100644 index 00000000..5d1c0da8 --- /dev/null +++ b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/ReferentielApplicationTests.java @@ -0,0 +1,68 @@ +package org.mte.numecoeval.referentiel; + +import io.restassured.RestAssured; +import io.restassured.http.ContentType; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.ErrorResponseDTO; +import org.mte.numecoeval.referentiel.infrastructure.restapi.controller.hypothese.ReferentielHypotheseRestApi; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.core.env.Environment; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.web.bind.annotation.GetMapping; + +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.Objects; + +import static io.restassured.RestAssured.given; +import static org.junit.jupiter.api.Assertions.assertEquals; + + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) +@ActiveProfiles(profiles = {"test"}) +class ReferentielApplicationTests { + + @Autowired + Environment environment; + + @BeforeEach + public void setupRestAssured() { + RestAssured.baseURI = "http://localhost"; + RestAssured.port = Integer.parseInt(Objects.requireNonNull(environment.getProperty("server.port"))); + } + @Test + void contextLoads() { + Assertions.assertNotNull(ReferentielApplication.class); + } + + @Test + void getHypothese_whenNoData_shouldReturn404() throws NoSuchMethodException { + String requestPath = Arrays.stream(ReferentielHypotheseRestApi.class.getMethod("get", String.class) + .getAnnotation(GetMapping.class).path()).findFirst().orElse(null); + var response = given() + .contentType(ContentType.JSON) + .param("cle", "NonExistant") + .get(requestPath) + .thenReturn(); + + assertEquals(404, response.getStatusCode()); + var errorResponseDTO = response.as(ErrorResponseDTO.class); + assertEquals(404, errorResponseDTO.getCode()); + assertEquals("Hypothèse non trouvé", errorResponseDTO.getMessage()); + } + + @Test + void unknownEndpoint_shouldReturn400() throws NoSuchMethodException { + var response = given() + .contentType(ContentType.JSON) + .get("/this/is/not/an/existing/endpoint?true=yes") + .thenReturn(); + + assertEquals(404, response.getStatusCode()); + var errorResponseDTO = response.as(LinkedHashMap.class); + assertEquals("/this/is/not/an/existing/endpoint", errorResponseDTO.get("path")); + } +} diff --git a/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/domain/port/input/ImportCSVReferentielPortTest.java b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/domain/port/input/ImportCSVReferentielPortTest.java new file mode 100644 index 00000000..77fc8f59 --- /dev/null +++ b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/domain/port/input/ImportCSVReferentielPortTest.java @@ -0,0 +1,47 @@ +package org.mte.numecoeval.referentiel.domain.port.input; + +import org.apache.commons.csv.CSVRecord; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.CorrespondanceRefEquipementDTO; +import org.mte.numecoeval.referentiel.domain.exception.ReferentielException; +import org.mte.numecoeval.referentiel.domain.ports.input.ImportCSVReferentielPort; +import org.mte.numecoeval.referentiel.domain.ports.input.impl.ImportCorrespondanceRefEquipementPortImpl; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class ImportCSVReferentielPortTest { + + @Mock + CSVRecord csvRecord; + + ImportCSVReferentielPort<CorrespondanceRefEquipementDTO> importPortToTest = new ImportCorrespondanceRefEquipementPortImpl(); + + @Test + void checkFieldIsMappedInCSVRecord_whenFieldIsntMapped_shouldThrowException() { + var field = "test"; + when(csvRecord.getRecordNumber()).thenReturn(0L); + when(csvRecord.isMapped(field)).thenReturn(false); + + var exception = assertThrows(ReferentielException.class, () -> importPortToTest.checkFieldIsMappedInCSVRecord(csvRecord, field)); + assertEquals("La ligne n°1 est invalide : La colonne test doit être présente", exception.getMessage()); + } + + @Test + void getStringValueFromRecord_shouldUseAlternativeNamesIfFieldUnavailable() { + var field = "test"; + var alternativeName = "alternativeField"; + when(csvRecord.isMapped(field)).thenReturn(false); + when(csvRecord.isMapped(alternativeName)).thenReturn(true); + String expectedValue = "Value"; + when(csvRecord.get(alternativeName)).thenReturn(expectedValue); + + var result = importPortToTest.getStringValueFromRecord(csvRecord, field, alternativeName); + assertEquals(expectedValue, result); + } +} diff --git a/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/domain/port/input/ImportCorrespondanceRefEquipementPortTest.java b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/domain/port/input/ImportCorrespondanceRefEquipementPortTest.java new file mode 100644 index 00000000..c5579678 --- /dev/null +++ b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/domain/port/input/ImportCorrespondanceRefEquipementPortTest.java @@ -0,0 +1,86 @@ +package org.mte.numecoeval.referentiel.domain.port.input; + +import org.junit.jupiter.api.Test; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.CorrespondanceRefEquipementDTO; +import org.mte.numecoeval.referentiel.domain.ports.input.ImportCSVReferentielPort; +import org.mte.numecoeval.referentiel.domain.ports.input.impl.ImportCorrespondanceRefEquipementPortImpl; +import org.springframework.util.ResourceUtils; + +import java.io.DataInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.*; + +class ImportCorrespondanceRefEquipementPortTest { + + ImportCSVReferentielPort<CorrespondanceRefEquipementDTO> importPortToTest = new ImportCorrespondanceRefEquipementPortImpl(); + + @Test + void importCSV_shouldImportAllDatas() throws Exception { + File file = ResourceUtils.getFile("classpath:csv/unit/correspondanceRefEquipement.csv"); + var resultatImport = importPortToTest.importCSV(new FileInputStream(file)); + + assertEquals(3, resultatImport.getNbrLignesImportees()); + assertEquals(0, resultatImport.getErreurs().size()); + assertEquals(3, resultatImport.getObjects().size()); + assertTrue(resultatImport.getObjects().stream().anyMatch(etapeDTO -> "modele01".equals(etapeDTO.getModeleEquipementSource()) && "refCible01".equals(etapeDTO.getRefEquipementCible()))); + assertTrue(resultatImport.getObjects().stream().anyMatch(etapeDTO -> "modele02".equals(etapeDTO.getModeleEquipementSource()) && "refCible01".equals(etapeDTO.getRefEquipementCible()))); + assertTrue(resultatImport.getObjects().stream().anyMatch(etapeDTO -> "modele03".equals(etapeDTO.getModeleEquipementSource()) && "refCible02".equals(etapeDTO.getRefEquipementCible()))); + } + + @Test + void importCSV_whenErrorInMiddleOfFile_shouldImportDatasWithErrors() throws Exception { + File file = ResourceUtils.getFile("classpath:csv/unit/correspondanceRefEquipement_errorInMiddle.csv"); + var resultatImport = importPortToTest.importCSV(new FileInputStream(file)); + + assertEquals(3, resultatImport.getNbrLignesImportees()); + assertEquals(2, resultatImport.getErreurs().size()); + assertTrue(resultatImport.getErreurs().stream().anyMatch("La ligne n°4 est invalide : La colonne modeleEquipementSource ne peut être vide"::equals)); + assertTrue(resultatImport.getErreurs().stream().anyMatch("La ligne n°5 est invalide : La colonne refEquipementCible ne peut être vide"::equals)); + assertEquals(3, resultatImport.getObjects().size()); + assertTrue(resultatImport.getObjects().stream().anyMatch(etapeDTO -> "modele01".equals(etapeDTO.getModeleEquipementSource()) && "refCible01".equals(etapeDTO.getRefEquipementCible()))); + assertTrue(resultatImport.getObjects().stream().anyMatch(etapeDTO -> "modele01".equals(etapeDTO.getModeleEquipementSource()) && "refCible01".equals(etapeDTO.getRefEquipementCible()))); + assertTrue(resultatImport.getObjects().stream().anyMatch(etapeDTO -> "modele03".equals(etapeDTO.getModeleEquipementSource()) && "refCible02".equals(etapeDTO.getRefEquipementCible()))); + } + + @Test + void importCSV_whenWrongFile_shouldReturnOnlyErrors() throws Exception { + File file = ResourceUtils.getFile("classpath:csv/unit/wrongCSVFile.csv"); + var resultatImport = importPortToTest.importCSV(new FileInputStream(file)); + + assertEquals(0, resultatImport.getNbrLignesImportees()); + assertEquals(1, resultatImport.getErreurs().size()); + assertTrue(resultatImport.getErreurs().stream().anyMatch("La ligne n°2 est invalide : Entêtes incohérentes"::equals)); + } + + @Test + void importCSV_whenStreamAlreadyClosedShouldReturnReportWithOneError() throws IOException { + DataInputStream dataInputStream = mock(DataInputStream.class); + doNothing().when(dataInputStream).close(); + + var resultatImport = importPortToTest.importCSV(dataInputStream); + + verify(dataInputStream).close(); + assertEquals(0, resultatImport.getNbrLignesImportees()); + assertEquals(1, resultatImport.getErreurs().size()); + assertEquals("Le fichier CSV n'a pas pu être lu.", resultatImport.getErreurs().get(0)); + } + + @Test + void importCSV_whenFileNotFoundShouldReturnReportWithOneError() throws IOException { + DataInputStream dataInputStream = mock(DataInputStream.class); + doThrow(new FileNotFoundException("start Read csv etape")).when(dataInputStream).close(); + + var resultatImport = importPortToTest.importCSV(dataInputStream); + + verify(dataInputStream).close(); + assertEquals(0, resultatImport.getNbrLignesImportees()); + assertEquals(1, resultatImport.getErreurs().size()); + assertEquals("Le fichier CSV n'a pas pu être lu.", resultatImport.getErreurs().get(0)); + } +} diff --git a/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/domain/port/input/ImportCriterePortTest.java b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/domain/port/input/ImportCriterePortTest.java new file mode 100644 index 00000000..0c003cb2 --- /dev/null +++ b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/domain/port/input/ImportCriterePortTest.java @@ -0,0 +1,128 @@ +package org.mte.numecoeval.referentiel.domain.port.input; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.CritereDTO; +import org.mte.numecoeval.referentiel.domain.ports.input.ImportCSVReferentielPort; +import org.mte.numecoeval.referentiel.domain.ports.input.impl.ImportCriterePortImpl; +import org.springframework.util.ResourceUtils; + +import java.io.DataInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; + +import static org.mockito.Mockito.*; + +class ImportCriterePortTest { + + ImportCSVReferentielPort<CritereDTO> importPortToTest = new ImportCriterePortImpl(); + + @Test + void importCSV_shouldImportAllDatas() throws Exception { + File file = ResourceUtils.getFile("classpath:csv/unit/critere.csv"); + var resultatImport = importPortToTest.importCSV(new FileInputStream(file)); + + Assertions.assertEquals(5, resultatImport.getNbrLignesImportees()); + Assertions.assertEquals(0, resultatImport.getErreurs().size()); + Assertions.assertEquals(5, resultatImport.getObjects().size()); + Assertions.assertTrue(resultatImport.getObjects().stream().anyMatch(dto -> + "Changement climatique".equals(dto.getNomCritere()) + && "".equals(dto.getDescription()) + && "kg CO_{2} eq".equals(dto.getUnite())) + ); + Assertions.assertTrue(resultatImport.getObjects().stream().anyMatch(dto -> + "Émissions de particules fines".equals(dto.getNomCritere()) + && "Émissions de particules fines".equals(dto.getDescription()) + && "Diseaseincidence".equals(dto.getUnite())) + ); + Assertions.assertTrue(resultatImport.getObjects().stream().anyMatch(dto -> + "Radiations ionisantes".equals(dto.getNomCritere()) + && "Description de Tests".equals(dto.getDescription()) + && "kBq U-235 eq".equals(dto.getUnite())) + ); + Assertions.assertTrue(resultatImport.getObjects().stream().anyMatch(dto -> + "Acidification".equals(dto.getNomCritere()) + && "".equals(dto.getDescription()) + && "mol H^{+} eq".equals(dto.getUnite())) + ); + Assertions.assertTrue(resultatImport.getObjects().stream().anyMatch(dto -> + "Épuisement des ressources naturelles (minérales et métaux)".equals(dto.getNomCritere()) + && "".equals(dto.getDescription()) + && "kg Sb eq".equals(dto.getUnite())) + ); + } + + @Test + void importCSV_whenErrorInMiddleOfFile_shouldImportDatasWithErrors() throws Exception { + File file = ResourceUtils.getFile("classpath:csv/unit/critere_errorInMiddle.csv"); + var resultatImport = importPortToTest.importCSV(new FileInputStream(file)); + + Assertions.assertEquals(5, resultatImport.getNbrLignesImportees()); + Assertions.assertEquals(1, resultatImport.getErreurs().size()); + Assertions.assertTrue(resultatImport.getErreurs().stream().anyMatch("La ligne n°4 est invalide : La colonne nomCritere ne peut être vide"::equals)); + Assertions.assertEquals(5, resultatImport.getObjects().size()); + Assertions.assertTrue(resultatImport.getObjects().stream().anyMatch(dto -> + "Changement climatique".equals(dto.getNomCritere()) + && "".equals(dto.getDescription()) + && "kg CO_{2} eq".equals(dto.getUnite())) + ); + Assertions.assertTrue(resultatImport.getObjects().stream().anyMatch(dto -> + "Émissions de particules fines".equals(dto.getNomCritere()) + && "Émissions de particules fines".equals(dto.getDescription()) + && "Diseaseincidence".equals(dto.getUnite())) + ); + Assertions.assertTrue(resultatImport.getObjects().stream().anyMatch(dto -> + "Radiations ionisantes".equals(dto.getNomCritere()) + && "Description de Tests".equals(dto.getDescription()) + && "kBq U-235 eq".equals(dto.getUnite())) + ); + Assertions.assertTrue(resultatImport.getObjects().stream().anyMatch(dto -> + "Acidification".equals(dto.getNomCritere()) + && "".equals(dto.getDescription()) + && "mol H^{+} eq".equals(dto.getUnite())) + ); + Assertions.assertTrue(resultatImport.getObjects().stream().anyMatch(dto -> + "Épuisement des ressources naturelles (minérales et métaux)".equals(dto.getNomCritere()) + && "".equals(dto.getDescription()) + && "kg Sb eq".equals(dto.getUnite())) + ); + } + + @Test + void importCSV_whenWrongFile_shouldReturnOnlyErrors() throws Exception { + File file = ResourceUtils.getFile("classpath:csv/unit/wrongCSVFile.csv"); + var resultatImport = importPortToTest.importCSV(new FileInputStream(file)); + + Assertions.assertEquals(0, resultatImport.getNbrLignesImportees()); + Assertions.assertEquals(1, resultatImport.getErreurs().size()); + Assertions.assertTrue(resultatImport.getErreurs().stream().anyMatch("La ligne n°2 est invalide : Entêtes incohérentes"::equals)); + } + + @Test + void importCSV_whenStreamAlreadyClosedShouldReturnReportWithOneError() throws IOException { + DataInputStream dataInputStream = mock(DataInputStream.class); + doNothing().when(dataInputStream).close(); + + var resultatImport = importPortToTest.importCSV(dataInputStream); + + verify(dataInputStream).close(); + Assertions.assertEquals(0, resultatImport.getNbrLignesImportees()); + Assertions.assertEquals(1, resultatImport.getErreurs().size()); + Assertions.assertEquals("Le fichier CSV n'a pas pu être lu.", resultatImport.getErreurs().get(0)); + } + + @Test + void importCSV_whenFileNotFoundShouldReturnReportWithOneError() throws IOException { + DataInputStream dataInputStream = mock(DataInputStream.class); + doThrow(new FileNotFoundException("start Read csv etape")).when(dataInputStream).close(); + + var resultatImport = importPortToTest.importCSV(dataInputStream); + + verify(dataInputStream).close(); + Assertions.assertEquals(0, resultatImport.getNbrLignesImportees()); + Assertions.assertEquals(1, resultatImport.getErreurs().size()); + Assertions.assertEquals("Le fichier CSV n'a pas pu être lu.", resultatImport.getErreurs().get(0)); + } +} diff --git a/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/domain/port/input/ImportEtapePortTest.java b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/domain/port/input/ImportEtapePortTest.java new file mode 100644 index 00000000..a7a63cb2 --- /dev/null +++ b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/domain/port/input/ImportEtapePortTest.java @@ -0,0 +1,86 @@ +package org.mte.numecoeval.referentiel.domain.port.input; + +import org.junit.jupiter.api.Test; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.EtapeDTO; +import org.mte.numecoeval.referentiel.domain.ports.input.ImportCSVReferentielPort; +import org.mte.numecoeval.referentiel.domain.ports.input.impl.ImportEtapePortImpl; +import org.springframework.util.ResourceUtils; + +import java.io.DataInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.*; + +class ImportEtapePortTest { + + ImportCSVReferentielPort<EtapeDTO> importPortToTest = new ImportEtapePortImpl(); + + @Test + void importCSV_shouldImportAllDatas() throws Exception { + File file = ResourceUtils.getFile("classpath:csv/unit/etapeACV.csv"); + var resultatImport = importPortToTest.importCSV(new FileInputStream(file)); + + assertEquals(4, resultatImport.getNbrLignesImportees()); + assertEquals(0, resultatImport.getErreurs().size()); + assertEquals(4, resultatImport.getObjects().size()); + assertTrue(resultatImport.getObjects().stream().anyMatch(etapeDTO -> "UTILISATION".equals(etapeDTO.getCode()) && "Utilisation".equals(etapeDTO.getLibelle()))); + assertTrue(resultatImport.getObjects().stream().anyMatch(etapeDTO -> "FABRICATION".equals(etapeDTO.getCode()) && "Fabrication".equals(etapeDTO.getLibelle()))); + assertTrue(resultatImport.getObjects().stream().anyMatch(etapeDTO -> "DISTRIBUTION".equals(etapeDTO.getCode()) && "Distribution".equals(etapeDTO.getLibelle()))); + assertTrue(resultatImport.getObjects().stream().anyMatch(etapeDTO -> "FIN_DE_VIE".equals(etapeDTO.getCode()) && "Fin de vie".equals(etapeDTO.getLibelle()))); + } + + @Test + void importCSV_whenErrorInMiddleOfFile_shouldImportDatasWithErrors() throws Exception { + File file = ResourceUtils.getFile("classpath:csv/unit/etapeACV_errorInMiddle.csv"); + var resultatImport = importPortToTest.importCSV(new FileInputStream(file)); + + assertEquals(3, resultatImport.getNbrLignesImportees()); + assertEquals(1, resultatImport.getErreurs().size()); + assertTrue(resultatImport.getErreurs().stream().anyMatch("La ligne n°4 est invalide : La colonne code ne peut être vide"::equals)); + assertEquals(3, resultatImport.getObjects().size()); + assertTrue(resultatImport.getObjects().stream().anyMatch(etapeDTO -> "FABRICATION".equals(etapeDTO.getCode()) && "Fabrication".equals(etapeDTO.getLibelle()))); + assertTrue(resultatImport.getObjects().stream().anyMatch(etapeDTO -> "DISTRIBUTION".equals(etapeDTO.getCode()) && "Distribution".equals(etapeDTO.getLibelle()))); + assertTrue(resultatImport.getObjects().stream().anyMatch(etapeDTO -> "FIN_DE_VIE".equals(etapeDTO.getCode()) && "Fin de vie".equals(etapeDTO.getLibelle()))); + } + + @Test + void importCSV_whenWrongFile_shouldReturnOnlyErrors() throws Exception { + File file = ResourceUtils.getFile("classpath:csv/unit/wrongCSVFile.csv"); + var resultatImport = importPortToTest.importCSV(new FileInputStream(file)); + + assertEquals(0, resultatImport.getNbrLignesImportees()); + assertEquals(1, resultatImport.getErreurs().size()); + assertTrue(resultatImport.getErreurs().stream().anyMatch("La ligne n°2 est invalide : Entêtes incohérentes"::equals)); + } + + @Test + void importCSV_whenStreamAlreadyClosedShouldReturnReportWithOneError() throws IOException { + DataInputStream dataInputStream = mock(DataInputStream.class); + doNothing().when(dataInputStream).close(); + + var resultatImport = importPortToTest.importCSV(dataInputStream); + + verify(dataInputStream).close(); + assertEquals(0, resultatImport.getNbrLignesImportees()); + assertEquals(1, resultatImport.getErreurs().size()); + assertEquals("Le fichier CSV n'a pas pu être lu.", resultatImport.getErreurs().get(0)); + } + + @Test + void importCSV_whenFileNotFoundShouldReturnReportWithOneError() throws IOException { + DataInputStream dataInputStream = mock(DataInputStream.class); + doThrow(new FileNotFoundException("start Read csv etape")).when(dataInputStream).close(); + + var resultatImport = importPortToTest.importCSV(dataInputStream); + + verify(dataInputStream).close(); + assertEquals(0, resultatImport.getNbrLignesImportees()); + assertEquals(1, resultatImport.getErreurs().size()); + assertEquals("Le fichier CSV n'a pas pu être lu.", resultatImport.getErreurs().get(0)); + } +} diff --git a/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/domain/port/input/ImportHypothesePortTest.java b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/domain/port/input/ImportHypothesePortTest.java new file mode 100644 index 00000000..aba280bc --- /dev/null +++ b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/domain/port/input/ImportHypothesePortTest.java @@ -0,0 +1,84 @@ +package org.mte.numecoeval.referentiel.domain.port.input; + +import org.junit.jupiter.api.Test; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.HypotheseDTO; +import org.mte.numecoeval.referentiel.domain.ports.input.ImportCSVReferentielPort; +import org.mte.numecoeval.referentiel.domain.ports.input.impl.ImportHypothesePortImpl; +import org.springframework.util.ResourceUtils; + +import java.io.DataInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.*; + +class ImportHypothesePortTest { + + ImportCSVReferentielPort<HypotheseDTO> importPortToTest = new ImportHypothesePortImpl(); + + @Test + void importCSV_shouldImportAllDatas() throws Exception { + File file = ResourceUtils.getFile("classpath:csv/unit/hypothese.csv"); + var resultatImport = importPortToTest.importCSV(new FileInputStream(file)); + + assertEquals(2, resultatImport.getNbrLignesImportees()); + assertEquals(0, resultatImport.getErreurs().size()); + assertEquals(2, resultatImport.getObjects().size()); + assertTrue(resultatImport.getObjects().stream().anyMatch(dto -> "PUEPardDfault".equals(dto.getCode()) && "1.6".equals(dto.getValeur()) && "expertise IJO".equals(dto.getSource()))); + assertTrue(resultatImport.getObjects().stream().anyMatch(dto -> "DureeVieServeurParDefaut".equals(dto.getCode()) && "3".equals(dto.getValeur()) && "US 46".equals(dto.getSource()))); + } + + @Test + void importCSV_whenErrorInMiddleOfFile_shouldImportDatasWithErrors() throws Exception { + File file = ResourceUtils.getFile("classpath:csv/unit/hypothese_errorInMiddle.csv"); + var resultatImport = importPortToTest.importCSV(new FileInputStream(file)); + + assertEquals(2, resultatImport.getNbrLignesImportees()); + assertEquals(2, resultatImport.getErreurs().size()); + assertTrue(resultatImport.getErreurs().stream().anyMatch("La ligne n°3 est invalide : La colonne cle ne peut être vide"::equals)); + assertTrue(resultatImport.getErreurs().stream().anyMatch("La ligne n°4 est invalide : La colonne valeur ne peut être vide"::equals)); + assertEquals(2, resultatImport.getObjects().size()); + assertTrue(resultatImport.getObjects().stream().anyMatch(dto -> "PUEPardDfault".equals(dto.getCode()) && "1.6".equals(dto.getValeur()) && "expertise IJO".equals(dto.getSource()))); + assertTrue(resultatImport.getObjects().stream().anyMatch(dto -> "DureeVieServeurParDefaut".equals(dto.getCode()) && "3".equals(dto.getValeur()) && "US 46".equals(dto.getSource()))); + } + + @Test + void importCSV_whenWrongFile_shouldReturnOnlyErrors() throws Exception { + File file = ResourceUtils.getFile("classpath:csv/unit/wrongCSVFile.csv"); + var resultatImport = importPortToTest.importCSV(new FileInputStream(file)); + + assertEquals(0, resultatImport.getNbrLignesImportees()); + assertEquals(1, resultatImport.getErreurs().size()); + assertTrue(resultatImport.getErreurs().stream().anyMatch("La ligne n°2 est invalide : Entêtes incohérentes"::equals)); + } + + @Test + void importCSV_whenStreamAlreadyClosedShouldReturnReportWithOneError() throws IOException { + DataInputStream dataInputStream = mock(DataInputStream.class); + doNothing().when(dataInputStream).close(); + + var resultatImport = importPortToTest.importCSV(dataInputStream); + + verify(dataInputStream).close(); + assertEquals(0, resultatImport.getNbrLignesImportees()); + assertEquals(1, resultatImport.getErreurs().size()); + assertEquals("Le fichier CSV n'a pas pu être lu.", resultatImport.getErreurs().get(0)); + } + + @Test + void importCSV_whenFileNotFoundShouldReturnReportWithOneError() throws IOException { + DataInputStream dataInputStream = mock(DataInputStream.class); + doThrow(new FileNotFoundException("start Read csv etape")).when(dataInputStream).close(); + + var resultatImport = importPortToTest.importCSV(dataInputStream); + + verify(dataInputStream).close(); + assertEquals(0, resultatImport.getNbrLignesImportees()); + assertEquals(1, resultatImport.getErreurs().size()); + assertEquals("Le fichier CSV n'a pas pu être lu.", resultatImport.getErreurs().get(0)); + } +} diff --git a/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/domain/port/input/ImportImpactEquipementPortTest.java b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/domain/port/input/ImportImpactEquipementPortTest.java new file mode 100644 index 00000000..5def8e6c --- /dev/null +++ b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/domain/port/input/ImportImpactEquipementPortTest.java @@ -0,0 +1,170 @@ +package org.mte.numecoeval.referentiel.domain.port.input; + +import org.junit.jupiter.api.Test; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.ImpactEquipementDTO; +import org.mte.numecoeval.referentiel.domain.ports.input.ImportCSVReferentielPort; +import org.mte.numecoeval.referentiel.domain.ports.input.impl.ImportImpactEquipementPortImpl; +import org.springframework.util.ResourceUtils; + +import java.io.DataInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.Objects; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.*; + +class ImportImpactEquipementPortTest { + + ImportCSVReferentielPort<ImpactEquipementDTO> importPortToTest = new ImportImpactEquipementPortImpl(); + + @Test + void importCSV_shouldImportAllDatas() throws Exception { + File file = ResourceUtils.getFile("classpath:csv/unit/impactEquipement.csv"); + var resultatImport = importPortToTest.importCSV(new FileInputStream(file)); + + assertEquals(4, resultatImport.getNbrLignesImportees()); + assertEquals(0, resultatImport.getErreurs().size()); + assertEquals(4, resultatImport.getObjects().size()); + assertTrue(resultatImport.getObjects().stream().anyMatch(dto -> + "Ordinateur portable 14.5 8 Go RAM 564 Go SSD".equals(dto.getRefEquipement()) + && "FABRICATION".equals(dto.getEtape()) + && "Changement climatique".equals(dto.getCritere()) + && Double.valueOf(149.0).equals(dto.getValeur()) + && Double.valueOf(30.1).equals(dto.getConsoElecMoyenne()) + && "Ref_Base_donnee_reference_20220728.xlsx".equals(dto.getSource()) + && "Ordinateur Portable".equals(dto.getType()) + && Objects.isNull(dto.getDescription()) + ) + ); + assertTrue(resultatImport.getObjects().stream().anyMatch(dto -> + "Ordinateur portable 14.5 8 Go RAM 564 Go SSD".equals(dto.getRefEquipement()) + && "DISTRIBUTION".equals(dto.getEtape()) + && "Changement climatique".equals(dto.getCritere()) + && Double.valueOf(11.0082325332).equals(dto.getValeur()) + && Double.valueOf(29.1).equals(dto.getConsoElecMoyenne()) + && "Ref_Base_donnee_reference_20220728.xlsx".equals(dto.getSource()) + && "Ordinateur Portable".equals(dto.getType()) + && Objects.isNull(dto.getDescription()) + ) + ); + assertTrue(resultatImport.getObjects().stream().anyMatch(dto -> + "Ordinateur portable 14.5 8 Go RAM 564 Go SSD".equals(dto.getRefEquipement()) + && "UTILISATION".equals(dto.getEtape()) + && "Changement climatique".equals(dto.getCritere()) + && Double.valueOf(1.9485359999999998).equals(dto.getValeur()) + && Double.valueOf(28.1).equals(dto.getConsoElecMoyenne()) + && "Ref_Base_donnee_reference_20220728.xlsx".equals(dto.getSource()) + && "Ordinateur Portable".equals(dto.getType()) + && Objects.isNull(dto.getDescription()) + ) + ); + assertTrue(resultatImport.getObjects().stream().anyMatch(dto -> + "Ordinateur portable 14.5 8 Go RAM 564 Go SSD".equals(dto.getRefEquipement()) + && "FIN_DE_VIE".equals(dto.getEtape()) + && "Changement climatique".equals(dto.getCritere()) + && Double.valueOf(1.46).equals(dto.getValeur()) + && Double.valueOf(1.1).equals(dto.getConsoElecMoyenne()) + && "Ref_Base_donnee_reference_20220728.xlsx".equals(dto.getSource()) + && "Ordinateur Portable".equals(dto.getType()) + && Objects.isNull(dto.getDescription()) + ) + ); + } + + @Test + void importCSV_whenErrorInMiddleOfFile_shouldImportDatasWithErrors() throws Exception { + File file = ResourceUtils.getFile("classpath:csv/unit/impactEquipement_errorInMiddle.csv"); + var resultatImport = importPortToTest.importCSV(new FileInputStream(file)); + + assertEquals(4, resultatImport.getNbrLignesImportees()); + assertEquals(3, resultatImport.getErreurs().size()); + assertTrue(resultatImport.getErreurs().stream().anyMatch("La ligne n°4 est invalide : La colonne refEquipement ne peut être vide"::equals)); + assertTrue(resultatImport.getErreurs().stream().anyMatch("La ligne n°5 est invalide : La colonne etapeacv ne peut être vide"::equals)); + assertTrue(resultatImport.getErreurs().stream().anyMatch("La ligne n°6 est invalide : La colonne critere ne peut être vide"::equals)); + assertEquals(4, resultatImport.getObjects().size()); + assertTrue(resultatImport.getObjects().stream().anyMatch(dto -> + "Ordinateur portable 14.5 8 Go RAM 564 Go SSD".equals(dto.getRefEquipement()) + && "FABRICATION".equals(dto.getEtape()) + && "Changement climatique".equals(dto.getCritere()) + && Double.valueOf(149.0).equals(dto.getValeur()) + && Double.valueOf(30.1).equals(dto.getConsoElecMoyenne()) + && "Ref_Base_donnee_reference_20220728.xlsx".equals(dto.getSource()) + && "Ordinateur Portable".equals(dto.getType()) + && "description Ordinateur Portable 1".equals(dto.getDescription()) + ) + ); + assertTrue(resultatImport.getObjects().stream().anyMatch(dto -> + "Ordinateur portable 14.5 8 Go RAM 564 Go SSD".equals(dto.getRefEquipement()) + && "DISTRIBUTION".equals(dto.getEtape()) + && "Changement climatique".equals(dto.getCritere()) + && Double.valueOf(11.0082325332).equals(dto.getValeur()) + && Double.valueOf(29.1).equals(dto.getConsoElecMoyenne()) + && "Ref_Base_donnee_reference_20220728.xlsx".equals(dto.getSource()) + && "Ordinateur Portable".equals(dto.getType()) + && "description Ordinateur Portable 2".equals(dto.getDescription()) + ) + ); + assertTrue(resultatImport.getObjects().stream().anyMatch(dto -> + "Ordinateur portable 14.5 8 Go RAM 564 Go SSD".equals(dto.getRefEquipement()) + && "UTILISATION".equals(dto.getEtape()) + && "Changement climatique".equals(dto.getCritere()) + && Double.valueOf(1.9485359999999998).equals(dto.getValeur()) + && Double.valueOf(28.1).equals(dto.getConsoElecMoyenne()) + && "Ref_Base_donnee_reference_20220728.xlsx".equals(dto.getSource()) + && "Ordinateur Portable".equals(dto.getType()) + && "description Ordinateur Portable 3".equals(dto.getDescription()) + ) + ); + assertTrue(resultatImport.getObjects().stream().anyMatch(dto -> + "Ordinateur portable 14.5 8 Go RAM 564 Go SSD".equals(dto.getRefEquipement()) + && "FIN_DE_VIE".equals(dto.getEtape()) + && "Changement climatique".equals(dto.getCritere()) + && Double.valueOf(1.46).equals(dto.getValeur()) + && Double.valueOf(1.1).equals(dto.getConsoElecMoyenne()) + && "Ref_Base_donnee_reference_20220728.xlsx".equals(dto.getSource()) + && "Ordinateur Portable".equals(dto.getType()) + && "description Ordinateur Portable 4".equals(dto.getDescription()) + ) + ); + } + + @Test + void importCSV_whenWrongFile_shouldReturnOnlyErrors() throws Exception { + File file = ResourceUtils.getFile("classpath:csv/unit/wrongCSVFile.csv"); + var resultatImport = importPortToTest.importCSV(new FileInputStream(file)); + + assertEquals(0, resultatImport.getNbrLignesImportees()); + assertEquals(1, resultatImport.getErreurs().size()); + assertTrue(resultatImport.getErreurs().stream().anyMatch("La ligne n°2 est invalide : Entêtes incohérentes"::equals)); + } + + @Test + void importCSV_whenStreamAlreadyClosedShouldReturnReportWithOneError() throws IOException { + DataInputStream dataInputStream = mock(DataInputStream.class); + doNothing().when(dataInputStream).close(); + + var resultatImport = importPortToTest.importCSV(dataInputStream); + + verify(dataInputStream).close(); + assertEquals(0, resultatImport.getNbrLignesImportees()); + assertEquals(1, resultatImport.getErreurs().size()); + assertEquals("Le fichier CSV n'a pas pu être lu.", resultatImport.getErreurs().get(0)); + } + + @Test + void importCSV_whenFileNotFoundShouldReturnReportWithOneError() throws IOException { + DataInputStream dataInputStream = mock(DataInputStream.class); + doThrow(new FileNotFoundException("start Read csv etape")).when(dataInputStream).close(); + + var resultatImport = importPortToTest.importCSV(dataInputStream); + + verify(dataInputStream).close(); + assertEquals(0, resultatImport.getNbrLignesImportees()); + assertEquals(1, resultatImport.getErreurs().size()); + assertEquals("Le fichier CSV n'a pas pu être lu.", resultatImport.getErreurs().get(0)); + } +} diff --git a/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/domain/port/input/ImportImpactMessageriePortTest.java b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/domain/port/input/ImportImpactMessageriePortTest.java new file mode 100644 index 00000000..acd3d077 --- /dev/null +++ b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/domain/port/input/ImportImpactMessageriePortTest.java @@ -0,0 +1,114 @@ +package org.mte.numecoeval.referentiel.domain.port.input; + +import org.junit.jupiter.api.Test; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.ImpactMessagerieDTO; +import org.mte.numecoeval.referentiel.domain.ports.input.ImportCSVReferentielPort; +import org.mte.numecoeval.referentiel.domain.ports.input.impl.ImportImpactMessageriePortImpl; +import org.springframework.util.ResourceUtils; + +import java.io.DataInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.*; + +class ImportImpactMessageriePortTest { + + ImportCSVReferentielPort<ImpactMessagerieDTO> importPortToTest = new ImportImpactMessageriePortImpl(); + + @Test + void importCSV_shouldImportAllDatas() throws Exception { + File file = ResourceUtils.getFile("classpath:csv/unit/impactMessagerie.csv"); + var resultatImport = importPortToTest.importCSV(new FileInputStream(file)); + + assertEquals(5, resultatImport.getNbrLignesImportees()); + assertEquals(0, resultatImport.getErreurs().size()); + assertEquals(5, resultatImport.getObjects().size()); + assertTrue(resultatImport.getObjects().stream().anyMatch(dto -> + "Changement climatique".equals(dto.getCritere()) + && Double.valueOf(0.5).equals(dto.getConstanteCoefficientDirecteur()) + && Double.valueOf(0).equals(dto.getConstanteOrdonneeOrigine()) + && "Tests".equals(dto.getSource()) + ) + ); + assertTrue(resultatImport.getObjects().stream().anyMatch(dto -> + "Émissions de particules fines".equals(dto.getCritere()) + && Double.valueOf(1.000111).equals(dto.getConstanteCoefficientDirecteur()) + && Double.valueOf(1.451).equals(dto.getConstanteOrdonneeOrigine()) + && "Tests".equals(dto.getSource()) + ) + ); + assertTrue(resultatImport.getObjects().stream().anyMatch(dto -> + "Radiations ionisantes".equals(dto.getCritere()) + && Double.valueOf(0.00005).equals(dto.getConstanteCoefficientDirecteur()) + && Double.valueOf(0.02315412).equals(dto.getConstanteOrdonneeOrigine()) + && "Tests".equals(dto.getSource()) + ) + ); + assertTrue(resultatImport.getObjects().stream().anyMatch(dto -> + "Acidification".equals(dto.getCritere()) + && Double.valueOf(1.224586).equals(dto.getConstanteCoefficientDirecteur()) + && Double.valueOf(0.042).equals(dto.getConstanteOrdonneeOrigine()) + && "Tests".equals(dto.getSource()) + ) + ); + assertTrue(resultatImport.getObjects().stream().anyMatch(dto -> + "Épuisement des ressources naturelles (minérales et métaux)".equals(dto.getCritere()) + && Double.valueOf(2.020).equals(dto.getConstanteCoefficientDirecteur()) + && Double.valueOf(0.678).equals(dto.getConstanteOrdonneeOrigine()) + && "Tests".equals(dto.getSource()) + ) + ); + } + + @Test + void importCSV_whenErrorInMiddleOfFile_shouldImportDatasWithErrors() throws Exception { + File file = ResourceUtils.getFile("classpath:csv/unit/impactMessagerie_errorInMiddle.csv"); + var resultatImport = importPortToTest.importCSV(new FileInputStream(file)); + + assertEquals(2, resultatImport.getNbrLignesImportees()); + assertEquals(1, resultatImport.getErreurs().size()); + assertTrue(resultatImport.getErreurs().stream().anyMatch("La ligne n°3 est invalide : La colonne critere ne peut être vide"::equals)); + assertEquals(2, resultatImport.getObjects().size()); + } + + @Test + void importCSV_whenWrongFile_shouldReturnOnlyErrors() throws Exception { + File file = ResourceUtils.getFile("classpath:csv/unit/wrongCSVFile.csv"); + var resultatImport = importPortToTest.importCSV(new FileInputStream(file)); + + assertEquals(0, resultatImport.getNbrLignesImportees()); + assertEquals(1, resultatImport.getErreurs().size()); + assertTrue(resultatImport.getErreurs().stream().anyMatch("La ligne n°2 est invalide : Entêtes incohérentes"::equals)); + } + + @Test + void importCSV_whenStreamAlreadyClosedShouldReturnReportWithOneError() throws IOException { + DataInputStream dataInputStream = mock(DataInputStream.class); + doNothing().when(dataInputStream).close(); + + var resultatImport = importPortToTest.importCSV(dataInputStream); + + verify(dataInputStream).close(); + assertEquals(0, resultatImport.getNbrLignesImportees()); + assertEquals(1, resultatImport.getErreurs().size()); + assertEquals("Le fichier CSV n'a pas pu être lu.", resultatImport.getErreurs().get(0)); + } + + @Test + void importCSV_whenFileNotFoundShouldReturnReportWithOneError() throws IOException { + DataInputStream dataInputStream = mock(DataInputStream.class); + doThrow(new FileNotFoundException("start Read csv etape")).when(dataInputStream).close(); + + var resultatImport = importPortToTest.importCSV(dataInputStream); + + verify(dataInputStream).close(); + assertEquals(0, resultatImport.getNbrLignesImportees()); + assertEquals(1, resultatImport.getErreurs().size()); + assertEquals("Le fichier CSV n'a pas pu être lu.", resultatImport.getErreurs().get(0)); + } +} diff --git a/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/domain/port/input/ImportImpactReseauPortTest.java b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/domain/port/input/ImportImpactReseauPortTest.java new file mode 100644 index 00000000..8607e23d --- /dev/null +++ b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/domain/port/input/ImportImpactReseauPortTest.java @@ -0,0 +1,148 @@ +package org.mte.numecoeval.referentiel.domain.port.input; + +import org.apache.commons.lang3.StringUtils; +import org.junit.jupiter.api.Test; +import org.mte.numecoeval.referentiel.domain.ports.input.ImportCSVReferentielPort; +import org.mte.numecoeval.referentiel.domain.ports.input.impl.ImportImpactReseauPortImpl; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.ImpactReseauDTO; +import org.springframework.util.ResourceUtils; + +import java.io.*; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.*; + +class ImportImpactReseauPortTest { + + ImportCSVReferentielPort<ImpactReseauDTO> importPortToTest = new ImportImpactReseauPortImpl(); + + @Test + void importCSV_shouldImportAllDatas() throws Exception { + File file = ResourceUtils.getFile("classpath:csv/unit/impactreseau.csv"); + var resultatImport = importPortToTest.importCSV(new FileInputStream(file)); + + assertEquals(4, resultatImport.getNbrLignesImportees()); + assertEquals(0, resultatImport.getErreurs().size()); + assertEquals(4, resultatImport.getObjects().size()); + assertTrue(resultatImport.getObjects().stream().anyMatch(dto -> + "impactReseauMobileMoyen".equals(dto.getRefReseau()) + && "DISTRIBUTION".equals(dto.getEtapeACV()) + && "Changement climatique".equals(dto.getCritere()) + && Double.valueOf(1.428).equals(dto.getValeur()) + && Double.valueOf(0.0020).equals(dto.getConsoElecMoyenne()) + && "RefTest V1.00".equals(dto.getSource())) + ); + assertTrue(resultatImport.getObjects().stream().anyMatch(dto -> + "impactReseauMobileMoyen".equals(dto.getRefReseau()) + && "FABRICATION".equals(dto.getEtapeACV()) + && "Changement climatique".equals(dto.getCritere()) + && Double.valueOf(518.28).equals(dto.getValeur()) + && StringUtils.isEmpty(dto.getUnite()) + && dto.getConsoElecMoyenne() == null + && "RefTest V1.00".equals(dto.getSource())) + ); + assertTrue(resultatImport.getObjects().stream().anyMatch(dto -> + "impactReseauMobileMoyen".equals(dto.getRefReseau()) + && "FIN_DE_VIE".equals(dto.getEtapeACV()) + && "Changement climatique".equals(dto.getCritere()) + && Double.valueOf(64.28).equals(dto.getValeur()) + && StringUtils.isEmpty(dto.getUnite()) + && dto.getConsoElecMoyenne() == null + && "RefTest V1.00".equals(dto.getSource())) + ); + assertTrue(resultatImport.getObjects().stream().anyMatch(dto -> + "impactReseauMobileMoyen".equals(dto.getRefReseau()) + && "UTILISATION".equals(dto.getEtapeACV()) + && "Changement climatique".equals(dto.getCritere()) + && Double.valueOf(76.28).equals(dto.getValeur()) + && StringUtils.isEmpty(dto.getUnite()) + && dto.getConsoElecMoyenne() == null + && "RefTest V1.00".equals(dto.getSource())) + ); + } + + @Test + void importCSV_whenErrorInMiddleOfFile_shouldImportDatasWithErrors() throws Exception { + File file = ResourceUtils.getFile("classpath:csv/unit/impactreseau_errorInMiddle.csv"); + var resultatImport = importPortToTest.importCSV(new FileInputStream(file)); + + assertEquals(4, resultatImport.getNbrLignesImportees()); + assertEquals(3, resultatImport.getErreurs().size()); + assertTrue(resultatImport.getErreurs().stream().anyMatch("La ligne n°4 est invalide : La colonne refReseau ne peut être vide"::equals)); + assertTrue(resultatImport.getErreurs().stream().anyMatch("La ligne n°5 est invalide : La colonne etapeACV ne peut être vide"::equals)); + assertTrue(resultatImport.getErreurs().stream().anyMatch("La ligne n°6 est invalide : La colonne critere ne peut être vide"::equals)); + assertEquals(4, resultatImport.getObjects().size()); + assertTrue(resultatImport.getObjects().stream().anyMatch(dto -> + "impactReseauMobileMoyen".equals(dto.getRefReseau()) + && "DISTRIBUTION".equals(dto.getEtapeACV()) + && "Changement climatique".equals(dto.getCritere()) + && Double.valueOf(1.428).equals(dto.getValeur()) + && Double.valueOf(0.0020).equals(dto.getConsoElecMoyenne()) + && "RefTest V1.00".equals(dto.getSource())) + ); + assertTrue(resultatImport.getObjects().stream().anyMatch(dto -> + "impactReseauMobileMoyen".equals(dto.getRefReseau()) + && "FABRICATION".equals(dto.getEtapeACV()) + && "Changement climatique".equals(dto.getCritere()) + && Double.valueOf(518.28).equals(dto.getValeur()) + && StringUtils.isEmpty(dto.getUnite()) + && dto.getConsoElecMoyenne() == null + && "RefTest V1.00".equals(dto.getSource())) + ); + assertTrue(resultatImport.getObjects().stream().anyMatch(dto -> + "impactReseauMobileMoyen".equals(dto.getRefReseau()) + && "FIN_DE_VIE".equals(dto.getEtapeACV()) + && "Changement climatique".equals(dto.getCritere()) + && Double.valueOf(64.28).equals(dto.getValeur()) + && StringUtils.isEmpty(dto.getUnite()) + && dto.getConsoElecMoyenne() == null + && "RefTest V1.00".equals(dto.getSource())) + ); + assertTrue(resultatImport.getObjects().stream().anyMatch(dto -> + "impactReseauMobileMoyen".equals(dto.getRefReseau()) + && "UTILISATION".equals(dto.getEtapeACV()) + && "Changement climatique".equals(dto.getCritere()) + && Double.valueOf(76.28).equals(dto.getValeur()) + && StringUtils.isEmpty(dto.getUnite()) + && dto.getConsoElecMoyenne() == null + && "RefTest V1.00".equals(dto.getSource())) + ); + } + + @Test + void importCSV_whenWrongFile_shouldReturnOnlyErrors() throws Exception { + File file = ResourceUtils.getFile("classpath:csv/unit/wrongCSVFile.csv"); + var resultatImport = importPortToTest.importCSV(new FileInputStream(file)); + + assertEquals(0, resultatImport.getNbrLignesImportees()); + assertEquals(1, resultatImport.getErreurs().size()); + assertTrue(resultatImport.getErreurs().stream().anyMatch("La ligne n°2 est invalide : Entêtes incohérentes"::equals)); + } + + @Test + void importCSV_whenStreamAlreadyClosedShouldReturnReportWithOneError() throws IOException { + DataInputStream dataInputStream = mock(DataInputStream.class); + doNothing().when(dataInputStream).close(); + + var resultatImport = importPortToTest.importCSV(dataInputStream); + + verify(dataInputStream).close(); + assertEquals(0, resultatImport.getNbrLignesImportees()); + assertEquals(1, resultatImport.getErreurs().size()); + assertEquals("Le fichier CSV n'a pas pu être lu.", resultatImport.getErreurs().get(0)); + } + + @Test + void importCSV_whenFileNotFoundShouldReturnReportWithOneError() throws IOException { + DataInputStream dataInputStream = mock(DataInputStream.class); + doThrow(new FileNotFoundException("start Read csv etape")).when(dataInputStream).close(); + + var resultatImport = importPortToTest.importCSV(dataInputStream); + + verify(dataInputStream).close(); + assertEquals(0, resultatImport.getNbrLignesImportees()); + assertEquals(1, resultatImport.getErreurs().size()); + assertEquals("Le fichier CSV n'a pas pu être lu.", resultatImport.getErreurs().get(0)); + } +} diff --git a/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/domain/port/input/ImportMixElectriquePortTest.java b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/domain/port/input/ImportMixElectriquePortTest.java new file mode 100644 index 00000000..0f0e3126 --- /dev/null +++ b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/domain/port/input/ImportMixElectriquePortTest.java @@ -0,0 +1,141 @@ +package org.mte.numecoeval.referentiel.domain.port.input; + +import org.junit.jupiter.api.Test; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.MixElectriqueDTO; +import org.mte.numecoeval.referentiel.domain.ports.input.ImportCSVReferentielPort; +import org.mte.numecoeval.referentiel.domain.ports.input.impl.ImportMixElectriquePortImpl; +import org.springframework.util.ResourceUtils; + +import java.io.DataInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.*; + +class ImportMixElectriquePortTest { + + ImportCSVReferentielPort<MixElectriqueDTO> importPortToTest = new ImportMixElectriquePortImpl(); + + @Test + void importCSV_shouldImportAllDatas() throws Exception { + File file = ResourceUtils.getFile("classpath:csv/unit/mixElectrique.csv"); + var resultatImport = importPortToTest.importCSV(new FileInputStream(file)); + + assertEquals(4, resultatImport.getNbrLignesImportees()); + assertEquals(0, resultatImport.getErreurs().size()); + assertEquals(4, resultatImport.getObjects().size()); + assertTrue(resultatImport.getObjects().stream().anyMatch(dto -> + "Changement climatique".equals(dto.getCritere()) + && "France".equals(dto.getPays()) + && "FR".equals(dto.getRaccourcisAnglais()) + && Double.valueOf(149).equals(dto.getValeur()) + && "Ref_Base_donnee_reference_20480728.xlsx".equals(dto.getSource())) + ); + assertTrue(resultatImport.getObjects().stream().anyMatch(dto -> + "Changement climatique".equals(dto.getCritere()) + && "Brésil".equals(dto.getPays()) + && "BR".equals(dto.getRaccourcisAnglais()) + && Double.valueOf(11.0082325332).equals(dto.getValeur()) + && "Ref_Base_donnee_reference_20480728.xlsx".equals(dto.getSource())) + ); + assertTrue(resultatImport.getObjects().stream().anyMatch(dto -> + "Changement climatique".equals(dto.getCritere()) + && "Angleterre".equals(dto.getPays()) + && "EN".equals(dto.getRaccourcisAnglais()) + && Double.valueOf(1.9485359999999998).equals(dto.getValeur()) + && "Ref_Base_donnee_reference_20480728.xlsx".equals(dto.getSource())) + ); + assertTrue(resultatImport.getObjects().stream().anyMatch(dto -> + "Changement climatique".equals(dto.getCritere()) + && "USA".equals(dto.getPays()) + && "US".equals(dto.getRaccourcisAnglais()) + && Double.valueOf(1.46).equals(dto.getValeur()) + && "Ref_Base_donnee_reference_20480728.xlsx".equals(dto.getSource())) + ); + } + + @Test + void importCSV_whenErrorInMiddleOfFile_shouldImportDatasWithErrors() throws Exception { + File file = ResourceUtils.getFile("classpath:csv/unit/mixElectrique_errorInMiddle.csv"); + var resultatImport = importPortToTest.importCSV(new FileInputStream(file)); + + assertEquals(4, resultatImport.getNbrLignesImportees()); + assertEquals(3, resultatImport.getErreurs().size()); + assertTrue(resultatImport.getErreurs().stream().anyMatch("La ligne n°4 est invalide : La colonne pays ne peut être vide"::equals)); + assertTrue(resultatImport.getErreurs().stream().anyMatch("La ligne n°5 est invalide : La colonne raccourcisAnglais ne peut être vide"::equals)); + assertTrue(resultatImport.getErreurs().stream().anyMatch("La ligne n°6 est invalide : La colonne critere ne peut être vide"::equals)); + assertEquals(4, resultatImport.getObjects().size()); + resultatImport.getObjects().forEach( + mixElectriqueDTO -> System.out.printf("%s ; %s ; %s ; %s%n", mixElectriqueDTO.getCritere(), mixElectriqueDTO.getPays(), mixElectriqueDTO.getRaccourcisAnglais(), mixElectriqueDTO.getValeur()) + ); + assertTrue(resultatImport.getObjects().stream().anyMatch(dto -> + "Changement climatique".equals(dto.getCritere()) + && "France".equals(dto.getPays()) + && "FR".equals(dto.getRaccourcisAnglais()) + && Double.valueOf(149).equals(dto.getValeur()) + && "Ref_Base_donnee_reference_20480728.xlsx".equals(dto.getSource())) + ); + assertTrue(resultatImport.getObjects().stream().anyMatch(dto -> + "Changement climatique".equals(dto.getCritere()) + && "Brésil".equals(dto.getPays()) + && "BR".equals(dto.getRaccourcisAnglais()) + //&& Double.valueOf(11.0082325332).equals(dto.getValeur()) + //&& "Ref_Base_donnee_reference_20480728.xlsx".equals(dto.getSource()) + ) + ); + assertTrue(resultatImport.getObjects().stream().anyMatch(dto -> + "Changement climatique".equals(dto.getCritere()) + && "Angleterre".equals(dto.getPays()) + && "EN".equals(dto.getRaccourcisAnglais()) + && Double.valueOf(1.9485359999999998).equals(dto.getValeur()) + && "Ref_Base_donnee_reference_20480728.xlsx".equals(dto.getSource())) + ); + assertTrue(resultatImport.getObjects().stream().anyMatch(dto -> + "Changement climatique".equals(dto.getCritere()) + && "USA".equals(dto.getPays()) + && "US".equals(dto.getRaccourcisAnglais()) + && Double.valueOf(1.46).equals(dto.getValeur()) + && "Ref_Base_donnee_reference_20480728.xlsx".equals(dto.getSource())) + ); + } + + @Test + void importCSV_whenWrongFile_shouldReturnOnlyErrors() throws Exception { + File file = ResourceUtils.getFile("classpath:csv/unit/wrongCSVFile.csv"); + var resultatImport = importPortToTest.importCSV(new FileInputStream(file)); + + assertEquals(0, resultatImport.getNbrLignesImportees()); + assertEquals(1, resultatImport.getErreurs().size()); + assertTrue(resultatImport.getErreurs().stream().anyMatch("La ligne n°2 est invalide : Entêtes incohérentes"::equals)); + } + + @Test + void importCSV_whenStreamAlreadyClosedShouldReturnReportWithOneError() throws IOException { + DataInputStream dataInputStream = mock(DataInputStream.class); + doNothing().when(dataInputStream).close(); + + var resultatImport = importPortToTest.importCSV(dataInputStream); + + verify(dataInputStream).close(); + assertEquals(0, resultatImport.getNbrLignesImportees()); + assertEquals(1, resultatImport.getErreurs().size()); + assertEquals("Le fichier CSV n'a pas pu être lu.", resultatImport.getErreurs().get(0)); + } + + @Test + void importCSV_whenFileNotFoundShouldReturnReportWithOneError() throws IOException { + DataInputStream dataInputStream = mock(DataInputStream.class); + doThrow(new FileNotFoundException("start Read csv etape")).when(dataInputStream).close(); + + var resultatImport = importPortToTest.importCSV(dataInputStream); + + verify(dataInputStream).close(); + assertEquals(0, resultatImport.getNbrLignesImportees()); + assertEquals(1, resultatImport.getErreurs().size()); + assertEquals("Le fichier CSV n'a pas pu être lu.", resultatImport.getErreurs().get(0)); + } +} diff --git a/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/domain/port/input/ImportTypeEquipementPortTest.java b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/domain/port/input/ImportTypeEquipementPortTest.java new file mode 100644 index 00000000..9a1ff3df --- /dev/null +++ b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/domain/port/input/ImportTypeEquipementPortTest.java @@ -0,0 +1,115 @@ +package org.mte.numecoeval.referentiel.domain.port.input; + +import org.junit.jupiter.api.Test; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.TypeEquipementDTO; +import org.mte.numecoeval.referentiel.domain.ports.input.ImportCSVReferentielPort; +import org.mte.numecoeval.referentiel.domain.ports.input.impl.ImportTypeEquipementPortImpl; +import org.springframework.util.ResourceUtils; + +import java.io.DataInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.*; + +class ImportTypeEquipementPortTest { + + ImportCSVReferentielPort<TypeEquipementDTO> importPortToTest = new ImportTypeEquipementPortImpl(); + + @Test + void importCSV_shouldImportAllDatas() throws Exception { + File file = ResourceUtils.getFile("classpath:csv/unit/typeEquipement.csv"); + var resultatImport = importPortToTest.importCSV(new FileInputStream(file)); + + assertEquals(2, resultatImport.getNbrLignesImportees()); + assertEquals(0, resultatImport.getErreurs().size()); + assertEquals(2, resultatImport.getObjects().size()); + assertTrue(resultatImport.getObjects().stream().anyMatch(typeEquipementDTO -> + "Serveur".equals(typeEquipementDTO.getType()) + && typeEquipementDTO.isServeur() + && Double.valueOf(6.0).equals(typeEquipementDTO.getDureeVieDefaut()) + && "Exemple de serveur basique".equals(typeEquipementDTO.getSource()) + && "Test simple".equals(typeEquipementDTO.getCommentaire()) + && "serveur_par_defaut".equals(typeEquipementDTO.getRefEquipementParDefaut()) + ) + ); + assertTrue(resultatImport.getObjects().stream().anyMatch(typeEquipementDTO -> + "Laptop".equals(typeEquipementDTO.getType()) + && !typeEquipementDTO.isServeur() + && Double.valueOf(5.0).equals(typeEquipementDTO.getDureeVieDefaut()) + && "ADEME, durée de vie des EEE, UF utiliser une ordinateur, écran non cathodique et LCD, https://vu.fr/bOQZ".equals(typeEquipementDTO.getSource()) + && "Test simple".equals(typeEquipementDTO.getCommentaire()) + && "laptop_par_defaut".equals(typeEquipementDTO.getRefEquipementParDefaut()) + ) + ); + } + + @Test + void importCSV_whenErrorInMiddleOfFile_shouldImportDatasWithErrors() throws Exception { + File file = ResourceUtils.getFile("classpath:csv/unit/typeEquipement_errorInMiddle.csv"); + var resultatImport = importPortToTest.importCSV(new FileInputStream(file)); + + assertEquals(2, resultatImport.getNbrLignesImportees()); + assertEquals(1, resultatImport.getErreurs().size()); + assertTrue(resultatImport.getErreurs().stream().anyMatch("La ligne n°3 est invalide : La colonne type ne peut être vide"::equals)); + assertEquals(2, resultatImport.getObjects().size()); + assertTrue(resultatImport.getObjects().stream().anyMatch(typeEquipementDTO -> + "Serveur".equals(typeEquipementDTO.getType()) + && typeEquipementDTO.isServeur() + && Double.valueOf(6.0).equals(typeEquipementDTO.getDureeVieDefaut()) + && "Exemple de serveur basique".equals(typeEquipementDTO.getSource()) + && "Test simple".equals(typeEquipementDTO.getCommentaire()) + && "serveur_par_defaut".equals(typeEquipementDTO.getRefEquipementParDefaut()) + ) + ); + assertTrue(resultatImport.getObjects().stream().anyMatch(typeEquipementDTO -> + "Laptop".equals(typeEquipementDTO.getType()) + && !typeEquipementDTO.isServeur() + && Double.valueOf(5.0).equals(typeEquipementDTO.getDureeVieDefaut()) + && "ADEME, durée de vie des EEE, UF utiliser une ordinateur, écran non cathodique et LCD, https://vu.fr/bOQZ".equals(typeEquipementDTO.getSource()) + && "Test simple".equals(typeEquipementDTO.getCommentaire()) + && "laptop_par_defaut".equals(typeEquipementDTO.getRefEquipementParDefaut()) + ) + ); + } + + @Test + void importCSV_whenWrongFile_shouldReturnOnlyErrors() throws Exception { + File file = ResourceUtils.getFile("classpath:csv/unit/wrongCSVFile.csv"); + var resultatImport = importPortToTest.importCSV(new FileInputStream(file)); + + assertEquals(0, resultatImport.getNbrLignesImportees()); + assertEquals(1, resultatImport.getErreurs().size()); + assertTrue(resultatImport.getErreurs().stream().anyMatch("La ligne n°2 est invalide : Entêtes incohérentes"::equals)); + } + + @Test + void importCSV_whenStreamAlreadyClosedShouldReturnReportWithOneError() throws IOException { + DataInputStream dataInputStream = mock(DataInputStream.class); + doNothing().when(dataInputStream).close(); + + var resultatImport = importPortToTest.importCSV(dataInputStream); + + verify(dataInputStream).close(); + assertEquals(0, resultatImport.getNbrLignesImportees()); + assertEquals(1, resultatImport.getErreurs().size()); + assertEquals("Le fichier CSV n'a pas pu être lu.", resultatImport.getErreurs().get(0)); + } + + @Test + void importCSV_whenFileNotFoundShouldReturnReportWithOneError() throws IOException { + DataInputStream dataInputStream = mock(DataInputStream.class); + doThrow(new FileNotFoundException("start Read csv etape")).when(dataInputStream).close(); + + var resultatImport = importPortToTest.importCSV(dataInputStream); + + verify(dataInputStream).close(); + assertEquals(0, resultatImport.getNbrLignesImportees()); + assertEquals(1, resultatImport.getErreurs().size()); + assertEquals("Le fichier CSV n'a pas pu être lu.", resultatImport.getErreurs().get(0)); + } +} diff --git a/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/factory/TestDataFactory.java b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/factory/TestDataFactory.java new file mode 100644 index 00000000..50a7ff9a --- /dev/null +++ b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/factory/TestDataFactory.java @@ -0,0 +1,398 @@ +package org.mte.numecoeval.referentiel.factory; + +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.CorrespondanceRefEquipementDTO; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.CritereDTO; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.EtapeDTO; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.HypotheseDTO; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.ImpactEquipementDTO; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.ImpactMessagerieDTO; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.ImpactReseauDTO; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.MixElectriqueDTO; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.TypeEquipementDTO; +import org.mte.numecoeval.referentiel.domain.model.CorrespondanceRefEquipement; +import org.mte.numecoeval.referentiel.domain.model.Critere; +import org.mte.numecoeval.referentiel.domain.model.Etape; +import org.mte.numecoeval.referentiel.domain.model.Hypothese; +import org.mte.numecoeval.referentiel.domain.model.ImpactEquipement; +import org.mte.numecoeval.referentiel.domain.model.ImpactMessagerie; +import org.mte.numecoeval.referentiel.domain.model.ImpactReseau; +import org.mte.numecoeval.referentiel.domain.model.MixElectrique; +import org.mte.numecoeval.referentiel.domain.model.TypeEquipement; +import org.mte.numecoeval.referentiel.domain.model.id.CritereId; +import org.mte.numecoeval.referentiel.domain.model.id.EtapeId; +import org.mte.numecoeval.referentiel.domain.model.id.HypotheseId; +import org.mte.numecoeval.referentiel.domain.model.id.ImpactEquipementId; +import org.mte.numecoeval.referentiel.domain.model.id.ImpactReseauId; +import org.mte.numecoeval.referentiel.domain.model.id.MixElectriqueId; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.CorrespondanceRefEquipementEntity; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.CritereEntity; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.EtapeEntity; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.HypotheseEntity; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.ImpactEquipementEntity; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.ImpactMessagerieEntity; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.ImpactReseauEntity; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.MixElectriqueEntity; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.TypeEquipementEntity; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.id.CritereIdEntity; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.id.EtapeIdEntity; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.id.HypotheseIdEntity; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.id.ImpactReseauIdEntity; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.id.MixElectriqueIdEntity; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.id.CritereIdDTO; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.id.EtapeIdDTO; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.id.HypotheseIdDTO; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.id.ImpactEquipementIdDTO; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.id.ImpactReseauIdDTO; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.id.MixElectriqueIdDTO; + +public class TestDataFactory { + + public static final String DEFAULT_ETAPE = "UTILISATION"; + + public static final String DEFAULT_CRITERE = "Changement Climatique"; + public static final String DEFAULT_UNITE = "kg CO² eq"; + + public static class CritereFactory { + public static CritereDTO dto(String nomCritere, String unite, String description) { + return new CritereDTO(nomCritere, unite, description); + } + + public static Critere domain(String nomCritere, String description, String unite) { + return new Critere() + .setNomCritere(nomCritere) + .setDescription(description) + .setUnite(unite); + } + + public static CritereEntity entity(String nomCritere, String description, String unite) { + return new CritereEntity() + .setNomCritere(nomCritere) + .setDescription(description) + .setUnite(unite); + } + + public static CritereIdDTO idDTO(String nomCritere) { + return new CritereIdDTO(nomCritere); + } + + public static CritereId idDomain(String nomCritere) { + return new CritereId() + .setNomCritere(nomCritere); + } + + public static CritereIdEntity idEntity(String nomCritere) { + return new CritereIdEntity() + .setNomCritere(nomCritere); + } + } + + public static class EtapeFactory { + + public static EtapeDTO dto(String code, String libelle) { + return new EtapeDTO(code, libelle); + } + + public static Etape domain(String code, String libelle) { + return new Etape() + .setCode(code) + .setLibelle(libelle); + } + + public static EtapeEntity entity(String code, String libelle) { + return new EtapeEntity() + .setCode(code) + .setLibelle(libelle); + } + + public static EtapeIdDTO idDTO(String code) { + return new EtapeIdDTO(code); + } + + public static EtapeId idDomain(String code) { + return new EtapeId() + .setCode(code); + } + + public static EtapeIdEntity idEntity(String code) { + return new EtapeIdEntity() + .setCode(code); + } + } + + public static class HypotheseFactory { + public static HypotheseDTO dto(String code, String valeur, String source) { + return new HypotheseDTO(code, valeur, source); + } + + public static Hypothese domain(String code, String valeur, String source) { + return new Hypothese() + .setCode(code) + .setValeur(valeur) + .setSource(source); + } + + public static HypotheseEntity entity(String code, String valeur, String source) { + return new HypotheseEntity() + .setCode(code) + .setValeur(valeur) + .setSource(source); + } + + public static HypotheseIdDTO idDTO(String code) { + return new HypotheseIdDTO() + .setCode(code); + } + + public static HypotheseId idDomain(String code) { + return new HypotheseId() + .setCode(code); + } + + public static HypotheseIdEntity idEntity(String code) { + return new HypotheseIdEntity() + .setCode(code); + } + } + + public static class TypeEquipementFactory { + public static TypeEquipementDTO dto(String type, boolean estUnServeur, Double dureeVieDefaut, String commentaire, String source, String refEquipementParDefaut) { + return TypeEquipementDTO.builder() + .type(type) + .serveur(estUnServeur) + .dureeVieDefaut(dureeVieDefaut) + .source(source) + .commentaire(commentaire) + .refEquipementParDefaut(refEquipementParDefaut) + .build(); + } + + public static TypeEquipement domain(String type, boolean estUnServeur, Double dureeVieDefaut, String commentaire, String source, String refEquipementParDefaut) { + return TypeEquipement.builder() + .type(type) + .serveur(estUnServeur) + .dureeVieDefaut(dureeVieDefaut) + .source(source) + .commentaire(commentaire) + .refEquipementParDefaut(refEquipementParDefaut) + .build(); + } + + public static TypeEquipementEntity entity(String type, boolean estUnServeur, Double dureeVieDefaut, String commentaire, String source, String refEquipementParDefaut) { + return new TypeEquipementEntity(type,estUnServeur,commentaire,dureeVieDefaut,source, refEquipementParDefaut); + } + } + + public static class MixElectriqueFactory { + + public static MixElectriqueDTO dto(CritereDTO critere, String pays, String raccourcisAnglais, Double valeur, String source) { + return new MixElectriqueDTO( + pays, + raccourcisAnglais, + critere.getNomCritere(), + valeur, + source + ); + } + + public static MixElectrique domain(Critere critere, String pays, String raccourcisAnglais, Double valeur, String source) { + return new MixElectrique() + .setCritere(critere.getNomCritere()) + .setPays(pays) + .setRaccourcisAnglais(raccourcisAnglais) + .setValeur(valeur) + .setSource(source); + } + + public static MixElectriqueEntity entity(CritereEntity critere, String pays, String raccourcisAnglais, Double valeur, String source) { + return new MixElectriqueEntity() + .setCritere(critere.getNomCritere()) + .setPays(pays) + .setRaccourcisAnglais(raccourcisAnglais) + .setValeur(valeur) + .setSource(source); + } + + public static MixElectriqueIdDTO idDTO(CritereIdDTO critereId, String pays) { + return MixElectriqueIdDTO + .builder() + .critere(critereId.getNomCritere()) + .pays(pays) + .build(); + } + + public static MixElectriqueId idDomain(CritereId critereId, String pays) { + return new MixElectriqueId() + .setCritere(critereId.getNomCritere()) + .setPays(pays); + } + + public static MixElectriqueIdEntity idEntity(CritereIdEntity critereId, String pays) { + return new MixElectriqueIdEntity() + .setCritere(critereId.getNomCritere()) + .setPays(pays); + } + } + + public static class ImpactEquipementFactory { + + public static ImpactEquipementDTO dto(String etape, String critere, String refEquipement, String source, String type, Double valeur, Double consoElecMoyenne, String description) { + return new ImpactEquipementDTO( + refEquipement, + etape, + critere, + source, + type, + valeur, + consoElecMoyenne, + description + ); + } + + public static ImpactEquipement domain(Etape etape, Critere critere, String refEquipement, String source, String type, Double valeur, Double consoElecMoyenne) { + return new ImpactEquipement() + .setEtape(etape.getCode()) + .setCritere(critere.getNomCritere()) + .setRefEquipement(refEquipement) + .setSource(source) + .setType(type) + .setValeur(valeur) + .setConsoElecMoyenne(consoElecMoyenne); + } + + public static ImpactEquipementEntity entity(EtapeEntity etape, CritereEntity critere, String refEquipement, String source, String type, Double valeur, Double consoElecMoyenne) { + return new ImpactEquipementEntity() + .setEtape(etape.getCode()) + .setCritere(critere.getNomCritere()) + .setRefEquipement(refEquipement) + .setSource(source) + .setType(type) + .setValeur(valeur) + .setConsoElecMoyenne(consoElecMoyenne); + } + + public static ImpactEquipementIdDTO idDTO(String etapeIdDTO, String critereId, String refEquipement) { + return ImpactEquipementIdDTO + .builder() + .etape(etapeIdDTO) + .critere(critereId) + .refEquipement(refEquipement) + .build(); + } + + public static ImpactEquipementId idDomain(String etapeId, String critereId, String refEquipement) { + return new ImpactEquipementId() + .setEtape(etapeId) + .setCritere(critereId) + .setRefEquipement(refEquipement); + } + + } + + public static class ImpactReseauFactory { + + public static ImpactReseauDTO dto(EtapeDTO etape, CritereDTO critere, String refReseau, String source, Double valeur, Double consoElecMoyenne) { + return new ImpactReseauDTO( + refReseau, + etape.getCode(), + critere.getNomCritere(), + null, + source, + valeur, + consoElecMoyenne + ); + } + + public static ImpactReseau domain(Etape etape, Critere critere, String refReseau, String source, Double valeur, Double consoElecMoyenne) { + return new ImpactReseau() + .setEtape(etape.getCode()) + .setCritere(critere.getNomCritere()) + .setRefReseau(refReseau) + .setSource(source) + .setValeur(valeur) + .setConsoElecMoyenne(consoElecMoyenne); + } + + public static ImpactReseauEntity entity(EtapeEntity etape, CritereEntity critere, String refReseau, String source, Double valeur, Double consoElecMoyenne) { + return new ImpactReseauEntity() + .setEtape(etape.getCode()) + .setCritere(critere.getNomCritere()) + .setRefReseau(refReseau) + .setSource(source) + .setValeur(valeur) + .setConsoElecMoyenne(consoElecMoyenne); + } + + public static ImpactReseauIdDTO idDTO(EtapeIdDTO etapeIdDTO, CritereIdDTO critereId, String refReseau) { + return ImpactReseauIdDTO + .builder() + .etapeACV(etapeIdDTO.getCode()) + .critere(critereId.getNomCritere()) + .refReseau(refReseau) + .build(); + } + + public static ImpactReseauId idDomain(EtapeId etapeId, CritereId critereId, String refReseau) { + return new ImpactReseauId() + .setEtape(etapeId.getCode()) + .setCritere(critereId.getNomCritere()) + .setRefReseau(refReseau); + } + + public static ImpactReseauIdEntity idEntity(EtapeIdEntity etapeIdEntity, CritereIdEntity critereId, String refReseau) { + return new ImpactReseauIdEntity() + .setEtape(etapeIdEntity.getCode()) + .setCritere(critereId.getNomCritere()) + .setRefReseau(refReseau); + } + } + + public static class ImpactMessagerieFactory { + + public static ImpactMessagerieDTO dto(CritereDTO critere, Double constanteCoefficientDirecteur, Double constanteOrdonneeOrigine, String source) { + return ImpactMessagerieDTO.builder() + .critere(critere.getNomCritere()) + .source(source) + .constanteCoefficientDirecteur(constanteCoefficientDirecteur) + .constanteOrdonneeOrigine(constanteOrdonneeOrigine) + .build(); + } + + public static ImpactMessagerie domain(Critere critere, Double constanteCoefficientDirecteur, Double constanteOrdonneeOrigine, String source) { + return new ImpactMessagerie() + .setCritere(critere.getNomCritere()) + .setSource(source) + .setConstanteCoefficientDirecteur(constanteCoefficientDirecteur) + .setConstanteOrdonneeOrigine(constanteOrdonneeOrigine); + } + + public static ImpactMessagerieEntity entity(CritereEntity critere, Double constanteCoefficientDirecteur, Double constanteOrdonneeOrigine, String source) { + return new ImpactMessagerieEntity() + .setCritere(critere.getNomCritere()) + .setSource(source) + .setConstanteCoefficientDirecteur(constanteCoefficientDirecteur) + .setConstanteOrdonneeOrigine(constanteOrdonneeOrigine); + } + } + + public static class CorrespondanceRefEquipementFactory { + public static CorrespondanceRefEquipementDTO dto(String modeleSource, String refEquipementCible) { + return CorrespondanceRefEquipementDTO.builder() + .modeleEquipementSource(modeleSource) + .refEquipementCible(refEquipementCible) + .build(); + } + + public static CorrespondanceRefEquipement domain(String modeleSource, String refEquipementCible) { + return CorrespondanceRefEquipement.builder() + .modeleEquipementSource(modeleSource) + .refEquipementCible(refEquipementCible) + .build(); + } + + public static CorrespondanceRefEquipementEntity entity(String modeleSource, String refEquipementCible) { + return new CorrespondanceRefEquipementEntity() + .setModeleEquipementSource(modeleSource) + .setRefEquipementCible(refEquipementCible); + } + } +} diff --git a/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/adapter/export/CritereCsvExportServiceTest.java b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/adapter/export/CritereCsvExportServiceTest.java new file mode 100644 index 00000000..2523f23e --- /dev/null +++ b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/adapter/export/CritereCsvExportServiceTest.java @@ -0,0 +1,100 @@ +package org.mte.numecoeval.referentiel.infrastructure.adapter.export; + +import org.apache.commons.csv.CSVPrinter; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.mte.numecoeval.referentiel.domain.ports.input.impl.ImportCriterePortImpl; +import org.mte.numecoeval.referentiel.factory.TestDataFactory; +import org.mte.numecoeval.referentiel.infrastructure.jpa.repository.CritereRepository; + +import java.io.IOException; +import java.io.StringWriter; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.when; + +class CritereCsvExportServiceTest { + + @InjectMocks + CritereCsvExportService exportService; + + @Mock + CritereRepository repository; + + @Mock + CSVPrinter csvPrinter; + + @BeforeEach + void setup() { + MockitoAnnotations.openMocks(this); + } + + @Test + void getHeadersShouldReturnSameHeadersAsImport() { + assertEquals(ImportCriterePortImpl.getHeaders(), exportService.getHeaders()); + } + + @Test + void getObjectsToWriteShouldReturnRepositoryFindAll() { + var entities = List.of( + TestDataFactory.CritereFactory.entity("Changement climatique", "","kg CO_{2} eq"), + TestDataFactory.CritereFactory.entity("Acidification", "","mol H^{+} eq") + ); + when(repository.findAll()).thenReturn(entities); + + assertEquals(entities, exportService.getObjectsToWrite()); + } + + @Test + void printRecordShouldUseEntityAttributes() throws IOException { + var entity = TestDataFactory.CritereFactory.entity("Changement climatique", "","kg CO_{2} eq"); + + assertDoesNotThrow(() -> exportService.printRecord(csvPrinter, entity)); + + Mockito.verify(csvPrinter, times(1)).printRecord(entity.getNomCritere(), entity.getDescription(), entity.getUnite()); + } + + @Test + void logRecordErrorShouldLogSpecificErrorForRecord(){ + var entity = TestDataFactory.CritereFactory.entity("Changement climatique", "","kg CO_{2} eq"); + + assertDoesNotThrow(() -> exportService.logRecordError(entity, new Exception("Test"))); + } + + @Test + void logRecordErrorShouldLogGenericErrorForRecord(){ + assertDoesNotThrow(() -> exportService.logRecordError(null, new Exception("Test"))); + } + + @Test + void logRecordErrorShouldLogGenericErrorForFile(){ + assertDoesNotThrow(() -> exportService.logWriterError(new Exception("Test"))); + } + + @Test + void writeToCsvShouldReturnCSV() { + // given + var entities = List.of( + TestDataFactory.CritereFactory.entity("Changement climatique", "Test","kg CO_{2} eq") + ); + when(repository.findAll()).thenReturn(entities); + StringWriter stringWriter = new StringWriter(); + + // when + exportService.writeToCsv(stringWriter); + + // Then + String result = stringWriter.toString(); + assertEquals( + "nomCritere;description;unite\r\nChangement climatique;Test;kg CO_{2} eq\r\n", + result + ); + } +} diff --git a/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/adapter/export/EtapeCsvExportServiceTest.java b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/adapter/export/EtapeCsvExportServiceTest.java new file mode 100644 index 00000000..7bde0c0e --- /dev/null +++ b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/adapter/export/EtapeCsvExportServiceTest.java @@ -0,0 +1,100 @@ +package org.mte.numecoeval.referentiel.infrastructure.adapter.export; + +import org.apache.commons.csv.CSVPrinter; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.mte.numecoeval.referentiel.domain.ports.input.impl.ImportEtapePortImpl; +import org.mte.numecoeval.referentiel.factory.TestDataFactory; +import org.mte.numecoeval.referentiel.infrastructure.jpa.repository.EtapeRepository; + +import java.io.IOException; +import java.io.StringWriter; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.when; + +class EtapeCsvExportServiceTest { + + @InjectMocks + EtapeCsvExportService exportService; + + @Mock + EtapeRepository repository; + + @Mock + CSVPrinter csvPrinter; + + @BeforeEach + void setup() { + MockitoAnnotations.openMocks(this); + } + + @Test + void getHeadersShouldReturnSameHeadersAsImport() { + assertEquals(ImportEtapePortImpl.getHeaders(), exportService.getHeaders()); + } + + @Test + void getObjectsToWriteShouldReturnRepositoryFindAll() { + var entities = List.of( + TestDataFactory.EtapeFactory.entity("UTILISATION","Utilisation"), + TestDataFactory.EtapeFactory.entity("FIN_DE_VIE","Fin de vie") + ); + when(repository.findAll()).thenReturn(entities); + + assertEquals(entities, exportService.getObjectsToWrite()); + } + + @Test + void printRecordShouldUseEntityAttributes() throws IOException { + var entity = TestDataFactory.EtapeFactory.entity("UTILISATION","Utilisation"); + + assertDoesNotThrow(() -> exportService.printRecord(csvPrinter, entity)); + + Mockito.verify(csvPrinter, times(1)).printRecord(entity.getCode(), entity.getLibelle()); + } + + @Test + void logRecordErrorShouldLogSpecificErrorForRecord(){ + var entity = TestDataFactory.EtapeFactory.entity("UTILISATION","Utilisation"); + + assertDoesNotThrow(() -> exportService.logRecordError(entity, new Exception("Test"))); + } + + @Test + void logRecordErrorShouldLogGenericErrorForRecord(){ + assertDoesNotThrow(() -> exportService.logRecordError(null, new Exception("Test"))); + } + + @Test + void logRecordErrorShouldLogGenericErrorForFile(){ + assertDoesNotThrow(() -> exportService.logWriterError(new Exception("Test"))); + } + + @Test + void writeToCsvShouldReturnCSV() { + // given + var entities = List.of( + TestDataFactory.EtapeFactory.entity("UTILISATION","Utilisation") + ); + when(repository.findAll()).thenReturn(entities); + StringWriter stringWriter = new StringWriter(); + + // when + exportService.writeToCsv(stringWriter); + + // Then + String result = stringWriter.toString(); + assertEquals( + "code;libelle\r\nUTILISATION;Utilisation\r\n", + result + ); + } +} diff --git a/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/adapter/export/HypotheseCsvExportServiceTest.java b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/adapter/export/HypotheseCsvExportServiceTest.java new file mode 100644 index 00000000..349805af --- /dev/null +++ b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/adapter/export/HypotheseCsvExportServiceTest.java @@ -0,0 +1,100 @@ +package org.mte.numecoeval.referentiel.infrastructure.adapter.export; + +import org.apache.commons.csv.CSVPrinter; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.mte.numecoeval.referentiel.domain.ports.input.impl.ImportHypothesePortImpl; +import org.mte.numecoeval.referentiel.factory.TestDataFactory; +import org.mte.numecoeval.referentiel.infrastructure.jpa.repository.HypotheseRepository; + +import java.io.IOException; +import java.io.StringWriter; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.when; + +class HypotheseCsvExportServiceTest { + + @InjectMocks + HypotheseCsvExportService exportService; + + @Mock + HypotheseRepository repository; + + @Mock + CSVPrinter csvPrinter; + + @BeforeEach + void setup() { + MockitoAnnotations.openMocks(this); + } + + @Test + void getHeadersShouldReturnSameHeadersAsImport() { + assertEquals(ImportHypothesePortImpl.getHeaders(), exportService.getHeaders()); + } + + @Test + void getObjectsToWriteShouldReturnRepositoryFindAll() { + var entities = List.of( + TestDataFactory.HypotheseFactory.entity("clé", "0.0","Source"), + TestDataFactory.HypotheseFactory.entity("clé2", "1.0","Source") + ); + when(repository.findAll()).thenReturn(entities); + + assertEquals(entities, exportService.getObjectsToWrite()); + } + + @Test + void printRecordShouldUseEntityAttributes() throws IOException { + var entity = TestDataFactory.HypotheseFactory.entity("clé", "0.0","Source"); + + assertDoesNotThrow(() -> exportService.printRecord(csvPrinter, entity)); + + Mockito.verify(csvPrinter, times(1)).printRecord(entity.getCode(), entity.getValeur(), entity.getSource()); + } + + @Test + void logRecordErrorShouldLogSpecificErrorForRecord(){ + var entity = TestDataFactory.HypotheseFactory.entity("clé", "0.0","Source"); + + assertDoesNotThrow(() -> exportService.logRecordError(entity, new Exception("Test"))); + } + + @Test + void logRecordErrorShouldLogGenericErrorForRecord(){ + assertDoesNotThrow(() -> exportService.logRecordError(null, new Exception("Test"))); + } + + @Test + void logRecordErrorShouldLogGenericErrorForFile(){ + assertDoesNotThrow(() -> exportService.logWriterError(new Exception("Test"))); + } + + @Test + void writeToCsvShouldReturnCSV() { + // given + var entities = List.of( + TestDataFactory.HypotheseFactory.entity("clé", "0.0","Source") + ); + when(repository.findAll()).thenReturn(entities); + StringWriter stringWriter = new StringWriter(); + + // when + exportService.writeToCsv(stringWriter); + + // Then + String result = stringWriter.toString(); + assertEquals( + "cle;valeur;source\r\nclé;0.0;Source\r\n", + result + ); + } +} diff --git a/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/adapter/export/ImpactEquipementCsvExportServiceTest.java b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/adapter/export/ImpactEquipementCsvExportServiceTest.java new file mode 100644 index 00000000..d33419aa --- /dev/null +++ b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/adapter/export/ImpactEquipementCsvExportServiceTest.java @@ -0,0 +1,128 @@ +package org.mte.numecoeval.referentiel.infrastructure.adapter.export; + +import org.apache.commons.csv.CSVPrinter; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.mte.numecoeval.referentiel.domain.ports.input.impl.ImportImpactEquipementPortImpl; +import org.mte.numecoeval.referentiel.factory.TestDataFactory; +import org.mte.numecoeval.referentiel.infrastructure.jpa.repository.ImpactEquipementRepository; + +import java.io.IOException; +import java.io.StringWriter; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.util.List; +import java.util.Locale; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.when; + +class ImpactEquipementCsvExportServiceTest { + + @InjectMocks + ImpactEquipementCsvExportService exportService; + + @Mock + ImpactEquipementRepository repository; + + @Mock + CSVPrinter csvPrinter; + + @BeforeEach + void setup() { + MockitoAnnotations.openMocks(this); + } + + @Test + void getHeadersShouldReturnSameHeadersAsImport() { + assertEquals(ImportImpactEquipementPortImpl.getHeaders(), exportService.getHeaders()); + } + + @Test + void getObjectsToWriteShouldReturnRepositoryFindAll() { + var entities = List.of( + TestDataFactory.ImpactEquipementFactory.entity( + TestDataFactory.EtapeFactory.entity("UTILISATION", ""), + TestDataFactory.CritereFactory.entity("Changement climatique", "", ""), + "Ref-Ecran", "RefTest", "Ecran", 0.1, 0.2 + ), + TestDataFactory.ImpactEquipementFactory.entity( + TestDataFactory.EtapeFactory.entity("DISTRIBUTION", ""), + TestDataFactory.CritereFactory.entity("Changement climatique", "", ""), + "Ref-Ecran", "RefTest", "Ecran", 0.01, 0.002 + ) + ); + when(repository.findAll()).thenReturn(entities); + + assertEquals(entities, exportService.getObjectsToWrite()); + } + + @Test + void printRecordShouldUseEntityAttributes() throws IOException { + var entity = TestDataFactory.ImpactEquipementFactory.entity( + TestDataFactory.EtapeFactory.entity("UTILISATION", ""), + TestDataFactory.CritereFactory.entity("Changement climatique", "", ""), + "Ref-Ecran", "RefTest", "Ecran", 0.1, 0.2 + ); + DecimalFormat df = new DecimalFormat("0", DecimalFormatSymbols.getInstance(Locale.ENGLISH)); + df.setMaximumFractionDigits(340); + + assertDoesNotThrow(() -> exportService.printRecord(csvPrinter, entity)); + + Mockito.verify(csvPrinter, times(1)).printRecord(entity.getRefEquipement(), + entity.getEtape(), entity.getCritere(), + df.format(entity.getConsoElecMoyenne()), df.format(entity.getValeur()), + entity.getSource(), entity.getType()); + } + + @Test + void logRecordErrorShouldLogSpecificErrorForRecord() { + var entity = TestDataFactory.ImpactEquipementFactory.entity( + TestDataFactory.EtapeFactory.entity("UTILISATION", ""), + TestDataFactory.CritereFactory.entity("Changement climatique", "", ""), + "Ref-Ecran", "RefTest", "Ecran", 0.1, 0.2 + ); + + assertDoesNotThrow(() -> exportService.logRecordError(entity, new Exception("Test"))); + } + + @Test + void logRecordErrorShouldLogGenericErrorForRecord() { + assertDoesNotThrow(() -> exportService.logRecordError(null, new Exception("Test"))); + } + + @Test + void logRecordErrorShouldLogGenericErrorForFile() { + assertDoesNotThrow(() -> exportService.logWriterError(new Exception("Test"))); + } + + @Test + void writeToCsvShouldReturnCSV() { + // given + var entities = List.of( + TestDataFactory.ImpactEquipementFactory.entity( + TestDataFactory.EtapeFactory.entity("UTILISATION", ""), + TestDataFactory.CritereFactory.entity("Changement climatique", "", ""), + "Ref-Ecran", "RefTest", "Ecran", 0.1, 0.2 + ) + ); + when(repository.findAll()).thenReturn(entities); + StringWriter stringWriter = new StringWriter(); + + // when + exportService.writeToCsv(stringWriter); + + // Then + String result = stringWriter.toString(); + assertEquals( + "refEquipement;etapeacv;critere;consoElecMoyenne;valeur;source;type\r\nRef-Ecran;UTILISATION;Changement climatique;0.2;0.1;RefTest;Ecran\r\n", + result + ); + } +} diff --git a/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/adapter/export/ImpactMessagerieCsvExportServiceTest.java b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/adapter/export/ImpactMessagerieCsvExportServiceTest.java new file mode 100644 index 00000000..843d2d3d --- /dev/null +++ b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/adapter/export/ImpactMessagerieCsvExportServiceTest.java @@ -0,0 +1,124 @@ +package org.mte.numecoeval.referentiel.infrastructure.adapter.export; + +import org.apache.commons.csv.CSVPrinter; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.mte.numecoeval.referentiel.domain.ports.input.impl.ImportImpactMessageriePortImpl; +import org.mte.numecoeval.referentiel.factory.TestDataFactory; +import org.mte.numecoeval.referentiel.infrastructure.jpa.repository.ImpactMessagerieRepository; + +import java.io.IOException; +import java.io.StringWriter; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.util.List; +import java.util.Locale; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.when; + +class ImpactMessagerieCsvExportServiceTest { + + @InjectMocks + ImpactMessagerieCsvExportService exportService; + + @Mock + ImpactMessagerieRepository repository; + + @Mock + CSVPrinter csvPrinter; + + @BeforeEach + void setup() { + MockitoAnnotations.openMocks(this); + } + + @Test + void getHeadersShouldReturnSameHeadersAsImport() { + assertEquals(ImportImpactMessageriePortImpl.getHeaders(), exportService.getHeaders()); + } + + @Test + void getObjectsToWriteShouldReturnRepositoryFindAll() { + var entities = List.of( + TestDataFactory.ImpactMessagerieFactory.entity( + TestDataFactory.CritereFactory.entity("Changement climatique", "",""), + 0.2, 0.0005, "Test" + ), + TestDataFactory.ImpactMessagerieFactory.entity( + TestDataFactory.CritereFactory.entity("Acidification", "",""), + 0.2, 0.0005, "Test" + ) + ); + when(repository.findAll()).thenReturn(entities); + + assertEquals(entities, exportService.getObjectsToWrite()); + } + + @Test + void printRecordShouldUseEntityAttributes() throws IOException { + var entity = TestDataFactory.ImpactMessagerieFactory.entity( + TestDataFactory.CritereFactory.entity("Changement climatique", "",""), + 0.2, 0.0005, "Test" + ); + DecimalFormat df = new DecimalFormat("0", DecimalFormatSymbols.getInstance(Locale.ENGLISH)); + df.setMaximumFractionDigits(340); + + assertDoesNotThrow(() -> exportService.printRecord(csvPrinter, entity)); + + Mockito.verify(csvPrinter, times(1)).printRecord( + entity.getCritere(), + df.format(entity.getConstanteCoefficientDirecteur()), + df.format(entity.getConstanteOrdonneeOrigine()), + entity.getSource()); + } + + @Test + void logRecordErrorShouldLogSpecificErrorForRecord(){ + var entity = TestDataFactory.ImpactMessagerieFactory.entity( + TestDataFactory.CritereFactory.entity("Changement climatique", "",""), + 0.2, 0.0005, "Test" + ); + + assertDoesNotThrow(() -> exportService.logRecordError(entity, new Exception("Test"))); + } + + @Test + void logRecordErrorShouldLogGenericErrorForRecord(){ + assertDoesNotThrow(() -> exportService.logRecordError(null, new Exception("Test"))); + } + + @Test + void logRecordErrorShouldLogGenericErrorForFile(){ + assertDoesNotThrow(() -> exportService.logWriterError(new Exception("Test"))); + } + + @Test + void writeToCsvShouldReturnCSV() { + // given + var entities = List.of( + TestDataFactory.ImpactMessagerieFactory.entity( + TestDataFactory.CritereFactory.entity("Changement climatique", "",""), + 0.2, 0.0005, "Test" + ) + ); + when(repository.findAll()).thenReturn(entities); + StringWriter stringWriter = new StringWriter(); + + // when + exportService.writeToCsv(stringWriter); + + // Then + String result = stringWriter.toString(); + assertEquals( + "critere;constanteCoefficientDirecteur;constanteOrdonneeOrigine;source\r\nChangement climatique;0.2;0.0005;Test\r\n", + result + ); + } +} diff --git a/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/adapter/export/ImpactMixElectriqueCsvExportServiceTest.java b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/adapter/export/ImpactMixElectriqueCsvExportServiceTest.java new file mode 100644 index 00000000..212d0da3 --- /dev/null +++ b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/adapter/export/ImpactMixElectriqueCsvExportServiceTest.java @@ -0,0 +1,126 @@ +package org.mte.numecoeval.referentiel.infrastructure.adapter.export; + +import org.apache.commons.csv.CSVPrinter; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.mte.numecoeval.referentiel.domain.ports.input.impl.ImportMixElectriquePortImpl; +import org.mte.numecoeval.referentiel.factory.TestDataFactory; +import org.mte.numecoeval.referentiel.infrastructure.jpa.repository.MixElectriqueRepository; + +import java.io.IOException; +import java.io.StringWriter; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.util.List; +import java.util.Locale; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.when; + +class ImpactMixElectriqueCsvExportServiceTest { + + @InjectMocks + MixElectriqueCsvExportService exportService; + + @Mock + MixElectriqueRepository repository; + + @Mock + CSVPrinter csvPrinter; + + @BeforeEach + void setup() { + MockitoAnnotations.openMocks(this); + } + + @Test + void getHeadersShouldReturnSameHeadersAsImport() { + Assertions.assertEquals(ImportMixElectriquePortImpl.getHeaders(), exportService.getHeaders()); + } + + @Test + void getObjectsToWriteShouldReturnRepositoryFindAll() { + var entities = List.of( + TestDataFactory.MixElectriqueFactory.entity( + TestDataFactory.CritereFactory.entity("Changement climatique", "",""), + "France", "FR",0.2, "Test" + ), + TestDataFactory.MixElectriqueFactory.entity( + TestDataFactory.CritereFactory.entity("Acidification", "",""), + "Angleterre", "EN",0.1, "Test" + ) + ); + when(repository.findAll()).thenReturn(entities); + + assertEquals(entities, exportService.getObjectsToWrite()); + } + + @Test + void printRecordShouldUseEntityAttributes() throws IOException { + var entity = TestDataFactory.MixElectriqueFactory.entity( + TestDataFactory.CritereFactory.entity("Changement climatique", "",""), + "France", "FR",0.2, "Test" + ); + DecimalFormat df = new DecimalFormat("0", DecimalFormatSymbols.getInstance(Locale.ENGLISH)); + df.setMaximumFractionDigits(340); + + assertDoesNotThrow(() -> exportService.printRecord(csvPrinter, entity)); + + Mockito.verify(csvPrinter, times(1)).printRecord( + entity.getPays(), + entity.getRaccourcisAnglais(), + entity.getCritere(), + df.format(entity.getValeur()), + entity.getSource()); + } + + @Test + void logRecordErrorShouldLogSpecificErrorForRecord(){ + var entity = TestDataFactory.MixElectriqueFactory.entity( + TestDataFactory.CritereFactory.entity("Changement climatique", "",""), + "France", "FR",0.2, "Test" + ); + + assertDoesNotThrow(() -> exportService.logRecordError(entity, new Exception("Test"))); + } + + @Test + void logRecordErrorShouldLogGenericErrorForRecord(){ + assertDoesNotThrow(() -> exportService.logRecordError(null, new Exception("Test"))); + } + + @Test + void logRecordErrorShouldLogGenericErrorForFile(){ + assertDoesNotThrow(() -> exportService.logWriterError(new Exception("Test"))); + } + + @Test + void writeToCsvShouldReturnCSV() { + // given + var entities = List.of( + TestDataFactory.MixElectriqueFactory.entity( + TestDataFactory.CritereFactory.entity("Changement climatique", "",""), + "France", "FR",0.2, "Test" + ) + ); + when(repository.findAll()).thenReturn(entities); + StringWriter stringWriter = new StringWriter(); + + // when + exportService.writeToCsv(stringWriter); + + // Then + String result = stringWriter.toString(); + assertEquals( + "pays;raccourcisAnglais;critere;valeur;source\r\nFrance;FR;Changement climatique;0.2;Test\r\n", + result + ); + } +} diff --git a/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/adapter/export/ImpactReseauCsvExportServiceTest.java b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/adapter/export/ImpactReseauCsvExportServiceTest.java new file mode 100644 index 00000000..3eeee785 --- /dev/null +++ b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/adapter/export/ImpactReseauCsvExportServiceTest.java @@ -0,0 +1,128 @@ +package org.mte.numecoeval.referentiel.infrastructure.adapter.export; + +import org.apache.commons.csv.CSVPrinter; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.mte.numecoeval.referentiel.domain.ports.input.impl.ImportImpactReseauPortImpl; +import org.mte.numecoeval.referentiel.factory.TestDataFactory; +import org.mte.numecoeval.referentiel.infrastructure.jpa.repository.ImpactReseauRepository; + +import java.io.IOException; +import java.io.StringWriter; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.util.List; +import java.util.Locale; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.when; + +class ImpactReseauCsvExportServiceTest { + + @InjectMocks + ImpactReseauCsvExportService exportService; + + @Mock + ImpactReseauRepository repository; + + @Mock + CSVPrinter csvPrinter; + + @BeforeEach + void setup() { + MockitoAnnotations.openMocks(this); + } + + @Test + void getHeadersShouldReturnSameHeadersAsImport() { + assertEquals(ImportImpactReseauPortImpl.getHeaders(), exportService.getHeaders()); + } + + @Test + void getObjectsToWriteShouldReturnRepositoryFindAll() { + var entities = List.of( + TestDataFactory.ImpactReseauFactory.entity( + TestDataFactory.EtapeFactory.entity("UTILISATION", ""), + TestDataFactory.CritereFactory.entity("Changement climatique", "", ""), + "Ref-Ecran", "RefTest", 0.1, 0.2 + ), + TestDataFactory.ImpactReseauFactory.entity( + TestDataFactory.EtapeFactory.entity("DISTRIBUTION", ""), + TestDataFactory.CritereFactory.entity("Changement climatique", "", ""), + "Ref-Ecran", "RefTest", 0.01, 0.002 + ) + ); + when(repository.findAll()).thenReturn(entities); + + assertEquals(entities, exportService.getObjectsToWrite()); + } + + @Test + void printRecordShouldUseEntityAttributes() throws IOException { + var entity = TestDataFactory.ImpactReseauFactory.entity( + TestDataFactory.EtapeFactory.entity("UTILISATION", ""), + TestDataFactory.CritereFactory.entity("Changement climatique", "", ""), + "Ref-Ecran", "RefTest", 0.1, 0.2 + ); + DecimalFormat df = new DecimalFormat("0", DecimalFormatSymbols.getInstance(Locale.ENGLISH)); + df.setMaximumFractionDigits(340); + + assertDoesNotThrow(() -> exportService.printRecord(csvPrinter, entity)); + + Mockito.verify(csvPrinter, times(1)).printRecord(entity.getRefReseau(), + entity.getEtape(), entity.getCritere(), + df.format(entity.getValeur()), df.format(entity.getConsoElecMoyenne()), + entity.getSource()); + } + + @Test + void logRecordErrorShouldLogSpecificErrorForRecord() { + var entity = TestDataFactory.ImpactReseauFactory.entity( + TestDataFactory.EtapeFactory.entity("UTILISATION", ""), + TestDataFactory.CritereFactory.entity("Changement climatique", "", ""), + "Ref-Ecran", "RefTest", 0.1, 0.2 + ); + + assertDoesNotThrow(() -> exportService.logRecordError(entity, new Exception("Test"))); + } + + @Test + void logRecordErrorShouldLogGenericErrorForRecord() { + assertDoesNotThrow(() -> exportService.logRecordError(null, new Exception("Test"))); + } + + @Test + void logRecordErrorShouldLogGenericErrorForFile() { + assertDoesNotThrow(() -> exportService.logWriterError(new Exception("Test"))); + } + + @Test + void writeToCsvShouldReturnCSV() { + // given + var entities = List.of( + TestDataFactory.ImpactReseauFactory.entity( + TestDataFactory.EtapeFactory.entity("UTILISATION", ""), + TestDataFactory.CritereFactory.entity("Changement climatique", "", ""), + "Ref-Ecran", "RefTest", 0.1, 0.2 + ) + ); + when(repository.findAll()).thenReturn(entities); + StringWriter stringWriter = new StringWriter(); + + // when + exportService.writeToCsv(stringWriter); + + // Then + String result = stringWriter.toString(); + assertEquals( + "refReseau;etapeACV;critere;valeur;consoElecMoyenne;source\r\nRef-Ecran;UTILISATION;Changement climatique;0.1;0.2;RefTest\r\n", + result + ); + } +} diff --git a/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/adapter/export/ReferentielCsvExportServiceTest.java b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/adapter/export/ReferentielCsvExportServiceTest.java new file mode 100644 index 00000000..e6ac4cf8 --- /dev/null +++ b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/adapter/export/ReferentielCsvExportServiceTest.java @@ -0,0 +1,158 @@ +package org.mte.numecoeval.referentiel.infrastructure.adapter.export; + +import lombok.Getter; +import org.apache.commons.csv.CSVPrinter; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockitoAnnotations; +import org.mte.numecoeval.referentiel.domain.ports.output.ReferentielCsvExportService; +import org.mte.numecoeval.referentiel.factory.TestDataFactory; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.EtapeEntity; + +import java.io.IOException; +import java.io.StringWriter; +import java.io.Writer; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class ReferentielCsvExportServiceTest { + + @BeforeEach + void setup() { + MockitoAnnotations.openMocks(this); + } + + @Test + void writeToCsvShouldLogGenericFileError() { + // given + var list = new ArrayList<EtapeEntity>(); + list.add(TestDataFactory.EtapeFactory.entity("UTILISATION", "Utilisation")); + list.add(null); + var exportService = new TestReferentielCsvExportService(new String[]{"code"}, list); + var faultyWriter = new Writer() { + @Override + public void write(char[] cbuf, int off, int len) throws IOException { + // Do nothing + } + + @Override + public void flush() throws IOException { + // Do nothing + } + + @Override + public void close() throws IOException { + throw new IOException("Exception de test"); + } + }; + + // When + assertDoesNotThrow(() -> exportService.writeToCsv(faultyWriter)); + + // Then + assertTrue(exportService.isLogFileErrorCalled()); + assertNotNull(exportService.getLogFileException()); + assertEquals("Exception de test", exportService.getLogFileException().getMessage()); + } + + @Test + void writeToCsvShouldLogGenericRecordErrorAndProducesCSVWithOnlyHeaders() { + // given + var list = new ArrayList<EtapeEntity>(); + list.add(null); + var exportService = new TestReferentielCsvExportService(new String[]{"code"}, list); + var stringWriter = new StringWriter(); + + // When + assertDoesNotThrow(() -> exportService.writeToCsv(stringWriter)); + + // Then + assertTrue(exportService.isLogRecordErrorCalled()); + assertNotNull(exportService.getLogRecordException()); + assertEquals(NullPointerException.class, exportService.getLogRecordException().getClass()); + String result = stringWriter.toString(); + assertEquals( + "code\r\n", + result + ); + } + + @Test + void writeToCsvGivenEmptyListShouldProducesCSVWithOnlyHeaders() { + // given + var list = new ArrayList<EtapeEntity>(); + var exportService = new TestReferentielCsvExportService(new String[]{"code"}, list); + var stringWriter = new StringWriter(); + + // When + assertDoesNotThrow(() -> exportService.writeToCsv(stringWriter)); + + // Then + assertFalse(exportService.isLogRecordErrorCalled()); + assertNull(exportService.getLogRecordException()); + String result = stringWriter.toString(); + assertEquals( + "code\r\n", + result + ); + } + + @Getter + static + class TestReferentielCsvExportService extends ReferentielCsvExportService<EtapeEntity> { + + boolean logRecordErrorCalled = false; + + Exception logRecordException = null; + + boolean logFileErrorCalled = false; + + Exception logFileException = null; + + String[] headers; + + List<EtapeEntity> records; + + public TestReferentielCsvExportService(String[] headers, List<EtapeEntity> records) { + super(EtapeEntity.class); + this.headers = headers; + this.records = records; + } + + @Override + public String[] getHeaders() { + return headers; + } + + @Override + protected String getObjectId(EtapeEntity object) { + return object.getCode(); + } + + @Override + public void printRecord(CSVPrinter csvPrinter, EtapeEntity objectToWrite) throws IOException { + csvPrinter.printRecord(objectToWrite.getCode()); + } + + @Override + public List<EtapeEntity> getObjectsToWrite() { + return records; + } + + @Override + public void logRecordError(EtapeEntity objectToWrite, Exception exception) { + super.logRecordError(objectToWrite, exception); + logRecordErrorCalled = true; + logRecordException = exception; + } + + @Override + public void logWriterError(Exception exception) { + super.logWriterError(exception); + logFileErrorCalled = true; + logFileException = exception; + } + } +} diff --git a/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/adapter/export/TypeEquipementCsvExportServiceTest.java b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/adapter/export/TypeEquipementCsvExportServiceTest.java new file mode 100644 index 00000000..2433525a --- /dev/null +++ b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/adapter/export/TypeEquipementCsvExportServiceTest.java @@ -0,0 +1,122 @@ +package org.mte.numecoeval.referentiel.infrastructure.adapter.export; + +import org.apache.commons.csv.CSVPrinter; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.mte.numecoeval.referentiel.domain.ports.input.impl.ImportTypeEquipementPortImpl; +import org.mte.numecoeval.referentiel.factory.TestDataFactory; +import org.mte.numecoeval.referentiel.infrastructure.jpa.repository.TypeEquipementRepository; + +import java.io.IOException; +import java.io.StringWriter; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.util.List; +import java.util.Locale; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.when; + +class TypeEquipementCsvExportServiceTest { + + @InjectMocks + TypeEquipementCsvExportService exportService; + + @Mock + TypeEquipementRepository repository; + + @Mock + CSVPrinter csvPrinter; + + @BeforeEach + void setup() { + MockitoAnnotations.openMocks(this); + } + + @Test + void getHeadersShouldReturnSameHeadersAsImport() { + Assertions.assertEquals(ImportTypeEquipementPortImpl.getHeaders(), exportService.getHeaders()); + } + + @Test + void getObjectsToWriteShouldReturnRepositoryFindAll() { + var entities = List.of( + TestDataFactory.TypeEquipementFactory.entity( + "Serveur", true, 6.0, "", "Test", + "test"), + TestDataFactory.TypeEquipementFactory.entity( + "Ecran", false, 4.0, "", "Test", + "test") + ); + when(repository.findAll()).thenReturn(entities); + + assertEquals(entities, exportService.getObjectsToWrite()); + } + + @Test + void printRecordShouldUseEntityAttributes() throws IOException { + var entity = TestDataFactory.TypeEquipementFactory.entity( + "Serveur", true, 6.0, "", "Test", + "test"); + DecimalFormat df = new DecimalFormat("0", DecimalFormatSymbols.getInstance(Locale.ENGLISH)); + df.setMaximumFractionDigits(340); + + assertDoesNotThrow(() -> exportService.printRecord(csvPrinter, entity)); + + Mockito.verify(csvPrinter, times(1)).printRecord( + entity.getType(), + entity.isServeur(), + entity.getCommentaire(), + df.format(entity.getDureeVieDefaut()), + entity.getSource(), + entity.getRefEquipementParDefaut()); + } + + @Test + void logRecordErrorShouldLogSpecificErrorForRecord() { + var entity = TestDataFactory.TypeEquipementFactory.entity( + "Serveur", true, 6.0, "", "Test", + "test"); + + assertDoesNotThrow(() -> exportService.logRecordError(entity, new Exception("Test"))); + } + + @Test + void logRecordErrorShouldLogGenericErrorForRecord() { + assertDoesNotThrow(() -> exportService.logRecordError(null, new Exception("Test"))); + } + + @Test + void logRecordErrorShouldLogGenericErrorForFile() { + assertDoesNotThrow(() -> exportService.logWriterError(new Exception("Test"))); + } + + @Test + void writeToCsvShouldReturnCSV() { + // given + var entities = List.of( + TestDataFactory.TypeEquipementFactory.entity( + "Serveur", true, 6.0, "Commentaires", "Test", + "test") + ); + when(repository.findAll()).thenReturn(entities); + StringWriter stringWriter = new StringWriter(); + + // when + exportService.writeToCsv(stringWriter); + + // Then + String result = stringWriter.toString(); + assertEquals( + "type;serveur;commentaire;dureeVieDefaut;source;refEquipementParDefaut\r\nServeur;true;Commentaires;6;Test;test\r\n", + result + ); + } +} diff --git a/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/configuration/openapi/ReferentielOpenApiConfigTest.java b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/configuration/openapi/ReferentielOpenApiConfigTest.java new file mode 100644 index 00000000..62cddece --- /dev/null +++ b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/configuration/openapi/ReferentielOpenApiConfigTest.java @@ -0,0 +1,55 @@ +package org.mte.numecoeval.referentiel.infrastructure.configuration.openapi; + +import io.swagger.v3.oas.models.ExternalDocumentation; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.SpecVersion; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.info.License; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +class ReferentielOpenApiConfigTest { + @Test + void testConfigOpenAPI() { + OpenAPI actualConfigOpenAPIResult = (new ReferentielOpenApiConfig()).configOpenAPI(); + assertNull(actualConfigOpenAPIResult.getComponents()); + assertNull(actualConfigOpenAPIResult.getWebhooks()); + assertNull(actualConfigOpenAPIResult.getTags()); + assertEquals(SpecVersion.V30, actualConfigOpenAPIResult.getSpecVersion()); + assertNull(actualConfigOpenAPIResult.getServers()); + assertNull(actualConfigOpenAPIResult.getSecurity()); + assertNull(actualConfigOpenAPIResult.getPaths()); + assertEquals("3.0.1", actualConfigOpenAPIResult.getOpenapi()); + assertNull(actualConfigOpenAPIResult.getExtensions()); + ExternalDocumentation externalDocs = actualConfigOpenAPIResult.getExternalDocs(); + assertEquals("https://gitlab-forge.din.developpement-durable.gouv.fr/pub/numeco/m4g/docs", externalDocs.getUrl()); + assertNull(externalDocs.getExtensions()); + assertEquals("NumEcoEval Documentation", externalDocs.getDescription()); + Info info = actualConfigOpenAPIResult.getInfo(); + assertEquals("API des référentiels de NumEcoEval", info.getTitle()); + assertNull(info.getTermsOfService()); + assertNull(info.getSummary()); + assertNull(info.getExtensions()); + assertEquals("v0.0.1", info.getVersion()); + assertEquals(""" + Endpoints permettant de manipuler les référentiels de NumEcoEval. + Les endpoints CRUD sont générés via Spring DataRest. + + Les endpoints d'export CSV permettent d'exporter l'intégralité d'un référentiel + sous forme de fichier CSV ré-importable via les endpoints d'imports. + + Les endpoints d'import fonctionnent en annule & remplace et supprimeront l'intégralité + du référentiel et utiliseront le contenu du CSV pour peupler le référentiel. + + Les endpoints internes sont utilisés par les différents modules de NumEcoEval. + """, + info.getDescription()); + assertNull(info.getContact()); + License license = info.getLicense(); + assertEquals("https://gitlab-forge.din.developpement-durable.gouv.fr/pub/numeco/m4g/api-referentiel/-/blob/main/LICENSE.txt", license.getUrl()); + assertEquals("Apache 2.0", license.getName()); + } +} + diff --git a/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/configuration/security/SecurityConfigTest.java b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/configuration/security/SecurityConfigTest.java new file mode 100644 index 00000000..03a7d637 --- /dev/null +++ b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/configuration/security/SecurityConfigTest.java @@ -0,0 +1,34 @@ +package org.mte.numecoeval.referentiel.infrastructure.configuration.security; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.test.context.ActiveProfiles; + +import java.util.HashMap; + +import static org.mockito.Mockito.mock; + +@SpringBootTest +@ActiveProfiles(profiles = {"test"}) +class SecurityConfigTest { + @Autowired + private SecurityConfig securityConfig; + + @Test + void testGlobalFilterChain() throws Exception { + ObjectPostProcessor<Object> opp = mock(ObjectPostProcessor.class); + AuthenticationProvider provider = mock(AuthenticationProvider.class); + AuthenticationManagerBuilder builder = new AuthenticationManagerBuilder(opp); + AuthenticationManagerBuilder authenticationBuilder = new AuthenticationManagerBuilder(opp); + securityConfig.globalFilterChain(new HttpSecurity(opp, authenticationBuilder, new HashMap<>())); + Assertions.assertNotNull(securityConfig); + + } +} + diff --git a/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/csv/ImportCSVReferentielPortTest.java b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/csv/ImportCSVReferentielPortTest.java new file mode 100644 index 00000000..1b1993fd --- /dev/null +++ b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/csv/ImportCSVReferentielPortTest.java @@ -0,0 +1,68 @@ +package org.mte.numecoeval.referentiel.infrastructure.csv; + +import org.apache.commons.csv.CSVRecord; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.mockito.MockitoAnnotations; +import org.mte.numecoeval.referentiel.domain.data.ResultatImport; +import org.mte.numecoeval.referentiel.domain.exception.ReferentielException; +import org.mte.numecoeval.referentiel.domain.ports.input.ImportCSVReferentielPort; + +import java.io.InputStream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class ImportCSVReferentielPortTest { + + CSVRecord csvRecord; + ImportCSVReferentielPort csvReferentielPort = new ImportCSVReferentielPort() { + @Override + public void checkCSVRecord(CSVRecord csvRecord) throws ReferentielException { + // Nothing to do + } + + @Override + public ResultatImport importCSV(InputStream csvInputStream) { + return new ResultatImport<>(); + } + }; + + @BeforeEach + void setup() { + MockitoAnnotations.openMocks(this); + csvRecord = mock(CSVRecord.class); + } + + @Test + void getDoubleValue_shouldReturnRecordValue() { + when(csvRecord.get("doubleValue")).thenReturn("1.6"); + + Double value = csvReferentielPort.getDoubleValueFromRecord(csvRecord, "doubleValue", null); + + assertEquals(1.6, value); + } + + @Test + void getDoubleValue_givenUntrimmedValue_shouldReturnRecordValue() { + when(csvRecord.get("doubleValue")).thenReturn(" 97.58 "); + + Double value = csvReferentielPort.getDoubleValueFromRecord(csvRecord, "doubleValue", null); + + assertEquals(97.58, value); + } + + @ParameterizedTest + @NullAndEmptySource + void getDoubleValue_shouldReturnDefaultValue(String nullOrEmptyValue) { + when(csvRecord.get("doubleValue")).thenReturn(nullOrEmptyValue); + + Double value = csvReferentielPort.getDoubleValueFromRecord(csvRecord, "doubleValue", 0.0); + + assertEquals(0.0, value); + } +} + diff --git a/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/jpa/CorrespondanceRefEquipementJpaAdapterTest.java b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/jpa/CorrespondanceRefEquipementJpaAdapterTest.java new file mode 100644 index 00000000..3d9f08d2 --- /dev/null +++ b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/jpa/CorrespondanceRefEquipementJpaAdapterTest.java @@ -0,0 +1,128 @@ +package org.mte.numecoeval.referentiel.infrastructure.jpa; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullSource; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.mte.numecoeval.referentiel.domain.exception.ReferentielException; +import org.mte.numecoeval.referentiel.domain.model.CorrespondanceRefEquipement; +import org.mte.numecoeval.referentiel.infrastructure.jpa.adapter.CorrespondanceRefEquipementJpaAdapter; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.CorrespondanceRefEquipementEntity; +import org.mte.numecoeval.referentiel.infrastructure.jpa.repository.CorrespondanceRefEquipementRepository; +import org.mte.numecoeval.referentiel.infrastructure.mapper.CorrespondanceRefEquipementMapper; +import org.mte.numecoeval.referentiel.infrastructure.mapper.CorrespondanceRefEquipementMapperImpl; +import org.springframework.test.util.ReflectionTestUtils; + +import java.util.Collections; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; + +class CorrespondanceRefEquipementJpaAdapterTest { + + @InjectMocks + private CorrespondanceRefEquipementJpaAdapter jpaAdapter; + + @Mock + CorrespondanceRefEquipementRepository hypotheseRepository; + + CorrespondanceRefEquipementMapper mapper = new CorrespondanceRefEquipementMapperImpl(); + + @BeforeEach + void setup() { + MockitoAnnotations.openMocks(this); + ReflectionTestUtils.setField(jpaAdapter, "mapper", mapper); + } + + @Test + void get_shouldReturnDomain() { + var expectedEntity = new CorrespondanceRefEquipementEntity() + .setModeleEquipementSource("source") + .setRefEquipementCible("cible"); + var wantedId = "source"; + + Mockito.when(hypotheseRepository.findById(wantedId)).thenReturn(Optional.of(expectedEntity)); + + var expectedDomain = assertDoesNotThrow( () -> jpaAdapter.get(wantedId) ); + + assertEquals(expectedEntity.getModeleEquipementSource(), expectedDomain.getModeleEquipementSource()); + assertEquals(expectedEntity.getRefEquipementCible(), expectedDomain.getRefEquipementCible()); + } + + @Test + void get_shouldThrowException() { + var wantedId = "notExisting"; + + Mockito.when(hypotheseRepository.findById(wantedId)).thenReturn(Optional.empty()); + + ReferentielException expectedException = assertThrows(ReferentielException.class, () -> jpaAdapter.get(wantedId) ); + + assertEquals("Correspondance au RefEquipement "+ wantedId +" non trouvé",expectedException.getMessage()); + } + + @Test + void get_withNullValueShouldThrowException() { + String wantedId = null; + + Mockito.when(hypotheseRepository.findById(wantedId)).thenReturn(Optional.empty()); + + ReferentielException expectedException = assertThrows(ReferentielException.class, () -> jpaAdapter.get(wantedId) ); + + assertEquals("Correspondance au RefEquipement (id null) non trouvé",expectedException.getMessage()); + } + + @Test + void purge_shouldCallDeleteAll() { + jpaAdapter.purge(); + + Mockito.verify(hypotheseRepository, Mockito.times(1)).deleteAll(); + } + + @Test + void getAll_shouldCallfindAll() { + jpaAdapter.getAll(); + + Mockito.verify(hypotheseRepository, Mockito.times(1)).findAll(); + } + + @Test + void saveAll_shouldCallsaveAll() { + var domainToSave = CorrespondanceRefEquipement.builder() + .modeleEquipementSource("source") + .refEquipementCible("cible") + .build(); + var entityToSave = mapper.toEntity(domainToSave); + + assertDoesNotThrow(() -> jpaAdapter.saveAll(Collections.singletonList(domainToSave))); + + Mockito.verify(hypotheseRepository, Mockito.times(1)).saveAll(Collections.singletonList(entityToSave)); + } + + @Test + void save_shouldSaveAndReturnDomain() { + var wantedDomain = CorrespondanceRefEquipement.builder() + .modeleEquipementSource("source") + .refEquipementCible("cible") + .build(); + var expectedEntity = mapper.toEntities(Collections.singletonList(wantedDomain)).get(0); + + Mockito.when(hypotheseRepository.save(expectedEntity)).thenReturn(expectedEntity); + + var expectedDomain = assertDoesNotThrow( () -> jpaAdapter.save(wantedDomain) ); + + assertEquals(expectedEntity.getModeleEquipementSource(), expectedDomain.getModeleEquipementSource()); + assertEquals(expectedEntity.getRefEquipementCible(), expectedDomain.getRefEquipementCible()); + } + + @ParameterizedTest + @NullSource + void save_shouldSaveAndReturnNull(CorrespondanceRefEquipement nullValue) { + var expectedDomain = assertDoesNotThrow( () -> jpaAdapter.save(nullValue) ); + + assertNull(expectedDomain); + } +} diff --git a/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/jpa/CritereJpaAdapterTest.java b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/jpa/CritereJpaAdapterTest.java new file mode 100644 index 00000000..90267150 --- /dev/null +++ b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/jpa/CritereJpaAdapterTest.java @@ -0,0 +1,170 @@ +package org.mte.numecoeval.referentiel.infrastructure.jpa; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mte.numecoeval.referentiel.domain.exception.ReferentielException; +import org.mte.numecoeval.referentiel.domain.model.Critere; +import org.mte.numecoeval.referentiel.domain.model.id.CritereId; +import org.mte.numecoeval.referentiel.infrastructure.jpa.adapter.CritereJpaAdapter; +import org.mte.numecoeval.referentiel.infrastructure.jpa.adapter.ImpactEquipementJpaAdapter; +import org.mte.numecoeval.referentiel.infrastructure.jpa.adapter.ImpactMessagerieJpaAdapter; +import org.mte.numecoeval.referentiel.infrastructure.jpa.adapter.ImpactReseauJpaAdapter; +import org.mte.numecoeval.referentiel.infrastructure.jpa.adapter.MixElectriqueJpaAdapter; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.CritereEntity; +import org.mte.numecoeval.referentiel.infrastructure.jpa.repository.CritereRepository; +import org.mte.numecoeval.referentiel.infrastructure.mapper.CritereMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ContextConfiguration(classes = {CritereJpaAdapter.class}) +@ExtendWith(SpringExtension.class) +class CritereJpaAdapterTest { + @Autowired + private CritereJpaAdapter critereJpaAdapter; + + @MockBean + private CritereMapper critereMapper; + + @MockBean + private CritereRepository critereRepository; + + @MockBean + private ImpactEquipementJpaAdapter impactEquipementJpaAdapter; + + @MockBean + private ImpactReseauJpaAdapter impactReseauJpaAdapter; + + @MockBean + private MixElectriqueJpaAdapter mixElectriqueJpaAdapter; + + @MockBean + private ImpactMessagerieJpaAdapter impactMessagerieJpaAdapter; + + /** + * Method under test: {@link CritereJpaAdapter#save(Critere)} + */ + @Test + void testSave() throws ReferentielException { + Critere critere = new Critere(); + critere.setDescription("The characteristics of someone or something"); + critere.setNomCritere("Nom Critere"); + critere.setUnite("Unite"); + assertNull(critereJpaAdapter.save(critere)); + } + + /** + * Method under test: {@link CritereJpaAdapter#save(Critere)} + */ + @Test + void testSave2() throws ReferentielException { + Critere critere = new Critere(); + critere.setDescription("The characteristics of someone or something"); + critere.setNomCritere("Nom Critere"); + critere.setUnite("Unite"); + + Critere critere1 = new Critere(); + critere1.setDescription("The characteristics of someone or something"); + critere1.setNomCritere("Nom Critere"); + critere1.setUnite("Unite"); + + Critere critere2 = new Critere(); + critere2.setDescription("The characteristics of someone or something"); + critere2.setNomCritere("Nom Critere"); + critere2.setUnite("Unite"); + Critere critere3 = mock(Critere.class); + when(critere3.setDescription((String) any())).thenReturn(critere); + when(critere3.setNomCritere((String) any())).thenReturn(critere1); + when(critere3.setUnite((String) any())).thenReturn(critere2); + critere3.setDescription("The characteristics of someone or something"); + critere3.setNomCritere("Nom Critere"); + critere3.setUnite("Unite"); + assertNull(critereJpaAdapter.save(critere3)); + verify(critere3).setDescription((String) any()); + verify(critere3).setNomCritere((String) any()); + verify(critere3).setUnite((String) any()); + } + + /** + * Method under test: {@link CritereJpaAdapter#saveAll(Collection)} + */ + @Test + void testSaveAll() throws ReferentielException { + when(critereMapper.toEntities((Collection<Critere>) any())).thenReturn(new ArrayList<>()); + when(critereRepository.saveAll((Iterable<CritereEntity>) any())).thenReturn(new ArrayList<>()); + critereJpaAdapter.saveAll(new ArrayList<>()); + verify(critereMapper).toEntities((Collection<Critere>) any()); + verify(critereRepository).saveAll((Iterable<CritereEntity>) any()); + assertTrue(critereJpaAdapter.getAll().isEmpty()); + } + + /** + * Method under test: {@link CritereJpaAdapter#get(CritereId)} + */ + @Test + void testGet() throws ReferentielException { + CritereId critereId = new CritereId(); + critereId.setNomCritere("Nom Critere"); + assertNull(critereJpaAdapter.get(critereId)); + } + + /** + * Method under test: {@link CritereJpaAdapter#get(CritereId)} + */ + @Test + void testGet2() throws ReferentielException { + CritereId critereId = new CritereId(); + critereId.setNomCritere("Nom Critere"); + CritereId critereId1 = mock(CritereId.class); + when(critereId1.setNomCritere((String) any())).thenReturn(critereId); + critereId1.setNomCritere("Nom Critere"); + assertNull(critereJpaAdapter.get(critereId1)); + verify(critereId1).setNomCritere((String) any()); + } + + /** + * Method under test: {@link CritereJpaAdapter#purge()} + */ + @Test + void testPurge() { + doNothing().when(impactReseauJpaAdapter).purge(); + doNothing().when(impactEquipementJpaAdapter).purge(); + doNothing().when(mixElectriqueJpaAdapter).purge(); + doNothing().when(impactMessagerieJpaAdapter).purge(); + doNothing().when(critereRepository).deleteAll(); + critereJpaAdapter.purge(); + + // Purge des données filles + verify(impactReseauJpaAdapter, times(0)).purge(); + verify(impactEquipementJpaAdapter, times(0)).purge(); + verify(mixElectriqueJpaAdapter, times(0)).purge(); + verify(impactMessagerieJpaAdapter, times(0)).purge(); + + verify(critereRepository).deleteAll(); + assertTrue(critereJpaAdapter.getAll().isEmpty()); + } + + /** + * Method under test: {@link CritereJpaAdapter#getAll()} + */ + @Test + void testGetAll() { + ArrayList<Critere> critereList = new ArrayList<>(); + when(critereMapper.toDomains((List<CritereEntity>) any())).thenReturn(critereList); + when(critereRepository.findAll()).thenReturn(new ArrayList<>()); + List<Critere> actualAll = critereJpaAdapter.getAll(); + assertSame(critereList, actualAll); + assertTrue(actualAll.isEmpty()); + verify(critereMapper).toDomains((List<CritereEntity>) any()); + verify(critereRepository).findAll(); + } +} + diff --git a/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/jpa/EtapeJpaAdapterTest.java b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/jpa/EtapeJpaAdapterTest.java new file mode 100644 index 00000000..42201c32 --- /dev/null +++ b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/jpa/EtapeJpaAdapterTest.java @@ -0,0 +1,147 @@ +package org.mte.numecoeval.referentiel.infrastructure.jpa; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mte.numecoeval.referentiel.domain.exception.ReferentielException; +import org.mte.numecoeval.referentiel.domain.model.Etape; +import org.mte.numecoeval.referentiel.domain.model.id.EtapeId; +import org.mte.numecoeval.referentiel.infrastructure.jpa.adapter.EtapeJpaAdapter; +import org.mte.numecoeval.referentiel.infrastructure.jpa.adapter.ImpactEquipementJpaAdapter; +import org.mte.numecoeval.referentiel.infrastructure.jpa.adapter.ImpactReseauJpaAdapter; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.EtapeEntity; +import org.mte.numecoeval.referentiel.infrastructure.jpa.repository.EtapeRepository; +import org.mte.numecoeval.referentiel.infrastructure.mapper.EtapeMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ContextConfiguration(classes = {EtapeJpaAdapter.class}) +@ExtendWith(SpringExtension.class) +class EtapeJpaAdapterTest { + @Autowired + private EtapeJpaAdapter etapeJpaAdapter; + + @MockBean + private EtapeMapper etapeMapper; + + @MockBean + private EtapeRepository etapeRepository; + + // Pour les purges des données filles + @MockBean + ImpactReseauJpaAdapter impactReseauJpaAdapter; + @MockBean + ImpactEquipementJpaAdapter impactEquipementJpaAdapter; + + /** + * Method under test: {@link EtapeJpaAdapter#save(Etape)} + */ + @Test + void testSave() throws ReferentielException { + Etape etape = new Etape(); + etape.setCode("Code"); + etape.setLibelle("Libelle"); + assertNull(etapeJpaAdapter.save(etape)); + } + + /** + * Method under test: {@link EtapeJpaAdapter#save(Etape)} + */ + @Test + void testSave2() throws ReferentielException { + Etape etape = new Etape(); + etape.setCode("Code"); + etape.setLibelle("Libelle"); + + Etape etape1 = new Etape(); + etape1.setCode("Code"); + etape1.setLibelle("Libelle"); + Etape etape2 = mock(Etape.class); + when(etape2.setCode((String) any())).thenReturn(etape); + when(etape2.setLibelle((String) any())).thenReturn(etape1); + etape2.setCode("Code"); + etape2.setLibelle("Libelle"); + assertNull(etapeJpaAdapter.save(etape2)); + verify(etape2).setCode((String) any()); + verify(etape2).setLibelle((String) any()); + } + + /** + * Method under test: {@link EtapeJpaAdapter#saveAll(Collection)} + */ + @Test + void testSaveAll() throws ReferentielException { + when(etapeMapper.toEntities((Collection<Etape>) any())).thenReturn(new ArrayList<>()); + when(etapeRepository.saveAll((Iterable<EtapeEntity>) any())).thenReturn(new ArrayList<>()); + etapeJpaAdapter.saveAll(new ArrayList<>()); + verify(etapeMapper).toEntities((Collection<Etape>) any()); + verify(etapeRepository).saveAll((Iterable<EtapeEntity>) any()); + assertTrue(etapeJpaAdapter.getAll().isEmpty()); + } + + /** + * Method under test: {@link EtapeJpaAdapter#get(EtapeId)} + */ + @Test + void testGet() throws ReferentielException { + EtapeId etapeId = new EtapeId(); + etapeId.setCode("Code"); + assertNull(etapeJpaAdapter.get(etapeId)); + } + + /** + * Method under test: {@link EtapeJpaAdapter#get(EtapeId)} + */ + @Test + void testGet2() throws ReferentielException { + EtapeId etapeId = new EtapeId(); + etapeId.setCode("Code"); + EtapeId etapeId1 = mock(EtapeId.class); + when(etapeId1.setCode((String) any())).thenReturn(etapeId); + etapeId1.setCode("Code"); + assertNull(etapeJpaAdapter.get(etapeId1)); + verify(etapeId1).setCode((String) any()); + } + + /** + * Method under test: {@link EtapeJpaAdapter#purge()} + */ + @Test + void testPurge() { + doNothing().when(impactReseauJpaAdapter).purge(); + doNothing().when(impactEquipementJpaAdapter).purge(); + doNothing().when(etapeRepository).deleteAll(); + + etapeJpaAdapter.purge(); + + // Purge des données filles + verify(impactReseauJpaAdapter, times(0)).purge(); + verify(impactEquipementJpaAdapter, times(0)).purge(); + verify(etapeRepository).deleteAll(); + assertTrue(etapeJpaAdapter.getAll().isEmpty()); + } + + /** + * Method under test: {@link EtapeJpaAdapter#getAll()} + */ + @Test + void testGetAll() { + ArrayList<Etape> etapeList = new ArrayList<>(); + when(etapeMapper.toDomains((List<EtapeEntity>) any())).thenReturn(etapeList); + when(etapeRepository.findAll()).thenReturn(new ArrayList<>()); + List<Etape> actualAll = etapeJpaAdapter.getAll(); + assertSame(etapeList, actualAll); + assertTrue(actualAll.isEmpty()); + verify(etapeMapper).toDomains((List<EtapeEntity>) any()); + verify(etapeRepository).findAll(); + } +} + diff --git a/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/jpa/HypotheseJpaAdapterTest.java b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/jpa/HypotheseJpaAdapterTest.java new file mode 100644 index 00000000..c76fc171 --- /dev/null +++ b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/jpa/HypotheseJpaAdapterTest.java @@ -0,0 +1,130 @@ +package org.mte.numecoeval.referentiel.infrastructure.jpa; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullSource; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.mte.numecoeval.referentiel.domain.exception.ReferentielException; +import org.mte.numecoeval.referentiel.domain.model.Hypothese; +import org.mte.numecoeval.referentiel.domain.model.id.HypotheseId; +import org.mte.numecoeval.referentiel.infrastructure.jpa.adapter.HypotheseJpaAdapter; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.HypotheseEntity; +import org.mte.numecoeval.referentiel.infrastructure.jpa.repository.HypotheseRepository; +import org.mte.numecoeval.referentiel.infrastructure.mapper.HypotheseMapper; +import org.mte.numecoeval.referentiel.infrastructure.mapper.HypotheseMapperImpl; +import org.springframework.test.util.ReflectionTestUtils; + +import java.util.Collections; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; + +class HypotheseJpaAdapterTest { + + @InjectMocks + private HypotheseJpaAdapter hypotheseJpaAdapter; + + @Mock + HypotheseRepository hypotheseRepository; + + HypotheseMapper hypotheseMapper = new HypotheseMapperImpl(); + + @BeforeEach + void setup() { + MockitoAnnotations.openMocks(this); + ReflectionTestUtils.setField(hypotheseJpaAdapter, "hypotheseMapper", hypotheseMapper); + } + + @Test + void get_shouldReturnDomain() { + var expectedEntity = new HypotheseEntity() + .setCode("code") + .setValeur("valeur") + .setSource("test"); + var wantedId = new HypotheseId() + .setCode(expectedEntity.getCode()); + var wantedEntityId = hypotheseMapper.toEntityId(wantedId); + + Mockito.when(hypotheseRepository.findById(wantedEntityId)).thenReturn(Optional.of(expectedEntity)); + + var expectedDomain = assertDoesNotThrow( () -> hypotheseJpaAdapter.get(wantedId) ); + + Assertions.assertEquals(expectedEntity.getCode(), expectedDomain.getCode()); + Assertions.assertEquals(expectedEntity.getValeur(), expectedDomain.getValeur()); + Assertions.assertEquals(expectedEntity.getSource(), expectedDomain.getSource()); + } + + @Test + void get_shouldThrowException() { + var expectedEntity = new HypotheseEntity() + .setCode("code") + .setValeur("valeur") + .setSource("test"); + var wantedId = new HypotheseId() + .setCode(expectedEntity.getCode()); + var wantedEntityId = hypotheseMapper.toEntityId(wantedId); + + Mockito.when(hypotheseRepository.findById(wantedEntityId)).thenReturn(Optional.empty()); + + ReferentielException expectedException = assertThrows(ReferentielException.class, () -> hypotheseJpaAdapter.get(wantedId) ); + + assertEquals("Hypothèse non trouvé",expectedException.getMessage()); + } + + @Test + void purge_shouldCallDeleteAll() { + hypotheseJpaAdapter.purge(); + + Mockito.verify(hypotheseRepository, Mockito.times(1)).deleteAll(); + } + + @Test + void getAll_shouldCallfindAll() { + hypotheseJpaAdapter.getAll(); + + Mockito.verify(hypotheseRepository, Mockito.times(1)).findAll(); + } + + @Test + void saveAll_shouldCallsaveAll() { + var domainToSave = new Hypothese() + .setCode("code") + .setValeur("valeur") + .setSource("test"); + var entityToSave = hypotheseMapper.toEntity(domainToSave); + + assertDoesNotThrow(() -> hypotheseJpaAdapter.saveAll(Collections.singletonList(domainToSave))); + + Mockito.verify(hypotheseRepository, Mockito.times(1)).saveAll(Collections.singletonList(entityToSave)); + } + + @Test + void save_shouldSaveAndReturnDomain() { + var wantedDomain = new Hypothese() + .setCode("code") + .setValeur("valeur") + .setSource("test"); + var expectedEntity = hypotheseMapper.toEntities(Collections.singletonList(wantedDomain)).get(0); + + Mockito.when(hypotheseRepository.save(expectedEntity)).thenReturn(expectedEntity); + + var expectedDomain = assertDoesNotThrow( () -> hypotheseJpaAdapter.save(wantedDomain) ); + + Assertions.assertEquals(wantedDomain.getCode(), expectedDomain.getCode()); + Assertions.assertEquals(wantedDomain.getValeur(), expectedDomain.getValeur()); + Assertions.assertEquals(wantedDomain.getSource(), expectedDomain.getSource()); + } + + @ParameterizedTest + @NullSource + void save_shouldSaveAndReturnNull(Hypothese nullValue) { + var expectedDomain = assertDoesNotThrow( () -> hypotheseJpaAdapter.save(nullValue) ); + + assertNull(expectedDomain); + } +} diff --git a/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/jpa/ImpactEquipementJpaAdapterTest.java b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/jpa/ImpactEquipementJpaAdapterTest.java new file mode 100644 index 00000000..d5485aa7 --- /dev/null +++ b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/jpa/ImpactEquipementJpaAdapterTest.java @@ -0,0 +1,159 @@ +package org.mte.numecoeval.referentiel.infrastructure.jpa; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullSource; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.mte.numecoeval.referentiel.domain.exception.ReferentielException; +import org.mte.numecoeval.referentiel.domain.model.ImpactEquipement; +import org.mte.numecoeval.referentiel.domain.model.id.ImpactEquipementId; +import org.mte.numecoeval.referentiel.infrastructure.jpa.adapter.ImpactEquipementJpaAdapter; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.ImpactEquipementEntity; +import org.mte.numecoeval.referentiel.infrastructure.jpa.repository.ImpactEquipementRepository; +import org.mte.numecoeval.referentiel.infrastructure.mapper.ImpactEquipementMapper; +import org.mte.numecoeval.referentiel.infrastructure.mapper.ImpactEquipementMapperImpl; +import org.springframework.test.util.ReflectionTestUtils; + +import java.util.Collections; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; + +class ImpactEquipementJpaAdapterTest { + + @InjectMocks + private ImpactEquipementJpaAdapter jpaAdapter; + + @Mock + ImpactEquipementRepository repository; + + ImpactEquipementMapper mapper = new ImpactEquipementMapperImpl(); + + @BeforeEach + void setup() { + MockitoAnnotations.openMocks(this); + ReflectionTestUtils.setField(jpaAdapter, "mapper", mapper); + } + + @Test + void get_shouldReturnDomain() { + var expectedEntity = new ImpactEquipementEntity() + .setEtape("UTILISATION") + .setCritere("Changement climatique") + .setRefEquipement("Ecran 27 pouces") + .setType("Monitor") + .setSource("Test") + .setConsoElecMoyenne(0.020) + .setValeur(0.120); + + var wantedId = new ImpactEquipementId() + .setEtape("UTILISATION") + .setCritere("Changement climatique") + .setRefEquipement(expectedEntity.getRefEquipement()); + var wantedEntityId = mapper.toEntityId(wantedId); + Mockito.when(repository.findById(wantedEntityId)).thenReturn(Optional.of(expectedEntity)); + + var expectedDomain = assertDoesNotThrow( () -> jpaAdapter.get(wantedId) ); + + assertNotNull(expectedDomain.getEtape()); + assertEquals(expectedEntity.getEtape(), expectedDomain.getEtape()); + assertNotNull(expectedDomain.getCritere()); + assertEquals(expectedEntity.getCritere(), expectedDomain.getCritere()); + assertEquals(expectedEntity.getRefEquipement(), expectedDomain.getRefEquipement()); + assertEquals(expectedEntity.getType(), expectedDomain.getType()); + assertEquals(expectedEntity.getConsoElecMoyenne(), expectedDomain.getConsoElecMoyenne()); + assertEquals(expectedEntity.getValeur(), expectedDomain.getValeur()); + assertEquals(expectedEntity.getSource(), expectedDomain.getSource()); + } + + @Test + void get_shouldThrowException() { + var wantedId = new ImpactEquipementId() + .setEtape("Absent") + .setCritere("Inexistant") + .setRefEquipement("NonExistant"); + var wantedEntityId = mapper.toEntityId(wantedId); + Mockito.when(repository.findById(wantedEntityId)).thenReturn(Optional.empty()); + + ReferentielException expectedException = assertThrows(ReferentielException.class, () -> jpaAdapter.get(wantedId) ); + + assertEquals("Impact Equipement non trouvé",expectedException.getMessage()); + } + + @Test + void get_whenNull_shouldThrowException() { + ReferentielException expectedException = assertThrows(ReferentielException.class, () -> jpaAdapter.get(null) ); + + assertEquals("Impact Equipement non trouvé",expectedException.getMessage()); + } + + @Test + void purge_shouldCallDeleteAll() { + jpaAdapter.purge(); + + Mockito.verify(repository, Mockito.times(1)).deleteAll(); + } + + @Test + void getAll_shouldCallfindAll() { + jpaAdapter.getAll(); + + Mockito.verify(repository, Mockito.times(1)).findAll(); + } + + @Test + void saveAll_shouldCallsaveAll() { + var domainToSave = new ImpactEquipement() + .setEtape("UTILISATION") + .setCritere("Changement climatique") + .setRefEquipement("Ecran 27 pouces") + .setType("Monitor") + .setSource("Test") + .setConsoElecMoyenne(0.020) + .setValeur(0.120); + var entityToSave = mapper.toEntity(domainToSave); + + assertDoesNotThrow(() -> jpaAdapter.saveAll(Collections.singletonList(domainToSave))); + + Mockito.verify(repository, Mockito.times(1)).saveAll(Collections.singletonList(entityToSave)); + } + + @Test + void save_shouldSaveAndReturnDomain() { + var wantedDomain = new ImpactEquipement() + .setEtape("UTILISATION") + .setCritere("Changement climatique") + .setRefEquipement("Ecran 27 pouces") + .setType("Monitor") + .setSource("Test") + .setConsoElecMoyenne(0.020) + .setValeur(0.120); + var expectedEntity = mapper.toEntities(Collections.singletonList(wantedDomain)).get(0); + + Mockito.when(repository.save(expectedEntity)).thenReturn(expectedEntity); + + var expectedDomain = assertDoesNotThrow( () -> jpaAdapter.save(wantedDomain) ); + + assertNotNull(expectedDomain.getEtape()); + assertEquals(expectedEntity.getEtape(), expectedDomain.getEtape()); + assertNotNull(expectedDomain.getCritere()); + assertEquals(expectedEntity.getCritere(), expectedDomain.getCritere()); + assertEquals(expectedEntity.getRefEquipement(), expectedDomain.getRefEquipement()); + assertEquals(expectedEntity.getType(), expectedDomain.getType()); + assertEquals(expectedEntity.getConsoElecMoyenne(), expectedDomain.getConsoElecMoyenne()); + assertEquals(expectedEntity.getValeur(), expectedDomain.getValeur()); + assertEquals(expectedEntity.getSource(), expectedDomain.getSource()); + } + + @ParameterizedTest + @NullSource + void save_shouldSaveAndReturnNull(ImpactEquipement nullValue) { + var expectedDomain = assertDoesNotThrow( () -> jpaAdapter.save(nullValue) ); + + assertNull(expectedDomain); + } +} diff --git a/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/jpa/ImpactMessagerieJpaAdapterTest.java b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/jpa/ImpactMessagerieJpaAdapterTest.java new file mode 100644 index 00000000..f292ec11 --- /dev/null +++ b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/jpa/ImpactMessagerieJpaAdapterTest.java @@ -0,0 +1,149 @@ +package org.mte.numecoeval.referentiel.infrastructure.jpa; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullSource; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.mte.numecoeval.referentiel.domain.exception.ReferentielException; +import org.mte.numecoeval.referentiel.domain.model.ImpactMessagerie; +import org.mte.numecoeval.referentiel.infrastructure.jpa.adapter.ImpactMessagerieJpaAdapter; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.ImpactMessagerieEntity; +import org.mte.numecoeval.referentiel.infrastructure.jpa.repository.ImpactMessagerieRepository; +import org.mte.numecoeval.referentiel.infrastructure.mapper.ImpactMessagerieMapper; +import org.mte.numecoeval.referentiel.infrastructure.mapper.ImpactMessagerieMapperImpl; +import org.springframework.test.util.ReflectionTestUtils; + +import java.util.Collections; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; + +class ImpactMessagerieJpaAdapterTest { + + @InjectMocks + private ImpactMessagerieJpaAdapter jpaAdapter; + + @Mock + ImpactMessagerieRepository repository; + + ImpactMessagerieMapper mapper = new ImpactMessagerieMapperImpl(); + + @BeforeEach + void setup() { + MockitoAnnotations.openMocks(this); + ReflectionTestUtils.setField(jpaAdapter, "impactMessagerieMapper", mapper); + } + + @Test + void get_shouldReturnDomain() { + var expectedCritere = "Changement climatique"; + var expectedEntity = new ImpactMessagerieEntity() + .setCritere("Changement climatique") + .setSource("Test") + .setConstanteCoefficientDirecteur(0.05) + .setConstanteOrdonneeOrigine(0.00120); + + Mockito.when(repository.findById(expectedCritere)).thenReturn(Optional.of(expectedEntity)); + + var expectedDomain = assertDoesNotThrow( () -> jpaAdapter.get(expectedCritere) ); + + assertNotNull(expectedDomain.getCritere()); + assertEquals(expectedEntity.getCritere(), expectedDomain.getCritere()); + assertEquals(expectedEntity.getConstanteCoefficientDirecteur(), expectedDomain.getConstanteCoefficientDirecteur()); + assertEquals(expectedEntity.getConstanteOrdonneeOrigine(), expectedDomain.getConstanteOrdonneeOrigine()); + assertEquals(expectedEntity.getSource(), expectedDomain.getSource()); + } + + @Test + void get_shouldThrowException() { + var expectedCritere = "Changement climatique"; + Mockito.when(repository.findById(expectedCritere)).thenReturn(Optional.empty()); + + ReferentielException expectedException = assertThrows(ReferentielException.class, () -> jpaAdapter.get(expectedCritere) ); + + assertEquals("ImpactMessagerie non trouvé",expectedException.getMessage()); + } + + @Test + void get_whenNull_shouldThrowException() { + ReferentielException expectedException = assertThrows(ReferentielException.class, () -> jpaAdapter.get(null) ); + + assertEquals("ImpactMessagerie non trouvé",expectedException.getMessage()); + } + + @Test + void purge_shouldCallDeleteAll() { + jpaAdapter.purge(); + + Mockito.verify(repository, Mockito.times(1)).deleteAll(); + } + + @Test + void getAll_shouldCallfindAll() { + var expectedEntity = new ImpactMessagerieEntity() + .setCritere("Changement climatique") + .setSource("Test") + .setConstanteCoefficientDirecteur(0.05) + .setConstanteOrdonneeOrigine(0.00120); + Mockito.when(repository.findAll()).thenReturn(Collections.singletonList(expectedEntity)); + + var result = jpaAdapter.getAll(); + + Mockito.verify(repository, Mockito.times(1)).findAll(); + assertNotNull(result); + assertEquals(1, result.size()); + + var expectedDomain = result.get(0); + assertNotNull(expectedDomain.getCritere()); + assertEquals(expectedEntity.getCritere(), expectedDomain.getCritere()); + assertEquals(expectedEntity.getConstanteCoefficientDirecteur(), expectedDomain.getConstanteCoefficientDirecteur()); + assertEquals(expectedEntity.getConstanteOrdonneeOrigine(), expectedDomain.getConstanteOrdonneeOrigine()); + assertEquals(expectedEntity.getSource(), expectedDomain.getSource()); + } + + @Test + void saveAll_shouldCallsaveAll() { + var domainToSave = new ImpactMessagerie() + .setCritere("Changement climatique") + .setSource("Test") + .setConstanteCoefficientDirecteur(0.05) + .setConstanteOrdonneeOrigine(0.00120); + var entityToSave = mapper.toEntity(domainToSave); + + assertDoesNotThrow(() -> jpaAdapter.saveAll(Collections.singletonList(domainToSave))); + + Mockito.verify(repository, Mockito.times(1)).saveAll(Collections.singletonList(entityToSave)); + } + + @Test + void save_shouldSaveAndReturnDomain() { + var wantedDomain = new ImpactMessagerie() + .setCritere("Changement climatique") + .setSource("Test") + .setConstanteCoefficientDirecteur(0.05) + .setConstanteOrdonneeOrigine(0.00120); + var expectedEntity = mapper.toEntities(Collections.singletonList(wantedDomain)).get(0); + + Mockito.when(repository.save(expectedEntity)).thenReturn(expectedEntity); + + var expectedDomain = assertDoesNotThrow( () -> jpaAdapter.save(wantedDomain) ); + + assertNotNull(expectedDomain.getCritere()); + assertEquals(wantedDomain.getCritere(), expectedDomain.getCritere()); + assertEquals(expectedEntity.getConstanteCoefficientDirecteur(), expectedDomain.getConstanteCoefficientDirecteur()); + assertEquals(expectedEntity.getConstanteOrdonneeOrigine(), expectedDomain.getConstanteOrdonneeOrigine()); + assertEquals(wantedDomain.getSource(), expectedDomain.getSource()); + } + + @ParameterizedTest + @NullSource + void save_shouldSaveAndReturnNull(ImpactMessagerie nullValue) { + var expectedDomain = assertDoesNotThrow( () -> jpaAdapter.save(nullValue) ); + + assertNull(expectedDomain); + } +} diff --git a/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/jpa/ImpactReseauJpaAdapterTest.java b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/jpa/ImpactReseauJpaAdapterTest.java new file mode 100644 index 00000000..258f988f --- /dev/null +++ b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/jpa/ImpactReseauJpaAdapterTest.java @@ -0,0 +1,197 @@ +package org.mte.numecoeval.referentiel.infrastructure.jpa; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mockito; +import org.mte.numecoeval.referentiel.domain.exception.ReferentielException; +import org.mte.numecoeval.referentiel.domain.model.ImpactReseau; +import org.mte.numecoeval.referentiel.domain.model.id.ImpactReseauId; +import org.mte.numecoeval.referentiel.infrastructure.jpa.adapter.ImpactReseauJpaAdapter; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.ImpactReseauEntity; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.id.ImpactReseauIdEntity; +import org.mte.numecoeval.referentiel.infrastructure.jpa.repository.ImpactReseauRepository; +import org.mte.numecoeval.referentiel.infrastructure.mapper.ImpactReseauMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ContextConfiguration(classes = {ImpactReseauJpaAdapter.class}) +@ExtendWith(SpringExtension.class) +class ImpactReseauJpaAdapterTest { + @Autowired + private ImpactReseauJpaAdapter impactReseauJpaAdapter; + + @MockBean + private ImpactReseauMapper impactReseauMapper; + + @MockBean + private ImpactReseauRepository impactReseauRepository; + + /** + * Method under test: {@link ImpactReseauJpaAdapter#save(ImpactReseau)} + */ + @Test + void testSave() throws ReferentielException { + String expectedCritere = "Nom Critere"; + String expectedEtape = "Code"; + ImpactReseau impactReseau = new ImpactReseau(); + impactReseau.setConsoElecMoyenne(10.0d); + impactReseau.setCritere(expectedCritere); + impactReseau.setEtape(expectedEtape); + impactReseau.setRefReseau("Ref Reseau"); + impactReseau.setSource("Source"); + impactReseau.setValeur(10.0d); + + ImpactReseauEntity impactReseauEntity = new ImpactReseauEntity(); + impactReseauEntity.setConsoElecMoyenne(10.0d); + impactReseauEntity.setCritere(expectedCritere); + + impactReseauEntity.setEtape(expectedEtape); + impactReseauEntity.setRefReseau("Ref Reseau"); + impactReseauEntity.setSource("Source"); + impactReseauEntity.setValeur(10.0d); + when(impactReseauMapper.toDomain((ImpactReseauEntity) any())).thenReturn(impactReseau); + when(impactReseauMapper.toEntity((ImpactReseau) any())).thenReturn(impactReseauEntity); + + ImpactReseauEntity impactReseauEntity1 = new ImpactReseauEntity(); + impactReseauEntity1.setConsoElecMoyenne(10.0d); + impactReseauEntity1.setCritere(expectedCritere); + impactReseauEntity1.setEtape(expectedEtape); + impactReseauEntity1.setRefReseau("Ref Reseau"); + impactReseauEntity1.setSource("Source"); + impactReseauEntity1.setValeur(10.0d); + when(impactReseauRepository.save((ImpactReseauEntity) any())).thenReturn(impactReseauEntity1); + + ImpactReseau impactReseau1 = new ImpactReseau(); + impactReseau1.setConsoElecMoyenne(10.0d); + impactReseau1.setCritere(expectedCritere); + impactReseau1.setEtape(expectedEtape); + impactReseau1.setRefReseau("Ref Reseau"); + impactReseau1.setSource("Source"); + impactReseau1.setValeur(10.0d); + assertSame(impactReseau, impactReseauJpaAdapter.save(impactReseau1)); + verify(impactReseauMapper).toDomain((ImpactReseauEntity) any()); + verify(impactReseauMapper).toEntity((ImpactReseau) any()); + verify(impactReseauRepository).save((ImpactReseauEntity) any()); + } + + /** + * Method under test: {@link ImpactReseauJpaAdapter#saveAll(Collection)} + */ + @Test + void testSaveAll() throws ReferentielException { + when(impactReseauMapper.toEntity((Collection<ImpactReseau>) any())).thenReturn(new ArrayList<>()); + when(impactReseauRepository.saveAll((Iterable<ImpactReseauEntity>) any())).thenReturn(new ArrayList<>()); + impactReseauJpaAdapter.saveAll(new ArrayList<>()); + verify(impactReseauMapper).toEntity((Collection<ImpactReseau>) any()); + verify(impactReseauRepository).saveAll((Iterable<ImpactReseauEntity>) any()); + } + + /** + * Method under test: {@link ImpactReseauJpaAdapter#get(ImpactReseauId)} + */ + @Test + void testGet() throws ReferentielException { + String expectedCritere = "Nom Critere"; + String expectedEtape = "Code"; + ImpactReseauIdEntity impactReseauIdEntity = new ImpactReseauIdEntity(); + impactReseauIdEntity.setCritere(expectedCritere); + impactReseauIdEntity.setEtape(expectedEtape); + impactReseauIdEntity.setRefReseau("Ref Reseau"); + + ImpactReseau impactReseau = new ImpactReseau(); + impactReseau.setConsoElecMoyenne(10.0d); + impactReseau.setCritere(expectedCritere); + impactReseau.setEtape(expectedEtape); + impactReseau.setRefReseau("Ref Reseau"); + impactReseau.setSource("Source"); + impactReseau.setValeur(10.0d); + when(impactReseauMapper.toDomain((ImpactReseauEntity) any())).thenReturn(impactReseau); + when(impactReseauMapper.toEntityId((ImpactReseauId) any())).thenReturn(impactReseauIdEntity); + + ImpactReseauEntity impactReseauEntity = new ImpactReseauEntity(); + impactReseauEntity.setConsoElecMoyenne(10.0d); + impactReseauEntity.setCritere(expectedCritere); + impactReseauEntity.setEtape(expectedEtape); + impactReseauEntity.setRefReseau("Ref Reseau"); + impactReseauEntity.setSource("Source"); + impactReseauEntity.setValeur(10.0d); + Optional<ImpactReseauEntity> ofResult = Optional.of(impactReseauEntity); + when(impactReseauRepository.findById((ImpactReseauIdEntity) any())).thenReturn(ofResult); + + ImpactReseauId impactReseauId = new ImpactReseauId(); + impactReseauId.setCritere(expectedCritere); + impactReseauId.setEtape(expectedEtape); + impactReseauId.setRefReseau("Ref Reseau"); + assertSame(impactReseau, impactReseauJpaAdapter.get(impactReseauId)); + verify(impactReseauMapper).toDomain((ImpactReseauEntity) any()); + verify(impactReseauMapper).toEntityId(any()); + verify(impactReseauRepository).findById(any()); + } + + /** + * Method under test: {@link ImpactReseauJpaAdapter#get(ImpactReseauId)} + */ + @Test + void testGet2() throws ReferentielException { + String expectedCritere = "Nom Critere"; + String expectedEtape = "Code"; + ImpactReseauIdEntity impactReseauIdEntity = new ImpactReseauIdEntity(); + impactReseauIdEntity.setCritere(expectedCritere); + impactReseauIdEntity.setEtape(expectedEtape); + impactReseauIdEntity.setRefReseau("Ref Reseau"); + + ImpactReseau impactReseau = new ImpactReseau(); + impactReseau.setConsoElecMoyenne(10.0d); + impactReseau.setCritere(expectedCritere); + impactReseau.setEtape(expectedEtape); + impactReseau.setRefReseau("Ref Reseau"); + impactReseau.setSource("Source"); + impactReseau.setValeur(10.0d); + when(impactReseauMapper.toDomain((ImpactReseauEntity) any())).thenReturn(impactReseau); + when(impactReseauMapper.toEntityId(any())).thenReturn(impactReseauIdEntity); + when(impactReseauRepository.findById(any())).thenReturn(Optional.empty()); + + ImpactReseauId impactReseauId = new ImpactReseauId(); + impactReseauId.setCritere(expectedCritere); + impactReseauId.setEtape(expectedEtape); + impactReseauId.setRefReseau("Ref Reseau"); + assertThrows(ReferentielException.class, () -> impactReseauJpaAdapter.get(impactReseauId)); + verify(impactReseauMapper).toEntityId((ImpactReseauId) any()); + verify(impactReseauRepository).findById((ImpactReseauIdEntity) any()); + } + + /** + * Method under test: {@link ImpactReseauJpaAdapter#purge()} + */ + @Test + void testPurge() { + doNothing().when(impactReseauRepository).deleteAll(); + impactReseauJpaAdapter.purge(); + verify(impactReseauRepository).deleteAll(); + } + + /** + * Methods under test: + * + * <ul> + * <li>default or parameterless constructor of {@link ImpactReseauJpaAdapter} + * <li>{@link ImpactReseauJpaAdapter#getAll()} + * </ul> + */ + @Test + void testConstructor() { + Mockito.when(impactReseauRepository.findAll()).thenReturn(Collections.emptyList()); + assertTrue((impactReseauJpaAdapter.getAll().isEmpty())); + } +} + diff --git a/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/jpa/MixElectriqueJpaAdapterTest.java b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/jpa/MixElectriqueJpaAdapterTest.java new file mode 100644 index 00000000..6de65843 --- /dev/null +++ b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/jpa/MixElectriqueJpaAdapterTest.java @@ -0,0 +1,169 @@ +package org.mte.numecoeval.referentiel.infrastructure.jpa; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullSource; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.mte.numecoeval.referentiel.domain.exception.ReferentielException; +import org.mte.numecoeval.referentiel.domain.model.MixElectrique; +import org.mte.numecoeval.referentiel.domain.model.id.MixElectriqueId; +import org.mte.numecoeval.referentiel.infrastructure.jpa.adapter.MixElectriqueJpaAdapter; +import org.mte.numecoeval.referentiel.infrastructure.jpa.entity.MixElectriqueEntity; +import org.mte.numecoeval.referentiel.infrastructure.jpa.repository.MixElectriqueRepository; +import org.mte.numecoeval.referentiel.infrastructure.mapper.MixElectriqueMapper; +import org.mte.numecoeval.referentiel.infrastructure.mapper.MixElectriqueMapperImpl; +import org.springframework.test.util.ReflectionTestUtils; + +import java.util.Collections; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; + +class MixElectriqueJpaAdapterTest { + + @InjectMocks + private MixElectriqueJpaAdapter jpaAdapter; + + @Mock + MixElectriqueRepository repository; + + MixElectriqueMapper mapper = new MixElectriqueMapperImpl(); + + @BeforeEach + void setup() { + MockitoAnnotations.openMocks(this); + ReflectionTestUtils.setField(jpaAdapter, "mixElectriqueMapper", mapper); + } + + @Test + void get_shouldReturnDomain() { + var expectedCritere = "Changement climatique"; + var expectedEntity = new MixElectriqueEntity() + .setCritere(expectedCritere) + .setPays("France") + .setSource("Test") + .setRaccourcisAnglais("FR") + .setValeur(0.120); + + var wantedId = new MixElectriqueId() + .setPays(expectedEntity.getPays()) + .setCritere(expectedCritere); + var wantedEntityId = mapper.toEntityId(wantedId); + Mockito.when(repository.findById(wantedEntityId)).thenReturn(Optional.of(expectedEntity)); + + var expectedDomain = assertDoesNotThrow(() -> jpaAdapter.get(wantedId)); + + assertNotNull(expectedDomain.getCritere()); + Assertions.assertEquals(expectedEntity.getCritere(), expectedDomain.getCritere()); + Assertions.assertEquals(expectedEntity.getPays(), expectedDomain.getPays()); + Assertions.assertEquals(expectedEntity.getRaccourcisAnglais(), expectedDomain.getRaccourcisAnglais()); + Assertions.assertEquals(expectedEntity.getValeur(), expectedDomain.getValeur()); + Assertions.assertEquals(expectedEntity.getSource(), expectedDomain.getSource()); + } + + @Test + void get_shouldThrowException() { + var wantedId = new MixElectriqueId() + .setPays("NonExistant") + .setCritere("Inexistant"); + var wantedEntityId = mapper.toEntityId(wantedId); + Mockito.when(repository.findById(wantedEntityId)).thenReturn(Optional.empty()); + + ReferentielException expectedException = assertThrows(ReferentielException.class, () -> jpaAdapter.get(wantedId)); + + assertEquals("Mix Electrique non trouvé pour l'id MixElectriqueId(pays=NonExistant, critere=Inexistant)", expectedException.getMessage()); + } + + @Test + void get_whenNull_shouldThrowException() { + ReferentielException expectedException = assertThrows(ReferentielException.class, () -> jpaAdapter.get(null)); + + assertEquals("Mix Electrique non trouvé pour l'id null", expectedException.getMessage()); + } + + @Test + void purge_shouldCallDeleteAll() { + jpaAdapter.purge(); + + Mockito.verify(repository, Mockito.times(1)).deleteAll(); + } + + @Test + void getAll_shouldCallfindAll() { + var expectedCritere = "Changement climatique"; + var expectedEntity = new MixElectriqueEntity() + .setCritere(expectedCritere) + .setPays("France") + .setSource("Test") + .setRaccourcisAnglais("FR") + .setValeur(0.120); + Mockito.when(repository.findAll()).thenReturn(Collections.singletonList(expectedEntity)); + + var result = jpaAdapter.getAll(); + + Mockito.verify(repository, Mockito.times(1)).findAll(); + assertNotNull(result); + assertEquals(1, result.size()); + + var expectedDomain = result.get(0); + assertNotNull(expectedDomain.getCritere()); + Assertions.assertEquals(expectedEntity.getCritere(), expectedDomain.getCritere()); + Assertions.assertEquals(expectedEntity.getPays(), expectedDomain.getPays()); + Assertions.assertEquals(expectedEntity.getRaccourcisAnglais(), expectedDomain.getRaccourcisAnglais()); + Assertions.assertEquals(expectedEntity.getValeur(), expectedDomain.getValeur()); + Assertions.assertEquals(expectedEntity.getSource(), expectedDomain.getSource()); + } + + @Test + void saveAll_shouldCallsaveAll() { + var wantedCritere = "Changement climatique"; + var domainToSave = new MixElectrique() + .setCritere(wantedCritere) + .setPays("France") + .setSource("Test") + .setRaccourcisAnglais("FR") + .setValeur(0.120); + + var entityToSave = mapper.toEntity(domainToSave); + + assertDoesNotThrow(() -> jpaAdapter.saveAll(Collections.singletonList(domainToSave))); + + Mockito.verify(repository, Mockito.times(1)).saveAll(Collections.singletonList(entityToSave)); + } + + @Test + void save_shouldSaveAndReturnDomain() { + var wantedCritere = "Changement climatique"; + var wantedDomain = new MixElectrique() + .setCritere(wantedCritere) + .setPays("France") + .setSource("Test") + .setRaccourcisAnglais("FR") + .setValeur(0.120); + var expectedEntity = mapper.toEntities(Collections.singletonList(wantedDomain)).get(0); + + Mockito.when(repository.save(expectedEntity)).thenReturn(expectedEntity); + + var expectedDomain = assertDoesNotThrow(() -> jpaAdapter.save(wantedDomain)); + + assertNotNull(expectedDomain.getCritere()); + Assertions.assertEquals(wantedDomain.getCritere(), expectedDomain.getCritere()); + Assertions.assertEquals(wantedDomain.getPays(), expectedDomain.getPays()); + Assertions.assertEquals(wantedDomain.getRaccourcisAnglais(), expectedDomain.getRaccourcisAnglais()); + Assertions.assertEquals(wantedDomain.getValeur(), expectedDomain.getValeur()); + Assertions.assertEquals(wantedDomain.getSource(), expectedDomain.getSource()); + } + + @ParameterizedTest + @NullSource + void save_shouldSaveAndReturnNull(MixElectrique nullValue) { + var expectedDomain = assertDoesNotThrow(() -> jpaAdapter.save(nullValue)); + + assertNull(expectedDomain); + } +} diff --git a/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/correspondance/ReferentielCorrespondanceRefEquipementRestApiImplTest.java b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/correspondance/ReferentielCorrespondanceRefEquipementRestApiImplTest.java new file mode 100644 index 00000000..c3ac93f2 --- /dev/null +++ b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/correspondance/ReferentielCorrespondanceRefEquipementRestApiImplTest.java @@ -0,0 +1,88 @@ +package org.mte.numecoeval.referentiel.infrastructure.restapi.controller.correspondance; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.CorrespondanceRefEquipementDTO; +import org.mte.numecoeval.referentiel.domain.exception.ReferentielException; +import org.mte.numecoeval.referentiel.infrastructure.adapter.export.CorrespondanceRefEquipemenetCsvExportService; +import org.mte.numecoeval.referentiel.infrastructure.restapi.facade.CorrespondanceRefEquipementFacade; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.HttpStatus; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.web.server.ResponseStatusException; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ContextConfiguration(classes = {ReferentielCorrespondanceRefEquipementRestApiImpl.class}) +@ExtendWith(SpringExtension.class) +class ReferentielCorrespondanceRefEquipementRestApiImplTest { + + @Autowired + private ReferentielCorrespondanceRefEquipementRestApiImpl referentielRestApi; + + @MockBean + private CorrespondanceRefEquipementFacade referentielFacade; + + @MockBean + private CorrespondanceRefEquipemenetCsvExportService csvExportService; + + @Test + void get_shouldCallFacadeGetAndReturnCorrespondingDTO() throws ReferentielException { + var wantedId = "modeleSource"; + CorrespondanceRefEquipementDTO expectedDto = CorrespondanceRefEquipementDTO.builder() + .modeleEquipementSource(wantedId) + .refEquipementCible("test") + .build(); + when(referentielFacade.get(wantedId)).thenReturn(expectedDto); + CorrespondanceRefEquipementDTO resultDto = referentielRestApi.get(wantedId); + assertSame(expectedDto, resultDto); + verify(referentielFacade).get(wantedId); + } + + @Test + void importCSV_shouldCallPurgeAndAddAll() throws IOException, ReferentielException { + doNothing().when(referentielFacade).purgeAndAddAll(any()); + referentielRestApi.importCSV(new MockMultipartFile("Name", "AAAAAAAA".getBytes(StandardCharsets.UTF_8))); + verify(referentielFacade).purgeAndAddAll(any()); + } + + @Test + void importCSV_whenEmptyFileThenShouldThrowException() throws ReferentielException { + doNothing().when(referentielFacade).purgeAndAddAll(any()); + MockMultipartFile file = new MockMultipartFile("Name", (byte[]) null); + ResponseStatusException responseStatusException = assertThrows(ResponseStatusException.class, () -> referentielRestApi.importCSV(file)); + assertEquals(HttpStatus.BAD_REQUEST, responseStatusException.getStatusCode()); + assertEquals("Le fichier n'existe pas ou alors il est vide", responseStatusException.getReason()); + } + + @Test + void importCSV_whenNullFileThenShouldThrowException() throws ReferentielException { + doNothing().when(referentielFacade).purgeAndAddAll(any()); + ResponseStatusException responseStatusException = assertThrows(ResponseStatusException.class, () -> referentielRestApi.importCSV(null)); + assertEquals(HttpStatus.BAD_REQUEST, responseStatusException.getStatusCode()); + assertEquals("Le fichier n'existe pas ou alors il est vide", responseStatusException.getReason()); + } + + @Test + void exportCSV_shouldReturnCSVFile() { + var servletResponse = new MockHttpServletResponse(); + + assertDoesNotThrow(() -> referentielRestApi.exportCSV(servletResponse)); + + assertEquals("text/csv;charset=UTF-8", servletResponse.getContentType()); + String headerContentDisposition = servletResponse.getHeader("Content-Disposition"); + assertNotNull(headerContentDisposition); + assertTrue(headerContentDisposition.contains("attachment; filename=correspondancesRefEquipement-")); + assertEquals(200, servletResponse.getStatus()); + + } +} diff --git a/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/critere/ReferentielCritereRestApiImplTest.java b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/critere/ReferentielCritereRestApiImplTest.java new file mode 100644 index 00000000..b9d1a385 --- /dev/null +++ b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/critere/ReferentielCritereRestApiImplTest.java @@ -0,0 +1,87 @@ +package org.mte.numecoeval.referentiel.infrastructure.restapi.controller.critere; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.CritereDTO; +import org.mte.numecoeval.referentiel.domain.exception.ReferentielException; +import org.mte.numecoeval.referentiel.infrastructure.adapter.export.CritereCsvExportService; +import org.mte.numecoeval.referentiel.infrastructure.restapi.facade.CritereFacade; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.HttpStatus; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.web.server.ResponseStatusException; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ContextConfiguration(classes = {ReferentielCritereRestApiImpl.class}) +@ExtendWith(SpringExtension.class) +class ReferentielCritereRestApiImplTest { + @Autowired + private ReferentielCritereRestApiImpl referentielRestApi; + + @MockBean + private CritereFacade referentielFacade; + + @MockBean + private CritereCsvExportService csvExportService; + + @Test + void getAll_shouldCallFacadeGetAllAndReturnAllDTOs() { + ArrayList<CritereDTO> critereDTOList = new ArrayList<>(); + when(referentielFacade.getAll()).thenReturn(critereDTOList); + List<CritereDTO> actualAll = referentielRestApi.getAll(); + assertSame(critereDTOList, actualAll); + assertTrue(actualAll.isEmpty()); + verify(referentielFacade).getAll(); + } + + @Test + void importCSV_shouldCallPurgeAndAddAll() throws IOException, ReferentielException { + doNothing().when(referentielFacade).purgeAndAddAll(any()); + referentielRestApi.importCSV(new MockMultipartFile("Name", "AAAAAAAA".getBytes(StandardCharsets.UTF_8))); + verify(referentielFacade).purgeAndAddAll(any()); + } + + @Test + void importCSV_whenEmptyFileThenShouldThrowException() throws ReferentielException { + doNothing().when(referentielFacade).purgeAndAddAll(any()); + MockMultipartFile file = new MockMultipartFile("Name", (byte[]) null); + ResponseStatusException responseStatusException = assertThrows(ResponseStatusException.class, () -> referentielRestApi.importCSV(file)); + assertEquals(HttpStatus.BAD_REQUEST, responseStatusException.getStatusCode()); + assertEquals("Le fichier n'existe pas ou alors il est vide", responseStatusException.getReason()); + } + + @Test + void importCSV_whenNullFileThenShouldThrowException() throws ReferentielException { + doNothing().when(referentielFacade).purgeAndAddAll(any()); + ResponseStatusException responseStatusException = assertThrows(ResponseStatusException.class, () -> referentielRestApi.importCSV(null)); + assertEquals(HttpStatus.BAD_REQUEST, responseStatusException.getStatusCode()); + assertEquals("Le fichier n'existe pas ou alors il est vide", responseStatusException.getReason()); + } + + @Test + void exportCSV_shouldReturnCSVFile() throws UnsupportedEncodingException { + var servletResponse = new MockHttpServletResponse(); + + assertDoesNotThrow(() -> referentielRestApi.exportCSV(servletResponse)); + + assertEquals("text/csv;charset=UTF-8", servletResponse.getContentType()); + String headerContentDisposition = servletResponse.getHeader("Content-Disposition"); + assertNotNull(headerContentDisposition); + assertTrue(headerContentDisposition.contains("attachment; filename=criteres-")); + assertEquals(200, servletResponse.getStatus()); + + } +} + diff --git a/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/etape/ReferentielEtapeRestApiImplTest.java b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/etape/ReferentielEtapeRestApiImplTest.java new file mode 100644 index 00000000..3929e969 --- /dev/null +++ b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/etape/ReferentielEtapeRestApiImplTest.java @@ -0,0 +1,87 @@ +package org.mte.numecoeval.referentiel.infrastructure.restapi.controller.etape; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.EtapeDTO; +import org.mte.numecoeval.referentiel.domain.exception.ReferentielException; +import org.mte.numecoeval.referentiel.infrastructure.adapter.export.EtapeCsvExportService; +import org.mte.numecoeval.referentiel.infrastructure.restapi.facade.EtapeFacade; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.HttpStatus; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.web.server.ResponseStatusException; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ContextConfiguration(classes = {ReferentielEtapeRestApiImpl.class}) +@ExtendWith(SpringExtension.class) +class ReferentielEtapeRestApiImplTest { + @Autowired + private ReferentielEtapeRestApiImpl referentielRestApi; + + @MockBean + private EtapeFacade referentielFacade; + + @MockBean + private EtapeCsvExportService csvExportService; + + @Test + void getAll_shouldCallFacadeGetAllAndReturnAllDTOs() { + ArrayList<EtapeDTO> EtapeDTOList = new ArrayList<>(); + when(referentielFacade.getAll()).thenReturn(EtapeDTOList); + List<EtapeDTO> actualAll = referentielRestApi.getAll(); + assertSame(EtapeDTOList, actualAll); + assertTrue(actualAll.isEmpty()); + verify(referentielFacade).getAll(); + } + + @Test + void importCSV_shouldCallPurgeAndAddAll() throws IOException, ReferentielException { + doNothing().when(referentielFacade).purgeAndAddAll(any()); + referentielRestApi.importCSV(new MockMultipartFile("Name", "AAAAAAAA".getBytes(StandardCharsets.UTF_8))); + verify(referentielFacade).purgeAndAddAll(any()); + } + + @Test + void importCSV_whenEmptyFileThenShouldThrowException() throws ReferentielException { + doNothing().when(referentielFacade).purgeAndAddAll(any()); + MockMultipartFile file = new MockMultipartFile("Name", (byte[]) null); + ResponseStatusException responseStatusException = assertThrows(ResponseStatusException.class, () -> referentielRestApi.importCSV(file)); + assertEquals(HttpStatus.BAD_REQUEST, responseStatusException.getStatusCode()); + assertEquals("Le fichier n'existe pas ou alors il est vide", responseStatusException.getReason()); + } + + @Test + void importCSV_whenNullFileThenShouldThrowException() throws ReferentielException { + doNothing().when(referentielFacade).purgeAndAddAll(any()); + ResponseStatusException responseStatusException = assertThrows(ResponseStatusException.class, () -> referentielRestApi.importCSV(null)); + assertEquals(HttpStatus.BAD_REQUEST, responseStatusException.getStatusCode()); + assertEquals("Le fichier n'existe pas ou alors il est vide", responseStatusException.getReason()); + } + + @Test + void exportCSV_shouldReturnCSVFile() throws UnsupportedEncodingException { + var servletResponse = new MockHttpServletResponse(); + + assertDoesNotThrow(() -> referentielRestApi.exportCSV(servletResponse)); + + assertEquals("text/csv;charset=UTF-8", servletResponse.getContentType()); + String headerContentDisposition = servletResponse.getHeader("Content-Disposition"); + assertNotNull(headerContentDisposition); + assertTrue(headerContentDisposition.contains("attachment; filename=etapes-")); + assertEquals(200, servletResponse.getStatus()); + + } +} + diff --git a/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/hypothese/ReferentielHypotheseRestApiImplTest.java b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/hypothese/ReferentielHypotheseRestApiImplTest.java new file mode 100644 index 00000000..3afe264b --- /dev/null +++ b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/hypothese/ReferentielHypotheseRestApiImplTest.java @@ -0,0 +1,99 @@ +package org.mte.numecoeval.referentiel.infrastructure.restapi.controller.hypothese; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.HypotheseDTO; +import org.mte.numecoeval.referentiel.domain.exception.ReferentielException; +import org.mte.numecoeval.referentiel.infrastructure.adapter.export.HypotheseCsvExportService; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.id.HypotheseIdDTO; +import org.mte.numecoeval.referentiel.infrastructure.restapi.facade.HypotheseFacade; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.HttpStatus; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.web.server.ResponseStatusException; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ContextConfiguration(classes = {ReferentielHypotheseRestApiImpl.class}) +@ExtendWith(SpringExtension.class) +class ReferentielHypotheseRestApiImplTest { + @Autowired + private ReferentielHypotheseRestApiImpl referentielRestApi; + + @MockBean + private HypotheseFacade referentielFacade; + + @MockBean + private HypotheseCsvExportService csvExportService; + + @Test + void get_shouldCallFacadeGetAndReturnMatchingDTO() throws ReferentielException { + String cle = "cle"; + HypotheseIdDTO idDTO = new HypotheseIdDTO().setCode(cle); + var expectedDTO = HypotheseDTO.builder().code(cle).valeur("test").build(); + + + when(referentielFacade.get(idDTO)).thenReturn(expectedDTO); + var receivedDTO = referentielRestApi.get(cle); + assertSame(receivedDTO, receivedDTO); + verify(referentielFacade).get(idDTO); + } + + @Test + void get_whenNotFound_thenShouldThrowReferentielException() throws ReferentielException { + String cle = "cle"; + HypotheseIdDTO idDTO = new HypotheseIdDTO().setCode(cle); + + when(referentielFacade.get(idDTO)).thenThrow(new ReferentielException("Hypothèse non trouvé")); + var exception = assertThrows(ReferentielException.class,() -> referentielRestApi.get(cle)); + assertEquals("Hypothèse non trouvé", exception.getMessage()); + } + + @Test + void importCSV_shouldCallPurgeAndAddAll() throws IOException, ReferentielException { + doNothing().when(referentielFacade).purgeAndAddAll(any()); + referentielRestApi.importCSV(new MockMultipartFile("Name", "AAAAAAAA".getBytes(StandardCharsets.UTF_8))); + verify(referentielFacade).purgeAndAddAll(any()); + } + + @Test + void importCSV_whenEmptyFileThenShouldThrowException() throws ReferentielException { + doNothing().when(referentielFacade).purgeAndAddAll(any()); + MockMultipartFile file = new MockMultipartFile("Name", (byte[]) null); + ResponseStatusException responseStatusException = assertThrows(ResponseStatusException.class, () -> referentielRestApi.importCSV(file)); + assertEquals(HttpStatus.BAD_REQUEST, responseStatusException.getStatusCode()); + assertEquals("Le fichier n'existe pas ou alors il est vide", responseStatusException.getReason()); + } + + @Test + void importCSV_whenNullFileThenShouldThrowException() throws ReferentielException { + doNothing().when(referentielFacade).purgeAndAddAll(any()); + ResponseStatusException responseStatusException = assertThrows(ResponseStatusException.class, () -> referentielRestApi.importCSV(null)); + assertEquals(HttpStatus.BAD_REQUEST, responseStatusException.getStatusCode()); + assertEquals("Le fichier n'existe pas ou alors il est vide", responseStatusException.getReason()); + } + + @Test + void exportCSV_shouldReturnCSVFile() throws UnsupportedEncodingException { + var servletResponse = new MockHttpServletResponse(); + + assertDoesNotThrow(() -> referentielRestApi.exportCSV(servletResponse)); + + assertEquals("text/csv;charset=UTF-8", servletResponse.getContentType()); + String headerContentDisposition = servletResponse.getHeader("Content-Disposition"); + assertNotNull(headerContentDisposition); + assertTrue(headerContentDisposition.contains("attachment; filename=hypotheses-")); + assertEquals(200, servletResponse.getStatus()); + + } +} + diff --git a/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/impactequipement/ReferentielImpactEquipementRestApiImplTest.java b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/impactequipement/ReferentielImpactEquipementRestApiImplTest.java new file mode 100644 index 00000000..a1273d33 --- /dev/null +++ b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/impactequipement/ReferentielImpactEquipementRestApiImplTest.java @@ -0,0 +1,115 @@ +package org.mte.numecoeval.referentiel.infrastructure.restapi.controller.impactequipement; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.ImpactEquipementDTO; +import org.mte.numecoeval.referentiel.domain.exception.ReferentielException; +import org.mte.numecoeval.referentiel.infrastructure.adapter.export.ImpactEquipementCsvExportService; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.id.ImpactEquipementIdDTO; +import org.mte.numecoeval.referentiel.infrastructure.restapi.facade.ImpactEquipementFacade; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.HttpStatus; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.web.server.ResponseStatusException; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ContextConfiguration(classes = {ReferentielImpactEquipementRestApiImpl.class}) +@ExtendWith(SpringExtension.class) +class ReferentielImpactEquipementRestApiImplTest { + @Autowired + private ReferentielImpactEquipementRestApiImpl referentielRestApi; + + @MockBean + private ImpactEquipementFacade referentielFacade; + + @MockBean + private ImpactEquipementCsvExportService csvExportService; + + @Test + void get_shouldCallFacadeGetAndReturnMatchingDTO() throws ReferentielException { + String refEquipement = "Ordinateur Portable"; + String nomCritere = "Changement Climatique"; + String codeEtapeACV = "UTILISATION"; + var idDTO = new ImpactEquipementIdDTO(refEquipement, + codeEtapeACV, + nomCritere + ); + var expectedDTO = ImpactEquipementDTO.builder() + .etape(codeEtapeACV) + .critere(nomCritere) + .refEquipement(refEquipement) + .source("Test") + .description("Test") + .valeur(1.0).build(); + + when(referentielFacade.get(idDTO)).thenReturn(expectedDTO); + + var receivedDTO = referentielRestApi.get(refEquipement, nomCritere, codeEtapeACV); + assertSame(receivedDTO, receivedDTO); + verify(referentielFacade).get(idDTO); + } + + @Test + void get_whenNotFound_thenShouldThrowReferentielException() throws ReferentielException { + String refEquipement = "Ordinateur Portable"; + String nomCritere = "Changement Climatique"; + String codeEtapeACV = "UTILISATION"; + var idDTO = new ImpactEquipementIdDTO(refEquipement, + codeEtapeACV, + nomCritere + ); + + when(referentielFacade.get(idDTO)).thenThrow(new ReferentielException("Impact Equipement non trouvé")); + var exception = assertThrows(ReferentielException.class,() -> referentielRestApi.get(refEquipement, nomCritere, codeEtapeACV)); + assertEquals("Impact Equipement non trouvé", exception.getMessage()); + } + + @Test + void importCSV_shouldCallPurgeAndAddAll() throws IOException, ReferentielException { + doNothing().when(referentielFacade).purgeAndAddAll(any()); + referentielRestApi.importCSV(new MockMultipartFile("Name", "AAAAAAAA".getBytes(StandardCharsets.UTF_8))); + verify(referentielFacade).purgeAndAddAll(any()); + } + + @Test + void importCSV_whenEmptyFileThenShouldThrowException() throws ReferentielException { + doNothing().when(referentielFacade).purgeAndAddAll(any()); + MockMultipartFile file = new MockMultipartFile("Name", (byte[]) null); + ResponseStatusException responseStatusException = assertThrows(ResponseStatusException.class, () -> referentielRestApi.importCSV(file)); + assertEquals(HttpStatus.BAD_REQUEST, responseStatusException.getStatusCode()); + assertEquals("Le fichier n'existe pas ou alors il est vide", responseStatusException.getReason()); + } + + @Test + void importCSV_whenNullFileThenShouldThrowException() throws ReferentielException { + doNothing().when(referentielFacade).purgeAndAddAll(any()); + ResponseStatusException responseStatusException = assertThrows(ResponseStatusException.class, () -> referentielRestApi.importCSV(null)); + assertEquals(HttpStatus.BAD_REQUEST, responseStatusException.getStatusCode()); + assertEquals("Le fichier n'existe pas ou alors il est vide", responseStatusException.getReason()); + } + + @Test + void exportCSV_shouldReturnCSVFile() throws UnsupportedEncodingException { + var servletResponse = new MockHttpServletResponse(); + + assertDoesNotThrow(() -> referentielRestApi.exportCSV(servletResponse)); + + assertEquals("text/csv;charset=UTF-8", servletResponse.getContentType()); + String headerContentDisposition = servletResponse.getHeader("Content-Disposition"); + assertNotNull(headerContentDisposition); + assertTrue(headerContentDisposition.contains("attachment; filename=impactEquipement-")); + assertEquals(200, servletResponse.getStatus()); + + } +} + diff --git a/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/impactmessagerie/ReferentielImpactMessagerieRestApiImplTest.java b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/impactmessagerie/ReferentielImpactMessagerieRestApiImplTest.java new file mode 100644 index 00000000..244bae39 --- /dev/null +++ b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/impactmessagerie/ReferentielImpactMessagerieRestApiImplTest.java @@ -0,0 +1,87 @@ +package org.mte.numecoeval.referentiel.infrastructure.restapi.controller.impactmessagerie; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.ImpactMessagerieDTO; +import org.mte.numecoeval.referentiel.domain.exception.ReferentielException; +import org.mte.numecoeval.referentiel.infrastructure.adapter.export.ImpactMessagerieCsvExportService; +import org.mte.numecoeval.referentiel.infrastructure.restapi.facade.ImpactMessagerieFacade; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.HttpStatus; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.web.server.ResponseStatusException; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ContextConfiguration(classes = {ReferentielImpactMessagerieRestApiImpl.class}) +@ExtendWith(SpringExtension.class) +class ReferentielImpactMessagerieRestApiImplTest { + @Autowired + private ReferentielImpactMessagerieRestApiImpl referentielRestApi; + + @MockBean + private ImpactMessagerieFacade referentielFacade; + + @MockBean + private ImpactMessagerieCsvExportService csvExportService; + + @Test + void getAll_shouldCallFacadeGetAllAndReturnAllDTOs() { + ArrayList<ImpactMessagerieDTO> dtos = new ArrayList<>(); + when(referentielFacade.getAllImpactMessagerie()).thenReturn(dtos); + List<ImpactMessagerieDTO> actualAll = referentielRestApi.getAllImpactMessagerie(); + assertSame(dtos, actualAll); + assertTrue(actualAll.isEmpty()); + verify(referentielFacade).getAllImpactMessagerie(); + } + + @Test + void importCSV_shouldCallPurgeAndAddAll() throws IOException, ReferentielException { + doNothing().when(referentielFacade).purgeAndAddAll(any()); + referentielRestApi.importCSV(new MockMultipartFile("Name", "AAAAAAAA".getBytes(StandardCharsets.UTF_8))); + verify(referentielFacade).purgeAndAddAll(any()); + } + + @Test + void importCSV_whenEmptyFileThenShouldThrowException() throws ReferentielException { + doNothing().when(referentielFacade).purgeAndAddAll(any()); + MockMultipartFile file = new MockMultipartFile("Name", (byte[]) null); + ResponseStatusException responseStatusException = assertThrows(ResponseStatusException.class, () -> referentielRestApi.importCSV(file)); + assertEquals(HttpStatus.BAD_REQUEST, responseStatusException.getStatusCode()); + assertEquals("Le fichier n'existe pas ou alors il est vide", responseStatusException.getReason()); + } + + @Test + void importCSV_whenNullFileThenShouldThrowException() throws ReferentielException { + doNothing().when(referentielFacade).purgeAndAddAll(any()); + ResponseStatusException responseStatusException = assertThrows(ResponseStatusException.class, () -> referentielRestApi.importCSV(null)); + assertEquals(HttpStatus.BAD_REQUEST, responseStatusException.getStatusCode()); + assertEquals("Le fichier n'existe pas ou alors il est vide", responseStatusException.getReason()); + } + + @Test + void exportCSV_shouldReturnCSVFile() throws UnsupportedEncodingException { + var servletResponse = new MockHttpServletResponse(); + + assertDoesNotThrow(() -> referentielRestApi.exportCSV(servletResponse)); + + assertEquals("text/csv;charset=UTF-8", servletResponse.getContentType()); + String headerContentDisposition = servletResponse.getHeader("Content-Disposition"); + assertNotNull(headerContentDisposition); + assertTrue(headerContentDisposition.contains("attachment; filename=impactMessagerie-")); + assertEquals(200, servletResponse.getStatus()); + + } +} diff --git a/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/impactreseau/ReferentielImpactReseauRestApiImplTest.java b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/impactreseau/ReferentielImpactReseauRestApiImplTest.java new file mode 100644 index 00000000..d9016bfe --- /dev/null +++ b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/impactreseau/ReferentielImpactReseauRestApiImplTest.java @@ -0,0 +1,180 @@ +package org.mte.numecoeval.referentiel.infrastructure.restapi.controller.impactreseau; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.ImpactReseauDTO; +import org.mte.numecoeval.referentiel.domain.exception.ReferentielException; +import org.mte.numecoeval.referentiel.infrastructure.adapter.export.ImpactReseauCsvExportService; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.id.ImpactReseauIdDTO; +import org.mte.numecoeval.referentiel.infrastructure.restapi.facade.ImpactReseauFacade; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.web.server.ResponseStatusException; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ContextConfiguration(classes = {ReferentielImpactReseauRestApiImpl.class}) +@ExtendWith(SpringExtension.class) +class ReferentielImpactReseauRestApiImplTest { + @MockBean + private ImpactReseauFacade referentielFacade; + + @MockBean + private ImpactReseauCsvExportService csvExportService; + + @Autowired + private ReferentielImpactReseauRestApiImpl referentielRestApi; + + @Test + void get_shouldCallFacadeGetAndReturnMatchingDTO() throws ReferentielException { + String refReseau = "ImpactReseauMobileMoyen"; + String nomCritere = "Changement Climatique"; + String codeEtapeACV = "UTILISATION"; + var idDTO = ImpactReseauIdDTO.builder() + .refReseau(refReseau) + .etapeACV(codeEtapeACV) + .critere(nomCritere) + .build(); + var expectedDTO = ImpactReseauDTO.builder() + .etapeACV(codeEtapeACV) + .critere(nomCritere) + .refReseau(refReseau) + .source("Test") + .valeur(1.0).build(); + + when(referentielFacade.get(idDTO)).thenReturn(expectedDTO); + + var receivedDTO = referentielRestApi.get(refReseau, nomCritere, codeEtapeACV); + assertSame(receivedDTO, receivedDTO); + verify(referentielFacade).get(idDTO); + } + + @Test + void get_whenNotFound_thenShouldThrowReferentielException() throws ReferentielException { + String refReseau = "ImpactReseauMobileMoyen"; + String nomCritere = "Changement Climatique"; + String codeEtapeACV = "UTILISATION"; + var idDTO = ImpactReseauIdDTO.builder() + .refReseau(refReseau) + .etapeACV(codeEtapeACV) + .critere(nomCritere) + .build(); + + when(referentielFacade.get(idDTO)).thenThrow(new ReferentielException("Impact Réseau non trouvé")); + var exception = assertThrows(ReferentielException.class,() -> referentielRestApi.get(refReseau, nomCritere, codeEtapeACV)); + assertEquals("Impact Réseau non trouvé", exception.getMessage()); + } + + @Test + void add_shouldCallFacadeAddOrUpdate() throws ReferentielException { + ImpactReseauDTO impactReseauDTO = new ImpactReseauDTO("ImpactReseauMobileMoyen", "UTILISATION", + "Changement Climatique", "kg CO² eq", "La source est obligatoire", 10.0d, 10.0d); + when(referentielFacade.addOrUpdate(any())).thenReturn(impactReseauDTO); + + ResponseEntity<ImpactReseauDTO> actualAddResult = referentielRestApi.add(impactReseauDTO); + + assertTrue(actualAddResult.hasBody()); + assertTrue(actualAddResult.getHeaders().isEmpty()); + assertEquals(HttpStatus.OK, actualAddResult.getStatusCode()); + assertEquals(impactReseauDTO, actualAddResult.getBody()); + verify(referentielFacade).addOrUpdate(any()); + } + + @Test + void add_whenEmptySource_shouldThrowBadRequestException() { + ImpactReseauDTO impactReseauDTO = new ImpactReseauDTO("ImpactReseauMobileMoyen", "UTILISATION", + "Changement Climatique", "kg CO² eq", null, 10.0d, 10.0d); + + var exception = assertThrows(ResponseStatusException.class, () -> referentielRestApi.add(impactReseauDTO)); + assertEquals(HttpStatus.BAD_REQUEST, exception.getStatusCode()); + assertEquals("La source est obligatoire", exception.getReason()); + } + + @Test + void add_whenNullValue_shouldThrowBadRequestException() { + var exception = assertThrows(ResponseStatusException.class, () -> referentielRestApi.add(null)); + assertEquals(HttpStatus.BAD_REQUEST, exception.getStatusCode()); + assertEquals("Le corps de la requête ne peut être null", exception.getReason()); + } + + @Test + void update_shouldCallFacadeAddOrUpdate() throws ReferentielException { + when(referentielFacade.addOrUpdate(any())).thenReturn(new ImpactReseauDTO()); + ImpactReseauDTO impactReseauDTO = new ImpactReseauDTO("ImpactReseauMobileMoyen", "UTILISATION", + "Changement Climatique", "kg CO² eq", "La source est obligatoire", 10.0d, 10.0d); + + ResponseEntity<ImpactReseauDTO> actualAddResult = referentielRestApi.update(impactReseauDTO); + + assertTrue(actualAddResult.hasBody()); + assertTrue(actualAddResult.getHeaders().isEmpty()); + assertEquals(HttpStatus.OK, actualAddResult.getStatusCode()); + verify(referentielFacade).addOrUpdate(any()); + } + + @Test + void update_whenEmptySource_shouldThrowBadRequestException() throws ReferentielException { + when(referentielFacade.addOrUpdate(any())).thenReturn(new ImpactReseauDTO()); + ImpactReseauDTO impactReseauDTO = new ImpactReseauDTO("ImpactReseauMobileMoyen", "UTILISATION", + "Changement Climatique", "kg CO² eq", "", 10.0d, 10.0d); + + var exception = assertThrows(ResponseStatusException.class, () -> referentielRestApi.update(impactReseauDTO)); + assertEquals(HttpStatus.BAD_REQUEST, exception.getStatusCode()); + assertEquals("La source est obligatoire", exception.getReason()); + } + + @Test + void update_whenNullValue_shouldThrowBadRequestException() { + var exception = assertThrows(ResponseStatusException.class, () -> referentielRestApi.update(null)); + assertEquals(HttpStatus.BAD_REQUEST, exception.getStatusCode()); + assertEquals("Le corps de la requête ne peut être null", exception.getReason()); + } + + @Test + void importCSV_shouldCallPurgeAndAddAll() throws IOException, ReferentielException { + doNothing().when(referentielFacade).purgeAndAddAll(any()); + referentielRestApi.importCSV(new MockMultipartFile("Name", "AAAAAAAA".getBytes(StandardCharsets.UTF_8))); + verify(referentielFacade).purgeAndAddAll(any()); + } + + @Test + void importCSV_whenEmptyFileThenShouldThrowException() throws ReferentielException { + doNothing().when(referentielFacade).purgeAndAddAll(any()); + MockMultipartFile file = new MockMultipartFile("Name", (byte[]) null); + ResponseStatusException responseStatusException = assertThrows(ResponseStatusException.class, () -> referentielRestApi.importCSV(file)); + assertEquals(HttpStatus.BAD_REQUEST, responseStatusException.getStatusCode()); + assertEquals("Le fichier n'existe pas ou alors il est vide", responseStatusException.getReason()); + } + + @Test + void importCSV_whenNullFileThenShouldThrowException() throws ReferentielException { + doNothing().when(referentielFacade).purgeAndAddAll(any()); + ResponseStatusException responseStatusException = assertThrows(ResponseStatusException.class, () -> referentielRestApi.importCSV(null)); + assertEquals(HttpStatus.BAD_REQUEST, responseStatusException.getStatusCode()); + assertEquals("Le fichier n'existe pas ou alors il est vide", responseStatusException.getReason()); + } + + @Test + void exportCSV_shouldReturnCSVFile() { + var servletResponse = new MockHttpServletResponse(); + + assertDoesNotThrow(() -> referentielRestApi.exportCSV(servletResponse)); + + assertEquals("text/csv;charset=UTF-8", servletResponse.getContentType()); + String headerContentDisposition = servletResponse.getHeader("Content-Disposition"); + assertNotNull(headerContentDisposition); + assertTrue(headerContentDisposition.contains("attachment; filename=impactReseaux-")); + assertEquals(200, servletResponse.getStatus()); + + } +} + diff --git a/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/mixelectrique/ReferentielMixElectriqueRestApiImplTest.java b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/mixelectrique/ReferentielMixElectriqueRestApiImplTest.java new file mode 100644 index 00000000..5cf890c6 --- /dev/null +++ b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/mixelectrique/ReferentielMixElectriqueRestApiImplTest.java @@ -0,0 +1,108 @@ +package org.mte.numecoeval.referentiel.infrastructure.restapi.controller.mixelectrique; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.MixElectriqueDTO; +import org.mte.numecoeval.referentiel.domain.exception.ReferentielException; +import org.mte.numecoeval.referentiel.infrastructure.adapter.export.MixElectriqueCsvExportService; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.id.MixElectriqueIdDTO; +import org.mte.numecoeval.referentiel.infrastructure.restapi.facade.MixElectriqueFacade; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.HttpStatus; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.web.server.ResponseStatusException; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ContextConfiguration(classes = {ReferentielMixElecRestApiImpl.class}) +@ExtendWith(SpringExtension.class) +class ReferentielMixElectriqueRestApiImplTest { + @Autowired + private ReferentielMixElecRestApiImpl referentielRestApi; + + @MockBean + private MixElectriqueFacade referentielFacade; + + @MockBean + private MixElectriqueCsvExportService csvExportService; + + @Test + void get_shouldCallFacadeGetAndReturnMatchingDTO() throws ReferentielException { + String pays = "France"; + String nomCritere = "Changement Climatique"; + var idDTO = new MixElectriqueIdDTO(pays, + nomCritere + ); + var expectedDTO = MixElectriqueDTO.builder() + .critere(nomCritere) + .pays(pays) + .source("Test") + .valeur(1.0).build(); + + when(referentielFacade.get(idDTO)).thenReturn(expectedDTO); + + var receivedDTO = referentielRestApi.get(pays, nomCritere); + assertSame(receivedDTO, receivedDTO); + verify(referentielFacade).get(idDTO); + } + + @Test + void get_whenNotFound_thenShouldThrowReferentielException() throws ReferentielException { + String pays = "France"; + String nomCritere = "Changement Climatique"; + var idDTO = new MixElectriqueIdDTO(pays, + nomCritere + ); + + when(referentielFacade.get(idDTO)).thenThrow(new ReferentielException("Mix Electrique non trouvé")); + var exception = assertThrows(ReferentielException.class,() -> referentielRestApi.get(pays, nomCritere)); + assertEquals("Mix Electrique non trouvé", exception.getMessage()); + } + + @Test + void importCSV_shouldCallPurgeAndAddAll() throws IOException, ReferentielException { + doNothing().when(referentielFacade).purgeAndAddAll(any()); + referentielRestApi.importCSV(new MockMultipartFile("Name", "AAAAAAAA".getBytes(StandardCharsets.UTF_8))); + verify(referentielFacade).purgeAndAddAll(any()); + } + + @Test + void importCSV_whenEmptyFileThenShouldThrowException() throws ReferentielException { + doNothing().when(referentielFacade).purgeAndAddAll(any()); + MockMultipartFile file = new MockMultipartFile("Name", (byte[]) null); + ResponseStatusException responseStatusException = assertThrows(ResponseStatusException.class, () -> referentielRestApi.importCSV(file)); + assertEquals(HttpStatus.BAD_REQUEST, responseStatusException.getStatusCode()); + assertEquals("Le fichier n'existe pas ou alors il est vide", responseStatusException.getReason()); + } + + @Test + void importCSV_whenNullFileThenShouldThrowException() throws ReferentielException { + doNothing().when(referentielFacade).purgeAndAddAll(any()); + ResponseStatusException responseStatusException = assertThrows(ResponseStatusException.class, () -> referentielRestApi.importCSV(null)); + assertEquals(HttpStatus.BAD_REQUEST, responseStatusException.getStatusCode()); + assertEquals("Le fichier n'existe pas ou alors il est vide", responseStatusException.getReason()); + } + + @Test + void exportCSV_shouldReturnCSVFile() { + var servletResponse = new MockHttpServletResponse(); + + assertDoesNotThrow(() -> referentielRestApi.exportCSV(servletResponse)); + + assertEquals("text/csv;charset=UTF-8", servletResponse.getContentType()); + String headerContentDisposition = servletResponse.getHeader("Content-Disposition"); + assertNotNull(headerContentDisposition); + assertTrue(headerContentDisposition.contains("attachment; filename=mixElec-")); + assertEquals(200, servletResponse.getStatus()); + + } +} + diff --git a/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/typeequipement/ReferentielTypeEquipementRestApiImplTest.java b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/typeequipement/ReferentielTypeEquipementRestApiImplTest.java new file mode 100644 index 00000000..41fb1b49 --- /dev/null +++ b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/restapi/controller/typeequipement/ReferentielTypeEquipementRestApiImplTest.java @@ -0,0 +1,97 @@ +package org.mte.numecoeval.referentiel.infrastructure.restapi.controller.typeequipement; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.TypeEquipementDTO; +import org.mte.numecoeval.referentiel.domain.exception.ReferentielException; +import org.mte.numecoeval.referentiel.factory.TestDataFactory; +import org.mte.numecoeval.referentiel.infrastructure.adapter.export.TypeEquipementCsvExportService; +import org.mte.numecoeval.referentiel.infrastructure.restapi.facade.TypeEquipementFacade; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.HttpStatus; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.web.server.ResponseStatusException; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ContextConfiguration(classes = {ReferentielTypeEquipementRestApiImpl.class}) +@ExtendWith(SpringExtension.class) +class ReferentielTypeEquipementRestApiImplTest { + @Autowired + private ReferentielTypeEquipementRestApiImpl referentielRestApi; + + @MockBean + private TypeEquipementFacade referentielFacade; + + @MockBean + private TypeEquipementCsvExportService csvExportService; + + @Test + void getAll_shouldCallFacadeGetAllAndReturnAllDTOs() { + ArrayList<TypeEquipementDTO> typeEquipementDTOS = new ArrayList<>(); + when(referentielFacade.getAllTypesEquipement()).thenReturn(typeEquipementDTOS); + List<TypeEquipementDTO> actualAll = referentielRestApi.getTypesEquipement(); + assertSame(typeEquipementDTOS, actualAll); + assertTrue(actualAll.isEmpty()); + verify(referentielFacade).getAllTypesEquipement(); + } + + @Test + void getTypeEquipement_shouldCallFacadeGetAndReturnDTO() throws ReferentielException { + String expectedType = "Switch"; + TypeEquipementDTO expectedDTO = TestDataFactory.TypeEquipementFactory.dto(expectedType, true, 1.0, "test", "test","test"); + when(referentielFacade.getTypeEquipementForType(expectedType)).thenReturn(expectedDTO); + TypeEquipementDTO actualResponse = referentielRestApi.getTypeEquipement(expectedType); + assertSame(expectedDTO, actualResponse); + verify(referentielFacade).getTypeEquipementForType(expectedType); + } + + @Test + void importCSV_shouldCallPurgeAndAddAll() throws IOException, ReferentielException { + doNothing().when(referentielFacade).purgeAndAddAll(any()); + referentielRestApi.importCSV(new MockMultipartFile("Name", "AAAAAAAA".getBytes(StandardCharsets.UTF_8))); + verify(referentielFacade).purgeAndAddAll(any()); + } + + @Test + void importCSV_whenEmptyFileThenShouldThrowException() throws ReferentielException { + doNothing().when(referentielFacade).purgeAndAddAll(any()); + MockMultipartFile file = new MockMultipartFile("Name", (byte[]) null); + ResponseStatusException responseStatusException = assertThrows(ResponseStatusException.class, () -> referentielRestApi.importCSV(file)); + assertEquals(HttpStatus.BAD_REQUEST, responseStatusException.getStatusCode()); + assertEquals("Le fichier n'existe pas ou alors il est vide", responseStatusException.getReason()); + } + + @Test + void importCSV_whenNullFileThenShouldThrowException() throws ReferentielException { + doNothing().when(referentielFacade).purgeAndAddAll(any()); + ResponseStatusException responseStatusException = assertThrows(ResponseStatusException.class, () -> referentielRestApi.importCSV(null)); + assertEquals(HttpStatus.BAD_REQUEST, responseStatusException.getStatusCode()); + assertEquals("Le fichier n'existe pas ou alors il est vide", responseStatusException.getReason()); + } + + @Test + void exportCSV_shouldReturnCSVFile() { + var servletResponse = new MockHttpServletResponse(); + + assertDoesNotThrow(() -> referentielRestApi.exportCSV(servletResponse)); + + assertEquals("text/csv;charset=UTF-8", servletResponse.getContentType()); + String headerContentDisposition = servletResponse.getHeader("Content-Disposition"); + assertNotNull(headerContentDisposition); + assertTrue(headerContentDisposition.contains("attachment; filename=typeEquipement-")); + assertEquals(200, servletResponse.getStatus()); + + } +} diff --git a/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/restapi/facade/CorrespondanceRefEquipementFacadeTest.java b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/restapi/facade/CorrespondanceRefEquipementFacadeTest.java new file mode 100644 index 00000000..b43464fb --- /dev/null +++ b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/restapi/facade/CorrespondanceRefEquipementFacadeTest.java @@ -0,0 +1,112 @@ +package org.mte.numecoeval.referentiel.infrastructure.restapi.facade; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.mte.numecoeval.referentiel.domain.exception.ReferentielException; +import org.mte.numecoeval.referentiel.domain.model.CorrespondanceRefEquipement; +import org.mte.numecoeval.referentiel.domain.ports.output.ReferentielPersistencePort; +import org.mte.numecoeval.referentiel.factory.TestDataFactory; +import org.mte.numecoeval.referentiel.infrastructure.mapper.CorrespondanceRefEquipementMapper; +import org.mte.numecoeval.referentiel.infrastructure.mapper.CorrespondanceRefEquipementMapperImpl; +import org.springframework.test.util.ReflectionTestUtils; + +import java.util.Arrays; +import java.util.Collection; + +import static org.junit.jupiter.api.Assertions.*; + +class CorrespondanceRefEquipementFacadeTest { + + @InjectMocks + CorrespondanceRefEquipementFacade facadeToTest; + + @Mock + private ReferentielPersistencePort<CorrespondanceRefEquipement, String> persistencePort; + + private CorrespondanceRefEquipementMapper mapper = new CorrespondanceRefEquipementMapperImpl(); + + @BeforeEach + void setup() { + MockitoAnnotations.openMocks(this); + ReflectionTestUtils.setField(facadeToTest, "mapper", mapper); + } + + @Test + void get_shouldReturnMatchingDTO() throws ReferentielException { + var wantedDTOId = "modele"; + var expectedDomainID = "modele"; + var expectedDomain = TestDataFactory.CorrespondanceRefEquipementFactory.domain( + "modele", "refCible"); + + Mockito.when(persistencePort.get(expectedDomainID)).thenReturn(expectedDomain); + + var result = Assertions.assertDoesNotThrow(() -> facadeToTest.get(wantedDTOId)); + + assertEquals(expectedDomain.getModeleEquipementSource(), result.getModeleEquipementSource()); + assertEquals(expectedDomain.getRefEquipementCible(), result.getRefEquipementCible()); + } + + @Test + void get_withNonMachingDTOShouldReturnNull() throws ReferentielException { + var wantedDTOId = "modele"; + var expectedDomainID = "modele"; + + Mockito.when(persistencePort.get(expectedDomainID)).thenReturn(null); + + var result = Assertions.assertDoesNotThrow(() -> facadeToTest.get(wantedDTOId)); + + assertNull(result); + } + + @Test + void getAll_shouldReturnMatchingAllDTOs() throws ReferentielException { + var expectedDomains = Arrays.asList( + TestDataFactory.CorrespondanceRefEquipementFactory.domain("modele", "refCible"), + TestDataFactory.CorrespondanceRefEquipementFactory.domain("modele2", "refCible") + ); + + Mockito.when(persistencePort.getAll()).thenReturn(expectedDomains); + + var result = Assertions.assertDoesNotThrow(() -> facadeToTest.getAll()); + + assertEquals(2, result.size()); + } + + @Test + void purgeAndAddAll_ShouldCallPurgeThenSaveAll() throws ReferentielException { + var dtosToSave = Arrays.asList( + TestDataFactory.CorrespondanceRefEquipementFactory.dto( + "modele01", "refCible" + ), + TestDataFactory.CorrespondanceRefEquipementFactory.dto( + "modele02", "refCible" + ) + ); + ArgumentCaptor<Collection<CorrespondanceRefEquipement>> valueCapture = ArgumentCaptor.forClass(Collection.class); + + assertDoesNotThrow(() -> facadeToTest.purgeAndAddAll(dtosToSave)); + + Mockito.verify(persistencePort, Mockito.times(1)).purge(); + Mockito.verify(persistencePort, Mockito.times(1)).saveAll(valueCapture.capture()); + + var expectedDomains = valueCapture.getValue(); + assertNotNull(expectedDomains); + assertEquals(dtosToSave.size(), expectedDomains.size()); + dtosToSave.forEach(dto -> { + var matchingDomain = expectedDomains.stream() + .filter(domain -> dto.getModeleEquipementSource().equals(domain.getModeleEquipementSource())) + .findAny(); + assertTrue(matchingDomain.isPresent()); + var domain = matchingDomain.get(); + + assertEquals(dto.getModeleEquipementSource(), domain.getModeleEquipementSource()); + assertEquals(dto.getRefEquipementCible(), domain.getRefEquipementCible()); + }); + } +} diff --git a/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/restapi/facade/CritereFacadeTest.java b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/restapi/facade/CritereFacadeTest.java new file mode 100644 index 00000000..b38d1388 --- /dev/null +++ b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/restapi/facade/CritereFacadeTest.java @@ -0,0 +1,96 @@ +package org.mte.numecoeval.referentiel.infrastructure.restapi.facade; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.mte.numecoeval.referentiel.domain.exception.ReferentielException; +import org.mte.numecoeval.referentiel.domain.model.Critere; +import org.mte.numecoeval.referentiel.domain.model.id.CritereId; +import org.mte.numecoeval.referentiel.domain.ports.output.ReferentielPersistencePort; +import org.mte.numecoeval.referentiel.factory.TestDataFactory; +import org.mte.numecoeval.referentiel.infrastructure.mapper.CritereMapper; +import org.mte.numecoeval.referentiel.infrastructure.mapper.CritereMapperImpl; +import org.springframework.test.util.ReflectionTestUtils; + +import java.util.Arrays; +import java.util.Collection; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mte.numecoeval.referentiel.factory.TestDataFactory.DEFAULT_CRITERE; +import static org.mte.numecoeval.referentiel.factory.TestDataFactory.DEFAULT_UNITE; + +class CritereFacadeTest { + + @InjectMocks + CritereFacade facadeToTest; + + @Mock + private ReferentielPersistencePort<Critere, CritereId> persistencePort; + + private CritereMapper mapper = new CritereMapperImpl(); + + @BeforeEach + void setup() { + MockitoAnnotations.openMocks(this); + ReflectionTestUtils.setField(facadeToTest, "mapper", mapper); + } + + @Test + void getAll_ShouldAllReturnMatchingDTO() { + var expectedDomains = Arrays.asList( + TestDataFactory.CritereFactory.domain(DEFAULT_CRITERE, "test", DEFAULT_UNITE), + TestDataFactory.CritereFactory.domain("Acidification", "Autre Test", "m3 eau") + ); + + Mockito.when(persistencePort.getAll()).thenReturn(expectedDomains); + + var result = assertDoesNotThrow( () -> facadeToTest.getAll() ); + + assertEquals(expectedDomains.size(), result.size()); + + expectedDomains.forEach( expectedDomain -> { + var matchingDTO = result.stream() + .filter(critereDTO -> expectedDomain.getNomCritere().equals(critereDTO.getNomCritere())) + .findAny(); + + assertTrue(matchingDTO.isPresent(), "Il n'existe pas de DTO correspondant au domain"); + var resultDTO = matchingDTO.get(); + assertEquals(expectedDomain.getNomCritere(), resultDTO.getNomCritere()); + assertEquals(expectedDomain.getDescription(), resultDTO.getDescription()); + assertEquals(expectedDomain.getUnite(), resultDTO.getUnite()); + + }); + } + + @Test + void purgeAndAddAll_ShouldCallPurgeThenSaveAll() throws ReferentielException { + var dtosToSave = Arrays.asList( + TestDataFactory.CritereFactory.dto(DEFAULT_CRITERE, "test", DEFAULT_UNITE), + TestDataFactory.CritereFactory.dto("Acidification", "Autre Test", "m3 eau") + ); + ArgumentCaptor<Collection<Critere>> valueCapture = ArgumentCaptor.forClass(Collection.class); + + assertDoesNotThrow(() -> facadeToTest.purgeAndAddAll(dtosToSave)); + + Mockito.verify(persistencePort, Mockito.times(1)).purge(); + Mockito.verify(persistencePort, Mockito.times(1)).saveAll(valueCapture.capture()); + + var expectedDomains = valueCapture.getValue(); + assertNotNull(expectedDomains); + assertEquals(dtosToSave.size(), expectedDomains.size()); + dtosToSave.forEach(dto -> { + var matchingDomain = expectedDomains.stream() + .filter(domain -> dto.getNomCritere().equals(domain.getNomCritere())) + .findAny(); + assertTrue(matchingDomain.isPresent()); + var critere = matchingDomain.get(); + assertEquals(dto.getNomCritere(), critere.getNomCritere()); + assertEquals(dto.getDescription(), critere.getDescription()); + assertEquals(dto.getUnite(), critere.getUnite()); + }); + } +} diff --git a/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/restapi/facade/EtapeFacadeTest.java b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/restapi/facade/EtapeFacadeTest.java new file mode 100644 index 00000000..5514c7ba --- /dev/null +++ b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/restapi/facade/EtapeFacadeTest.java @@ -0,0 +1,92 @@ +package org.mte.numecoeval.referentiel.infrastructure.restapi.facade; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.mte.numecoeval.referentiel.domain.exception.ReferentielException; +import org.mte.numecoeval.referentiel.domain.model.Etape; +import org.mte.numecoeval.referentiel.domain.model.id.EtapeId; +import org.mte.numecoeval.referentiel.domain.ports.output.ReferentielPersistencePort; +import org.mte.numecoeval.referentiel.factory.TestDataFactory; +import org.mte.numecoeval.referentiel.infrastructure.mapper.EtapeMapper; +import org.mte.numecoeval.referentiel.infrastructure.mapper.EtapeMapperImpl; +import org.springframework.test.util.ReflectionTestUtils; + +import java.util.Arrays; +import java.util.Collection; + +import static org.junit.jupiter.api.Assertions.*; + +class EtapeFacadeTest { + + @InjectMocks + EtapeFacade facadeToTest; + + @Mock + private ReferentielPersistencePort<Etape, EtapeId> persistencePort; + + private EtapeMapper mapper = new EtapeMapperImpl(); + + @BeforeEach + void setup() { + MockitoAnnotations.openMocks(this); + ReflectionTestUtils.setField(facadeToTest, "mapper", mapper); + } + + @Test + void getAll_ShouldAllReturnMatchingDTO() { + var expectedDomains = Arrays.asList( + TestDataFactory.EtapeFactory.domain(TestDataFactory.DEFAULT_CRITERE, "test"), + TestDataFactory.EtapeFactory.domain("Acidification", "Autre Test") + ); + + Mockito.when(persistencePort.getAll()).thenReturn(expectedDomains); + + var result = assertDoesNotThrow( () -> facadeToTest.getAll() ); + + assertEquals(expectedDomains.size(), result.size()); + + expectedDomains.forEach( expectedDomain -> { + var matchingDTO = result.stream() + .filter(dto -> expectedDomain.getCode().equals(dto.getCode())) + .findAny(); + + assertTrue(matchingDTO.isPresent(), "Il n'existe pas de DTO correspondant au domain"); + var resultDTO = matchingDTO.get(); + assertEquals(expectedDomain.getCode(), resultDTO.getCode()); + assertEquals(expectedDomain.getLibelle(), resultDTO.getLibelle()); + + }); + } + + @Test + void purgeAndAddAll_ShouldCallPurgeThenSaveAll() throws ReferentielException { + var dtosToSave = Arrays.asList( + TestDataFactory.EtapeFactory.dto(TestDataFactory.DEFAULT_CRITERE, "test"), + TestDataFactory.EtapeFactory.dto("Acidification", "Autre Test") + ); + ArgumentCaptor<Collection<Etape>> valueCapture = ArgumentCaptor.forClass(Collection.class); + + assertDoesNotThrow(() -> facadeToTest.purgeAndAddAll(dtosToSave)); + + Mockito.verify(persistencePort, Mockito.times(1)).purge(); + Mockito.verify(persistencePort, Mockito.times(1)).saveAll(valueCapture.capture()); + + var expectedCriteres = valueCapture.getValue(); + assertNotNull(expectedCriteres); + assertEquals(dtosToSave.size(), expectedCriteres.size()); + dtosToSave.forEach(dto -> { + var matchingCriteres = expectedCriteres.stream() + .filter(domain -> dto.getCode().equals(domain.getCode())) + .findAny(); + assertTrue(matchingCriteres.isPresent()); + var critere = matchingCriteres.get(); + assertEquals(dto.getCode(), critere.getCode()); + assertEquals(dto.getLibelle(), critere.getLibelle()); + }); + } +} diff --git a/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/restapi/facade/HypotheseFacadeTest.java b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/restapi/facade/HypotheseFacadeTest.java new file mode 100644 index 00000000..8e40c311 --- /dev/null +++ b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/restapi/facade/HypotheseFacadeTest.java @@ -0,0 +1,109 @@ +package org.mte.numecoeval.referentiel.infrastructure.restapi.facade; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.mte.numecoeval.referentiel.domain.exception.ReferentielException; +import org.mte.numecoeval.referentiel.domain.model.Hypothese; +import org.mte.numecoeval.referentiel.domain.model.id.HypotheseId; +import org.mte.numecoeval.referentiel.domain.ports.output.ReferentielPersistencePort; +import org.mte.numecoeval.referentiel.factory.TestDataFactory; +import org.mte.numecoeval.referentiel.infrastructure.mapper.HypotheseMapper; +import org.mte.numecoeval.referentiel.infrastructure.mapper.HypotheseMapperImpl; +import org.springframework.test.util.ReflectionTestUtils; + +import java.util.Arrays; +import java.util.Collection; + +import static org.junit.jupiter.api.Assertions.*; + +class HypotheseFacadeTest { + + @InjectMocks + HypotheseFacade facadeToTest; + + @Mock + private ReferentielPersistencePort<Hypothese, HypotheseId> persistencePort; + + private HypotheseMapper mapper = new HypotheseMapperImpl(); + + @BeforeEach + void setup() { + MockitoAnnotations.openMocks(this); + ReflectionTestUtils.setField(facadeToTest, "mapper", mapper); + } + + @Test + void get_shouldReturnMatchingDTO() throws ReferentielException { + var wantedDTOId = TestDataFactory.HypotheseFactory.idDTO("code"); + var expectedDomainID = TestDataFactory.HypotheseFactory.idDomain("code"); + var expectedDomain = TestDataFactory.HypotheseFactory.domain("code", "1.1", "Test"); + + Mockito.when(persistencePort.get(expectedDomainID)).thenReturn(expectedDomain); + + var result = assertDoesNotThrow(() -> facadeToTest.get(wantedDTOId)); + + assertEquals(expectedDomain.getCode(), result.getCode()); + assertEquals(expectedDomain.getValeur(), result.getValeur()); + assertEquals(expectedDomain.getSource(), result.getSource()); + } + + @Test + void getAll_ShouldAllReturnMatchingDTO() { + var expectedDomains = Arrays.asList( + TestDataFactory.HypotheseFactory.domain("code", "2.0", "Test"), + TestDataFactory.HypotheseFactory.domain("code2", "3.0", "Test") + ); + + Mockito.when(persistencePort.getAll()).thenReturn(expectedDomains); + + var result = assertDoesNotThrow( () -> facadeToTest.getAll() ); + + assertEquals(expectedDomains.size(), result.size()); + + expectedDomains.forEach( expectedDomain -> { + var matchingDTO = result.stream() + .filter(critereDTO -> expectedDomain.getCode().equals(critereDTO.getCode())) + .findAny(); + + assertTrue(matchingDTO.isPresent(), "Il n'existe pas de DTO correspondant au domain"); + var resultDTO = matchingDTO.get(); + assertEquals(expectedDomain.getCode(), resultDTO.getCode()); + assertEquals(expectedDomain.getValeur(), resultDTO.getValeur()); + assertEquals(expectedDomain.getSource(), resultDTO.getSource()); + + }); + } + + @Test + void purgeAndAddAll_ShouldCallPurgeThenSaveAll() throws ReferentielException { + var dtosToSave = Arrays.asList( + TestDataFactory.HypotheseFactory.dto("hyp", "1.0", "Test"), + TestDataFactory.HypotheseFactory.dto("hyp2", "m3", "Autre Test") + ); + ArgumentCaptor<Collection<Hypothese>> valueCapture = ArgumentCaptor.forClass(Collection.class); + + assertDoesNotThrow(() -> facadeToTest.purgeAndAddAll(dtosToSave)); + + Mockito.verify(persistencePort, Mockito.times(1)).purge(); + Mockito.verify(persistencePort, Mockito.times(1)).saveAll(valueCapture.capture()); + + var expectedDomains = valueCapture.getValue(); + assertNotNull(expectedDomains); + assertEquals(dtosToSave.size(), expectedDomains.size()); + dtosToSave.forEach(dto -> { + var matchingCriteres = expectedDomains.stream() + .filter(domain -> dto.getCode().equals(domain.getCode())) + .findAny(); + assertTrue(matchingCriteres.isPresent()); + var critere = matchingCriteres.get(); + assertEquals(dto.getCode(), critere.getCode()); + assertEquals(dto.getSource(), critere.getSource()); + assertEquals(dto.getValeur(), critere.getValeur()); + }); + } +} diff --git a/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/restapi/facade/ImpactEquipementFacadeTest.java b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/restapi/facade/ImpactEquipementFacadeTest.java new file mode 100644 index 00000000..19f6fe32 --- /dev/null +++ b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/restapi/facade/ImpactEquipementFacadeTest.java @@ -0,0 +1,113 @@ +package org.mte.numecoeval.referentiel.infrastructure.restapi.facade; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.mte.numecoeval.referentiel.domain.exception.ReferentielException; +import org.mte.numecoeval.referentiel.domain.model.ImpactEquipement; +import org.mte.numecoeval.referentiel.domain.model.id.ImpactEquipementId; +import org.mte.numecoeval.referentiel.domain.ports.output.ReferentielPersistencePort; +import org.mte.numecoeval.referentiel.factory.TestDataFactory; +import org.mte.numecoeval.referentiel.infrastructure.mapper.ImpactEquipementMapper; +import org.mte.numecoeval.referentiel.infrastructure.mapper.ImpactEquipementMapperImpl; +import org.springframework.test.util.ReflectionTestUtils; + +import java.util.Arrays; +import java.util.Collection; + +import static org.junit.jupiter.api.Assertions.*; + +class ImpactEquipementFacadeTest { + + @InjectMocks + ImpactEquipementFacade facadeToTest; + + @Mock + private ReferentielPersistencePort<ImpactEquipement, ImpactEquipementId> persistencePort; + + private ImpactEquipementMapper mapper = new ImpactEquipementMapperImpl(); + + @BeforeEach + void setup() { + MockitoAnnotations.openMocks(this); + ReflectionTestUtils.setField(facadeToTest, "mapper", mapper); + } + + @Test + void get_shouldReturnMatchingDTO() throws ReferentielException { + var wantedDTOId = TestDataFactory.ImpactEquipementFactory.idDTO( + TestDataFactory.DEFAULT_ETAPE, + TestDataFactory.DEFAULT_CRITERE, + "Ecran 27 pouces" + ); + var expectedDomainID = TestDataFactory.ImpactEquipementFactory.idDomain( + TestDataFactory.DEFAULT_ETAPE, + TestDataFactory.DEFAULT_CRITERE, + "Ecran 27 pouces" + ); + var expectedDomain = TestDataFactory.ImpactEquipementFactory.domain( + TestDataFactory.EtapeFactory.domain(TestDataFactory.DEFAULT_ETAPE, "Test"), + TestDataFactory.CritereFactory.domain(TestDataFactory.DEFAULT_CRITERE, TestDataFactory.DEFAULT_UNITE, TestDataFactory.DEFAULT_CRITERE), + "Ecran 27 pouces", "Test", "Monitor", 0.12, 0.0012 + ); + Mockito.when(persistencePort.get(expectedDomainID)).thenReturn(expectedDomain); + + var dto = assertDoesNotThrow(() -> facadeToTest.get(wantedDTOId)); + + Assertions.assertEquals(expectedDomain.getEtape(), dto.getEtape()); + Assertions.assertEquals(expectedDomain.getCritere(), dto.getCritere()); + Assertions.assertEquals(expectedDomain.getRefEquipement(), dto.getRefEquipement()); + Assertions.assertEquals(expectedDomain.getType(), dto.getType()); + Assertions.assertEquals(expectedDomain.getSource(), dto.getSource()); + Assertions.assertEquals(expectedDomain.getValeur(), dto.getValeur()); + Assertions.assertEquals(expectedDomain.getConsoElecMoyenne(), dto.getConsoElecMoyenne()); + } + + @Test + void purgeAndAddAll_ShouldCallPurgeThenSaveAll() throws ReferentielException { + var dtosToSave = Arrays.asList( + TestDataFactory.ImpactEquipementFactory.dto( + TestDataFactory.DEFAULT_ETAPE, + TestDataFactory.DEFAULT_CRITERE, + "Ecran 27 pouces", "Test", "Monitor", 0.12, 0.0012, + "test"), + TestDataFactory.ImpactEquipementFactory.dto( + "FIN_DE_VIE", + "Acidification", + "Ecran 27 pouces", "Test", "Monitor", 0.12, 0.0012, + "test") + ); + ArgumentCaptor<Collection<ImpactEquipement>> valueCapture = ArgumentCaptor.forClass(Collection.class); + + assertDoesNotThrow(() -> facadeToTest.purgeAndAddAll(dtosToSave)); + + Mockito.verify(persistencePort, Mockito.times(1)).purge(); + Mockito.verify(persistencePort, Mockito.times(1)).saveAll(valueCapture.capture()); + + var expectedDomains = valueCapture.getValue(); + assertNotNull(expectedDomains); + assertEquals(dtosToSave.size(), expectedDomains.size()); + dtosToSave.forEach(dto -> { + var matchingDomain = expectedDomains.stream() + .filter(domain -> + dto.getEtape().equals(domain.getEtape()) + && dto.getCritere().equals(domain.getCritere()) + && dto.getRefEquipement().equals(domain.getRefEquipement())) + .findAny(); + assertTrue(matchingDomain.isPresent()); + var domain = matchingDomain.get(); + assertEquals(dto.getEtape(), domain.getEtape()); + assertEquals(dto.getCritere(), domain.getCritere()); + assertEquals(dto.getRefEquipement(), domain.getRefEquipement()); + assertEquals(dto.getType(), domain.getType()); + assertEquals(dto.getSource(), domain.getSource()); + assertEquals(dto.getValeur(), domain.getValeur()); + assertEquals(dto.getConsoElecMoyenne(), domain.getConsoElecMoyenne()); + }); + } +} diff --git a/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/restapi/facade/ImpactMessagerieFacadeTest.java b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/restapi/facade/ImpactMessagerieFacadeTest.java new file mode 100644 index 00000000..5c9fc73c --- /dev/null +++ b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/restapi/facade/ImpactMessagerieFacadeTest.java @@ -0,0 +1,126 @@ +package org.mte.numecoeval.referentiel.infrastructure.restapi.facade; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.mte.numecoeval.referentiel.domain.exception.ReferentielException; +import org.mte.numecoeval.referentiel.domain.model.ImpactMessagerie; +import org.mte.numecoeval.referentiel.domain.ports.output.ReferentielPersistencePort; +import org.mte.numecoeval.referentiel.factory.TestDataFactory; +import org.mte.numecoeval.referentiel.infrastructure.mapper.ImpactMessagerieMapper; +import org.mte.numecoeval.referentiel.infrastructure.mapper.ImpactMessagerieMapperImpl; +import org.springframework.test.util.ReflectionTestUtils; + +import java.util.Arrays; +import java.util.Collection; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mte.numecoeval.referentiel.factory.TestDataFactory.DEFAULT_CRITERE; +import static org.mte.numecoeval.referentiel.factory.TestDataFactory.DEFAULT_UNITE; + +class ImpactMessagerieFacadeTest { + + @InjectMocks + ImpactMessagerieFacade facadeToTest; + + @Mock + private ReferentielPersistencePort<ImpactMessagerie, String> persistencePort; + + private ImpactMessagerieMapper mapper = new ImpactMessagerieMapperImpl(); + + @BeforeEach + void setup() { + MockitoAnnotations.openMocks(this); + ReflectionTestUtils.setField(facadeToTest, "mapper", mapper); + } + + @Test + void get_shouldReturnMatchingDTO() throws ReferentielException { + var wantedDTOId = DEFAULT_CRITERE; + var expectedDomainID = DEFAULT_CRITERE; + var expectedDomain = TestDataFactory.ImpactMessagerieFactory.domain( + TestDataFactory.CritereFactory.domain(DEFAULT_CRITERE, DEFAULT_UNITE, DEFAULT_CRITERE), + 2.0, 0.0005, "test" + ); + + Mockito.when(persistencePort.get(expectedDomainID)).thenReturn(expectedDomain); + + var result = Assertions.assertDoesNotThrow(() -> facadeToTest.getImpactMessagerieForCritere(wantedDTOId)); + + assertEquals(expectedDomain.getCritere(), result.getCritere()); + assertEquals(expectedDomain.getConstanteCoefficientDirecteur(), result.getConstanteCoefficientDirecteur()); + assertEquals(expectedDomain.getConstanteOrdonneeOrigine(), result.getConstanteOrdonneeOrigine()); + assertEquals(expectedDomain.getSource(), result.getSource()); + } + + @Test + void purgeAndAddAll_ShouldCallPurgeThenSaveAll() throws ReferentielException { + var dtosToSave = Arrays.asList( + TestDataFactory.ImpactMessagerieFactory.dto( + TestDataFactory.CritereFactory.dto(DEFAULT_CRITERE, DEFAULT_UNITE, DEFAULT_CRITERE), + 2.0, 0.0005, "test" + ), + TestDataFactory.ImpactMessagerieFactory.dto( + TestDataFactory.CritereFactory.dto("Acidification", "m3", "Autre Test"), + 0.005051, 0.0000000523, "test" + ) + ); + ArgumentCaptor<Collection<ImpactMessagerie>> valueCapture = ArgumentCaptor.forClass(Collection.class); + + assertDoesNotThrow(() -> facadeToTest.purgeAndAddAll(dtosToSave)); + + Mockito.verify(persistencePort, Mockito.times(1)).purge(); + Mockito.verify(persistencePort, Mockito.times(1)).saveAll(valueCapture.capture()); + + var expectedDomains = valueCapture.getValue(); + assertNotNull(expectedDomains); + assertEquals(dtosToSave.size(), expectedDomains.size()); + dtosToSave.forEach(dto -> { + var matchingDomain = expectedDomains.stream() + .filter(domain -> dto.getCritere().equals(domain.getCritere())) + .findAny(); + assertTrue(matchingDomain.isPresent()); + var domain = matchingDomain.get(); + assertEquals(dto.getCritere(), domain.getCritere()); + assertEquals(dto.getConstanteCoefficientDirecteur(), domain.getConstanteCoefficientDirecteur()); + assertEquals(dto.getConstanteOrdonneeOrigine(), domain.getConstanteOrdonneeOrigine()); + assertEquals(dto.getSource(), domain.getSource()); + }); + } + + @Test + void getAllImpactMessagerie_ShouldReturnAllDataAvailable() throws ReferentielException { + var domainsAvailable = Arrays.asList( + TestDataFactory.ImpactMessagerieFactory.domain( + TestDataFactory.CritereFactory.domain(DEFAULT_CRITERE, DEFAULT_UNITE, DEFAULT_CRITERE), + 2.0, 0.0005, "test" + ), + TestDataFactory.ImpactMessagerieFactory.domain( + TestDataFactory.CritereFactory.domain("Acidification", "m3", "Autre Test"), + 0.005051, 0.0000000523, "test" + ) + ); + Mockito.when(persistencePort.getAll()).thenReturn(domainsAvailable); + + var expectedDTOs = Assertions.assertDoesNotThrow(() -> facadeToTest.getAllImpactMessagerie()); + + Mockito.verify(persistencePort, Mockito.times(1)).getAll(); + + domainsAvailable.forEach(domain -> { + var matchingDTO = expectedDTOs.stream() + .filter(dto -> domain.getCritere().equals(dto.getCritere())) + .findAny(); + assertTrue(matchingDTO.isPresent()); + var dto = matchingDTO.get(); + assertEquals(domain.getCritere(), dto.getCritere()); + assertEquals(domain.getConstanteCoefficientDirecteur(), dto.getConstanteCoefficientDirecteur()); + assertEquals(domain.getConstanteOrdonneeOrigine(), dto.getConstanteOrdonneeOrigine()); + assertEquals(domain.getSource(), dto.getSource()); + }); + } +} diff --git a/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/restapi/facade/ImpactReseauFacadeTest.java b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/restapi/facade/ImpactReseauFacadeTest.java new file mode 100644 index 00000000..3587b1fc --- /dev/null +++ b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/restapi/facade/ImpactReseauFacadeTest.java @@ -0,0 +1,154 @@ +package org.mte.numecoeval.referentiel.infrastructure.restapi.facade; + +import org.hibernate.exception.ConstraintViolationException; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.mte.numecoeval.referentiel.domain.exception.ReferentielException; +import org.mte.numecoeval.referentiel.domain.model.ImpactReseau; +import org.mte.numecoeval.referentiel.domain.model.id.ImpactReseauId; +import org.mte.numecoeval.referentiel.domain.ports.output.ReferentielPersistencePort; +import org.mte.numecoeval.referentiel.factory.TestDataFactory; +import org.mte.numecoeval.referentiel.infrastructure.mapper.ImpactReseauMapper; +import org.mte.numecoeval.referentiel.infrastructure.mapper.ImpactReseauMapperImpl; +import org.springframework.test.util.ReflectionTestUtils; + +import java.util.Arrays; +import java.util.Collection; + +import static org.junit.jupiter.api.Assertions.*; + +class ImpactReseauFacadeTest { + + @InjectMocks + ImpactReseauFacade facadeToTest; + + @Mock + private ReferentielPersistencePort<ImpactReseau, ImpactReseauId> persistencePort; + + private ImpactReseauMapper mapper = new ImpactReseauMapperImpl(); + + @BeforeEach + void setup() { + MockitoAnnotations.openMocks(this); + ReflectionTestUtils.setField(facadeToTest, "mapper", mapper); + } + + @Test + void get_shouldReturnMatchingDTO() throws ReferentielException { + var wantedDTOId = TestDataFactory.ImpactReseauFactory.idDTO( + TestDataFactory.EtapeFactory.idDTO(TestDataFactory.DEFAULT_ETAPE), + TestDataFactory.CritereFactory.idDTO(TestDataFactory.DEFAULT_CRITERE), + "impactReseauMobileMoyen" + ); + var expectedDomainID = TestDataFactory.ImpactReseauFactory.idDomain( + TestDataFactory.EtapeFactory.idDomain(TestDataFactory.DEFAULT_ETAPE), + TestDataFactory.CritereFactory.idDomain(TestDataFactory.DEFAULT_CRITERE), + "impactReseauMobileMoyen" + ); + var expectedDomain = TestDataFactory.ImpactReseauFactory.domain( + TestDataFactory.EtapeFactory.domain(TestDataFactory.DEFAULT_ETAPE, "Test"), + TestDataFactory.CritereFactory.domain(TestDataFactory.DEFAULT_CRITERE, TestDataFactory.DEFAULT_UNITE, TestDataFactory.DEFAULT_CRITERE), + "impactReseauMobileMoyen", "Test", 0.12, 0.0012 + ); + + Mockito.when(persistencePort.get(expectedDomainID)).thenReturn(expectedDomain); + + var dto = assertDoesNotThrow(() -> facadeToTest.get(wantedDTOId)); + + + Assertions.assertEquals(expectedDomain.getEtape(), dto.getEtapeACV()); + Assertions.assertEquals(expectedDomain.getCritere(), dto.getCritere()); + Assertions.assertEquals(expectedDomain.getRefReseau(), dto.getRefReseau()); + Assertions.assertEquals(expectedDomain.getSource(), dto.getSource()); + Assertions.assertEquals(expectedDomain.getValeur(), dto.getValeur()); + Assertions.assertEquals(expectedDomain.getConsoElecMoyenne(), dto.getConsoElecMoyenne()); + } + + @Test + void addOrUpdate_shouldCallSaveThenReturnMatchingDTO() throws ReferentielException { + var dtoToSave = TestDataFactory.ImpactReseauFactory.dto( + TestDataFactory.EtapeFactory.dto(TestDataFactory.DEFAULT_ETAPE, "Test"), + TestDataFactory.CritereFactory.dto(TestDataFactory.DEFAULT_CRITERE, TestDataFactory.DEFAULT_UNITE, TestDataFactory.DEFAULT_CRITERE), + "impactReseauMobileMoyen", "Test", 0.12, 0.0012 + ); + Mockito.when(persistencePort.save(Mockito.any())).thenAnswer(invocationOnMock -> invocationOnMock.getArgument(0)); + ArgumentCaptor<ImpactReseau> valueCapture = ArgumentCaptor.forClass(ImpactReseau.class); + + var savedDTO = assertDoesNotThrow(() -> facadeToTest.addOrUpdate(dtoToSave)); + + Mockito.verify(persistencePort, Mockito.times(1)).save(valueCapture.capture()); + + var domainSaved = valueCapture.getValue(); + assertEquals(domainSaved.getEtape(), savedDTO.getEtapeACV()); + assertEquals(domainSaved.getCritere(), savedDTO.getCritere()); + assertEquals(domainSaved.getRefReseau(), savedDTO.getRefReseau()); + assertEquals(domainSaved.getSource(), savedDTO.getSource()); + assertEquals(domainSaved.getValeur(), savedDTO.getValeur()); + assertEquals(domainSaved.getConsoElecMoyenne(), savedDTO.getConsoElecMoyenne()); + + assertEquals(dtoToSave, savedDTO); + } + + @Test + void addOrUpdate_whenErrorOnSave_shouldThrowException() throws ReferentielException { + var dtoToSave = TestDataFactory.ImpactReseauFactory.dto( + TestDataFactory.EtapeFactory.dto("NonExistante", "Test"), + TestDataFactory.CritereFactory.dto("NonExistant", TestDataFactory.DEFAULT_UNITE, TestDataFactory.DEFAULT_CRITERE), + "impactReseauMobileMoyen", "Test", 0.12, 0.0012 + ); + ConstraintViolationException expectedSaveException = new ConstraintViolationException("Erreur de contrainte sur la table parente", null, "ETAPE"); + Mockito.when(persistencePort.save(Mockito.any())).thenThrow(expectedSaveException); + + var exception = assertThrows(RuntimeException.class, () -> facadeToTest.addOrUpdate(dtoToSave)); + + assertEquals(expectedSaveException, exception); + } + + @Test + void purgeAndAddAll_ShouldCallPurgeThenSaveAll() throws ReferentielException { + var dtosToSave = Arrays.asList( + TestDataFactory.ImpactReseauFactory.dto( + TestDataFactory.EtapeFactory.dto(TestDataFactory.DEFAULT_ETAPE, "Test"), + TestDataFactory.CritereFactory.dto(TestDataFactory.DEFAULT_CRITERE, TestDataFactory.DEFAULT_UNITE, TestDataFactory.DEFAULT_CRITERE), + "impactReseauMobileMoyen", "Monitor", 0.12, 0.0012 + ), + TestDataFactory.ImpactReseauFactory.dto( + TestDataFactory.EtapeFactory.dto("FIN_DE_VIE", "Autre Test"), + TestDataFactory.CritereFactory.dto("Acidification", "m3", "Autre Test"), + "impactReseauMobileMoyen", "Monitor", 0.12, 0.0012 + ) + ); + ArgumentCaptor<Collection<ImpactReseau>> valueCapture = ArgumentCaptor.forClass(Collection.class); + + assertDoesNotThrow(() -> facadeToTest.purgeAndAddAll(dtosToSave)); + + Mockito.verify(persistencePort, Mockito.times(1)).purge(); + Mockito.verify(persistencePort, Mockito.times(1)).saveAll(valueCapture.capture()); + + var expectedDomains = valueCapture.getValue(); + assertNotNull(expectedDomains); + assertEquals(dtosToSave.size(), expectedDomains.size()); + dtosToSave.forEach(dto -> { + var matchingDomain = expectedDomains.stream() + .filter(domain -> + dto.getEtapeACV().equals(domain.getEtape()) + && dto.getCritere().equals(domain.getCritere()) + && dto.getRefReseau().equals(domain.getRefReseau())) + .findAny(); + assertTrue(matchingDomain.isPresent()); + var domain = matchingDomain.get(); + assertEquals(dto.getEtapeACV(), domain.getEtape()); + assertEquals(dto.getCritere(), domain.getCritere()); + assertEquals(dto.getRefReseau(), domain.getRefReseau()); + assertEquals(dto.getSource(), domain.getSource()); + assertEquals(dto.getValeur(), domain.getValeur()); + assertEquals(dto.getConsoElecMoyenne(), domain.getConsoElecMoyenne()); + }); + } +} diff --git a/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/restapi/facade/MixElectriqueFacadeTest.java b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/restapi/facade/MixElectriqueFacadeTest.java new file mode 100644 index 00000000..e8850288 --- /dev/null +++ b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/infrastructure/restapi/facade/MixElectriqueFacadeTest.java @@ -0,0 +1,103 @@ +package org.mte.numecoeval.referentiel.infrastructure.restapi.facade; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.mte.numecoeval.referentiel.domain.exception.ReferentielException; +import org.mte.numecoeval.referentiel.domain.model.MixElectrique; +import org.mte.numecoeval.referentiel.domain.model.id.MixElectriqueId; +import org.mte.numecoeval.referentiel.domain.ports.output.ReferentielPersistencePort; +import org.mte.numecoeval.referentiel.factory.TestDataFactory; +import org.mte.numecoeval.referentiel.infrastructure.mapper.MixElectriqueMapper; +import org.mte.numecoeval.referentiel.infrastructure.mapper.MixElectriqueMapperImpl; +import org.springframework.test.util.ReflectionTestUtils; + +import java.util.Arrays; +import java.util.Collection; + +import static org.junit.jupiter.api.Assertions.*; + +class MixElectriqueFacadeTest { + + @InjectMocks + MixElectriqueFacade facadeToTest; + + @Mock + private ReferentielPersistencePort<MixElectrique, MixElectriqueId> persistencePort; + + private MixElectriqueMapper mapper = new MixElectriqueMapperImpl(); + + @BeforeEach + void setup() { + MockitoAnnotations.openMocks(this); + ReflectionTestUtils.setField(facadeToTest, "mapper", mapper); + } + + @Test + void get_shouldReturnMatchingDTO() throws ReferentielException { + var wantedDTOId = TestDataFactory.MixElectriqueFactory.idDTO( + TestDataFactory.CritereFactory.idDTO(TestDataFactory.DEFAULT_CRITERE), + "France" + ); + var expectedDomainID = TestDataFactory.MixElectriqueFactory.idDomain( + TestDataFactory.CritereFactory.idDomain(TestDataFactory.DEFAULT_CRITERE), + "France" + ); + var expectedDomain = TestDataFactory.MixElectriqueFactory.domain( + TestDataFactory.CritereFactory.domain(TestDataFactory.DEFAULT_CRITERE, TestDataFactory.DEFAULT_UNITE, TestDataFactory.DEFAULT_CRITERE), + "France", "FR", 1.0, "test" + ); + + Mockito.when(persistencePort.get(expectedDomainID)).thenReturn(expectedDomain); + + var result = assertDoesNotThrow(() -> facadeToTest.get(wantedDTOId)); + + Assertions.assertEquals(expectedDomain.getCritere(), result.getCritere()); + Assertions.assertEquals(expectedDomain.getPays(), result.getPays()); + Assertions.assertEquals(expectedDomain.getRaccourcisAnglais(), result.getRaccourcisAnglais()); + Assertions.assertEquals(expectedDomain.getValeur(), result.getValeur()); + Assertions.assertEquals(expectedDomain.getSource(), result.getSource()); + } + + @Test + void purgeAndAddAll_ShouldCallPurgeThenSaveAll() throws ReferentielException { + var dtosToSave = Arrays.asList( + TestDataFactory.MixElectriqueFactory.dto( + TestDataFactory.CritereFactory.dto(TestDataFactory.DEFAULT_CRITERE, TestDataFactory.DEFAULT_UNITE, TestDataFactory.DEFAULT_CRITERE), + "France", "FR", 1.0, "test" + ), + TestDataFactory.MixElectriqueFactory.dto( + TestDataFactory.CritereFactory.dto("Acidification", "m3", "Autre Test"), + "France", "FR", 1.0, "test" + ) + ); + ArgumentCaptor<Collection<MixElectrique>> valueCapture = ArgumentCaptor.forClass(Collection.class); + + assertDoesNotThrow(() -> facadeToTest.purgeAndAddAll(dtosToSave)); + + Mockito.verify(persistencePort, Mockito.times(1)).purge(); + Mockito.verify(persistencePort, Mockito.times(1)).saveAll(valueCapture.capture()); + + var expectedDomains = valueCapture.getValue(); + assertNotNull(expectedDomains); + assertEquals(dtosToSave.size(), expectedDomains.size()); + dtosToSave.forEach(dto -> { + var matchingDomain = expectedDomains.stream() + .filter(domain -> dto.getCritere().equals(domain.getCritere()) + && dto.getPays().equals(domain.getPays())) + .findAny(); + assertTrue(matchingDomain.isPresent()); + var domain = matchingDomain.get(); + assertEquals(dto.getCritere(), domain.getCritere()); + assertEquals(dto.getPays(), domain.getPays()); + assertEquals(dto.getRaccourcisAnglais(), domain.getRaccourcisAnglais()); + assertEquals(dto.getValeur(), domain.getValeur()); + assertEquals(dto.getSource(), domain.getSource()); + }); + } +} diff --git a/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/steps/AbstractStepDefinitions.java b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/steps/AbstractStepDefinitions.java new file mode 100644 index 00000000..71c6e4c7 --- /dev/null +++ b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/steps/AbstractStepDefinitions.java @@ -0,0 +1,65 @@ +package org.mte.numecoeval.referentiel.steps; + +import io.restassured.RestAssured; +import io.restassured.response.Response; +import org.mte.numecoeval.referentiel.infrastructure.restapi.controller.critere.ReferentielAdministrationCritereRestApi; +import org.mte.numecoeval.referentiel.infrastructure.restapi.controller.etape.ReferentielAdministrationEtapeRestApi; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.env.Environment; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.ResourceUtils; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.multipart.MultipartFile; + +import java.io.File; +import java.io.FileNotFoundException; +import java.util.Arrays; +import java.util.Objects; + +import static io.restassured.RestAssured.given; +import static org.junit.jupiter.api.Assertions.assertEquals; + +@Transactional +public abstract class AbstractStepDefinitions { + + @Autowired + protected Environment environment; + + protected Response response; + + /** + * Configuration par défaut pour les clients d'API + */ + protected void setupRestAssured() { + RestAssured.baseURI = "http://localhost"; + RestAssured.port = Integer.parseInt(Objects.requireNonNull(environment.getProperty("server.port"))); + } + + /** + * Initialisation des référentiels étapes et critères avec des valeurs par défaut. + * @throws FileNotFoundException + * @throws NoSuchMethodException + */ + protected void initAlimentationEtapeEtCritere() throws FileNotFoundException, NoSuchMethodException { + RestAssured.baseURI = "http://localhost"; + RestAssured.port = Integer.parseInt(Objects.requireNonNull(environment.getProperty("server.port"))); + File fileCritere = ResourceUtils.getFile("classpath:" + "csv/ref_Critere.csv"); + File fileEtape = ResourceUtils.getFile("classpath:" + "csv/ref_etapeACV.csv"); + //chargement Etape + String path = Arrays.stream(ReferentielAdministrationCritereRestApi.class.getMethod("importCSV", MultipartFile.class) + .getAnnotation(PostMapping.class).path()).findFirst().orElse(null); + Response response = given() + .multiPart("file", fileCritere) + .post(path) + .thenReturn(); + assertEquals(200, response.getStatusCode()); + + path = Arrays.stream(ReferentielAdministrationEtapeRestApi.class.getMethod("importCSV", MultipartFile.class) + .getAnnotation(PostMapping.class).path()).findFirst().orElse(null); + response = given() + .multiPart("file", fileEtape) + .post(path) + .thenReturn(); + assertEquals(200, response.getStatusCode()); + } +} diff --git a/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/steps/CorrespondanceRefEquipementStepDefinitions.java b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/steps/CorrespondanceRefEquipementStepDefinitions.java new file mode 100644 index 00000000..6a5f7896 --- /dev/null +++ b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/steps/CorrespondanceRefEquipementStepDefinitions.java @@ -0,0 +1,71 @@ +package org.mte.numecoeval.referentiel.steps; + +import io.cucumber.java.DataTableType; +import io.cucumber.java.fr.Alors; +import io.cucumber.java.fr.Et; +import io.cucumber.java.fr.Quand; +import org.mte.numecoeval.referentiel.domain.model.CorrespondanceRefEquipement; +import org.mte.numecoeval.referentiel.infrastructure.jpa.adapter.CorrespondanceRefEquipementJpaAdapter; +import org.mte.numecoeval.referentiel.infrastructure.restapi.controller.correspondance.ReferentielAdministrationCorrespondanceRefEquipementRestApi; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.ResourceUtils; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.multipart.MultipartFile; + +import java.io.File; +import java.io.FileNotFoundException; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import static io.restassured.RestAssured.given; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +public class CorrespondanceRefEquipementStepDefinitions extends AbstractStepDefinitions { + + @Autowired + CorrespondanceRefEquipementJpaAdapter jpaAdapter; + + @DataTableType + public CorrespondanceRefEquipement correspondanceRefEquipement(Map<String, String> entry) { + return CorrespondanceRefEquipement.builder() + .modeleEquipementSource(entry.get("modeleEquipementSource")) + .refEquipementCible(entry.get("refEquipementCible")) + .build(); + } + + @Quand("le fichier CSV des correspondances d'équipements {string} est importé par API et la réponse est {int}") + public void importReferentielCorrespondanceRefEquipements(String nomFichier, int codeReponseHttpAttendu) throws FileNotFoundException, NoSuchMethodException { + setupRestAssured(); + initAlimentationEtapeEtCritere(); + File file = ResourceUtils.getFile("classpath:" + nomFichier); + String requestPath = Arrays.stream(ReferentielAdministrationCorrespondanceRefEquipementRestApi.class.getMethod("importCSV", MultipartFile.class) + .getAnnotation(PostMapping.class).path()).findFirst().orElse(null); + response = given() + .multiPart("file", file) + .post(requestPath) + .thenReturn(); + assertEquals(codeReponseHttpAttendu, response.getStatusCode()); + } + + @Et("La récupération de tous les correspondances d'équipements renvoie {int} lignes") + public void getAllCorrespondanceRefEquipement(int nombreLignesAttendues) throws NoSuchMethodException { + List<CorrespondanceRefEquipement> domains = jpaAdapter.getAll(); + + assertEquals(nombreLignesAttendues, domains.size()); + } + + @Alors("Vérifications des correspondances d'équipements disponibles") + public void checkImpactEquipements(List<CorrespondanceRefEquipement> dataTable) { + List<CorrespondanceRefEquipement> datasInDatabase = jpaAdapter.getAll(); + + assertEquals(dataTable.size(), datasInDatabase.size()); + dataTable.forEach(expected -> { + var dataInDatabase = datasInDatabase.stream().filter(data -> data.getModeleEquipementSource().equals(expected.getModeleEquipementSource())).findFirst().orElse(null); + assertNotNull(dataInDatabase, "Il existe une correspondance d'équipement avec le modèle %s".formatted(expected.getModeleEquipementSource())); + + assertEquals(expected.getRefEquipementCible(), dataInDatabase.getRefEquipementCible(), "CorrespondanceRefEquipement %s : la référence cible ne matche pas".formatted(expected.getRefEquipementCible())); + }); + } +} diff --git a/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/steps/CritereStepdefs.java b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/steps/CritereStepdefs.java new file mode 100644 index 00000000..4c51f110 --- /dev/null +++ b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/steps/CritereStepdefs.java @@ -0,0 +1,74 @@ +package org.mte.numecoeval.referentiel.steps; + +import io.cucumber.java.fr.Alors; +import io.cucumber.java.fr.Et; +import io.cucumber.java.fr.Quand; +import io.restassured.http.ContentType; +import io.restassured.response.Response; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.CritereDTO; +import org.mte.numecoeval.referentiel.domain.ports.input.impl.ImportCriterePortImpl; +import org.mte.numecoeval.referentiel.infrastructure.restapi.controller.critere.ReferentielAdministrationCritereRestApi; +import org.mte.numecoeval.referentiel.infrastructure.restapi.controller.critere.ReferentielCritereRestApi; +import org.springframework.util.ResourceUtils; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.multipart.MultipartFile; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import static io.restassured.RestAssured.given; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class CritereStepdefs extends AbstractStepDefinitions { + + private List<CritereDTO> critereDTOS; + + private List<CritereDTO> critereCSV; + + @Quand("le fichier CSV {string} est inject") + public void leFichierCSVEstInject(String nomFichier) throws FileNotFoundException, NoSuchMethodException { + setupRestAssured(); + File file = ResourceUtils.getFile("classpath:" + nomFichier); + + // Chargement des données + critereCSV = new ImportCriterePortImpl().importCSV(new FileInputStream(file)).getObjects(); + + String requestPath = Arrays.stream(ReferentielAdministrationCritereRestApi.class.getMethod("importCSV", MultipartFile.class) + .getAnnotation(PostMapping.class).path()).findFirst().orElse(null); + Response response = given() + .multiPart("file", file) + .post(requestPath) + .thenReturn(); + //check que le status est OK + assertEquals(200, response.getStatusCode()); + } + + @Alors("Il existe {int} elements dans le referentiel critere") + public void ilExisteElementsDansLeReferentielCritere(int nombreElements) throws NoSuchMethodException { + String requestPath = Arrays.stream(ReferentielCritereRestApi.class.getMethod("getAll") + .getAnnotation(GetMapping.class).path()).findFirst().orElse(null); + Response response = given() + .contentType(ContentType.JSON) + .get(requestPath) + .thenReturn(); + critereDTOS = Arrays.asList(response.as(CritereDTO[].class)); + assertEquals(nombreElements, response.as(List.class).size()); + + } + + @Et("chacun des elements du referential ajouté est identique au element du fichier csv") + public void chacunDesElementsDuReferentialAjouteEstIdentiqueAuElementDuFichierCsv() { + List<CritereDTO> resultEgalite = critereCSV.stream().filter(critereEntity -> + critereDTOS.stream().anyMatch(api -> + Optional.of(critereEntity.getDescription()).equals(Optional.of(api.getDescription())) + && critereEntity.getNomCritere().equals(api.getNomCritere()) + && critereEntity.getUnite().equals(api.getUnite()) + )).toList(); + assertEquals(critereCSV.size(), resultEgalite.size()); + } +} diff --git a/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/steps/EtapeStepdefs.java b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/steps/EtapeStepdefs.java new file mode 100644 index 00000000..67807c3c --- /dev/null +++ b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/steps/EtapeStepdefs.java @@ -0,0 +1,76 @@ +package org.mte.numecoeval.referentiel.steps; + +import io.cucumber.java.fr.Alors; +import io.cucumber.java.fr.Et; +import io.cucumber.java.fr.Quand; +import io.restassured.http.ContentType; +import io.restassured.response.Response; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.EtapeDTO; +import org.mte.numecoeval.referentiel.domain.data.ResultatImport; +import org.mte.numecoeval.referentiel.domain.ports.input.impl.ImportEtapePortImpl; +import org.mte.numecoeval.referentiel.infrastructure.restapi.controller.etape.ReferentielAdministrationEtapeRestApi; +import org.mte.numecoeval.referentiel.infrastructure.restapi.controller.etape.ReferentielEtapeRestApi; +import org.springframework.util.ResourceUtils; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.multipart.MultipartFile; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import static io.restassured.RestAssured.given; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class EtapeStepdefs extends AbstractStepDefinitions { + private List<EtapeDTO> etapeCVS; + private EtapeDTO[] etapesAPI; + + @Quand("le fichier CSV {string} est injecte") + public void leFichierCSVEstInjecte(String nomFichier) throws FileNotFoundException, NoSuchMethodException { + setupRestAssured(); + File file = ResourceUtils.getFile("classpath:" + nomFichier); + //stockage des dtos etapes pour verifications avec la ressource rest api apres alimentation + ResultatImport<EtapeDTO> resultatImport = new ImportEtapePortImpl().importCSV(new FileInputStream(file)); + etapeCVS = resultatImport.getObjects(); + String requestPath = Arrays.stream(ReferentielAdministrationEtapeRestApi.class.getMethod("importCSV", MultipartFile.class) + .getAnnotation(PostMapping.class).path()).findFirst().orElse(null); + Response response = given() + .multiPart("file", file) + .post(requestPath) + .thenReturn(); + //check que le status est OK + assertEquals(200, response.getStatusCode()); + } + + @Alors("Il existe {int} elements dans le referentiel Etape ACV") + public void ilExisteElementsDansLeReferentielEtapeACV(int taille) throws NoSuchMethodException { + String requestPath = Arrays.stream(ReferentielEtapeRestApi.class.getMethod("getAll") + .getAnnotation(GetMapping.class).path()).findFirst().orElse(null); + Response response = given() + .contentType(ContentType.JSON) + .get(requestPath) + .thenReturn(); + etapesAPI = response.as(EtapeDTO[].class); + assertEquals(taille, response.as(List.class).size()); + + + } + + + @Et("chacun des elements du referentiel ajouté est identique au element du fichier csv") + public void chacunDesElementsDuReferentielAjoutéEstIdentiqueAuElementDuFichierCsv() { + + List<EtapeDTO> resultEgalite = etapeCVS.stream().filter(csv -> + Arrays.stream(etapesAPI).collect(Collectors.toList()).stream().anyMatch(api -> + csv.getLibelle().equals(api.getLibelle()) + && csv.getCode().equals(api.getCode()) + )) + .collect(Collectors.toList()); + + assertEquals(etapeCVS.size(), resultEgalite.size()); + } +} diff --git a/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/steps/HypotheseStepdefs.java b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/steps/HypotheseStepdefs.java new file mode 100644 index 00000000..0eb99ad1 --- /dev/null +++ b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/steps/HypotheseStepdefs.java @@ -0,0 +1,65 @@ +package org.mte.numecoeval.referentiel.steps; + +import io.cucumber.java.fr.Alors; +import io.cucumber.java.fr.Quand; +import io.restassured.http.ContentType; +import io.restassured.path.json.JsonPath; +import org.mte.numecoeval.referentiel.infrastructure.restapi.controller.hypothese.ReferentielAdministrationHypotheseRestApi; +import org.mte.numecoeval.referentiel.infrastructure.restapi.controller.hypothese.ReferentielHypotheseRestApi; +import org.springframework.util.ResourceUtils; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.multipart.MultipartFile; + +import java.io.File; +import java.io.FileNotFoundException; +import java.util.Arrays; + +import static io.restassured.RestAssured.given; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class HypotheseStepdefs extends AbstractStepDefinitions { + + @Quand("le fichier CSV REF HYPOTHESE{string} est inject") + public void leFichierCSVREFHYPOTHESEEstInject(String nomFichier) throws FileNotFoundException, NoSuchMethodException { + + setupRestAssured(); + File file = ResourceUtils.getFile("classpath:" + nomFichier); + String requestPath = Arrays.stream(ReferentielAdministrationHypotheseRestApi.class.getMethod("importCSV", MultipartFile.class) + .getAnnotation(PostMapping.class).path()).findFirst().orElse(null); + response = given() + .multiPart("file", file) + .post(requestPath) + .thenReturn(); + + } + + @Alors("le statut http est {int}") + public void checkReponseApi(int codeHttpAttendu) { + assertEquals(codeHttpAttendu, response.getStatusCode()); + } + + @Quand("lors de la recherche de hypothese avec la cle {string}") + public void lorsDeLaRechercheDeHypotheseAvecLaCle(String cle) throws NoSuchMethodException { + setupRestAssured(); + String requestPath = Arrays.stream(ReferentielHypotheseRestApi.class.getMethod("get", String.class) + .getAnnotation(GetMapping.class).path()).findFirst().orElse(null); + response = given() + .contentType(ContentType.JSON) + .param("cle", cle) + .get(requestPath) + .thenReturn(); + assertEquals(200, response.getStatusCode()); + + } + + @Alors("Le resultat est : cle = {string} valeur={string} et source={string}") + public void leResultatEstCleValeurEtSource(String cle, String valeur, String source) { + JsonPath jsonPath = response.jsonPath(); + assertEquals(cle, jsonPath.get("code")); + assertEquals(source, jsonPath.get("source")); + assertEquals(valeur, jsonPath.get("valeur")); + + + } +} diff --git a/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/steps/ImpactImpactEquipementStepDefinitions.java b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/steps/ImpactImpactEquipementStepDefinitions.java new file mode 100644 index 00000000..959e3e91 --- /dev/null +++ b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/steps/ImpactImpactEquipementStepDefinitions.java @@ -0,0 +1,82 @@ +package org.mte.numecoeval.referentiel.steps; + +import io.cucumber.java.DataTableType; +import io.cucumber.java.fr.Alors; +import io.cucumber.java.fr.Et; +import io.cucumber.java.fr.Quand; +import org.apache.commons.lang3.math.NumberUtils; +import org.mte.numecoeval.referentiel.domain.model.ImpactEquipement; +import org.mte.numecoeval.referentiel.infrastructure.jpa.adapter.ImpactEquipementJpaAdapter; +import org.mte.numecoeval.referentiel.infrastructure.restapi.controller.impactequipement.ReferentielAdministrationImpactEquipementRestApi; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.ResourceUtils; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.multipart.MultipartFile; + +import java.io.File; +import java.io.FileNotFoundException; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import static io.restassured.RestAssured.given; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class ImpactImpactEquipementStepDefinitions extends AbstractStepDefinitions { + + @Autowired + ImpactEquipementJpaAdapter jpaAdapter; + + @DataTableType + public ImpactEquipement impactEquipement(Map<String, String> entry) { + var impact = new ImpactEquipement(); + impact.setEtape(entry.get("etape")); + impact.setCritere(entry.get("critere")); + impact.setRefEquipement(entry.get("refEquipement")); + impact.setValeur(NumberUtils.createDouble(entry.get("valeur"))); + impact.setConsoElecMoyenne(NumberUtils.createDouble(entry.get("consoElecMoyenne"))); + impact.setSource(entry.get("source")); + impact.setType(entry.get("type")); + impact.setDescription(entry.get("description")); + return impact; + } + + @Quand("le fichier CSV des impacts d'équipements {string} est importé par API et la réponse est {int}") + public void importReferentielImpactEquipements(String nomFichier, int codeReponseHttpAttendu) throws FileNotFoundException, NoSuchMethodException { + setupRestAssured(); + initAlimentationEtapeEtCritere(); + File file = ResourceUtils.getFile("classpath:" + nomFichier); + String requestPath = Arrays.stream(ReferentielAdministrationImpactEquipementRestApi.class.getMethod("importCSV", MultipartFile.class) + .getAnnotation(PostMapping.class).path()).findFirst().orElse(null); + response = given() + .multiPart("file", file) + .post(requestPath) + .thenReturn(); + assertEquals(codeReponseHttpAttendu, response.getStatusCode()); + } + + @Et("La récupération de tous les impacts d'équipements renvoie {int} lignes") + public void getAllImpactEquipements(int nombreLignesAttendues) throws NoSuchMethodException { + List<ImpactEquipement> impactEquipements = jpaAdapter.getAll(); + + assertEquals(nombreLignesAttendues, impactEquipements.size()); + } + + @Alors("Vérifications des impacts équipements disponibles") + public void checkImpactEquipements(List<ImpactEquipement> dataTable) { + List<ImpactEquipement> impactEquipements = jpaAdapter.getAll(); + + assertEquals(dataTable.size(), impactEquipements.size()); + dataTable.forEach( expected -> + assertTrue( + impactEquipements.stream().anyMatch(impactEquipement1 -> impactEquipement1.equals(expected)), + "L'équipement étape: %s, critere: %s, refEquipement: %s n'est pas disponible".formatted( + expected.getEtape(), + expected.getCritere(), + expected.getRefEquipement() + ) + ) + ); + } +} diff --git a/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/steps/ImpactMessagerieStepDefinitions.java b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/steps/ImpactMessagerieStepDefinitions.java new file mode 100644 index 00000000..4f546f6e --- /dev/null +++ b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/steps/ImpactMessagerieStepDefinitions.java @@ -0,0 +1,67 @@ +package org.mte.numecoeval.referentiel.steps; + +import io.cucumber.java.fr.Alors; +import io.cucumber.java.fr.Et; +import io.cucumber.java.fr.Quand; +import org.apache.commons.lang3.StringUtils; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.ImpactMessagerieDTO; +import org.mte.numecoeval.referentiel.infrastructure.restapi.controller.impactmessagerie.ReferentielImpactMessagerieRestApi; +import org.mte.numecoeval.referentiel.infrastructure.restapi.controller.impactmessagerie.ReferentielInterneImpactMessagerieRestApi; +import org.springframework.util.ResourceUtils; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.multipart.MultipartFile; + +import java.io.File; +import java.io.FileNotFoundException; +import java.util.Arrays; +import java.util.List; + +import static io.restassured.RestAssured.given; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class ImpactMessagerieStepDefinitions extends AbstractStepDefinitions { + + List<ImpactMessagerieDTO> impactsMessageriesDisponibles; + + @Quand("le fichier CSV des impacts messageries {string} est importé par API et la réponse est {int}") + public void importReferentielImpactMessagerie(String nomFichier, int codeReponseHttpAttendu) throws FileNotFoundException, NoSuchMethodException { + setupRestAssured(); + File file = ResourceUtils.getFile("classpath:" + nomFichier); + String requestPath = Arrays.stream(ReferentielInterneImpactMessagerieRestApi.class.getMethod("importCSV", MultipartFile.class) + .getAnnotation(PostMapping.class).path()).findFirst().orElse(null); + response = given() + .multiPart("file", file) + .post(requestPath) + .thenReturn(); + assertEquals(codeReponseHttpAttendu, response.getStatusCode()); + } + + @Et("La récupération de tous les impacts de messagerie renvoie {int} lignes") + public void getAllImpactMessagerie(int nombreLignesAttendues) throws NoSuchMethodException { + String requestPath = Arrays.stream(ReferentielImpactMessagerieRestApi.class.getMethod("getAllImpactMessagerie") + .getAnnotation(GetMapping.class).path()).findFirst().orElse(null); + response = given() + .get(requestPath) + .thenReturn(); + + assertEquals(200, response.getStatusCode()); + impactsMessageriesDisponibles = Arrays.asList(response.as(ImpactMessagerieDTO[].class)); + assertEquals(nombreLignesAttendues, impactsMessageriesDisponibles.size()); + } + + @Alors("un impact messagerie pour le critère {string} a une fonction y = {string}x + {string} et de source {string}") + public void checkImpactMessagerieDisponible(String critere, String constanteCoefficientDirecteur, String constanteOrdonneeOrigine, String source) { + + assertTrue( + impactsMessageriesDisponibles.stream().anyMatch(impactMessagerieDTO -> + StringUtils.equals(critere, impactMessagerieDTO.getCritere()) + && Double.valueOf(constanteCoefficientDirecteur).equals(impactMessagerieDTO.getConstanteCoefficientDirecteur()) + && Double.valueOf(constanteOrdonneeOrigine).equals(impactMessagerieDTO.getConstanteOrdonneeOrigine()) + && StringUtils.equals(source, impactMessagerieDTO.getSource()) + ) + ); + + } +} diff --git a/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/steps/ImpactReseauStepDefinitions.java b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/steps/ImpactReseauStepDefinitions.java new file mode 100644 index 00000000..f0edc8e5 --- /dev/null +++ b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/steps/ImpactReseauStepDefinitions.java @@ -0,0 +1,258 @@ +package org.mte.numecoeval.referentiel.steps; + +import io.cucumber.java.fr.Alors; +import io.cucumber.java.fr.Quand; +import io.restassured.http.ContentType; +import io.restassured.response.Response; +import lombok.extern.slf4j.Slf4j; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.ErrorResponseDTO; +import org.mte.numecoeval.referentiel.infrastructure.restapi.dto.ImpactReseauDTO; +import org.mte.numecoeval.referentiel.domain.exception.ReferentielException; +import org.mte.numecoeval.referentiel.infrastructure.restapi.controller.impactreseau.ReferentielAdministrationImpactReseauRestApi; +import org.mte.numecoeval.referentiel.infrastructure.restapi.controller.impactreseau.ReferentielImpactReseauRestApi; +import org.springframework.http.HttpStatus; +import org.springframework.util.ResourceUtils; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.multipart.MultipartFile; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.Arrays; + +import static io.restassured.RestAssured.given; +import static org.junit.jupiter.api.Assertions.*; + +@Slf4j +public class ImpactReseauStepDefinitions extends AbstractStepDefinitions { + + @Quand("on ajoute un facteur impact reseau dont la refReseau est {string}, etapeACV est {string} , critere est {string} la source est {string} la valeur est {string} et l'unité est {string}") + public void onAjouteUnFacteurImpactReseauDontLaRefReseauEstEtapeACVEstCritereEstLa(String refReseau, String etapeACV, String critere, String source, String valeur, String unite) throws ReferentielException, NoSuchMethodException, FileNotFoundException { + log.debug("Debut creation d'un impact reseau dans table ref_impactReseau"); + initAlimentationEtapeEtCritere(); + String requestPath = Arrays.stream(ReferentielAdministrationImpactReseauRestApi.class.getMethod("add", ImpactReseauDTO.class) + .getAnnotation(PostMapping.class).path()).findFirst().orElse(null); + ImpactReseauDTO impactReseau = new ImpactReseauDTO(); + impactReseau.setRefReseau(refReseau); + impactReseau.setEtapeACV(etapeACV); + impactReseau.setCritere(critere); + impactReseau.setSource(source); + impactReseau.setValeur(Double.valueOf(valeur)); + Response response = given() + .contentType(ContentType.JSON) + .body(impactReseau) + .post(requestPath) + .thenReturn(); + assertEquals(200, response.getStatusCode()); + ImpactReseauDTO impactReseauResultat = response.as(ImpactReseauDTO.class); + //verification de la sortie + assertEquals(refReseau, impactReseauResultat.getRefReseau()); + assertEquals(refReseau, impactReseauResultat.getRefReseau()); + assertEquals(source, impactReseauResultat.getSource()); + assertEquals(critere, impactReseauResultat.getCritere()); + assertEquals(etapeACV, impactReseauResultat.getEtapeACV()); + assertEquals(Double.valueOf(valeur), impactReseauResultat.getValeur()); + log.debug("Debut creation d'un impact reseau dans table ref_impactReseau"); + } + + @Alors("impact reseau est persister dans la table ref_impactReseau avec les elements suivants: la refReseau est {string}, etapeACV est {string} , critere est {string} la source est {string} la valeur est {string} et l'unité est {string}") + public void impactReseauEstPersisterDansLaTableRef_impactReseau(String refReseau, String etapeACV, String critere, String source, String valeur, String unite) throws ReferentielException, NoSuchMethodException { + log.debug("Debut recuperation depuis table ref_impactReseau pour verification de la CAF1"); + //check avec la ressource get + //invocation de la ressource restapi ajout impact reseau + String requestPath = Arrays.stream(ReferentielImpactReseauRestApi.class.getMethod("get", String.class, String.class, String.class) + .getAnnotation(GetMapping.class).path()).findFirst().orElse(null); + Response response = given() + .contentType(ContentType.JSON) + .queryParam("refReseau", refReseau) + .queryParam("etapeacv", etapeACV) + .queryParam("critere", critere) + .get(requestPath) + .thenReturn(); + assertEquals(200, response.getStatusCode()); + ImpactReseauDTO impactReseauResultat = response.as(ImpactReseauDTO.class); + //verification de la sortie + assertEquals(refReseau, impactReseauResultat.getRefReseau()); + assertEquals(refReseau, impactReseauResultat.getRefReseau()); + assertEquals(source, impactReseauResultat.getSource()); + assertEquals(critere, impactReseauResultat.getCritere()); + assertEquals(etapeACV, impactReseauResultat.getEtapeACV()); + assertEquals(Double.valueOf(valeur), impactReseauResultat.getValeur()); + log.debug("END recuperation depuis table ref_impactReseau pour verification de la CAF1"); + } + + @Quand("on persiste un facteur impact reseau dont la refReseau est {string}, etapeACV est {string} , critere est {string} la source est {string}") + public void onPersisteUnFacteurImpactReseauDontLaRefReseauEstEtapeACVEstCritereEstLaSourceEst(String refReseau, String etapeACV, String critere, String source) throws NoSuchMethodException, FileNotFoundException { + log.debug("Debut creation à vide dans table ref_impactReseau"); + //invoquation de la ressource restapi ajout impact reseau + setupRestAssured(); + String requestPath = Arrays.stream(ReferentielAdministrationImpactReseauRestApi.class.getMethod("add", ImpactReseauDTO.class) + .getAnnotation(PostMapping.class).path()).findFirst().orElse(null); + ImpactReseauDTO impactReseau = new ImpactReseauDTO(); + impactReseau.setRefReseau(refReseau); + impactReseau.setEtapeACV(etapeACV); + impactReseau.setCritere(critere); + impactReseau.setSource(source); + Response response = given() + .contentType(ContentType.JSON) + .body(impactReseau) + .post(requestPath) + .thenReturn(); + assertEquals(200, response.getStatusCode()); + ImpactReseauDTO impactReseauResultat = response.as(ImpactReseauDTO.class); + //verification de la sortie + assertEquals(refReseau, impactReseauResultat.getRefReseau()); + assertEquals(refReseau, impactReseauResultat.getRefReseau()); + assertEquals(source, impactReseauResultat.getSource()); + assertEquals(critere, impactReseauResultat.getCritere()); + assertEquals(etapeACV, impactReseauResultat.getEtapeACV()); + log.debug("Fin creation à vide dans table ref_impactReseau"); + } + + + @Alors("il est possible de modifier impact Reseau avec les élements suivants: le refReseau est {string}, etapeACV est {string} , critere est {string} la source est {string}, valeur est {string} et l'unité est {string}") + public void ilEstPossibleDeModifierImpactReseau(String refReseau, String etapeACV, String critere, String source, String valeur, String unite) throws ReferentielException, NoSuchMethodException { + log.debug("Debut modification impact reseau depuis table ref_impactReseau"); + //check avec REST API GeT + String requestPathGet = Arrays.stream(ReferentielImpactReseauRestApi.class.getMethod("get", String.class, String.class, String.class) + .getAnnotation(GetMapping.class).path()).findFirst().orElse(null); + Response responseGet = given() + .contentType(ContentType.JSON) + .queryParam("refReseau", refReseau) + .queryParam("etapeacv", etapeACV) + .queryParam("critere", critere) + .get(requestPathGet) + .thenReturn(); + assertEquals(200, responseGet.getStatusCode()); + ImpactReseauDTO impactReseauResultatGet = responseGet.as(ImpactReseauDTO.class); + //verification de la sortie + assertEquals(etapeACV, impactReseauResultatGet.getEtapeACV()); + assertEquals(refReseau, impactReseauResultatGet.getRefReseau()); + assertEquals(critere, impactReseauResultatGet.getCritere()); + assertEquals(source, impactReseauResultatGet.getSource()); + assertNull(impactReseauResultatGet.getConsoElecMoyenne()); + //upadate avec rest api update + //modification de l'impact vide + //setupRestAssured(); + String requestPathUpdate = Arrays.stream(ReferentielAdministrationImpactReseauRestApi.class.getMethod("update", ImpactReseauDTO.class) + .getAnnotation(PutMapping.class).path()).findFirst().orElse(null); + ImpactReseauDTO impactReseau = new ImpactReseauDTO(); + impactReseau.setRefReseau(refReseau); + impactReseau.setEtapeACV(etapeACV); + impactReseau.setCritere(critere); + impactReseau.setSource(source); + impactReseau.setValeur(Double.valueOf(valeur)); + Response response = given() + .contentType(ContentType.JSON) + .body(impactReseau) + .post(requestPathUpdate) + .thenReturn(); + assertEquals(200, response.getStatusCode()); + ImpactReseauDTO impactReseauResultatUpdate = response.as(ImpactReseauDTO.class); + //verification de la sortie + assertEquals(refReseau, impactReseauResultatUpdate.getRefReseau()); + assertEquals(refReseau, impactReseauResultatUpdate.getRefReseau()); + assertEquals(source, impactReseauResultatUpdate.getSource()); + assertEquals(critere, impactReseauResultatUpdate.getCritere()); + assertEquals(etapeACV, impactReseauResultatUpdate.getEtapeACV()); + assertEquals(Double.valueOf(valeur), impactReseauResultatUpdate.getValeur()); + log.debug("Fin modification impact reseau depuis table ref_impactReseau"); + } + + @Quand("on ajoute un facteur impact reseau dont la refReseau est {string}, etapeACV est {string} , critere est {string} la valeur est {string} et l'unité est {string}") + public void onAjouteUnFacteurImpactReseauDontLaRefReseau(String refReseau, String etapeACV, String critere, String valeur, String unite) throws NoSuchMethodException, FileNotFoundException { + log.debug("Debut creation impact reseau sans source dans table ref_impactReseau"); + //invoquation de la ressource restapi ajout impact reseau + setupRestAssured(); + String requestPath = Arrays.stream(ReferentielAdministrationImpactReseauRestApi.class.getMethod("add", ImpactReseauDTO.class) + .getAnnotation(PostMapping.class).path()).findFirst().orElse(null); + ImpactReseauDTO impactReseau = new ImpactReseauDTO(); + impactReseau.setRefReseau(refReseau); + impactReseau.setEtapeACV(etapeACV); + impactReseau.setCritere(critere); + impactReseau.setValeur(Double.valueOf(valeur)); + response = given() + .contentType(ContentType.JSON) + .body(impactReseau) + .post(requestPath) + .thenReturn(); + log.error("Fin creation impact reseau sans source dans table ref_impactReseau"); + } + + + @Alors("Impossible d ajouter l'impact réseau dans la table ref_impactreseau") + public void impossibleDAjouterLImpactReseauDansLaTableRef_impactreseau() { + assertEquals(500, response.getStatusCode()); + ErrorResponseDTO impactReseauResultat = response.as(ErrorResponseDTO.class); + //assertEquals("La source est obligatoire", impactReseauResultat.getMessage()); + assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, impactReseauResultat.getStatus()); + assertEquals(500, impactReseauResultat.getCode()); + assertNotNull(impactReseauResultat.getTimestamp()); + } + + @Quand("le fichier CSV {string} est envoyé") + public void leFichierCSVEstEnvoye(String fichier) throws IOException, NoSuchMethodException { + File file = ResourceUtils.getFile("classpath:" + fichier); + setupRestAssured(); + String requestPath = Arrays.stream(ReferentielAdministrationImpactReseauRestApi.class.getMethod("importCSV", MultipartFile.class) + .getAnnotation(PostMapping.class).path()).findFirst().orElse(null); + Response response = given() + .multiPart("file", file) + .post(requestPath) + .thenReturn(); + //check que le status est OK + assertEquals(200, response.getStatusCode()); + } + + @Alors("l impact reseau refReseal est {string}, etapeACV est {string} , critere est {string} la source est {string} la valeur est {string} et l'unité est {string} existe") + public void lImpactReseauRefResealEstE(String refReseau, String etapeACV, String critere, String source, String valeur, String unite) throws NoSuchMethodException { + //recuperataion d'un resultat ajouter + String requestPath = Arrays.stream(ReferentielImpactReseauRestApi.class.getMethod("get", String.class, String.class, String.class) + .getAnnotation(GetMapping.class).path()).findFirst().orElse(null); + Response response = given() + .contentType(ContentType.JSON) + .queryParam("refReseau", refReseau) + .queryParam("etapeacv", etapeACV) + .queryParam("critere", critere) + .get(requestPath) + .thenReturn(); + ImpactReseauDTO impactReseauDTO = response.as(ImpactReseauDTO.class); + assertEquals(200, response.getStatusCode()); + ImpactReseauDTO impactReseauResultat = response.as(ImpactReseauDTO.class); + //verification de la sortie aleatoire + assertEquals(refReseau, impactReseauResultat.getRefReseau()); + assertEquals(refReseau, impactReseauResultat.getRefReseau()); + assertEquals(source, impactReseauResultat.getSource()); + assertEquals(critere, impactReseauResultat.getCritere()); + assertEquals(etapeACV, impactReseauResultat.getEtapeACV()); + assertEquals(Double.valueOf(valeur), impactReseauResultat.getValeur()); + } + + + @Quand("on cherche l impact reseau dont la refReseau est {string}, etapeACV est {string} , critere est {string}") + public void onChercheLImpactReseauquinexistePAs(String refReseau, String etapeACV, String critere) throws NoSuchMethodException, FileNotFoundException { + setupRestAssured(); + String requestPath = Arrays.stream(ReferentielImpactReseauRestApi.class.getMethod("get", String.class, String.class, String.class) + .getAnnotation(GetMapping.class).path()).findFirst().orElse(null); + response = given() + .contentType(ContentType.JSON) + .queryParam("refReseau", refReseau) + .queryParam("etapeacv", etapeACV) + .queryParam("critere", critere) + .get(requestPath) + .thenReturn(); + } + + @Alors("lecture reponse impact resau n existe pas") + public void lectureReponseImpactResauNExistePas() { + assertEquals(404, response.getStatusCode()); + ErrorResponseDTO impactReseauResultat = response.as(ErrorResponseDTO.class); + assertEquals("Impact Réseau non trouvé", impactReseauResultat.getMessage()); + assertEquals(HttpStatus.NOT_FOUND, impactReseauResultat.getStatus()); + assertEquals(404, impactReseauResultat.getCode()); + assertNotNull(impactReseauResultat.getTimestamp()); + } + +} \ No newline at end of file diff --git a/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/steps/TypeEquipementStepDefinitions.java b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/steps/TypeEquipementStepDefinitions.java new file mode 100644 index 00000000..a4f2ce2e --- /dev/null +++ b/services/api-referentiel/src/test/java/org/mte/numecoeval/referentiel/steps/TypeEquipementStepDefinitions.java @@ -0,0 +1,95 @@ +package org.mte.numecoeval.referentiel.steps; + +import io.cucumber.java.DataTableType; +import io.cucumber.java.fr.Alors; +import io.cucumber.java.fr.Et; +import io.cucumber.java.fr.Quand; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.math.NumberUtils; +import org.mte.numecoeval.referentiel.domain.model.TypeEquipement; +import org.mte.numecoeval.referentiel.infrastructure.jpa.adapter.TypeEquipementJpaAdapter; +import org.mte.numecoeval.referentiel.infrastructure.restapi.controller.typeequipement.ReferentielAdministrationTypeEquipementRestApi; +import org.mte.numecoeval.referentiel.infrastructure.restapi.controller.typeequipement.ReferentielTypeEquipementRestApi; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.ResourceUtils; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.multipart.MultipartFile; + +import java.io.File; +import java.io.FileNotFoundException; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import static io.restassured.RestAssured.given; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +public class TypeEquipementStepDefinitions extends AbstractStepDefinitions { + + @Autowired + TypeEquipementJpaAdapter jpaAdapter; + + @DataTableType + public TypeEquipement typeEquipement(Map<String, String> entry) { + return TypeEquipement.builder() + .type(entry.get("type")) + .source(StringUtils.defaultString(entry.get("source"))) + .commentaire(StringUtils.defaultString(entry.get("commentaire"))) + .serveur(Boolean.parseBoolean(entry.get("serveur"))) + .refEquipementParDefaut(StringUtils.defaultString(entry.get("refEquipementParDefaut"))) + .dureeVieDefaut(NumberUtils.createDouble(entry.get("dureeVieDefaut"))) + .build(); + } + + @Quand("le fichier CSV des types d'équipements {string} est importé par API et la réponse est {int}") + public void importReferentielTypeEquipements(String nomFichier, int codeReponseHttpAttendu) throws FileNotFoundException, NoSuchMethodException { + setupRestAssured(); + initAlimentationEtapeEtCritere(); + File file = ResourceUtils.getFile("classpath:" + nomFichier); + String requestPath = Arrays.stream(ReferentielAdministrationTypeEquipementRestApi.class.getMethod("importCSV", MultipartFile.class) + .getAnnotation(PostMapping.class).path()).findFirst().orElse(null); + response = given() + .multiPart("file", file) + .post(requestPath) + .thenReturn(); + assertEquals(codeReponseHttpAttendu, response.getStatusCode()); + } + + @Et("La récupération de tous les types d'équipements renvoie {int} lignes") + public void getAllTypeEquipement(int nombreLignesAttendues) throws NoSuchMethodException { + List<TypeEquipement> typeEquipements = jpaAdapter.getAll(); + + assertEquals(nombreLignesAttendues, typeEquipements.size()); + } + + @Alors("Vérifications des types d'équipements disponibles") + public void checkImpactEquipements(List<TypeEquipement> dataTable) { + List<TypeEquipement> datasInDatabase = jpaAdapter.getAll(); + + assertEquals(dataTable.size(), datasInDatabase.size()); + dataTable.forEach(expected -> { + var dataInDatabase = datasInDatabase.stream().filter(data -> data.getType().equals(expected.getType())).findFirst().orElse(null); + assertNotNull(dataInDatabase, "Il existe un type d'équipement avec le type %s".formatted(expected.getType())); + + assertEquals(expected.getCommentaire(), dataInDatabase.getCommentaire(), "TypeEquipement %s : le commentaire ne matche pas".formatted(expected.getType())); + assertEquals(expected.getSource(), dataInDatabase.getSource(), "TypeEquipement %s : la source ne matche pas".formatted(expected.getType())); + assertEquals(expected.getDureeVieDefaut(), dataInDatabase.getDureeVieDefaut(), "TypeEquipement %s : la durée de vie par défaut ne matche pas".formatted(expected.getType())); + assertEquals(expected.getRefEquipementParDefaut(), dataInDatabase.getRefEquipementParDefaut(), "TypeEquipement %s : la référence d'équipement par défaut ne matche pas".formatted(expected.getType())); + assertEquals(expected.isServeur(), dataInDatabase.isServeur(), "TypeEquipement %s : le flag serveur ne matche pas".formatted(expected.getType())); + }); + } + + @Et("La récupération unitaire d\'un type équipement {string} répond {int}") + public void getTypeEquipementAndCheckHttpStatus(String type, int codeReponseHttpAttendu) throws NoSuchMethodException { + + String requestPath = Arrays.stream(ReferentielTypeEquipementRestApi.class.getMethod("getTypeEquipement", String.class) + .getAnnotation(GetMapping.class).path()).findFirst().orElse(null); + response = given() + .pathParam("type", type) + .get(requestPath) + .thenReturn(); + assertEquals(codeReponseHttpAttendu, response.getStatusCode()); + } +} diff --git a/services/api-referentiel/src/test/resources/application-test.yaml b/services/api-referentiel/src/test/resources/application-test.yaml new file mode 100644 index 00000000..974db9b1 --- /dev/null +++ b/services/api-referentiel/src/test/resources/application-test.yaml @@ -0,0 +1,25 @@ +#CONFIGURATION BASES +spring: + sql: + init: + mode: never + platform: hsqldb + # Base de données + datasource: + # HSQLDB + generate-unique-name: true + url: "jdbc:hsqldb:file:target/classes/db/default.db" + username: sa + password: sa + driver-class-name: org.hsqldb.jdbc.JDBCDriver + tomcat: + test-on-borrow: false + jmx-enabled: false + max-active: 100 + jpa: + # HSQLDB + generate-ddl: true + database-platform: org.hibernate.dialect.HSQLDialect + hibernate: + ddl-auto: create-drop + diff --git a/services/api-referentiel/src/test/resources/csv/ref_Critere.csv b/services/api-referentiel/src/test/resources/csv/ref_Critere.csv new file mode 100644 index 00000000..f275ab0b --- /dev/null +++ b/services/api-referentiel/src/test/resources/csv/ref_Critere.csv @@ -0,0 +1,6 @@ +nomCritere;description;unite +Changement climatique;;kg CO_{2} eq +Émissions de particules fines;;Diseaseincidence +Radiations ionisantes;;kBq U-235 eq +Acidification;;mol H^{+} eq +Épuisement des ressources naturelles (minérales et métaux);;kg Sb eq diff --git a/services/api-referentiel/src/test/resources/csv/ref_Hypothese.csv b/services/api-referentiel/src/test/resources/csv/ref_Hypothese.csv new file mode 100644 index 00000000..a1a6b286 --- /dev/null +++ b/services/api-referentiel/src/test/resources/csv/ref_Hypothese.csv @@ -0,0 +1,3 @@ +cle;valeur;source +PUEPardDfault;1.6;expertise IJO +DureeVieServeurParDefaut;3;US 46 diff --git a/services/api-referentiel/src/test/resources/csv/ref_ImpactMessagerie.csv b/services/api-referentiel/src/test/resources/csv/ref_ImpactMessagerie.csv new file mode 100644 index 00000000..49e5fa58 --- /dev/null +++ b/services/api-referentiel/src/test/resources/csv/ref_ImpactMessagerie.csv @@ -0,0 +1,2 @@ +critere;constanteCoefficientDirecteur;constanteOrdonneeOrigine;source +Changement climatique;0.0112;5.9606;"US #66" diff --git a/services/api-referentiel/src/test/resources/csv/ref_MixElectrique.csv b/services/api-referentiel/src/test/resources/csv/ref_MixElectrique.csv new file mode 100644 index 00000000..64eae19e --- /dev/null +++ b/services/api-referentiel/src/test/resources/csv/ref_MixElectrique.csv @@ -0,0 +1,21 @@ +pays ;raccourcisAnglais;critere ;valeur ;source +France;FR ;Acidification ;0.913 ;Ref_Base_donnee_reference_20480728.xlsx +France;FR ;Changement climatique ;149 ;Ref_Base_donnee_reference_20480728.xlsx +France;FR ;Radiations ionisantes ;69.1 ;Ref_Base_donnee_reference_20480728.xlsx +France;FR ;Épuisement des ressources naturelles (minérales et métaux);0.00913 ;Ref_Base_donnee_reference_20480728.xlsx +France;FR ;Émissions de particules fines ;0.00000523 ;Ref_Base_donnee_reference_20480728.xlsx +France;FR ;Acidification ;0.05573787269904 ;Ref_Base_donnee_reference_20480728.xlsx +France;FR ;Changement climatique ;11.0082325332 ;Ref_Base_donnee_reference_20480728.xlsx +France;FR ;Radiations ionisantes ;0.02997901628064 ;Ref_Base_donnee_reference_20480728.xlsx +France;FR ;Épuisement des ressources naturelles (minérales et métaux);0.00000043143695751072003;Ref_Base_donnee_reference_20480728.xlsx +France;FR ;Émissions de particules fines ;0.0000003353284653936 ;Ref_Base_donnee_reference_20480728.xlsx +France;FR ;Acidification ;0.01120932 ;Ref_Base_donnee_reference_20480728.xlsx +France;FR ;Changement climatique ;1.9485359999999998 ;Ref_Base_donnee_reference_20480728.xlsx +France;FR ;Radiations ionisantes ;0.38956 ;Ref_Base_donnee_reference_20480728.xlsx +France;FR ;Épuisement des ressources naturelles (minérales et métaux);0.0009197928000000001 ;Ref_Base_donnee_reference_20480728.xlsx +France;FR ;Émissions de particules fines ;0.00043580159999999995 ;Ref_Base_donnee_reference_20480728.xlsx +France;FR ;Acidification ;0.0219 ;Ref_Base_donnee_reference_20480728.xlsx +France;FR ;Changement climatique ;1.46 ;Ref_Base_donnee_reference_20480728.xlsx +France;FR ;Radiations ionisantes ;0.166 ;Ref_Base_donnee_reference_20480728.xlsx +France;FR ;Épuisement des ressources naturelles (minérales et métaux);0.00000388 ;Ref_Base_donnee_reference_20480728.xlsx +France;FR ;Émissions de particules fines ;0.0000000775 ;Ref_Base_donnee_reference_20480728.xlsx diff --git a/services/api-referentiel/src/test/resources/csv/ref_etapeACV.csv b/services/api-referentiel/src/test/resources/csv/ref_etapeACV.csv new file mode 100644 index 00000000..d9a41e03 --- /dev/null +++ b/services/api-referentiel/src/test/resources/csv/ref_etapeACV.csv @@ -0,0 +1,5 @@ +code;libelle +FABRICATION;Fabrication +DISTRIBUTION;Distribution +UTILISATION;Utilisation +FIN_DE_VIE;Fin de vie \ No newline at end of file diff --git a/services/api-referentiel/src/test/resources/csv/ref_impactEquipement.csv b/services/api-referentiel/src/test/resources/csv/ref_impactEquipement.csv new file mode 100644 index 00000000..8d5c0e53 --- /dev/null +++ b/services/api-referentiel/src/test/resources/csv/ref_impactEquipement.csv @@ -0,0 +1,109 @@ +refEquipement;etapeacv;critere;consoElecMoyenne;valeur;source;type +"Ordinateur portable 14.5 8 Go RAM 564 Go SSD";FABRICATION;Acidification;29.1;0.913;Ref_Base_donnee_reference_20220728.xlsx; +"Ordinateur portable 14.5 8 Go RAM 564 Go SSD";FABRICATION;Changement climatique;29.1;149;Ref_Base_donnee_reference_20220728.xlsx; +"Ordinateur portable 14.5 8 Go RAM 564 Go SSD";FABRICATION;Radiations ionisantes;29.1;69.1;Ref_Base_donnee_reference_20220728.xlsx; +"Ordinateur portable 14.5 8 Go RAM 564 Go SSD";FABRICATION;Épuisement des ressources naturelles (minérales et métaux);29.1;0.00913;Ref_Base_donnee_reference_20220728.xlsx; +"Ordinateur portable 14.5 8 Go RAM 564 Go SSD";FABRICATION;Émissions de particules fines;29.1;0.00000523;Ref_Base_donnee_reference_20220728.xlsx; +"Ordinateur portable 14.5 8 Go RAM 564 Go SSD";DISTRIBUTION;Acidification;29.1;0.05573787269904;Ref_Base_donnee_reference_20220728.xlsx; +"Ordinateur portable 14.5 8 Go RAM 564 Go SSD";DISTRIBUTION;Changement climatique;29.1;11.0082325332;Ref_Base_donnee_reference_20220728.xlsx; +"Ordinateur portable 14.5 8 Go RAM 564 Go SSD";DISTRIBUTION;Radiations ionisantes;29.1;0.02997901628064;Ref_Base_donnee_reference_20220728.xlsx; +"Ordinateur portable 14.5 8 Go RAM 564 Go SSD";DISTRIBUTION;Épuisement des ressources naturelles (minérales et métaux);29.1;0.00000043143695751072003;Ref_Base_donnee_reference_20220728.xlsx; +"Ordinateur portable 14.5 8 Go RAM 564 Go SSD";DISTRIBUTION;Émissions de particules fines;29.1;0.0000003353284653936;Ref_Base_donnee_reference_20220728.xlsx; +"Ordinateur portable 14.5 8 Go RAM 564 Go SSD";UTILISATION;Acidification;29.1;0.01120932;Ref_Base_donnee_reference_20220728.xlsx; +"Ordinateur portable 14.5 8 Go RAM 564 Go SSD";UTILISATION;Changement climatique;29.1;1.9485359999999998;Ref_Base_donnee_reference_20220728.xlsx; +"Ordinateur portable 14.5 8 Go RAM 564 Go SSD";UTILISATION;Radiations ionisantes;29.1;50.38956;Ref_Base_donnee_reference_20220728.xlsx; +"Ordinateur portable 14.5 8 Go RAM 564 Go SSD";UTILISATION;Épuisement des ressources naturelles (minérales et métaux);29.1;0.0000009197928000000001;Ref_Base_donnee_reference_20220728.xlsx; +"Ordinateur portable 14.5 8 Go RAM 564 Go SSD";UTILISATION;Émissions de particules fines;29.1;0.00000043580159999999995;Ref_Base_donnee_reference_20220728.xlsx; +"Ordinateur portable 14.5 8 Go RAM 564 Go SSD";FIN_DE_VIE;Acidification;29.1;0.0219;Ref_Base_donnee_reference_20220728.xlsx; +"Ordinateur portable 14.5 8 Go RAM 564 Go SSD";FIN_DE_VIE;Changement climatique;29.1;1.46;Ref_Base_donnee_reference_20220728.xlsx; +"Ordinateur portable 14.5 8 Go RAM 564 Go SSD";FIN_DE_VIE;Radiations ionisantes;29.1;0.166;Ref_Base_donnee_reference_20220728.xlsx; +"Ordinateur portable 14.5 8 Go RAM 564 Go SSD";FIN_DE_VIE;Épuisement des ressources naturelles (minérales et métaux);29.1;0.00000388;Ref_Base_donnee_reference_20220728.xlsx; +"Ordinateur portable 14.5 8 Go RAM 564 Go SSD";FIN_DE_VIE;Émissions de particules fines;29.1;0.0000000775;Ref_Base_donnee_reference_20220728.xlsx; +"Ordinateur portable 15.6 16 Go RAM 512 Go SSD carte graphique";FABRICATION;Acidification;29.1;1.78;Ref_Base_donnee_reference_20220728.xlsx; +"Ordinateur portable 15.6 16 Go RAM 512 Go SSD carte graphique";FABRICATION;Changement climatique;29.1;308;Ref_Base_donnee_reference_20220728.xlsx; +"Ordinateur portable 15.6 16 Go RAM 512 Go SSD carte graphique";FABRICATION;Radiations ionisantes;29.1;98.1;Ref_Base_donnee_reference_20220728.xlsx; +"Ordinateur portable 15.6 16 Go RAM 512 Go SSD carte graphique";FABRICATION;Épuisement des ressources naturelles (minérales et métaux);29.1;0.012;Ref_Base_donnee_reference_20220728.xlsx; +"Ordinateur portable 15.6 16 Go RAM 512 Go SSD carte graphique";FABRICATION;Émissions de particules fines;29.1;0.0000101;Ref_Base_donnee_reference_20220728.xlsx; +"Ordinateur portable 15.6 16 Go RAM 512 Go SSD carte graphique";DISTRIBUTION;Acidification;29.1;0.09322822755018001;Ref_Base_donnee_reference_20220728.xlsx; +"Ordinateur portable 15.6 16 Go RAM 512 Go SSD carte graphique";DISTRIBUTION;Changement climatique;29.1;18.41257941565;Ref_Base_donnee_reference_20220728.xlsx; +"Ordinateur portable 15.6 16 Go RAM 512 Go SSD carte graphique";DISTRIBUTION;Radiations ionisantes;29.1;0.050143473659880006;Ref_Base_donnee_reference_20220728.xlsx; +"Ordinateur portable 15.6 16 Go RAM 512 Go SSD carte graphique";DISTRIBUTION;Épuisement des ressources naturelles (minérales et métaux);29.1;0.00000072162967297924;Ref_Base_donnee_reference_20220728.xlsx; +"Ordinateur portable 15.6 16 Go RAM 512 Go SSD carte graphique";DISTRIBUTION;Émissions de particules fines;29.1;0.0000005608767784262;Ref_Base_donnee_reference_20220728.xlsx; +"Ordinateur portable 15.6 16 Go RAM 512 Go SSD carte graphique";UTILISATION;Acidification;29.1;0.01120932;Ref_Base_donnee_reference_20220728.xlsx; +"Ordinateur portable 15.6 16 Go RAM 512 Go SSD carte graphique";UTILISATION;Changement climatique;29.1;1.9485359999999998;Ref_Base_donnee_reference_20220728.xlsx; +"Ordinateur portable 15.6 16 Go RAM 512 Go SSD carte graphique";UTILISATION;Radiations ionisantes;29.1;50.38956;Ref_Base_donnee_reference_20220728.xlsx; +"Ordinateur portable 15.6 16 Go RAM 512 Go SSD carte graphique";UTILISATION;Épuisement des ressources naturelles (minérales et métaux);29.1;0.0000009197928000000001;Ref_Base_donnee_reference_20220728.xlsx; +"Ordinateur portable 15.6 16 Go RAM 512 Go SSD carte graphique";UTILISATION;Émissions de particules fines;29.1;0.00000043580159999999995;Ref_Base_donnee_reference_20220728.xlsx; +"Ordinateur portable 15.6 16 Go RAM 512 Go SSD carte graphique";FIN_DE_VIE;Acidification;29.1;0.0222;Ref_Base_donnee_reference_20220728.xlsx; +"Ordinateur portable 15.6 16 Go RAM 512 Go SSD carte graphique";FIN_DE_VIE;Changement climatique;29.1;1.48;Ref_Base_donnee_reference_20220728.xlsx; +"Ordinateur portable 15.6 16 Go RAM 512 Go SSD carte graphique";FIN_DE_VIE;Radiations ionisantes;29.1;0.187;Ref_Base_donnee_reference_20220728.xlsx; +"Ordinateur portable 15.6 16 Go RAM 512 Go SSD carte graphique";FIN_DE_VIE;Épuisement des ressources naturelles (minérales et métaux);29.1;0.00000383;Ref_Base_donnee_reference_20220728.xlsx; +"Ordinateur portable 15.6 16 Go RAM 512 Go SSD carte graphique";FIN_DE_VIE;Émissions de particules fines;29.1;0.0000000774;Ref_Base_donnee_reference_20220728.xlsx; +"Tablette 11.1 6 Go RAM 512 Go SSD";DISTRIBUTION;Épuisement des ressources naturelles (minérales et métaux);18.6;0.00000016364437964909999;Ref_Base_donnee_reference_20220728.xlsx; +"Tablette 11.1 6 Go RAM 512 Go SSD";DISTRIBUTION;Émissions de particules fines;18.6;0.00000013479328674935998;Ref_Base_donnee_reference_20220728.xlsx; +"Tablette 11.1 6 Go RAM 512 Go SSD";UTILISATION;Acidification;18.6;0.00716472;Ref_Base_donnee_reference_20220728.xlsx; +"Tablette 11.1 6 Go RAM 512 Go SSD";UTILISATION;Changement climatique;18.6;1.245456;Ref_Base_donnee_reference_20220728.xlsx; +"Tablette 11.1 6 Go RAM 512 Go SSD";UTILISATION;Radiations ionisantes;18.6;32.20776;Ref_Base_donnee_reference_20220728.xlsx; +"Tablette 11.1 6 Go RAM 512 Go SSD";UTILISATION;Épuisement des ressources naturelles (minérales et métaux);18.6;0.0000005879088000000001;Ref_Base_donnee_reference_20220728.xlsx; +"Tablette 11.1 6 Go RAM 512 Go SSD";UTILISATION;Émissions de particules fines;18.6;0.0000002785536;Ref_Base_donnee_reference_20220728.xlsx; +"Tablette 11.1 6 Go RAM 512 Go SSD";FIN_DE_VIE;Acidification;18.6;0.00498;Ref_Base_donnee_reference_20220728.xlsx; +"Tablette 11.1 6 Go RAM 512 Go SSD";FIN_DE_VIE;Changement climatique;18.6;0.404;Ref_Base_donnee_reference_20220728.xlsx; +"Tablette 11.1 6 Go RAM 512 Go SSD";FIN_DE_VIE;Radiations ionisantes;18.6;0.0736;Ref_Base_donnee_reference_20220728.xlsx; +"Tablette 11.1 6 Go RAM 512 Go SSD";FIN_DE_VIE;Épuisement des ressources naturelles (minérales et métaux);18.6;0.00000019;Ref_Base_donnee_reference_20220728.xlsx; +"Tablette 11.1 6 Go RAM 512 Go SSD";FIN_DE_VIE;Émissions de particules fines;18.6;0.0000000176;Ref_Base_donnee_reference_20220728.xlsx; +"Smartphone 6.72 OLED 11 Go RAM 341 Go SSD";FABRICATION;Acidification;3.9;0.268;Ref_Base_donnee_reference_20220728.xlsx; +"Smartphone 6.72 OLED 11 Go RAM 341 Go SSD";FABRICATION;Changement climatique;3.9;44.1;Ref_Base_donnee_reference_20220728.xlsx; +"Smartphone 6.72 OLED 11 Go RAM 341 Go SSD";FABRICATION;Radiations ionisantes;3.9;12.2;Ref_Base_donnee_reference_20220728.xlsx; +"Smartphone 6.72 OLED 11 Go RAM 341 Go SSD";FABRICATION;Épuisement des ressources naturelles (minérales et métaux);3.9;0.00263;Ref_Base_donnee_reference_20220728.xlsx; +"Smartphone 6.72 OLED 11 Go RAM 341 Go SSD";FABRICATION;Émissions de particules fines;3.9;0.00000151;Ref_Base_donnee_reference_20220728.xlsx; +"Smartphone 6.72 OLED 11 Go RAM 341 Go SSD";DISTRIBUTION;Acidification;3.9;0.008213194002682;Ref_Base_donnee_reference_20220728.xlsx; +"Smartphone 6.72 OLED 11 Go RAM 341 Go SSD";DISTRIBUTION;Changement climatique;3.9;1.5308100265771998;Ref_Base_donnee_reference_20220728.xlsx; +"Smartphone 6.72 OLED 11 Go RAM 341 Go SSD";DISTRIBUTION;Radiations ionisantes;3.9;0.010082124523881202;Ref_Base_donnee_reference_20220728.xlsx; +"Smartphone 6.72 OLED 11 Go RAM 341 Go SSD";DISTRIBUTION;Épuisement des ressources naturelles (minérales et métaux);3.9;0.000000059421842323899996;Ref_Base_donnee_reference_20220728.xlsx; +"Smartphone 6.72 OLED 11 Go RAM 341 Go SSD";DISTRIBUTION;Émissions de particules fines;3.9;0.00000004894555773144;Ref_Base_donnee_reference_20220728.xlsx; +"Smartphone 6.72 OLED 11 Go RAM 341 Go SSD";UTILISATION;Acidification;3.9;0.00150228;Ref_Base_donnee_reference_20220728.xlsx; +"Smartphone 6.72 OLED 11 Go RAM 341 Go SSD";UTILISATION;Changement climatique;3.9;0.261144;Ref_Base_donnee_reference_20220728.xlsx; +"Smartphone 6.72 OLED 11 Go RAM 341 Go SSD";UTILISATION;Radiations ionisantes;3.9;6.75324;Ref_Base_donnee_reference_20220728.xlsx; +"Smartphone 6.72 OLED 11 Go RAM 341 Go SSD";UTILISATION;Épuisement des ressources naturelles (minérales et métaux);3.9;0.0000001232712;Ref_Base_donnee_reference_20220728.xlsx; +"Smartphone 6.72 OLED 11 Go RAM 341 Go SSD";UTILISATION;Émissions de particules fines;3.9;0.00000005840639999999999;Ref_Base_donnee_reference_20220728.xlsx; +"Smartphone 6.72 OLED 11 Go RAM 341 Go SSD";FIN_DE_VIE;Acidification;3.9;0.00428;Ref_Base_donnee_reference_20220728.xlsx; +"Smartphone 6.72 OLED 11 Go RAM 341 Go SSD";FIN_DE_VIE;Changement climatique;3.9;0.244;Ref_Base_donnee_reference_20220728.xlsx; +"Smartphone 6.72 OLED 11 Go RAM 341 Go SSD";FIN_DE_VIE;Radiations ionisantes;3.9;0.039;Ref_Base_donnee_reference_20220728.xlsx; +"Smartphone 6.72 OLED 11 Go RAM 341 Go SSD";FIN_DE_VIE;Épuisement des ressources naturelles (minérales et métaux);3.9;0.000000123;Ref_Base_donnee_reference_20220728.xlsx; +"Smartphone 6.72 OLED 11 Go RAM 341 Go SSD";FIN_DE_VIE;Émissions de particules fines;3.9;0.0000000118;Ref_Base_donnee_reference_20220728.xlsx; +"Ordinateur fixe (sans écran) 8 Go RAM 250 Go SSD 1 000 Go HDD carte graphique";FABRICATION;Acidification;100;1.87;Ref_Base_donnee_reference_20220728.xlsx; +"Ordinateur fixe (sans écran) 8 Go RAM 250 Go SSD 1 000 Go HDD carte graphique";FABRICATION;Changement climatique;100;289;Ref_Base_donnee_reference_20220728.xlsx; +"Ordinateur fixe (sans écran) 8 Go RAM 250 Go SSD 1 000 Go HDD carte graphique";FABRICATION;Radiations ionisantes;100;1330;Ref_Base_donnee_reference_20220728.xlsx; +"Ordinateur fixe (sans écran) 8 Go RAM 250 Go SSD 1 000 Go HDD carte graphique";FABRICATION;Épuisement des ressources naturelles (minérales et métaux);100;0.0279;Ref_Base_donnee_reference_20220728.xlsx; +"Ordinateur fixe (sans écran) 8 Go RAM 250 Go SSD 1 000 Go HDD carte graphique";FABRICATION;Émissions de particules fines;100;0.0000108;Ref_Base_donnee_reference_20220728.xlsx; +"Ordinateur fixe (sans écran) 8 Go RAM 250 Go SSD 1 000 Go HDD carte graphique";DISTRIBUTION;Acidification;100;0.11085933300800001;Ref_Base_donnee_reference_20220728.xlsx; +"Ordinateur fixe (sans écran) 8 Go RAM 250 Go SSD 1 000 Go HDD carte graphique";DISTRIBUTION;Changement climatique;100;3.1848028296;Ref_Base_donnee_reference_20220728.xlsx; +"Ordinateur fixe (sans écran) 8 Go RAM 250 Go SSD 1 000 Go HDD carte graphique";DISTRIBUTION;Radiations ionisantes;100;0.0065306696856;Ref_Base_donnee_reference_20220728.xlsx; +"Ordinateur fixe (sans écran) 8 Go RAM 250 Go SSD 1 000 Go HDD carte graphique";DISTRIBUTION;Épuisement des ressources naturelles (minérales et métaux);100;0.00000011291803934400001;Ref_Base_donnee_reference_20220728.xlsx; +"Ordinateur fixe (sans écran) 8 Go RAM 250 Go SSD 1 000 Go HDD carte graphique";DISTRIBUTION;Émissions de particules fines;100;0.0000005823743571200001;Ref_Base_donnee_reference_20220728.xlsx; +"Ordinateur fixe (sans écran) 8 Go RAM 250 Go SSD 1 000 Go HDD carte graphique";UTILISATION;Acidification;100;0.03852;Ref_Base_donnee_reference_20220728.xlsx; +"Ordinateur fixe (sans écran) 8 Go RAM 250 Go SSD 1 000 Go HDD carte graphique";UTILISATION;Changement climatique;100;6.695999999999999;Ref_Base_donnee_reference_20220728.xlsx; +"Ordinateur fixe (sans écran) 8 Go RAM 250 Go SSD 1 000 Go HDD carte graphique";UTILISATION;Radiations ionisantes;100;173.16;Ref_Base_donnee_reference_20220728.xlsx; +"Ordinateur fixe (sans écran) 8 Go RAM 250 Go SSD 1 000 Go HDD carte graphique";UTILISATION;Épuisement des ressources naturelles (minérales et métaux);100;0.0000031608000000000003;Ref_Base_donnee_reference_20220728.xlsx; +"Ordinateur fixe (sans écran) 8 Go RAM 250 Go SSD 1 000 Go HDD carte graphique";UTILISATION;Émissions de particules fines;100;0.0000014975999999999999;Ref_Base_donnee_reference_20220728.xlsx; +"Ordinateur fixe (sans écran) 8 Go RAM 250 Go SSD 1 000 Go HDD carte graphique";FIN_DE_VIE;Acidification;100;0.0966;Ref_Base_donnee_reference_20220728.xlsx; +"Ordinateur fixe (sans écran) 8 Go RAM 250 Go SSD 1 000 Go HDD carte graphique";FIN_DE_VIE;Changement climatique;100;6.61;Ref_Base_donnee_reference_20220728.xlsx; +"Ordinateur fixe (sans écran) 8 Go RAM 250 Go SSD 1 000 Go HDD carte graphique";FIN_DE_VIE;Radiations ionisantes;100;0.625;Ref_Base_donnee_reference_20220728.xlsx; +"Ordinateur fixe (sans écran) 8 Go RAM 250 Go SSD 1 000 Go HDD carte graphique";FIN_DE_VIE;Épuisement des ressources naturelles (minérales et métaux);100;0.0000135;Ref_Base_donnee_reference_20220728.xlsx; +"Ordinateur fixe (sans écran) 8 Go RAM 250 Go SSD 1 000 Go HDD carte graphique";FIN_DE_VIE;Émissions de particules fines;100;0.000000363;Ref_Base_donnee_reference_20220728.xlsx; +"Ecran d'ordinateur LCD 24";FABRICATION;Acidification;70;0.394;Ref_Base_donnee_reference_20220728.xlsx; +"Ecran d'ordinateur LCD 24";FABRICATION;Changement climatique;70;59;Ref_Base_donnee_reference_20220728.xlsx; +"Ecran d'ordinateur LCD 24";FABRICATION;Radiations ionisantes;70;143;Ref_Base_donnee_reference_20220728.xlsx; +"Ecran d'ordinateur LCD 24";FABRICATION;Épuisement des ressources naturelles (minérales et métaux);70;0.0117;Ref_Base_donnee_reference_20220728.xlsx; +"Ecran d'ordinateur LCD 24";FABRICATION;Émissions de particules fines;70;0.00000247;Ref_Base_donnee_reference_20220728.xlsx; +"Ecran d'ordinateur LCD 24";DISTRIBUTION;Acidification;70;0.044321516904000005;Ref_Base_donnee_reference_20220728.xlsx; +"Ecran d'ordinateur LCD 24";DISTRIBUTION;Changement climatique;70;1.2732828948000001;Ref_Base_donnee_reference_20220728.xlsx; +"Ecran d'ordinateur LCD 24";DISTRIBUTION;Radiations ionisantes;70;0.0026109591228;Ref_Base_donnee_reference_20220728.xlsx; +"Ecran d'ordinateur LCD 24";DISTRIBUTION;Épuisement des ressources naturelles (minérales et métaux);70;0.000000045144586872;Ref_Base_donnee_reference_20220728.xlsx; +"Ecran d'ordinateur LCD 24";DISTRIBUTION;Émissions de particules fines;70;0.00000023283303456000003;Ref_Base_donnee_reference_20220728.xlsx; +"Ecran d'ordinateur LCD 24";UTILISATION;Acidification;70;0.026964;Ref_Base_donnee_reference_20220728.xlsx; +"Ecran d'ordinateur LCD 24";UTILISATION;Changement climatique;70;4.6872;Ref_Base_donnee_reference_20220728.xlsx; +"Ecran d'ordinateur LCD 24";UTILISATION;Radiations ionisantes;70;121.212;Ref_Base_donnee_reference_20220728.xlsx; +"Ecran d'ordinateur LCD 24";UTILISATION;Épuisement des ressources naturelles (minérales et métaux);70;0.00000221256;Ref_Base_donnee_reference_20220728.xlsx; +"Ecran d'ordinateur LCD 24";FIN_DE_VIE;Épuisement des ressources naturelles (minérales et métaux);70;0.000012;Ref_Base_donnee_reference_20220728.xlsx; +"Ecran d'ordinateur LCD 24";FIN_DE_VIE;Émissions de particules fines;70;0.000000149;Ref_Base_donnee_reference_20220728.xlsx; diff --git a/services/api-referentiel/src/test/resources/csv/ref_impactreseau.csv b/services/api-referentiel/src/test/resources/csv/ref_impactreseau.csv new file mode 100644 index 00000000..e8e363c6 --- /dev/null +++ b/services/api-referentiel/src/test/resources/csv/ref_impactreseau.csv @@ -0,0 +1,101 @@ +refReseau;etapeACV;critere;valeur;unite;consoElecMoyenne;source +impactReseauMobileMoyen;DISTRIBUTION;Acidification;138.28;;;RefTest V1.00 +impactReseauMobileMoyen;FABRICATION;Acidification;148.28;;;RefTest V1.00 +impactReseauMobileMoyen;FIN_DE_VIE;Acidification;1.28;;;RefTest V1.00 +impactReseauMobileMoyen;UTILISATION;Acidification;18.8;;;RefTest V1.00 +impactReseauMobileMoyen;DISTRIBUTION;Changement climatique;1.428;;;RefTest V1.00 +impactReseauMobileMoyen;FABRICATION;Changement climatique;518.28;;;RefTest V1.00 +impactReseauMobileMoyen;FIN_DE_VIE;Changement climatique;64.28;;;RefTest V1.00 +impactReseauMobileMoyen;UTILISATION;Changement climatique;76.28;;;RefTest V1.00 +impactReseauMobileMoyen;DISTRIBUTION;Épuisement des ressources naturelles (minérales et métaux);78.28;;;RefTest V1.00 +impactReseauMobileMoyen;FABRICATION;Épuisement des ressources naturelles (minérales et métaux);87.28;;;RefTest V1.00 +impactReseauMobileMoyen;FIN_DE_VIE;Épuisement des ressources naturelles (minérales et métaux);54.28;;;RefTest V1.00 +impactReseauMobileMoyen;UTILISATION;Épuisement des ressources naturelles (minérales et métaux);34.76;;;RefTest V1.00 +impactReseauMobileMoyen;DISTRIBUTION;Radiations ionisantes;138.28;;;RefTest V1.00 +impactReseauMobileMoyen;FABRICATION;Radiations ionisantes;148.28;;;RefTest V1.00 +impactReseauMobileMoyen;FIN_DE_VIE;Radiations ionisantes;1.28;;;RefTest V1.00 +impactReseauMobileMoyen;UTILISATION;Radiations ionisantes;18.8;;;RefTest V1.00 +impactReseauMobileMoyen;DISTRIBUTION;Émissions de particules fines;138.28;;;RefTest V1.00 +impactReseauMobileMoyen;FABRICATION;Émissions de particules fines;148.28;;;RefTest V1.00 +impactReseauMobileMoyen;FIN_DE_VIE;Émissions de particules fines;1.28;;;RefTest V1.00 +impactReseauMobileMoyen;UTILISATION;Émissions de particules fines;18.8;;;RefTest V1.00 +impactReseauMobileFRANCE;DISTRIBUTION;Acidification;138.28;;;RefTest V1.00 +impactReseauMobileFRANCE;FABRICATION;Acidification;148.28;;;RefTest V1.00 +impactReseauMobileFRANCE;FIN_DE_VIE;Acidification;1.28;;;RefTest V1.00 +impactReseauMobileFRANCE;UTILISATION;Acidification;18.8;;;RefTest V1.00 +impactReseauMobileFRANCE;DISTRIBUTION;Changement climatique;1.428;;;RefTest V1.00 +impactReseauMobileFRANCE;FABRICATION;Changement climatique;518.28;;;RefTest V1.00 +impactReseauMobileFRANCE;FIN_DE_VIE;Changement climatique;64.28;;;RefTest V1.00 +impactReseauMobileFRANCE;UTILISATION;Changement climatique;76.28;;;RefTest V1.00 +impactReseauMobileFRANCE;DISTRIBUTION;Épuisement des ressources naturelles (minérales et métaux);78.28;;;RefTest V1.00 +impactReseauMobileFRANCE;FABRICATION;Épuisement des ressources naturelles (minérales et métaux);87.28;;;RefTest V1.00 +impactReseauMobileFRANCE;FIN_DE_VIE;Épuisement des ressources naturelles (minérales et métaux);54.28;;;RefTest V1.00 +impactReseauMobileFRANCE;UTILISATION;Épuisement des ressources naturelles (minérales et métaux);34.76;;;RefTest V1.00 +impactReseauMobileFRANCE;DISTRIBUTION;Radiations ionisantes;138.28;;;RefTest V1.00 +impactReseauMobileFRANCE;FABRICATION;Radiations ionisantes;148.28;;;RefTest V1.00 +impactReseauMobileFRANCE;FIN_DE_VIE;Radiations ionisantes;1.28;;;RefTest V1.00 +impactReseauMobileFRANCE;UTILISATION;Radiations ionisantes;18.8;;;RefTest V1.00 +impactReseauMobileFRANCE;DISTRIBUTION;Émissions de particules fines;138.28;;;RefTest V1.00 +impactReseauMobileFRANCE;FABRICATION;Émissions de particules fines;148.28;;;RefTest V1.00 +impactReseauMobileFRANCE;FIN_DE_VIE;Émissions de particules fines;1.28;;;RefTest V1.00 +impactReseauMobileFRANCE;UTILISATION;Émissions de particules fines;18.8;;;RefTest V1.00 +impactReseauMobile4G;DISTRIBUTION;Acidification;138.28;;;RefTest V1.00 +impactReseauMobile4G;FABRICATION;Acidification;148.28;;;RefTest V1.00 +impactReseauMobile4G;FIN_DE_VIE;Acidification;1.28;;;RefTest V1.00 +impactReseauMobile4G;UTILISATION;Acidification;18.8;;;RefTest V1.00 +impactReseauMobile4G;DISTRIBUTION;Changement climatique;1.428;;;RefTest V1.00 +impactReseauMobile4G;FABRICATION;Changement climatique;518.28;;;RefTest V1.00 +impactReseauMobile4G;FIN_DE_VIE;Changement climatique;64.28;;;RefTest V1.00 +impactReseauMobile4G;UTILISATION;Changement climatique;76.28;;;RefTest V1.00 +impactReseauMobile4G;DISTRIBUTION;Épuisement des ressources naturelles (minérales et métaux);78.28;;;RefTest V1.00 +impactReseauMobile4G;FABRICATION;Épuisement des ressources naturelles (minérales et métaux);87.28;;;RefTest V1.00 +impactReseauMobile4G;FIN_DE_VIE;Épuisement des ressources naturelles (minérales et métaux);54.28;;;RefTest V1.00 +impactReseauMobile4G;UTILISATION;Épuisement des ressources naturelles (minérales et métaux);34.76;;;RefTest V1.00 +impactReseauMobile4G;DISTRIBUTION;Radiations ionisantes;138.28;;;RefTest V1.00 +impactReseauMobile4G;FABRICATION;Radiations ionisantes;148.28;;;RefTest V1.00 +impactReseauMobile4G;FIN_DE_VIE;Radiations ionisantes;1.28;;;RefTest V1.00 +impactReseauMobile4G;UTILISATION;Radiations ionisantes;18.8;;;RefTest V1.00 +impactReseauMobile4G;DISTRIBUTION;Émissions de particules fines;138.28;;;RefTest V1.00 +impactReseauMobile4G;FABRICATION;Émissions de particules fines;148.28;;;RefTest V1.00 +impactReseauMobile4G;FIN_DE_VIE;Émissions de particules fines;1.28;;;RefTest V1.00 +impactReseauMobile4G;UTILISATION;Émissions de particules fines;18.8;;;RefTest V1.00 +impactReseauMobile5G;DISTRIBUTION;Acidification;138.28;;;RefTest V1.00 +impactReseauMobile5G;FABRICATION;Acidification;148.28;;;RefTest V1.00 +impactReseauMobile5G;FIN_DE_VIE;Acidification;1.28;;;RefTest V1.00 +impactReseauMobile5G;UTILISATION;Acidification;18.8;;;RefTest V1.00 +impactReseauMobile5G;DISTRIBUTION;Changement climatique;1.428;;;RefTest V1.00 +impactReseauMobile5G;FABRICATION;Changement climatique;518.28;;;RefTest V1.00 +impactReseauMobile5G;FIN_DE_VIE;Changement climatique;64.28;;;RefTest V1.00 +impactReseauMobile5G;UTILISATION;Changement climatique;76.28;;;RefTest V1.00 +impactReseauMobile5G;DISTRIBUTION;Épuisement des ressources naturelles (minérales et métaux);78.28;;;RefTest V1.00 +impactReseauMobile5G;FABRICATION;Épuisement des ressources naturelles (minérales et métaux);87.28;;;RefTest V1.00 +impactReseauMobile5G;FIN_DE_VIE;Épuisement des ressources naturelles (minérales et métaux);54.28;;;RefTest V1.00 +impactReseauMobile5G;UTILISATION;Épuisement des ressources naturelles (minérales et métaux);34.76;;;RefTest V1.00 +impactReseauMobile5G;DISTRIBUTION;Radiations ionisantes;138.28;;;RefTest V1.00 +impactReseauMobile5G;FABRICATION;Radiations ionisantes;148.28;;;RefTest V1.00 +impactReseauMobile5G;FIN_DE_VIE;Radiations ionisantes;1.28;;;RefTest V1.00 +impactReseauMobile5G;UTILISATION;Radiations ionisantes;18.8;;;RefTest V1.00 +impactReseauMobile5G;DISTRIBUTION;Émissions de particules fines;138.28;;;RefTest V1.00 +impactReseauMobile5G;FABRICATION;Émissions de particules fines;148.28;;;RefTest V1.00 +impactReseauMobile5G;FIN_DE_VIE;Émissions de particules fines;1.28;;;RefTest V1.00 +impactReseauMobile5G;UTILISATION;Émissions de particules fines;18.8;;;RefTest V1.00 +impactReseauMobile2G;DISTRIBUTION;Acidification;138.28;;;RefTest V1.00 +impactReseauMobile2G;FABRICATION;Acidification;148.28;;;RefTest V1.00 +impactReseauMobile2G;FIN_DE_VIE;Acidification;1.28;;;RefTest V1.00 +impactReseauMobile2G;UTILISATION;Acidification;18.8;;;RefTest V1.00 +impactReseauMobile2G;DISTRIBUTION;Changement climatique;1.428;;;RefTest V1.00 +impactReseauMobile2G;FABRICATION;Changement climatique;518.28;;;RefTest V1.00 +impactReseauMobile2G;FIN_DE_VIE;Changement climatique;64.28;;;RefTest V1.00 +impactReseauMobile2G;UTILISATION;Changement climatique;76.28;;;RefTest V1.00 +impactReseauMobile2G;DISTRIBUTION;Épuisement des ressources naturelles (minérales et métaux);78.28;;;RefTest V1.00 +impactReseauMobile2G;FABRICATION;Épuisement des ressources naturelles (minérales et métaux);87.28;;;RefTest V1.00 +impactReseauMobile2G;FIN_DE_VIE;Épuisement des ressources naturelles (minérales et métaux);54.28;;;RefTest V1.00 +impactReseauMobile2G;UTILISATION;Épuisement des ressources naturelles (minérales et métaux);34.76;;;RefTest V1.00 +impactReseauMobile2G;DISTRIBUTION;Radiations ionisantes;138.28;;;RefTest V1.00 +impactReseauMobile2G;FABRICATION;Radiations ionisantes;148.28;;;RefTest V1.00 +impactReseauMobile2G;FIN_DE_VIE;Radiations ionisantes;1.28;;;RefTest V1.00 +impactReseauMobile2G;UTILISATION;Radiations ionisantes;18.8;;;RefTest V1.00 +impactReseauMobile2G;DISTRIBUTION;Émissions de particules fines;138.28;;;RefTest V1.00 +impactReseauMobile2G;FABRICATION;Émissions de particules fines;148.28;;;RefTest V1.00 +impactReseauMobile2G;FIN_DE_VIE;Émissions de particules fines;1.28;;;RefTest V1.00 + diff --git a/services/api-referentiel/src/test/resources/csv/sprint10/ref_CorrespondanceRefEquipement.csv b/services/api-referentiel/src/test/resources/csv/sprint10/ref_CorrespondanceRefEquipement.csv new file mode 100644 index 00000000..c16f1e6f --- /dev/null +++ b/services/api-referentiel/src/test/resources/csv/sprint10/ref_CorrespondanceRefEquipement.csv @@ -0,0 +1,4 @@ +modeleEquipementSource;refEquipementCible +Fairphone V1;smartphone01 +Fairphone V2;smartphone01 +Ecran 27 pouces;ecran01 \ No newline at end of file diff --git a/services/api-referentiel/src/test/resources/csv/sprint10/ref_ImpactEquipement.csv b/services/api-referentiel/src/test/resources/csv/sprint10/ref_ImpactEquipement.csv new file mode 100644 index 00000000..13f505c0 --- /dev/null +++ b/services/api-referentiel/src/test/resources/csv/sprint10/ref_ImpactEquipement.csv @@ -0,0 +1,7 @@ +refEquipement;etapeacv;critere;consoElecMoyenne;valeur;source;type;description +"computer monitor-01-23";FIN_DE_VIE;Acidification;35.81692801813716;0.020296259210277726;reftest_20221013;computer monitor;Description Impact Equipement computer monitor +"digital price tag - power unit-01";FABRICATION;Radiations ionisantes;;5.89E+01;reftest_20221013;digital price tag - power unit;Description Impact Equipement digital price tag - power unit +"docking station-01";FABRICATION;Radiations ionisantes;1.28;3.50E+01;reftest_20221013;docking station;Description Impact Equipement docking station +"empty 42u bay-01";FABRICATION;Radiations ionisantes;;5.87E+01;reftest_20221013;empty 42u bay;Description Impact Equipement empty 42u bay +"feature phone-01";FABRICATION;Radiations ionisantes;0.09;5.11E+00;reftest_20221013;feature phone;Description Impact Equipement feature phone +"firewall-01";FABRICATION;Radiations ionisantes;876;2.65E+02;reftest_20221013;firewall;Description Impact Equipement firewall diff --git a/services/api-referentiel/src/test/resources/csv/sprint10/ref_TypeEquipement.csv b/services/api-referentiel/src/test/resources/csv/sprint10/ref_TypeEquipement.csv new file mode 100644 index 00000000..f5d8fc3e --- /dev/null +++ b/services/api-referentiel/src/test/resources/csv/sprint10/ref_TypeEquipement.csv @@ -0,0 +1,18 @@ +type;serveur;commentaire;dureeVieDefaut;refEquipementParDefaut;source +ServeurCalcul;true;commentaires;6;serveur_par_defaut;Test +ServeurStockage;true;;6;serveur_par_defaut;Test +Armoire;false;;7;armoir_par_defaut;Test +NAS;false;;6;baie_stockage_par_defaut;Test +Switch;false;;6;switch_par_defaut;Test +OrdinateurPortable;false;;4;laptop_par_defaut;Test +OrdinateurDeBureau;false;;5;desktop_par_defaut;Test +Tablette;false;;2.5;tablette_par_defaut;Test +TelephoneMobile;false;commentaires;2.5;smartphone_par_defaut;Test +TelephoneFixe;false;;10;telephone_fixe_par_defaut;Test +EquipementReseau;false;commentaires;5;switch_par_defaut;Test +BorneWifi;false;;4;borneWifi_par_defaut;Test +Imprimante;false;;5;imprimante_par_defaut;Test +Ecran;false;;8;ecran_par_defaut;Test +AccessoirePosteDeTravail;false;souris calvier, camera casque;7;stationTravail_par_defaut;Test +Connectique;false;;7;;Test +Autre;false;;2;;Test diff --git a/services/api-referentiel/src/test/resources/csv/unit/correspondanceRefEquipement.csv b/services/api-referentiel/src/test/resources/csv/unit/correspondanceRefEquipement.csv new file mode 100644 index 00000000..2e13fbcd --- /dev/null +++ b/services/api-referentiel/src/test/resources/csv/unit/correspondanceRefEquipement.csv @@ -0,0 +1,4 @@ +modeleEquipementSource;refEquipementCible +modele01;refCible01 +modele02;refCible01 +modele03;refCible02 \ No newline at end of file diff --git a/services/api-referentiel/src/test/resources/csv/unit/correspondanceRefEquipement_errorInMiddle.csv b/services/api-referentiel/src/test/resources/csv/unit/correspondanceRefEquipement_errorInMiddle.csv new file mode 100644 index 00000000..47834cba --- /dev/null +++ b/services/api-referentiel/src/test/resources/csv/unit/correspondanceRefEquipement_errorInMiddle.csv @@ -0,0 +1,6 @@ +modeleEquipementSource;refEquipementCible +modele01;refCible01 +modele02;refCible01 +;;Ceci;N'estPas;UneLigne;Valide +Tests;;Ceci;N'estPasNonPlus;UneLigne;Valide +modele03;refCible02 \ No newline at end of file diff --git a/services/api-referentiel/src/test/resources/csv/unit/correspondanceRefEquipement_utilitaires.csv b/services/api-referentiel/src/test/resources/csv/unit/correspondanceRefEquipement_utilitaires.csv new file mode 100644 index 00000000..2e13fbcd --- /dev/null +++ b/services/api-referentiel/src/test/resources/csv/unit/correspondanceRefEquipement_utilitaires.csv @@ -0,0 +1,4 @@ +modeleEquipementSource;refEquipementCible +modele01;refCible01 +modele02;refCible01 +modele03;refCible02 \ No newline at end of file diff --git a/services/api-referentiel/src/test/resources/csv/unit/critere.csv b/services/api-referentiel/src/test/resources/csv/unit/critere.csv new file mode 100644 index 00000000..2e4ba083 --- /dev/null +++ b/services/api-referentiel/src/test/resources/csv/unit/critere.csv @@ -0,0 +1,6 @@ +nomCritere;description;unite +Changement climatique;;kg CO_{2} eq +Émissions de particules fines;Émissions de particules fines;Diseaseincidence +Radiations ionisantes;Description de Tests;kBq U-235 eq +Acidification;;mol H^{+} eq +Épuisement des ressources naturelles (minérales et métaux);;kg Sb eq diff --git a/services/api-referentiel/src/test/resources/csv/unit/critere_errorInMiddle.csv b/services/api-referentiel/src/test/resources/csv/unit/critere_errorInMiddle.csv new file mode 100644 index 00000000..7c31b93c --- /dev/null +++ b/services/api-referentiel/src/test/resources/csv/unit/critere_errorInMiddle.csv @@ -0,0 +1,7 @@ +nomCritere;description;unite +Changement climatique;;kg CO_{2} eq +Émissions de particules fines;Émissions de particules fines;Diseaseincidence +;Le reste de la ligne n'a pas d'importance;Test;Pas;Le;Bon;Nombre;De;Colonne +Radiations ionisantes;Description de Tests;kBq U-235 eq +Acidification;;mol H^{+} eq +Épuisement des ressources naturelles (minérales et métaux);;kg Sb eq diff --git a/services/api-referentiel/src/test/resources/csv/unit/csvHelper.csv b/services/api-referentiel/src/test/resources/csv/unit/csvHelper.csv new file mode 100644 index 00000000..7c564e13 --- /dev/null +++ b/services/api-referentiel/src/test/resources/csv/unit/csvHelper.csv @@ -0,0 +1,3 @@ +cle;valeurDouble +PUEPardDfault;1.6 +DureeVieServeurParDefaut;3 diff --git a/services/api-referentiel/src/test/resources/csv/unit/etapeACV.csv b/services/api-referentiel/src/test/resources/csv/unit/etapeACV.csv new file mode 100644 index 00000000..d9a41e03 --- /dev/null +++ b/services/api-referentiel/src/test/resources/csv/unit/etapeACV.csv @@ -0,0 +1,5 @@ +code;libelle +FABRICATION;Fabrication +DISTRIBUTION;Distribution +UTILISATION;Utilisation +FIN_DE_VIE;Fin de vie \ No newline at end of file diff --git a/services/api-referentiel/src/test/resources/csv/unit/etapeACV_errorInMiddle.csv b/services/api-referentiel/src/test/resources/csv/unit/etapeACV_errorInMiddle.csv new file mode 100644 index 00000000..dea9b4f7 --- /dev/null +++ b/services/api-referentiel/src/test/resources/csv/unit/etapeACV_errorInMiddle.csv @@ -0,0 +1,5 @@ +code;libelle +FABRICATION;Fabrication +DISTRIBUTION;Distribution +;Cette ligne n'a pas de code et est une erreur +FIN_DE_VIE;Fin de vie diff --git a/services/api-referentiel/src/test/resources/csv/unit/hypothese.csv b/services/api-referentiel/src/test/resources/csv/unit/hypothese.csv new file mode 100644 index 00000000..a1a6b286 --- /dev/null +++ b/services/api-referentiel/src/test/resources/csv/unit/hypothese.csv @@ -0,0 +1,3 @@ +cle;valeur;source +PUEPardDfault;1.6;expertise IJO +DureeVieServeurParDefaut;3;US 46 diff --git a/services/api-referentiel/src/test/resources/csv/unit/hypothese_errorInMiddle.csv b/services/api-referentiel/src/test/resources/csv/unit/hypothese_errorInMiddle.csv new file mode 100644 index 00000000..66f419ba --- /dev/null +++ b/services/api-referentiel/src/test/resources/csv/unit/hypothese_errorInMiddle.csv @@ -0,0 +1,5 @@ +cle;valeur;source +PUEPardDfault;1.6;expertise IJO +;PasUnChiffreMaisPasGrave;Test;Pas;Le;Bon;Nombre;De;Colonne +ErreurValeur;;Test;Pas;Le;Bon;Nombre;De;Colonne +DureeVieServeurParDefaut;3;US 46 diff --git a/services/api-referentiel/src/test/resources/csv/unit/impactEquipement.csv b/services/api-referentiel/src/test/resources/csv/unit/impactEquipement.csv new file mode 100644 index 00000000..c55231f9 --- /dev/null +++ b/services/api-referentiel/src/test/resources/csv/unit/impactEquipement.csv @@ -0,0 +1,5 @@ +refEquipement;etapeacv;critere;consoElecMoyenne;valeur;source;type +"Ordinateur portable 14.5 8 Go RAM 564 Go SSD";FABRICATION;Changement climatique;30.1;149;Ref_Base_donnee_reference_20220728.xlsx;Ordinateur Portable +"Ordinateur portable 14.5 8 Go RAM 564 Go SSD";DISTRIBUTION;Changement climatique;29.1;11.0082325332;Ref_Base_donnee_reference_20220728.xlsx;Ordinateur Portable +"Ordinateur portable 14.5 8 Go RAM 564 Go SSD";UTILISATION;Changement climatique;28.1;1.9485359999999998;Ref_Base_donnee_reference_20220728.xlsx;Ordinateur Portable +"Ordinateur portable 14.5 8 Go RAM 564 Go SSD";FIN_DE_VIE;Changement climatique;1.1;1.46;Ref_Base_donnee_reference_20220728.xlsx;Ordinateur Portable diff --git a/services/api-referentiel/src/test/resources/csv/unit/impactEquipement_errorInMiddle.csv b/services/api-referentiel/src/test/resources/csv/unit/impactEquipement_errorInMiddle.csv new file mode 100644 index 00000000..bfd62dd0 --- /dev/null +++ b/services/api-referentiel/src/test/resources/csv/unit/impactEquipement_errorInMiddle.csv @@ -0,0 +1,8 @@ +refEquipement;etapeacv;critere;consoElecMoyenne;valeur;source;type;description +"Ordinateur portable 14.5 8 Go RAM 564 Go SSD";FABRICATION;Changement climatique;30.1;149;Ref_Base_donnee_reference_20220728.xlsx;Ordinateur Portable;description Ordinateur Portable 1 +"Ordinateur portable 14.5 8 Go RAM 564 Go SSD";DISTRIBUTION;Changement climatique;29.1;11.0082325332;Ref_Base_donnee_reference_20220728.xlsx;Ordinateur Portable;description Ordinateur Portable 2 +;SansRefEquipement;Critère inconnu;0.0;0.0;;Test;Ces;Colonnes;Sont;Des;Erreurs; +SansEtape;;Critère inconnu;0.0;0.0;;Test;Ces;Colonnes;Sont;Des;Erreurs; +SansCritere;SansCritere;;0.0;0.0;;Test;Ces;Colonnes;Sont;Des;Erreurs; +"Ordinateur portable 14.5 8 Go RAM 564 Go SSD";UTILISATION;Changement climatique;28.1;1.9485359999999998;Ref_Base_donnee_reference_20220728.xlsx;Ordinateur Portable;description Ordinateur Portable 3 +"Ordinateur portable 14.5 8 Go RAM 564 Go SSD";FIN_DE_VIE;Changement climatique;1.1;1.46;Ref_Base_donnee_reference_20220728.xlsx;Ordinateur Portable;description Ordinateur Portable 4 diff --git a/services/api-referentiel/src/test/resources/csv/unit/impactMessagerie.csv b/services/api-referentiel/src/test/resources/csv/unit/impactMessagerie.csv new file mode 100644 index 00000000..8b416bf6 --- /dev/null +++ b/services/api-referentiel/src/test/resources/csv/unit/impactMessagerie.csv @@ -0,0 +1,6 @@ +critere;constanteCoefficientDirecteur;constanteOrdonneeOrigine;source +Changement climatique;0.5;0;Tests +Émissions de particules fines;1.000111;1.451;Tests +Radiations ionisantes;0.00005;0.02315412;Tests +Acidification;1.224586;0.042;Tests +Épuisement des ressources naturelles (minérales et métaux);2.020;0.678;Tests diff --git a/services/api-referentiel/src/test/resources/csv/unit/impactMessagerie_errorInMiddle.csv b/services/api-referentiel/src/test/resources/csv/unit/impactMessagerie_errorInMiddle.csv new file mode 100644 index 00000000..2a7c0368 --- /dev/null +++ b/services/api-referentiel/src/test/resources/csv/unit/impactMessagerie_errorInMiddle.csv @@ -0,0 +1,4 @@ +critere;constanteCoefficientDirecteur;constanteOrdonneeOrigine;source +Changement climatique;0.5;0;Tests +;CritereAbsent;Messagerie;CritereAbsent;AutresColonnes;Inutiles +Acidification;1.224586;0.042;Tests \ No newline at end of file diff --git a/services/api-referentiel/src/test/resources/csv/unit/impactreseau.csv b/services/api-referentiel/src/test/resources/csv/unit/impactreseau.csv new file mode 100644 index 00000000..d459911d --- /dev/null +++ b/services/api-referentiel/src/test/resources/csv/unit/impactreseau.csv @@ -0,0 +1,7 @@ +refReseau;etapeACV;critere;valeur;unite;consoElecMoyenne;source +impactReseauMobileMoyen;DISTRIBUTION;Changement climatique;1.428;;0.0020;RefTest V1.00 +impactReseauMobileMoyen;FABRICATION;Changement climatique;518.28;;;RefTest V1.00 +impactReseauMobileMoyen;FIN_DE_VIE;Changement climatique;64.28;;;RefTest V1.00 +impactReseauMobileMoyen;UTILISATION;Changement climatique;76.28;;;RefTest V1.00 + + diff --git a/services/api-referentiel/src/test/resources/csv/unit/impactreseau_errorInMiddle.csv b/services/api-referentiel/src/test/resources/csv/unit/impactreseau_errorInMiddle.csv new file mode 100644 index 00000000..869d874e --- /dev/null +++ b/services/api-referentiel/src/test/resources/csv/unit/impactreseau_errorInMiddle.csv @@ -0,0 +1,10 @@ +refReseau;etapeACV;critere;valeur;unite;consoElecMoyenne;source +impactReseauMobileMoyen;DISTRIBUTION;Changement climatique;1.428;;0.0020;RefTest V1.00 +impactReseauMobileMoyen;FABRICATION;Changement climatique;518.28;;;RefTest V1.00 +;RefReseauAbsent;RefReseauAbsent;;;;Test;Ces;Colonnes;Sont;Des;Erreurs +RefEtapeAbsent;;RefEtapeAbsent;;;;Test;Ces;Colonnes;Sont;Des;Erreurs +RefCritereAbsent;RefCritereAbsent;;;;;Test;Ces;Colonnes;Sont;Des;Erreurs +impactReseauMobileMoyen;FIN_DE_VIE;Changement climatique;64.28;;;RefTest V1.00 +impactReseauMobileMoyen;UTILISATION;Changement climatique;76.28;;;RefTest V1.00 + + diff --git a/services/api-referentiel/src/test/resources/csv/unit/mixElectrique.csv b/services/api-referentiel/src/test/resources/csv/unit/mixElectrique.csv new file mode 100644 index 00000000..3c401da9 --- /dev/null +++ b/services/api-referentiel/src/test/resources/csv/unit/mixElectrique.csv @@ -0,0 +1,5 @@ +pays ;raccourcisAnglais;critere ;valeur ;source +France ;FR ;Changement climatique;149 ;Ref_Base_donnee_reference_20480728.xlsx +Brésil ;BR ;Changement climatique;11.0082325332 ;Ref_Base_donnee_reference_20480728.xlsx +Angleterre;EN ;Changement climatique;1.9485359999999998;Ref_Base_donnee_reference_20480728.xlsx +USA ;US ;Changement climatique;1.46 ;Ref_Base_donnee_reference_20480728.xlsx diff --git a/services/api-referentiel/src/test/resources/csv/unit/mixElectrique_errorInMiddle.csv b/services/api-referentiel/src/test/resources/csv/unit/mixElectrique_errorInMiddle.csv new file mode 100644 index 00000000..69025ee8 --- /dev/null +++ b/services/api-referentiel/src/test/resources/csv/unit/mixElectrique_errorInMiddle.csv @@ -0,0 +1,8 @@ +pays ;raccourcisAnglais;critere ;valeur ;source +France ;FR ;Changement climatique;149 ;Ref_Base_donnee_reference_20480728.xlsx +Brésil ;BR ;Changement climatique;11.0082325332 ;Ref_Base_donnee_reference_20480728.xlsx +;PaysAbsent;PaysAbsent;0.0;0.0;;Test;Ces;Colonnes;Sont;Des;Erreurs +RaccourciPaysAbsent;;RaccourciPaysAbsent;0.0;0.0;;Test;Ces;Colonnes;Sont;Des;Erreurs +CritereAbsent;CritereAbsent;;0.0;0.0;;Test;Ces;Colonnes;Sont;Des;Erreurs +Angleterre;EN ;Changement climatique;1.9485359999999998;Ref_Base_donnee_reference_20480728.xlsx +USA ;US ;Changement climatique;1.46 ;Ref_Base_donnee_reference_20480728.xlsx diff --git a/services/api-referentiel/src/test/resources/csv/unit/typeEquipement.csv b/services/api-referentiel/src/test/resources/csv/unit/typeEquipement.csv new file mode 100644 index 00000000..030ca4de --- /dev/null +++ b/services/api-referentiel/src/test/resources/csv/unit/typeEquipement.csv @@ -0,0 +1,3 @@ +type;serveur;commentaire;dureeVieDefaut;source;refEquipementParDefaut +Serveur;true;"Test simple";6.0;"Exemple de serveur basique";"serveur_par_defaut" +Laptop;false;"Test simple";5.0;"ADEME, durée de vie des EEE, UF utiliser une ordinateur, écran non cathodique et LCD, https://vu.fr/bOQZ";"laptop_par_defaut" diff --git a/services/api-referentiel/src/test/resources/csv/unit/typeEquipement_errorInMiddle.csv b/services/api-referentiel/src/test/resources/csv/unit/typeEquipement_errorInMiddle.csv new file mode 100644 index 00000000..a53451b3 --- /dev/null +++ b/services/api-referentiel/src/test/resources/csv/unit/typeEquipement_errorInMiddle.csv @@ -0,0 +1,4 @@ +type;serveur;commentaire;dureeVieDefaut;source;refEquipementParDefaut +Serveur;true;"Test simple";6.0;"Exemple de serveur basique";"serveur_par_defaut" +;TypeAbsent;"est une erreur";complete;"Sur toute";"La";"Ligne" +Laptop;false;"Test simple";5.0;"ADEME, durée de vie des EEE, UF utiliser une ordinateur, écran non cathodique et LCD, https://vu.fr/bOQZ";"laptop_par_defaut" diff --git a/services/api-referentiel/src/test/resources/csv/unit/wrongCSVFile.csv b/services/api-referentiel/src/test/resources/csv/unit/wrongCSVFile.csv new file mode 100644 index 00000000..7ab8b51e --- /dev/null +++ b/services/api-referentiel/src/test/resources/csv/unit/wrongCSVFile.csv @@ -0,0 +1,2 @@ +mauvais +fichier \ No newline at end of file diff --git a/services/api-referentiel/src/test/resources/logback.xml b/services/api-referentiel/src/test/resources/logback.xml new file mode 100644 index 00000000..1590b195 --- /dev/null +++ b/services/api-referentiel/src/test/resources/logback.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<configuration> +<!-- Spring default.xml --> + <conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter" /> + <conversionRule conversionWord="wex" converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter" /> + <conversionRule conversionWord="wEx" converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter" /> + + <property name="CONSOLE_LOG_PATTERN" value="${CONSOLE_LOG_PATTERN:-%clr(%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/> + <property name="CONSOLE_LOG_CHARSET" value="${CONSOLE_LOG_CHARSET:-${file.encoding:-UTF-8}}"/> + <property name="FILE_LOG_PATTERN" value="${FILE_LOG_PATTERN:-%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}} ${LOG_LEVEL_PATTERN:-%5p} ${PID:- } --- [%t] %-40.40logger{39} : %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/> + <property name="FILE_LOG_CHARSET" value="${FILE_LOG_CHARSET:-${file.encoding:-UTF-8}}"/> + +<!-- console-appender.xml--> + <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> + <encoder> + <pattern>${CONSOLE_LOG_PATTERN}</pattern> + <charset>${CONSOLE_LOG_CHARSET}</charset> + </encoder> + </appender> + + <root level="INFO"> + <appender-ref ref="CONSOLE" /> + </root> + <logger name="org.springframework.web" level="INFO"/> +</configuration> \ No newline at end of file diff --git a/services/api-referentiel/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/services/api-referentiel/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 00000000..ca6ee9ce --- /dev/null +++ b/services/api-referentiel/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline \ No newline at end of file diff --git a/services/api-referentiel/src/test/resources/org/mte/numecoeval/referentiel/referentiel_criteres.feature b/services/api-referentiel/src/test/resources/org/mte/numecoeval/referentiel/referentiel_criteres.feature new file mode 100644 index 00000000..afab48b8 --- /dev/null +++ b/services/api-referentiel/src/test/resources/org/mte/numecoeval/referentiel/referentiel_criteres.feature @@ -0,0 +1,17 @@ +#language: fr +#encoding: utf-8 + +Fonctionnalité: Gestion du referentiel DES CRITERES + Le but étant de rendre disponible le référentiel referentiel CRITERES afin de rendre possible + les calculs d'impacts environnementaux sur les équipements + + + Scénario: CAF1: alimentation en masse du referentiel CRITERES depuis un ficher csv + Quand le fichier CSV "csv/ref_Critere.csv" est inject + Alors Il existe 5 elements dans le referentiel critere + Et chacun des elements du referential ajouté est identique au element du fichier csv + + + + + diff --git a/services/api-referentiel/src/test/resources/org/mte/numecoeval/referentiel/referentiel_etape_acv.feature b/services/api-referentiel/src/test/resources/org/mte/numecoeval/referentiel/referentiel_etape_acv.feature new file mode 100644 index 00000000..2addaecd --- /dev/null +++ b/services/api-referentiel/src/test/resources/org/mte/numecoeval/referentiel/referentiel_etape_acv.feature @@ -0,0 +1,16 @@ +#language: fr +#encoding: utf-8 +Fonctionnalité: Gestion du referentiel ETAPE ACV + Le but étant de rendre disponible le référentiel referentiel ETAPE ACV afin de rendre possible + les calculs d'impacts environnementaux sur les équipements + + + Scénario: CAF1: alimentation en masse du referentiel etape acv depuis un ficher csv + Quand le fichier CSV "csv/ref_etapeACV.csv" est injecte + Alors Il existe 4 elements dans le referentiel Etape ACV + Et chacun des elements du referentiel ajouté est identique au element du fichier csv + + + + + diff --git a/services/api-referentiel/src/test/resources/org/mte/numecoeval/referentiel/referentiel_facteurs_impact_reseau.feature b/services/api-referentiel/src/test/resources/org/mte/numecoeval/referentiel/referentiel_facteurs_impact_reseau.feature new file mode 100644 index 00000000..d567c0bd --- /dev/null +++ b/services/api-referentiel/src/test/resources/org/mte/numecoeval/referentiel/referentiel_facteurs_impact_reseau.feature @@ -0,0 +1,31 @@ +#language: fr +#encoding: utf-8 +Fonctionnalité: Gestion du referentiel impact reseau + Le but étant de rendre disponible le référentiel refImpactReseau afin de rendre possible + les calculs d'impacts environnementaux sur les équipements + + + Scénario: CAF1: Ajouter un facteur impact lié au reseau dans la table ref_impactReseau + Quand on ajoute un facteur impact reseau dont la refReseau est "impactReseauMobileMoyen", etapeACV est "UTILISATION" , critere est "Acidification" la source est "RefTest V1.00" la valeur est "8.28" et l'unité est "mol H^{+} eq" + Alors impact reseau est persister dans la table ref_impactReseau avec les elements suivants: la refReseau est "impactReseauMobileMoyen", etapeACV est "UTILISATION" , critere est "Acidification" la source est "RefTest V1.00" la valeur est "8.28" et l'unité est "mol H^{+} eq" + + + Scénario: CAF2: Ajouter un facteur impact lié au reseau dans la table ref_impactReseau sans unité ni valeur et modification des valeurs unite et valeur de l'impact reseau ajouté + Quand on persiste un facteur impact reseau dont la refReseau est "impactReseauMobileFRANCE", etapeACV est "FABRICATION" , critere est "Radiations ionisantes" la source est "RefTest V1.00" + Alors il est possible de modifier impact Reseau avec les élements suivants: le refReseau est "impactReseauMobileFRANCE", etapeACV est "FABRICATION" , critere est "Radiations ionisantes" la source est "RefTest V1.00", valeur est "10" et l'unité est "kBq U-235 eq" + + Scénario: CAF3: interdir l'ajout d'un facteur impact lié au reseau dans la table ref_impactReseau sans source + Quand on ajoute un facteur impact reseau dont la refReseau est "impactReseauMobileMoyenSansSource", etapeACV est "UTILISATION" , critere est "Changement Climatique" la valeur est "8.28" et l'unité est "CO2 eq / an" + Alors Impossible d ajouter l'impact réseau dans la table ref_impactreseau + + Scénario: CAF4: alimentation en masse du referentiel impact reseau depuis un ficher csv + Quand le fichier CSV "csv/ref_impactreseau.csv" est envoyé + Alors l impact reseau refReseal est "impactReseauMobile2G", etapeACV est "UTILISATION" , critere est "Radiations ionisantes" la source est "RefTest V1.00" la valeur est "18.8" et l'unité est "kBq U-235 eq" existe + + + Scénario: CAF5: impact reseau qui n'existe pas + Quand on cherche l impact reseau dont la refReseau est "nexistepas", etapeACV est "nexistepas" , critere est "nexistepas" + Alors lecture reponse impact resau n existe pas + + + diff --git a/services/api-referentiel/src/test/resources/org/mte/numecoeval/referentiel/referentiel_hypothese.feature b/services/api-referentiel/src/test/resources/org/mte/numecoeval/referentiel/referentiel_hypothese.feature new file mode 100644 index 00000000..4a29f717 --- /dev/null +++ b/services/api-referentiel/src/test/resources/org/mte/numecoeval/referentiel/referentiel_hypothese.feature @@ -0,0 +1,19 @@ +#language: fr +#encoding: utf-8 +Fonctionnalité: Gestion du referentiel DES HYPOTHESES + Le but étant de rendre disponible le référentiel referentiel DES HYPOTHESES afin de rendre possible + les calculs d'impacts environnementaux sur les équipements + + + Scénario: CAF1: alimentation en masse du referentiel DES HYPOTHESES depuis un ficher csv + Quand le fichier CSV REF HYPOTHESE"csv/ref_Hypothese.csv" est inject + Alors le statut http est 200 + + + Scénario: CAF2: recherche d'une hypothese + Quand lors de la recherche de hypothese avec la cle "PUEPardDfault" + Alors Le resultat est : cle = "PUEPardDfault" valeur="1.6" et source="expertise IJO" + + + + diff --git a/services/api-referentiel/src/test/resources/org/mte/numecoeval/referentiel/referentiel_impact_equipement.feature b/services/api-referentiel/src/test/resources/org/mte/numecoeval/referentiel/referentiel_impact_equipement.feature new file mode 100644 index 00000000..4b6a862a --- /dev/null +++ b/services/api-referentiel/src/test/resources/org/mte/numecoeval/referentiel/referentiel_impact_equipement.feature @@ -0,0 +1,25 @@ +#language: fr +#encoding: utf-8 +Fonctionnalité: Gestion du referentiel ref_ImpactEquipement + Chargement et Mise à disposition du référentiel des impacts d'équipements. + + + Scénario: Sprint 10 - Taiga 869: Ajout d'une colonne description + Quand le fichier CSV des impacts d'équipements "csv/sprint10/ref_ImpactEquipement.csv" est importé par API et la réponse est 200 + Alors La récupération de tous les impacts d'équipements renvoie 6 lignes + Et Vérifications des impacts équipements disponibles + | etape | critere | refEquipement | valeur | consoElecMoyenne | source | type | description | + | FIN_DE_VIE | Acidification | computer monitor-01-23 | 0.020296259210277726 | 35.81692801813716 | reftest_20221013 | computer monitor | Description Impact Equipement computer monitor | + | FABRICATION | Radiations ionisantes | digital price tag - power unit-01 | 5.89E+01 | | reftest_20221013 | digital price tag - power unit | Description Impact Equipement digital price tag - power unit | + | FABRICATION | Radiations ionisantes | docking station-01 | 3.50E+01 | 1.28 | reftest_20221013 | docking station | Description Impact Equipement docking station | + | FABRICATION | Radiations ionisantes | empty 42u bay-01 | 5.87E+01 | | reftest_20221013 | empty 42u bay | Description Impact Equipement empty 42u bay | + | FABRICATION | Radiations ionisantes | feature phone-01 | 5.11E+00 | 0.09 | reftest_20221013 | feature phone | Description Impact Equipement feature phone | + | FABRICATION | Radiations ionisantes | firewall-01 | 2.65E+02 | 876 | reftest_20221013 | firewall | Description Impact Equipement firewall | + + + + + + + + diff --git a/services/api-referentiel/src/test/resources/org/mte/numecoeval/referentiel/referentiel_sprint10.feature b/services/api-referentiel/src/test/resources/org/mte/numecoeval/referentiel/referentiel_sprint10.feature new file mode 100644 index 00000000..c60c9988 --- /dev/null +++ b/services/api-referentiel/src/test/resources/org/mte/numecoeval/referentiel/referentiel_sprint10.feature @@ -0,0 +1,42 @@ +#language: fr +#encoding: utf-8 +Fonctionnalité: Sprint 10 - Features du Sprint 10 (type équipement par défaut, correspondances...) + Nouvelles features disponibles: + - Reférentiel TypeEquipement avec le refEquipementParDefaut correctement géré + - Nouveau référentiel CorrespondanceRefEquipement + + + Scénario: Sprint 10 - Taiga 862: Ajout d'une colonne refEquipementParDefaut dans ref_TypeEquipement + Quand le fichier CSV des types d'équipements "csv/sprint10/ref_TypeEquipement.csv" est importé par API et la réponse est 200 + Alors La récupération de tous les types d'équipements renvoie 17 lignes + Et La récupération unitaire d'un type équipement "ServeurCalcul" répond 200 + Et Vérifications des types d'équipements disponibles + | type | serveur | commentaire | dureeVieDefaut | refEquipementParDefaut | source | + | ServeurCalcul | true | commentaires | 6 | serveur_par_defaut | Test | + | ServeurStockage | true | | 6 | serveur_par_defaut | Test | + | Armoire | false | | 7 | armoir_par_defaut | Test | + | NAS | false | | 6 | baie_stockage_par_defaut | Test | + | Switch | false | | 6 | switch_par_defaut | Test | + | OrdinateurPortable | false | | 4 | laptop_par_defaut | Test | + | OrdinateurDeBureau | false | | 5 | desktop_par_defaut | Test | + | Tablette | false | | 2.5 | tablette_par_defaut | Test | + | TelephoneMobile | false | commentaires | 2.5 | smartphone_par_defaut | Test | + | TelephoneFixe | false | | 10 | telephone_fixe_par_defaut | Test | + | EquipementReseau | false | commentaires | 5 | switch_par_defaut | Test | + | BorneWifi | false | | 4 | borneWifi_par_defaut | Test | + | Imprimante | false | | 5 | imprimante_par_defaut | Test | + | Ecran | false | | 8 | ecran_par_defaut | Test | + | AccessoirePosteDeTravail | false | souris calvier, camera casque | 7 | stationTravail_par_defaut | Test | + | Connectique | false | | 7 | | Test | + | Autre | false | | 2 | | Test | + + + Scénario: Sprint 10 - Taiga 319: Import des données de correspondances de références d'équipements + Quand le fichier CSV des correspondances d'équipements "csv/sprint10/ref_CorrespondanceRefEquipement.csv" est importé par API et la réponse est 200 + Alors La récupération de tous les correspondances d'équipements renvoie 3 lignes + Et Vérifications des correspondances d'équipements disponibles + | modeleEquipementSource | refEquipementCible | + | Fairphone V1 | smartphone01 | + | Fairphone V2 | smartphone01 | + | Ecran 27 pouces | ecran01 | + diff --git a/services/calculs/LICENSE b/services/calculs/LICENSE new file mode 100644 index 00000000..7e131afa --- /dev/null +++ b/services/calculs/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2023 pub / Numérique et Écologie / NumEcoEval - M4G + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/services/calculs/README.md b/services/calculs/README.md new file mode 100644 index 00000000..d1a5f7b2 --- /dev/null +++ b/services/calculs/README.md @@ -0,0 +1,27 @@ +# calculs + +Module contenant l'intégralité des règles de calculs et du code métier lié aux calculs d'impact d'équipement. + +**Ce module s'utilise comme une librairie et non comme une application.** + +## Requirements + +- Any JDK 17 Implementation + - [Coretto](https://docs.aws.amazon.com/corretto/latest/corretto-17-ug/downloads-list.html) + - [Open JDK](https://jdk.java.net/java-se-ri/17) +- [Maven 3](https://maven.apache.org/download.cgi) +- IDE compatible with Maven 3 and JDK 17 are highly recommended to develop + +### Build + +To build the app : + +```bash +mvn clean install +``` + +## License +Le projet est sous licence Apache 2 + +## Project status +If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers. diff --git a/services/calculs/ci_settings.xml b/services/calculs/ci_settings.xml new file mode 100644 index 00000000..5205903f --- /dev/null +++ b/services/calculs/ci_settings.xml @@ -0,0 +1,16 @@ +<settings xmlns="http://maven.apache.org/SETTINGS/1.1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.1.0 http://maven.apache.org/xsd/settings-1.1.0.xsd"> + <servers> + <server> + <id>gitlab-maven</id> + <configuration> + <httpHeaders> + <property> + <name>Job-Token</name> + <value>${env.CI_JOB_TOKEN}</value> + </property> + </httpHeaders> + </configuration> + </server> + </servers> +</settings> diff --git a/services/calculs/dependency_check_suppressions.xml b/services/calculs/dependency_check_suppressions.xml new file mode 100644 index 00000000..8e4bfe62 --- /dev/null +++ b/services/calculs/dependency_check_suppressions.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8"?> +<suppressions xmlns="https://jeremylong.github.io/DependencyCheck/dependency-suppression.1.3.xsd"> + + <suppress> + <notes><![CDATA[ + Non-applicable : json-smart est une dépendance transitive. + Les json sont limités à la taille des inventaires : la taille des listes n'a pas réellement de limite + et c'est souhaités comme cela (traitement de gros volumes de données). + ]]></notes> + <cve>CVE-2023-1370</cve> + </suppress> +</suppressions> diff --git a/services/calculs/pom.xml b/services/calculs/pom.xml new file mode 100644 index 00000000..75e41b0c --- /dev/null +++ b/services/calculs/pom.xml @@ -0,0 +1,124 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.mte.numecoeval</groupId> + <artifactId>core</artifactId> + <version>1.2.3-SNAPSHOT</version> + <relativePath/> <!-- lookup parent from repository --> + </parent> + <artifactId>calculs</artifactId> + <version>1.2.3-SNAPSHOT</version> + <name>calculs</name> + <description>Module contenant l'intégralité des règles de calculs et du code métier lié aux calculs d'impact + d'équipement + </description> + + <repositories> + <repository> + <id>gitlab-maven</id> + <url>https://gitlab-forge.din.developpement-durable.gouv.fr/api/v4/projects/20519/packages/maven</url> + </repository> + </repositories> + + <distributionManagement> + <repository> + <id>gitlab-maven</id> + <url>https://gitlab-forge.din.developpement-durable.gouv.fr/api/v4/projects/20519/packages/maven</url> + </repository> + + <snapshotRepository> + <id>gitlab-maven</id> + <url>https://gitlab-forge.din.developpement-durable.gouv.fr/api/v4/projects/20519/packages/maven</url> + </snapshotRepository> + </distributionManagement> + + <properties> + </properties> + <dependencies> + <!-- Pour les JsonType --> + <dependency> + <groupId>org.openapitools</groupId> + <artifactId>jackson-databind-nullable</artifactId> + </dependency> + <!-- Pour les loggers --> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-api</artifactId> + </dependency> + + <!-- Utilitaires --> + <dependency> + <groupId>org.apache.commons</groupId> + <artifactId>commons-lang3</artifactId> + <version>${apache-commons-lang3.version}</version> + </dependency> + <dependency> + <groupId>org.apache.commons</groupId> + <artifactId>commons-collections4</artifactId> + <version>${apache-commons-collection4.version}</version> + </dependency> + <dependency> + <groupId>commons-io</groupId> + <artifactId>commons-io</artifactId> + <version>${apache-commons-io.version}</version> + </dependency> + + <dependency> + <groupId>org.projectlombok</groupId> + <artifactId>lombok</artifactId> + <optional>true</optional> + </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-test</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.springframework.security</groupId> + <artifactId>spring-security-test</artifactId> + <scope>test</scope> + </dependency> + + <!-- Données de tests --> + <dependency> + <groupId>org.instancio</groupId> + <artifactId>instancio-junit</artifactId> + <scope>test</scope> + </dependency> + + <!-- Tests fonctionnels --> + <dependency> + <groupId>org.junit.platform</groupId> + <artifactId>junit-platform-suite</artifactId> + <scope>test</scope> + </dependency> + + <dependency> + <groupId>io.cucumber</groupId> + <artifactId>cucumber-core</artifactId> + <scope>test</scope> + </dependency> + + <dependency> + <groupId>io.cucumber</groupId> + <artifactId>cucumber-java</artifactId> + <scope>test</scope> + </dependency> + + <dependency> + <groupId>io.cucumber</groupId> + <artifactId>cucumber-spring</artifactId> + <scope>test</scope> + </dependency> + + <dependency> + <groupId>io.cucumber</groupId> + <artifactId>cucumber-junit-platform-engine</artifactId> + <scope>test</scope> + </dependency> + + </dependencies> + +</project> diff --git a/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/demande/DemandeCalculImpactApplication.java b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/demande/DemandeCalculImpactApplication.java new file mode 100644 index 00000000..b2b6d4b4 --- /dev/null +++ b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/demande/DemandeCalculImpactApplication.java @@ -0,0 +1,34 @@ +package org.mte.numecoeval.calculs.domain.data.demande; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; +import org.mte.numecoeval.calculs.domain.data.entree.Application; +import org.mte.numecoeval.calculs.domain.data.indicateurs.ImpactEquipementVirtuel; + +import java.time.LocalDateTime; + +@Builder +@AllArgsConstructor +@Getter +@Setter +public class DemandeCalculImpactApplication { + + LocalDateTime dateCalcul; + Application application; + Integer nbApplications; + ImpactEquipementVirtuel impactEquipementVirtuel; + + /** + * Renvoie le nombre d'application pour un calcul. + * @return si nbApplications est différent de null et supérieur à 0, renvoie {@link #nbApplications} sinon {@code 1} + */ + public Integer getNbApplicationsForCalcul() { + if(nbApplications != null && nbApplications > 0){ + return nbApplications; + } + + return 1; + } +} diff --git a/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/demande/DemandeCalculImpactEquipementPhysique.java b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/demande/DemandeCalculImpactEquipementPhysique.java new file mode 100644 index 00000000..535d1b77 --- /dev/null +++ b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/demande/DemandeCalculImpactEquipementPhysique.java @@ -0,0 +1,90 @@ +package org.mte.numecoeval.calculs.domain.data.demande; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; +import org.apache.commons.collections4.CollectionUtils; +import org.mte.numecoeval.calculs.domain.data.entree.EquipementPhysique; +import org.mte.numecoeval.calculs.domain.data.referentiel.*; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +@Builder +@AllArgsConstructor +@Getter +@Setter +public class DemandeCalculImpactEquipementPhysique { + + LocalDateTime dateCalcul; + + EquipementPhysique equipementPhysique; + + ReferentielEtapeACV etape; + + ReferentielCritere critere; + + ReferentielTypeEquipement typeEquipement; + + ReferentielCorrespondanceRefEquipement correspondanceRefEquipement; + + List<ReferentielHypothese> hypotheses; + + List<ReferentielImpactEquipement> impactEquipements; + + List<ReferentielMixElectrique> mixElectriques; + + public Optional<ReferentielHypothese> getHypotheseFromCode(String code) { + if (code == null) { + return Optional.empty(); + } + return CollectionUtils.emptyIfNull(hypotheses) + .stream() + .filter(hypothese -> code.equals(hypothese.getCode())) + .findFirst(); + } + + public Optional<ReferentielImpactEquipement> getImpactEquipement(String refEquipement) { + if (refEquipement == null) { + return Optional.empty(); + } + return CollectionUtils.emptyIfNull(impactEquipements) + .stream() + .filter(Objects::nonNull) + .filter(impactEquipement -> refEquipement.equals(impactEquipement.getRefEquipement()) + && critere.getNomCritere().equals(impactEquipement.getCritere()) + && etape.getCode().equals(impactEquipement.getEtape()) + ) + .findFirst(); + } + + public Optional<ReferentielMixElectrique> getMixElectrique(String pays) { + if (pays == null) { + return Optional.empty(); + } + return CollectionUtils.emptyIfNull(mixElectriques) + .stream() + .filter(Objects::nonNull) + .filter(mixElectrique -> pays.equals(mixElectrique.getPays()) + && critere.getNomCritere().equals(mixElectrique.getCritere()) + ) + .findFirst(); + } + + public String getRefEquipementCible() { + if (correspondanceRefEquipement != null) { + return correspondanceRefEquipement.getRefEquipementCible(); + } + return null; + } + + public String getRefEquipementParDefaut() { + if (typeEquipement != null) { + return typeEquipement.getRefEquipementParDefaut(); + } + return null; + } +} diff --git a/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/demande/DemandeCalculImpactEquipementVirtuel.java b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/demande/DemandeCalculImpactEquipementVirtuel.java new file mode 100644 index 00000000..abb39c39 --- /dev/null +++ b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/demande/DemandeCalculImpactEquipementVirtuel.java @@ -0,0 +1,24 @@ +package org.mte.numecoeval.calculs.domain.data.demande; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; +import org.mte.numecoeval.calculs.domain.data.entree.EquipementVirtuel; +import org.mte.numecoeval.calculs.domain.data.indicateurs.ImpactEquipementPhysique; + +import java.time.LocalDateTime; + +@Builder +@AllArgsConstructor +@Getter +@Setter +public class DemandeCalculImpactEquipementVirtuel { + + LocalDateTime dateCalcul; + EquipementVirtuel equipementVirtuel; + Integer nbEquipementsVirtuels; + Integer nbTotalVCPU; + ImpactEquipementPhysique impactEquipement; + Double stockageTotalVirtuel; +} diff --git a/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/demande/DemandeCalculImpactMessagerie.java b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/demande/DemandeCalculImpactMessagerie.java new file mode 100644 index 00000000..2c10b3be --- /dev/null +++ b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/demande/DemandeCalculImpactMessagerie.java @@ -0,0 +1,27 @@ +package org.mte.numecoeval.calculs.domain.data.demande; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; +import org.mte.numecoeval.calculs.domain.data.entree.Messagerie; +import org.mte.numecoeval.calculs.domain.data.referentiel.ReferentielCritere; +import org.mte.numecoeval.calculs.domain.data.referentiel.ReferentielImpactMessagerie; + +import java.time.LocalDateTime; +import java.util.List; + +@Builder +@AllArgsConstructor +@Getter +@Setter +public class DemandeCalculImpactMessagerie { + + LocalDateTime dateCalcul; + + Messagerie messagerie; + + ReferentielCritere critere; + + List<ReferentielImpactMessagerie> impactsMessagerie; +} diff --git a/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/demande/DemandeCalculImpactReseau.java b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/demande/DemandeCalculImpactReseau.java new file mode 100644 index 00000000..4a76a7ad --- /dev/null +++ b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/demande/DemandeCalculImpactReseau.java @@ -0,0 +1,30 @@ +package org.mte.numecoeval.calculs.domain.data.demande; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; +import org.mte.numecoeval.calculs.domain.data.entree.EquipementPhysique; +import org.mte.numecoeval.calculs.domain.data.referentiel.ReferentielCritere; +import org.mte.numecoeval.calculs.domain.data.referentiel.ReferentielEtapeACV; +import org.mte.numecoeval.calculs.domain.data.referentiel.ReferentielImpactReseau; + +import java.time.LocalDateTime; +import java.util.List; + +@Builder +@AllArgsConstructor +@Getter +@Setter +public class DemandeCalculImpactReseau { + + LocalDateTime dateCalcul; + + EquipementPhysique equipementPhysique; + + ReferentielEtapeACV etape; + + ReferentielCritere critere; + + List<ReferentielImpactReseau> impactsReseau; +} diff --git a/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/entree/Application.java b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/entree/Application.java new file mode 100644 index 00000000..74624e54 --- /dev/null +++ b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/entree/Application.java @@ -0,0 +1,24 @@ +package org.mte.numecoeval.calculs.domain.data.entree; + +import lombok.Builder; +import lombok.Data; + +import java.time.LocalDate; + +@Data +@Builder +public class Application { + private String nomApplication; + private String typeEnvironnement; + private String nomEquipementVirtuel; + private String nomSourceDonneeEquipementVirtuel; + + private String nomEquipementPhysique; + private String domaine; + private String sousDomaine; + private String nomLot; + private LocalDate dateLot; + private String nomOrganisation; + private String nomEntite; + private String nomSourceDonnee; +} diff --git a/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/entree/DataCenter.java b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/entree/DataCenter.java new file mode 100644 index 00000000..48255278 --- /dev/null +++ b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/entree/DataCenter.java @@ -0,0 +1,21 @@ +package org.mte.numecoeval.calculs.domain.data.entree; + +import lombok.Builder; +import lombok.Data; + +import java.time.LocalDate; + +@Data +@Builder +public class DataCenter { + + private String nomCourtDatacenter; + private String nomLongDatacenter; + private Double pue; + private String localisation; + private String nomLot; + private LocalDate dateLot; + private String nomOrganisation; + private String nomEntite; + private String nomSourceDonnee; +} diff --git a/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/entree/EquipementPhysique.java b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/entree/EquipementPhysique.java new file mode 100644 index 00000000..5a054b52 --- /dev/null +++ b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/entree/EquipementPhysique.java @@ -0,0 +1,39 @@ +package org.mte.numecoeval.calculs.domain.data.entree; + +import lombok.Builder; +import lombok.Data; + +import java.time.LocalDate; +import java.util.List; + +@Data +@Builder +public class EquipementPhysique { + private String nomEquipementPhysique; + private String modele; + private String type; + private Double consoElecAnnuelle; + private String statut; + private String paysDUtilisation; + private String utilisateur; + private LocalDate dateAchat; + private LocalDate dateRetrait; + private Double quantite; + private Float goTelecharge; + private Double nbJourUtiliseAn; + private String nbCoeur; + private String nomCourtDatacenter; + private DataCenter dataCenter; + private boolean serveur; + private Double dureeVieDefaut; + private List<EquipementVirtuel> equipementsVirtuels; + private String nomLot; + private LocalDate dateLot; + private String nomOrganisation; + private String nomEntite; + private String nomSourceDonnee; + private Integer nbEquipementsVirtuels; + private Integer nbTotalVCPU; + private String modeUtilisation; + private Double tauxUtilisation; +} diff --git a/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/entree/EquipementVirtuel.java b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/entree/EquipementVirtuel.java new file mode 100644 index 00000000..dad80c1a --- /dev/null +++ b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/entree/EquipementVirtuel.java @@ -0,0 +1,27 @@ +package org.mte.numecoeval.calculs.domain.data.entree; + + +import lombok.Builder; +import lombok.Data; + +import java.time.LocalDate; + +@Data +@Builder +public class EquipementVirtuel { + private Long id; + private String nomEquipementVirtuel; + private String nomEquipementPhysique; + private String nomSourceDonneeEquipementPhysique; + private Integer vCPU; + private String cluster; + private String nomLot; + private LocalDate dateLot; + private String nomOrganisation; + private String nomEntite; + private String nomSourceDonnee; + private Double consoElecAnnuelle; + private String typeEqv; + private Double capaciteStockage; + private Double cleRepartition; +} diff --git a/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/entree/Messagerie.java b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/entree/Messagerie.java new file mode 100644 index 00000000..3df95aa4 --- /dev/null +++ b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/entree/Messagerie.java @@ -0,0 +1,21 @@ +package org.mte.numecoeval.calculs.domain.data.entree; + +import lombok.Builder; +import lombok.Data; + +import java.time.LocalDate; + +@Data +@Builder +public class Messagerie { + private Double volumeTotalMailEmis; + private Double nombreMailEmis; + private Double nombreMailEmisXDestinataires; + private Integer moisAnnee; + private String nomLot; + private LocalDate dateLot; + private String nomOrganisation; + private String nomEntite; + private String nomSourceDonnee; + +} diff --git a/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/erreur/TypeErreurCalcul.java b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/erreur/TypeErreurCalcul.java new file mode 100644 index 00000000..6b5e35d7 --- /dev/null +++ b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/erreur/TypeErreurCalcul.java @@ -0,0 +1,14 @@ +package org.mte.numecoeval.calculs.domain.data.erreur; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum TypeErreurCalcul { + + ERREUR_FONCTIONNELLE("ErrCalcFonc"), + ERREUR_TECHNIQUE("ErrCalcTech"); + + private final String code; +} diff --git a/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/indicateurs/ImpactApplication.java b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/indicateurs/ImpactApplication.java new file mode 100644 index 00000000..7f615258 --- /dev/null +++ b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/indicateurs/ImpactApplication.java @@ -0,0 +1,38 @@ +package org.mte.numecoeval.calculs.domain.data.indicateurs; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +@Builder +@AllArgsConstructor +@Getter +@Setter +public class ImpactApplication { + LocalDateTime dateCalcul; + String versionCalcul; + String etapeACV; + String critere; + String source; + String statutIndicateur; + String trace; + String nomLot; + LocalDate dateLot; + String unite; + String nomOrganisation; + String nomEntite; + String nomSourceDonnee; + String nomEquipementPhysique; + String nomEquipementVirtuel; + String nomApplication; + String typeEnvironnement; + String domaine; + String sousDomaine; + Double impactUnitaire; + Double consoElecMoyenne; + Long idEntree; +} diff --git a/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/indicateurs/ImpactEquipementPhysique.java b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/indicateurs/ImpactEquipementPhysique.java new file mode 100644 index 00000000..610d6bd2 --- /dev/null +++ b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/indicateurs/ImpactEquipementPhysique.java @@ -0,0 +1,36 @@ +package org.mte.numecoeval.calculs.domain.data.indicateurs; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +@Builder +@AllArgsConstructor +@Getter +@Setter +public class ImpactEquipementPhysique { + LocalDateTime dateCalcul; + String versionCalcul; + String etapeACV; + String critere; + String source; + String statutIndicateur; + String trace; + String nomLot; + LocalDate dateLot; + String unite; + String nomOrganisation; + String nomEntite; + String nomSourceDonnee; + String reference; + String nomEquipement; + String typeEquipement; + Double impactUnitaire; + Double consoElecMoyenne; + Double quantite; + String statutEquipementPhysique; +} diff --git a/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/indicateurs/ImpactEquipementVirtuel.java b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/indicateurs/ImpactEquipementVirtuel.java new file mode 100644 index 00000000..03965dc4 --- /dev/null +++ b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/indicateurs/ImpactEquipementVirtuel.java @@ -0,0 +1,35 @@ +package org.mte.numecoeval.calculs.domain.data.indicateurs; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +@Builder +@AllArgsConstructor +@Getter +@Setter +public class ImpactEquipementVirtuel { + LocalDateTime dateCalcul; + String versionCalcul; + String etapeACV; + String critere; + String source; + String statutIndicateur; + String trace; + String nomLot; + LocalDate dateLot; + String unite; + String nomOrganisation; + String nomEntite; + String nomEquipement; + String nomSourceDonnee; + String nomEquipementVirtuel; + String cluster; + Double impactUnitaire; + Double consoElecMoyenne; + Long idEntree; +} diff --git a/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/indicateurs/ImpactMessagerie.java b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/indicateurs/ImpactMessagerie.java new file mode 100644 index 00000000..d87ca265 --- /dev/null +++ b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/indicateurs/ImpactMessagerie.java @@ -0,0 +1,34 @@ +package org.mte.numecoeval.calculs.domain.data.indicateurs; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +@Builder +@AllArgsConstructor +@Getter +@Setter +public class ImpactMessagerie { + + LocalDateTime dateCalcul; + String versionCalcul; + String critere; + String source; + String statutIndicateur; + String trace; + String nomLot; + LocalDate dateLot; + String unite; + String nomOrganisation; + String nomEntite; + String nomSourceDonnee; + Double impactMensuel; + Integer moisAnnee; + Double volumeTotalMailEmis; + Double nombreMailEmis; + +} diff --git a/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/indicateurs/ImpactReseau.java b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/indicateurs/ImpactReseau.java new file mode 100644 index 00000000..b439f953 --- /dev/null +++ b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/indicateurs/ImpactReseau.java @@ -0,0 +1,35 @@ +package org.mte.numecoeval.calculs.domain.data.indicateurs; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +@Builder +@AllArgsConstructor +@Getter +@Setter +public class ImpactReseau { + LocalDateTime dateCalcul; + String etapeACV; + String critere; + String nomLot; + LocalDate dateLot; + String nomOrganisation; + String nomEquipement; + String nomEntite; + String nomSourceDonnee; + + + String versionCalcul; + String source; + String statutIndicateur; + String trace; + String unite; + Double impactUnitaire; + String reference; + +} diff --git a/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/referentiel/ReferentielCorrespondanceRefEquipement.java b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/referentiel/ReferentielCorrespondanceRefEquipement.java new file mode 100644 index 00000000..0170b74f --- /dev/null +++ b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/referentiel/ReferentielCorrespondanceRefEquipement.java @@ -0,0 +1,17 @@ +package org.mte.numecoeval.calculs.domain.data.referentiel; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; + +@Builder +@AllArgsConstructor +@Getter +@Setter +public class ReferentielCorrespondanceRefEquipement { + + String modeleEquipementSource; + + String refEquipementCible; +} diff --git a/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/referentiel/ReferentielCritere.java b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/referentiel/ReferentielCritere.java new file mode 100644 index 00000000..20969e56 --- /dev/null +++ b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/referentiel/ReferentielCritere.java @@ -0,0 +1,13 @@ +package org.mte.numecoeval.calculs.domain.data.referentiel; + +import lombok.Builder; +import lombok.Data; + +@Builder +@Data +public class ReferentielCritere { + String nomCritere; + String unite; + String description; + +} diff --git a/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/referentiel/ReferentielEtapeACV.java b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/referentiel/ReferentielEtapeACV.java new file mode 100644 index 00000000..9e42c595 --- /dev/null +++ b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/referentiel/ReferentielEtapeACV.java @@ -0,0 +1,12 @@ +package org.mte.numecoeval.calculs.domain.data.referentiel; + +import lombok.Builder; +import lombok.Data; + +@Builder +@Data +public class ReferentielEtapeACV { + String code; + String libelle; + +} diff --git a/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/referentiel/ReferentielHypothese.java b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/referentiel/ReferentielHypothese.java new file mode 100644 index 00000000..351db7d6 --- /dev/null +++ b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/referentiel/ReferentielHypothese.java @@ -0,0 +1,14 @@ +package org.mte.numecoeval.calculs.domain.data.referentiel; + + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class ReferentielHypothese { + + private String code; + private Double valeur; + private String source; +} diff --git a/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/referentiel/ReferentielImpactEquipement.java b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/referentiel/ReferentielImpactEquipement.java new file mode 100644 index 00000000..7aa514cd --- /dev/null +++ b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/referentiel/ReferentielImpactEquipement.java @@ -0,0 +1,16 @@ +package org.mte.numecoeval.calculs.domain.data.referentiel; + +import lombok.Builder; +import lombok.Data; + +@Builder +@Data +public class ReferentielImpactEquipement { + String refEquipement; + String etape; + String critere; + String source; + String type; + Double valeur; + Double consoElecMoyenne; +} diff --git a/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/referentiel/ReferentielImpactMessagerie.java b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/referentiel/ReferentielImpactMessagerie.java new file mode 100644 index 00000000..127756ae --- /dev/null +++ b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/referentiel/ReferentielImpactMessagerie.java @@ -0,0 +1,13 @@ +package org.mte.numecoeval.calculs.domain.data.referentiel; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class ReferentielImpactMessagerie { + private Double constanteCoefficientDirecteur; + private Double constanteOrdonneeOrigine; + private String critere; + private String source; +} diff --git a/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/referentiel/ReferentielImpactReseau.java b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/referentiel/ReferentielImpactReseau.java new file mode 100644 index 00000000..6ed08356 --- /dev/null +++ b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/referentiel/ReferentielImpactReseau.java @@ -0,0 +1,19 @@ +package org.mte.numecoeval.calculs.domain.data.referentiel; + +import lombok.Builder; +import lombok.Data; + +@Builder +@Data +public class ReferentielImpactReseau { + + private String refReseau; + private String critere; + private String etapeACV; + private String unite; + private String source; + private Double impactReseauMobileMoyen; + private Double consoElecMoyenne; + + +} diff --git a/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/referentiel/ReferentielMixElectrique.java b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/referentiel/ReferentielMixElectrique.java new file mode 100644 index 00000000..67ca4449 --- /dev/null +++ b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/referentiel/ReferentielMixElectrique.java @@ -0,0 +1,14 @@ +package org.mte.numecoeval.calculs.domain.data.referentiel; + +import lombok.Builder; +import lombok.Data; + +@Builder +@Data +public class ReferentielMixElectrique { + String pays; + String raccourcisAnglais; + String critere; + Double valeur; + String source; +} diff --git a/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/referentiel/ReferentielTypeEquipement.java b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/referentiel/ReferentielTypeEquipement.java new file mode 100644 index 00000000..7a762ff5 --- /dev/null +++ b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/referentiel/ReferentielTypeEquipement.java @@ -0,0 +1,24 @@ +package org.mte.numecoeval.calculs.domain.data.referentiel; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; + +@Builder +@AllArgsConstructor +@Getter +@Setter +public class ReferentielTypeEquipement { + String type; + + Boolean serveur; + + String commentaire; + + Double dureeVieDefaut; + + String source; + + String refEquipementParDefaut; +} diff --git a/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/trace/ConsoElecAnMoyenne.java b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/trace/ConsoElecAnMoyenne.java new file mode 100644 index 00000000..fc99e3b3 --- /dev/null +++ b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/trace/ConsoElecAnMoyenne.java @@ -0,0 +1,15 @@ +package org.mte.numecoeval.calculs.domain.data.trace; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Builder; +import lombok.Data; + +@Builder +@Data +@JsonInclude(JsonInclude.Include.NON_NULL) +public class ConsoElecAnMoyenne { + private Double valeur; + private Double valeurEquipementConsoElecAnnuelle; + private Double valeurReferentielConsoElecMoyenne; + private String sourceReferentielImpactEquipement; +} diff --git a/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/trace/DureeDeVie.java b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/trace/DureeDeVie.java new file mode 100644 index 00000000..1697802d --- /dev/null +++ b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/trace/DureeDeVie.java @@ -0,0 +1,15 @@ +package org.mte.numecoeval.calculs.domain.data.trace; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +@JsonInclude(JsonInclude.Include.NON_NULL) +public class DureeDeVie { + private Double valeur; + private String dateAchat; + private String dateRetrait; + private DureeDeVieParDefaut dureeDeVieParDefaut; +} diff --git a/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/trace/DureeDeVieParDefaut.java b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/trace/DureeDeVieParDefaut.java new file mode 100644 index 00000000..bb046fc9 --- /dev/null +++ b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/trace/DureeDeVieParDefaut.java @@ -0,0 +1,15 @@ +package org.mte.numecoeval.calculs.domain.data.trace; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +@JsonInclude(JsonInclude.Include.NON_NULL) +public class DureeDeVieParDefaut { + private Double valeur; + private Double valeurEquipementDureeVieDefaut; + private Double valeurReferentielHypothese; + private String sourceReferentielHypothese; +} diff --git a/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/trace/MixElectrique.java b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/trace/MixElectrique.java new file mode 100644 index 00000000..5f6e0e45 --- /dev/null +++ b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/trace/MixElectrique.java @@ -0,0 +1,16 @@ +package org.mte.numecoeval.calculs.domain.data.trace; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Builder; +import lombok.Data; + +@Builder +@Data +@JsonInclude(JsonInclude.Include.NON_NULL) +public class MixElectrique { + private Double valeur; + private boolean isServeur; + private Double dataCenterPue; + private Double valeurReferentielMixElectrique; + private String sourceReferentielMixElectrique; +} diff --git a/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/trace/TraceCalculImpactApplication.java b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/trace/TraceCalculImpactApplication.java new file mode 100644 index 00000000..e6da1f22 --- /dev/null +++ b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/trace/TraceCalculImpactApplication.java @@ -0,0 +1,15 @@ +package org.mte.numecoeval.calculs.domain.data.trace; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +@JsonInclude(JsonInclude.Include.NON_NULL) +public class TraceCalculImpactApplication { + + String formule; + String erreur; + Integer nbApplications; +} diff --git a/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/trace/TraceCalculImpactEquipementPhysique.java b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/trace/TraceCalculImpactEquipementPhysique.java new file mode 100644 index 00000000..a8236a86 --- /dev/null +++ b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/trace/TraceCalculImpactEquipementPhysique.java @@ -0,0 +1,18 @@ +package org.mte.numecoeval.calculs.domain.data.trace; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +@JsonInclude(JsonInclude.Include.NON_NULL) +public class TraceCalculImpactEquipementPhysique { + private String formule; + private ConsoElecAnMoyenne consoElecAnMoyenne; + private MixElectrique mixElectrique; + private Double valeurReferentielImpactEquipement; + private String sourceReferentielImpactEquipement; + private DureeDeVie dureeDeVie; + private String erreur; +} diff --git a/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/trace/TraceCalculImpactEquipementVirtuel.java b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/trace/TraceCalculImpactEquipementVirtuel.java new file mode 100644 index 00000000..910b9402 --- /dev/null +++ b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/trace/TraceCalculImpactEquipementVirtuel.java @@ -0,0 +1,16 @@ +package org.mte.numecoeval.calculs.domain.data.trace; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +@JsonInclude(JsonInclude.Include.NON_NULL) +public class TraceCalculImpactEquipementVirtuel { + String formule; + String erreur; + Integer nbEquipementsVirtuels; + Integer nbTotalVCPU; + Double stockageTotalVirtuel; +} diff --git a/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/trace/TraceCalculImpactMessagerie.java b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/trace/TraceCalculImpactMessagerie.java new file mode 100644 index 00000000..b13e97b9 --- /dev/null +++ b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/trace/TraceCalculImpactMessagerie.java @@ -0,0 +1,20 @@ +package org.mte.numecoeval.calculs.domain.data.trace; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +@JsonInclude(JsonInclude.Include.NON_NULL) +public class TraceCalculImpactMessagerie { + private String critere; + private Double volumeTotalMailEmis; + private Double nombreMailEmis; + private Double constanteCoefficientDirecteur; + private Double poidsMoyenMail; + private Double constanteOrdonneeOrigine; + private Double nombreMailEmisXDestinataires; + private String formule; + private String erreur; +} diff --git a/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/trace/TraceCalculImpactReseau.java b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/trace/TraceCalculImpactReseau.java new file mode 100644 index 00000000..ae8dd1e0 --- /dev/null +++ b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/data/trace/TraceCalculImpactReseau.java @@ -0,0 +1,20 @@ +package org.mte.numecoeval.calculs.domain.data.trace; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +@JsonInclude(JsonInclude.Include.NON_NULL) +public class TraceCalculImpactReseau { + + private String critere; + private String etapeACV; + private String sourceReferentielReseau; + private String equipementPhysique; + private String impactReseauMobileMoyen; + private String goTelecharge; + private String formule; + private String erreur; +} diff --git a/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/exception/CalculImpactException.java b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/exception/CalculImpactException.java new file mode 100644 index 00000000..e6efb40b --- /dev/null +++ b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/exception/CalculImpactException.java @@ -0,0 +1,15 @@ +package org.mte.numecoeval.calculs.domain.exception; + +public class CalculImpactException extends Exception{ + + private final String errorType; + + public CalculImpactException(String errorType,String message) { + super(message); + this.errorType = errorType; + } + + public String getErrorType() { + return errorType; + } +} diff --git a/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/exception/CalculImpactRuntimeException.java b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/exception/CalculImpactRuntimeException.java new file mode 100644 index 00000000..bc4fd98e --- /dev/null +++ b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/exception/CalculImpactRuntimeException.java @@ -0,0 +1,25 @@ +package org.mte.numecoeval.calculs.domain.exception; + +public class CalculImpactRuntimeException extends RuntimeException{ + + private final String errorType; + + public CalculImpactRuntimeException(String message) { + super(message); + this.errorType = null; + } + + public CalculImpactRuntimeException(String message, Throwable cause) { + super(message, cause); + this.errorType = null; + } + + public CalculImpactRuntimeException(String errorType, String message ) { + super(message); + this.errorType = errorType; + } + + public String getErrorType() { + return errorType; + } +} diff --git a/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/port/input/service/CalculImpactApplicationService.java b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/port/input/service/CalculImpactApplicationService.java new file mode 100644 index 00000000..197a7964 --- /dev/null +++ b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/port/input/service/CalculImpactApplicationService.java @@ -0,0 +1,10 @@ +package org.mte.numecoeval.calculs.domain.port.input.service; + +import org.mte.numecoeval.calculs.domain.data.demande.DemandeCalculImpactApplication; +import org.mte.numecoeval.calculs.domain.data.indicateurs.ImpactApplication; + +public interface CalculImpactApplicationService { + + ImpactApplication calculImpactApplicatif(DemandeCalculImpactApplication demandeCalcul); + +} diff --git a/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/port/input/service/CalculImpactEquipementPhysiqueService.java b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/port/input/service/CalculImpactEquipementPhysiqueService.java new file mode 100644 index 00000000..1f32ad22 --- /dev/null +++ b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/port/input/service/CalculImpactEquipementPhysiqueService.java @@ -0,0 +1,9 @@ +package org.mte.numecoeval.calculs.domain.port.input.service; + +import org.mte.numecoeval.calculs.domain.data.demande.DemandeCalculImpactEquipementPhysique; +import org.mte.numecoeval.calculs.domain.data.indicateurs.ImpactEquipementPhysique; + +public interface CalculImpactEquipementPhysiqueService { + + ImpactEquipementPhysique calculerImpactEquipementPhysique(DemandeCalculImpactEquipementPhysique demandeCalculImpactEquipementPhysique); +} diff --git a/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/port/input/service/CalculImpactEquipementVirtuelService.java b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/port/input/service/CalculImpactEquipementVirtuelService.java new file mode 100644 index 00000000..68bb6036 --- /dev/null +++ b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/port/input/service/CalculImpactEquipementVirtuelService.java @@ -0,0 +1,9 @@ +package org.mte.numecoeval.calculs.domain.port.input.service; + +import org.mte.numecoeval.calculs.domain.data.demande.DemandeCalculImpactEquipementVirtuel; +import org.mte.numecoeval.calculs.domain.data.indicateurs.ImpactEquipementVirtuel; + +public interface CalculImpactEquipementVirtuelService { + + ImpactEquipementVirtuel calculerImpactEquipementVirtuel(DemandeCalculImpactEquipementVirtuel demandeCalcul); +} \ No newline at end of file diff --git a/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/port/input/service/CalculImpactMessagerieService.java b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/port/input/service/CalculImpactMessagerieService.java new file mode 100644 index 00000000..a4ad4439 --- /dev/null +++ b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/port/input/service/CalculImpactMessagerieService.java @@ -0,0 +1,10 @@ +package org.mte.numecoeval.calculs.domain.port.input.service; + +import org.mte.numecoeval.calculs.domain.data.demande.DemandeCalculImpactMessagerie; +import org.mte.numecoeval.calculs.domain.data.indicateurs.ImpactMessagerie; + +public interface CalculImpactMessagerieService { + + ImpactMessagerie calculerImpactMessagerie(DemandeCalculImpactMessagerie demandeCalcul); + +} diff --git a/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/port/input/service/CalculImpactReseauService.java b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/port/input/service/CalculImpactReseauService.java new file mode 100644 index 00000000..5d2b8b5b --- /dev/null +++ b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/port/input/service/CalculImpactReseauService.java @@ -0,0 +1,12 @@ +package org.mte.numecoeval.calculs.domain.port.input.service; + +import org.mte.numecoeval.calculs.domain.data.demande.DemandeCalculImpactReseau; +import org.mte.numecoeval.calculs.domain.data.indicateurs.ImpactReseau; + +public interface CalculImpactReseauService { + + String REF_RESEAU = "impactReseauMobileMoyen"; + + ImpactReseau calculerImpactReseau(DemandeCalculImpactReseau demandeCalcul); + +} diff --git a/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/port/input/service/DureeDeVieEquipementPhysiqueService.java b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/port/input/service/DureeDeVieEquipementPhysiqueService.java new file mode 100644 index 00000000..2eb64689 --- /dev/null +++ b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/port/input/service/DureeDeVieEquipementPhysiqueService.java @@ -0,0 +1,13 @@ +package org.mte.numecoeval.calculs.domain.port.input.service; + +import org.mte.numecoeval.calculs.domain.data.demande.DemandeCalculImpactEquipementPhysique; +import org.mte.numecoeval.calculs.domain.data.trace.DureeDeVie; +import org.mte.numecoeval.calculs.domain.data.trace.DureeDeVieParDefaut; +import org.mte.numecoeval.calculs.domain.exception.CalculImpactException; + +public interface DureeDeVieEquipementPhysiqueService { + + DureeDeVieParDefaut calculerDureeVieDefaut(DemandeCalculImpactEquipementPhysique demandeCalcul) throws CalculImpactException; + + DureeDeVie calculerDureeVie(DemandeCalculImpactEquipementPhysique demandeCalcul) throws CalculImpactException; +} diff --git a/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/port/input/service/impl/CalculImpactApplicationServiceImpl.java b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/port/input/service/impl/CalculImpactApplicationServiceImpl.java new file mode 100644 index 00000000..d2ff9742 --- /dev/null +++ b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/port/input/service/impl/CalculImpactApplicationServiceImpl.java @@ -0,0 +1,129 @@ +package org.mte.numecoeval.calculs.domain.port.input.service.impl; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.mte.numecoeval.calculs.domain.data.demande.DemandeCalculImpactApplication; +import org.mte.numecoeval.calculs.domain.data.erreur.TypeErreurCalcul; +import org.mte.numecoeval.calculs.domain.data.indicateurs.ImpactApplication; +import org.mte.numecoeval.calculs.domain.exception.CalculImpactException; +import org.mte.numecoeval.calculs.domain.port.input.service.CalculImpactApplicationService; +import org.mte.numecoeval.calculs.domain.traceur.TraceCalculImpactApplicationUtils; +import org.mte.numecoeval.calculs.domain.traceur.TraceCalculImpactVirtuelUtils; +import org.mte.numecoeval.calculs.domain.traceur.TraceUtils; + +@Slf4j +@AllArgsConstructor +public class CalculImpactApplicationServiceImpl implements CalculImpactApplicationService { + + private static final String VERSION_CALCUL = "1.1"; + + private ObjectMapper objectMapper; + + @Override + public ImpactApplication calculImpactApplicatif(DemandeCalculImpactApplication demandeCalcul) { + ImpactApplication impactErreur; + try { + return calculerImpact(demandeCalcul); + } catch (CalculImpactException e) { + log.debug("Erreur de calcul d'impact d'équipement virtuel : Type : {}, Cause: {}, Etape: {}, Critere: {}, Nom Equipment Physique: {}, Nom Equipement Virtuel: {}, Nom Application: {}, Type Environnement: {}", + e.getErrorType(), e.getMessage(), + demandeCalcul.getImpactEquipementVirtuel().getEtapeACV(), + demandeCalcul.getImpactEquipementVirtuel().getCritere(), + demandeCalcul.getApplication().getNomEquipementPhysique(), + demandeCalcul.getApplication().getNomEquipementVirtuel(), + demandeCalcul.getApplication().getNomApplication(), + demandeCalcul.getApplication().getTypeEnvironnement() + ); + + impactErreur = buildImpactForError(demandeCalcul, e); + } catch (Exception e) { + log.debug("Erreur de calcul d'impact d'équipement virtuel : Type : {}, Cause: {}, Etape: {}, Critere: {}, Nom Equipment Physique: {}, Nom Equipement Virtuel: {}, Nom Application: {}, Type Environnement: {}", + TypeErreurCalcul.ERREUR_TECHNIQUE.getCode(), e.getMessage(), + demandeCalcul.getImpactEquipementVirtuel().getEtapeACV(), + demandeCalcul.getImpactEquipementVirtuel().getCritere(), + demandeCalcul.getApplication().getNomEquipementPhysique(), + demandeCalcul.getApplication().getNomEquipementVirtuel(), + demandeCalcul.getApplication().getNomApplication(), + demandeCalcul.getApplication().getTypeEnvironnement() + ); + impactErreur = buildImpactForError(demandeCalcul, new CalculImpactException(TypeErreurCalcul.ERREUR_TECHNIQUE.getCode(), e.getMessage())); + } + + return impactErreur; + } + + private ImpactApplication buildImpactForError(DemandeCalculImpactApplication demandeCalcul, CalculImpactException exception) { + return ImpactApplication.builder() + .dateCalcul(demandeCalcul.getDateCalcul()) + .versionCalcul(VERSION_CALCUL) + .etapeACV(demandeCalcul.getImpactEquipementVirtuel().getEtapeACV()) + .critere(demandeCalcul.getImpactEquipementVirtuel().getCritere()) + .statutIndicateur("ERREUR") + .trace(TraceUtils.getTraceFromTraceur(objectMapper, TraceCalculImpactVirtuelUtils.buildTraceErreur(exception))) + .nomLot(demandeCalcul.getApplication().getNomLot()) + .nomSourceDonnee(demandeCalcul.getApplication().getNomSourceDonnee()) + .dateLot(demandeCalcul.getApplication().getDateLot()) + .unite(demandeCalcul.getImpactEquipementVirtuel().getUnite()) + .nomOrganisation(demandeCalcul.getApplication().getNomOrganisation()) + .nomEntite(demandeCalcul.getApplication().getNomEntite()) + .nomEquipementPhysique(demandeCalcul.getApplication().getNomEquipementPhysique()) + .nomEquipementVirtuel(demandeCalcul.getApplication().getNomEquipementVirtuel()) + .nomApplication(demandeCalcul.getApplication().getNomApplication()) + .typeEnvironnement(demandeCalcul.getApplication().getTypeEnvironnement()) + .domaine(demandeCalcul.getApplication().getDomaine()) + .sousDomaine(demandeCalcul.getApplication().getSousDomaine()) + .impactUnitaire(null) + .consoElecMoyenne(null) + .build(); + } + + private ImpactApplication calculerImpact(DemandeCalculImpactApplication demandeCalcul) throws CalculImpactException { + if (!"OK".equals(demandeCalcul.getImpactEquipementVirtuel().getStatutIndicateur())) { + throw new CalculImpactException( + TypeErreurCalcul.ERREUR_FONCTIONNELLE.getCode(), + "L'indicateur d'impact équipement virtuel associé est au statut : " + demandeCalcul.getImpactEquipementVirtuel().getStatutIndicateur() + ); + } + if (demandeCalcul.getImpactEquipementVirtuel().getImpactUnitaire() == null) { + throw new CalculImpactException( + TypeErreurCalcul.ERREUR_FONCTIONNELLE.getCode(), + "L'indicateur d'impact équipement virtuel associé est null." + ); + } + + var nbApplications = demandeCalcul.getNbApplicationsForCalcul(); + + var resultCalcul = new CalculImpactApplicationResult( + demandeCalcul.getImpactEquipementVirtuel().getImpactUnitaire() / nbApplications, + demandeCalcul.getImpactEquipementVirtuel().getConsoElecMoyenne() != null ? demandeCalcul.getImpactEquipementVirtuel().getConsoElecMoyenne() / nbApplications : null + ); + + return ImpactApplication.builder() + .dateCalcul(demandeCalcul.getDateCalcul()) + .nomApplication(demandeCalcul.getApplication().getNomApplication()) + .typeEnvironnement(demandeCalcul.getApplication().getTypeEnvironnement()) + .domaine(demandeCalcul.getApplication().getDomaine()) + .sousDomaine(demandeCalcul.getApplication().getSousDomaine()) + .etapeACV(demandeCalcul.getImpactEquipementVirtuel().getEtapeACV()) + .critere(demandeCalcul.getImpactEquipementVirtuel().getCritere()) + .unite(demandeCalcul.getImpactEquipementVirtuel().getUnite()) + .nomEquipementVirtuel(demandeCalcul.getApplication().getNomEquipementVirtuel()) + .nomEquipementPhysique(demandeCalcul.getApplication().getNomEquipementPhysique()) + .nomOrganisation(demandeCalcul.getApplication().getNomOrganisation()) + .nomEntite(demandeCalcul.getApplication().getNomEntite()) + .nomLot(demandeCalcul.getApplication().getNomLot()) + .nomSourceDonnee(demandeCalcul.getApplication().getNomSourceDonnee()) + .dateLot(demandeCalcul.getApplication().getDateLot()) + .versionCalcul(VERSION_CALCUL) + .statutIndicateur("OK") + .trace(TraceUtils.getTraceFromTraceur(objectMapper, TraceCalculImpactApplicationUtils.buildTrace(demandeCalcul))) + .impactUnitaire(resultCalcul.valeurImpact) + .consoElecMoyenne(resultCalcul.consoElecMoyenne) + .build(); + } + + private record CalculImpactApplicationResult(Double valeurImpact, Double consoElecMoyenne) { + } + +} diff --git a/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/port/input/service/impl/CalculImpactEquipementPhysiqueServiceImpl.java b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/port/input/service/impl/CalculImpactEquipementPhysiqueServiceImpl.java new file mode 100644 index 00000000..a35d29cd --- /dev/null +++ b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/port/input/service/impl/CalculImpactEquipementPhysiqueServiceImpl.java @@ -0,0 +1,284 @@ +package org.mte.numecoeval.calculs.domain.port.input.service.impl; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.mte.numecoeval.calculs.domain.data.demande.DemandeCalculImpactEquipementPhysique; +import org.mte.numecoeval.calculs.domain.data.erreur.TypeErreurCalcul; +import org.mte.numecoeval.calculs.domain.data.indicateurs.ImpactEquipementPhysique; +import org.mte.numecoeval.calculs.domain.data.referentiel.ReferentielImpactEquipement; +import org.mte.numecoeval.calculs.domain.data.trace.ConsoElecAnMoyenne; +import org.mte.numecoeval.calculs.domain.data.trace.DureeDeVie; +import org.mte.numecoeval.calculs.domain.data.trace.MixElectrique; +import org.mte.numecoeval.calculs.domain.data.trace.TraceCalculImpactEquipementPhysique; +import org.mte.numecoeval.calculs.domain.exception.CalculImpactException; +import org.mte.numecoeval.calculs.domain.port.input.service.CalculImpactEquipementPhysiqueService; +import org.mte.numecoeval.calculs.domain.port.input.service.DureeDeVieEquipementPhysiqueService; +import org.mte.numecoeval.calculs.domain.traceur.TraceCalculImpactEquipementPhysiqueUtils; +import org.mte.numecoeval.calculs.domain.traceur.TraceUtils; + +import java.util.Optional; + +@Slf4j +@AllArgsConstructor +public class CalculImpactEquipementPhysiqueServiceImpl implements CalculImpactEquipementPhysiqueService { + + private static final String VERSION_CALCUL = "1.0"; + public static final String NB_JOUR_UTILISE_PAR_DEFAUT = "NbJourUtiliseParDefaut"; + public static final String ERREUR_DE_CALCUL_IMPACT_EQUIPEMENT_MESSAGE = "Erreur de calcul impact équipement: Type: {}, Cause: {}, Etape: {}, Critere: {}, Equipement Physique: {}, RefEquipementRetenu: {}, RefEquipementParDefaut: {}"; + private final DureeDeVieEquipementPhysiqueService dureeDeVieEquipementPhysiqueService; + + private final ObjectMapper objectMapper; + + + public ImpactEquipementPhysique calculerImpactEquipementPhysique(DemandeCalculImpactEquipementPhysique demandeCalcul) { + log.debug("Début de calcul d'impact d'équipement physique : {}, {}, {} ", + demandeCalcul.getEtape().getCode(), + demandeCalcul.getCritere().getNomCritere(), + demandeCalcul.getEquipementPhysique().getNomEquipementPhysique()); + ImpactEquipementPhysique impactErreur = null; + + try { + return getCalculImpactEquipementPhysique(demandeCalcul); + } catch (CalculImpactException e) { + log.debug(ERREUR_DE_CALCUL_IMPACT_EQUIPEMENT_MESSAGE, e.getErrorType(), e.getMessage(), + demandeCalcul.getEtape().getCode(), + demandeCalcul.getCritere().getNomCritere(), + demandeCalcul.getEquipementPhysique().getNomEquipementPhysique(), + demandeCalcul.getRefEquipementCible(), + demandeCalcul.getRefEquipementParDefaut()); + impactErreur = buildCalculImpactForError(demandeCalcul, e); + } catch (Exception e) { + log.debug("{} : Erreur de calcul impact équipement physique: Erreur technique de l'indicateur : {}", TypeErreurCalcul.ERREUR_TECHNIQUE.getCode(), e.getMessage()); + impactErreur = buildCalculImpactForError(demandeCalcul, new CalculImpactException(TypeErreurCalcul.ERREUR_TECHNIQUE.getCode(), "Erreur publication de l'indicateur : " + e.getMessage())); + } + + return impactErreur; + } + + private ImpactEquipementPhysique getCalculImpactEquipementPhysique(DemandeCalculImpactEquipementPhysique demandeCalcul) throws CalculImpactException { + double valeurImpactUnitaire; + Double consoElecMoyenne = null; + TraceCalculImpactEquipementPhysique traceCalculImpactEquipementPhysique; + var quantite = demandeCalcul.getEquipementPhysique().getQuantite(); + double tauxUtilisationEqPhysique = getTauxUtilisationEqPhysique(demandeCalcul); + if ("UTILISATION".equals(demandeCalcul.getEtape().getCode())) { + var consoElecAnMoyenne = getConsoElecAnMoyenne(demandeCalcul); + var mixElectrique = getMixElectrique(demandeCalcul); + valeurImpactUnitaire = quantite + * consoElecAnMoyenne.getValeur() + * mixElectrique.getValeur() + * tauxUtilisationEqPhysique + ; + consoElecMoyenne = consoElecAnMoyenne.getValeur(); + traceCalculImpactEquipementPhysique = TraceCalculImpactEquipementPhysiqueUtils.buildTracePremierScenario(quantite, consoElecAnMoyenne, mixElectrique, tauxUtilisationEqPhysique); + } else { + var refImpactEquipementOpt = getImpactEquipement(demandeCalcul); + if (refImpactEquipementOpt.isPresent()) { + ReferentielImpactEquipement referentielImpactEquipement = refImpactEquipementOpt.get(); + var valeurReferentiel = referentielImpactEquipement.getValeur(); + valeurImpactUnitaire = quantite * valeurReferentiel * tauxUtilisationEqPhysique; + DureeDeVie dureeVie = dureeDeVieEquipementPhysiqueService.calculerDureeVie(demandeCalcul); + if (dureeVie != null && dureeVie.getValeur() != null && dureeVie.getValeur() > 0) { + var valeurDureeVie = dureeVie.getValeur(); + valeurImpactUnitaire = valeurImpactUnitaire / valeurDureeVie; + traceCalculImpactEquipementPhysique = TraceCalculImpactEquipementPhysiqueUtils.buildTraceSecondScenario(quantite, valeurReferentiel, referentielImpactEquipement.getSource(), dureeVie, tauxUtilisationEqPhysique); + } else { + throw new CalculImpactException(TypeErreurCalcul.ERREUR_FONCTIONNELLE.getCode(), "Durée de vie de l'équipement inconnue"); + } + } else { + throw new CalculImpactException(TypeErreurCalcul.ERREUR_FONCTIONNELLE.getCode(), "Référentiel Impact Equipement inconnu"); + } + } + + var calcul = buildCalculImpactEquipementPhysique(demandeCalcul, valeurImpactUnitaire, consoElecMoyenne); + calcul.setTrace(TraceUtils.getTraceFromTraceur(objectMapper, traceCalculImpactEquipementPhysique)); + return calcul; + } + + private Optional<ReferentielImpactEquipement> getImpactEquipement(DemandeCalculImpactEquipementPhysique demandeCalcul) { + + Optional<ReferentielImpactEquipement> resultFromRefEquipementCible = Optional.empty(); + if (StringUtils.isNotBlank(demandeCalcul.getRefEquipementCible())) { + resultFromRefEquipementCible = demandeCalcul.getImpactEquipement( + demandeCalcul.getCorrespondanceRefEquipement().getRefEquipementCible() + ); + } + if (resultFromRefEquipementCible.isEmpty()) { + return demandeCalcul.getImpactEquipement( + demandeCalcul.getRefEquipementParDefaut() + ); + } + return resultFromRefEquipementCible; + } + + private ImpactEquipementPhysique buildCalculImpactEquipementPhysique(DemandeCalculImpactEquipementPhysique demandeCalcul, Double valeurImpactUnitaire, Double consoElecMoyenne) { + return ImpactEquipementPhysique + .builder() + .nomEquipement(demandeCalcul.getEquipementPhysique().getNomEquipementPhysique()) + .etapeACV(demandeCalcul.getEtape().getCode()) + .critere(demandeCalcul.getCritere().getNomCritere()) + .dateCalcul(demandeCalcul.getDateCalcul()) + .versionCalcul(VERSION_CALCUL) + .reference(StringUtils.defaultIfEmpty(demandeCalcul.getRefEquipementCible(), demandeCalcul.getRefEquipementParDefaut())) + .typeEquipement(demandeCalcul.getEquipementPhysique().getType()) + .quantite(demandeCalcul.getEquipementPhysique().getQuantite()) + .statutEquipementPhysique(demandeCalcul.getEquipementPhysique().getStatut()) + .statutIndicateur("OK") + .impactUnitaire(valeurImpactUnitaire) + .unite(demandeCalcul.getCritere().getUnite()) + .consoElecMoyenne(consoElecMoyenne) + .nomLot(demandeCalcul.getEquipementPhysique().getNomLot()) + .nomSourceDonnee(demandeCalcul.getEquipementPhysique().getNomSourceDonnee()) + .dateLot(demandeCalcul.getEquipementPhysique().getDateLot()) + .nomEntite(demandeCalcul.getEquipementPhysique().getNomEntite()) + .nomOrganisation(demandeCalcul.getEquipementPhysique().getNomOrganisation()) + .build(); + } + + private ImpactEquipementPhysique buildCalculImpactForError(DemandeCalculImpactEquipementPhysique demandeCalcul, CalculImpactException exception) { + return ImpactEquipementPhysique + .builder() + .nomEquipement(demandeCalcul.getEquipementPhysique().getNomEquipementPhysique()) + .etapeACV(demandeCalcul.getEtape().getCode()) + .critere(demandeCalcul.getCritere().getNomCritere()) + .dateCalcul(demandeCalcul.getDateCalcul()) + .versionCalcul(VERSION_CALCUL) + .reference(StringUtils.defaultIfEmpty(demandeCalcul.getRefEquipementCible(), demandeCalcul.getRefEquipementParDefaut())) + .typeEquipement(demandeCalcul.getEquipementPhysique().getType()) + .quantite(demandeCalcul.getEquipementPhysique().getQuantite()) + .statutEquipementPhysique(demandeCalcul.getEquipementPhysique().getStatut()) + .statutIndicateur("ERREUR") + .impactUnitaire(null) + .unite(demandeCalcul.getCritere().getUnite()) + .consoElecMoyenne(null) + .nomLot(demandeCalcul.getEquipementPhysique().getNomLot()) + .nomSourceDonnee(demandeCalcul.getEquipementPhysique().getNomSourceDonnee()) + .dateLot(demandeCalcul.getEquipementPhysique().getDateLot()) + .nomEntite(demandeCalcul.getEquipementPhysique().getNomEntite()) + .nomOrganisation(demandeCalcul.getEquipementPhysique().getNomOrganisation()) + .trace(TraceUtils.getTraceFromTraceur(objectMapper, TraceCalculImpactEquipementPhysiqueUtils.buildTraceErreur(exception))) + .build(); + } + + private ConsoElecAnMoyenne getConsoElecAnMoyenne(DemandeCalculImpactEquipementPhysique demandeCalcul) throws CalculImpactException { + if (demandeCalcul.getEquipementPhysique().getConsoElecAnnuelle() != null) { + var valeur = demandeCalcul.getEquipementPhysique().getConsoElecAnnuelle(); + return ConsoElecAnMoyenne.builder() + .valeurEquipementConsoElecAnnuelle(valeur) + .valeur(valeur) + .build(); + } + + + if (StringUtils.isAllBlank(demandeCalcul.getRefEquipementCible(), demandeCalcul.getRefEquipementParDefaut())) { + throw new CalculImpactException(TypeErreurCalcul.ERREUR_FONCTIONNELLE.getCode(), "Référentiel Impact Equipement inconnu"); + } + + var referentielImpactEquipementOpt = getImpactEquipement(demandeCalcul); + if (referentielImpactEquipementOpt.isEmpty()) { + throw new CalculImpactException(TypeErreurCalcul.ERREUR_FONCTIONNELLE.getCode(), "Référentiel Impact Equipement inconnu"); + } + + if (referentielImpactEquipementOpt.get().getConsoElecMoyenne() == null) { + throw new CalculImpactException(TypeErreurCalcul.ERREUR_FONCTIONNELLE.getCode(), + "Donnée de consommation electrique manquante : equipementPhysique : " + demandeCalcul.getEquipementPhysique().getNomEquipementPhysique() + + ", RefEquipementCible : " + demandeCalcul.getRefEquipementCible() + + ", RefEquipementParDefaut : " + demandeCalcul.getRefEquipementParDefaut() + ); + } + + var valeur = referentielImpactEquipementOpt.get().getConsoElecMoyenne(); + return ConsoElecAnMoyenne.builder() + .sourceReferentielImpactEquipement(referentielImpactEquipementOpt.get().getSource()) + .valeurReferentielConsoElecMoyenne(valeur) + .valeur(valeur) + .build(); + + } + + private MixElectrique getMixElectrique(DemandeCalculImpactEquipementPhysique demandeCalcul) throws CalculImpactException { + // Taiga #918 - EquipementPhysique.dataCenter != null est équivalent à EquipementPhysique.nomCourtDatacenter est renseigné + le DataCenter existe dans les données + if (demandeCalcul.getEquipementPhysique().getDataCenter() != null + && StringUtils.isNotBlank(demandeCalcul.getEquipementPhysique().getDataCenter().getLocalisation())) { + var pays = demandeCalcul.getEquipementPhysique().getDataCenter().getLocalisation(); + var refMixElecOpt = demandeCalcul.getMixElectrique(pays); + if (refMixElecOpt.isPresent()) { + var refMixElec = refMixElecOpt.get().getValeur(); + var pue = getDataCenterPUE(demandeCalcul); + return MixElectrique.builder() + .isServeur(true) + .dataCenterPue(pue) + .valeurReferentielMixElectrique(refMixElec) + .sourceReferentielMixElectrique(refMixElecOpt.get().getSource()) + .valeur(refMixElec * pue) + .build(); + } + } + if (StringUtils.isNotBlank(demandeCalcul.getEquipementPhysique().getPaysDUtilisation())) { + var refMixElecOpt = demandeCalcul.getMixElectrique(demandeCalcul.getEquipementPhysique().getPaysDUtilisation()); + if (refMixElecOpt.isPresent()) { + var refMixElec = refMixElecOpt.get(); + return MixElectrique.builder() + .valeurReferentielMixElectrique(refMixElec.getValeur()) + .sourceReferentielMixElectrique(refMixElec.getSource()) + .valeur(refMixElec.getValeur()) + .build(); + } + } + throw new CalculImpactException(TypeErreurCalcul.ERREUR_FONCTIONNELLE.getCode(), + "Il n'existe pas de Mix électrique pour cet équipement : critere: " + demandeCalcul.getCritere().getNomCritere() + + " - equipement: " + demandeCalcul.getEquipementPhysique().getNomEquipementPhysique() + + " - Pays d'utilisation: " + demandeCalcul.getEquipementPhysique().getPaysDUtilisation() + + " - NomCourtDatacenter: " + demandeCalcul.getEquipementPhysique().getNomCourtDatacenter() + + ); + + } + + private Double getDataCenterPUE(DemandeCalculImpactEquipementPhysique demandeCalcul) throws CalculImpactException { + if (demandeCalcul.getEquipementPhysique().getDataCenter().getPue() != null) { + return demandeCalcul.getEquipementPhysique().getDataCenter().getPue(); + } + var optionalPUEParDefaut = demandeCalcul.getHypotheseFromCode("PUEParDefaut"); + if (optionalPUEParDefaut.isPresent()) { + return optionalPUEParDefaut.get().getValeur(); + } + throw new CalculImpactException(TypeErreurCalcul.ERREUR_FONCTIONNELLE.getCode(), + "Le PUE est manquant et ne permet le calcul de l'impact à l'usage de l'équipement :" + + " equipement: " + demandeCalcul.getEquipementPhysique().getNomEquipementPhysique() + + " - RefEquipementCible: " + demandeCalcul.getRefEquipementCible() + + " - refEquipementParDefaut: " + demandeCalcul.getRefEquipementParDefaut() + + " - NomCourtDatacenter: " + demandeCalcul.getEquipementPhysique().getNomCourtDatacenter() + + ); + } + + + /** + * compute the tauxUtilisationEquipementPhysique + * if the tauxUtilisation has been completed, we directly use this value + * else we retrieve the value of modeUtilisation and get the corresponding default value in ref_hypotheses + * + * @param demandeCalcul la demande de calcul + * @return le taux d'utilsiation pour l'equipement physique + */ + private Double getTauxUtilisationEqPhysique(DemandeCalculImpactEquipementPhysique demandeCalcul) { + var tauxUtilisation = demandeCalcul.getEquipementPhysique().getTauxUtilisation(); + + if (tauxUtilisation != null && tauxUtilisation > 0.0 && tauxUtilisation <= 1.0) { + return tauxUtilisation; + } + + if (demandeCalcul.getEquipementPhysique().getModeUtilisation() == null) { + return 1.0; + } + + var tauxUtilisationEqPhysique = demandeCalcul.getHypotheseFromCode("taux_utilisation_" + demandeCalcul.getEquipementPhysique().getModeUtilisation()); + if (tauxUtilisationEqPhysique.isPresent()) { + return tauxUtilisationEqPhysique.get().getValeur(); + } + return 1.0; + } +} diff --git a/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/port/input/service/impl/CalculImpactEquipementVirtuelServiceImpl.java b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/port/input/service/impl/CalculImpactEquipementVirtuelServiceImpl.java new file mode 100644 index 00000000..ef137c98 --- /dev/null +++ b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/port/input/service/impl/CalculImpactEquipementVirtuelServiceImpl.java @@ -0,0 +1,163 @@ +package org.mte.numecoeval.calculs.domain.port.input.service.impl; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.mte.numecoeval.calculs.domain.data.demande.DemandeCalculImpactEquipementVirtuel; +import org.mte.numecoeval.calculs.domain.data.erreur.TypeErreurCalcul; +import org.mte.numecoeval.calculs.domain.data.indicateurs.ImpactEquipementVirtuel; +import org.mte.numecoeval.calculs.domain.data.trace.TraceCalculImpactEquipementVirtuel; +import org.mte.numecoeval.calculs.domain.exception.CalculImpactException; +import org.mte.numecoeval.calculs.domain.port.input.service.CalculImpactEquipementVirtuelService; +import org.mte.numecoeval.calculs.domain.traceur.TraceCalculImpactVirtuelUtils; +import org.mte.numecoeval.calculs.domain.traceur.TraceUtils; + +@Slf4j +@AllArgsConstructor +public class CalculImpactEquipementVirtuelServiceImpl implements CalculImpactEquipementVirtuelService { + + private static final String VERSION_CALCUL = "1.1"; + public static final String TYPE_EQUIPEMENT_VIRTUEL_STOCKAGE = "stockage"; + public static final String TYPE_EQUIPEMENT_VIRTUEL_CALCUL = "calcul"; + private final ObjectMapper objectMapper; + + @Override + public ImpactEquipementVirtuel calculerImpactEquipementVirtuel(DemandeCalculImpactEquipementVirtuel demandeCalcul) { + ImpactEquipementVirtuel impactErreur; + try { + return calculerImpact(demandeCalcul); + } catch (CalculImpactException e) { + log.debug("Erreur de calcul d'impact d'équipement virtuel : Type : {}, Cause: {}, Etape: {}, Critere: {}, Nom Equipment Physique: {}, Nom Equipement Virtuel: {}", + e.getErrorType(), e.getMessage(), + demandeCalcul.getImpactEquipement().getEtapeACV(), + demandeCalcul.getImpactEquipement().getCritere(), + demandeCalcul.getEquipementVirtuel().getNomEquipementPhysique(), + demandeCalcul.getEquipementVirtuel().getNomEquipementVirtuel()); + + impactErreur = buildImpactForError(demandeCalcul, e); + } catch (Exception e) { + log.debug("Erreur de calcul d'impact d'équipement virtuel : Type : {}, Cause: {}, Etape: {}, Critere: {}, Nom Equipment Physique: {}, Nom Equipement Virtuel: {}", + TypeErreurCalcul.ERREUR_TECHNIQUE.getCode(), e.getMessage(), + demandeCalcul.getImpactEquipement().getEtapeACV(), + demandeCalcul.getImpactEquipement().getCritere(), + demandeCalcul.getEquipementVirtuel().getNomEquipementPhysique(), + demandeCalcul.getEquipementVirtuel().getNomEquipementVirtuel()); + impactErreur = buildImpactForError(demandeCalcul, new CalculImpactException(TypeErreurCalcul.ERREUR_TECHNIQUE.getCode(), e.getMessage())); + } + + return impactErreur; + } + + private ImpactEquipementVirtuel calculerImpact(DemandeCalculImpactEquipementVirtuel demandeCalcul) throws CalculImpactException { + if (!"OK".equals(demandeCalcul.getImpactEquipement().getStatutIndicateur())) { + throw new CalculImpactException( + TypeErreurCalcul.ERREUR_FONCTIONNELLE.getCode(), + "L'indicateur d'impact équipement associé est au statut : " + demandeCalcul.getImpactEquipement().getStatutIndicateur() + ); + } + if (demandeCalcul.getImpactEquipement().getImpactUnitaire() == null) { + throw new CalculImpactException( + TypeErreurCalcul.ERREUR_FONCTIONNELLE.getCode(), + "L'impact unitaire de l'équipement physique parent est null" + ); + } + + Double valeurImpactEquipementPhysique = demandeCalcul.getImpactEquipement().getImpactUnitaire(); + Double consoElecMoyenneEquipementPhysique = demandeCalcul.getImpactEquipement().getConsoElecMoyenne(); + + TraceCalculImpactEquipementVirtuel trace = TraceCalculImpactVirtuelUtils.buildTrace(demandeCalcul); + + CalculImpactUnitaireEquipementVirtuel result = calculImpactUnitaire(demandeCalcul, valeurImpactEquipementPhysique, consoElecMoyenneEquipementPhysique); + + return ImpactEquipementVirtuel.builder() + .dateCalcul(demandeCalcul.getDateCalcul()) + .versionCalcul(VERSION_CALCUL) + .etapeACV(demandeCalcul.getImpactEquipement().getEtapeACV()) + .critere(demandeCalcul.getImpactEquipement().getCritere()) + .unite(demandeCalcul.getImpactEquipement().getUnite()) + .statutIndicateur("OK") + .trace(TraceUtils.getTraceFromTraceur(objectMapper, trace)) + .nomLot(demandeCalcul.getEquipementVirtuel().getNomLot()) + .nomSourceDonnee(demandeCalcul.getEquipementVirtuel().getNomSourceDonnee()) + .dateLot(demandeCalcul.getEquipementVirtuel().getDateLot()) + .nomOrganisation(demandeCalcul.getEquipementVirtuel().getNomOrganisation()) + .nomEntite(demandeCalcul.getEquipementVirtuel().getNomEntite()) + .nomEquipement(demandeCalcul.getEquipementVirtuel().getNomEquipementPhysique()) + .nomEquipementVirtuel(demandeCalcul.getEquipementVirtuel().getNomEquipementVirtuel()) + .cluster(demandeCalcul.getEquipementVirtuel().getCluster()) + .impactUnitaire(result.valeurImpactUnitaire()) + .consoElecMoyenne(result.consoElecMoyenne()) + .idEntree(demandeCalcul.getEquipementVirtuel().getId()) + .build(); + } + + private ImpactEquipementVirtuel buildImpactForError(DemandeCalculImpactEquipementVirtuel demandeCalcul, CalculImpactException exception) { + return ImpactEquipementVirtuel.builder() + .dateCalcul(demandeCalcul.getDateCalcul()) + .versionCalcul(VERSION_CALCUL) + .etapeACV(demandeCalcul.getImpactEquipement().getEtapeACV()) + .critere(demandeCalcul.getImpactEquipement().getCritere()) + .unite(demandeCalcul.getImpactEquipement().getUnite()) + .statutIndicateur("ERREUR") + .trace(TraceUtils.getTraceFromTraceur(objectMapper, TraceCalculImpactVirtuelUtils.buildTraceErreur(exception))) + .nomLot(demandeCalcul.getEquipementVirtuel().getNomLot()) + .nomSourceDonnee(demandeCalcul.getEquipementVirtuel().getNomSourceDonnee()) + .dateLot(demandeCalcul.getEquipementVirtuel().getDateLot()) + .nomOrganisation(demandeCalcul.getEquipementVirtuel().getNomOrganisation()) + .nomEntite(demandeCalcul.getEquipementVirtuel().getNomEntite()) + .nomEquipement(demandeCalcul.getEquipementVirtuel().getNomEquipementPhysique()) + .nomEquipementVirtuel(demandeCalcul.getEquipementVirtuel().getNomEquipementVirtuel()) + .cluster(demandeCalcul.getEquipementVirtuel().getCluster()) + .impactUnitaire(null) + .consoElecMoyenne(null) + .idEntree(demandeCalcul.getEquipementVirtuel().getId()) + .build(); + } + + private static CalculImpactUnitaireEquipementVirtuel calculImpactUnitaire(DemandeCalculImpactEquipementVirtuel demandeCalcul, Double valeurImpactEquipementPhysique, Double consoElecMoyenneEquipementPhysique) throws CalculImpactException { + double valeurImpactUnitaire; + Double consoElecMoyenne; + if (cleRepartitionEstDisponible(demandeCalcul)) { + valeurImpactUnitaire = valeurImpactEquipementPhysique * demandeCalcul.getEquipementVirtuel().getCleRepartition(); + consoElecMoyenne = consoElecMoyenneEquipementPhysique != null ? consoElecMoyenneEquipementPhysique * demandeCalcul.getEquipementVirtuel().getCleRepartition() : null; + } else if (calculPourCalculEstDisponible(demandeCalcul)) { + valeurImpactUnitaire = valeurImpactEquipementPhysique * demandeCalcul.getEquipementVirtuel().getVCPU() / demandeCalcul.getNbTotalVCPU(); + consoElecMoyenne = consoElecMoyenneEquipementPhysique != null ? consoElecMoyenneEquipementPhysique * demandeCalcul.getEquipementVirtuel().getVCPU() / demandeCalcul.getNbTotalVCPU() : null; + } else if (calculPourStockageEstDisponible(demandeCalcul)) { + valeurImpactUnitaire = valeurImpactEquipementPhysique * demandeCalcul.getEquipementVirtuel().getCapaciteStockage() / demandeCalcul.getStockageTotalVirtuel(); + consoElecMoyenne = consoElecMoyenneEquipementPhysique != null ? consoElecMoyenneEquipementPhysique * demandeCalcul.getEquipementVirtuel().getCapaciteStockage() / demandeCalcul.getStockageTotalVirtuel() : null; + } else if (calculParRepartiParNombreEquipementEstDisponible(demandeCalcul)) { + valeurImpactUnitaire = valeurImpactEquipementPhysique / demandeCalcul.getNbEquipementsVirtuels(); + consoElecMoyenne = consoElecMoyenneEquipementPhysique != null ? consoElecMoyenneEquipementPhysique / demandeCalcul.getNbEquipementsVirtuels() : null; + } else { + throw new CalculImpactException(TypeErreurCalcul.ERREUR_FONCTIONNELLE.getCode(), "Certaines données sur l'équipement virtuel sont manquantes ou incorrectes"); + } + return new CalculImpactUnitaireEquipementVirtuel(valeurImpactUnitaire, consoElecMoyenne); + } + + public static boolean calculParRepartiParNombreEquipementEstDisponible(DemandeCalculImpactEquipementVirtuel demandeCalcul) { + return demandeCalcul.getNbEquipementsVirtuels() != null && demandeCalcul.getNbEquipementsVirtuels() >= 1; + } + + public static boolean calculPourStockageEstDisponible(DemandeCalculImpactEquipementVirtuel demandeCalcul) { + return TYPE_EQUIPEMENT_VIRTUEL_STOCKAGE.equals(demandeCalcul.getEquipementVirtuel().getTypeEqv()) && demandeCalcul.getStockageTotalVirtuel() != null; + } + + public static boolean calculPourCalculEstDisponible(DemandeCalculImpactEquipementVirtuel demandeCalcul) { + return TYPE_EQUIPEMENT_VIRTUEL_CALCUL.equals(demandeCalcul.getEquipementVirtuel().getTypeEqv()) && demandeCalcul.getNbTotalVCPU() != null; + } + + public static boolean cleRepartitionEstDisponible(DemandeCalculImpactEquipementVirtuel demandeCalcul) { + return demandeCalcul.getEquipementVirtuel().getCleRepartition() != null; + } + + /** + * Record pour le stockage du résultat du calcul d'impact + * + * @param valeurImpactUnitaire impact unitaire pour l'équipement virtuel + * @param consoElecMoyenne conso électrique moyenne pour l'équipement virtuel + */ + private record CalculImpactUnitaireEquipementVirtuel(double valeurImpactUnitaire, Double consoElecMoyenne) { + } + +} diff --git a/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/port/input/service/impl/CalculImpactMessagerieServiceImpl.java b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/port/input/service/impl/CalculImpactMessagerieServiceImpl.java new file mode 100644 index 00000000..35fa91da --- /dev/null +++ b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/port/input/service/impl/CalculImpactMessagerieServiceImpl.java @@ -0,0 +1,106 @@ +package org.mte.numecoeval.calculs.domain.port.input.service.impl; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.mte.numecoeval.calculs.domain.data.demande.DemandeCalculImpactMessagerie; +import org.mte.numecoeval.calculs.domain.data.erreur.TypeErreurCalcul; +import org.mte.numecoeval.calculs.domain.data.indicateurs.ImpactMessagerie; +import org.mte.numecoeval.calculs.domain.data.referentiel.ReferentielImpactMessagerie; +import org.mte.numecoeval.calculs.domain.exception.CalculImpactException; +import org.mte.numecoeval.calculs.domain.port.input.service.CalculImpactMessagerieService; +import org.mte.numecoeval.calculs.domain.traceur.TraceCalculImpactMessagerieUtils; +import org.mte.numecoeval.calculs.domain.traceur.TraceUtils; + +@Slf4j +@AllArgsConstructor +public class CalculImpactMessagerieServiceImpl implements CalculImpactMessagerieService { + private static final String VERSION_CALCUL = "1.0"; + private final ObjectMapper objectMapper; + + @Override + public ImpactMessagerie calculerImpactMessagerie(DemandeCalculImpactMessagerie demandeCalcul) { + ImpactMessagerie impactErreur; + try { + return calculerImpact(demandeCalcul); + } catch (CalculImpactException e) { + log.debug("Erreur de calcul d'impact de messagerie : Type : {}, Cause: {}, Critere: {}, Mois-Années: {}", + e.getErrorType(), e.getMessage(), + demandeCalcul.getCritere().getNomCritere(), + demandeCalcul.getMessagerie().getMoisAnnee() + ); + + impactErreur = buildImpactForError(demandeCalcul, e); + } catch (Exception e) { + log.debug("Erreur générale de calcul d'impact de messagerie : Type : {}, Cause: {}, Critere: {}, Mois-Années: {}", + TypeErreurCalcul.ERREUR_TECHNIQUE.getCode(), e.getMessage(), + demandeCalcul.getCritere().getNomCritere(), + demandeCalcul.getMessagerie().getMoisAnnee() + ); + impactErreur = buildImpactForError(demandeCalcul, new CalculImpactException(TypeErreurCalcul.ERREUR_TECHNIQUE.getCode(), e.getMessage())); + } + + return impactErreur; + } + + private ImpactMessagerie calculerImpact(DemandeCalculImpactMessagerie demandeCalcul) throws CalculImpactException { + Double nombreMailEmis = demandeCalcul.getMessagerie().getNombreMailEmis(); + var referentielImpactMessagerie = demandeCalcul.getImpactsMessagerie() + .stream() + .filter(refImpactMessagerie -> demandeCalcul.getCritere().getNomCritere().equals(refImpactMessagerie.getCritere())) + .findFirst() + .orElseThrow(() -> new CalculImpactException( + TypeErreurCalcul.ERREUR_FONCTIONNELLE.getCode(), "Référentiel ImpactMessagerie indisponible pour le critère " + demandeCalcul.getCritere().getNomCritere()) + ); + if (nombreMailEmis > 0) { + var poidsMoyenMail = demandeCalcul.getMessagerie().getVolumeTotalMailEmis() / nombreMailEmis; + Double impactMensuel = (referentielImpactMessagerie.getConstanteCoefficientDirecteur() * poidsMoyenMail + referentielImpactMessagerie.getConstanteOrdonneeOrigine()) * demandeCalcul.getMessagerie().getNombreMailEmisXDestinataires(); + return buildImpactMessagerie(demandeCalcul, referentielImpactMessagerie, impactMensuel); + } + throw new CalculImpactException( + TypeErreurCalcul.ERREUR_FONCTIONNELLE.getCode(), + "Calcul d'impact messagerie : Critère : " + demandeCalcul.getCritere().getNomCritere() + ", Mois Années : " + demandeCalcul.getMessagerie().getMoisAnnee() + ", nombreMailEmis " + demandeCalcul.getMessagerie().getNombreMailEmis() + "=< 0.0" + ); + } + + private ImpactMessagerie buildImpactMessagerie(DemandeCalculImpactMessagerie demandeCalcul, ReferentielImpactMessagerie referentielImpactMessagerie, Double impact) { + return ImpactMessagerie.builder() + .dateCalcul(demandeCalcul.getDateCalcul()) + .critere(demandeCalcul.getCritere().getNomCritere()) + .unite(demandeCalcul.getCritere().getUnite()) + .moisAnnee(demandeCalcul.getMessagerie().getMoisAnnee()) + .nombreMailEmis(demandeCalcul.getMessagerie().getNombreMailEmis()) + .volumeTotalMailEmis(demandeCalcul.getMessagerie().getVolumeTotalMailEmis()) + .impactMensuel(impact) + .statutIndicateur("OK") + .nomLot(demandeCalcul.getMessagerie().getNomLot()) + .nomSourceDonnee(demandeCalcul.getMessagerie().getNomSourceDonnee()) + .dateLot(demandeCalcul.getMessagerie().getDateLot()) + .nomEntite(demandeCalcul.getMessagerie().getNomEntite()) + .nomOrganisation(demandeCalcul.getMessagerie().getNomOrganisation()) + .versionCalcul(VERSION_CALCUL) + .trace(TraceUtils.getTraceFromTraceur(objectMapper, TraceCalculImpactMessagerieUtils.buildTrace(demandeCalcul, referentielImpactMessagerie))) + .build(); + } + + private ImpactMessagerie buildImpactForError(DemandeCalculImpactMessagerie demandeCalcul, CalculImpactException exception) { + return ImpactMessagerie.builder() + .dateCalcul(demandeCalcul.getDateCalcul()) + .critere(demandeCalcul.getCritere().getNomCritere()) + .unite(demandeCalcul.getCritere().getUnite()) + .moisAnnee(demandeCalcul.getMessagerie().getMoisAnnee()) + .impactMensuel(null) + .nombreMailEmis(demandeCalcul.getMessagerie().getNombreMailEmis()) + .volumeTotalMailEmis(demandeCalcul.getMessagerie().getVolumeTotalMailEmis()) + .statutIndicateur("ERREUR") + .nomLot(demandeCalcul.getMessagerie().getNomLot()) + .nomSourceDonnee(demandeCalcul.getMessagerie().getNomSourceDonnee()) + .dateLot(demandeCalcul.getMessagerie().getDateLot()) + .nomEntite(demandeCalcul.getMessagerie().getNomEntite()) + .nomOrganisation(demandeCalcul.getMessagerie().getNomOrganisation()) + .versionCalcul(VERSION_CALCUL) + .trace(TraceUtils.getTraceFromTraceur(objectMapper, TraceCalculImpactMessagerieUtils.buildTraceError(exception))) + .build(); + } + +} diff --git a/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/port/input/service/impl/CalculImpactReseauServiceImpl.java b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/port/input/service/impl/CalculImpactReseauServiceImpl.java new file mode 100644 index 00000000..aa948581 --- /dev/null +++ b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/port/input/service/impl/CalculImpactReseauServiceImpl.java @@ -0,0 +1,139 @@ +package org.mte.numecoeval.calculs.domain.port.input.service.impl; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.mte.numecoeval.calculs.domain.data.demande.DemandeCalculImpactReseau; +import org.mte.numecoeval.calculs.domain.data.erreur.TypeErreurCalcul; +import org.mte.numecoeval.calculs.domain.data.indicateurs.ImpactReseau; +import org.mte.numecoeval.calculs.domain.data.referentiel.ReferentielImpactReseau; +import org.mte.numecoeval.calculs.domain.exception.CalculImpactException; +import org.mte.numecoeval.calculs.domain.port.input.service.CalculImpactReseauService; +import org.mte.numecoeval.calculs.domain.traceur.TraceCalculImpactReseauUtils; +import org.mte.numecoeval.calculs.domain.traceur.TraceUtils; + +import java.util.Objects; + +@Slf4j +@AllArgsConstructor +public class CalculImpactReseauServiceImpl implements CalculImpactReseauService { + + private static final String VERSION_CALCUL = "1.0"; + + private final ObjectMapper objectMapper; + + @Override + public ImpactReseau calculerImpactReseau(DemandeCalculImpactReseau demandeCalcul) { + try { + if (demandeCalcul.getEquipementPhysique().getGoTelecharge() == null) { + throw new CalculImpactException( + TypeErreurCalcul.ERREUR_FONCTIONNELLE.getCode(), + "Erreur de calcul impact réseau: Etape: %s, Critere: %s, Equipement Physique: %s : la valeur en_EqP(Equipement).goTelecharge est nulle" + .formatted( + demandeCalcul.getEtape().getCode(), + demandeCalcul.getCritere().getNomCritere(), + demandeCalcul.getEquipementPhysique().getNomEquipementPhysique() + ) + ); + } + + var referentielImpactReseau = getReferentielImpactReseau(demandeCalcul, REF_RESEAU); + + return calculerImpact(demandeCalcul, referentielImpactReseau); + } catch (CalculImpactException exception) { + log.debug("{} : {}", exception.getErrorType(), exception.getMessage()); + return buildCalculForError(demandeCalcul, exception); + } catch (Exception exception) { + log.debug("{} : {}", TypeErreurCalcul.ERREUR_TECHNIQUE, exception.getMessage()); + return buildCalculForError(demandeCalcul, new CalculImpactException(TypeErreurCalcul.ERREUR_TECHNIQUE.getCode(), exception.getMessage())); + } + } + + private ReferentielImpactReseau getReferentielImpactReseau(DemandeCalculImpactReseau demandeCalcul, String refReseau) throws CalculImpactException { + var referentielImpactReseau = demandeCalcul.getImpactsReseau().stream() + .filter(Objects::nonNull) + .filter(refImpactReseau -> + demandeCalcul.getEtape().getCode().equals(refImpactReseau.getEtapeACV()) + && demandeCalcul.getCritere().getNomCritere().equals(refImpactReseau.getCritere()) + && StringUtils.equals(refImpactReseau.getRefReseau(), refReseau) + ) + .findFirst() + .orElseThrow(() -> new CalculImpactException( + TypeErreurCalcul.ERREUR_FONCTIONNELLE.getCode(), + "Erreur de calcul impact réseau: Etape: %s, Critere: %s, RefReseau: %s, Equipement Physique: %s : La référence ImpactReseau n'existe pas." + .formatted( + demandeCalcul.getEtape().getCode(), + demandeCalcul.getCritere().getNomCritere(), + REF_RESEAU, + demandeCalcul.getEquipementPhysique().getNomEquipementPhysique() + ) + ) + ); + + if (Objects.isNull(referentielImpactReseau.getImpactReseauMobileMoyen())) { + throw new CalculImpactException( + TypeErreurCalcul.ERREUR_FONCTIONNELLE.getCode(), + "Erreur de calcul impact réseau: Etape: %s, Critere: %s, RefReseau: %s, Equipement Physique: %s : L'impactReseauMobileMoyen de la référence est null." + .formatted( + demandeCalcul.getEtape().getCode(), + demandeCalcul.getCritere().getNomCritere(), + REF_RESEAU, + demandeCalcul.getEquipementPhysique().getNomEquipementPhysique() + ) + ); + } + + return referentielImpactReseau; + } + + private ImpactReseau calculerImpact(DemandeCalculImpactReseau demandeCalcul, ReferentielImpactReseau referentielImpactReseau) { + return ImpactReseau.builder() + .versionCalcul(VERSION_CALCUL) + .dateCalcul(demandeCalcul.getDateCalcul()) + .nomLot(demandeCalcul.getEquipementPhysique().getNomLot()) + .nomSourceDonnee(demandeCalcul.getEquipementPhysique().getNomSourceDonnee()) + .dateLot(demandeCalcul.getEquipementPhysique().getDateLot()) + .etapeACV(demandeCalcul.getEtape().getCode()) + .critere(demandeCalcul.getCritere().getNomCritere()) + .statutIndicateur("OK") + .source(referentielImpactReseau.getSource()) + .nomEntite(demandeCalcul.getEquipementPhysique().getNomEntite()) + .nomOrganisation(demandeCalcul.getEquipementPhysique().getNomOrganisation()) + .nomEquipement(demandeCalcul.getEquipementPhysique().getNomEquipementPhysique()) + .impactUnitaire( + demandeCalcul.getEquipementPhysique().getGoTelecharge() * referentielImpactReseau.getImpactReseauMobileMoyen() + ) + .unite(referentielImpactReseau.getUnite()) + .trace( + TraceUtils.getTraceFromTraceur( + objectMapper, + TraceCalculImpactReseauUtils.buildTrace(demandeCalcul, referentielImpactReseau)) + ) + .build(); + } + + private ImpactReseau buildCalculForError(DemandeCalculImpactReseau demandeCalcul, CalculImpactException exception) { + return ImpactReseau.builder() + .versionCalcul(VERSION_CALCUL) + .dateCalcul(demandeCalcul.getDateCalcul()) + .nomLot(demandeCalcul.getEquipementPhysique().getNomLot()) + .nomSourceDonnee(demandeCalcul.getEquipementPhysique().getNomSourceDonnee()) + .dateLot(demandeCalcul.getEquipementPhysique().getDateLot()) + .etapeACV(demandeCalcul.getEtape().getCode()) + .critere(demandeCalcul.getCritere().getNomCritere()) + .statutIndicateur("ERREUR") + .nomEntite(demandeCalcul.getEquipementPhysique().getNomEntite()) + .nomOrganisation(demandeCalcul.getEquipementPhysique().getNomOrganisation()) + .nomEquipement(demandeCalcul.getEquipementPhysique().getNomEquipementPhysique()) + .unite(demandeCalcul.getCritere().getUnite()) + .impactUnitaire(null) + .unite(demandeCalcul.getCritere().getUnite()) + .trace(TraceUtils.getTraceFromTraceur( + objectMapper, + TraceCalculImpactReseauUtils.buildTraceErreur(exception)) + ) + .build(); + } + +} \ No newline at end of file diff --git a/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/port/input/service/impl/DureeDeVieEquipementPhysiqueServiceImpl.java b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/port/input/service/impl/DureeDeVieEquipementPhysiqueServiceImpl.java new file mode 100644 index 00000000..717db5f0 --- /dev/null +++ b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/port/input/service/impl/DureeDeVieEquipementPhysiqueServiceImpl.java @@ -0,0 +1,77 @@ +package org.mte.numecoeval.calculs.domain.port.input.service.impl; + +import lombok.AllArgsConstructor; +import org.mte.numecoeval.calculs.domain.data.demande.DemandeCalculImpactEquipementPhysique; +import org.mte.numecoeval.calculs.domain.data.erreur.TypeErreurCalcul; +import org.mte.numecoeval.calculs.domain.data.referentiel.ReferentielHypothese; +import org.mte.numecoeval.calculs.domain.data.trace.DureeDeVie; +import org.mte.numecoeval.calculs.domain.data.trace.DureeDeVieParDefaut; +import org.mte.numecoeval.calculs.domain.exception.CalculImpactException; +import org.mte.numecoeval.calculs.domain.port.input.service.DureeDeVieEquipementPhysiqueService; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; + +@AllArgsConstructor +public class DureeDeVieEquipementPhysiqueServiceImpl implements DureeDeVieEquipementPhysiqueService { + + @Override + public DureeDeVie calculerDureeVie(DemandeCalculImpactEquipementPhysique demandeCalcul) throws CalculImpactException { + var dateAchat = demandeCalcul.getEquipementPhysique().getDateAchat(); + var dateRetrait = demandeCalcul.getEquipementPhysique().getDateRetrait(); + var result = DureeDeVie.builder().build(); + if (dateAchat != null && dateRetrait != null) { + + if (dateAchat.isBefore(dateRetrait)) { + Double valeur ; + if (ChronoUnit.MONTHS.between(dateAchat, dateRetrait) < 12) { + valeur= 1d; + } else { + valeur = ChronoUnit.DAYS.between(dateAchat, dateRetrait) / 365d; + } + result.setValeur(valeur); + result.setDateAchat(dateAchat.format(DateTimeFormatter.ISO_DATE)); + result.setDateRetrait(dateRetrait.format(DateTimeFormatter.ISO_DATE)); + } else { + throw new CalculImpactException(TypeErreurCalcul.ERREUR_FONCTIONNELLE.getCode(),"La durée de vie de l'équipement n'a pas pu être déterminée"); + } + } + // Taiga#864 - Si la date d'achat est présente et non la date de retrait, on prend la date du jour devient la date de retrait pour le calcul + else if (dateAchat != null) { + result.setValeur( + ChronoUnit.DAYS.between(dateAchat, LocalDate.now()) / 365d + ); + result.setDateAchat(dateAchat.format(DateTimeFormatter.ISO_DATE)); + result.setDateRetrait(LocalDate.now().format(DateTimeFormatter.ISO_DATE)); + } + else { + var dureeDeVieParDefaut = calculerDureeVieDefaut(demandeCalcul); + result.setDureeDeVieParDefaut(dureeDeVieParDefaut); + result.setValeur(dureeDeVieParDefaut.getValeur()); + } + return result; + } + + @Override + public DureeDeVieParDefaut calculerDureeVieDefaut(DemandeCalculImpactEquipementPhysique demandeCalcul) throws CalculImpactException { + + if(demandeCalcul.getTypeEquipement() != null && demandeCalcul.getTypeEquipement().getDureeVieDefaut() !=null){ + return DureeDeVieParDefaut.builder() + .valeurEquipementDureeVieDefaut(demandeCalcul.getTypeEquipement().getDureeVieDefaut()) + .valeur(demandeCalcul.getTypeEquipement().getDureeVieDefaut()) + .build(); + } + var refHypotheseOpt= demandeCalcul.getHypotheseFromCode("dureeVieParDefaut"); + if (refHypotheseOpt.isPresent() && refHypotheseOpt.get().getValeur() != null){ + ReferentielHypothese hypothese = refHypotheseOpt.get(); + return DureeDeVieParDefaut.builder() + .valeurReferentielHypothese(hypothese.getValeur()) + .sourceReferentielHypothese(hypothese.getSource()) + .valeur(hypothese.getValeur()) + .build(); + } + throw new CalculImpactException(TypeErreurCalcul.ERREUR_FONCTIONNELLE.getCode(),"La durée de vie par défaut de l'équipement n'a pas pu être déterminée"); + } + +} diff --git a/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/traceur/TraceCalculImpactApplicationUtils.java b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/traceur/TraceCalculImpactApplicationUtils.java new file mode 100644 index 00000000..e1f861fb --- /dev/null +++ b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/traceur/TraceCalculImpactApplicationUtils.java @@ -0,0 +1,29 @@ +package org.mte.numecoeval.calculs.domain.traceur; + +import org.mte.numecoeval.calculs.domain.data.demande.DemandeCalculImpactApplication; +import org.mte.numecoeval.calculs.domain.data.trace.TraceCalculImpactApplication; +import org.mte.numecoeval.calculs.domain.exception.CalculImpactException; + +public class TraceCalculImpactApplicationUtils { + + private TraceCalculImpactApplicationUtils() { + // private constructor + } + + public static TraceCalculImpactApplication buildTrace(DemandeCalculImpactApplication demandeCalcul) { + return TraceCalculImpactApplication.builder() + .formule("ImpactApplication = ImpactEquipementVirtuel(%s) / nbApplications(%s)".formatted(demandeCalcul.getImpactEquipementVirtuel().getImpactUnitaire(), demandeCalcul.getNbApplicationsForCalcul())) + .nbApplications(demandeCalcul.getNbApplications()) + .build(); + } + + public static TraceCalculImpactApplication buildTraceError(CalculImpactException exception) { + var message = "Erreur lors du calcul de l'impact d'application"; + if(exception != null) { + message = exception.getErrorType() + " : " + exception.getMessage(); + } + return TraceCalculImpactApplication.builder() + .erreur(message) + .build(); + } +} diff --git a/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/traceur/TraceCalculImpactEquipementPhysiqueUtils.java b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/traceur/TraceCalculImpactEquipementPhysiqueUtils.java new file mode 100644 index 00000000..d1aafb97 --- /dev/null +++ b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/traceur/TraceCalculImpactEquipementPhysiqueUtils.java @@ -0,0 +1,49 @@ +package org.mte.numecoeval.calculs.domain.traceur; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import org.mte.numecoeval.calculs.domain.data.trace.ConsoElecAnMoyenne; +import org.mte.numecoeval.calculs.domain.data.trace.DureeDeVie; +import org.mte.numecoeval.calculs.domain.data.trace.MixElectrique; +import org.mte.numecoeval.calculs.domain.data.trace.TraceCalculImpactEquipementPhysique; +import org.mte.numecoeval.calculs.domain.exception.CalculImpactException; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class TraceCalculImpactEquipementPhysiqueUtils { + + public static TraceCalculImpactEquipementPhysique buildTraceErreur(CalculImpactException exception) { + var message = "Erreur lors du calcul de l'impact d'équipement physique"; + if (exception != null) { + message = exception.getErrorType() + " : " + exception.getMessage(); + } + return TraceCalculImpactEquipementPhysique.builder() + .erreur(message) + .build(); + } + + public static TraceCalculImpactEquipementPhysique buildTracePremierScenario(Double quantite, ConsoElecAnMoyenne consoElecAnMoyenne, MixElectrique mixElectriqueValeur, Double taux_utilisation) { + return TraceCalculImpactEquipementPhysique.builder() + .formule(getFormulePremierScenario(quantite, consoElecAnMoyenne.getValeur(), mixElectriqueValeur.getValeur(), taux_utilisation)) + .consoElecAnMoyenne(consoElecAnMoyenne) + .mixElectrique(mixElectriqueValeur) + .build(); + } + + public static TraceCalculImpactEquipementPhysique buildTraceSecondScenario(Double quantite, Double valeurRefrentiel, String sourceReferentiel, DureeDeVie dureeDeVie, Double taux_utilisation) { + return TraceCalculImpactEquipementPhysique.builder() + .formule(getFormuleSecondScenario(quantite, valeurRefrentiel, dureeDeVie.getValeur(), taux_utilisation)) + .valeurReferentielImpactEquipement(valeurRefrentiel) + .sourceReferentielImpactEquipement(sourceReferentiel) + .dureeDeVie(dureeDeVie) + .build(); + } + + public static String getFormulePremierScenario(Double quantite, Double consoElecAnMoyenne, Double mixElectriqueValeur, Double taux_utilisation) { + return "ImpactEquipementPhysique = (Quantité(%s) * ConsoElecAnMoyenne(%s) * MixElectrique(%s) * TauxUtilisation(%s)) / 365".formatted(quantite, consoElecAnMoyenne, mixElectriqueValeur, taux_utilisation); + } + + public static String getFormuleSecondScenario(Double quantite, Double valeurRefrentiel, Double dureeVie, Double taux_utilisation) { + return "ImpactEquipementPhysique = (Quantité(%s) * referentielImpactEquipement(%s) * TauxUtilisation(%s)) / dureeVie(%s)".formatted(quantite, valeurRefrentiel, taux_utilisation, dureeVie); + } + +} diff --git a/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/traceur/TraceCalculImpactMessagerieUtils.java b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/traceur/TraceCalculImpactMessagerieUtils.java new file mode 100644 index 00000000..d1b28ac0 --- /dev/null +++ b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/traceur/TraceCalculImpactMessagerieUtils.java @@ -0,0 +1,50 @@ +package org.mte.numecoeval.calculs.domain.traceur; + +import org.mte.numecoeval.calculs.domain.data.demande.DemandeCalculImpactMessagerie; +import org.mte.numecoeval.calculs.domain.data.referentiel.ReferentielImpactMessagerie; +import org.mte.numecoeval.calculs.domain.data.trace.TraceCalculImpactMessagerie; +import org.mte.numecoeval.calculs.domain.exception.CalculImpactException; + +public class TraceCalculImpactMessagerieUtils { + + private TraceCalculImpactMessagerieUtils() { + // private constructor + } + + public static TraceCalculImpactMessagerie buildTrace(DemandeCalculImpactMessagerie demandeCalcul, ReferentielImpactMessagerie referentielImpactMessagerie) { + return TraceCalculImpactMessagerie.builder() + .formule(getFormule( + demandeCalcul.getMessagerie().getVolumeTotalMailEmis()/demandeCalcul.getMessagerie().getNombreMailEmis(), + demandeCalcul.getMessagerie().getVolumeTotalMailEmis(), + demandeCalcul.getMessagerie().getNombreMailEmis(), + referentielImpactMessagerie.getConstanteCoefficientDirecteur(), + referentielImpactMessagerie.getConstanteOrdonneeOrigine(), + demandeCalcul.getMessagerie().getNombreMailEmisXDestinataires() + )) + .critere(demandeCalcul.getCritere().getNomCritere()) + .volumeTotalMailEmis(demandeCalcul.getMessagerie().getVolumeTotalMailEmis()) + .nombreMailEmis(demandeCalcul.getMessagerie().getNombreMailEmis()) + .nombreMailEmisXDestinataires(demandeCalcul.getMessagerie().getNombreMailEmisXDestinataires()) + .constanteCoefficientDirecteur(referentielImpactMessagerie.getConstanteCoefficientDirecteur()) + .constanteOrdonneeOrigine(referentielImpactMessagerie.getConstanteOrdonneeOrigine()) + .poidsMoyenMail(demandeCalcul.getMessagerie().getVolumeTotalMailEmis()/demandeCalcul.getMessagerie().getNombreMailEmis()) + .build(); + } + + public static TraceCalculImpactMessagerie buildTraceError(CalculImpactException exception) { + var message = "Erreur lors du calcul de l'impact de messagerie"; + if(exception != null) { + message = exception.getErrorType() + " : " + exception.getMessage(); + } + return TraceCalculImpactMessagerie.builder() + .erreur(message) + .build(); + } + + private static String getFormule(Double poidsMoyenMail, Double volumeTotalMailEmis, Double nombreMailEmis, Double constanteCoefficientDirecteur, Double constanteOrdonneeOrigine, Double nombreMailEmisXDestinataires){ + return """ + poidsMoyenMail(%s) = volumeTotalMailEmis(%s)/nombreMailEmis(%s); + impactMensuel = (constanteCoefficientDirecteur (%s) * poidsMoyenMail(%s) + constanteOrdonneeOrigine(%s)) * nombreMailEmisXDestinataires(%s) + """.formatted(poidsMoyenMail,volumeTotalMailEmis,nombreMailEmis,constanteCoefficientDirecteur,poidsMoyenMail,constanteOrdonneeOrigine, nombreMailEmisXDestinataires); + } +} diff --git a/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/traceur/TraceCalculImpactReseauUtils.java b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/traceur/TraceCalculImpactReseauUtils.java new file mode 100644 index 00000000..69c37222 --- /dev/null +++ b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/traceur/TraceCalculImpactReseauUtils.java @@ -0,0 +1,44 @@ +package org.mte.numecoeval.calculs.domain.traceur; + +import org.mte.numecoeval.calculs.domain.data.demande.DemandeCalculImpactReseau; +import org.mte.numecoeval.calculs.domain.data.referentiel.ReferentielImpactReseau; +import org.mte.numecoeval.calculs.domain.data.trace.TraceCalculImpactReseau; +import org.mte.numecoeval.calculs.domain.exception.CalculImpactException; + +public class TraceCalculImpactReseauUtils { + private TraceCalculImpactReseauUtils() { + } + + private static String getFormule(Float goTelecharge, String critere, String etapeACV, String equipementPhysique, Double impactReseauMobileMoyen) { + return "impactReseau = 'equipementPhysique.goTelecharge (%s) x ref_ImpactReseau(%s, %s, %s).valeur(%s)'".formatted(goTelecharge, critere, etapeACV, equipementPhysique, impactReseauMobileMoyen); + } + + public static TraceCalculImpactReseau buildTraceErreur(CalculImpactException exception) { + var message = "Erreur lors du calcul de l'impact réseau pour un équipement physique"; + if(exception != null) { + message = exception.getErrorType() + " : " + exception.getMessage(); + } + return TraceCalculImpactReseau.builder() + .erreur(message) + .build(); + } + + public static TraceCalculImpactReseau buildTrace(DemandeCalculImpactReseau demandeCalculImpactReseau, ReferentielImpactReseau referentielImpactReseau){ + + return TraceCalculImpactReseau.builder() + .critere(demandeCalculImpactReseau.getCritere().getNomCritere()) + .etapeACV(demandeCalculImpactReseau.getEtape().getCode()) + .equipementPhysique(demandeCalculImpactReseau.getEquipementPhysique().getNomEquipementPhysique()) + .impactReseauMobileMoyen(referentielImpactReseau.getImpactReseauMobileMoyen().toString()) + .goTelecharge(demandeCalculImpactReseau.getEquipementPhysique().getGoTelecharge().toString()) + .sourceReferentielReseau(referentielImpactReseau.getSource()) + .formule(getFormule( + demandeCalculImpactReseau.getEquipementPhysique().getGoTelecharge(), + demandeCalculImpactReseau.getCritere().getNomCritere(), + demandeCalculImpactReseau.getEtape().getCode(), + referentielImpactReseau.getRefReseau(), + referentielImpactReseau.getImpactReseauMobileMoyen() + )) + .build(); + } +} diff --git a/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/traceur/TraceCalculImpactVirtuelUtils.java b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/traceur/TraceCalculImpactVirtuelUtils.java new file mode 100644 index 00000000..19c98a42 --- /dev/null +++ b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/traceur/TraceCalculImpactVirtuelUtils.java @@ -0,0 +1,82 @@ +package org.mte.numecoeval.calculs.domain.traceur; + +import org.mte.numecoeval.calculs.domain.data.demande.DemandeCalculImpactEquipementVirtuel; +import org.mte.numecoeval.calculs.domain.data.trace.TraceCalculImpactEquipementVirtuel; +import org.mte.numecoeval.calculs.domain.exception.CalculImpactException; +import org.mte.numecoeval.calculs.domain.port.input.service.impl.CalculImpactEquipementVirtuelServiceImpl; + +public class TraceCalculImpactVirtuelUtils { + + private TraceCalculImpactVirtuelUtils(){} + + public static TraceCalculImpactEquipementVirtuel buildTrace(DemandeCalculImpactEquipementVirtuel demandeCalcul) { + if(CalculImpactEquipementVirtuelServiceImpl.cleRepartitionEstDisponible(demandeCalcul)) { + return TraceCalculImpactEquipementVirtuel.builder() + .formule(getFormuleForCleRepartition(demandeCalcul)) + .build(); + } + else if(CalculImpactEquipementVirtuelServiceImpl.calculPourCalculEstDisponible(demandeCalcul)) { + return TraceCalculImpactEquipementVirtuel.builder() + .formule(getFormuleWithTotalVCPU(demandeCalcul)) + .nbTotalVCPU(demandeCalcul.getNbTotalVCPU()) + .nbEquipementsVirtuels(demandeCalcul.getNbEquipementsVirtuels()) + .build(); + } + else if(CalculImpactEquipementVirtuelServiceImpl.calculPourStockageEstDisponible(demandeCalcul)) { + return TraceCalculImpactEquipementVirtuel.builder() + .formule(getFormuleForStockage(demandeCalcul)) + .stockageTotalVirtuel(demandeCalcul.getStockageTotalVirtuel()) + .build(); + } + + return TraceCalculImpactEquipementVirtuel.builder() + .formule(getFormuleWithNombreEquipementVirtuel(demandeCalcul)) + .nbTotalVCPU(demandeCalcul.getNbTotalVCPU()) + .nbEquipementsVirtuels(demandeCalcul.getNbEquipementsVirtuels()) + .build(); + } + + public static TraceCalculImpactEquipementVirtuel buildTraceErreur(CalculImpactException exception) { + var message = "Erreur lors du calcul de l'impact d'équipement virtuel"; + if(exception != null) { + message = exception.getErrorType() + " : " + exception.getMessage(); + } + return TraceCalculImpactEquipementVirtuel.builder() + .erreur(message) + .build(); + } + + public static String getFormuleWithTotalVCPU(DemandeCalculImpactEquipementVirtuel demandeCalcul){ + return "valeurImpactUnitaire = valeurImpactEquipementPhysique(%s) * equipementVirtuel.vCPU(%s) / nbvCPU(%s)" + .formatted( + demandeCalcul.getImpactEquipement().getImpactUnitaire(), + demandeCalcul.getEquipementVirtuel().getVCPU(), + demandeCalcul.getNbTotalVCPU() + ); + } + + public static String getFormuleForStockage(DemandeCalculImpactEquipementVirtuel demandeCalcul){ + return "valeurImpactUnitaire = valeurImpactEquipementPhysique(%s) * equipementVirtuel.capaciteStockage(%s) / stockageTotalVirtuel(%s)" + .formatted( + demandeCalcul.getImpactEquipement().getImpactUnitaire(), + demandeCalcul.getEquipementVirtuel().getCapaciteStockage(), + demandeCalcul.getStockageTotalVirtuel() + ); + } + + public static String getFormuleForCleRepartition(DemandeCalculImpactEquipementVirtuel demandeCalcul){ + return "valeurImpactUnitaire = valeurImpactEquipementPhysique(%s) * equipementVirtuel.cleRepartition(%s)" + .formatted( + demandeCalcul.getImpactEquipement().getImpactUnitaire(), + demandeCalcul.getEquipementVirtuel().getCleRepartition() + ); + } + + public static String getFormuleWithNombreEquipementVirtuel(DemandeCalculImpactEquipementVirtuel demandeCalcul){ + return "valeurImpactUnitaire = valeurImpactEquipementPhysique(%s) / nbVM(%s)" + .formatted( + demandeCalcul.getImpactEquipement().getImpactUnitaire(), + demandeCalcul.getNbEquipementsVirtuels() + ); + } +} diff --git a/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/traceur/TraceUtils.java b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/traceur/TraceUtils.java new file mode 100644 index 00000000..28f04788 --- /dev/null +++ b/services/calculs/src/main/java/org/mte/numecoeval/calculs/domain/traceur/TraceUtils.java @@ -0,0 +1,23 @@ +package org.mte.numecoeval.calculs.domain.traceur; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class TraceUtils { + + private TraceUtils() { + // private constructor + } + + public static String getTraceFromTraceur(ObjectMapper objectMapper, Object traceur) { + String trace= ""; + try { + trace = objectMapper.writeValueAsString(traceur); + } catch (JsonProcessingException e) { + log.error(e.getMessage()); + } + return trace; + } +} diff --git a/services/calculs/src/test/java/org/mte/numecoeval/calculs/CucumberIntegrationTest.java b/services/calculs/src/test/java/org/mte/numecoeval/calculs/CucumberIntegrationTest.java new file mode 100644 index 00000000..651063bd --- /dev/null +++ b/services/calculs/src/test/java/org/mte/numecoeval/calculs/CucumberIntegrationTest.java @@ -0,0 +1,51 @@ +package org.mte.numecoeval.calculs; + +import io.cucumber.spring.CucumberContextConfiguration; +import org.junit.jupiter.api.Test; +import org.junit.platform.suite.api.ConfigurationParameter; +import org.junit.platform.suite.api.IncludeEngines; +import org.junit.platform.suite.api.SelectClasspathResource; +import org.junit.platform.suite.api.Suite; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.core.env.Environment; +import org.springframework.test.context.ActiveProfiles; + +import java.text.NumberFormat; +import java.time.format.DateTimeFormatter; +import java.util.Locale; + +import static io.cucumber.junit.platform.engine.Constants.GLUE_PROPERTY_NAME; +import static io.cucumber.junit.platform.engine.Constants.PLUGIN_PROPERTY_NAME; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +@CucumberContextConfiguration +@SpringBootTest(classes = CucumberIntegrationTest.TestApplication.class) +@ActiveProfiles(profiles = { "test" }) +@Suite +@IncludeEngines("cucumber") +@SelectClasspathResource("org/mte/numecoeval/calculs") +@ConfigurationParameter(key = PLUGIN_PROPERTY_NAME, value = "pretty,html:target/cucumber-reports.html") +@ConfigurationParameter(key = GLUE_PROPERTY_NAME, value = "org.mte.numecoeval.calculs") +public class CucumberIntegrationTest { + + public static final DateTimeFormatter FORMATTER_FRENCH_FORMAT = DateTimeFormatter.ofPattern("dd/MM/yyyy"); + + public static final NumberFormat NUMBER_FRENCH_FORMAT = NumberFormat.getInstance(Locale.FRENCH); + + @Test + void testContextLoadOK(@Autowired Environment environment) { + assertNotNull(environment, "L'environnement est correctement chargé"); + } + + @SpringBootApplication + public static class TestApplication { + + public static void main(String[] args) { + SpringApplication.run(TestApplication.class, args); + } + + } +} diff --git a/services/calculs/src/test/java/org/mte/numecoeval/calculs/domain/service/CalculImpactApplicationServiceImplTest.java b/services/calculs/src/test/java/org/mte/numecoeval/calculs/domain/service/CalculImpactApplicationServiceImplTest.java new file mode 100644 index 00000000..ee897213 --- /dev/null +++ b/services/calculs/src/test/java/org/mte/numecoeval/calculs/domain/service/CalculImpactApplicationServiceImplTest.java @@ -0,0 +1,346 @@ +package org.mte.numecoeval.calculs.domain.service; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mte.numecoeval.calculs.domain.data.demande.DemandeCalculImpactApplication; +import org.mte.numecoeval.calculs.domain.data.entree.Application; +import org.mte.numecoeval.calculs.domain.data.indicateurs.ImpactApplication; +import org.mte.numecoeval.calculs.domain.data.indicateurs.ImpactEquipementVirtuel; +import org.mte.numecoeval.calculs.domain.port.input.service.impl.CalculImpactApplicationServiceImpl; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +import static org.junit.jupiter.api.Assertions.*; + +@ExtendWith(MockitoExtension.class) +class CalculImpactApplicationServiceImplTest { + + CalculImpactApplicationServiceImpl calculImpactApplicatifService ; + + @BeforeEach + void setUp(){ + calculImpactApplicatifService = new CalculImpactApplicationServiceImpl(new ObjectMapper()); + } + + @Test + void whenImpactEquipementVirtuelIsInError_ShouldReturnImpactError() { + String applicationName ="MS-Word"; + String environnement = "Production"; + LocalDate dateLot = LocalDate.of(2023, 4, 1); + Application application = Application.builder() + .dateLot(dateLot) + .nomOrganisation("Test") + .nomLot("Test|20220101") + .nomSourceDonnee("Source_Test") + .nomApplication(applicationName) + .typeEnvironnement(environnement) + .domaine("Bureautique") + .sousDomaine("Licence") + .nomEquipementPhysique("nomEquipementPhysique") + .nomEquipementVirtuel("nomEquipementVirtuel") + .build(); + ImpactEquipementVirtuel impactEquipementVirtuel = ImpactEquipementVirtuel.builder() + .dateLot(dateLot) + .etapeACV("Utilisation") + .critere("Critere") + .unite("kgCO2eq") + .dateCalcul(LocalDateTime.now().minusSeconds(10)) + .statutIndicateur("ERREUR") + .impactUnitaire(61.744d) + .nomEquipement(application.getNomEquipementPhysique()) + .nomEquipementVirtuel(application.getNomEquipementVirtuel()) + .trace("{\"erreur\":\"Erreur lors du calcul d'impact équipement virtuel\"}") + .build(); + DemandeCalculImpactApplication demanceCalcul = DemandeCalculImpactApplication.builder() + .application(application) + .dateCalcul(LocalDateTime.now()) + .nbApplications(1) + .impactEquipementVirtuel(impactEquipementVirtuel) + .build(); + + // When + var impactApplication = calculImpactApplicatifService.calculImpactApplicatif(demanceCalcul); + + assertContentIndicateur(demanceCalcul, impactApplication); + + + assertNull(impactApplication.getImpactUnitaire()); + assertNull(impactApplication.getConsoElecMoyenne()); + + assertEquals("ERREUR", impactApplication.getStatutIndicateur()); + assertEquals("{\"erreur\":\"ErrCalcFonc : L'indicateur d'impact équipement virtuel associé est au statut : ERREUR\"}", impactApplication.getTrace()); + } + + @Test + void whenImpactEquipementVirtuelIsNull_ShouldReturnImpactError() { + String applicationName ="MS-Word"; + String environnement = "Production"; + LocalDate dateLot = LocalDate.of(2023, 4, 1); + Application application = Application.builder() + .dateLot(dateLot) + .nomOrganisation("Test") + .nomLot("Test|20220101") + .nomSourceDonnee("Source_Test") + .nomApplication(applicationName) + .typeEnvironnement(environnement) + .domaine("Bureautique") + .sousDomaine("Licence") + .nomEquipementPhysique("nomEquipementPhysique") + .nomEquipementVirtuel("nomEquipementVirtuel") + .build(); + ImpactEquipementVirtuel impactEquipementVirtuel = ImpactEquipementVirtuel.builder() + .dateLot(dateLot) + .etapeACV("Utilisation") + .critere("Critere") + .unite("kgCO2eq") + .dateCalcul(LocalDateTime.now().minusSeconds(10)) + .statutIndicateur("OK") + .impactUnitaire(null) + .nomEquipement(application.getNomEquipementPhysique()) + .nomEquipementVirtuel(application.getNomEquipementVirtuel()) + .trace("{\"erreur\":\"Erreur lors du calcul d'impact équipement virtuel\"}") + .build(); + DemandeCalculImpactApplication demanceCalcul = DemandeCalculImpactApplication.builder() + .application(application) + .dateCalcul(LocalDateTime.now()) + .nbApplications(1) + .impactEquipementVirtuel(impactEquipementVirtuel) + .build(); + + // When + var impactApplication = calculImpactApplicatifService.calculImpactApplicatif(demanceCalcul); + + assertContentIndicateur(demanceCalcul, impactApplication); + + + assertNull(impactApplication.getImpactUnitaire()); + assertNull(impactApplication.getConsoElecMoyenne()); + + assertEquals("ERREUR", impactApplication.getStatutIndicateur()); + assertEquals("{\"erreur\":\"ErrCalcFonc : L'indicateur d'impact équipement virtuel associé est null.\"}", impactApplication.getTrace()); + } + + @ParameterizedTest + @ValueSource(ints = 0) + void whenNbApplicationIs0_ShouldApplyRatio1AndReturnImpactOK(Integer nbApplications) { + String applicationName ="MS-Word"; + String environnement = "Production"; + LocalDate dateLot = LocalDate.of(2023, 4, 1); + Application application = Application.builder() + .dateLot(dateLot) + .nomOrganisation("Test") + .nomLot("Test|20220101") + .nomSourceDonnee("Source_Test") + .nomApplication(applicationName) + .typeEnvironnement(environnement) + .domaine("Bureautique") + .sousDomaine("Licence") + .nomEquipementPhysique("nomEquipementPhysique") + .nomEquipementVirtuel("nomEquipementVirtuel") + .build(); + ImpactEquipementVirtuel impactEquipementVirtuel = ImpactEquipementVirtuel.builder() + .dateLot(dateLot) + .etapeACV("Utilisation") + .critere("Critere") + .unite("kgCO2eq") + .dateCalcul(LocalDateTime.now().minusSeconds(10)) + .statutIndicateur("OK") + .impactUnitaire(61.744d) + .nomEquipement(application.getNomEquipementPhysique()) + .nomEquipementVirtuel(application.getNomEquipementVirtuel()) + .build(); + DemandeCalculImpactApplication demanceCalcul = DemandeCalculImpactApplication.builder() + .application(application) + .dateCalcul(LocalDateTime.now()) + .nbApplications(nbApplications) + .impactEquipementVirtuel(impactEquipementVirtuel) + .build(); + + // When + var impactApplication = calculImpactApplicatifService.calculImpactApplicatif(demanceCalcul); + + assertContentIndicateur(demanceCalcul, impactApplication); + + assertEquals(demanceCalcul.getImpactEquipementVirtuel().getImpactUnitaire(), impactApplication.getImpactUnitaire()); + assertEquals(demanceCalcul.getImpactEquipementVirtuel().getConsoElecMoyenne(), impactApplication.getConsoElecMoyenne()); + + assertEquals("OK", impactApplication.getStatutIndicateur()); + assertEquals("{\"formule\":\"ImpactApplication = ImpactEquipementVirtuel(61.744) / nbApplications(1)\",\"nbApplications\":0}", impactApplication.getTrace()); + } + + @ParameterizedTest + @NullSource + void whenNbApplicationIsNull_ShouldApplyRatio1AndReturnImpactOK(Integer nbApplications) { + String applicationName ="MS-Word"; + String environnement = "Production"; + LocalDate dateLot = LocalDate.of(2023, 4, 1); + Application application = Application.builder() + .dateLot(dateLot) + .nomOrganisation("Test") + .nomLot("Test|20220101") + .nomSourceDonnee("Source_Test") + .nomApplication(applicationName) + .typeEnvironnement(environnement) + .domaine("Bureautique") + .sousDomaine("Licence") + .nomEquipementPhysique("nomEquipementPhysique") + .nomEquipementVirtuel("nomEquipementVirtuel") + .build(); + ImpactEquipementVirtuel impactEquipementVirtuel = ImpactEquipementVirtuel.builder() + .dateLot(dateLot) + .etapeACV("Utilisation") + .critere("Critere") + .unite("kgCO2eq") + .dateCalcul(LocalDateTime.now().minusSeconds(10)) + .statutIndicateur("OK") + .impactUnitaire(61.744d) + .nomEquipement(application.getNomEquipementPhysique()) + .nomEquipementVirtuel(application.getNomEquipementVirtuel()) + .build(); + DemandeCalculImpactApplication demanceCalcul = DemandeCalculImpactApplication.builder() + .application(application) + .dateCalcul(LocalDateTime.now()) + .nbApplications(nbApplications) + .impactEquipementVirtuel(impactEquipementVirtuel) + .build(); + + // When + var impactApplication = calculImpactApplicatifService.calculImpactApplicatif(demanceCalcul); + + assertContentIndicateur(demanceCalcul, impactApplication); + + assertEquals(demanceCalcul.getImpactEquipementVirtuel().getImpactUnitaire(), impactApplication.getImpactUnitaire()); + assertEquals(demanceCalcul.getImpactEquipementVirtuel().getConsoElecMoyenne(), impactApplication.getConsoElecMoyenne()); + + assertEquals("OK", impactApplication.getStatutIndicateur()); + assertEquals("{\"formule\":\"ImpactApplication = ImpactEquipementVirtuel(61.744) / nbApplications(1)\"}", impactApplication.getTrace()); + } + + @Test + void whenImpactEquipementVirtuelIsOK_ShouldReturnImpactOK() { + String applicationName ="MS-Word"; + String environnement = "Production"; + LocalDate dateLot = LocalDate.of(2023, 4, 1); + Application application = Application.builder() + .dateLot(dateLot) + .nomOrganisation("Test") + .nomLot("Test|20220101") + .nomSourceDonnee("Source_Test") + .nomApplication(applicationName) + .typeEnvironnement(environnement) + .domaine("Bureautique") + .sousDomaine("Licence") + .nomEquipementPhysique("nomEquipementPhysique") + .nomEquipementVirtuel("nomEquipementVirtuel") + .build(); + ImpactEquipementVirtuel impactEquipementVirtuel = ImpactEquipementVirtuel.builder() + .dateLot(dateLot) + .etapeACV("Utilisation") + .critere("Critere") + .unite("kgCO2eq") + .dateCalcul(LocalDateTime.now().minusSeconds(10)) + .statutIndicateur("OK") + .impactUnitaire(61.744d) + .nomEquipement(application.getNomEquipementPhysique()) + .nomEquipementVirtuel(application.getNomEquipementVirtuel()) + .build(); + DemandeCalculImpactApplication demanceCalcul = DemandeCalculImpactApplication.builder() + .application(application) + .dateCalcul(LocalDateTime.now()) + .nbApplications(1) + .impactEquipementVirtuel(impactEquipementVirtuel) + .build(); + + // When + var impactApplication = calculImpactApplicatifService.calculImpactApplicatif(demanceCalcul); + + assertContentIndicateur(demanceCalcul, impactApplication); + + assertEquals(demanceCalcul.getImpactEquipementVirtuel().getImpactUnitaire(), impactApplication.getImpactUnitaire()); + assertEquals(demanceCalcul.getImpactEquipementVirtuel().getConsoElecMoyenne(), impactApplication.getConsoElecMoyenne()); + + assertEquals("OK", impactApplication.getStatutIndicateur()); + assertEquals("{\"formule\":\"ImpactApplication = ImpactEquipementVirtuel(61.744) / nbApplications(1)\",\"nbApplications\":1}", impactApplication.getTrace()); + } + + @Test + void taiga1025_whenNbApplicationsIsMoreThan1_ThenImpactShouldBeDividedAndImpactShouldBeOK() { + String applicationName ="MS-Word"; + String environnement = "Production"; + LocalDate dateLot = LocalDate.of(2023, 4, 1); + Application application = Application.builder() + .dateLot(dateLot) + .nomOrganisation("Test") + .nomLot("Test|20220101") + .nomSourceDonnee("Source_Test") + .nomApplication(applicationName) + .typeEnvironnement(environnement) + .domaine("Bureautique") + .sousDomaine("Licence") + .nomEquipementPhysique("nomEquipementPhysique") + .nomEquipementVirtuel("nomEquipementVirtuel") + .build(); + ImpactEquipementVirtuel impactEquipementVirtuel = ImpactEquipementVirtuel.builder() + .dateLot(dateLot) + .etapeACV("Utilisation") + .critere("Critere") + .unite("kgCO2eq") + .dateCalcul(LocalDateTime.now().minusSeconds(10)) + .statutIndicateur("OK") + .impactUnitaire(61.744d) + .consoElecMoyenne(61.744d) + .nomEquipement(application.getNomEquipementPhysique()) + .nomEquipementVirtuel(application.getNomEquipementVirtuel()) + .trace("{\"erreur\":\"Erreur lors du calcul d'impact équipement virtuel\"}") + .build(); + DemandeCalculImpactApplication demanceCalcul = DemandeCalculImpactApplication.builder() + .application(application) + .dateCalcul(LocalDateTime.now()) + .nbApplications(10) + .impactEquipementVirtuel(impactEquipementVirtuel) + .build(); + + // When + var impactApplication = calculImpactApplicatifService.calculImpactApplicatif(demanceCalcul); + + assertContentIndicateur(demanceCalcul, impactApplication); + + assertEquals(demanceCalcul.getImpactEquipementVirtuel().getImpactUnitaire()/demanceCalcul.getNbApplications(), impactApplication.getImpactUnitaire()); + assertEquals(demanceCalcul.getImpactEquipementVirtuel().getConsoElecMoyenne()/demanceCalcul.getNbApplications(), impactApplication.getConsoElecMoyenne()); + + assertEquals("OK", impactApplication.getStatutIndicateur()); + assertEquals("{\"formule\":\"ImpactApplication = ImpactEquipementVirtuel(61.744) / nbApplications(10)\",\"nbApplications\":10}", impactApplication.getTrace()); + } + + private static void assertContentIndicateur(DemandeCalculImpactApplication demanceCalcul, ImpactApplication impactApplication) { + assertNotNull(impactApplication); + assertEquals(demanceCalcul.getApplication().getNomApplication(), impactApplication.getNomApplication()); + assertEquals(demanceCalcul.getApplication().getTypeEnvironnement(), impactApplication.getTypeEnvironnement()); + assertEquals(demanceCalcul.getApplication().getDomaine(), impactApplication.getDomaine()); + assertEquals(demanceCalcul.getApplication().getSousDomaine(), impactApplication.getSousDomaine()); + + assertEquals(demanceCalcul.getApplication().getNomLot(), impactApplication.getNomLot()); + assertEquals(demanceCalcul.getApplication().getDateLot(), impactApplication.getDateLot()); + assertEquals(demanceCalcul.getApplication().getNomEquipementPhysique(), impactApplication.getNomEquipementPhysique()); + assertEquals(demanceCalcul.getApplication().getNomEquipementVirtuel(), impactApplication.getNomEquipementVirtuel()); + assertEquals(demanceCalcul.getApplication().getNomOrganisation(), impactApplication.getNomOrganisation()); + assertEquals(demanceCalcul.getApplication().getNomEntite(), impactApplication.getNomEntite()); + assertEquals(demanceCalcul.getApplication().getNomSourceDonnee(), impactApplication.getNomSourceDonnee()); + + assertEquals(demanceCalcul.getDateCalcul(), impactApplication.getDateCalcul()); + + assertEquals(demanceCalcul.getImpactEquipementVirtuel().getCritere(), impactApplication.getCritere()); + assertEquals(demanceCalcul.getImpactEquipementVirtuel().getEtapeACV(), impactApplication.getEtapeACV()); + assertEquals(demanceCalcul.getImpactEquipementVirtuel().getUnite(), impactApplication.getUnite()); + + assertEquals("1.1", impactApplication.getVersionCalcul()); + } + +} \ No newline at end of file diff --git a/services/calculs/src/test/java/org/mte/numecoeval/calculs/domain/service/CalculImpactEquipementPhysiqueServiceTest.java b/services/calculs/src/test/java/org/mte/numecoeval/calculs/domain/service/CalculImpactEquipementPhysiqueServiceTest.java new file mode 100644 index 00000000..ffa130a0 --- /dev/null +++ b/services/calculs/src/test/java/org/mte/numecoeval/calculs/domain/service/CalculImpactEquipementPhysiqueServiceTest.java @@ -0,0 +1,2293 @@ +package org.mte.numecoeval.calculs.domain.service; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mte.numecoeval.calculs.domain.data.demande.DemandeCalculImpactEquipementPhysique; +import org.mte.numecoeval.calculs.domain.data.entree.DataCenter; +import org.mte.numecoeval.calculs.domain.data.entree.EquipementPhysique; +import org.mte.numecoeval.calculs.domain.data.indicateurs.ImpactEquipementPhysique; +import org.mte.numecoeval.calculs.domain.data.referentiel.*; +import org.mte.numecoeval.calculs.domain.data.trace.ConsoElecAnMoyenne; +import org.mte.numecoeval.calculs.domain.data.trace.DureeDeVie; +import org.mte.numecoeval.calculs.domain.data.trace.MixElectrique; +import org.mte.numecoeval.calculs.domain.data.trace.TraceCalculImpactEquipementPhysique; +import org.mte.numecoeval.calculs.domain.exception.CalculImpactException; +import org.mte.numecoeval.calculs.domain.port.input.service.CalculImpactEquipementPhysiqueService; +import org.mte.numecoeval.calculs.domain.port.input.service.impl.CalculImpactEquipementPhysiqueServiceImpl; +import org.mte.numecoeval.calculs.domain.port.input.service.impl.DureeDeVieEquipementPhysiqueServiceImpl; +import org.mte.numecoeval.calculs.domain.traceur.TraceCalculImpactEquipementPhysiqueUtils; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + + +@ExtendWith(MockitoExtension.class) +class CalculImpactEquipementPhysiqueServiceTest { + + private CalculImpactEquipementPhysiqueService calculImpactEquipementPhysiqueService; + private String etapeACV; + private ReferentielCritere critere; + private LocalDateTime dateCalcul; + private String refEquipementCible; + private String refEquipementParDefaut; + private EquipementPhysique equipementPhysique; + private ReferentielImpactEquipement referentielImpactEquipement; + private ReferentielMixElectrique referentielMixElectrique; + ReferentielCorrespondanceRefEquipement refEquipement; + ReferentielTypeEquipement typeEquipement; + ReferentielHypothese referentielHypothese; + @Mock + private DureeDeVieEquipementPhysiqueServiceImpl dureeDeVieEquipementPhysiqueService; + + @Spy + private ObjectMapper objectMapper; + + @BeforeEach + public void init() throws Exception { + MockitoAnnotations.openMocks(this); + calculImpactEquipementPhysiqueService = new CalculImpactEquipementPhysiqueServiceImpl( + dureeDeVieEquipementPhysiqueService, + objectMapper + ); + etapeACV = "UTILISATION"; + String etapeACV = "UTILISATION"; + critere = ReferentielCritere.builder() + .nomCritere("Changement Climatique") + .unite("kg CO_{2} eq") + .build(); + dateCalcul = LocalDateTime.now(); + refEquipementCible = "Apple Iphone 11"; + refEquipementParDefaut = "Apple Iphone 11"; + equipementPhysique = EquipementPhysique.builder() + .dateLot(LocalDate.of(2022, 1, 1)) + .nomOrganisation("Test") + .nomLot("Test|20220101") + .nomSourceDonnee("Source_Test") + .type("Téléphone") + .nomEquipementPhysique("Apple Iphone 11") + .modele("Apple Iphone 11") + .paysDUtilisation("France") + .quantite(1.0) + .nbJourUtiliseAn(216.0) + .consoElecAnnuelle(0.09) + .modeUtilisation("COPE") + .build(); + referentielImpactEquipement = ReferentielImpactEquipement.builder() + .source("CAF1") + .critere(critere.getNomCritere()) + .etape(etapeACV) + .refEquipement(refEquipementCible) + .consoElecMoyenne(0.09) + .build(); + referentielMixElectrique = ReferentielMixElectrique.builder() + .critere(critere.getNomCritere()) + .pays("France") + .raccourcisAnglais("FR") + .source("CAF1") + .valeur(0.0813225) + .build(); + refEquipement = ReferentielCorrespondanceRefEquipement.builder() + .refEquipementCible(refEquipementCible) + .modeleEquipementSource(equipementPhysique.getModele()) + .build(); + typeEquipement = ReferentielTypeEquipement.builder() + .type(equipementPhysique.getType()) + .refEquipementParDefaut(refEquipementParDefaut) + .dureeVieDefaut(null) + .build(); + } + + @Test + void shouldCalculerImpactEquipementPhysique_CAF1() { + + //Given + String etapeACV = "UTILISATION"; + ReferentielCritere critere = ReferentielCritere.builder() + .nomCritere("Changement Climatique") + .unite("kg CO_{2} eq") + .build(); + LocalDateTime dateCalcul = LocalDateTime.now(); + String refEquipementCible = "Apple Iphone 11"; + String refEquipementParDefaut = "Apple Iphone 11"; + EquipementPhysique equipementPhysique = EquipementPhysique.builder() + .dateLot(LocalDate.of(2022, 1, 1)) + .nomOrganisation("Test") + .nomLot("Test|20220101") + .nomSourceDonnee("Source_Test") + .type("Téléphone") + .nomEquipementPhysique("Apple Iphone 11") + .modele("Apple Iphone 11") + .paysDUtilisation("France") + .quantite(1.0) + .nbJourUtiliseAn(216.0) + // Consommation electrique annuelle à null, celle du référentiel sera utilisé + .consoElecAnnuelle(null) + .build(); + ReferentielImpactEquipement referentielImpactEquipement = ReferentielImpactEquipement.builder() + .source("CAF1") + .critere(critere.getNomCritere()) + .etape(etapeACV) + .refEquipement(refEquipementCible) + .consoElecMoyenne(0.09) + .build(); + ReferentielMixElectrique referentielMixElectrique = ReferentielMixElectrique.builder() + .critere(critere.getNomCritere()) + .pays("France") + .raccourcisAnglais("FR") + .source("CAF1") + .valeur(0.0813225) + .build(); + ReferentielCorrespondanceRefEquipement refEquipement = ReferentielCorrespondanceRefEquipement.builder() + .refEquipementCible(refEquipementCible) + .modeleEquipementSource(equipementPhysique.getModele()) + .build(); + ReferentielTypeEquipement typeEquipement = ReferentielTypeEquipement.builder() + .type(equipementPhysique.getType()) + .refEquipementParDefaut(refEquipementParDefaut) + .dureeVieDefaut(null) + .build(); + DemandeCalculImpactEquipementPhysique demandeCalcul = DemandeCalculImpactEquipementPhysique + .builder() + .dateCalcul(dateCalcul) + .etape(ReferentielEtapeACV.builder().code(etapeACV).build()) + .critere(critere) + .equipementPhysique(equipementPhysique) + .correspondanceRefEquipement(refEquipement) + .typeEquipement(typeEquipement) + .impactEquipements(Collections.singletonList(referentielImpactEquipement)) + .mixElectriques(Collections.singletonList(referentielMixElectrique)) + .build(); + + //When + var actual = calculImpactEquipementPhysiqueService.calculerImpactEquipementPhysique( + demandeCalcul + ); + + //Then + assertContentIndicateur(demandeCalcul, actual); + assertEquals("OK", actual.getStatutIndicateur()); + assertEquals(dateCalcul, actual.getDateCalcul()); + assertNotNull(actual.getImpactUnitaire()); + double actualImpactUnitaireLimited = BigDecimal.valueOf(actual.getImpactUnitaire()).setScale(4, RoundingMode.DOWN).doubleValue(); + assertEquals(0.0073, actualImpactUnitaireLimited); + assertEquals(referentielImpactEquipement.getConsoElecMoyenne(), actual.getConsoElecMoyenne()); + } + + /* même cas que le CAF1 mais avec une consoElecAnnuelle à 0 au lieu de null. + * Conformément au Taiga 895 : le 0 est bien appliqué et ramène le résultat à 0 + */ + @Test + void taiga895_shouldApplyConsoElecAnnuelWithConsoElecAnnuelleAt0_CAF1() { + + //Given + String etapeACV = "UTILISATION"; + ReferentielCritere critere = ReferentielCritere.builder() + .nomCritere("Changement Climatique") + .unite("kg CO_{2} eq") + .build(); + LocalDateTime dateCalcul = LocalDateTime.now(); + String refEquipementCible = "Apple Iphone 11"; + String refEquipementParDefaut = "Apple Iphone 11"; + EquipementPhysique equipementPhysique = EquipementPhysique.builder() + .dateLot(LocalDate.of(2022, 1, 1)) + .nomOrganisation("Test") + .nomLot("Test|20220101") + .nomSourceDonnee("Source_Test") + .type("Téléphone") + .nomEquipementPhysique("Apple Iphone 11") + .modele("Apple Iphone 11") + .paysDUtilisation("France") + .quantite(1.0) + .nbJourUtiliseAn(216.0) + // Consommation electrique annuelle à 0, elle sera utilisé et rend le calcul à 0 + .consoElecAnnuelle(0.0) + .build(); + ReferentielImpactEquipement referentielImpactEquipement = ReferentielImpactEquipement.builder() + .source("CAF1") + .critere(critere.getNomCritere()) + .etape(etapeACV) + .refEquipement(equipementPhysique.getModele()) + .consoElecMoyenne(0.09) + .build(); + ReferentielMixElectrique referentielMixElectrique = ReferentielMixElectrique.builder() + .critere(critere.getNomCritere()) + .pays("France") + .raccourcisAnglais("FR") + .source("CAF1") + .valeur(0.0813225) + .build(); + ReferentielCorrespondanceRefEquipement refEquipement = ReferentielCorrespondanceRefEquipement.builder() + .refEquipementCible(refEquipementCible) + .modeleEquipementSource(equipementPhysique.getModele()) + .build(); + ReferentielTypeEquipement typeEquipement = ReferentielTypeEquipement.builder() + .type(equipementPhysique.getType()) + .refEquipementParDefaut(refEquipementParDefaut) + .dureeVieDefaut(null) + .build(); + DemandeCalculImpactEquipementPhysique demandeCalcul = DemandeCalculImpactEquipementPhysique + .builder() + .dateCalcul(dateCalcul) + .etape(ReferentielEtapeACV.builder().code(etapeACV).build()) + .critere(critere) + .equipementPhysique(equipementPhysique) + .correspondanceRefEquipement(refEquipement) + .typeEquipement(typeEquipement) + .impactEquipements(Collections.singletonList(referentielImpactEquipement)) + .mixElectriques(Collections.singletonList(referentielMixElectrique)) + .build(); + + //When + var actual = calculImpactEquipementPhysiqueService.calculerImpactEquipementPhysique( + demandeCalcul + ); + + //Then + assertContentIndicateur(demandeCalcul, actual); + assertEquals("OK", actual.getStatutIndicateur()); + assertEquals(dateCalcul, actual.getDateCalcul()); + assertNotNull(actual.getImpactUnitaire()); + double actualImpactUnitaireLimited = BigDecimal.valueOf(actual.getImpactUnitaire()).setScale(4, RoundingMode.DOWN).doubleValue(); + assertEquals(0, actualImpactUnitaireLimited); + assertEquals(0, actual.getConsoElecMoyenne()); + assertNotEquals(referentielImpactEquipement.getConsoElecMoyenne(), actual.getConsoElecMoyenne()); + } + + @Test + void shouldCalculerImpactEquipementPhysique_CAF2_neededCorrection() { + + //Given + String etapeACV = "UTILISATION"; + ReferentielCritere critere = ReferentielCritere.builder() + .nomCritere("Changement Climatique") + .unite("kg CO_{2} eq") + .build(); + LocalDateTime dateCalcul = LocalDateTime.now(); + String refEquipementCible = "Ecran2"; + String refEquipementParDefaut = "Ecran2"; + EquipementPhysique equipementPhysique = EquipementPhysique.builder() + .dateLot(LocalDate.of(2022, 1, 1)) + .nomOrganisation("Test") + .nomLot("Test|20220101") + .nomSourceDonnee("Source_Test") + .type("Ecran") + .nomEquipementPhysique("Ecran2") + .modele("Ecran2") + .paysDUtilisation("France") + .quantite(1.0) + .nbJourUtiliseAn(30.0) + .build(); + ReferentielImpactEquipement referentielImpactEquipement = ReferentielImpactEquipement.builder() + .source("CAF2") + .critere(critere.getNomCritere()) + .etape(etapeACV) + .refEquipement(equipementPhysique.getModele()) + .consoElecMoyenne(70.0) + .build(); + ReferentielMixElectrique referentielMixElectrique = ReferentielMixElectrique.builder() + .critere(critere.getNomCritere()) + .pays("France") + .raccourcisAnglais("FR") + .source("CAF1") + .valeur(0.0813225) + .build(); + ReferentielCorrespondanceRefEquipement refEquipement = ReferentielCorrespondanceRefEquipement.builder() + .refEquipementCible(refEquipementCible) + .modeleEquipementSource(equipementPhysique.getModele()) + .build(); + ReferentielTypeEquipement typeEquipement = ReferentielTypeEquipement.builder() + .type(equipementPhysique.getType()) + .refEquipementParDefaut(refEquipementParDefaut) + .dureeVieDefaut(null) + .build(); + DemandeCalculImpactEquipementPhysique demandeCalcul = DemandeCalculImpactEquipementPhysique + .builder() + .dateCalcul(dateCalcul) + .etape(ReferentielEtapeACV.builder().code(etapeACV).build()) + .critere(critere) + .equipementPhysique(equipementPhysique) + .correspondanceRefEquipement(refEquipement) + .typeEquipement(typeEquipement) + .impactEquipements(Collections.singletonList(referentielImpactEquipement)) + .mixElectriques(Collections.singletonList(referentielMixElectrique)) + .build(); + + //When + var actual = calculImpactEquipementPhysiqueService.calculerImpactEquipementPhysique( + demandeCalcul + ); + + //Then + assertContentIndicateur(demandeCalcul, actual); + assertEquals("OK", actual.getStatutIndicateur()); + assertEquals(dateCalcul, actual.getDateCalcul()); + assertNotNull(actual.getImpactUnitaire()); + double actualImpactUnitaireLimited = BigDecimal.valueOf(actual.getImpactUnitaire()).setScale(3, RoundingMode.DOWN).doubleValue(); + assertEquals(5.692, actualImpactUnitaireLimited); + assertEquals(referentielImpactEquipement.getConsoElecMoyenne(), actual.getConsoElecMoyenne()); + } + + @Test + void shouldCalculerImpactEquipementPhysique_CAF3_decimalFixing() { + + //Given + String etapeACV = "UTILISATION"; + ReferentielCritere critere = ReferentielCritere.builder() + .nomCritere("Changement Climatique") + .unite("kg CO_{2} eq") + .build(); + LocalDateTime dateCalcul = LocalDateTime.now(); + String refEquipementCible = "OrdinateurBureau3"; + String refEquipementParDefaut = "OrdinateurBureau3"; + EquipementPhysique equipementPhysique = EquipementPhysique.builder() + .dateLot(LocalDate.of(2022, 1, 1)) + .nomOrganisation("Test") + .nomLot("Test|20220101") + .nomSourceDonnee("Source_Test") + .type("OrdinateurBureau") + .nomEquipementPhysique("OrdinateurBureau3") + .modele("OrdinateurBureau3") + .paysDUtilisation("Irlande") + .nomCourtDatacenter(null) + .quantite(1.0) + .nbJourUtiliseAn(365.0) + .build(); + ReferentielImpactEquipement referentielImpactEquipement = ReferentielImpactEquipement.builder() + .source("CAF3") + .critere(critere.getNomCritere()) + .etape(etapeACV) + .refEquipement(equipementPhysique.getModele()) + .consoElecMoyenne(3504.0) + .build(); + ReferentielMixElectrique referentielMixElectrique = ReferentielMixElectrique.builder() + .critere(critere.getNomCritere()) + .pays("Irlande") + .raccourcisAnglais("IR") + .source("CAF3") + .valeur(0.648118) + .build(); + ReferentielCorrespondanceRefEquipement refEquipement = ReferentielCorrespondanceRefEquipement.builder() + .refEquipementCible(refEquipementCible) + .modeleEquipementSource(equipementPhysique.getModele()) + .build(); + ReferentielTypeEquipement typeEquipement = ReferentielTypeEquipement.builder() + .type(equipementPhysique.getType()) + .refEquipementParDefaut(refEquipementParDefaut) + .dureeVieDefaut(null) + .build(); + DemandeCalculImpactEquipementPhysique demandeCalcul = DemandeCalculImpactEquipementPhysique + .builder() + .dateCalcul(dateCalcul) + .etape(ReferentielEtapeACV.builder().code(etapeACV).build()) + .critere(critere) + .equipementPhysique(equipementPhysique) + .correspondanceRefEquipement(refEquipement) + .typeEquipement(typeEquipement) + .impactEquipements(Collections.singletonList(referentielImpactEquipement)) + .mixElectriques(Collections.singletonList(referentielMixElectrique)) + .build(); + + //When + var actual = calculImpactEquipementPhysiqueService.calculerImpactEquipementPhysique( + demandeCalcul + ); + + //Then + assertContentIndicateur(demandeCalcul, actual); + assertEquals("OK", actual.getStatutIndicateur()); + assertNotNull(actual.getImpactUnitaire()); + assertEquals(dateCalcul, actual.getDateCalcul()); + double actualImpactUnitaireLimited = BigDecimal.valueOf(actual.getImpactUnitaire()).setScale(4, RoundingMode.DOWN).doubleValue(); + // quantité * refEquipementPhysique.consoElecMoyenne * MixElec + assertEquals(BigDecimal.valueOf(1.0 * 3504.0 * 0.648118).setScale(4, RoundingMode.DOWN).doubleValue(), actualImpactUnitaireLimited); + assertEquals(referentielImpactEquipement.getConsoElecMoyenne(), actual.getConsoElecMoyenne()); + } + + @Test + void taiga918_DataCenterReferencedAndUsedForPUE_CAF1() { + + //Given + String etapeACV = "UTILISATION"; + ReferentielCritere critere = ReferentielCritere.builder() + .nomCritere("Changement Climatique") + .unite("kg CO_{2} eq") + .build(); + DataCenter dataCenter = DataCenter.builder() + .nomCourtDatacenter("sequoia2") + .nomLongDatacenter("sequoia2") + .localisation("France") + .pue(1.43) + .build(); + String refEquipementCible = "ref-Serveur1"; + String refEquipementParDefaut = "ref-Serveur1"; + LocalDateTime dateCalcul = LocalDateTime.now(); + EquipementPhysique equipementPhysique = EquipementPhysique.builder() + .dateLot(LocalDate.of(2022, 1, 1)) + .nomOrganisation("Test") + .nomLot("Test|20220101") + .nomSourceDonnee("Source_Test") + .type("Serveur") + .nomEquipementPhysique("Serveur_100") + .dateAchat(LocalDate.of(2018, 10, 1)) + .modele("ref-Serveur1") + .paysDUtilisation("France") + .serveur(true) + .nomCourtDatacenter("sequoia2") + .dataCenter(dataCenter) + .quantite(2.0) + .consoElecAnnuelle(500.0) + .nbJourUtiliseAn(365.0) + .build(); + ReferentielMixElectrique referentielMixElectrique = ReferentielMixElectrique.builder() + .critere(critere.getNomCritere()) + .pays("France") + .raccourcisAnglais("FR") + .source("CAF1") + .valeur(0.08) + .build(); + + ReferentielCorrespondanceRefEquipement refEquipement = ReferentielCorrespondanceRefEquipement.builder() + .refEquipementCible(refEquipementCible) + .modeleEquipementSource(equipementPhysique.getModele()) + .build(); + ReferentielTypeEquipement typeEquipement = ReferentielTypeEquipement.builder() + .type(equipementPhysique.getType()) + .refEquipementParDefaut(refEquipementParDefaut) + .dureeVieDefaut(null) + .build(); + DemandeCalculImpactEquipementPhysique demandeCalcul = DemandeCalculImpactEquipementPhysique + .builder() + .dateCalcul(dateCalcul) + .etape(ReferentielEtapeACV.builder().code(etapeACV).build()) + .critere(critere) + .equipementPhysique(equipementPhysique) + .correspondanceRefEquipement(refEquipement) + .typeEquipement(typeEquipement) + .mixElectriques(Collections.singletonList(referentielMixElectrique)) + .build(); + + //When + var actual = calculImpactEquipementPhysiqueService.calculerImpactEquipementPhysique( + demandeCalcul + ); + + //Then + assertContentIndicateur(demandeCalcul, actual); + assertEquals("OK", actual.getStatutIndicateur()); + assertNotNull(actual.getImpactUnitaire()); + assertEquals(dateCalcul, actual.getDateCalcul()); + double actualImpactUnitaireLimited = BigDecimal.valueOf(actual.getImpactUnitaire()).setScale(4, RoundingMode.DOWN).doubleValue(); + // quantité * equipementPhysique.consoElecAnnuelle * DataCenter.pue * MixElec(France) + assertEquals(BigDecimal.valueOf(2.0 * 500.0 * 1.43 * 0.08).setScale(4, RoundingMode.DOWN).doubleValue(), actualImpactUnitaireLimited); + } + + @Test + void taiga918_DataCenterWithoutPUEAndDefaultPUEUsed_CAF2() { + + //Given + String etapeACV = "UTILISATION"; + ReferentielCritere critere = ReferentielCritere.builder() + .nomCritere("Changement Climatique") + .unite("kg CO_{2} eq") + .build(); + DataCenter dataCenter = DataCenter.builder() + .nomCourtDatacenter("sequoia3") + .nomLongDatacenter("sequoia3") + .localisation("France") + .pue(null) + .build(); + String refEquipementCible = "ref-Serveur4"; + String refEquipementParDefaut = "ref-Serveur4"; + LocalDateTime dateCalcul = LocalDateTime.now(); + EquipementPhysique equipementPhysique = EquipementPhysique.builder() + .dateLot(LocalDate.of(2022, 1, 1)) + .nomOrganisation("Test") + .nomLot("Test|20220101") + .nomSourceDonnee("Source_Test") + .type("Serveur") + .nomEquipementPhysique("Serveur_104") + .modele("ref-Serveur4") + .paysDUtilisation("France") + .serveur(true) + .nomCourtDatacenter("sequoia3") + .dataCenter(dataCenter) + .consoElecAnnuelle(500.0) + .quantite(1.0) + .nbJourUtiliseAn(365.0) + .build(); + ReferentielHypothese referentielHypothese = ReferentielHypothese.builder() + .valeur(2.0) + .code("PUEParDefaut") + .source("CAF2") + .build(); + ReferentielMixElectrique referentielMixElectrique = ReferentielMixElectrique.builder() + .critere(critere.getNomCritere()) + .pays("France") + .raccourcisAnglais("FR") + .source("CAF2") + .valeur(0.08) + .build(); + ReferentielCorrespondanceRefEquipement refEquipement = ReferentielCorrespondanceRefEquipement.builder() + .refEquipementCible(refEquipementCible) + .modeleEquipementSource(equipementPhysique.getModele()) + .build(); + ReferentielTypeEquipement typeEquipement = ReferentielTypeEquipement.builder() + .type(equipementPhysique.getType()) + .refEquipementParDefaut(refEquipementParDefaut) + .dureeVieDefaut(null) + .build(); + DemandeCalculImpactEquipementPhysique demandeCalcul = DemandeCalculImpactEquipementPhysique + .builder() + .dateCalcul(dateCalcul) + .etape(ReferentielEtapeACV.builder().code(etapeACV).build()) + .critere(critere) + .equipementPhysique(equipementPhysique) + .correspondanceRefEquipement(refEquipement) + .typeEquipement(typeEquipement) + .mixElectriques(Collections.singletonList(referentielMixElectrique)) + .hypotheses(Collections.singletonList(referentielHypothese)) + .build(); + + //When + var actual = calculImpactEquipementPhysiqueService.calculerImpactEquipementPhysique( + demandeCalcul + ); + + //Then + assertContentIndicateur(demandeCalcul, actual); + assertEquals("OK", actual.getStatutIndicateur()); + assertNotNull(actual.getImpactUnitaire()); + assertEquals(dateCalcul, actual.getDateCalcul()); + double actualImpactUnitaireLimited = BigDecimal.valueOf(actual.getImpactUnitaire()).setScale(4, RoundingMode.DOWN).doubleValue(); + // quantité * equipementPhysique.consoElecAnnuelle * ref_Hypothese(PUEParDefaut).valeur * MixElec France + assertEquals(BigDecimal.valueOf(1.0 * 500.0 * 2.0 * 0.08).setScale(4, RoundingMode.DOWN).doubleValue(), actualImpactUnitaireLimited); + } + + @Test + void taiga918_ConsoElecMoyenneUsedFromRef_CAF3() { + + //Given + String etapeACV = "UTILISATION"; + ReferentielCritere critere = ReferentielCritere.builder() + .nomCritere("Changement Climatique") + .unite("kg CO_{2} eq") + .build(); + DataCenter dataCenter = DataCenter.builder() + .nomCourtDatacenter("sequoia2") + .nomLongDatacenter("sequoia2") + .localisation("France") + .pue(1.43) + .build(); + String refEquipementCible = "ref-Serveur2"; + String refEquipementParDefaut = "ref-Serveur2"; + LocalDateTime dateCalcul = LocalDateTime.now(); + EquipementPhysique equipementPhysique = EquipementPhysique.builder() + .dateLot(LocalDate.of(2022, 1, 1)) + .nomOrganisation("Test") + .nomLot("Test|20220101") + .nomSourceDonnee("Source_Test") + .type("Serveur") + .nomEquipementPhysique("Serveur_101") + .modele("ref-Serveur2") + .paysDUtilisation("France") + .serveur(true) + .nomCourtDatacenter("sequoia2") + .dataCenter(dataCenter) + .quantite(1.0) + .nbJourUtiliseAn(365.0) + .build(); + ReferentielImpactEquipement referentielImpactEquipement = ReferentielImpactEquipement.builder() + .source("CAF2") + .critere(critere.getNomCritere()) + .etape(etapeACV) + .refEquipement(equipementPhysique.getModele()) + .consoElecMoyenne(10512.0) + .valeur(1139.839200000007) + .build(); + ReferentielMixElectrique referentielMixElectrique = ReferentielMixElectrique.builder() + .critere(critere.getNomCritere()) + .pays("France") + .raccourcisAnglais("FR") + .source("CAF3") + .valeur(0.08) + .build(); + ReferentielCorrespondanceRefEquipement refEquipement = ReferentielCorrespondanceRefEquipement.builder() + .refEquipementCible(refEquipementCible) + .modeleEquipementSource(equipementPhysique.getModele()) + .build(); + ReferentielTypeEquipement typeEquipement = ReferentielTypeEquipement.builder() + .type(equipementPhysique.getType()) + .refEquipementParDefaut(refEquipementParDefaut) + .dureeVieDefaut(null) + .build(); + DemandeCalculImpactEquipementPhysique demandeCalcul = DemandeCalculImpactEquipementPhysique + .builder() + .dateCalcul(dateCalcul) + .etape(ReferentielEtapeACV.builder().code(etapeACV).build()) + .critere(critere) + .equipementPhysique(equipementPhysique) + .correspondanceRefEquipement(refEquipement) + .typeEquipement(typeEquipement) + .impactEquipements(Collections.singletonList(referentielImpactEquipement)) + .mixElectriques(Collections.singletonList(referentielMixElectrique)) + .build(); + + //When + var actual = calculImpactEquipementPhysiqueService.calculerImpactEquipementPhysique( + demandeCalcul + ); + + //Then + assertContentIndicateur(demandeCalcul, actual); + assertEquals("OK", actual.getStatutIndicateur()); + assertNotNull(actual.getImpactUnitaire()); + assertEquals(dateCalcul, actual.getDateCalcul()); + double actualImpactUnitaireLimited = BigDecimal.valueOf(actual.getImpactUnitaire()).setScale(4, RoundingMode.DOWN).doubleValue(); + // quantité * refEquipementPhysique.consoElecMoyenne * DataCEnter.pue * MixElec France + assertEquals(BigDecimal.valueOf(1.0 * 10512.0 * 1.43 * 0.08).setScale(4, RoundingMode.DOWN).doubleValue(), actualImpactUnitaireLimited); + assertEquals(referentielImpactEquipement.getConsoElecMoyenne(), actual.getConsoElecMoyenne()); + } + + @Test + void taiga918_CAF4_whenDonneesConsoElecAbsente_shouldReturnErreurCalcul() { + + //Given + String etapeACV = "UTILISATION"; + ReferentielCritere critere = ReferentielCritere.builder() + .nomCritere("Changement Climatique") + .unite("kg CO_{2} eq") + .build(); + DataCenter dataCenter = DataCenter.builder() + .nomCourtDatacenter("sequoia2") + .nomLongDatacenter("sequoia2") + .localisation("France") + .pue(1.43) + .build(); + LocalDateTime dateCalcul = LocalDateTime.now(); + String refEquipementCible = "ref-Serveur3"; + String refEquipementParDefaut = "ref-Serveur3"; + EquipementPhysique equipementPhysique = EquipementPhysique.builder() + .dateLot(LocalDate.of(2022, 1, 1)) + .nomOrganisation("Test") + .nomLot("Test|20220101") + .nomSourceDonnee("Source_Test") + .type("Serveur") + .nomEquipementPhysique("Serveur_103") + .modele("ref-Serveur3") + .paysDUtilisation("France") + .serveur(true) + .nomCourtDatacenter("sequoia2") + .dataCenter(dataCenter) + .consoElecAnnuelle(null) + .quantite(1.0) + .nbJourUtiliseAn(365.0) + .build(); + ReferentielImpactEquipement referentielImpactEquipement = ReferentielImpactEquipement.builder() + .source("CAF4") + .critere(critere.getNomCritere()) + .etape(etapeACV) + .refEquipement(equipementPhysique.getModele()) + .consoElecMoyenne(null) + .valeur(1139.839200000007) + .build(); + ReferentielCorrespondanceRefEquipement refEquipement = ReferentielCorrespondanceRefEquipement.builder() + .refEquipementCible(refEquipementCible) + .modeleEquipementSource(equipementPhysique.getModele()) + .build(); + ReferentielTypeEquipement typeEquipement = ReferentielTypeEquipement.builder() + .type(equipementPhysique.getType()) + .refEquipementParDefaut(refEquipementParDefaut) + .dureeVieDefaut(null) + .build(); + DemandeCalculImpactEquipementPhysique demandeCalcul = DemandeCalculImpactEquipementPhysique + .builder() + .dateCalcul(dateCalcul) + .etape(ReferentielEtapeACV.builder().code(etapeACV).build()) + .critere(critere) + .equipementPhysique(equipementPhysique) + .correspondanceRefEquipement(refEquipement) + .typeEquipement(typeEquipement) + .impactEquipements(Collections.singletonList(referentielImpactEquipement)) + .build(); + + //When + var actual = calculImpactEquipementPhysiqueService.calculerImpactEquipementPhysique( + demandeCalcul + ); + + //Then + assertContentIndicateur(demandeCalcul, actual); + assertNull(actual.getImpactUnitaire()); + assertEquals("ERREUR", actual.getStatutIndicateur()); + assertEquals( + "{\"erreur\":\"ErrCalcFonc : Donnée de consommation electrique manquante : equipementPhysique : Serveur_103, RefEquipementCible : ref-Serveur3, RefEquipementParDefaut : ref-Serveur3\"}", + actual.getTrace() + ); + } + + @Test + void taiga918_ecranCAF5() { + + //Given + String etapeACV = "UTILISATION"; + ReferentielCritere critere = ReferentielCritere.builder() + .nomCritere("Changement Climatique") + .unite("kg CO_{2} eq") + .build(); + LocalDateTime dateCalcul = LocalDateTime.now(); + String refEquipementCible = "ref-Ecran"; + String refEquipementParDefaut = "ref-Ecran"; + EquipementPhysique equipementPhysique = EquipementPhysique.builder() + .dateLot(LocalDate.of(2022, 1, 1)) + .nomOrganisation("Test") + .nomLot("Test|20220101") + .nomSourceDonnee("Source_Test") + .type("Ecran") + .nomEquipementPhysique("Ecran_1") + .modele("ref-Ecran") + .paysDUtilisation("France") + .serveur(false) + .nomCourtDatacenter(null) + .dataCenter(null) + .quantite(1.0) + .consoElecAnnuelle(100.0) + .nbJourUtiliseAn(365.0) + .build(); + ReferentielMixElectrique referentielMixElectrique = ReferentielMixElectrique.builder() + .critere(critere.getNomCritere()) + .pays("France") + .raccourcisAnglais("FR") + .source("CAF5") + .valeur(0.08) + .build(); + ReferentielCorrespondanceRefEquipement refEquipement = ReferentielCorrespondanceRefEquipement.builder() + .refEquipementCible(refEquipementCible) + .modeleEquipementSource(equipementPhysique.getModele()) + .build(); + ReferentielTypeEquipement typeEquipement = ReferentielTypeEquipement.builder() + .type(equipementPhysique.getType()) + .refEquipementParDefaut(refEquipementParDefaut) + .dureeVieDefaut(null) + .build(); + DemandeCalculImpactEquipementPhysique demandeCalcul = DemandeCalculImpactEquipementPhysique + .builder() + .dateCalcul(dateCalcul) + .etape(ReferentielEtapeACV.builder().code(etapeACV).build()) + .critere(critere) + .equipementPhysique(equipementPhysique) + .correspondanceRefEquipement(refEquipement) + .typeEquipement(typeEquipement) + .mixElectriques(Collections.singletonList(referentielMixElectrique)) + .build(); + + //When + var actual = calculImpactEquipementPhysiqueService.calculerImpactEquipementPhysique( + demandeCalcul + ); + + //Then + assertContentIndicateur(demandeCalcul, actual); + assertEquals("OK", actual.getStatutIndicateur()); + assertNotNull(actual.getImpactUnitaire()); + assertEquals(dateCalcul, actual.getDateCalcul()); + double actualImpactUnitaireLimited = BigDecimal.valueOf(actual.getImpactUnitaire()).setScale(4, RoundingMode.DOWN).doubleValue(); + // quantité * equipementPhysique.consoElecAnnuelle * MixElec France + assertEquals(BigDecimal.valueOf(1.0 * 100.0 * 0.08).setScale(4, RoundingMode.DOWN).doubleValue(), actualImpactUnitaireLimited); + } + + @Test + void shouldCalculerImpactEquipementPhysique_CAF4() throws Exception { + + //Given + String etapeACV = "FABRICATION"; + ReferentielCritere critere = ReferentielCritere.builder() + .nomCritere("Changement Climatique") + .unite("kg CO_{2} eq") + .build(); + LocalDateTime dateCalcul = LocalDateTime.now(); + String refEquipementCible = "Ordinateur4"; + String refEquipementParDefaut = "Ordinateur4"; + EquipementPhysique equipementPhysique = EquipementPhysique.builder() + .dateLot(LocalDate.of(2022, 1, 1)) + .nomOrganisation("Test") + .nomLot("Test|20220101") + .nomSourceDonnee("Source_Test") + .type("Desktop") + .nomEquipementPhysique("Ordinateur4") + .modele("Ordinateur4") + .paysDUtilisation("France") + .quantite(1.0) + .nbJourUtiliseAn(null) + .build(); + ReferentielImpactEquipement referentielImpactEquipement = ReferentielImpactEquipement.builder() + .source("CAF4") + .critere(critere.getNomCritere()) + .etape(etapeACV) + .refEquipement(equipementPhysique.getModele()) + .consoElecMoyenne(null) + .valeur(142.0) + .build(); + ReferentielCorrespondanceRefEquipement refEquipement = ReferentielCorrespondanceRefEquipement.builder() + .refEquipementCible(refEquipementCible) + .modeleEquipementSource(equipementPhysique.getModele()) + .build(); + ReferentielTypeEquipement typeEquipement = ReferentielTypeEquipement.builder() + .type(equipementPhysique.getType()) + .refEquipementParDefaut(refEquipementParDefaut) + .dureeVieDefaut(null) + .build(); + DemandeCalculImpactEquipementPhysique demandeCalcul = DemandeCalculImpactEquipementPhysique + .builder() + .dateCalcul(dateCalcul) + .etape(ReferentielEtapeACV.builder().code(etapeACV).build()) + .critere(critere) + .equipementPhysique(equipementPhysique) + .correspondanceRefEquipement(refEquipement) + .typeEquipement(typeEquipement) + .impactEquipements(Collections.singletonList(referentielImpactEquipement)) + .build(); + + when(dureeDeVieEquipementPhysiqueService.calculerDureeVie(any(DemandeCalculImpactEquipementPhysique.class))).thenReturn(DureeDeVie.builder().valeur(5.0d).build()); + + //When + var actual = calculImpactEquipementPhysiqueService.calculerImpactEquipementPhysique( + demandeCalcul + ); + + //Then + assertContentIndicateur(demandeCalcul, actual); + assertEquals("OK", actual.getStatutIndicateur()); + assertEquals(dateCalcul, actual.getDateCalcul()); + assertNotNull(actual.getImpactUnitaire()); + double actualImpactUnitaireLimited = BigDecimal.valueOf(actual.getImpactUnitaire()).setScale(4, RoundingMode.DOWN).doubleValue(); + assertEquals(28.4, actualImpactUnitaireLimited); + verify(dureeDeVieEquipementPhysiqueService, times(1)).calculerDureeVie(demandeCalcul); + assertNull(actual.getConsoElecMoyenne()); + } + + @Test + void shouldCalculerImpactEquipementPhysique_CAF5() throws Exception { + + //Given + String etapeACV = "FIN_DE_VIE"; + ReferentielCritere critere = ReferentielCritere.builder() + .nomCritere("Changement Climatique") + .unite("kg CO_{2} eq") + .build(); + LocalDateTime dateCalcul = LocalDateTime.now(); + String refEquipementCible = "KyoceraTaskalfa3253Ci"; + String refEquipementParDefaut = "KyoceraTaskalfa3253Ci"; + EquipementPhysique equipementPhysique = EquipementPhysique.builder() + .dateLot(LocalDate.of(2022, 1, 1)) + .nomOrganisation("Test") + .nomLot("Test|20220101") + .nomSourceDonnee("Source_Test") + .type("Imprimante") + .nomEquipementPhysique("Imprimante5") + .modele("KyoceraTaskalfa3253Ci") + .paysDUtilisation("France") + .quantite(1.0) + .nbJourUtiliseAn(null) + .build(); + ReferentielImpactEquipement referentielImpactEquipement = ReferentielImpactEquipement.builder() + .source("CAF5") + .etape(etapeACV) + .critere(critere.getNomCritere()) + .refEquipement(equipementPhysique.getModele()) + .consoElecMoyenne(123.3) + .valeur(17.3) + .build(); + ReferentielCorrespondanceRefEquipement refEquipement = ReferentielCorrespondanceRefEquipement.builder() + .refEquipementCible(refEquipementCible) + .modeleEquipementSource(equipementPhysique.getModele()) + .build(); + ReferentielTypeEquipement typeEquipement = ReferentielTypeEquipement.builder() + .type(equipementPhysique.getType()) + .refEquipementParDefaut(refEquipementParDefaut) + .dureeVieDefaut(null) + .build(); + DemandeCalculImpactEquipementPhysique demandeCalcul = DemandeCalculImpactEquipementPhysique + .builder() + .dateCalcul(dateCalcul) + .etape(ReferentielEtapeACV.builder().code(etapeACV).build()) + .critere(critere) + .impactEquipements(Collections.singletonList(referentielImpactEquipement)) + .equipementPhysique(equipementPhysique) + .correspondanceRefEquipement(refEquipement) + .typeEquipement(typeEquipement) + .build(); + when(dureeDeVieEquipementPhysiqueService.calculerDureeVie(any(DemandeCalculImpactEquipementPhysique.class))).thenReturn(DureeDeVie.builder().valeur(6.0).build()); + + //When + var actual = calculImpactEquipementPhysiqueService.calculerImpactEquipementPhysique( + demandeCalcul + ); + + //Then + assertContentIndicateur(demandeCalcul, actual); + assertEquals("OK", actual.getStatutIndicateur()); + assertEquals(dateCalcul, actual.getDateCalcul()); + assertNotNull(actual.getImpactUnitaire()); + double actualImpactUnitaireLimited = BigDecimal.valueOf(actual.getImpactUnitaire()).setScale(4, RoundingMode.DOWN).doubleValue(); + assertEquals(2.8833, actualImpactUnitaireLimited); + verify(dureeDeVieEquipementPhysiqueService, times(1)).calculerDureeVie(demandeCalcul); + assertNull(actual.getConsoElecMoyenne()); + } + + @Test + void shouldCalculerImpactEquipementPhysique_CAF6() throws Exception { + + //Given + String etapeACV = "DISTRIBUTION"; + ReferentielCritere critere = ReferentielCritere.builder() + .nomCritere("Changement Climatique") + .unite("kg CO_{2} eq") + .build(); + LocalDateTime dateCalcul = LocalDateTime.now(); + String refEquipementCible = "Ecran6"; + String refEquipementParDefaut = "Ecran6"; + EquipementPhysique equipementPhysique = EquipementPhysique.builder() + .dateLot(LocalDate.of(2022, 1, 1)) + .nomOrganisation("Test") + .nomLot("Test|20220101") + .nomSourceDonnee("Source_Test") + .type("Ecran") + .nomEquipementPhysique("Ecran6") + .modele("Ecran6") + .paysDUtilisation("France") + .quantite(1.0) + .nbJourUtiliseAn(null) + .build(); + ReferentielImpactEquipement referentielImpactEquipement = ReferentielImpactEquipement.builder() + .source("CAF6") + .critere(critere.getNomCritere()) + .etape(etapeACV) + .refEquipement(equipementPhysique.getModele()) + .consoElecMoyenne(123.3) + .valeur(1.273) + .build(); + ReferentielCorrespondanceRefEquipement refEquipement = ReferentielCorrespondanceRefEquipement.builder() + .refEquipementCible(refEquipementCible) + .modeleEquipementSource(equipementPhysique.getModele()) + .build(); + ReferentielTypeEquipement typeEquipement = ReferentielTypeEquipement.builder() + .type(equipementPhysique.getType()) + .refEquipementParDefaut(refEquipementParDefaut) + .dureeVieDefaut(null) + .build(); + DemandeCalculImpactEquipementPhysique demandeCalcul = DemandeCalculImpactEquipementPhysique + .builder() + .dateCalcul(dateCalcul) + .etape(ReferentielEtapeACV.builder().code(etapeACV).build()) + .critere(critere) + .equipementPhysique(equipementPhysique) + .correspondanceRefEquipement(refEquipement) + .typeEquipement(typeEquipement) + .impactEquipements(Collections.singletonList(referentielImpactEquipement)) + .build(); + when(dureeDeVieEquipementPhysiqueService.calculerDureeVie(any(DemandeCalculImpactEquipementPhysique.class))).thenReturn(DureeDeVie.builder().valeur(7.0).build()); + + //When + var actual = calculImpactEquipementPhysiqueService.calculerImpactEquipementPhysique( + demandeCalcul + ); + + //Then + assertContentIndicateur(demandeCalcul, actual); + assertEquals("OK", actual.getStatutIndicateur()); + assertEquals(dateCalcul, actual.getDateCalcul()); + assertNotNull(actual.getImpactUnitaire()); + double actualImpactUnitaireLimited = BigDecimal.valueOf(actual.getImpactUnitaire()).setScale(4, RoundingMode.DOWN).doubleValue(); + assertEquals(0.1818, actualImpactUnitaireLimited); + verify(dureeDeVieEquipementPhysiqueService, times(1)).calculerDureeVie(demandeCalcul); + assertNull(actual.getConsoElecMoyenne()); + } + + @Test + void shouldCalculerImpactEquipementPhysique_whenEtapeACVUTILISATIONAndEquipementConsoElecMoyenneFilled() throws Exception { + + //Given + String etapeACV = "UTILISATION"; + ReferentielCritere critere = ReferentielCritere.builder() + .nomCritere("Changement Climatique") + .unite("kg CO_{2} eq") + .build(); + LocalDateTime dateCalcul = LocalDateTime.now(); + String refEquipementCible = "Apple Iphone 11"; + String refEquipementParDefaut = "Apple Iphone 11"; + EquipementPhysique equipementPhysique = EquipementPhysique.builder() + .dateLot(LocalDate.of(2022, 1, 1)) + .nomOrganisation("Test") + .nomLot("Test|20220101") + .nomSourceDonnee("Source_Test") + .type("Téléphone") + .nomEquipementPhysique("Apple Iphone 11") + .modele("Apple Iphone 11") + .paysDUtilisation("France") + .consoElecAnnuelle(0.09) + .quantite(1.0) + .nbJourUtiliseAn(216.0) + .build(); + ReferentielMixElectrique referentielMixElectrique = ReferentielMixElectrique.builder() + .critere(critere.getNomCritere()) + .pays("France") + .raccourcisAnglais("FR") + .source("CAF1") + .valeur(0.0813225) + .build(); + ReferentielCorrespondanceRefEquipement refEquipement = ReferentielCorrespondanceRefEquipement.builder() + .refEquipementCible(refEquipementCible) + .modeleEquipementSource(equipementPhysique.getModele()) + .build(); + ReferentielTypeEquipement typeEquipement = ReferentielTypeEquipement.builder() + .type(equipementPhysique.getType()) + .refEquipementParDefaut(refEquipementParDefaut) + .dureeVieDefaut(null) + .build(); + DemandeCalculImpactEquipementPhysique demandeCalcul = DemandeCalculImpactEquipementPhysique + .builder() + .dateCalcul(dateCalcul) + .etape(ReferentielEtapeACV.builder().code(etapeACV).build()) + .critere(critere) + .equipementPhysique(equipementPhysique) + .correspondanceRefEquipement(refEquipement) + .typeEquipement(typeEquipement) + .mixElectriques(Collections.singletonList(referentielMixElectrique)) + .build(); + + //When + var actual = calculImpactEquipementPhysiqueService.calculerImpactEquipementPhysique( + demandeCalcul + ); + + //Then + assertContentIndicateur(demandeCalcul, actual); + assertEquals("OK", actual.getStatutIndicateur()); + assertEquals(dateCalcul, actual.getDateCalcul()); + assertNotNull(actual.getImpactUnitaire()); + double actualImpactUnitaireLimited = BigDecimal.valueOf(actual.getImpactUnitaire()).setScale(4, RoundingMode.DOWN).doubleValue(); + assertEquals(0.0073, actualImpactUnitaireLimited); + assertEquals(equipementPhysique.getConsoElecAnnuelle(), actual.getConsoElecMoyenne()); + } + + @Test + void shouldBuildTraceWithFirstFormulaScenario1() throws JsonProcessingException { + + //GIVEN + String etapeACV = "UTILISATION"; + ReferentielCritere critere = ReferentielCritere.builder().nomCritere("Changement Climatique").unite("CO2").build(); + String refEquipementCible = "Samsung SMP"; + String refEquipementParDefaut = "Samsung SMP"; + EquipementPhysique equipementPhysique = EquipementPhysique.builder() + .dateLot(LocalDate.of(2022, 1, 1)) + .nomOrganisation("Test") + .nomLot("Test|20220101") + .nomSourceDonnee("Source_Test") + .nomEquipementPhysique("Samsung S21") + .modele("Samsung SMP") + .consoElecAnnuelle(200d) + .paysDUtilisation("France") + .nbJourUtiliseAn(230d) + .serveur(false) + .quantite(3d) + .build(); + + ReferentielMixElectrique referentielMixElectrique = ReferentielMixElectrique.builder() + .critere(critere.getNomCritere()) + .pays("France") + .raccourcisAnglais("FR") + .source("Source-Mix") + .valeur(150d) + .build(); + ReferentielCorrespondanceRefEquipement refEquipement = ReferentielCorrespondanceRefEquipement.builder() + .refEquipementCible(refEquipementCible) + .modeleEquipementSource(equipementPhysique.getModele()) + .build(); + ReferentielTypeEquipement typeEquipement = ReferentielTypeEquipement.builder() + .type(equipementPhysique.getType()) + .refEquipementParDefaut(refEquipementParDefaut) + .dureeVieDefaut(null) + .build(); + DemandeCalculImpactEquipementPhysique demandeCalcul = DemandeCalculImpactEquipementPhysique + .builder() + .dateCalcul(LocalDateTime.now()) + .etape(ReferentielEtapeACV.builder().code(etapeACV).build()) + .critere(critere) + .equipementPhysique(equipementPhysique) + .correspondanceRefEquipement(refEquipement) + .typeEquipement(typeEquipement) + .mixElectriques(Collections.singletonList(referentielMixElectrique)) + .build(); + + var formule = TraceCalculImpactEquipementPhysiqueUtils.getFormulePremierScenario(3d, 200d, 150d, 1.0); + + var consoElecAnMoyenne = ConsoElecAnMoyenne.builder().valeurEquipementConsoElecAnnuelle(200d).valeur(200d).build(); + var mixElectrique = MixElectrique.builder().valeur(150d).valeurReferentielMixElectrique(150d).sourceReferentielMixElectrique("Source-Mix").build(); + TraceCalculImpactEquipementPhysique trace = TraceCalculImpactEquipementPhysique.builder() + .formule(formule) + .consoElecAnMoyenne(consoElecAnMoyenne) + .mixElectrique(mixElectrique) + .build(); + var expected = objectMapper.writeValueAsString(trace); + //WHEN + var actual = calculImpactEquipementPhysiqueService.calculerImpactEquipementPhysique(demandeCalcul); + //THEN + assertEquals(expected, actual.getTrace()); + } + + @Test + void shouldBuildTraceWithFirstFormulaScenario2() throws JsonProcessingException { + String etapeACV = "UTILISATION"; + ReferentielCritere critere = ReferentielCritere.builder().nomCritere("Changement Climatique").unite("CO2").build(); + DataCenter dataCenter = DataCenter.builder() + .localisation("Spain") + .pue(5d) + .build(); + String refEquipementCible = "IBM E1080"; + String refEquipementParDefaut = "IBM E1080"; + EquipementPhysique equipementPhysique = EquipementPhysique.builder() + .dateLot(LocalDate.of(2022, 1, 1)) + .nomOrganisation("Test") + .nomLot("Test|20220101") + .nomSourceDonnee("Source_Test") + .quantite(6d) + .modele("IBM E1080") + .serveur(true) + .dataCenter(dataCenter) + .build(); + ReferentielImpactEquipement referentielImpactEquipement = ReferentielImpactEquipement.builder() + .critere(critere.getNomCritere()) + .etape(etapeACV) + .refEquipement("IBM E1080") + .source("source-RefEquipement") + .consoElecMoyenne(130d) + .build(); + ReferentielMixElectrique referentielMixElectrique = ReferentielMixElectrique.builder() + .critere(critere.getNomCritere()) + .pays("Spain") + .source("Source-Mix") + .valeur(25d) + .build(); + + ReferentielCorrespondanceRefEquipement refEquipement = ReferentielCorrespondanceRefEquipement.builder() + .refEquipementCible(refEquipementCible) + .modeleEquipementSource(equipementPhysique.getModele()) + .build(); + ReferentielTypeEquipement typeEquipement = ReferentielTypeEquipement.builder() + .type(equipementPhysique.getType()) + .refEquipementParDefaut(refEquipementParDefaut) + .dureeVieDefaut(null) + .build(); + DemandeCalculImpactEquipementPhysique demandeCalcul = DemandeCalculImpactEquipementPhysique + .builder() + .dateCalcul(LocalDateTime.now()) + .etape(ReferentielEtapeACV.builder().code(etapeACV).build()) + .critere(critere) + .equipementPhysique(equipementPhysique) + .correspondanceRefEquipement(refEquipement) + .typeEquipement(typeEquipement) + .impactEquipements(Collections.singletonList(referentielImpactEquipement)) + .mixElectriques(Collections.singletonList(referentielMixElectrique)) + .build(); + + var formule = TraceCalculImpactEquipementPhysiqueUtils.getFormulePremierScenario(6d, 130d, 5d * 25d, 1.0); + + var consoElecAnMoyenne = ConsoElecAnMoyenne.builder() + .valeur(130d) + .valeurReferentielConsoElecMoyenne(130d) + .sourceReferentielImpactEquipement("source-RefEquipement") + .build(); + var mixElectrique = MixElectrique.builder() + .valeur(125d) + .dataCenterPue(5d) + .valeurReferentielMixElectrique(25d) + .sourceReferentielMixElectrique("Source-Mix") + .isServeur(true) + .build(); + TraceCalculImpactEquipementPhysique trace = TraceCalculImpactEquipementPhysique.builder() + .formule(formule) + .consoElecAnMoyenne(consoElecAnMoyenne) + .mixElectrique(mixElectrique) + .build(); + var expected = objectMapper.writeValueAsString(trace); + //WHEN + var actual = calculImpactEquipementPhysiqueService.calculerImpactEquipementPhysique(demandeCalcul); + //THEN + assertEquals(expected, actual.getTrace()); + } + + @Test + void shouldBuildTraceWithSecondFormula() throws JsonProcessingException, CalculImpactException { + String etapeACV = "Fin de vie"; + ReferentielCritere critere = ReferentielCritere.builder().nomCritere("Changement Climatique").unite("CO2").build(); + LocalDate dateAchat = LocalDate.of(2020, 1, 22); + LocalDate dateRetrait = LocalDate.of(2021, 5, 12); + double quantite = 6d; + double tauxUtilisation = 1.0; + String refEquipementCible = "IBM E1080"; + String refEquipementParDefaut = "IBM E1080"; + EquipementPhysique equipementPhysique = EquipementPhysique.builder() + .dateLot(LocalDate.of(2022, 1, 1)) + .nomOrganisation("Test") + .nomLot("Test|20220101") + .nomSourceDonnee("Source_Test") + .quantite(quantite) + .modele("IBM E1080") + .dateAchat(dateAchat) + .dateRetrait(dateRetrait) + .serveur(false) + .build(); + double valeurRefrentiel = 100d; + String sourceReferentielImpactEquipement = "source-RefEquipement"; + ReferentielImpactEquipement referentielImpactEquipement = ReferentielImpactEquipement.builder() + .critere(critere.getNomCritere()) + .etape(etapeACV) + .refEquipement("IBM E1080") + .source(sourceReferentielImpactEquipement) + .consoElecMoyenne(130d) + .valeur(valeurRefrentiel) + .build(); + + + ReferentielCorrespondanceRefEquipement refEquipement = ReferentielCorrespondanceRefEquipement.builder() + .refEquipementCible(refEquipementCible) + .modeleEquipementSource(equipementPhysique.getModele()) + .build(); + ReferentielTypeEquipement typeEquipement = ReferentielTypeEquipement.builder() + .type(equipementPhysique.getType()) + .refEquipementParDefaut(refEquipementParDefaut) + .dureeVieDefaut(null) + .build(); + DemandeCalculImpactEquipementPhysique demandeCalcul = DemandeCalculImpactEquipementPhysique + .builder() + .dateCalcul(LocalDateTime.now()) + .etape(ReferentielEtapeACV.builder().code(etapeACV).build()) + .critere(critere) + .equipementPhysique(equipementPhysique) + .correspondanceRefEquipement(refEquipement) + .typeEquipement(typeEquipement) + .impactEquipements(Collections.singletonList(referentielImpactEquipement)) + .build(); + + + when(dureeDeVieEquipementPhysiqueService.calculerDureeVie(demandeCalcul)).thenCallRealMethod(); + + var valeurDureeVie = ChronoUnit.DAYS.between(dateAchat, dateRetrait) / 365d; + DureeDeVie dureeDeVie = DureeDeVie.builder() + .valeur(valeurDureeVie) + .dateAchat(dateAchat.format(DateTimeFormatter.ISO_DATE)) + .dateRetrait(dateRetrait.format(DateTimeFormatter.ISO_DATE)) + .build(); + + var formule = TraceCalculImpactEquipementPhysiqueUtils.getFormuleSecondScenario(quantite, valeurRefrentiel, valeurDureeVie, tauxUtilisation); + + TraceCalculImpactEquipementPhysique trace = TraceCalculImpactEquipementPhysique.builder() + .formule(formule) + .dureeDeVie(dureeDeVie) + .valeurReferentielImpactEquipement(valeurRefrentiel) + .sourceReferentielImpactEquipement(sourceReferentielImpactEquipement) + .build(); + var expected = objectMapper.writeValueAsString(trace); + //WHEN + var actual = calculImpactEquipementPhysiqueService.calculerImpactEquipementPhysique(demandeCalcul); + + //THEN + assertEquals(expected, actual.getTrace()); + } + + @Test + void taiga862_CAF1_calculImpactEquipementPhysiqueWithoutCorrespondanceShouldUseRefEquipementParDefaut() throws Exception { + + //Given + String etapeACV = "DISTRIBUTION"; + ReferentielCritere critere = ReferentielCritere.builder() + .nomCritere("Changement Climatique") + .unite("kg CO_{2} eq") + .build(); + LocalDateTime dateCalcul = LocalDateTime.now(); + String refEquipementParDefaut = "EcranParDefaut"; + EquipementPhysique equipementPhysique = EquipementPhysique.builder() + .dateLot(LocalDate.of(2022, 1, 1)) + .nomOrganisation("Test") + .nomLot("Test|20220101") + .nomSourceDonnee("Source_Test") + .type("Ecran") + .nomEquipementPhysique("Ecran6") + .modele("Ecran6") + .paysDUtilisation("France") + .quantite(1.0) + .nbJourUtiliseAn(null) + .build(); + ReferentielImpactEquipement referentielImpactEquipement = ReferentielImpactEquipement.builder() + .critere(critere.getNomCritere()) + .etape(etapeACV) + .source("CAF6") + .refEquipement(refEquipementParDefaut) + .consoElecMoyenne(123.3) + .valeur(1.273) + .build(); + + ReferentielCorrespondanceRefEquipement refEquipement = null; + ReferentielTypeEquipement typeEquipement = ReferentielTypeEquipement.builder() + .type(equipementPhysique.getType()) + .refEquipementParDefaut(refEquipementParDefaut) + .dureeVieDefaut(null) + .build(); + DemandeCalculImpactEquipementPhysique demandeCalcul = DemandeCalculImpactEquipementPhysique + .builder() + .dateCalcul(dateCalcul) + .etape(ReferentielEtapeACV.builder().code(etapeACV).build()) + .critere(critere) + .equipementPhysique(equipementPhysique) + .correspondanceRefEquipement(refEquipement) + .typeEquipement(typeEquipement) + .impactEquipements(Collections.singletonList(referentielImpactEquipement)) + .build(); + + when(dureeDeVieEquipementPhysiqueService.calculerDureeVie(any(DemandeCalculImpactEquipementPhysique.class))).thenReturn(DureeDeVie.builder().valeur(7.0).build()); + + //When + var actual = calculImpactEquipementPhysiqueService.calculerImpactEquipementPhysique( + demandeCalcul + ); + + //Then + assertContentIndicateur(demandeCalcul, actual); + assertEquals("OK", actual.getStatutIndicateur()); + assertEquals(dateCalcul, actual.getDateCalcul()); + assertNotNull(actual.getImpactUnitaire()); + double actualImpactUnitaireLimited = BigDecimal.valueOf(actual.getImpactUnitaire()).setScale(4, RoundingMode.DOWN).doubleValue(); + assertEquals(0.1818, actualImpactUnitaireLimited); + verify(dureeDeVieEquipementPhysiqueService, times(1)).calculerDureeVie(demandeCalcul); + assertNull(actual.getConsoElecMoyenne()); + } + + @Test + void taiga862_CAF1_calculImpactEquipementPhysiqueWithoutImpactForCorrespondanceShouldUseRefEquipementParDefaut() throws Exception { + + //Given + String etapeACV = "DISTRIBUTION"; + ReferentielCritere critere = ReferentielCritere.builder() + .nomCritere("Changement Climatique") + .unite("kg CO_{2} eq") + .build(); + LocalDateTime dateCalcul = LocalDateTime.now(); + String refEquipementParDefaut = "EcranParDefaut"; + ReferentielImpactEquipement referentielImpactEquipement = ReferentielImpactEquipement.builder() + .critere(critere.getNomCritere()) + .etape(etapeACV) + .source("CAF6") + .refEquipement(refEquipementParDefaut) + .consoElecMoyenne(123.3) + .valeur(1.273) + .build(); + String refEquipementCible = "Ecran6"; + EquipementPhysique equipementPhysique = EquipementPhysique.builder() + .dateLot(LocalDate.of(2022, 1, 1)) + .nomOrganisation("Test") + .nomLot("Test|20220101") + .nomSourceDonnee("Source_Test") + .type("Ecran") + .nomEquipementPhysique("Ecran6") + .modele("Ecran6") + .paysDUtilisation("France") + .quantite(1.0) + .nbJourUtiliseAn(null) + .build(); + + ReferentielCorrespondanceRefEquipement refEquipement = ReferentielCorrespondanceRefEquipement.builder() + .refEquipementCible(refEquipementCible) + .modeleEquipementSource(equipementPhysique.getModele()) + .build(); + ReferentielTypeEquipement typeEquipement = ReferentielTypeEquipement.builder() + .type(equipementPhysique.getType()) + .refEquipementParDefaut(refEquipementParDefaut) + .dureeVieDefaut(null) + .build(); + DemandeCalculImpactEquipementPhysique demandeCalcul = DemandeCalculImpactEquipementPhysique + .builder() + .dateCalcul(dateCalcul) + .etape(ReferentielEtapeACV.builder().code(etapeACV).build()) + .critere(critere) + .equipementPhysique(equipementPhysique) + .correspondanceRefEquipement(refEquipement) + .typeEquipement(typeEquipement) + .impactEquipements(Collections.singletonList(referentielImpactEquipement)) + .build(); + + when(dureeDeVieEquipementPhysiqueService.calculerDureeVie(any(DemandeCalculImpactEquipementPhysique.class))).thenReturn(DureeDeVie.builder().valeur(7.0).build()); + + //When + var actual = calculImpactEquipementPhysiqueService.calculerImpactEquipementPhysique( + demandeCalcul + ); + + //Then + assertContentIndicateur(demandeCalcul, actual); + assertEquals("OK", actual.getStatutIndicateur()); + assertEquals(dateCalcul, actual.getDateCalcul()); + assertNotNull(actual.getImpactUnitaire()); + double actualImpactUnitaireLimited = BigDecimal.valueOf(actual.getImpactUnitaire()).setScale(4, RoundingMode.DOWN).doubleValue(); + assertEquals(0.1818, actualImpactUnitaireLimited); + verify(dureeDeVieEquipementPhysiqueService, times(1)).calculerDureeVie(demandeCalcul); + assertNull(actual.getConsoElecMoyenne()); + } + + @Test + void tech_whenDureeDeVieUnknown_shouldReturnIndicateurWithError() { + + //Given + String etapeACV = "FIN_DE_VIE"; + ReferentielCritere critere = ReferentielCritere.builder() + .nomCritere("Changement Climatique") + .unite("kg CO_{2} eq") + .build(); + DataCenter dataCenter = DataCenter.builder() + .nomCourtDatacenter("sequoia2") + .nomLongDatacenter("sequoia2") + .localisation("France") + .pue(1.43) + .build(); + LocalDateTime dateCalcul = LocalDateTime.now(); + String refEquipementCible = "ref-Serveur3"; + String refEquipementParDefaut = "ref-Serveur3"; + EquipementPhysique equipementPhysique = EquipementPhysique.builder() + .dateLot(LocalDate.of(2022, 1, 1)) + .nomOrganisation("Test") + .nomLot("Test|20220101") + .nomSourceDonnee("Source_Test") + .type("Serveur") + .nomEquipementPhysique("Serveur_103") + .modele("ref-Serveur3") + .paysDUtilisation("France") + .serveur(true) + .nomCourtDatacenter("sequoia2") + .dataCenter(dataCenter) + .consoElecAnnuelle(null) + .quantite(1.0) + .nbJourUtiliseAn(365.0) + .build(); + ReferentielImpactEquipement referentielImpactEquipement = ReferentielImpactEquipement.builder() + .source("CAF4") + .critere(critere.getNomCritere()) + .etape(etapeACV) + .refEquipement(equipementPhysique.getModele()) + .consoElecMoyenne(100.0) + .valeur(1139.839200000007) + .build(); + ReferentielCorrespondanceRefEquipement refEquipement = ReferentielCorrespondanceRefEquipement.builder() + .refEquipementCible(refEquipementCible) + .modeleEquipementSource(equipementPhysique.getModele()) + .build(); + ReferentielTypeEquipement typeEquipement = ReferentielTypeEquipement.builder() + .type(equipementPhysique.getType()) + .refEquipementParDefaut(refEquipementParDefaut) + .dureeVieDefaut(null) + .build(); + DemandeCalculImpactEquipementPhysique demandeCalcul = DemandeCalculImpactEquipementPhysique + .builder() + .dateCalcul(dateCalcul) + .etape(ReferentielEtapeACV.builder().code(etapeACV).build()) + .critere(critere) + .equipementPhysique(equipementPhysique) + .correspondanceRefEquipement(refEquipement) + .typeEquipement(typeEquipement) + .impactEquipements(Collections.singletonList(referentielImpactEquipement)) + .build(); + + //When + var actual = calculImpactEquipementPhysiqueService.calculerImpactEquipementPhysique( + demandeCalcul + ); + + //Then + assertContentIndicateur(demandeCalcul, actual); + assertNull(actual.getImpactUnitaire()); + assertEquals("ERREUR", actual.getStatutIndicateur()); + assertEquals( + "{\"erreur\":\"ErrCalcFonc : Durée de vie de l'équipement inconnue\"}", + actual.getTrace() + ); + } + + @Test + void verifyErrorWhenCallConsoElecAnMoyenneWithoutConsoInEntryWithImpactRefWithoutConso() { + + //Given + String etapeACV = "UTILISATION"; + ReferentielCritere critere = ReferentielCritere.builder() + .nomCritere("Changement Climatique") + .unite("kg CO_{2} eq") + .build(); + LocalDateTime dateCalcul = LocalDateTime.now(); + String refEquipementCible = "Apple Iphone 11"; + String refEquipementParDefaut = "Apple Iphone 11"; + EquipementPhysique equipementPhysique = EquipementPhysique.builder() + .dateLot(LocalDate.of(2022, 1, 1)) + .nomOrganisation("Test") + .nomLot("Test|20220101") + .nomSourceDonnee("Source_Test") + .type("Téléphone") + .nomEquipementPhysique("Apple Iphone 11") + .modele("Apple Iphone 11") + .paysDUtilisation("France") + .quantite(1.0) + .nbJourUtiliseAn(216.0) + .build(); + ReferentielMixElectrique referentielMixElectrique = ReferentielMixElectrique.builder() + .critere(critere.getNomCritere()) + .pays("France") + .raccourcisAnglais("FR") + .source("CAF5") + .valeur(0.08) + .build(); + ReferentielImpactEquipement referentielImpactEquipement = ReferentielImpactEquipement.builder() + .source("CAF4") + .critere(critere.getNomCritere()) + .etape(etapeACV) + .refEquipement(equipementPhysique.getModele()) + .consoElecMoyenne(null) + .valeur(1139.839200000007) + .build(); + ReferentielCorrespondanceRefEquipement refEquipement = ReferentielCorrespondanceRefEquipement.builder() + .refEquipementCible(refEquipementCible) + .modeleEquipementSource(equipementPhysique.getModele()) + .build(); + ReferentielTypeEquipement typeEquipement = ReferentielTypeEquipement.builder() + .type(equipementPhysique.getType()) + .refEquipementParDefaut(refEquipementParDefaut) + .dureeVieDefaut(null) + .build(); + DemandeCalculImpactEquipementPhysique demandeCalcul = DemandeCalculImpactEquipementPhysique + .builder() + .dateCalcul(dateCalcul) + .etape(ReferentielEtapeACV.builder().code(etapeACV).build()) + .critere(critere) + .equipementPhysique(equipementPhysique) + .correspondanceRefEquipement(refEquipement) + .typeEquipement(typeEquipement) + .mixElectriques(Collections.singletonList(referentielMixElectrique)) + .impactEquipements(Collections.singletonList(referentielImpactEquipement)) + .build(); + + //When + var actual = calculImpactEquipementPhysiqueService.calculerImpactEquipementPhysique( + demandeCalcul + ); + + //Then + assertContentIndicateur(demandeCalcul, actual); + assertNull(actual.getImpactUnitaire()); + assertEquals("ERREUR", actual.getStatutIndicateur()); + assertEquals( + "{\"erreur\":\"ErrCalcFonc : Donnée de consommation electrique manquante : equipementPhysique : Apple Iphone 11, RefEquipementCible : Apple Iphone 11, RefEquipementParDefaut : Apple Iphone 11\"}", + actual.getTrace() + ); + } + + @Test + void verifyErrorWhenCallConsoElecAnMoyenneWithoutConsoInEntryAndNoImpactRef() { + + //Given + String etapeACV = "UTILISATION"; + ReferentielCritere critere = ReferentielCritere.builder() + .nomCritere("Changement Climatique") + .unite("kg CO_{2} eq") + .build(); + LocalDateTime dateCalcul = LocalDateTime.now(); + String refEquipementParDefaut = "EcranParDefaut"; + EquipementPhysique equipementPhysique = EquipementPhysique.builder() + .dateLot(LocalDate.of(2022, 1, 1)) + .nomOrganisation("Test") + .nomLot("Test|20220101") + .nomSourceDonnee("Source_Test") + .type("Ecran") + .nomEquipementPhysique("Ecran6") + .modele("Ecran6") + .paysDUtilisation("France") + .quantite(1.0) + .nbJourUtiliseAn(null) + .build(); + DemandeCalculImpactEquipementPhysique demandeCalcul = DemandeCalculImpactEquipementPhysique + .builder() + .dateCalcul(dateCalcul) + .etape(ReferentielEtapeACV.builder().code(etapeACV).build()) + .critere(critere) + .equipementPhysique(equipementPhysique) + .build(); + //When + var actual = calculImpactEquipementPhysiqueService.calculerImpactEquipementPhysique( + demandeCalcul + ); + + //Then + assertContentIndicateur(demandeCalcul, actual); + assertNull(actual.getImpactUnitaire()); + assertEquals("ERREUR", actual.getStatutIndicateur()); + assertEquals( + "{\"erreur\":\"ErrCalcFonc : Référentiel Impact Equipement inconnu\"}", + actual.getTrace() + ); + } + + @Test + void tech_whenNoReferentielImpactEquipement_shouldReturnIndicateurWithError() throws CalculImpactException { + //Given + String etapeACV = "FIN_DE_VIE"; + ReferentielCritere critere = ReferentielCritere.builder() + .nomCritere("Changement Climatique") + .unite("kg CO_{2} eq") + .build(); + DataCenter dataCenter = DataCenter.builder() + .nomCourtDatacenter("sequoia2") + .nomLongDatacenter("sequoia2") + .localisation("France") + .pue(1.43) + .build(); + LocalDateTime dateCalcul = LocalDateTime.now(); + String refEquipementCible = "ref-Serveur3"; + String refEquipementParDefaut = "ref-Serveur3"; + EquipementPhysique equipementPhysique = EquipementPhysique.builder() + .dateLot(LocalDate.of(2022, 1, 1)) + .nomOrganisation("Test") + .nomLot("Test|20220101") + .nomSourceDonnee("Source_Test") + .type("Serveur") + .nomEquipementPhysique("Serveur_103") + .modele("ref-Serveur3") + .paysDUtilisation("France") + .serveur(true) + .nomCourtDatacenter("sequoia2") + .dataCenter(dataCenter) + .consoElecAnnuelle(null) + .quantite(1.0) + .nbJourUtiliseAn(365.0) + .build(); + ReferentielCorrespondanceRefEquipement refEquipement = ReferentielCorrespondanceRefEquipement.builder() + .refEquipementCible(refEquipementCible) + .modeleEquipementSource(equipementPhysique.getModele()) + .build(); + ReferentielTypeEquipement typeEquipement = ReferentielTypeEquipement.builder() + .type(equipementPhysique.getType()) + .refEquipementParDefaut(refEquipementParDefaut) + .dureeVieDefaut(6.0) + .build(); + DemandeCalculImpactEquipementPhysique demandeCalcul = DemandeCalculImpactEquipementPhysique + .builder() + .dateCalcul(dateCalcul) + .etape(ReferentielEtapeACV.builder().code(etapeACV).build()) + .critere(critere) + .equipementPhysique(equipementPhysique) + .correspondanceRefEquipement(refEquipement) + .typeEquipement(typeEquipement) + .impactEquipements(Collections.emptyList()) + .build(); + + //When + var actual = calculImpactEquipementPhysiqueService.calculerImpactEquipementPhysique( + demandeCalcul + ); + + //Then + assertContentIndicateur(demandeCalcul, actual); + assertNull(actual.getImpactUnitaire()); + assertEquals("ERREUR", actual.getStatutIndicateur()); + assertEquals( + "{\"erreur\":\"ErrCalcFonc : Référentiel Impact Equipement inconnu\"}", + actual.getTrace() + ); + } + + @Test + void tech_whenMixElectriqueNotFound_shouldReturnIndicateurWithError() throws Exception { + + //Given + String etapeACV = "UTILISATION"; + ReferentielCritere critere = ReferentielCritere.builder() + .nomCritere("Changement Climatique") + .unite("kg CO_{2} eq") + .build(); + LocalDateTime dateCalcul = LocalDateTime.now(); + String refEquipementCible = "Apple Iphone 11"; + String refEquipementParDefaut = "Apple Iphone 11"; + EquipementPhysique equipementPhysique = EquipementPhysique.builder() + .dateLot(LocalDate.of(2022, 1, 1)) + .nomOrganisation("Test") + .nomLot("Test|20220101") + .nomSourceDonnee("Source_Test") + .type("Téléphone") + .nomEquipementPhysique("Apple Iphone 11") + .modele("Apple Iphone 11") + .paysDUtilisation("France") + .consoElecAnnuelle(0.09) + .quantite(1.0) + .nbJourUtiliseAn(216.0) + .build(); + ReferentielCorrespondanceRefEquipement refEquipement = ReferentielCorrespondanceRefEquipement.builder() + .refEquipementCible(refEquipementCible) + .modeleEquipementSource(equipementPhysique.getModele()) + .build(); + ReferentielTypeEquipement typeEquipement = ReferentielTypeEquipement.builder() + .type(equipementPhysique.getType()) + .refEquipementParDefaut(refEquipementParDefaut) + .dureeVieDefaut(null) + .build(); + DemandeCalculImpactEquipementPhysique demandeCalcul = DemandeCalculImpactEquipementPhysique + .builder() + .dateCalcul(dateCalcul) + .etape(ReferentielEtapeACV.builder().code(etapeACV).build()) + .critere(critere) + .equipementPhysique(equipementPhysique) + .correspondanceRefEquipement(refEquipement) + .typeEquipement(typeEquipement) + .mixElectriques(Collections.emptyList()) + .build(); + + //When + var actual = calculImpactEquipementPhysiqueService.calculerImpactEquipementPhysique( + demandeCalcul + ); + + //Then + assertContentIndicateur(demandeCalcul, actual); + assertNull(actual.getImpactUnitaire()); + assertEquals("ERREUR", actual.getStatutIndicateur()); + assertEquals( + "{\"erreur\":\"ErrCalcFonc : Il n'existe pas de Mix électrique pour cet équipement : critere: Changement Climatique - equipement: Apple Iphone 11 - Pays d'utilisation: France - NomCourtDatacenter: null\"}", + actual.getTrace() + ); + } + + @Test + void tech_whenMixElectriqueNotFoundWithDataCenter_shouldReturnIndicateurWithError() throws Exception { + + //Given + String etapeACV = "UTILISATION"; + ReferentielCritere critere = ReferentielCritere.builder() + .nomCritere("Changement Climatique") + .unite("kg CO_{2} eq") + .build(); + LocalDateTime dateCalcul = LocalDateTime.now(); + String refEquipementCible = "IBM X96"; + String refEquipementParDefaut = "IBM X96"; + EquipementPhysique equipementPhysique = EquipementPhysique.builder() + .dateLot(LocalDate.of(2022, 1, 1)) + .nomOrganisation("Test") + .nomLot("Test|20220101") + .nomSourceDonnee("Source_Test") + .type("Serveur") + .nomEquipementPhysique("IBM X96") + .modele("IBM X96") + .paysDUtilisation("France") + .consoElecAnnuelle(0.09) + .quantite(1.0) + .nbJourUtiliseAn(216.0) + .nomCourtDatacenter("TanaMana 2") + .dataCenter( + DataCenter.builder() + .nomCourtDatacenter("TanaMana 2") + .nomLongDatacenter("TanaMana 2 - Rack B") + .localisation("France") + .pue(6.2) + .build() + ) + .build(); + ReferentielCorrespondanceRefEquipement refEquipement = ReferentielCorrespondanceRefEquipement.builder() + .refEquipementCible(refEquipementCible) + .modeleEquipementSource(equipementPhysique.getModele()) + .build(); + ReferentielTypeEquipement typeEquipement = ReferentielTypeEquipement.builder() + .type(equipementPhysique.getType()) + .refEquipementParDefaut(refEquipementParDefaut) + .dureeVieDefaut(null) + .build(); + DemandeCalculImpactEquipementPhysique demandeCalcul = DemandeCalculImpactEquipementPhysique + .builder() + .dateCalcul(dateCalcul) + .etape(ReferentielEtapeACV.builder().code(etapeACV).build()) + .critere(critere) + .equipementPhysique(equipementPhysique) + .correspondanceRefEquipement(refEquipement) + .typeEquipement(typeEquipement) + .mixElectriques(Collections.emptyList()) + .build(); + + //When + var actual = calculImpactEquipementPhysiqueService.calculerImpactEquipementPhysique( + demandeCalcul + ); + + //Then + assertContentIndicateur(demandeCalcul, actual); + assertNull(actual.getImpactUnitaire()); + assertEquals("ERREUR", actual.getStatutIndicateur()); + assertEquals( + "{\"erreur\":\"ErrCalcFonc : Il n'existe pas de Mix électrique pour cet équipement : critere: Changement Climatique - equipement: IBM X96 - Pays d'utilisation: France - NomCourtDatacenter: TanaMana 2\"}", + actual.getTrace() + ); + } + + @Test + void tech_whenPUEUknown_shouldReturnIndicateurWithError() throws Exception { + + //Given + String etapeACV = "UTILISATION"; + ReferentielCritere critere = ReferentielCritere.builder() + .nomCritere("Changement Climatique") + .unite("kg CO_{2} eq") + .build(); + LocalDateTime dateCalcul = LocalDateTime.now(); + String refEquipementCible = "IBM X96"; + String refEquipementParDefaut = "IBM X96"; + EquipementPhysique equipementPhysique = EquipementPhysique.builder() + .dateLot(LocalDate.of(2022, 1, 1)) + .nomOrganisation("Test") + .nomLot("Test|20220101") + .nomSourceDonnee("Source_Test") + .type("Serveur") + .nomEquipementPhysique("Tanabana2-IBM X96") + .modele("IBM X96") + .paysDUtilisation("France") + .consoElecAnnuelle(0.09) + .quantite(1.0) + .nbJourUtiliseAn(216.0) + .nomCourtDatacenter("TanaMana 2") + .dataCenter( + DataCenter.builder() + .nomCourtDatacenter("TanaMana 2") + .nomLongDatacenter("TanaMana 2 - Rack B") + .localisation("France") + .pue(null) + .build() + ) + .build(); + ReferentielCorrespondanceRefEquipement refEquipement = ReferentielCorrespondanceRefEquipement.builder() + .refEquipementCible(refEquipementCible) + .modeleEquipementSource(equipementPhysique.getModele()) + .build(); + ReferentielTypeEquipement typeEquipement = ReferentielTypeEquipement.builder() + .type(equipementPhysique.getType()) + .refEquipementParDefaut(refEquipementParDefaut) + .dureeVieDefaut(null) + .build(); + ReferentielMixElectrique referentielMixElectrique = ReferentielMixElectrique.builder() + .critere(critere.getNomCritere()) + .pays("France") + .raccourcisAnglais("FR") + .source("CAF1") + .valeur(0.0813225) + .build(); + DemandeCalculImpactEquipementPhysique demandeCalcul = DemandeCalculImpactEquipementPhysique + .builder() + .dateCalcul(dateCalcul) + .etape(ReferentielEtapeACV.builder().code(etapeACV).build()) + .critere(critere) + .equipementPhysique(equipementPhysique) + .correspondanceRefEquipement(refEquipement) + .typeEquipement(typeEquipement) + .mixElectriques(Collections.singletonList(referentielMixElectrique)) + .build(); + + //When + var actual = calculImpactEquipementPhysiqueService.calculerImpactEquipementPhysique( + demandeCalcul + ); + + //Then + assertContentIndicateur(demandeCalcul, actual); + assertNull(actual.getImpactUnitaire()); + assertEquals("ERREUR", actual.getStatutIndicateur()); + assertEquals( + "{\"erreur\":\"ErrCalcFonc : Le PUE est manquant et ne permet le calcul de l'impact à l'usage de l'équipement : equipement: Tanabana2-IBM X96 - RefEquipementCible: IBM X96 - refEquipementParDefaut: IBM X96 - NomCourtDatacenter: TanaMana 2\"}", + actual.getTrace() + ); + } + + private void assertContentIndicateur(DemandeCalculImpactEquipementPhysique source, ImpactEquipementPhysique result) { + assertNotNull(result); + assertEquals(source.getEquipementPhysique().getNomEquipementPhysique(), result.getNomEquipement()); + assertEquals(source.getEquipementPhysique().getType(), result.getTypeEquipement()); + assertEquals(source.getEquipementPhysique().getStatut(), result.getStatutEquipementPhysique()); + assertEquals(source.getEquipementPhysique().getQuantite(), result.getQuantite()); + + assertEquals(source.getEquipementPhysique().getNomLot(), result.getNomLot()); + assertEquals(source.getEquipementPhysique().getDateLot(), result.getDateLot()); + assertEquals(source.getEquipementPhysique().getNomOrganisation(), result.getNomOrganisation()); + assertEquals(source.getEquipementPhysique().getNomEntite(), result.getNomEntite()); + assertEquals(source.getEquipementPhysique().getNomSourceDonnee(), result.getNomSourceDonnee()); + + assertEquals(source.getDateCalcul(), result.getDateCalcul()); + + assertEquals(source.getCritere().getNomCritere(), result.getCritere()); + assertEquals(source.getEtape().getCode(), result.getEtapeACV()); + assertEquals(source.getCritere().getUnite(), result.getUnite()); + + assertEquals("1.0", result.getVersionCalcul()); + } + + @Test + void verifyFormulaForModeUtilisationCOPE() { + //Given + EquipementPhysique equipementPhysique = EquipementPhysique.builder() + .dateLot(LocalDate.of(2022, 1, 1)) + .nomOrganisation("Test") + .nomLot("Test|20220101") + .nomSourceDonnee("Source_Test") + .type("Téléphone") + .nomEquipementPhysique("Apple Iphone 11") + .modele("Apple Iphone 11") + .paysDUtilisation("France") + .quantite(1.0) + .nbJourUtiliseAn(216.0) + .consoElecAnnuelle(0.09) + .modeUtilisation("COPE") + .build(); + ReferentielHypothese referentielHypothese = ReferentielHypothese.builder() + .valeur(0.8) + .code("taux_utilisation_COPE") + .source("RCP_SI") + .build(); + DemandeCalculImpactEquipementPhysique demandeCalcul = DemandeCalculImpactEquipementPhysique + .builder() + .dateCalcul(dateCalcul) + .etape(ReferentielEtapeACV.builder().code(etapeACV).build()) + .critere(critere) + .equipementPhysique(equipementPhysique) + .correspondanceRefEquipement(refEquipement) + .typeEquipement(typeEquipement) + .impactEquipements(Collections.singletonList(referentielImpactEquipement)) + .mixElectriques(Collections.singletonList(referentielMixElectrique)) + .hypotheses(Collections.singletonList(referentielHypothese)) + .build(); + //When + var actual = calculImpactEquipementPhysiqueService.calculerImpactEquipementPhysique( + demandeCalcul + ); + //Then + assertContentIndicateur(demandeCalcul, actual); + assertEquals("OK", actual.getStatutIndicateur()); + assertNotNull(actual.getImpactUnitaire()); + double actualImpactUnitaireLimited = BigDecimal.valueOf(actual.getImpactUnitaire()).setScale(4, RoundingMode.DOWN).doubleValue(); + assertEquals(0.0058, actualImpactUnitaireLimited); + assertEquals(referentielImpactEquipement.getConsoElecMoyenne(), actual.getConsoElecMoyenne()); + } + + @Test + void verifyFormulaForModeUtilisationBYOD() { + //Given + EquipementPhysique equipementPhysique = EquipementPhysique.builder() + .dateLot(LocalDate.of(2022, 1, 1)) + .nomOrganisation("Test") + .nomLot("Test|20220101") + .nomSourceDonnee("Source_Test") + .type("Téléphone") + .nomEquipementPhysique("Apple Iphone 11") + .modele("Apple Iphone 11") + .paysDUtilisation("France") + .quantite(1.0) + .nbJourUtiliseAn(216.0) + .consoElecAnnuelle(0.09) + .modeUtilisation("BYOD") + .build(); + ReferentielHypothese referentielHypothese = ReferentielHypothese.builder() + .valeur(0.57) + .code("taux_utilisation_BYOD") + .source("RCP_SI") + .build(); + DemandeCalculImpactEquipementPhysique demandeCalcul = DemandeCalculImpactEquipementPhysique + .builder() + .dateCalcul(dateCalcul) + .etape(ReferentielEtapeACV.builder().code(etapeACV).build()) + .critere(critere) + .equipementPhysique(equipementPhysique) + .correspondanceRefEquipement(refEquipement) + .typeEquipement(typeEquipement) + .impactEquipements(Collections.singletonList(referentielImpactEquipement)) + .mixElectriques(Collections.singletonList(referentielMixElectrique)) + .hypotheses(Collections.singletonList(referentielHypothese)) + .build(); + //When + var actual = calculImpactEquipementPhysiqueService.calculerImpactEquipementPhysique( + demandeCalcul + ); + //Then + assertContentIndicateur(demandeCalcul, actual); + assertEquals("OK", actual.getStatutIndicateur()); + assertNotNull(actual.getImpactUnitaire()); + double actualImpactUnitaireLimited = BigDecimal.valueOf(actual.getImpactUnitaire()).setScale(4, RoundingMode.DOWN).doubleValue(); + assertEquals(0.0041, actualImpactUnitaireLimited); + assertEquals(referentielImpactEquipement.getConsoElecMoyenne(), actual.getConsoElecMoyenne()); + } + + @Test + void verifyFormulaForModeUtilisationModeUtilisationNull() { + //Given + EquipementPhysique equipementPhysique = EquipementPhysique.builder() + .dateLot(LocalDate.of(2022, 1, 1)) + .nomOrganisation("Test") + .nomLot("Test|20220101") + .nomSourceDonnee("Source_Test") + .type("Téléphone") + .nomEquipementPhysique("Apple Iphone 11") + .modele("Apple Iphone 11") + .paysDUtilisation("France") + .quantite(1.0) + .nbJourUtiliseAn(216.0) + .consoElecAnnuelle(0.09) + .modeUtilisation("") + .build(); + ReferentielHypothese referentielHypothese = ReferentielHypothese.builder() + .valeur(0.57) + .code("taux_utilisation_BYOD") + .source("RCP_SI") + .build(); + DemandeCalculImpactEquipementPhysique demandeCalcul = DemandeCalculImpactEquipementPhysique + .builder() + .dateCalcul(dateCalcul) + .etape(ReferentielEtapeACV.builder().code(etapeACV).build()) + .critere(critere) + .equipementPhysique(equipementPhysique) + .correspondanceRefEquipement(refEquipement) + .typeEquipement(typeEquipement) + .impactEquipements(Collections.singletonList(referentielImpactEquipement)) + .mixElectriques(Collections.singletonList(referentielMixElectrique)) + .hypotheses(Collections.singletonList(referentielHypothese)) + .build(); + //When + var actual = calculImpactEquipementPhysiqueService.calculerImpactEquipementPhysique( + demandeCalcul + ); + //Then + assertContentIndicateur(demandeCalcul, actual); + assertEquals("OK", actual.getStatutIndicateur()); + assertNotNull(actual.getImpactUnitaire()); + double actualImpactUnitaireLimited = BigDecimal.valueOf(actual.getImpactUnitaire()).setScale(4, RoundingMode.DOWN).doubleValue(); + assertEquals(0.0073, actualImpactUnitaireLimited); + assertEquals(referentielImpactEquipement.getConsoElecMoyenne(), actual.getConsoElecMoyenne()); + } + + @Test + void verifyFormulaForModeUtilisationModeUtilisationUnknown() { + //Given + EquipementPhysique equipementPhysique = EquipementPhysique.builder() + .dateLot(LocalDate.of(2022, 1, 1)) + .nomOrganisation("Test") + .nomLot("Test|20220101") + .nomSourceDonnee("Source_Test") + .type("Téléphone") + .nomEquipementPhysique("Apple Iphone 11") + .modele("Apple Iphone 11") + .paysDUtilisation("France") + .quantite(1.0) + .nbJourUtiliseAn(216.0) + .consoElecAnnuelle(0.09) + .modeUtilisation("COB") + .build(); + ReferentielHypothese referentielHypothese = ReferentielHypothese.builder() + .valeur(0.57) + .code("taux_utilisation_BYOD") + .source("RCP_SI") + .build(); + DemandeCalculImpactEquipementPhysique demandeCalcul = DemandeCalculImpactEquipementPhysique + .builder() + .dateCalcul(dateCalcul) + .etape(ReferentielEtapeACV.builder().code(etapeACV).build()) + .critere(critere) + .equipementPhysique(equipementPhysique) + .correspondanceRefEquipement(refEquipement) + .typeEquipement(typeEquipement) + .impactEquipements(Collections.singletonList(referentielImpactEquipement)) + .mixElectriques(Collections.singletonList(referentielMixElectrique)) + .hypotheses(Collections.singletonList(referentielHypothese)) + .build(); + //When + var actual = calculImpactEquipementPhysiqueService.calculerImpactEquipementPhysique( + demandeCalcul + ); + //Then + assertContentIndicateur(demandeCalcul, actual); + assertEquals("OK", actual.getStatutIndicateur()); + assertNotNull(actual.getImpactUnitaire()); + double actualImpactUnitaireLimited = BigDecimal.valueOf(actual.getImpactUnitaire()).setScale(4, RoundingMode.DOWN).doubleValue(); + assertEquals(0.0073, actualImpactUnitaireLimited); + assertEquals(referentielImpactEquipement.getConsoElecMoyenne(), actual.getConsoElecMoyenne()); + } + + @Test + void verifyFormulaForModeUtilisationModeUtilisationCOPEButNotInRefHypotheses() { + //Given + EquipementPhysique equipementPhysique = EquipementPhysique.builder() + .dateLot(LocalDate.of(2022, 1, 1)) + .nomOrganisation("Test") + .nomLot("Test|20220101") + .nomSourceDonnee("Source_Test") + .type("Téléphone") + .nomEquipementPhysique("Apple Iphone 11") + .modele("Apple Iphone 11") + .paysDUtilisation("France") + .quantite(1.0) + .nbJourUtiliseAn(216.0) + .consoElecAnnuelle(0.09) + .modeUtilisation("COPE") + .build(); + DemandeCalculImpactEquipementPhysique demandeCalcul = DemandeCalculImpactEquipementPhysique + .builder() + .dateCalcul(dateCalcul) + .etape(ReferentielEtapeACV.builder().code(etapeACV).build()) + .critere(critere) + .equipementPhysique(equipementPhysique) + .correspondanceRefEquipement(refEquipement) + .typeEquipement(typeEquipement) + .impactEquipements(Collections.singletonList(referentielImpactEquipement)) + .mixElectriques(Collections.singletonList(referentielMixElectrique)) + .build(); + //When + var actual = calculImpactEquipementPhysiqueService.calculerImpactEquipementPhysique( + demandeCalcul + ); + //Then + assertContentIndicateur(demandeCalcul, actual); + assertEquals("OK", actual.getStatutIndicateur()); + assertNotNull(actual.getImpactUnitaire()); + double actualImpactUnitaireLimited = BigDecimal.valueOf(actual.getImpactUnitaire()).setScale(4, RoundingMode.DOWN).doubleValue(); + assertEquals(0.0073, actualImpactUnitaireLimited); + assertEquals(referentielImpactEquipement.getConsoElecMoyenne(), actual.getConsoElecMoyenne()); + } + + @Test + void verifyFormulaForNotTUILISATIONModeUtilisationCOPE() throws Exception { + //Given + String etapeACV = "FABRICATION"; + ReferentielCritere critere = ReferentielCritere.builder() + .nomCritere("Changement Climatique") + .unite("kg CO_{2} eq") + .build(); + LocalDateTime dateCalcul = LocalDateTime.now(); + String refEquipementCible = "Ordinateur4"; + String refEquipementParDefaut = "Ordinateur4"; + EquipementPhysique equipementPhysique = EquipementPhysique.builder() + .dateLot(LocalDate.of(2022, 1, 1)) + .nomOrganisation("Test") + .nomLot("Test|20220101") + .nomSourceDonnee("Source_Test") + .type("Desktop") + .nomEquipementPhysique("Ordinateur4") + .modele("Ordinateur4") + .paysDUtilisation("France") + .quantite(1.0) + .modeUtilisation("COPE") + .nbJourUtiliseAn(null) + .build(); + ReferentielImpactEquipement referentielImpactEquipement = ReferentielImpactEquipement.builder() + .source("CAF4") + .critere(critere.getNomCritere()) + .etape(etapeACV) + .refEquipement(equipementPhysique.getModele()) + .consoElecMoyenne(null) + .valeur(142.0) + .build(); + ReferentielCorrespondanceRefEquipement refEquipement = ReferentielCorrespondanceRefEquipement.builder() + .refEquipementCible(refEquipementCible) + .modeleEquipementSource(equipementPhysique.getModele()) + .build(); + ReferentielTypeEquipement typeEquipement = ReferentielTypeEquipement.builder() + .type(equipementPhysique.getType()) + .refEquipementParDefaut(refEquipementParDefaut) + .dureeVieDefaut(null) + .build(); + ReferentielHypothese referentielHypothese = ReferentielHypothese.builder() + .valeur(0.85) + .code("taux_utilisation_COPE") + .source("RCP_SI") + .build(); + DemandeCalculImpactEquipementPhysique demandeCalcul = DemandeCalculImpactEquipementPhysique + .builder() + .dateCalcul(dateCalcul) + .etape(ReferentielEtapeACV.builder().code(etapeACV).build()) + .critere(critere) + .equipementPhysique(equipementPhysique) + .correspondanceRefEquipement(refEquipement) + .typeEquipement(typeEquipement) + .hypotheses(Collections.singletonList(referentielHypothese)) + .impactEquipements(Collections.singletonList(referentielImpactEquipement)) + .build(); + when(dureeDeVieEquipementPhysiqueService.calculerDureeVie(any(DemandeCalculImpactEquipementPhysique.class))).thenReturn(DureeDeVie.builder().valeur(5.0d).build()); + //When + var actual = calculImpactEquipementPhysiqueService.calculerImpactEquipementPhysique( + demandeCalcul + ); + //Then + assertContentIndicateur(demandeCalcul, actual); + assertEquals("OK", actual.getStatutIndicateur()); + assertEquals(dateCalcul, actual.getDateCalcul()); + assertNotNull(actual.getImpactUnitaire()); + double actualImpactUnitaireLimited = BigDecimal.valueOf(actual.getImpactUnitaire()).setScale(4, RoundingMode.DOWN).doubleValue(); + assertEquals(24.14, actualImpactUnitaireLimited); + verify(dureeDeVieEquipementPhysiqueService, times(1)).calculerDureeVie(demandeCalcul); + assertNull(actual.getConsoElecMoyenne()); + } + + @Test + void verifyFormulaForUTILISATIONTauxUtilisationFilled() throws Exception { + //Given + EquipementPhysique equipementPhysique = EquipementPhysique.builder() + .dateLot(LocalDate.of(2022, 1, 1)) + .nomOrganisation("Test") + .nomLot("Test|20220101") + .nomSourceDonnee("Source_Test") + .type("Téléphone") + .nomEquipementPhysique("Apple Iphone 11") + .modele("Apple Iphone 11") + .paysDUtilisation("France") + .quantite(1.0) + .nbJourUtiliseAn(216.0) + .consoElecAnnuelle(0.09) + .modeUtilisation("") + .tauxUtilisation(0.1) + .build(); + ReferentielHypothese referentielHypothese = ReferentielHypothese.builder() + .valeur(0.57) + .code("taux_utilisation_BYOD") + .source("RCP_SI") + .build(); + DemandeCalculImpactEquipementPhysique demandeCalcul = DemandeCalculImpactEquipementPhysique + .builder() + .dateCalcul(dateCalcul) + .etape(ReferentielEtapeACV.builder().code(etapeACV).build()) + .critere(critere) + .equipementPhysique(equipementPhysique) + .correspondanceRefEquipement(refEquipement) + .typeEquipement(typeEquipement) + .impactEquipements(Collections.singletonList(referentielImpactEquipement)) + .mixElectriques(Collections.singletonList(referentielMixElectrique)) + .hypotheses(Collections.singletonList(referentielHypothese)) + .build(); + //When + var actual = calculImpactEquipementPhysiqueService.calculerImpactEquipementPhysique( + demandeCalcul + ); + //Then + assertContentIndicateur(demandeCalcul, actual); + assertEquals("OK", actual.getStatutIndicateur()); + assertNotNull(actual.getImpactUnitaire()); + double actualImpactUnitaireLimited = BigDecimal.valueOf(actual.getImpactUnitaire()).setScale(5, RoundingMode.DOWN).doubleValue(); + assertEquals(0.00073, actualImpactUnitaireLimited); + assertEquals(referentielImpactEquipement.getConsoElecMoyenne(), actual.getConsoElecMoyenne()); + } +} \ No newline at end of file diff --git a/services/calculs/src/test/java/org/mte/numecoeval/calculs/domain/service/CalculImpactEquipementVirtuelServiceTest.java b/services/calculs/src/test/java/org/mte/numecoeval/calculs/domain/service/CalculImpactEquipementVirtuelServiceTest.java new file mode 100644 index 00000000..c197969a --- /dev/null +++ b/services/calculs/src/test/java/org/mte/numecoeval/calculs/domain/service/CalculImpactEquipementVirtuelServiceTest.java @@ -0,0 +1,673 @@ +package org.mte.numecoeval.calculs.domain.service; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mte.numecoeval.calculs.domain.data.demande.DemandeCalculImpactEquipementVirtuel; +import org.mte.numecoeval.calculs.domain.data.entree.EquipementVirtuel; +import org.mte.numecoeval.calculs.domain.data.indicateurs.ImpactEquipementPhysique; +import org.mte.numecoeval.calculs.domain.data.indicateurs.ImpactEquipementVirtuel; +import org.mte.numecoeval.calculs.domain.data.trace.TraceCalculImpactEquipementVirtuel; +import org.mte.numecoeval.calculs.domain.port.input.service.CalculImpactEquipementVirtuelService; +import org.mte.numecoeval.calculs.domain.port.input.service.impl.CalculImpactEquipementVirtuelServiceImpl; +import org.mte.numecoeval.calculs.domain.traceur.TraceCalculImpactVirtuelUtils; +import org.springframework.boot.test.system.OutputCaptureExtension; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +import static org.junit.jupiter.api.Assertions.*; + +@Slf4j +@ExtendWith({MockitoExtension.class, OutputCaptureExtension.class}) +class CalculImpactEquipementVirtuelServiceTest { + + @Spy + private ObjectMapper objectMapper; + private CalculImpactEquipementVirtuelService calculImpactEquipementVirtuelService; + + @BeforeEach + void setUp() { + calculImpactEquipementVirtuelService = new CalculImpactEquipementVirtuelServiceImpl(objectMapper); + } + + /** + * CAF 1 + * Nombre total de vCPU renseigné + * calculIndicateurImpactEquipementPhysique * EqV.vCPU / NbvCPU + * Impact d’utilisation « VM1 »= 234,62784 x 7/19=86,442kgCO2eq + */ + @Test + void whenTypeEqvIsCalculAndTotalVCPUIsNotNull_shouldReturnCalculImpactUsingTotalVCPU() { + + //GIVEN + EquipementVirtuel equipementVirtuel1 = EquipementVirtuel.builder() + .dateLot(LocalDate.of(2022,1,1)) + .nomOrganisation("Test") + .nomLot("Test|20220101") + .nomSourceDonnee("Source_Test") + .nomEquipementPhysique("equipement1") + .nomEquipementVirtuel("Vm1") + .vCPU(7) + .typeEqv("calcul") + .build(); + ImpactEquipementPhysique impactEquipementPhysique = ImpactEquipementPhysique.builder() + .etapeACV("UTILISATION") + .critere("Changement climatique") + .typeEquipement("Serveur") + .impactUnitaire(234.62784d) + .unite("kgCO2eq") + .consoElecMoyenne(100.0d) + .statutIndicateur("OK") + .nomEquipement(equipementVirtuel1.getNomEquipementPhysique()) + .build(); + LocalDateTime dateCalcul = LocalDateTime.now(); + DemandeCalculImpactEquipementVirtuel demandeCalcul = DemandeCalculImpactEquipementVirtuel.builder() + .dateCalcul(dateCalcul) + .equipementVirtuel(equipementVirtuel1) + .impactEquipement(impactEquipementPhysique) + .nbEquipementsVirtuels(3) + .nbTotalVCPU(7 + 3 + 9) + .build(); + //WHEN + var impactEquipementVirtuel = calculImpactEquipementVirtuelService.calculerImpactEquipementVirtuel(demandeCalcul); + assertNotNull(impactEquipementVirtuel); + + //THEN + assertContentIndicateur(demandeCalcul, impactEquipementVirtuel); + assertEquals("OK", impactEquipementVirtuel.getStatutIndicateur()); + assertEquals(86.44183578947367d, impactEquipementVirtuel.getImpactUnitaire()); + assertEquals(36.8421052631579d, impactEquipementVirtuel.getConsoElecMoyenne()); + assertEquals("{\"formule\":\"valeurImpactUnitaire = valeurImpactEquipementPhysique(234.62784) * equipementVirtuel.vCPU(7) / nbvCPU(19)\",\"nbEquipementsVirtuels\":3,\"nbTotalVCPU\":19}", impactEquipementVirtuel.getTrace()); + } + + + /** + * CAF3 + * Nombre total de vCPU renseigné non renseigné et Nombre de VM > 1 + * calculIndicateurImpactEquipementPhysique * 1 / NbVM + * Impact d’utilisation « VM3 »=234,62784 x 1/3=78.209279999 kgCO2eq + */ + @Test + void whenTotalVCPUIsNull_shouldReturnCalculImpactUsingTotalNumberOfVirtualEquipment() { + //GIVEN + EquipementVirtuel equipementVirtuel1 = EquipementVirtuel.builder() + .dateLot(LocalDate.of(2022,1,1)) + .nomOrganisation("Test") + .nomLot("Test|20220101") + .nomSourceDonnee("Source_Test") + .nomEquipementPhysique("equipement1") + .nomEquipementVirtuel("Vm1") + .build(); + ImpactEquipementPhysique impactEquipementPhysique = ImpactEquipementPhysique.builder() + .etapeACV("UTILISATION") + .critere("Changement climatique") + .typeEquipement("Serveur") + .consoElecMoyenne(100.0d) + .impactUnitaire(234.62784d) + .unite("kgCO2eq") + .statutIndicateur("OK") + .nomEquipement(equipementVirtuel1.getNomEquipementPhysique()) + .build(); + LocalDateTime dateCalcul = LocalDateTime.now(); + + DemandeCalculImpactEquipementVirtuel demandeCalcul = DemandeCalculImpactEquipementVirtuel.builder() + .dateCalcul(dateCalcul) + .equipementVirtuel(equipementVirtuel1) + .impactEquipement(impactEquipementPhysique) + .nbEquipementsVirtuels(3) + .nbTotalVCPU(null) + .build(); + + //WHEN + var impactEquipementVirtuel = calculImpactEquipementVirtuelService.calculerImpactEquipementVirtuel(demandeCalcul); + + //THEN + assertContentIndicateur(demandeCalcul, impactEquipementVirtuel); + assertEquals("OK", impactEquipementVirtuel.getStatutIndicateur()); + assertEquals(78.20927999999999, impactEquipementVirtuel.getImpactUnitaire()); + assertEquals(33.333333333333336, impactEquipementVirtuel.getConsoElecMoyenne()); + assertEquals("{\"formule\":\"valeurImpactUnitaire = valeurImpactEquipementPhysique(234.62784) / nbVM(3)\",\"nbEquipementsVirtuels\":3}", impactEquipementVirtuel.getTrace()); + + } + + /** + * Taiga 457 + * Type d'équipement virtuel n'est pas "calcul" ET Nombre total de vCPU renseigné et Nombre de VM > 1 + * calculIndicateurImpactEquipementPhysique * 1 / NbVM + * Impact d’utilisation « VM3 »=234,62784 x 1/3=78.209279999 kgCO2eq + */ + @Test + void whenTypeEqvIsNotCalculAndTotalVCPUIsNull_shouldReturnCalculImpactUsingTotalNumberOfVirtualEquipment() { + //GIVEN + EquipementVirtuel equipementVirtuel1 = EquipementVirtuel.builder() + .dateLot(LocalDate.of(2022,1,1)) + .nomOrganisation("Test") + .nomLot("Test|20220101") + .nomSourceDonnee("Source_Test") + .nomEquipementPhysique("equipement1") + .nomEquipementVirtuel("Vm1") + .typeEqv("notCalcul") + .vCPU(7) + .build(); + ImpactEquipementPhysique impactEquipementPhysique = ImpactEquipementPhysique.builder() + .etapeACV("UTILISATION") + .critere("Changement climatique") + .typeEquipement("Serveur") + .consoElecMoyenne(100.0d) + .impactUnitaire(234.62784d) + .unite("kgCO2eq") + .statutIndicateur("OK") + .nomEquipement(equipementVirtuel1.getNomEquipementPhysique()) + .build(); + LocalDateTime dateCalcul = LocalDateTime.now(); + + DemandeCalculImpactEquipementVirtuel demandeCalcul = DemandeCalculImpactEquipementVirtuel.builder() + .dateCalcul(dateCalcul) + .equipementVirtuel(equipementVirtuel1) + .impactEquipement(impactEquipementPhysique) + .nbEquipementsVirtuels(3) + .nbTotalVCPU(7 + 3 + 9) + .build(); + + //WHEN + var impactEquipementVirtuel = calculImpactEquipementVirtuelService.calculerImpactEquipementVirtuel(demandeCalcul); + + //THEN + assertContentIndicateur(demandeCalcul, impactEquipementVirtuel); + assertEquals("OK", impactEquipementVirtuel.getStatutIndicateur()); + assertEquals(78.20927999999999, impactEquipementVirtuel.getImpactUnitaire()); + assertEquals(33.333333333333336, impactEquipementVirtuel.getConsoElecMoyenne()); + assertEquals("{\"formule\":\"valeurImpactUnitaire = valeurImpactEquipementPhysique(234.62784) / nbVM(3)\",\"nbEquipementsVirtuels\":3,\"nbTotalVCPU\":19}", impactEquipementVirtuel.getTrace()); + } + + /** + * CAF4 + * Le nombre de VM est null et le nombre total de vCPU est null : le calcul est impossible et fini par une erreur Fonctionnelle. + * Impact d'utilisation « VM4 »= ErrCalcFonc("certaines données sur l'équipement virtuel sont manquantes ou incorrectes") + */ + @Test + void whenTotalVCPUAndNbrVirtualEquipementAreNull_shouldReturnImpactError() { + EquipementVirtuel equipementVirtuel = EquipementVirtuel.builder() + .dateLot(LocalDate.of(2022,1,1)) + .nomOrganisation("Test") + .nomLot("Test|20220101") + .nomSourceDonnee("Source_Test") + .nomEquipementPhysique("equipement1") + .nomEquipementVirtuel("Vm1") + .build(); + ImpactEquipementPhysique impactEquipementPhysique = ImpactEquipementPhysique.builder() + .etapeACV("UTILISATION") + .critere("Changement climatique") + .typeEquipement("Serveur") + .impactUnitaire(234.62784d) + .unite("kgCO2eq") + .statutIndicateur("OK") + .nomEquipement(equipementVirtuel.getNomEquipementPhysique()) + .build(); + + DemandeCalculImpactEquipementVirtuel demandeCalcul = DemandeCalculImpactEquipementVirtuel.builder() + .dateCalcul(LocalDateTime.now()) + .equipementVirtuel(equipementVirtuel) + .impactEquipement(impactEquipementPhysique) + .nbEquipementsVirtuels(null) + .nbTotalVCPU(null) + .build(); + + var impactEquipementVirtuel = calculImpactEquipementVirtuelService.calculerImpactEquipementVirtuel(demandeCalcul); + assertContentIndicateur(demandeCalcul, impactEquipementVirtuel); + assertEquals("ERREUR", impactEquipementVirtuel.getStatutIndicateur()); + assertNull(impactEquipementVirtuel.getImpactUnitaire()); + assertNull(impactEquipementVirtuel.getConsoElecMoyenne()); + assertTrue(impactEquipementVirtuel.getTrace().contains("Certaines données sur l'équipement virtuel sont manquantes ou incorrectes")); + } + + @Test + void whenNbrVirtualEquipementIs0_shouldReturnImpactError() { + //GIVEN + EquipementVirtuel equipementVirtuel1 = EquipementVirtuel.builder() + .dateLot(LocalDate.of(2022,1,1)) + .nomOrganisation("Test") + .nomLot("Test|20220101") + .nomSourceDonnee("Source_Test") + .nomEquipementPhysique("equipement1") + .nomEquipementVirtuel("Vm1") + .build(); + ImpactEquipementPhysique impactEquipementPhysique = ImpactEquipementPhysique.builder() + .etapeACV("UTILISATION") + .critere("Changement climatique") + .typeEquipement("Serveur") + .consoElecMoyenne(100.0d) + .impactUnitaire(234.62784d) + .unite("kgCO2eq") + .statutIndicateur("OK") + .nomEquipement(equipementVirtuel1.getNomEquipementPhysique()) + .build(); + LocalDateTime dateCalcul = LocalDateTime.now(); + + DemandeCalculImpactEquipementVirtuel demandeCalcul = DemandeCalculImpactEquipementVirtuel.builder() + .dateCalcul(dateCalcul) + .equipementVirtuel(equipementVirtuel1) + .impactEquipement(impactEquipementPhysique) + .nbEquipementsVirtuels(0) + .nbTotalVCPU(null) + .build(); + + //WHEN + var impactEquipementVirtuel = calculImpactEquipementVirtuelService.calculerImpactEquipementVirtuel(demandeCalcul); + + //THEN + assertContentIndicateur(demandeCalcul, impactEquipementVirtuel); + assertEquals("ERREUR", impactEquipementVirtuel.getStatutIndicateur()); + assertNull(impactEquipementVirtuel.getImpactUnitaire()); + assertNull(impactEquipementVirtuel.getConsoElecMoyenne()); + assertTrue(impactEquipementVirtuel.getTrace().contains("Certaines données sur l'équipement virtuel sont manquantes ou incorrectes")); + } + + /** + * Cas arrivé avec la V2. + * Si l'impact unitaire de l'équipement physique est à un statut différent de OK, alors l'impact résultant est en erreur. + */ + @Test + void whenBaseImpactIsInError_shouldReturnImpactError() { + + //GIVEN + EquipementVirtuel equipementVirtuel1 = EquipementVirtuel.builder() + .dateLot(LocalDate.of(2022,1,1)) + .nomOrganisation("Test") + .nomLot("Test|20220101") + .nomSourceDonnee("Source_Test") + .nomEquipementPhysique("equipement1") + .nomEquipementVirtuel("Vm1") + .vCPU(7) + .build(); + ImpactEquipementPhysique impactEquipementPhysique = ImpactEquipementPhysique.builder() + .etapeACV("UTILISATION") + .critere("Changement climatique") + .typeEquipement("Serveur") + .impactUnitaire(null) + .unite("kgCO2eq") + .consoElecMoyenne(null) + .statutIndicateur("ERREUR") + .nomEquipement(equipementVirtuel1.getNomEquipementPhysique()) + .build(); + LocalDateTime dateCalcul = LocalDateTime.now(); + DemandeCalculImpactEquipementVirtuel demandeCalcul = DemandeCalculImpactEquipementVirtuel.builder() + .dateCalcul(dateCalcul) + .equipementVirtuel(equipementVirtuel1) + .impactEquipement(impactEquipementPhysique) + .nbEquipementsVirtuels(3) + .nbTotalVCPU(7 + 3 + 9) + .build(); + //WHEN + var impactEquipementVirtuel = calculImpactEquipementVirtuelService.calculerImpactEquipementVirtuel(demandeCalcul); + assertNotNull(impactEquipementVirtuel); + + //THEN + assertContentIndicateur(demandeCalcul, impactEquipementVirtuel); + assertNull(impactEquipementVirtuel.getImpactUnitaire()); + assertNull(impactEquipementVirtuel.getConsoElecMoyenne()); + assertEquals("ERREUR", impactEquipementVirtuel.getStatutIndicateur()); + assertTrue(impactEquipementVirtuel.getTrace().contains("L'indicateur d'impact équipement associé est au statut : ERREUR")); + } + + /** + * Cas arrivé avec la V2. + * Si l'impact unitaire de l'équipement physique est null, alors l'impact résultant est en erreur. + */ + @Test + void whenBaseImpactWithNullValues_shouldReturnImpactError() { + + //GIVEN + EquipementVirtuel equipementVirtuel1 = EquipementVirtuel.builder() + .dateLot(LocalDate.of(2022,1,1)) + .nomOrganisation("Test") + .nomLot("Test|20220101") + .nomSourceDonnee("Source_Test") + .nomEquipementPhysique("equipement1") + .nomEquipementVirtuel("Vm1") + .vCPU(7) + .build(); + ImpactEquipementPhysique impactEquipementPhysique = ImpactEquipementPhysique.builder() + .etapeACV("UTILISATION") + .critere("Changement climatique") + .typeEquipement("Serveur") + .impactUnitaire(null) + .unite("kgCO2eq") + .consoElecMoyenne(null) + .statutIndicateur("OK") + .nomEquipement(equipementVirtuel1.getNomEquipementPhysique()) + .build(); + LocalDateTime dateCalcul = LocalDateTime.now(); + DemandeCalculImpactEquipementVirtuel demandeCalcul = DemandeCalculImpactEquipementVirtuel.builder() + .dateCalcul(dateCalcul) + .equipementVirtuel(equipementVirtuel1) + .impactEquipement(impactEquipementPhysique) + .nbEquipementsVirtuels(3) + .nbTotalVCPU(7 + 3 + 9) + .build(); + //WHEN + var impactEquipementVirtuel = calculImpactEquipementVirtuelService.calculerImpactEquipementVirtuel(demandeCalcul); + assertNotNull(impactEquipementVirtuel); + + //THEN + assertContentIndicateur(demandeCalcul, impactEquipementVirtuel); + assertNull(impactEquipementVirtuel.getImpactUnitaire()); + assertNull(impactEquipementVirtuel.getConsoElecMoyenne()); + assertEquals("ERREUR", impactEquipementVirtuel.getStatutIndicateur()); + assertTrue(impactEquipementVirtuel.getTrace().contains("L'impact unitaire de l'équipement physique parent est null")); + } + + + /** + * CAF5 + * Si ConsoElecMoyenne est null et impact unitaire OK, calcul impact OK + */ + @Test + void caf5() { + + EquipementVirtuel equipementVirtuel1 = EquipementVirtuel.builder() + .dateLot(LocalDate.of(2022,1,1)) + .nomOrganisation("Test") + .nomLot("Test|20220101") + .nomSourceDonnee("Source_Test") + .nomEquipementPhysique("equipement1") + .nomEquipementVirtuel("Vm1") + .vCPU(7) + .build(); + ImpactEquipementPhysique impactEquipementPhysique = ImpactEquipementPhysique.builder() + .etapeACV("UTILISATION") + .critere("Changement climatique") + .typeEquipement("Serveur") + .impactUnitaire(234.62784d) + .unite("kgCO2eq") + .statutIndicateur("OK") + .nomEquipement(equipementVirtuel1.getNomEquipementPhysique()) + .build(); + LocalDateTime dateCalcul = LocalDateTime.now(); + + DemandeCalculImpactEquipementVirtuel demandeCalcul = DemandeCalculImpactEquipementVirtuel.builder() + .dateCalcul(dateCalcul) + .equipementVirtuel(equipementVirtuel1) + .impactEquipement(impactEquipementPhysique) + .nbEquipementsVirtuels(2) + .nbTotalVCPU(null) + .build(); + + var impactEquipementVirtuel = calculImpactEquipementVirtuelService.calculerImpactEquipementVirtuel(demandeCalcul); + assertContentIndicateur(demandeCalcul, impactEquipementVirtuel); + assertEquals("OK", impactEquipementVirtuel.getStatutIndicateur()); + assertEquals(117.31392d, impactEquipementVirtuel.getImpactUnitaire()); + assertNull(impactEquipementVirtuel.getConsoElecMoyenne()); + } + + @Test + void shouldGetTraceCalculVm() throws Exception { + //GIVEN + EquipementVirtuel equipementVirtuel1 = EquipementVirtuel.builder() + .dateLot(LocalDate.of(2022,1,1)) + .nomOrganisation("Test") + .nomLot("Test|20220101") + .nomSourceDonnee("Source_Test") + .nomEquipementPhysique("equipement1") + .nomEquipementVirtuel("Vm1") + .vCPU(7) + .typeEqv("calcul") + .build(); + ImpactEquipementPhysique impactEquipementPhysique = ImpactEquipementPhysique.builder() + .etapeACV("UTILISATION") + .critere("Changement climatique") + .typeEquipement("Serveur") + .impactUnitaire(234.62784d) + .unite("kgCO2eq") + .statutIndicateur("OK") + .nomEquipement(equipementVirtuel1.getNomEquipementPhysique()) + .build(); + LocalDateTime dateCalcul = LocalDateTime.now(); + + DemandeCalculImpactEquipementVirtuel demandeCalcul = DemandeCalculImpactEquipementVirtuel.builder() + .dateCalcul(dateCalcul) + .equipementVirtuel(equipementVirtuel1) + .impactEquipement(impactEquipementPhysique) + .nbEquipementsVirtuels(3) + .nbTotalVCPU(7+3+9) + .build(); + //WHEN + var impactEquipementVirtuel = calculImpactEquipementVirtuelService.calculerImpactEquipementVirtuel(demandeCalcul); + + var formule = TraceCalculImpactVirtuelUtils.getFormuleWithTotalVCPU(demandeCalcul); + var expected = objectMapper.writeValueAsString(TraceCalculImpactEquipementVirtuel.builder() + .nbTotalVCPU(demandeCalcul.getNbTotalVCPU()) + .nbEquipementsVirtuels(demandeCalcul.getNbEquipementsVirtuels()) + .formule(formule) + .build()); + + assertEquals(expected, impactEquipementVirtuel.getTrace()); + } + + /* + Taiga 437 + */ + @Test + void forTaiga437VM1_shouldReturnCalculImpactUsingProrataFormula() { + //GIVEN + EquipementVirtuel equipementVirtuel1 = EquipementVirtuel.builder() + .dateLot(LocalDate.of(2022,1,1)) + .nomOrganisation("Test") + .nomLot("Test|20220101") + .nomSourceDonnee("Source_Test") + .nomEquipementPhysique("equipement1") + .nomEquipementVirtuel("VM1") + .typeEqv("calcul") + .vCPU(8) + .capaciteStockage(5.0) + .cleRepartition(0.2) + .build(); + ImpactEquipementPhysique impactEquipementPhysique = ImpactEquipementPhysique.builder() + .etapeACV("UTILISATION") + .critere("Changement climatique") + .typeEquipement("Serveur") + .consoElecMoyenne(2840.d) + .impactUnitaire(2840.d) + .unite("kgCO2eq") + .statutIndicateur("OK") + .nomEquipement(equipementVirtuel1.getNomEquipementPhysique()) + .build(); + LocalDateTime dateCalcul = LocalDateTime.now(); + + DemandeCalculImpactEquipementVirtuel demandeCalcul = DemandeCalculImpactEquipementVirtuel.builder() + .dateCalcul(dateCalcul) + .equipementVirtuel(equipementVirtuel1) + .impactEquipement(impactEquipementPhysique) + .nbEquipementsVirtuels(10) + .nbTotalVCPU(null) + .stockageTotalVirtuel(null) + .build(); + + //WHEN + var impactEquipementVirtuel = calculImpactEquipementVirtuelService.calculerImpactEquipementVirtuel(demandeCalcul); + + //THEN + assertContentIndicateur(demandeCalcul, impactEquipementVirtuel); + assertEquals("OK", impactEquipementVirtuel.getStatutIndicateur()); + assertEquals(568.0, impactEquipementVirtuel.getImpactUnitaire()); + assertEquals(568.0, impactEquipementVirtuel.getConsoElecMoyenne()); + assertEquals("{\"formule\":\"valeurImpactUnitaire = valeurImpactEquipementPhysique(2840.0) * equipementVirtuel.cleRepartition(0.2)\"}", impactEquipementVirtuel.getTrace()); + } + + @Test + void forTaiga437VM2_shouldReturnCalculImpactUsingProrataOnCPUFormula() { + //GIVEN + EquipementVirtuel equipementVirtuel1 = EquipementVirtuel.builder() + .dateLot(LocalDate.of(2022,1,1)) + .nomOrganisation("Test") + .nomLot("Test|20220101") + .nomSourceDonnee("Source_Test") + .nomEquipementPhysique("equipement1") + .nomEquipementVirtuel("VM2") + .typeEqv("calcul") + .vCPU(8) + .capaciteStockage(null) + .cleRepartition(null) + .build(); + ImpactEquipementPhysique impactEquipementPhysique = ImpactEquipementPhysique.builder() + .etapeACV("UTILISATION") + .critere("Changement climatique") + .typeEquipement("Serveur") + .consoElecMoyenne(2840.d) + .impactUnitaire(2840.d) + .unite("kgCO2eq") + .statutIndicateur("OK") + .nomEquipement(equipementVirtuel1.getNomEquipementPhysique()) + .build(); + LocalDateTime dateCalcul = LocalDateTime.now(); + + DemandeCalculImpactEquipementVirtuel demandeCalcul = DemandeCalculImpactEquipementVirtuel.builder() + .dateCalcul(dateCalcul) + .equipementVirtuel(equipementVirtuel1) + .impactEquipement(impactEquipementPhysique) + .nbEquipementsVirtuels(10) + .nbTotalVCPU(16) + .stockageTotalVirtuel(null) + .build(); + + //WHEN + var impactEquipementVirtuel = calculImpactEquipementVirtuelService.calculerImpactEquipementVirtuel(demandeCalcul); + + //THEN + assertContentIndicateur(demandeCalcul, impactEquipementVirtuel); + assertEquals("OK", impactEquipementVirtuel.getStatutIndicateur()); + assertEquals(1420.0, impactEquipementVirtuel.getImpactUnitaire()); + assertEquals(1420.0, impactEquipementVirtuel.getConsoElecMoyenne()); + assertEquals("{\"formule\":\"valeurImpactUnitaire = valeurImpactEquipementPhysique(2840.0) * equipementVirtuel.vCPU(8) / nbvCPU(16)\",\"nbEquipementsVirtuels\":10,\"nbTotalVCPU\":16}", impactEquipementVirtuel.getTrace()); + } + + /* + Calcul au prorata du nombre d'équipement + */ + @Test + void forTaiga437VM4_shouldReturnCalculImpactUsingProrataFormula() { + //GIVEN + EquipementVirtuel equipementVirtuel1 = EquipementVirtuel.builder() + .dateLot(LocalDate.of(2022,1,1)) + .nomOrganisation("Test") + .nomLot("Test|20220101") + .nomSourceDonnee("Source_Test") + .nomEquipementPhysique("equipement1") + .nomEquipementVirtuel("VM4") + .typeEqv("calcul") + .vCPU(null) + .capaciteStockage(5.0) + .cleRepartition(null) + .build(); + EquipementVirtuel equipementVirtuel2 = EquipementVirtuel.builder() + .dateLot(LocalDate.of(2022,1,1)) + .nomOrganisation("Test") + .nomLot("Test|20220101") + .nomSourceDonnee("Source_Test") + .nomEquipementPhysique("equipement1") + .nomEquipementVirtuel("VM5") + .typeEqv("calcul") + .vCPU(null) + .capaciteStockage(10.0) + .cleRepartition(null) + .build(); + EquipementVirtuel equipementVirtuel3 = EquipementVirtuel.builder() + .dateLot(LocalDate.of(2022,1,1)) + .nomOrganisation("Test") + .nomLot("Test|20220101") + .nomSourceDonnee("Source_Test") + .nomEquipementPhysique("equipement1") + .nomEquipementVirtuel("VM5") + .typeEqv("calcul") + .vCPU(null) + .capaciteStockage(30.0) + .cleRepartition(null) + .build(); + ImpactEquipementPhysique impactEquipementPhysique = ImpactEquipementPhysique.builder() + .etapeACV("UTILISATION") + .critere("Changement climatique") + .typeEquipement("Serveur") + .consoElecMoyenne(2840.d) + .impactUnitaire(2840.d) + .unite("kgCO2eq") + .statutIndicateur("OK") + .nomEquipement(equipementVirtuel1.getNomEquipementPhysique()) + .build(); + LocalDateTime dateCalcul = LocalDateTime.now(); + + DemandeCalculImpactEquipementVirtuel demandeCalcul = DemandeCalculImpactEquipementVirtuel.builder() + .dateCalcul(dateCalcul) + .equipementVirtuel(equipementVirtuel1) + .impactEquipement(impactEquipementPhysique) + .nbEquipementsVirtuels(3) + .nbTotalVCPU(null) + .stockageTotalVirtuel(null) + .build(); + + DemandeCalculImpactEquipementVirtuel demandeCalcul2 = DemandeCalculImpactEquipementVirtuel.builder() + .dateCalcul(dateCalcul) + .equipementVirtuel(equipementVirtuel2) + .impactEquipement(impactEquipementPhysique) + .nbEquipementsVirtuels(3) + .nbTotalVCPU(null) + .stockageTotalVirtuel(null) + .build(); + + DemandeCalculImpactEquipementVirtuel demandeCalcul3 = DemandeCalculImpactEquipementVirtuel.builder() + .dateCalcul(dateCalcul) + .equipementVirtuel(equipementVirtuel3) + .impactEquipement(impactEquipementPhysique) + .nbEquipementsVirtuels(3) + .nbTotalVCPU(null) + .stockageTotalVirtuel(null) + .build(); + + //WHEN + var impactEquipementVirtuel = calculImpactEquipementVirtuelService.calculerImpactEquipementVirtuel(demandeCalcul); + var impactEquipementVirtuel2 = calculImpactEquipementVirtuelService.calculerImpactEquipementVirtuel(demandeCalcul2); + var impactEquipementVirtuel3 = calculImpactEquipementVirtuelService.calculerImpactEquipementVirtuel(demandeCalcul3); + + //THEN + assertContentIndicateur(demandeCalcul, impactEquipementVirtuel); + assertContentIndicateur(demandeCalcul2, impactEquipementVirtuel2); + assertContentIndicateur(demandeCalcul3, impactEquipementVirtuel3); + assertEquals("OK", impactEquipementVirtuel.getStatutIndicateur()); + assertEquals(946.6666666666666, impactEquipementVirtuel.getImpactUnitaire()); + assertEquals(946.6666666666666, impactEquipementVirtuel.getConsoElecMoyenne()); + assertEquals("{\"formule\":\"valeurImpactUnitaire = valeurImpactEquipementPhysique(2840.0) / nbVM(3)\",\"nbEquipementsVirtuels\":3}", impactEquipementVirtuel.getTrace()); + assertEquals("OK", impactEquipementVirtuel2.getStatutIndicateur()); + assertEquals(946.6666666666666, impactEquipementVirtuel2.getImpactUnitaire()); + assertEquals(946.6666666666666, impactEquipementVirtuel2.getConsoElecMoyenne()); + assertEquals("{\"formule\":\"valeurImpactUnitaire = valeurImpactEquipementPhysique(2840.0) / nbVM(3)\",\"nbEquipementsVirtuels\":3}", impactEquipementVirtuel2.getTrace()); + assertEquals("OK", impactEquipementVirtuel3.getStatutIndicateur()); + assertEquals(946.6666666666666, impactEquipementVirtuel3.getImpactUnitaire()); + assertEquals(946.6666666666666, impactEquipementVirtuel3.getConsoElecMoyenne()); + assertEquals("{\"formule\":\"valeurImpactUnitaire = valeurImpactEquipementPhysique(2840.0) / nbVM(3)\",\"nbEquipementsVirtuels\":3}", impactEquipementVirtuel3.getTrace()); + } + + + private void assertContentIndicateur(DemandeCalculImpactEquipementVirtuel source, ImpactEquipementVirtuel result) { + assertNotNull(result); + assertEquals(source.getEquipementVirtuel().getNomEquipementPhysique(), result.getNomEquipement()); + assertEquals(source.getEquipementVirtuel().getNomEquipementVirtuel(), result.getNomEquipementVirtuel()); + + assertEquals(source.getEquipementVirtuel().getNomLot(), result.getNomLot()); + assertEquals(source.getEquipementVirtuel().getDateLot(), result.getDateLot()); + assertEquals(source.getEquipementVirtuel().getNomOrganisation(), result.getNomOrganisation()); + assertEquals(source.getEquipementVirtuel().getNomEntite(), result.getNomEntite()); + assertEquals(source.getEquipementVirtuel().getNomSourceDonnee(), result.getNomSourceDonnee()); + + assertEquals(source.getDateCalcul(), result.getDateCalcul()); + + assertEquals(source.getImpactEquipement().getCritere(), result.getCritere()); + assertEquals(source.getImpactEquipement().getEtapeACV(), result.getEtapeACV()); + assertEquals(source.getImpactEquipement().getUnite(), result.getUnite()); + + assertEquals("1.1", result.getVersionCalcul()); + } +} \ No newline at end of file diff --git a/services/calculs/src/test/java/org/mte/numecoeval/calculs/domain/service/CalculImpactMessagerieServiceTest.java b/services/calculs/src/test/java/org/mte/numecoeval/calculs/domain/service/CalculImpactMessagerieServiceTest.java new file mode 100644 index 00000000..55c55784 --- /dev/null +++ b/services/calculs/src/test/java/org/mte/numecoeval/calculs/domain/service/CalculImpactMessagerieServiceTest.java @@ -0,0 +1,154 @@ +package org.mte.numecoeval.calculs.domain.service; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; +import org.mte.numecoeval.calculs.domain.data.demande.DemandeCalculImpactMessagerie; +import org.mte.numecoeval.calculs.domain.data.entree.Messagerie; +import org.mte.numecoeval.calculs.domain.data.indicateurs.ImpactMessagerie; +import org.mte.numecoeval.calculs.domain.data.referentiel.ReferentielCritere; +import org.mte.numecoeval.calculs.domain.data.referentiel.ReferentielImpactMessagerie; +import org.mte.numecoeval.calculs.domain.port.input.service.CalculImpactMessagerieService; +import org.mte.numecoeval.calculs.domain.port.input.service.impl.CalculImpactMessagerieServiceImpl; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.*; + +class CalculImpactMessagerieServiceTest { + + ObjectMapper objectMapper = new ObjectMapper(); + CalculImpactMessagerieService calculImpactMessagerieService = new CalculImpactMessagerieServiceImpl(objectMapper); + @Test + void whenValidMessagerie_shoudCalculerImpactMessagerie(){ + //GIVEN + var dateCalcul = LocalDateTime.now(); + ReferentielCritere critere = ReferentielCritere.builder() + .nomCritere("Changement Climatique") + .unite("kg CO_{2} eq") + .build(); + var referentielImpactMessagerie = ReferentielImpactMessagerie.builder() + .critere(critere.getNomCritere()) + .source("Source RFT") + .constanteOrdonneeOrigine(20d) + .constanteCoefficientDirecteur(30d) + .build(); + var messagerie = Messagerie.builder() + .dateLot(LocalDate.of(2022,1,1)) + .nomOrganisation("Test") + .nomLot("Test|20220101") + .nomSourceDonnee("Source_Test") + .moisAnnee(202211) + .nombreMailEmis(300d) + .nombreMailEmisXDestinataires(900d) + .volumeTotalMailEmis(5000d) + .build(); + var demandeCalcul = DemandeCalculImpactMessagerie.builder() + .dateCalcul(dateCalcul) + .critere(critere) + .impactsMessagerie(Collections.singletonList(referentielImpactMessagerie)) + .messagerie(messagerie) + .build(); + + + //WHEN + var impactMessagerie = calculImpactMessagerieService.calculerImpactMessagerie(demandeCalcul); + + //THEN + assertContentIndicateur(demandeCalcul, impactMessagerie); + assertEquals(468000d , impactMessagerie.getImpactMensuel()); + assertEquals("OK", impactMessagerie.getStatutIndicateur()); + assertEquals("{\"critere\":\"Changement Climatique\",\"volumeTotalMailEmis\":5000.0,\"nombreMailEmis\":300.0,\"constanteCoefficientDirecteur\":30.0,\"poidsMoyenMail\":16.666666666666668,\"constanteOrdonneeOrigine\":20.0,\"nombreMailEmisXDestinataires\":900.0,\"formule\":\"poidsMoyenMail(16.666666666666668) = volumeTotalMailEmis(5000.0)/nombreMailEmis(300.0);\\nimpactMensuel = (constanteCoefficientDirecteur (30.0) * poidsMoyenMail(16.666666666666668) + constanteOrdonneeOrigine(20.0)) * nombreMailEmisXDestinataires(900.0)\\n\"}", impactMessagerie.getTrace()); + } + + @Test + void whenNonValidMessagerie_shouldReturnEmptyResult(){ + + + var dateCalcul = LocalDateTime.now(); + ReferentielCritere critere = ReferentielCritere.builder() + .nomCritere("Changement Climatique") + .unite("kg CO_{2} eq") + .build(); + var referentielImpactMessagerie = ReferentielImpactMessagerie.builder() + .critere(critere.getNomCritere()) + .source("Source RFT") + .constanteOrdonneeOrigine(20d) + .constanteCoefficientDirecteur(30d) + .build(); + var messagerie = Messagerie.builder() + .dateLot(LocalDate.of(2022,1,1)) + .nomOrganisation("Test") + .nomLot("Test|20220101") + .nomSourceDonnee("Source_Test") + .moisAnnee(202211) + .nombreMailEmis(0d) + .nombreMailEmisXDestinataires(0d) + .volumeTotalMailEmis(0d) + .build(); + var demandeCalcul = DemandeCalculImpactMessagerie.builder() + .dateCalcul(dateCalcul) + .critere(critere) + .impactsMessagerie(Collections.singletonList(referentielImpactMessagerie)) + .messagerie(messagerie) + .build(); + + var impactMessagerie = calculImpactMessagerieService.calculerImpactMessagerie(demandeCalcul); + + assertContentIndicateur(demandeCalcul, impactMessagerie); + assertNull(impactMessagerie.getImpactMensuel()); + assertEquals("ERREUR", impactMessagerie.getStatutIndicateur()); + assertEquals("{\"erreur\":\"ErrCalcFonc : Calcul d'impact messagerie : Critère : Changement Climatique, Mois Années : 202211, nombreMailEmis 0.0=< 0.0\"}", impactMessagerie.getTrace()); + } + + @Test + void whenReferentielIsNotFound_shouldReturnEmptyResult(){ + + + var dateCalcul = LocalDateTime.now(); + ReferentielCritere critere = ReferentielCritere.builder() + .nomCritere("Changement Climatique") + .unite("kg CO_{2} eq") + .build(); + var messagerie = Messagerie.builder() + .dateLot(LocalDate.of(2022,1,1)) + .nomOrganisation("Test") + .nomLot("Test|20220101") + .nomSourceDonnee("Source_Test") + .moisAnnee(202211) + .nombreMailEmis(0d) + .nombreMailEmisXDestinataires(0d) + .volumeTotalMailEmis(0d) + .build(); + var demandeCalcul = DemandeCalculImpactMessagerie.builder() + .dateCalcul(dateCalcul) + .critere(critere) + .impactsMessagerie(Collections.emptyList()) + .messagerie(messagerie) + .build(); + + var impactMessagerie = calculImpactMessagerieService.calculerImpactMessagerie(demandeCalcul); + + assertContentIndicateur(demandeCalcul, impactMessagerie); + assertNull(impactMessagerie.getImpactMensuel()); + assertEquals("ERREUR", impactMessagerie.getStatutIndicateur()); + assertEquals("{\"erreur\":\"ErrCalcFonc : Référentiel ImpactMessagerie indisponible pour le critère Changement Climatique\"}", impactMessagerie.getTrace()); + } + + private static void assertContentIndicateur(DemandeCalculImpactMessagerie demandeCalcul, ImpactMessagerie impactMessagerie) { + assertNotNull(impactMessagerie); + assertEquals("1.0", impactMessagerie.getVersionCalcul()); + assertEquals(demandeCalcul.getDateCalcul(), impactMessagerie.getDateCalcul()); + assertEquals(demandeCalcul.getCritere().getNomCritere(), impactMessagerie.getCritere()); + assertEquals(demandeCalcul.getCritere().getUnite(), impactMessagerie.getUnite()); + assertEquals(demandeCalcul.getMessagerie().getMoisAnnee(), impactMessagerie.getMoisAnnee()); + assertEquals(demandeCalcul.getMessagerie().getNombreMailEmis(), impactMessagerie.getNombreMailEmis()); + assertEquals(demandeCalcul.getMessagerie().getVolumeTotalMailEmis(), impactMessagerie.getVolumeTotalMailEmis()); + assertEquals(demandeCalcul.getMessagerie().getNomLot(), impactMessagerie.getNomLot()); + assertEquals(demandeCalcul.getMessagerie().getDateLot(), impactMessagerie.getDateLot()); + assertEquals(demandeCalcul.getMessagerie().getNomEntite(), impactMessagerie.getNomEntite()); + assertEquals(demandeCalcul.getMessagerie().getNomOrganisation(), impactMessagerie.getNomOrganisation()); + assertEquals(demandeCalcul.getMessagerie().getNomSourceDonnee(), impactMessagerie.getNomSourceDonnee()); + } +} diff --git a/services/calculs/src/test/java/org/mte/numecoeval/calculs/domain/service/CalculImpactReseauServiceTest.java b/services/calculs/src/test/java/org/mte/numecoeval/calculs/domain/service/CalculImpactReseauServiceTest.java new file mode 100644 index 00000000..b20da91b --- /dev/null +++ b/services/calculs/src/test/java/org/mte/numecoeval/calculs/domain/service/CalculImpactReseauServiceTest.java @@ -0,0 +1,220 @@ +package org.mte.numecoeval.calculs.domain.service; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockitoAnnotations; +import org.mte.numecoeval.calculs.domain.data.demande.DemandeCalculImpactReseau; +import org.mte.numecoeval.calculs.domain.data.entree.EquipementPhysique; +import org.mte.numecoeval.calculs.domain.data.indicateurs.ImpactReseau; +import org.mte.numecoeval.calculs.domain.data.referentiel.ReferentielCritere; +import org.mte.numecoeval.calculs.domain.data.referentiel.ReferentielEtapeACV; +import org.mte.numecoeval.calculs.domain.data.referentiel.ReferentielImpactReseau; +import org.mte.numecoeval.calculs.domain.port.input.service.CalculImpactReseauService; +import org.mte.numecoeval.calculs.domain.port.input.service.impl.CalculImpactReseauServiceImpl; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.*; + +class CalculImpactReseauServiceTest { + + private final String REF_RESEAU = "impactReseauMobileMoyen"; + + private CalculImpactReseauService calculImpactService; + + ObjectMapper objectMapper = new ObjectMapper(); + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + calculImpactService = new CalculImpactReseauServiceImpl(objectMapper); + } + + @Test + void shouldReturnCorrectCalculImpact_whenValidInputs() { + // Given + LocalDateTime dateCalcul = LocalDateTime.now(); + ReferentielEtapeACV etapeACV = ReferentielEtapeACV.builder() + .code("UTILISATION") + .build(); + ReferentielCritere critere = ReferentielCritere.builder() + .nomCritere("Changement Climatique") + .unite("kg CO_{2} eq") + .build(); + EquipementPhysique equipementPhysique = EquipementPhysique.builder() + .dateLot(LocalDate.of(2022,1,1)) + .nomOrganisation("Test") + .nomLot("Test|20220101") + .nomSourceDonnee("Source_Test") + .nomEquipementPhysique("Equipement 1") + .goTelecharge(3.5f) + .build(); + ReferentielImpactReseau referentielImpactReseau = ReferentielImpactReseau.builder() + .etapeACV(etapeACV.getCode()) + .critere(critere.getNomCritere()) + .refReseau(REF_RESEAU) + .impactReseauMobileMoyen(4d) + .build(); + var demandeCalcul = DemandeCalculImpactReseau.builder() + .dateCalcul(dateCalcul) + .equipementPhysique(equipementPhysique) + .etape(etapeACV) + .critere(critere) + .impactsReseau(Collections.singletonList(referentielImpactReseau)) + .build(); + Double expected = 4d * 3.5f; + + //When + var impactReseau = assertDoesNotThrow(() -> calculImpactService.calculerImpactReseau(demandeCalcul)); + + //Then + assertNotNull(impactReseau.getImpactUnitaire()); + assertEquals(expected, impactReseau.getImpactUnitaire()); + assertContentIndicateur(demandeCalcul, impactReseau); + assertEquals("OK", impactReseau.getStatutIndicateur()); + assertEquals( + "{\"critere\":\"Changement Climatique\",\"etapeACV\":\"UTILISATION\",\"equipementPhysique\":\"Equipement 1\",\"impactReseauMobileMoyen\":\"4.0\",\"goTelecharge\":\"3.5\",\"formule\":\"impactReseau = 'equipementPhysique.goTelecharge (3.5) x ref_ImpactReseau(Changement Climatique, UTILISATION, impactReseauMobileMoyen).valeur(4.0)'\"}", + impactReseau.getTrace() + ); + } + + @Test + void shouldReturnError_whenInvalidInputs() { + //Given + ReferentielEtapeACV etapeACV = ReferentielEtapeACV.builder() + .code("UTILISATION") + .build(); + ReferentielCritere critere = ReferentielCritere.builder() + .nomCritere("Changement Climatique") + .unite("kg CO_{2} eq") + .build(); + EquipementPhysique equipementPhysique = EquipementPhysique.builder() + .dateLot(LocalDate.of(2022,1,1)) + .nomOrganisation("Test") + .nomLot("Test|20220101") + .nomSourceDonnee("Source_Test") + .nomEquipementPhysique("Equipement 1") + .goTelecharge(null) + .build(); + var demandeCalcul = DemandeCalculImpactReseau.builder() + .dateCalcul(LocalDateTime.now()) + .equipementPhysique(equipementPhysique) + .etape(etapeACV) + .critere(critere) + .impactsReseau(Collections.emptyList()) + .build(); + + //When + var impactReseau = calculImpactService.calculerImpactReseau(demandeCalcul); + + //Then + assertContentIndicateur(demandeCalcul, impactReseau); + assertEquals("ERREUR", impactReseau.getStatutIndicateur()); + assertNull(impactReseau.getImpactUnitaire()); + assertEquals("{\"erreur\":\"ErrCalcFonc : Erreur de calcul impact réseau: Etape: UTILISATION, Critere: Changement Climatique, Equipement Physique: Equipement 1 : la valeur en_EqP(Equipement).goTelecharge est nulle\"}", impactReseau.getTrace()); + } + + @Test + void shouldReturnError_whenInvalidReference() { + //Given + EquipementPhysique equipementPhysique = EquipementPhysique.builder() + .dateLot(LocalDate.of(2022,1,1)) + .nomOrganisation("Test") + .nomLot("Test|20220101") + .nomSourceDonnee("Source_Test") + .nomEquipementPhysique("Equipement 1") + .goTelecharge(4.0f) + .build(); + ReferentielEtapeACV etapeACV = ReferentielEtapeACV.builder() + .code("UTILISATION") + .build(); + ReferentielCritere critere = ReferentielCritere.builder() + .nomCritere("Changement Climatique") + .unite("kg CO_{2} eq") + .build(); + ReferentielImpactReseau referentielImpactReseau = ReferentielImpactReseau.builder() + .etapeACV(etapeACV.getCode()) + .critere(critere.getNomCritere()) + .refReseau(REF_RESEAU) + .build(); + var demandeCalcul = DemandeCalculImpactReseau.builder() + .dateCalcul(LocalDateTime.now()) + .equipementPhysique(equipementPhysique) + .etape(etapeACV) + .critere(critere) + .impactsReseau(Collections.singletonList(referentielImpactReseau)) + .build(); + + //When + var impactReseau = calculImpactService.calculerImpactReseau(demandeCalcul); + + //Then + assertContentIndicateur(demandeCalcul, impactReseau); + assertEquals("ERREUR", impactReseau.getStatutIndicateur()); + assertNull(impactReseau.getImpactUnitaire()); + assertEquals("{\"erreur\":\"ErrCalcFonc : Erreur de calcul impact réseau: Etape: UTILISATION, Critere: Changement Climatique, RefReseau: impactReseauMobileMoyen, Equipement Physique: Equipement 1 : L'impactReseauMobileMoyen de la référence est null.\"}", impactReseau.getTrace()); + } + + @Test + void shouldReturnError_whenReferenceNotFound() { + //Given + EquipementPhysique equipementPhysique = EquipementPhysique.builder() + .dateLot(LocalDate.of(2022,1,1)) + .nomOrganisation("Test") + .nomLot("Test|20220101") + .nomSourceDonnee("Source_Test") + .nomEquipementPhysique("Equipement 1") + .goTelecharge(4.0f) + .dateLot(LocalDate.of(2023,1,1)) + .build(); + ReferentielEtapeACV etapeACV = ReferentielEtapeACV.builder() + .code("UTILISATION") + .build(); + ReferentielCritere critere = ReferentielCritere.builder() + .nomCritere("Changement Climatique") + .unite("kg CO_{2} eq") + .build(); + ReferentielImpactReseau referentielImpactReseau = ReferentielImpactReseau.builder() + .etapeACV(etapeACV.getCode()) + .critere(critere.getNomCritere()) + .refReseau(REF_RESEAU) + .build(); + var demandeCalcul = DemandeCalculImpactReseau.builder() + .dateCalcul(LocalDateTime.now()) + .equipementPhysique(equipementPhysique) + .etape(etapeACV) + .critere(critere) + .impactsReseau(Collections.singletonList(referentielImpactReseau)) + .build(); + + //When + var impactReseau = calculImpactService.calculerImpactReseau(demandeCalcul); + + //Then + assertContentIndicateur(demandeCalcul, impactReseau); + assertEquals("ERREUR", impactReseau.getStatutIndicateur()); + assertNull(impactReseau.getImpactUnitaire()); + assertEquals("{\"erreur\":\"ErrCalcFonc : Erreur de calcul impact réseau: Etape: UTILISATION, Critere: Changement Climatique, RefReseau: impactReseauMobileMoyen, Equipement Physique: Equipement 1 : L'impactReseauMobileMoyen de la référence est null.\"}", impactReseau.getTrace()); + } + + private static void assertContentIndicateur(DemandeCalculImpactReseau demandeCalcul, ImpactReseau impactReseau) { + assertEquals(demandeCalcul.getDateCalcul(), impactReseau.getDateCalcul()); + + assertEquals(demandeCalcul.getEtape().getCode(), impactReseau.getEtapeACV()); + assertEquals(demandeCalcul.getCritere().getNomCritere(), impactReseau.getCritere()); + assertEquals(demandeCalcul.getCritere().getNomCritere(), impactReseau.getCritere()); + + assertEquals(demandeCalcul.getEquipementPhysique().getNomLot(), impactReseau.getNomLot()); + assertEquals(demandeCalcul.getEquipementPhysique().getDateLot(), impactReseau.getDateLot()); + assertEquals(demandeCalcul.getEquipementPhysique().getNomEntite(), impactReseau.getNomEntite()); + assertEquals(demandeCalcul.getEquipementPhysique().getNomOrganisation(), impactReseau.getNomOrganisation()); + assertEquals(demandeCalcul.getEquipementPhysique().getNomEquipementPhysique(), impactReseau.getNomEquipement()); + assertEquals(demandeCalcul.getEquipementPhysique().getNomSourceDonnee(), impactReseau.getNomSourceDonnee()); + + assertEquals("1.0", impactReseau.getVersionCalcul()); + } + +} \ No newline at end of file diff --git a/services/calculs/src/test/java/org/mte/numecoeval/calculs/domain/service/DureeDeVieEquipementPhysiqueServiceTest.java b/services/calculs/src/test/java/org/mte/numecoeval/calculs/domain/service/DureeDeVieEquipementPhysiqueServiceTest.java new file mode 100644 index 00000000..044d18d5 --- /dev/null +++ b/services/calculs/src/test/java/org/mte/numecoeval/calculs/domain/service/DureeDeVieEquipementPhysiqueServiceTest.java @@ -0,0 +1,228 @@ +package org.mte.numecoeval.calculs.domain.service; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mte.numecoeval.calculs.domain.data.demande.DemandeCalculImpactEquipementPhysique; +import org.mte.numecoeval.calculs.domain.data.entree.EquipementPhysique; +import org.mte.numecoeval.calculs.domain.data.erreur.TypeErreurCalcul; +import org.mte.numecoeval.calculs.domain.data.referentiel.ReferentielHypothese; +import org.mte.numecoeval.calculs.domain.data.referentiel.ReferentielTypeEquipement; +import org.mte.numecoeval.calculs.domain.data.trace.DureeDeVie; +import org.mte.numecoeval.calculs.domain.exception.CalculImpactException; +import org.mte.numecoeval.calculs.domain.port.input.service.DureeDeVieEquipementPhysiqueService; +import org.mte.numecoeval.calculs.domain.port.input.service.impl.DureeDeVieEquipementPhysiqueServiceImpl; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.*; + +@ExtendWith(MockitoExtension.class) +class DureeDeVieEquipementPhysiqueServiceTest { + + private final DureeDeVieEquipementPhysiqueService dureeDeVieEquipementPhysiqueService = new DureeDeVieEquipementPhysiqueServiceImpl(); + + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("d/MM/yyyy"); + + @Test + void whenTypeEquipementDoesntHaveDureeDeVie_shouldUseHypotheseForDureeDeVie()throws Exception{ + + //Given + EquipementPhysique equipementPhysique = EquipementPhysique.builder() + .type("serveur") + .build(); + ReferentielHypothese hypothese = ReferentielHypothese.builder() + .code("dureeVieParDefaut") + .valeur(1.17d) + .build(); + ReferentielTypeEquipement typeEquipement = ReferentielTypeEquipement.builder() + .type("serveur") + .dureeVieDefaut(null) + .build(); + DemandeCalculImpactEquipementPhysique demandeCalcul = DemandeCalculImpactEquipementPhysique + .builder() + .equipementPhysique(equipementPhysique) + .hypotheses(Collections.singletonList(hypothese)) + .typeEquipement(typeEquipement) + .build(); + + //When + var actual = dureeDeVieEquipementPhysiqueService.calculerDureeVie(demandeCalcul); + + //Then + assertEquals(1.17d,actual.getValeur()); + } + + + @Test + void shouldCalculerDureeVie1d_whenEquipementPhyisqueWithValidDates() throws Exception{ + //Given + LocalDate dateAchat = LocalDate.parse("15/08/2020",formatter); + LocalDate dateRetrait = LocalDate.parse("15/06/2021",formatter); + var equipement = EquipementPhysique.builder() + .dateAchat(dateAchat) + .dateRetrait(dateRetrait) + .build(); + DemandeCalculImpactEquipementPhysique demandeCalcul = DemandeCalculImpactEquipementPhysique + .builder() + .equipementPhysique(equipement) + .typeEquipement(null) + .build(); + //When + var actual = dureeDeVieEquipementPhysiqueService.calculerDureeVie(demandeCalcul); + + //Then + assertEquals(1d,actual.getValeur()); + } + + @Test + void shouldCalculerDureeVie365_whenEquipementPhyisqueWithValidDates()throws Exception{ + //Given + LocalDate dateAchat = LocalDate.parse("15/08/2020",formatter); + LocalDate dateRetrait = LocalDate.parse("15/10/2021",formatter); + var equipement = EquipementPhysique.builder() + .dateAchat(dateAchat) + .dateRetrait(dateRetrait) + .build(); + DemandeCalculImpactEquipementPhysique demandeCalcul = DemandeCalculImpactEquipementPhysique + .builder() + .equipementPhysique(equipement) + .typeEquipement(null) + .build(); + //When + var actual = dureeDeVieEquipementPhysiqueService.calculerDureeVie(demandeCalcul); + var expected = 426d/365d; + //Then + assertEquals(expected,actual.getValeur()); + } + + @Test + void taiga864_whenEquipementPhysiqueDateRetraitIsNullShouldUseCurrentDayAsDateRetrait()throws Exception{ + //Given + LocalDate dateAchat = LocalDate.now().minusDays(30); + var equipement = EquipementPhysique.builder() + .dateAchat(dateAchat) + .dateRetrait(null) + .build(); + DemandeCalculImpactEquipementPhysique demandeCalcul = DemandeCalculImpactEquipementPhysique + .builder() + .equipementPhysique(equipement) + .typeEquipement(null) + .build(); + //When + var actual = dureeDeVieEquipementPhysiqueService.calculerDureeVie(demandeCalcul); + var expected = 30/365d; + //Then + assertEquals(expected,actual.getValeur()); + assertEquals(dateAchat.format(DateTimeFormatter.ISO_DATE),actual.getDateAchat()); + assertEquals(LocalDate.now().format(DateTimeFormatter.ISO_DATE),actual.getDateRetrait()); + } + + @Test + void shouldThrowException_whenInvalideDatesOrder()throws Exception{ + //Given + LocalDate dateRetrait= LocalDate.parse("15/08/2020",formatter); + LocalDate dateAchat = LocalDate.parse("15/10/2021",formatter); + var equipement = EquipementPhysique.builder() + .dateAchat(dateAchat) + .dateRetrait(dateRetrait) + .build(); + DemandeCalculImpactEquipementPhysique demandeCalcul = DemandeCalculImpactEquipementPhysique + .builder() + .equipementPhysique(equipement) + .typeEquipement(null) + .build(); + //When + CalculImpactException exception = + //Then + assertThrows(CalculImpactException.class, + ()-> dureeDeVieEquipementPhysiqueService.calculerDureeVie(demandeCalcul)); + + assertEquals(TypeErreurCalcul.ERREUR_FONCTIONNELLE.getCode(), exception.getErrorType()); + String expectedMessage = "La durée de vie de l'équipement n'a pas pu être déterminée"; + String actualMessage = exception.getMessage(); + assertTrue(actualMessage.contains(expectedMessage)); + } + + @Test + void shouldReturnDureeVieDefaut_whenMissingDate()throws Exception{ + //Given + var equipement = EquipementPhysique.builder() + .type("laptop") + .build(); + ReferentielHypothese hypothese = ReferentielHypothese.builder() + .code("dureeVieParDefaut") + .valeur(1.17d) + .build(); + DemandeCalculImpactEquipementPhysique demandeCalcul = DemandeCalculImpactEquipementPhysique + .builder() + .equipementPhysique(equipement) + .hypotheses(Collections.singletonList(hypothese)) + .typeEquipement(null) + .build(); + //When + DureeDeVie actual = dureeDeVieEquipementPhysiqueService.calculerDureeVie(demandeCalcul); + + //Then + assertEquals(1.17d,actual.getValeur()); + } + + @Test + void whenMissingDate_ShouldUseDureeDeVieTypeEquipementInPriority()throws Exception{ + //Given + var equipement = EquipementPhysique.builder() + .type("laptop") + .build(); + ReferentielHypothese hypothese = ReferentielHypothese.builder() + .code("dureeVieParDefaut") + .valeur(1.17d) + .build(); + ReferentielTypeEquipement typeEquipement = ReferentielTypeEquipement.builder() + .type("laptop") + .dureeVieDefaut(3.5d) + .build(); + DemandeCalculImpactEquipementPhysique demandeCalcul = DemandeCalculImpactEquipementPhysique + .builder() + .equipementPhysique(equipement) + .typeEquipement(typeEquipement) + .hypotheses(Collections.singletonList(hypothese)) + .build(); + //When + DureeDeVie actual = dureeDeVieEquipementPhysiqueService.calculerDureeVie(demandeCalcul); + + //Then + assertEquals(3.5d,actual.getValeur()); + } + + @Test + void whenMissingDureeForTypeAndHypothese_ShouldThrowException()throws Exception{ + //Given + var equipement = EquipementPhysique.builder() + .type("laptop") + .build(); + ReferentielHypothese hypothese = ReferentielHypothese.builder() + .code("dureeVieParDefaut") + .valeur(null) + .build(); + ReferentielTypeEquipement typeEquipement = ReferentielTypeEquipement.builder() + .type("laptop") + .dureeVieDefaut(null) + .build(); + DemandeCalculImpactEquipementPhysique demandeCalcul = DemandeCalculImpactEquipementPhysique + .builder() + .equipementPhysique(equipement) + .typeEquipement(typeEquipement) + .hypotheses(Collections.singletonList(hypothese)) + .build(); + //When + CalculImpactException exception = assertThrows(CalculImpactException.class, () -> dureeDeVieEquipementPhysiqueService.calculerDureeVie(demandeCalcul)); + + //Then + assertNotNull(exception, "Une exception doit être levée"); + assertEquals(TypeErreurCalcul.ERREUR_FONCTIONNELLE.getCode(), exception.getErrorType()); + assertEquals("La durée de vie par défaut de l'équipement n'a pas pu être déterminée", exception.getMessage()); + } + +} \ No newline at end of file diff --git a/services/calculs/src/test/java/org/mte/numecoeval/calculs/steps/CalculImpactEquipementVirtuelStepDefinitions.java b/services/calculs/src/test/java/org/mte/numecoeval/calculs/steps/CalculImpactEquipementVirtuelStepDefinitions.java new file mode 100644 index 00000000..fb172819 --- /dev/null +++ b/services/calculs/src/test/java/org/mte/numecoeval/calculs/steps/CalculImpactEquipementVirtuelStepDefinitions.java @@ -0,0 +1,51 @@ +package org.mte.numecoeval.calculs.steps; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.cucumber.datatable.DataTable; +import io.cucumber.java.fr.Alors; +import io.cucumber.java.fr.Soit; +import org.mte.numecoeval.calculs.domain.data.demande.DemandeCalculImpactEquipementVirtuel; +import org.mte.numecoeval.calculs.domain.port.input.service.CalculImpactEquipementVirtuelService; +import org.mte.numecoeval.calculs.domain.port.input.service.impl.CalculImpactEquipementVirtuelServiceImpl; + +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +public class CalculImpactEquipementVirtuelStepDefinitions { + + DemandeCalculImpactEquipementVirtuel demandeCalculCourante; + + @Soit("Une demande de calcul pour un équipement virtuel tel que") + public void initDemandeCalculEquipementVirtuel(DemandeCalculImpactEquipementVirtuel demande) { + demandeCalculCourante = demande; + } + + @Alors("l'impact d'équipement virtuel résultant est tel que") + public void checkImpactEquipementVirtuel(DataTable dataTable) { + CalculImpactEquipementVirtuelService service = new CalculImpactEquipementVirtuelServiceImpl(new ObjectMapper()); + var result = assertDoesNotThrow(() -> service.calculerImpactEquipementVirtuel(demandeCalculCourante)); + + assertNotNull(result); + Map<String, String> entry = dataTable.entries().get(0); + assertNotNull(entry); + + DataTableTypeDefinitions.assertStringIfAvailable(result.getVersionCalcul(), entry, "versionCalcul"); + DataTableTypeDefinitions.assertStringIfAvailable(result.getNomLot(), entry, "nomLot"); + DataTableTypeDefinitions.assertDateIfAvailable(result.getDateLot(), entry, "dateLot"); + DataTableTypeDefinitions.assertStringIfAvailable(result.getNomOrganisation(), entry, "nomOrganisation"); + DataTableTypeDefinitions.assertStringIfAvailable(result.getNomEntite(), entry, "nomEntite"); + DataTableTypeDefinitions.assertStringIfAvailable(result.getCritere(), entry, "critere"); + DataTableTypeDefinitions.assertStringIfAvailable(result.getEtapeACV(), entry, "etapeACV"); + DataTableTypeDefinitions.assertStringIfAvailable(result.getStatutIndicateur(), entry, "statutIndicateur"); + DataTableTypeDefinitions.assertDoubleIfAvailable(result.getImpactUnitaire(), entry, "impactUnitaire"); + DataTableTypeDefinitions.assertDoubleIfAvailable(result.getConsoElecMoyenne(), entry, "consoElecMoyenne"); + DataTableTypeDefinitions.assertStringIfAvailable(result.getNomSourceDonnee(), entry, "nomSourceDonnee"); + DataTableTypeDefinitions.assertStringIfAvailable(result.getUnite(), entry, "unite"); + DataTableTypeDefinitions.assertStringIfAvailable(result.getNomEquipementVirtuel(), entry, "nomEquipementVirtuel"); + DataTableTypeDefinitions.assertStringIfAvailable(result.getNomEquipement(), entry, "nomEquipement"); + DataTableTypeDefinitions.assertStringIfAvailable(result.getCluster(), entry, "cluster"); + DataTableTypeDefinitions.assertStringIfAvailable(result.getTrace(), entry, "trace"); + } +} diff --git a/services/calculs/src/test/java/org/mte/numecoeval/calculs/steps/DataTableTypeDefinitions.java b/services/calculs/src/test/java/org/mte/numecoeval/calculs/steps/DataTableTypeDefinitions.java new file mode 100644 index 00000000..cf75dd9b --- /dev/null +++ b/services/calculs/src/test/java/org/mte/numecoeval/calculs/steps/DataTableTypeDefinitions.java @@ -0,0 +1,152 @@ +package org.mte.numecoeval.calculs.steps; + +import io.cucumber.java.DataTableType; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.mte.numecoeval.calculs.CucumberIntegrationTest; +import org.mte.numecoeval.calculs.domain.data.demande.DemandeCalculImpactEquipementPhysique; +import org.mte.numecoeval.calculs.domain.data.demande.DemandeCalculImpactEquipementVirtuel; +import org.mte.numecoeval.calculs.domain.data.entree.EquipementPhysique; +import org.mte.numecoeval.calculs.domain.data.entree.EquipementVirtuel; +import org.mte.numecoeval.calculs.domain.data.indicateurs.ImpactEquipementPhysique; +import org.mte.numecoeval.calculs.domain.data.referentiel.ReferentielHypothese; +import org.mte.numecoeval.calculs.domain.data.referentiel.ReferentielTypeEquipement; + +import java.text.ParseException; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mte.numecoeval.calculs.CucumberIntegrationTest.NUMBER_FRENCH_FORMAT; + +@Slf4j +public class DataTableTypeDefinitions { + + public static void assertDateIfAvailable(LocalDate valueToTest, Map<String, String> entry, String key) { + assertEquals(getDateOrNull(entry, key), valueToTest); + } + public static void assertDoubleIfAvailable(Double valueToTest, Map<String, String> entry, String key) { + assertEquals(getDoubleOrNull(entry, key), valueToTest); + } + public static void assertIntegerIfAvailable(Integer valueToTest, Map<String, String> entry, String key) { + assertEquals(getIntegerOrNull(entry, key), valueToTest); + } + public static void assertStringIfAvailable(String valueToTest, Map<String, String> entry, String key) { + assertEquals(entry.getOrDefault(key, null), valueToTest); + } + + private static LocalDate getDateOrNull(Map<String, String> entry, String key) { + if( StringUtils.isNotBlank(entry.get(key)) ) { + return LocalDate.parse(entry.get(key), CucumberIntegrationTest.FORMATTER_FRENCH_FORMAT); + } + return null; + } + + private static Double getDoubleOrNull(Map<String, String> entry, String key) { + if( StringUtils.isNotBlank(entry.get(key)) ) { + try { + return NUMBER_FRENCH_FORMAT.parse(entry.get(key)).doubleValue(); + } + catch (ParseException e) { + log.error("Erreur au parsing de la valeur {} : {}", entry.get(key), e.getMessage()); + return null; + } + } + return null; + } + + private static Integer getIntegerOrNull(Map<String, String> entry, String key) { + if( StringUtils.isNotBlank(entry.get(key)) ) { + try { + return NUMBER_FRENCH_FORMAT.parse(entry.get(key)).intValue(); + } + catch (ParseException e) { + log.error("Erreur au parsing de la valeur {} : {}", entry.get(key), e.getMessage()); + return null; + } + } + return null; + } + + @DataTableType + public DemandeCalculImpactEquipementPhysique demandeCalculImpactEquipementPhysique(Map<String, String> entry) { + var builder = DemandeCalculImpactEquipementPhysique.builder() + .dateCalcul(LocalDateTime.now()) + .equipementPhysique( + EquipementPhysique.builder() + .nomEquipementPhysique(entry.getOrDefault("nomEquipementPhysique", null)) + .dateAchat(getDateOrNull(entry, "dateAchat")) + .dateRetrait(getDateOrNull(entry, "dateRetrait")) + .build() + ); + var hypotheses = new ArrayList<ReferentielHypothese>(); + + if(StringUtils.isNotBlank(entry.get("refTypeEquipement.dureeVieDefaut"))) { + builder.typeEquipement( + ReferentielTypeEquipement.builder() + .dureeVieDefaut(getDoubleOrNull(entry, "refTypeEquipement.dureeVieDefaut")) + .build() + ); + } + + if(StringUtils.isNotBlank(entry.get("hypothese.dureeVieParDefaut"))) { + hypotheses.add( + ReferentielHypothese.builder() + .code("dureeVieParDefaut") + .valeur(getDoubleOrNull(entry, "hypothese.dureeVieParDefaut")) + .build() + ); + } + + builder.hypotheses(hypotheses); + + return builder.build(); + } + + @DataTableType + public DemandeCalculImpactEquipementVirtuel demandeCalculImpactEquipementVirtuel(Map<String, String> entry) { + var builder = DemandeCalculImpactEquipementVirtuel.builder() + .dateCalcul(LocalDateTime.now()) + .nbEquipementsVirtuels(getIntegerOrNull(entry, "nbEquipementsVirtuels")) + .nbTotalVCPU(getIntegerOrNull(entry, "nbTotalVCPU")) + .stockageTotalVirtuel(getDoubleOrNull(entry, "stockageTotalVirtuel")) + .equipementVirtuel( + EquipementVirtuel.builder() + .nomLot(entry.getOrDefault("equipementVirtuel.nomLot", null)) + .dateLot(getDateOrNull(entry, "equipementVirtuel.dateLot")) + .nomOrganisation(entry.getOrDefault("equipementVirtuel.nomOrganisation", null)) + .nomEntite(entry.getOrDefault("equipementVirtuel.nomEntite", null)) + .nomSourceDonnee(entry.getOrDefault("equipementVirtuel.nomSourceDonnee", null)) + .nomEquipementVirtuel(entry.getOrDefault("equipementVirtuel.nomEquipementVirtuel", null)) + .cluster(entry.getOrDefault("equipementVirtuel.cluster", null)) + .nomEquipementPhysique(entry.getOrDefault("equipementVirtuel.nomEquipementPhysique", null)) + .typeEqv(entry.getOrDefault("equipementVirtuel.typeEqv", null)) + .vCPU(getIntegerOrNull(entry, "equipementVirtuel.vCPU")) + .consoElecAnnuelle(getDoubleOrNull(entry, "equipementVirtuel.consoElecAnnuelle")) + .capaciteStockage(getDoubleOrNull(entry, "equipementVirtuel.capaciteStockage")) + .cleRepartition(getDoubleOrNull(entry, "equipementVirtuel.cleRepartition")) + .build() + ); + + var impactEquipement = ImpactEquipementPhysique.builder().build(); + impactEquipement.setStatutIndicateur(entry.getOrDefault("impactEquipementPhysique.statutIndicateur", null)); + impactEquipement.setEtapeACV(entry.getOrDefault("impactEquipementPhysique.etape", null)); + impactEquipement.setCritere(entry.getOrDefault("impactEquipementPhysique.critere", null)); + impactEquipement.setUnite(entry.getOrDefault("impactEquipementPhysique.unite", null)); + impactEquipement.setNomLot(entry.getOrDefault("impactEquipementPhysique.nomLot", null)); + impactEquipement.setNomOrganisation(entry.getOrDefault("impactEquipementPhysique.nomOrganisation", null)); + impactEquipement.setDateLot(getDateOrNull(entry, "impactEquipementPhysique.dateLot")); + impactEquipement.setNomEntite(entry.getOrDefault("impactEquipementPhysique.nomEntite", null)); + if(StringUtils.isNotBlank(entry.get("impactEquipementPhysique.impactUnitaire"))) { + impactEquipement.setImpactUnitaire(getDoubleOrNull(entry, "impactEquipementPhysique.impactUnitaire")); + } + if(StringUtils.isNotBlank(entry.get("impactEquipementPhysique.consoElecMoyenne"))) { + impactEquipement.setConsoElecMoyenne(getDoubleOrNull(entry, "impactEquipementPhysique.consoElecMoyenne")); + } + builder.impactEquipement(impactEquipement); + + return builder.build(); + } +} diff --git a/services/calculs/src/test/java/org/mte/numecoeval/calculs/steps/DureeDeVieStepDefinitions.java b/services/calculs/src/test/java/org/mte/numecoeval/calculs/steps/DureeDeVieStepDefinitions.java new file mode 100644 index 00000000..abeee6a6 --- /dev/null +++ b/services/calculs/src/test/java/org/mte/numecoeval/calculs/steps/DureeDeVieStepDefinitions.java @@ -0,0 +1,86 @@ +package org.mte.numecoeval.calculs.steps; + +import io.cucumber.java.fr.Alors; +import io.cucumber.java.fr.Soit; +import org.mte.numecoeval.calculs.domain.data.demande.DemandeCalculImpactEquipementPhysique; +import org.mte.numecoeval.calculs.domain.data.entree.EquipementPhysique; +import org.mte.numecoeval.calculs.domain.data.trace.DureeDeVie; +import org.mte.numecoeval.calculs.domain.exception.CalculImpactException; +import org.mte.numecoeval.calculs.domain.port.input.service.DureeDeVieEquipementPhysiqueService; +import org.mte.numecoeval.calculs.domain.port.input.service.impl.DureeDeVieEquipementPhysiqueServiceImpl; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mte.numecoeval.calculs.CucumberIntegrationTest.FORMATTER_FRENCH_FORMAT; + +public class DureeDeVieStepDefinitions { + + DemandeCalculImpactEquipementPhysique demandeCalculCourante; + + @Soit("Un équipement physique {string} avec la date d'achat {string} et la date de retrait {string}") + public void soitEquipementPhysiqueAvecDateAchatEtDateRetrait(String nomEquipementPhysique, String dateAchat, String dateRetrait) { + demandeCalculCourante = DemandeCalculImpactEquipementPhysique.builder() + .dateCalcul(LocalDateTime.now()) + .equipementPhysique( + EquipementPhysique.builder() + .nomEquipementPhysique(nomEquipementPhysique) + .dateAchat(LocalDate.parse(dateAchat, FORMATTER_FRENCH_FORMAT)) + .dateRetrait(LocalDate.parse(dateRetrait, FORMATTER_FRENCH_FORMAT)) + .build() + ) + .build(); + } + + @Soit("Un équipement physique {string} avec la date d'achat {string}") + public void soitEquipementPhysiqueAvecDateAchat(String nomEquipementPhysique, String dateAchat) { + demandeCalculCourante = DemandeCalculImpactEquipementPhysique.builder() + .dateCalcul(LocalDateTime.now()) + .equipementPhysique( + EquipementPhysique.builder() + .nomEquipementPhysique(nomEquipementPhysique) + .dateAchat(LocalDate.parse(dateAchat, FORMATTER_FRENCH_FORMAT)) + .dateRetrait(null) + .build() + ) + .build(); + } + + @Alors("la durée de vie de l'équipement est {double}") + public void checkDureeDeVie(Double dureeDeVieAttendue) { + + DureeDeVieEquipementPhysiqueService service = new DureeDeVieEquipementPhysiqueServiceImpl(); + DureeDeVie result = assertDoesNotThrow(() -> service.calculerDureeVie(demandeCalculCourante)); + + assertNotNull(result); + assertEquals(dureeDeVieAttendue, result.getValeur()); + } + + @Alors("la durée de vie correspond à la différence entre la date d'achat et la date du jour") + public void checkDureeDeVieDateDuJour() { + + DureeDeVieEquipementPhysiqueService service = new DureeDeVieEquipementPhysiqueServiceImpl(); + DureeDeVie result = assertDoesNotThrow(() -> service.calculerDureeVie(demandeCalculCourante)); + + assertNotNull(result); + double differenceAvecDateDuJour = ChronoUnit.DAYS.between(demandeCalculCourante.getEquipementPhysique().getDateAchat(), LocalDate.now()) / 365d; + assertEquals(differenceAvecDateDuJour, result.getValeur()); + } + + @Soit("Une demande de calcule de durée de vie tel que") + public void initDemandeCalculDureeDeVie(DemandeCalculImpactEquipementPhysique demande) { + demandeCalculCourante = demande; + } + + @Alors("le calcul de durée de vie lève une erreur de type {string} avec le message {string}") + public void checkErreurCalculDureeDeVie(String typeErreur, String messageErreur) { + DureeDeVieEquipementPhysiqueService service = new DureeDeVieEquipementPhysiqueServiceImpl(); + CalculImpactException result = assertThrows(CalculImpactException.class, () -> service.calculerDureeVie(demandeCalculCourante)); + + assertNotNull(result); + assertEquals(typeErreur, result.getErrorType()); + assertEquals(messageErreur, result.getMessage()); + } +} diff --git a/services/calculs/src/test/resources/application-test.yaml b/services/calculs/src/test/resources/application-test.yaml new file mode 100644 index 00000000..13d59437 --- /dev/null +++ b/services/calculs/src/test/resources/application-test.yaml @@ -0,0 +1,70 @@ +# Application +numecoeval: + kafka: + topic: + name: "data" + referentiel: + endpoint: + typesEquipement: "/referentiel/typesEquipement" + server: + url: "http://localhost:19090" + +# Serveur Web +server: + port: 18080 + tomcat: + mbeanregistry: + enabled: true + +# Actuator +management: + server: + port: 18080 + security: + user: + name: "test" + password: "test" + roles: ACTUATOR_ADMIN + endpoints: + web: + exposure: + include: health,prometheus,httptrace,info,metrics,mappings + +#CONFIGURATION BASES +spring: + sql: + init: + mode: never + platform: hsqldb + datasource: + url: jdbc:hsqldb:mem:testdb;DB_CLOSE_DELAY=-1;sql.syntax_pgs=true + username: sa + password: sa + driverClassName: org.hsqldb.jdbc.JDBCDriver + jpa: + database-platform: org.hibernate.dialect.H2Dialect + hibernate: + ddl-auto: create-drop + show-sql: true + # Kafka + kafka: + client-id: api-exposition + bootstrap-servers: localhost:19093 + producer: + key-serializer: org.apache.kafka.common.serialization.StringSerializer + value-serializer: org.springframework.kafka.support.serializer.JsonSerializer + properties: + spring: + json: + trusted: + packages: "org.mte.numecoeval.topic.*" + # Uniquement pour les tests + consumer: + group-id: api-exposition-test + key-deserializer: org.apache.kafka.common.serialization.StringDeserializer + value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer + properties: + spring: + json: + trusted: + packages: "*" diff --git a/services/calculs/src/test/resources/logback-test.xml b/services/calculs/src/test/resources/logback-test.xml new file mode 100644 index 00000000..9aee4ea0 --- /dev/null +++ b/services/calculs/src/test/resources/logback-test.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="UTF-8"?> +<configuration> +<!-- Spring default.xml --> + <conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter" /> + <conversionRule conversionWord="wex" converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter" /> + <conversionRule conversionWord="wEx" converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter" /> + + <property name="CONSOLE_LOG_PATTERN" value="${CONSOLE_LOG_PATTERN:-%clr(%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-42.42logger{0}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/> + <property name="CONSOLE_LOG_CHARSET" value="${CONSOLE_LOG_CHARSET:-${file.encoding:-UTF-8}}"/> + <property name="FILE_LOG_PATTERN" value="${FILE_LOG_PATTERN:-%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}} ${LOG_LEVEL_PATTERN:-%5p} ${PID:- } --- [%t] %-42.42logger{0} : %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/> + <property name="FILE_LOG_CHARSET" value="${FILE_LOG_CHARSET:-${file.encoding:-UTF-8}}"/> + +<!-- console-appender.xml--> + <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> + <encoder> + <pattern>${CONSOLE_LOG_PATTERN}</pattern> + <charset>${CONSOLE_LOG_CHARSET}</charset> + </encoder> + </appender> + + <root level="INFO"> + <appender-ref ref="CONSOLE" /> + </root> + <logger name="org.springframework.web" level="INFO"/> + <logger name="org.springframework.kafka" level="ERROR"/> + <logger name="org.apache.kafka" level="ERROR"/> + <logger name="kafka" level="ERROR"/> + <logger name="kafka.zookeeper" level="ERROR"/> +</configuration> \ No newline at end of file diff --git a/services/calculs/src/test/resources/org/mte/numecoeval/calculs/CalculImpactUnitaire-EquipementVirtuel.feature b/services/calculs/src/test/resources/org/mte/numecoeval/calculs/CalculImpactUnitaire-EquipementVirtuel.feature new file mode 100644 index 00000000..dd7ef8de --- /dev/null +++ b/services/calculs/src/test/resources/org/mte/numecoeval/calculs/CalculImpactUnitaire-EquipementVirtuel.feature @@ -0,0 +1,93 @@ +#language: fr +#encoding: utf-8 +Fonctionnalité: Calcul d'impact d'une machine virtuelle + + Scénario: Calcul d'impact d'équipement Virtuel - Erreur car indicateur d'origine à null + Soit Une demande de calcul pour un équipement virtuel tel que + | nbEquipementsVirtuels | nbTotalVCPU | stockageTotalVirtuel | equipementVirtuel.nomLot | equipementVirtuel.dateLot | equipementVirtuel.nomOrganisation | equipementVirtuel.nomEntite | equipementVirtuel.cluster | equipementVirtuel.nomSourceDonnee | equipementVirtuel.nomEquipementVirtuel | equipementVirtuel.nomEquipementPhysique | equipementVirtuel.typeEqv | equipementVirtuel.vCPU | equipementVirtuel.consoElecAnnuelle | equipementVirtuel.capaciteStockage | equipementVirtuel.cleRepartition | impactEquipementPhysique.statutIndicateur | impactEquipementPhysique.impactUnitaire | impactEquipementPhysique.consoElecMoyenne | impactEquipementPhysique.etape | impactEquipementPhysique.critere | impactEquipementPhysique.unite | impactEquipementPhysique.nomLot | impactEquipementPhysique.dateLot | impactEquipementPhysique.nomOrganisation | impactEquipementPhysique.nomEntite | + | 3 | 19 | | 01/01/2022-Tests | 01/01/2022 | Tests | CAT0 | Test | Gherkin | Test | Test | calcul | 7 | | | | ERREUR | 234,62784 | 100 | UTILISATION | Changement climatique | kgCO2eq | Tests-01/01/2022 | 01/01/2022 | Tests | CAT0 | + Alors l'impact d'équipement virtuel résultant est tel que + | versionCalcul | nomLot | dateLot | nomOrganisation | nomEntite | critere | etapeACV | statutIndicateur | impactUnitaire | consoElecMoyenne | nomSourceDonnee | unite | nomEquipementVirtuel | nomEquipement | cluster | trace | + | 1.1 | 01/01/2022-Tests | 01/01/2022 | Tests | CAT0 | Changement climatique | UTILISATION | ERREUR | | | Gherkin | kgCO2eq | Test | Test | Test | {"erreur":"ErrCalcFonc : L'indicateur d'impact équipement associé est au statut : ERREUR"} | + + Scénario: Calcul d'impact d'équipement Virtuel - Cas passant technique + # tableau complet avec tous les champs renseignés + Soit Une demande de calcul pour un équipement virtuel tel que + | nbEquipementsVirtuels | nbTotalVCPU | stockageTotalVirtuel | equipementVirtuel.nomLot | equipementVirtuel.dateLot | equipementVirtuel.nomOrganisation | equipementVirtuel.nomEntite | equipementVirtuel.cluster | equipementVirtuel.nomSourceDonnee | equipementVirtuel.nomEquipementVirtuel | equipementVirtuel.nomEquipementPhysique | equipementVirtuel.typeEqv | equipementVirtuel.vCPU | equipementVirtuel.consoElecAnnuelle | equipementVirtuel.capaciteStockage | equipementVirtuel.cleRepartition | impactEquipementPhysique.statutIndicateur | impactEquipementPhysique.impactUnitaire | impactEquipementPhysique.consoElecMoyenne | impactEquipementPhysique.etape | impactEquipementPhysique.critere | impactEquipementPhysique.unite | impactEquipementPhysique.nomLot | impactEquipementPhysique.dateLot | impactEquipementPhysique.nomOrganisation | impactEquipementPhysique.nomEntite | + | 3 | 19 | | 01/01/2022-Tests | 01/01/2022 | Tests | CAT1 | Test | Gherkin | Test | Test | calcul | 7 | | | | OK | 234,62784 | 100 | UTILISATION | Changement climatique | kgCO2eq | Tests-01/01/2022 | 01/01/2022 | Tests | CAT1 | + Alors l'impact d'équipement virtuel résultant est tel que + | versionCalcul | nomLot | dateLot | nomOrganisation | nomEntite | critere | etapeACV | statutIndicateur | impactUnitaire | consoElecMoyenne | nomSourceDonnee | unite | nomEquipementVirtuel | nomEquipement | cluster | trace | + | 1.1 | 01/01/2022-Tests | 01/01/2022 | Tests | CAT1 | Changement climatique | UTILISATION | OK | 86,44183578947367d | 36,8421052631579d | Gherkin | kgCO2eq | Test | Test | Test | {"formule":"valeurImpactUnitaire = valeurImpactEquipementPhysique(234.62784) * equipementVirtuel.vCPU(7) / nbvCPU(19)","nbEquipementsVirtuels":3,"nbTotalVCPU":19} | + + Scénario: Calcul d'impact d'équipement Virtuel - Cas passant technique - Alternatif sans consoElecMoyenne + # tableau complet avec tous les champs renseignés + Soit Une demande de calcul pour un équipement virtuel tel que + | nbEquipementsVirtuels | nbTotalVCPU | stockageTotalVirtuel | equipementVirtuel.nomLot | equipementVirtuel.dateLot | equipementVirtuel.nomOrganisation | equipementVirtuel.nomEntite | equipementVirtuel.cluster | equipementVirtuel.nomSourceDonnee | equipementVirtuel.nomEquipementVirtuel | equipementVirtuel.nomEquipementPhysique | equipementVirtuel.typeEqv | equipementVirtuel.vCPU | equipementVirtuel.consoElecAnnuelle | equipementVirtuel.capaciteStockage | equipementVirtuel.cleRepartition | impactEquipementPhysique.statutIndicateur | impactEquipementPhysique.impactUnitaire | impactEquipementPhysique.consoElecMoyenne | impactEquipementPhysique.etape | impactEquipementPhysique.critere | impactEquipementPhysique.unite | impactEquipementPhysique.nomLot | impactEquipementPhysique.dateLot | impactEquipementPhysique.nomOrganisation | impactEquipementPhysique.nomEntite | + | 3 | 19 | | 01/01/2022-Tests | 01/01/2022 | Tests | CAT1 | Test | Gherkin | Test | Test | calcul | 7 | | | | OK | 234,62784 | | UTILISATION | Changement climatique | kgCO2eq | Tests-01/01/2022 | 01/01/2022 | Tests | CAT1 | + Alors l'impact d'équipement virtuel résultant est tel que + | versionCalcul | nomLot | dateLot | nomOrganisation | nomEntite | critere | etapeACV | statutIndicateur | impactUnitaire | consoElecMoyenne | nomSourceDonnee | unite | nomEquipementVirtuel | nomEquipement | cluster | trace | + | 1.1 | 01/01/2022-Tests | 01/01/2022 | Tests | CAT1 | Changement climatique | UTILISATION | OK | 86,44183578947367d | | Gherkin | kgCO2eq | Test | Test | Test | {"formule":"valeurImpactUnitaire = valeurImpactEquipementPhysique(234.62784) * equipementVirtuel.vCPU(7) / nbvCPU(19)","nbEquipementsVirtuels":3,"nbTotalVCPU":19} | + + Scénario: Calcul d'impact d'équipement Virtuel - CAF1 + Soit Une demande de calcul pour un équipement virtuel tel que + | nbEquipementsVirtuels | nbTotalVCPU | stockageTotalVirtuel | equipementVirtuel.nomLot | equipementVirtuel.dateLot | equipementVirtuel.nomOrganisation | equipementVirtuel.nomEntite | equipementVirtuel.cluster | equipementVirtuel.nomSourceDonnee | equipementVirtuel.nomEquipementVirtuel | equipementVirtuel.nomEquipementPhysique | equipementVirtuel.typeEqv | equipementVirtuel.vCPU | equipementVirtuel.consoElecAnnuelle | equipementVirtuel.capaciteStockage | equipementVirtuel.cleRepartition | impactEquipementPhysique.statutIndicateur | impactEquipementPhysique.impactUnitaire | impactEquipementPhysique.consoElecMoyenne | impactEquipementPhysique.etape | impactEquipementPhysique.critere | impactEquipementPhysique.unite | impactEquipementPhysique.nomLot | impactEquipementPhysique.dateLot | impactEquipementPhysique.nomOrganisation | impactEquipementPhysique.nomEntite | + | 10 | | | 01/01/2022-Tests | 01/01/2022 | Tests | CAF1 | Test | Gherkin | VM1 | Test | calcul | 8 | | 5,0 | 0,2 | OK | 2840,00 | 2840,00 | UTILISATION | Changement climatique | kgCO2eq | Tests-01/01/2022 | 01/01/2022 | Tests | CAF1 | + Alors l'impact d'équipement virtuel résultant est tel que + | versionCalcul | nomLot | dateLot | nomOrganisation | nomEntite | critere | etapeACV | statutIndicateur | impactUnitaire | consoElecMoyenne | nomSourceDonnee | unite | nomEquipementVirtuel | nomEquipement | cluster | trace | + | 1.1 | 01/01/2022-Tests | 01/01/2022 | Tests | CAF1 | Changement climatique | UTILISATION | OK | 568,0 | 568,0 | Gherkin | kgCO2eq | VM1 | Test | Test | {"formule":"valeurImpactUnitaire = valeurImpactEquipementPhysique(2840.0) * equipementVirtuel.cleRepartition(0.2)"} | + + Scénario: Calcul d'impact d'équipement Virtuel - CAF1 - Alternatif sans consoElecMoyenne + Soit Une demande de calcul pour un équipement virtuel tel que + | nbEquipementsVirtuels | nbTotalVCPU | stockageTotalVirtuel | equipementVirtuel.nomLot | equipementVirtuel.dateLot | equipementVirtuel.nomOrganisation | equipementVirtuel.nomEntite | equipementVirtuel.cluster | equipementVirtuel.nomSourceDonnee | equipementVirtuel.nomEquipementVirtuel | equipementVirtuel.nomEquipementPhysique | equipementVirtuel.typeEqv | equipementVirtuel.vCPU | equipementVirtuel.consoElecAnnuelle | equipementVirtuel.capaciteStockage | equipementVirtuel.cleRepartition | impactEquipementPhysique.statutIndicateur | impactEquipementPhysique.impactUnitaire | impactEquipementPhysique.consoElecMoyenne | impactEquipementPhysique.etape | impactEquipementPhysique.critere | impactEquipementPhysique.unite | impactEquipementPhysique.nomLot | impactEquipementPhysique.dateLot | impactEquipementPhysique.nomOrganisation | impactEquipementPhysique.nomEntite | + | 10 | | | 01/01/2022-Tests | 01/01/2022 | Tests | CAF1 | Test | Gherkin | VM1 | Test | calcul | 8 | | 5,0 | 0,2 | OK | 2840,00 | | UTILISATION | Changement climatique | kgCO2eq | Tests-01/01/2022 | 01/01/2022 | Tests | CAF1 | + Alors l'impact d'équipement virtuel résultant est tel que + | versionCalcul | nomLot | dateLot | nomOrganisation | nomEntite | critere | etapeACV | statutIndicateur | impactUnitaire | consoElecMoyenne | nomSourceDonnee | unite | nomEquipementVirtuel | nomEquipement | cluster | trace | + | 1.1 | 01/01/2022-Tests | 01/01/2022 | Tests | CAF1 | Changement climatique | UTILISATION | OK | 568,0 | | Gherkin | kgCO2eq | VM1 | Test | Test | {"formule":"valeurImpactUnitaire = valeurImpactEquipementPhysique(2840.0) * equipementVirtuel.cleRepartition(0.2)"} | + + Scénario: Calcul d'impact d'équipement Virtuel - CAF2 + Soit Une demande de calcul pour un équipement virtuel tel que + | nbEquipementsVirtuels | nbTotalVCPU | stockageTotalVirtuel | equipementVirtuel.nomLot | equipementVirtuel.dateLot | equipementVirtuel.nomOrganisation | equipementVirtuel.nomEntite | equipementVirtuel.cluster | equipementVirtuel.nomSourceDonnee | equipementVirtuel.nomEquipementVirtuel | equipementVirtuel.nomEquipementPhysique | equipementVirtuel.typeEqv | equipementVirtuel.vCPU | equipementVirtuel.consoElecAnnuelle | equipementVirtuel.capaciteStockage | equipementVirtuel.cleRepartition | impactEquipementPhysique.statutIndicateur | impactEquipementPhysique.impactUnitaire | impactEquipementPhysique.consoElecMoyenne | impactEquipementPhysique.etape | impactEquipementPhysique.critere | impactEquipementPhysique.unite | impactEquipementPhysique.nomLot | impactEquipementPhysique.dateLot | impactEquipementPhysique.nomOrganisation | impactEquipementPhysique.nomEntite | + | | 16 | | 01/01/2022-Tests | 01/01/2022 | Tests | CAF2 | Test | Gherkin | VM2 | Test | calcul | 8 | | 5,0 | | OK | 2840,00 | 2840,00 | UTILISATION | Changement climatique | kgCO2eq | Tests-01/01/2022 | 01/01/2022 | Tests | CAF2 | + Alors l'impact d'équipement virtuel résultant est tel que + | versionCalcul | nomLot | dateLot | nomOrganisation | nomEntite | critere | etapeACV | statutIndicateur | impactUnitaire | consoElecMoyenne | nomSourceDonnee | unite | nomEquipementVirtuel | nomEquipement | cluster | trace | + | 1.1 | 01/01/2022-Tests | 01/01/2022 | Tests | CAF2 | Changement climatique | UTILISATION | OK | 1420,0 | 1420,0 | Gherkin | kgCO2eq | VM2 | Test | Test | {"formule":"valeurImpactUnitaire = valeurImpactEquipementPhysique(2840.0) * equipementVirtuel.vCPU(8) / nbvCPU(16)","nbTotalVCPU":16} | + + Scénario: Calcul d'impact d'équipement Virtuel - CAF2 - Alternatif sans consoElecMoyenne + Soit Une demande de calcul pour un équipement virtuel tel que + | nbEquipementsVirtuels | nbTotalVCPU | stockageTotalVirtuel | equipementVirtuel.nomLot | equipementVirtuel.dateLot | equipementVirtuel.nomOrganisation | equipementVirtuel.nomEntite | equipementVirtuel.cluster | equipementVirtuel.nomSourceDonnee | equipementVirtuel.nomEquipementVirtuel | equipementVirtuel.nomEquipementPhysique | equipementVirtuel.typeEqv | equipementVirtuel.vCPU | equipementVirtuel.consoElecAnnuelle | equipementVirtuel.capaciteStockage | equipementVirtuel.cleRepartition | impactEquipementPhysique.statutIndicateur | impactEquipementPhysique.impactUnitaire | impactEquipementPhysique.consoElecMoyenne | impactEquipementPhysique.etape | impactEquipementPhysique.critere | impactEquipementPhysique.unite | impactEquipementPhysique.nomLot | impactEquipementPhysique.dateLot | impactEquipementPhysique.nomOrganisation | impactEquipementPhysique.nomEntite | + | | 16 | | 01/01/2022-Tests | 01/01/2022 | Tests | CAF2 | Test | Gherkin | VM2 | Test | calcul | 8 | | 5,0 | | OK | 2840,00 | | UTILISATION | Changement climatique | kgCO2eq | Tests-01/01/2022 | 01/01/2022 | Tests | CAF2 | + Alors l'impact d'équipement virtuel résultant est tel que + | versionCalcul | nomLot | dateLot | nomOrganisation | nomEntite | critere | etapeACV | statutIndicateur | impactUnitaire | consoElecMoyenne | nomSourceDonnee | unite | nomEquipementVirtuel | nomEquipement | cluster | trace | + | 1.1 | 01/01/2022-Tests | 01/01/2022 | Tests | CAF2 | Changement climatique | UTILISATION | OK | 1420,0 | | Gherkin | kgCO2eq | VM2 | Test | Test | {"formule":"valeurImpactUnitaire = valeurImpactEquipementPhysique(2840.0) * equipementVirtuel.vCPU(8) / nbvCPU(16)","nbTotalVCPU":16} | + + Scénario: Calcul d'impact d'équipement Virtuel - CAF3 + Soit Une demande de calcul pour un équipement virtuel tel que + | nbEquipementsVirtuels | nbTotalVCPU | stockageTotalVirtuel | equipementVirtuel.nomLot | equipementVirtuel.dateLot | equipementVirtuel.nomOrganisation | equipementVirtuel.nomEntite | equipementVirtuel.cluster | equipementVirtuel.nomSourceDonnee | equipementVirtuel.nomEquipementVirtuel | equipementVirtuel.nomEquipementPhysique | equipementVirtuel.typeEqv | equipementVirtuel.vCPU | equipementVirtuel.consoElecAnnuelle | equipementVirtuel.capaciteStockage | equipementVirtuel.cleRepartition | impactEquipementPhysique.statutIndicateur | impactEquipementPhysique.impactUnitaire | impactEquipementPhysique.consoElecMoyenne | impactEquipementPhysique.etape | impactEquipementPhysique.critere | impactEquipementPhysique.unite | impactEquipementPhysique.nomLot | impactEquipementPhysique.dateLot | impactEquipementPhysique.nomOrganisation | impactEquipementPhysique.nomEntite | + | 3 | | | 01/01/2022-Tests | 01/01/2022 | Tests | CAF3 | Test | Gherkin | VM4 | Test | calcul | | | 5,0 | | OK | 2840,00 | 2840,00 | UTILISATION | Changement climatique | kgCO2eq | Tests-01/01/2022 | 01/01/2022 | Tests | CAF3 | + Alors l'impact d'équipement virtuel résultant est tel que + | versionCalcul | nomLot | dateLot | nomOrganisation | nomEntite | critere | etapeACV | statutIndicateur | impactUnitaire | consoElecMoyenne | nomSourceDonnee | unite | nomEquipementVirtuel | nomEquipement | cluster | trace | + | 1.1 | 01/01/2022-Tests | 01/01/2022 | Tests | CAF3 | Changement climatique | UTILISATION | OK | 946,6666666666666 | 946,6666666666666 | Gherkin | kgCO2eq | VM4 | Test | Test | {"formule":"valeurImpactUnitaire = valeurImpactEquipementPhysique(2840.0) / nbVM(3)","nbEquipementsVirtuels":3} | + + Scénario: Calcul d'impact d'équipement Virtuel - CAF3 - Alternatif sans consoElecMoyenne + Soit Une demande de calcul pour un équipement virtuel tel que + | nbEquipementsVirtuels | nbTotalVCPU | stockageTotalVirtuel | equipementVirtuel.nomLot | equipementVirtuel.dateLot | equipementVirtuel.nomOrganisation | equipementVirtuel.nomEntite | equipementVirtuel.cluster | equipementVirtuel.nomSourceDonnee | equipementVirtuel.nomEquipementVirtuel | equipementVirtuel.nomEquipementPhysique | equipementVirtuel.typeEqv | equipementVirtuel.vCPU | equipementVirtuel.consoElecAnnuelle | equipementVirtuel.capaciteStockage | equipementVirtuel.cleRepartition | impactEquipementPhysique.statutIndicateur | impactEquipementPhysique.impactUnitaire | impactEquipementPhysique.consoElecMoyenne | impactEquipementPhysique.etape | impactEquipementPhysique.critere | impactEquipementPhysique.unite | impactEquipementPhysique.nomLot | impactEquipementPhysique.dateLot | impactEquipementPhysique.nomOrganisation | impactEquipementPhysique.nomEntite | + | 3 | | | 01/01/2022-Tests | 01/01/2022 | Tests | CAF3 | Test | Gherkin | VM4 | Test | calcul | | | 5,0 | | OK | 2840,00 | | UTILISATION | Changement climatique | kgCO2eq | Tests-01/01/2022 | 01/01/2022 | Tests | CAF3 | + Alors l'impact d'équipement virtuel résultant est tel que + | versionCalcul | nomLot | dateLot | nomOrganisation | nomEntite | critere | etapeACV | statutIndicateur | impactUnitaire | consoElecMoyenne | nomSourceDonnee | unite | nomEquipementVirtuel | nomEquipement | cluster | trace | + | 1.1 | 01/01/2022-Tests | 01/01/2022 | Tests | CAF3 | Changement climatique | UTILISATION | OK | 946,6666666666666 | | Gherkin | kgCO2eq | VM4 | Test | Test | {"formule":"valeurImpactUnitaire = valeurImpactEquipementPhysique(2840.0) / nbVM(3)","nbEquipementsVirtuels":3} | + + Scénario: Calcul d'impact d'équipement Virtuel - CAF5 + Soit Une demande de calcul pour un équipement virtuel tel que + | nbEquipementsVirtuels | nbTotalVCPU | stockageTotalVirtuel | equipementVirtuel.nomLot | equipementVirtuel.dateLot | equipementVirtuel.nomOrganisation | equipementVirtuel.nomEntite | equipementVirtuel.cluster | equipementVirtuel.nomSourceDonnee | equipementVirtuel.nomEquipementVirtuel | equipementVirtuel.nomEquipementPhysique | equipementVirtuel.typeEqv | equipementVirtuel.vCPU | equipementVirtuel.consoElecAnnuelle | equipementVirtuel.capaciteStockage | equipementVirtuel.cleRepartition | impactEquipementPhysique.statutIndicateur | impactEquipementPhysique.impactUnitaire | impactEquipementPhysique.consoElecMoyenne | impactEquipementPhysique.etape | impactEquipementPhysique.critere | impactEquipementPhysique.unite | impactEquipementPhysique.nomLot | impactEquipementPhysique.dateLot | impactEquipementPhysique.nomOrganisation | impactEquipementPhysique.nomEntite | + | 3 | | 100 | 01/01/2022-Tests | 01/01/2022 | Tests | CAF5 | Test | Gherkin | VM4 | Test | stockage | | | 5,0 | | OK | 100,00 | 100,00 | UTILISATION | Changement climatique | kgCO2eq | Tests-01/01/2022 | 01/01/2022 | Tests | CAF5 | + Alors l'impact d'équipement virtuel résultant est tel que + | versionCalcul | nomLot | dateLot | nomOrganisation | nomEntite | critere | etapeACV | statutIndicateur | impactUnitaire | consoElecMoyenne | nomSourceDonnee | unite | nomEquipementVirtuel | nomEquipement | cluster | trace | + | 1.1 | 01/01/2022-Tests | 01/01/2022 | Tests | CAF5 | Changement climatique | UTILISATION | OK | 5,0 | 5,0 | Gherkin | kgCO2eq | VM4 | Test | Test | {"formule":"valeurImpactUnitaire = valeurImpactEquipementPhysique(100.0) * equipementVirtuel.capaciteStockage(5.0) / stockageTotalVirtuel(100.0)","stockageTotalVirtuel":100.0} | + + Scénario: Calcul d'impact d'équipement Virtuel - CAF5 - Alternatif sans consoElecMoyenne + Soit Une demande de calcul pour un équipement virtuel tel que + | nbEquipementsVirtuels | nbTotalVCPU | stockageTotalVirtuel | equipementVirtuel.nomLot | equipementVirtuel.dateLot | equipementVirtuel.nomOrganisation | equipementVirtuel.nomEntite | equipementVirtuel.cluster | equipementVirtuel.nomSourceDonnee | equipementVirtuel.nomEquipementVirtuel | equipementVirtuel.nomEquipementPhysique | equipementVirtuel.typeEqv | equipementVirtuel.vCPU | equipementVirtuel.consoElecAnnuelle | equipementVirtuel.capaciteStockage | equipementVirtuel.cleRepartition | impactEquipementPhysique.statutIndicateur | impactEquipementPhysique.impactUnitaire | impactEquipementPhysique.consoElecMoyenne | impactEquipementPhysique.etape | impactEquipementPhysique.critere | impactEquipementPhysique.unite | impactEquipementPhysique.nomLot | impactEquipementPhysique.dateLot | impactEquipementPhysique.nomOrganisation | impactEquipementPhysique.nomEntite | + | 3 | | 100 | 01/01/2022-Tests | 01/01/2022 | Tests | CAF5 | Test | Gherkin | VM4 | Test | stockage | | | 5,0 | | OK | 100,00 | | UTILISATION | Changement climatique | kgCO2eq | Tests-01/01/2022 | 01/01/2022 | Tests | CAF5 | + Alors l'impact d'équipement virtuel résultant est tel que + | versionCalcul | nomLot | dateLot | nomOrganisation | nomEntite | critere | etapeACV | statutIndicateur | impactUnitaire | consoElecMoyenne | nomSourceDonnee | unite | nomEquipementVirtuel | nomEquipement | cluster | trace | + | 1.1 | 01/01/2022-Tests | 01/01/2022 | Tests | CAF5 | Changement climatique | UTILISATION | OK | 5,0 | | Gherkin | kgCO2eq | VM4 | Test | Test | {"formule":"valeurImpactUnitaire = valeurImpactEquipementPhysique(100.0) * equipementVirtuel.capaciteStockage(5.0) / stockageTotalVirtuel(100.0)","stockageTotalVirtuel":100.0} | diff --git a/services/calculs/src/test/resources/org/mte/numecoeval/calculs/DureeDeVie-EquipementPhysique.feature b/services/calculs/src/test/resources/org/mte/numecoeval/calculs/DureeDeVie-EquipementPhysique.feature new file mode 100644 index 00000000..ef9b48d7 --- /dev/null +++ b/services/calculs/src/test/resources/org/mte/numecoeval/calculs/DureeDeVie-EquipementPhysique.feature @@ -0,0 +1,47 @@ +#language: fr +#encoding: utf-8 +Fonctionnalité: : Durée de Vie d'un équipement physique / Durée de vie des équipements par défaut + # JDDs - 1A à 1F + # Les noms d'équipement physique ont été simplifié + Scénario: Calcul de la Durée de Vie avec Date d'achat et Date retrait connues (période < 1 an) + Soit Une demande de calcule de durée de vie tel que + | nomEquipementPhysique | dateAchat | dateRetrait | refTypeEquipement.dureeVieDefaut | hypothese.dureeVieParDefaut | + | 2023-04-OrdinateurPortable-test1A | 01/10/2022 | 01/03/2023 | | | + Alors la durée de vie de l'équipement est 1 + + Scénario: Calcul de la Durée de Vie avec Date d'achat et Date retrait connues (période > 1 an) + Soit Une demande de calcule de durée de vie tel que + | nomEquipementPhysique | dateAchat | dateRetrait | refTypeEquipement.dureeVieDefaut | hypothese.dureeVieParDefaut | + | 2023-04-OrdinateurPortable-test1B | 01/10/2021 | 01/03/2023 | | | + Alors la durée de vie de l'équipement est 1,4136986301369863 + + Scénario: Calcul de la Durée de Vie avec Date d'achat connue mais Date retrait inconnue + Soit Une demande de calcule de durée de vie tel que + | nomEquipementPhysique | dateAchat | dateRetrait | refTypeEquipement.dureeVieDefaut | hypothese.dureeVieParDefaut | + | 2023-04-OrdinateurPortable-test1C | 01/10/2022 | | | | + Alors la durée de vie correspond à la différence entre la date d'achat et la date du jour + + Scénario: Calcul de la Durée de Vie avec Date d'achat et Date retrait inconnues - Equipement physique présent dans la table des références d'équipement + Soit Une demande de calcule de durée de vie tel que + | nomEquipementPhysique | dateAchat | dateRetrait | refTypeEquipement.dureeVieDefaut | hypothese.dureeVieParDefaut | + | 2023-04-OrdinateurPortable-test1D | | | 6,7 | | + Alors la durée de vie de l'équipement est 6,7 + + Scénario: Calcul de la Durée de Vie avec Date d'achat et Date retrait inconnues - Equipement physique non présent dans la table des références d'équipement - Durée de vie par défaut des équipements présent dans la table des hypothèses + Soit Une demande de calcule de durée de vie tel que + | nomEquipementPhysique | dateAchat | dateRetrait | refTypeEquipement.dureeVieDefaut | hypothese.dureeVieParDefaut | + | 2023-04-OrdinateurPortable-test1E | | | | 5,2 | + Alors la durée de vie de l'équipement est 5,2 + + Scénario: Calcul de la Durée de Vie avec Date d'achat et Date retrait inconnues - Equipement physique non présent dans la table des références d'équipement - Durée de vie par défaut des équipements non présent dans la table des hypothèses + Soit Une demande de calcule de durée de vie tel que + | nomEquipementPhysique | dateAchat | dateRetrait | refTypeEquipement.dureeVieDefaut | hypothese.dureeVieParDefaut | + | 2023-04-OrdinateurPortable-test1F | | | | | + Alors le calcul de durée de vie lève une erreur de type "ErrCalcFonc" avec le message "La durée de vie par défaut de l'équipement n'a pas pu être déterminée" + + Scénario: Calcul de la Durée de Vie avec Date d'achat et Date retrait mal renseignées (date de retrait avant date d'achat) + Soit Une demande de calcule de durée de vie tel que + | nomEquipementPhysique | dateAchat | dateRetrait | refTypeEquipement.dureeVieDefaut | hypothese.dureeVieParDefaut | + | 2023-04-OrdinateurPortable-test1F | 01/01/2023 | 01/02/2022 | | | + Alors le calcul de durée de vie lève une erreur de type "ErrCalcFonc" avec le message "La durée de vie de l'équipement n'a pas pu être déterminée" + diff --git a/services/common/.gitignore b/services/common/.gitignore new file mode 100644 index 00000000..20c8d182 --- /dev/null +++ b/services/common/.gitignore @@ -0,0 +1,32 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/services/common/.gitlab-ci-library.yml b/services/common/.gitlab-ci-library.yml new file mode 100644 index 00000000..333d3823 --- /dev/null +++ b/services/common/.gitlab-ci-library.yml @@ -0,0 +1,47 @@ +include: + - project: "pub/numeco/m4g/ci-library" + ref: main + file: "/fragments/meta.yml" + - project: "pub/numeco/m4g/ci-library" + ref: main + file: "/fragments/meta-java.yml" + - project: "pub/numeco/m4g/ci-library" + ref: main + file: "/configuration/before-script-template.yml" + - project: "pub/numeco/m4g/ci-library" + ref: main + file: "/stages/analysis/dependency-check.yml" + - project: "pub/numeco/m4g/ci-library" + ref: main + file: "/stages/analysis/sonarqube.yml" + +stages: + - test-application + - build-application + - analysis + +cache: + paths: + - .m2/ + +variables: + MAVEN_PROJECT_DIR: services/$COMPONENT + PROJECT_DIR: services/$COMPONENT + MAVEN_CLI_OPTS: "-s ci_settings.xml --batch-mode" + MAVEN_OPTS: "-Dmaven.repo.local=.m2" + DEPENDENCY_CHECK_SCAN_PATH: $CI_PROJECT_DIR/services/$COMPONENT + DEPENDENCY_CHECK_SUPPRESSION_PATH: $CI_PROJECT_DIR/services/$COMPONENT + +#Compilation et Tests pour les Merges requests et les commits +test-application: + stage: test-application + extends: + - .tags-runner + - .java:test + +#Compilation+Déploiement dans le repository Gitlab +build-application: + stage: build-application + extends: + - .tags-runner + - .java:build diff --git a/services/common/.gitlab-ci-maven.yml b/services/common/.gitlab-ci-maven.yml new file mode 100644 index 00000000..8a32043e --- /dev/null +++ b/services/common/.gitlab-ci-maven.yml @@ -0,0 +1,86 @@ +include: + - project: "pub/numeco/m4g/ci-library" + ref: main + file: "/fragments/meta.yml" + - project: "pub/numeco/m4g/ci-library" + ref: main + file: "/fragments/meta-java.yml" + - project: "pub/numeco/m4g/ci-library" + ref: main + file: "/configuration/before-script-template.yml" + - project: "pub/numeco/m4g/ci-library" + ref: main + file: "/stages/build/build-image-kaniko.yml" + - project: "pub/numeco/m4g/ci-library" + ref: main + file: "/stages/analysis/mte-image-scan.yml" + - project: "pub/numeco/m4g/ci-library" + ref: main + file: "/stages/analysis/dependency-check.yml" + - project: "pub/numeco/m4g/ci-library" + ref: main + file: "/stages/analysis/sonarqube.yml" + + +stages: + - test-application + - build-application + - build-image + - analysis + +variables: + MAVEN_PROJECT_DIR: services/$COMPONENT + PROJECT_DIR: services/$COMPONENT + MAVEN_CLI_OPTS: "-s $CI_PROJECT_DIR/services/common/ci_settings.xml --batch-mode" + MAVEN_OPTS: "-Dmaven.repo.local=.m2" + DEPENDENCY_CHECK_SCAN_PATH: $CI_PROJECT_DIR/services/$COMPONENT + DEPENDENCY_CHECK_SUPPRESSION_PATH: $CI_PROJECT_DIR/services/$COMPONENT + DOCKER_IMAGE: "$CI_REGISTRY_IMAGE/$COMPONENT:$CI_COMMIT_REF_NAME" + +#Compilation et Tests pour les Merges requests et les commits +test-application: + stage: test-application + before_script: + - sed -i 's#${project.basedir}/../common#${project.basedir}/services/common#g' services/$COMPONENT/pom.xml + extends: + - .tags-runner + - .java:test + +#Compilation+Déploiement dans le repository Gitlab +build-application: + stage: build-application + before_script: | + sed -i 's#${project.basedir}/../common#${project.basedir}/services/common#g' services/$COMPONENT/pom.xml + if [ "${COMPONENT}" = "api-referentiel" ];then + VERSION=$(cat services/$COMPONENT/pom.xml | grep "<version>" | head -n1 | sed "s# ##g" | sed "s#version##g" | sed "s#[<>/]##g") + echo "Version: $VERSION" + sed -i "s|version: .*|version: \"${VERSION}\"|" services/$COMPONENT/src/main/resources/application.yaml + cat services/$COMPONENT/src/main/resources/application.yaml + fi + extends: + - .tags-runner + - .java:build + only: + - main + - develop + - tags + +build-image: + stage: build-image + extends: .kaniko_build + before_script: + - cp -f services/common/Dockerfile . + - cp -f services/common/entrypoint.sh . + - cd target/ + - rm -f *.original + - rm -f $(ls | grep .jar | grep -iv $COMPONENT) + - cd ../ + variables: + DOCKER_USER: $CI_REGISTRY_USER + DOCKER_PASSWORD: $CI_REGISTRY_PASSWORD + REGISTRY_URL: $CI_REGISTRY + IMAGE_URL: $CI_REGISTRY_IMAGE/$COMPONENT + only: + - main + - develop + - tags diff --git a/services/common/Dockerfile b/services/common/Dockerfile new file mode 100644 index 00000000..9ba5cf5d --- /dev/null +++ b/services/common/Dockerfile @@ -0,0 +1,35 @@ +FROM openjdk:17-jdk-slim + +# Logs +ENV LOGS_DIR=/app/logs + +RUN apt-get update ; apt-get upgrade --no-cache -f ; \ + apt-get install -y openssl tzdata util-linux bash curl coreutils jq dos2unix + +# Utilisateur spring +ENV GID_USER=1104 +ENV UID_USER=$GID_USER +RUN adduser -q --group spring && adduser spring --ingroup spring --disabled-password --gecos "" && \ + mkdir /app && mkdir $LOGS_DIR && \ + chown spring:spring -R /app && chown spring:spring -R $LOGS_DIR + +#Changement de la timezone +ENV TZ Europe/Paris + +USER spring:spring + +#PORT Exposé par l'application +ENV SERVER_PORT 8080 +ENV MANAGEMENT_SERVER_PORT $SERVER_PORT +EXPOSE $SERVER_PORT + +# Copie de l'entrypoint +COPY --chown=spring:spring entrypoint.sh /app/entrypoint.sh +RUN chmod u+x /app/entrypoint.sh && dos2unix /app/entrypoint.sh + +# Copie du JAR +ARG JAR_FILE=target/*.jar +COPY --chown=spring:spring ${JAR_FILE} /app/app.jar +WORKDIR /app + +ENTRYPOINT ["/app/entrypoint.sh"] diff --git a/services/common/README.md b/services/common/README.md new file mode 100644 index 00000000..16a1b3a4 --- /dev/null +++ b/services/common/README.md @@ -0,0 +1,3 @@ +# common + +Module Maven contenant les éléments communs aux applications diff --git a/services/common/ci_settings.xml b/services/common/ci_settings.xml new file mode 100644 index 00000000..5205903f --- /dev/null +++ b/services/common/ci_settings.xml @@ -0,0 +1,16 @@ +<settings xmlns="http://maven.apache.org/SETTINGS/1.1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.1.0 http://maven.apache.org/xsd/settings-1.1.0.xsd"> + <servers> + <server> + <id>gitlab-maven</id> + <configuration> + <httpHeaders> + <property> + <name>Job-Token</name> + <value>${env.CI_JOB_TOKEN}</value> + </property> + </httpHeaders> + </configuration> + </server> + </servers> +</settings> diff --git a/services/common/dependency_check_suppressions.xml b/services/common/dependency_check_suppressions.xml new file mode 100644 index 00000000..790bf7a1 --- /dev/null +++ b/services/common/dependency_check_suppressions.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<suppressions xmlns="https://jeremylong.github.io/DependencyCheck/dependency-suppression.1.3.xsd"> + + <suppress> + <notes><![CDATA[ + Non-applicable : Snakeyaml n'est utilisé que par Spring Boot pour les fichiers application.yml + et non des fichiers externes. + ]]></notes> + <cve>CVE-2022-1471</cve> + </suppress> + + <suppress> + <notes><![CDATA[ + Non-applicable : json-smart est une dépendance transitive. + Les json sont limités à la taille des inventaires : la taille des listes n'a pas réellement de limite + et c'est souhaités comme cela (traitement de gros volumes de données). + ]]></notes> + <cve>CVE-2023-1370</cve> + </suppress> +</suppressions> diff --git a/services/common/entrypoint.sh b/services/common/entrypoint.sh new file mode 100644 index 00000000..bbdb9583 --- /dev/null +++ b/services/common/entrypoint.sh @@ -0,0 +1,13 @@ +#!/bin/bash +echo "Vérification de la présence du fichier d'environnement" +if [ -d /run/secrets ]; then + echo "Chargement des fichiers d'environnement" + for secretFile in /run/secrets/*; do + if [ -f "$secretFile" ]; then + . $secretFile + fi + done +fi + +echo "Lancement de l'application" +exec java $NUMECOEVAL_JAVA_OPTIONS -jar /app/app.jar diff --git a/services/common/pom.xml b/services/common/pom.xml new file mode 100644 index 00000000..28812c5f --- /dev/null +++ b/services/common/pom.xml @@ -0,0 +1,122 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.mte.numecoeval</groupId> + <artifactId>core</artifactId> + <version>1.2.3-SNAPSHOT</version> + <relativePath/> <!-- lookup parent from repository --> + </parent> + + <artifactId>common</artifactId> + <version>1.2.3-SNAPSHOT</version> + <name>common</name> + <description>Module commun</description> + + <repositories> + <repository> + <id>gitlab-maven</id> + <url>https://gitlab-forge.din.developpement-durable.gouv.fr/api/v4/projects/20519/packages/maven</url> + </repository> + </repositories> + + <distributionManagement> + <repository> + <id>gitlab-maven</id> + <url>https://gitlab-forge.din.developpement-durable.gouv.fr/api/v4/projects/20519/packages/maven</url> + </repository> + + <snapshotRepository> + <id>gitlab-maven</id> + <url>https://gitlab-forge.din.developpement-durable.gouv.fr/api/v4/projects/20519/packages/maven</url> + </snapshotRepository> + </distributionManagement> + + <dependencies> + <dependency> + <groupId>org.projectlombok</groupId> + <artifactId>lombok</artifactId> + <optional>true</optional> + </dependency> + <dependency> + <groupId>org.openapitools</groupId> + <artifactId>jackson-databind-nullable</artifactId> + </dependency> + + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-validation</artifactId> + </dependency> + + <dependency> + <groupId>jakarta.validation</groupId> + <artifactId>jakarta.validation-api</artifactId> + </dependency> + + <dependency> + <groupId>io.swagger.core.v3</groupId> + <artifactId>swagger-annotations</artifactId> + <version>2.2.8</version> + </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-test</artifactId> + <scope>test</scope> + </dependency> + </dependencies> + + <build> + <plugins> + <plugin> + <groupId>org.openapitools</groupId> + <artifactId>openapi-generator-maven-plugin</artifactId> + <version>${openapi-generator-version}</version> + <executions> + <execution> + <id>generation_model_asyncapi</id> + <goals> + <goal>generate</goal> + </goals> + <configuration> + <inputSpec>${project.basedir}/src/main/resources/static/asyncapi_merge.yaml + </inputSpec> + <output>${project.build.directory}/generated-sources</output> + <modelPackage>org.mte.numecoeval.topic.data</modelPackage> + <generateApis>false</generateApis> + <generateModels>true</generateModels> + <generateModelTests>false</generateModelTests> + <generateApiTests>false</generateApiTests> + <generateSupportingFiles>false</generateSupportingFiles> + <generateModelDocumentation>false</generateModelDocumentation> + <generateApiDocumentation>false</generateApiDocumentation> + <generatorName>spring</generatorName> + <library>spring-boot</library> + <configOptions> + <useJakartaEe>true</useJakartaEe> + <useTags>true</useTags> + <skipDefaultInterface>true</skipDefaultInterface> + <dateLibrary>java8-localdatetime</dateLibrary> + <useSpringBoot3>true</useSpringBoot3> + <sourceFolder>src/gen/java</sourceFolder> + <serializableModel>true</serializableModel> + <interfaceOnly>true</interfaceOnly> + <reactive>false</reactive> + <useBeanValidation>false</useBeanValidation> + <performBeanValidation>false</performBeanValidation> + <useOptional>false</useOptional> + <serviceInterface>false</serviceInterface> + <serviceImplementation>false</serviceImplementation> + <booleanGetterPrefix>is</booleanGetterPrefix> + <additionalModelTypeAnnotations> + @lombok.AllArgsConstructor;@lombok.Builder;@lombok.NoArgsConstructor + </additionalModelTypeAnnotations> + </configOptions> + </configuration> + </execution> + </executions> + + </plugin> + </plugins> + </build> +</project> diff --git a/services/common/src/main/java/org/mte/numecoeval/common/exception/NumEcoEvalRuntimeException.java b/services/common/src/main/java/org/mte/numecoeval/common/exception/NumEcoEvalRuntimeException.java new file mode 100644 index 00000000..b32f608e --- /dev/null +++ b/services/common/src/main/java/org/mte/numecoeval/common/exception/NumEcoEvalRuntimeException.java @@ -0,0 +1,18 @@ +package org.mte.numecoeval.common.exception; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * RuntimeException spécifique à NumEcoEval. + */ +@Getter +@AllArgsConstructor +public class NumEcoEvalRuntimeException extends RuntimeException { + + /** + * Description de l'erreur + */ + private final String description; + +} diff --git a/services/common/src/main/java/org/mte/numecoeval/common/utils/PreparedStatementUtils.java b/services/common/src/main/java/org/mte/numecoeval/common/utils/PreparedStatementUtils.java new file mode 100644 index 00000000..9545a1b1 --- /dev/null +++ b/services/common/src/main/java/org/mte/numecoeval/common/utils/PreparedStatementUtils.java @@ -0,0 +1,50 @@ +package org.mte.numecoeval.common.utils; + +import java.sql.Date; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.sql.Types; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Objects; + +public class PreparedStatementUtils { + + private PreparedStatementUtils() { + // Private constructor + } + + public static void setDoubleValue(PreparedStatement ps, int index, Double value) throws SQLException { + if(Objects.isNull(value)) { + ps.setNull(index, Types.DOUBLE ); + } + else { + ps.setDouble(index, value); + } + } + + public static void setIntValue(PreparedStatement ps, int index, Integer value) throws SQLException { + if(Objects.isNull(value)) { + ps.setNull(index, Types.INTEGER ); + } + else { + ps.setInt(index, value); + } + } + + public static Timestamp getTimestampFromLocalDateTime(LocalDateTime localDateTime) { + if(Objects.isNull(localDateTime)) { + return null; + } + return Timestamp.valueOf(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS").format(localDateTime)); + } + + public static Date getDateFromLocalDate(LocalDate localDate) { + if(Objects.isNull(localDate)) { + return null; + } + return Date.valueOf(DateTimeFormatter.ISO_LOCAL_DATE.format(localDate)) ; + } +} diff --git a/services/common/src/main/java/org/mte/numecoeval/common/utils/ResultSetUtils.java b/services/common/src/main/java/org/mte/numecoeval/common/utils/ResultSetUtils.java new file mode 100644 index 00000000..7298d644 --- /dev/null +++ b/services/common/src/main/java/org/mte/numecoeval/common/utils/ResultSetUtils.java @@ -0,0 +1,55 @@ +package org.mte.numecoeval.common.utils; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +public class ResultSetUtils { + + private ResultSetUtils() { + // Nothing to do + } + + public static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + public static LocalDate getLocalDate(ResultSet resultSet, String columnName) throws SQLException { + if (checkColumnInResultSet(resultSet, columnName)) return null; + + return resultSet.getDate(columnName).toLocalDate(); + } + + public static LocalDateTime getLocalDateTime(ResultSet resultSet, String columnName) throws SQLException { + if (checkColumnInResultSet(resultSet, columnName)) return null; + + return resultSet.getTimestamp(columnName).toLocalDateTime(); + } + + public static Integer getInteger(ResultSet resultSet, String columnName) throws SQLException { + if (checkColumnInResultSet(resultSet, columnName)) return null; + + return resultSet.getInt(columnName); + } + + public static Float getFloat(ResultSet resultSet, String columnName) throws SQLException { + if (checkColumnInResultSet(resultSet, columnName)) return null; + + return resultSet.getFloat(columnName); + } + + public static Double getDouble(ResultSet resultSet, String columnName) throws SQLException { + if (checkColumnInResultSet(resultSet, columnName)) return null; + + return resultSet.getDouble(columnName); + } + + private static boolean checkColumnInResultSet(ResultSet resultSet, String columnName) throws SQLException { + if (resultSet == null || "".equals(columnName)) { + return true; + } + + var value = resultSet.getString(columnName); + return value == null || "".equals(value); + } +} diff --git a/services/common/src/main/resources/static/README.md b/services/common/src/main/resources/static/README.md new file mode 100644 index 00000000..0ee297a8 --- /dev/null +++ b/services/common/src/main/resources/static/README.md @@ -0,0 +1,22 @@ +# Swagger OpenAPI + +Comment générer les fichiers openapi. + +Mode opératoire générique : + +- lancer le composant $COMPOSANT +- aller sur http://localhost:$PORT/swagger-ui/index.html +- Cliquer sur "/v3/api-docs/..." +- copier le contenu dans https://editor.swagger.io/ et récupérer le yaml + +Fichiers openapi __générés__ par les composants: + +- api-referentiels-openapi.yaml, COMPOSANT : api-referentiels, PORT : 18080 +- api-expositiondonneesentrees-openapi.yaml, COMPOSANT : api-expositiondonneesentrees, PORT : 18081 +- api-event-calculs-openapi.yaml, COMPOSANT : api-event-calculs, PORT : 18092 + +Fichiers pilotés __manuellement__ dans ce composant common: + +- asyncapi_*.yaml +- api-event-calculs-async-openapi.yaml +- api-event-calculs-sync-openapi.yaml \ No newline at end of file diff --git a/services/common/src/main/resources/static/api-event-calculs-async-openapi.yaml b/services/common/src/main/resources/static/api-event-calculs-async-openapi.yaml new file mode 100644 index 00000000..2d849a92 --- /dev/null +++ b/services/common/src/main/resources/static/api-event-calculs-async-openapi.yaml @@ -0,0 +1,789 @@ +openapi: 3.0.1 +info: + title: NumEcoEval - API calculs + version: v1 + license: + name: Apache 2.0 + description: | + API permettant de réaliser des calculs unitaires d'impact écologiques. \ +tags: + - name: Calculs Equipement Physique + description: Endpoints liés aux calculs pour un équipement physique + - name: Calculs Equipement Virtuel + description: Endpoints liés aux calculs pour un équipement virtuel + - name: Calculs Application + description: Endpoints liés aux calculs pour une application + - name: Calculs Messagerie + description: Endpoints liés aux calculs pour un élément de messagerie + - name: Calculs Reseau + description: Endpoints liés aux calculs autour de l'usage du réseau +paths: + /calculs/equipementPhysique: + post: + summary: Endpoint pour le calcul unitaire d'impact écologique pour un équipement physique. + description: | + Endpoint pour le calcul unitaire d'impact écologique pour un équipement physique. + tags: + - Calculs Equipement Physique + operationId: calculerImpactEquipementPhysique + requestBody: + required: true + content: + 'application/json': + schema: + $ref: "#/components/schemas/DemandeCalculImpactEquipementPhysiqueRest" + responses: + "500": + description: Erreur interne du service + content: + 'application/json': + schema: + $ref: "#/components/schemas/ErreurRest" + "400": + description: Indicateur en erreur + content: + 'application/json': + schema: + $ref: "#/components/schemas/IndicateurImpactEquipementPhysiqueRest" + "200": + description: Indicateur calculé. + content: + 'application/json': + schema: + $ref: "#/components/schemas/IndicateurImpactEquipementPhysiqueRest" + /calculs/reseau/equipementPhysique: + post: + summary: Endpoint pour le calcul unitaire d'impact écologique pour un équipement physique lié à l'usage du réseau. + description: | + Endpoint pour le calcul unitaire d'impact écologique pour un équipement physique lié à l'usage du réseau. + tags: + - Calculs Reseau + operationId: calculerImpactReseau + requestBody: + required: true + content: + 'application/json': + schema: + $ref: "#/components/schemas/DemandeCalculImpactReseauEquipementPhysiqueRest" + responses: + "500": + description: Erreur interne du service + content: + 'application/json': + schema: + $ref: "#/components/schemas/ErreurRest" + "400": + description: Indicateur en erreur + content: + 'application/json': + schema: + $ref: "#/components/schemas/IndicateurImpactReseauRest" + "200": + description: Indicateur calculé. + content: + 'application/json': + schema: + $ref: "#/components/schemas/IndicateurImpactReseauRest" + /calculs/equipementVirtuel: + post: + summary: Endpoint pour le calcul unitaire d'impact écologique pour un équipement virtuel. + description: | + Endpoint pour le calcul unitaire d'impact écologique pour un équipement virtuel. + Une demande peut se baser sur le contenu de la sortie du calcul d'impact d'équipement physique. + tags: + - Calculs Equipement Virtuel + operationId: calculerImpactEquipementVirtuel + requestBody: + required: true + content: + 'application/json': + schema: + $ref: "#/components/schemas/DemandeCalculEquipementVirtuelRest" + responses: + "500": + description: Erreur interne du service + content: + 'application/json': + schema: + $ref: "#/components/schemas/ErreurRest" + "400": + description: Indicateur en erreur + content: + 'application/json': + schema: + $ref: "#/components/schemas/IndicateurImpactEquipementVirtuelRest" + "200": + description: Indicateur calculé. + content: + 'application/json': + schema: + $ref: "#/components/schemas/IndicateurImpactEquipementVirtuelRest" + /calculs/application: + post: + summary: Endpoint pour le calcul unitaire d'impact écologique pour une application. + description: | + Endpoint pour le calcul unitaire d'impact écologique pour une application. + Une demande peut se baser sur le contenu de la sortie du calcul d'impact d'équipement virtuel. + tags: + - Calculs Application + operationId: calculerImpactApplication + requestBody: + required: true + content: + 'application/json': + schema: + $ref: "#/components/schemas/DemandeCalculApplicationRest" + responses: + "500": + description: Erreur interne du service + content: + 'application/json': + schema: + $ref: "#/components/schemas/ErreurRest" + "400": + description: Indicateur en erreur + content: + 'application/json': + schema: + $ref: "#/components/schemas/IndicateurImpactApplicationRest" + "200": + description: Indicateur calculé. + content: + 'application/json': + schema: + $ref: "#/components/schemas/IndicateurImpactApplicationRest" + /calculs/messagerie: + post: + summary: Endpoint pour le calcul unitaire d'impact écologique pour un élément de messagerie. + description: | + Endpoint pour le calcul unitaire d'impact écologique pour un élément de messagerie + tags: + - Calculs Messagerie + operationId: calculerImpactMessagerie + requestBody: + required: true + content: + 'application/json': + schema: + $ref: "#/components/schemas/DemandeCalculMessagerieRest" + responses: + "500": + description: Erreur interne du service + content: + 'application/json': + schema: + $ref: "#/components/schemas/ErreurRest" + "400": + description: Indicateur en erreur + content: + 'application/json': + schema: + $ref: "#/components/schemas/IndicateurImpactMessagerieRest" + "200": + description: Indicateur calculé. + content: + 'application/json': + schema: + $ref: "#/components/schemas/IndicateurImpactMessagerieRest" +components: + schemas: + ErreurRest: + description: Objet standard pour les réponses en cas d'erreur d'API + type: object + properties: + code: + description: Code de l'erreur + type: string + message: + description: Message de l'erreur + type: string + status: + description: Code Statut HTTP de la réponse + type: integer + timestamp: + description: Date & Heure de l'erreur + type: string + format: date-time + # Entrées + EquipementPhysiqueRest: + description: Représentation d'un équipement physique dans NumEcoEval + type: object + properties: + nomEquipementPhysique: + description: "" + type: string + modele: + description: "" + type: string + type: + description: "" + type: string + statut: + description: "" + type: string + paysDUtilisation: + description: "" + type: string + utilisateur: + description: "" + type: string + dateAchat: + description: "" + type: string + format: date + dateRetrait: + description: "" + type: string + format: date + nbCoeur: + description: "" + type: string + modeUtilisation: + description: "" + type: string + tauxUtilisation: + description: "" + type: number + format: double + nomCourtDatacenter: + description: "" + type: string + nbJourUtiliseAn: + description: "" + type: number + format: double + goTelecharge: + description: "" + type: number + format: float + quantite: + description: "" + type: number + format: double + default: 1.0 + consoElecAnnuelle: + description: "" + type: number + format: double + nbEquipementsVirtuels: + description: "Nombre d'équipements virtuels rattachés à l'équipement physique - Utilisé dans les traitements" + type: integer + nbTotalVCPU: + description: "Nombre total de VCPU (correspond à la somme des vCPU définis dans les équipements virtuels) ou null si un des équipements virtuels n'a pas de vCPU - Utilisé dans les traitements" + type: integer + dataCenter: + $ref: "#/components/schemas/DataCenterRest" + DataCenterRest: + description: Représentation d'un Data Center dans NumEcoEval + properties: + nomCourtDatacenter: + description: "" + type: string + nomLongDatacenter: + description: "" + type: string + pue: + description: "" + type: number + format: double + localisation: + description: "" + type: string + EquipementVirtuelRest: + description: Représentation d'un équipement virtuel dans NumEcoEval + properties: + nomEquipementVirtuel: + description: "" + type: string + nomEquipementPhysique: + description: "" + type: string + vCPU: + description: "" + type: integer + cluster: + description: "" + type: string + typeEqv: + description: | + Le type d'équipement virtuel contient "calcul", "stockage" ou null. + Peut être omis entièrement si ce n'est pas applicable. + type: string + capaciteStockage: + description: | + Capacité de stockage de l'équipement virtuel en To. + type: number + format: double + cleRepartition: + description: | + La clé de repartition est exprimée comme une fraction. + type: number + format: double + consoElecAnnuelle: + description: | + Consommation électrique annuelle de l'équipement virtuel. + type: number + format: double + ApplicationRest: + description: Représentation d'une application dans NumEcoEval + properties: + nomApplication: + description: "" + type: string + typeEnvironnement: + description: "" + type: string + nomEquipementVirtuel: + description: "" + type: string + nomEquipementPhysique: + description: "" + type: string + domaine: + description: "" + type: string + sousDomaine: + description: "" + type: string + MessagerieRest: + description: Représentation d'éléments de messagerie dans NumEcoEval + properties: + nombreMailEmis: + description: "" + type: integer + nombreMailEmisXDestinataires: + description: "" + type: integer + volumeTotalMailEmis: + description: "" + type: number + format: double + moisAnnee: + description: "Mois et Année rattachés au données, format MMAAAA" + type: integer + nomEntite: + description: "" + type: string + + + # Référentiels + CorrespondanceRefEquipementRest: + type: object + properties: + modeleEquipementSource: + type: string + description: Modèle de l'équipement + refEquipementCible: + type: string + description: Référence d'équipement correspondant au modèle de l'équipement + description: Référentiel de correspondance entre un modèle d'équipement physique + et une référence d'équipement dans les référentiels. + TypeEquipementRest: + type: object + properties: + type: + type: string + serveur: + type: boolean + commentaire: + type: string + dureeVieDefaut: + type: number + format: double + refEquipementParDefaut: + type: string + description: Référentiel de type d'équipement physique. + HypotheseRest: + type: object + properties: + code: + description: | + Code de l'hypothèse. + type: string + valeur: + description: "Valeur de l'hypothèse" + type: number + format: double + EtapeRest: + type: object + properties: + code: + type: string + libelle: + type: string + CritereRest: + type: object + properties: + nomCritere: + type: string + description: Nom du critère d'impact écologique + unite: + type: string + description: Unité du critère d'impact écologique + MixElectriqueRest: + type: object + properties: + pays: + type: string + critere: + type: string + valeur: + type: number + format: double + description: Référentiel de critère d'impact écologique + ImpactReseauRest: + type: object + properties: + refReseau: + type: string + etapeACV: + type: string + critere: + type: string + unite: + type: string + valeur: + type: number + format: double + consoElecMoyenne: + type: number + format: double + description: Référentiel d'impact écologique pour l'usage du réseau par un équipement + ImpactEquipementRest: + type: object + properties: + refEquipement: + type: string + etape: + type: string + critere: + type: string + type: + type: string + valeur: + type: number + format: double + consoElecMoyenne: + type: number + format: double + description: Référentiel d'impact écologique pour un équipement physique + ImpactMessagerieRest: + type: object + description: Référentiel - L'équation d'impact est une fonction affine de la forme a * x + b + properties: + constanteCoefficientDirecteur: + description: Cette constante correspond à "a" dans la fonction affine + type: number + format: double + constanteOrdonneeOrigine: + description: Cette constante correspond à "b" dans la fonction affine + type: number + format: double + critere: + description: Critère associé à l'impact + type: string + + # Indicateurs + IndicateurImpactEquipementPhysiqueRest: + type: object + properties: + dateCalcul: + type: string + description: "Date et Heure du calcul, même valeur pour tous les indicateurs\ + \ créés avec le même lot d'objets d'entrées" + format: date-time + versionCalcul: + type: string + etapeACV: + type: string + description: "Code de l'étape ACV associé à l'indicateur, peut être null\ + \ pour certains indicateurs" + critere: + type: string + description: "Nom du critère associé à l'indicateur, peut être null pour\ + \ certains indicateurs" + statutIndicateur: + type: string + description: "Statut de l'indicateur, vaut \"OK\" si l'indicateur est calcul\ + \ et \"ERREUR\" si l'indicateur est en erreur" + trace: + type: string + description: Trace du calcul ayant produit l'indicateur + unite: + type: string + description: "Unite du critère associé, peut être null" + nomEquipement: + type: string + typeEquipement: + type: string + impactUnitaire: + type: number + format: double + consoElecMoyenne: + type: number + format: double + quantite: + type: integer + format: int32 + statutEquipementPhysique: + type: string + IndicateurImpactReseauRest: + type: object + properties: + dateCalcul: + type: string + description: "Date et Heure du calcul, même valeur pour tous les indicateurs\ + \ créés avec le même lot d'objets d'entrées" + format: date-time + versionCalcul: + type: string + etapeACV: + type: string + description: "Code de l'étape ACV associé à l'indicateur, peut être null\ + \ pour certains indicateurs" + critere: + type: string + description: "Nom du critère associé à l'indicateur, peut être null pour\ + \ certains indicateurs" + statutIndicateur: + type: string + description: "Statut de l'indicateur, vaut \"OK\" si l'indicateur est calcul\ + \ et \"ERREUR\" si l'indicateur est en erreur" + trace: + type: string + description: Trace du calcul ayant produit l'indicateur + unite: + type: string + description: "Unite du critère associé, peut être null" + impactUnitaire: + type: number + format: double + IndicateurImpactEquipementVirtuelRest: + description: Indicateur d'impact écologique d'un équipement virtuel + type: object + properties: + dateCalcul: + type: string + description: "Date et Heure du calcul, même valeur pour tous les indicateurs\ + \ créés avec le même lot d'objets d'entrées" + format: date-time + versionCalcul: + type: string + etapeACV: + type: string + description: "Code de l'étape ACV associé à l'indicateur, peut être null\ + \ pour certains indicateurs" + critere: + type: string + description: "Nom du critère associé à l'indicateur, peut être null pour\ + \ certains indicateurs" + source: + type: string + statutIndicateur: + type: string + description: "Statut de l'indicateur, vaut \"OK\" si l'indicateur est calcul\ + \ et \"ERREUR\" si l'indicateur est en erreur" + trace: + type: string + description: Trace du calcul ayant produit l'indicateur + unite: + type: string + description: "Unite du critère associé, peut être null" + nomEquipementVirtuel: + type: string + nomEquipement: + type: string + impactUnitaire: + type: number + format: double + cluster: + type: string + consoElecMoyenne: + type: number + format: double + IndicateurImpactApplicationRest: + description: Impact d'application + properties: + dateCalcul: + type: string + description: "Date et Heure du calcul, même valeur pour tous les indicateurs\ + \ créés avec le même lot d'objets d'entrées" + format: date-time + versionCalcul: + type: string + etapeACV: + type: string + description: "Code de l'étape ACV associé à l'indicateur, peut être null\ + \ pour certains indicateurs" + critere: + type: string + description: "Nom du critère associé à l'indicateur, peut être null pour\ + \ certains indicateurs" + statutIndicateur: + type: string + description: "Statut de l'indicateur, vaut \"OK\" si l'indicateur est calcul\ + \ et \"ERREUR\" si l'indicateur est en erreur" + trace: + type: string + description: Trace du calcul ayant produit l'indicateur + unite: + type: string + description: "Unite du critère associé, peut être null" + nomEquipementPhysique: + description: "" + type: string + nomEquipementVirtuel: + description: "" + type: string + nomApplication: + type: string + impactUnitaire: + type: number + format: double + domaine: + type: string + sousDomaine: + type: string + typeEnvironnement: + type: string + consoElecMoyenne: + type: number + format: double + IndicateurImpactMessagerieRest: + description: Indicateur d'impact de messagerie + properties: + dateCalcul: + type: string + description: "Date et Heure du calcul, même valeur pour tous les indicateurs\ + \ créés avec le même lot d'objets d'entrées" + format: date-time + versionCalcul: + type: string + critere: + type: string + description: "Nom du critère associé à l'indicateur, peut être null pour\ + \ certains indicateurs" + statutIndicateur: + type: string + description: "Statut de l'indicateur, vaut \"OK\" si l'indicateur est calcul\ + \ et \"ERREUR\" si l'indicateur est en erreur" + trace: + type: string + description: Trace du calcul ayant produit l'indicateur + unite: + type: string + description: "Unite du critère associé, peut être null" + impactMensuel: + type: number + format: double + moisAnnee: + type: integer + volumeTotalMailEmis: + type: number + format: double + nombreMailEmis: + type: number + format: double + + + # Demandes de calculs unitaires + DemandeCalculImpactEquipementPhysiqueRest: + description: + Demande de calcul d'impact d'équipement physique. + Contient tous les éléments nécéssaire au calcul d'impact (équipement physique, référentiels) + properties: + equipementPhysique: + $ref: "#/components/schemas/EquipementPhysiqueRest" + typeEquipement: + $ref: "#/components/schemas/TypeEquipementRest" + correspondanceRefEquipement: + $ref: "#/components/schemas/CorrespondanceRefEquipementRest" + hypotheses: + type: array + items: + $ref: "#/components/schemas/HypotheseRest" + etape: + $ref: "#/components/schemas/EtapeRest" + critere: + $ref: "#/components/schemas/CritereRest" + mixElectriques: + description: Mix électrique du pays d'utilisation de l'équipement physique + type: array + items: + $ref: "#/components/schemas/MixElectriqueRest" + impactEquipements: + description: | + Référentiels des Critères d'impact d'équipements. + La liste ne peut contenir maximum 2 entrées : + - 1 pour la référence par défaut (typeEquipement.refEquipementParDefaut) + - 1 pour la correspondance cible (correspondanceRefEquipement.refEquipementCible) + type: array + items: + $ref: "#/components/schemas/ImpactEquipementRest" + DemandeCalculImpactReseauEquipementPhysiqueRest: + description: | + Demande de calcul d'impact d'équipement physique. + Contient tous les éléments nécéssaire au calcul d'impact (équipement physique, référentiels) + properties: + equipementPhysique: + $ref: "#/components/schemas/EquipementPhysiqueRest" + etape: + $ref: "#/components/schemas/EtapeRest" + critere: + $ref: "#/components/schemas/CritereRest" + impactsReseau: + description: | + Référentiels des Critères d'impact réseau. + La référence réseau doit être "impactReseauMobileMoyen". + type: array + maxItems: 1 + items: + $ref: "#/components/schemas/ImpactReseauRest" + DemandeCalculEquipementVirtuelRest: + description: Objet regroupant toutes les données nécessaires au calcul unitaire d'indicateur d'impact écologique pour un équipement virtuel. + required: + - equipementVirtuel + - impactEquipement + properties: + equipementVirtuel: + $ref: "#/components/schemas/EquipementVirtuelRest" + nbTotalVCPU: + description: | + Nombre total de CPU virtuels sur l'équipement physique parent. + Si renseigné, sera utilisé en priorité. + Ne sera utilisé que si le type d'équipement virtuel n'est pas "calcul". + type: integer + nbEquipementsVirtuels: + description: | + Nombre total d'équipements virtuels sur l'équipement physique parent. + type: integer + stockageTotalVirtuel: + description: | + Stockage total en To pour tous les équipements virtuels sur l'équipement physique parent. + Ne sera utilisé que si le type d'équipement virtuel n'est pas "stockage". + type: number + format: double + impactEquipement: + $ref: "#/components/schemas/IndicateurImpactEquipementPhysiqueRest" + DemandeCalculApplicationRest: + description: Demande de calcul unitaire d'indicateur d'impact écologique pour une application + properties: + application: + $ref: "#/components/schemas/ApplicationRest" + nbApplications: + description: Nombre d'applications rattachées à l'équipement virtuel portant l'application + type: integer + impactEquipementVirtuel: + $ref: "#/components/schemas/IndicateurImpactEquipementVirtuelRest" + DemandeCalculMessagerieRest: + description: Objet regroupant toutes les données pour le calcul des indicateurs d'impact de messagerie dans NumEcoEval + type: object + properties: + messagerie: + $ref: "#/components/schemas/MessagerieRest" + critere: + $ref: "#/components/schemas/CritereRest" + impactsMessagerie: + type: array + items: + $ref: "#/components/schemas/ImpactMessagerieRest" \ No newline at end of file diff --git a/services/common/src/main/resources/static/api-event-calculs-openapi.yaml b/services/common/src/main/resources/static/api-event-calculs-openapi.yaml new file mode 100644 index 00000000..05125bcc --- /dev/null +++ b/services/common/src/main/resources/static/api-event-calculs-openapi.yaml @@ -0,0 +1,782 @@ +openapi: 3.0.1 +info: + title: NumEcoEval - API calculs + version: v1 + license: + name: Apache 2.0 + description: | + API permettant de réaliser des calculs unitaires d'impact écologiques. \ +tags: + - name: Calculs Equipement Physique + description: Endpoints liés aux calculs pour un équipement physique + - name: Calculs Equipement Virtuel + description: Endpoints liés aux calculs pour un équipement virtuel + - name: Calculs Application + description: Endpoints liés aux calculs pour une application + - name: Calculs Messagerie + description: Endpoints liés aux calculs pour un élément de messagerie + - name: Calculs Reseau + description: Endpoints liés aux calculs autour de l'usage du réseau +paths: + /calculs/equipementPhysique: + post: + summary: Endpoint pour le calcul unitaire d'impact écologique pour un équipement physique. + description: | + Endpoint pour le calcul unitaire d'impact écologique pour un équipement physique. + tags: + - Calculs Equipement Physique + operationId: calculerImpactEquipementPhysique + requestBody: + required: true + content: + 'application/json': + schema: + $ref: "#/components/schemas/DemandeCalculImpactEquipementPhysiqueRest" + responses: + "500": + description: Erreur interne du service + content: + 'application/json': + schema: + $ref: "#/components/schemas/ErreurRest" + "400": + description: Indicateur en erreur + content: + 'application/json': + schema: + $ref: "#/components/schemas/IndicateurImpactEquipementPhysiqueRest" + "200": + description: Indicateur calculé. + content: + 'application/json': + schema: + $ref: "#/components/schemas/IndicateurImpactEquipementPhysiqueRest" + /calculs/reseau/equipementPhysique: + post: + summary: Endpoint pour le calcul unitaire d'impact écologique pour un équipement physique lié à l'usage du réseau. + description: | + Endpoint pour le calcul unitaire d'impact écologique pour un équipement physique lié à l'usage du réseau. + tags: + - Calculs Reseau + operationId: calculerImpactReseau + requestBody: + required: true + content: + 'application/json': + schema: + $ref: "#/components/schemas/DemandeCalculImpactReseauEquipementPhysiqueRest" + responses: + "500": + description: Erreur interne du service + content: + 'application/json': + schema: + $ref: "#/components/schemas/ErreurRest" + "400": + description: Indicateur en erreur + content: + 'application/json': + schema: + $ref: "#/components/schemas/IndicateurImpactReseauRest" + "200": + description: Indicateur calculé. + content: + 'application/json': + schema: + $ref: "#/components/schemas/IndicateurImpactReseauRest" + /calculs/equipementVirtuel: + post: + summary: Endpoint pour le calcul unitaire d'impact écologique pour un équipement virtuel. + description: | + Endpoint pour le calcul unitaire d'impact écologique pour un équipement virtuel. + Une demande peut se baser sur le contenu de la sortie du calcul d'impact d'équipement physique. + tags: + - Calculs Equipement Virtuel + operationId: calculerImpactEquipementVirtuel + requestBody: + required: true + content: + 'application/json': + schema: + $ref: "#/components/schemas/DemandeCalculEquipementVirtuelRest" + responses: + "500": + description: Erreur interne du service + content: + 'application/json': + schema: + $ref: "#/components/schemas/ErreurRest" + "400": + description: Indicateur en erreur + content: + 'application/json': + schema: + $ref: "#/components/schemas/IndicateurImpactEquipementVirtuelRest" + "200": + description: Indicateur calculé. + content: + 'application/json': + schema: + $ref: "#/components/schemas/IndicateurImpactEquipementVirtuelRest" + /calculs/application: + post: + summary: Endpoint pour le calcul unitaire d'impact écologique pour une application. + description: | + Endpoint pour le calcul unitaire d'impact écologique pour une application. + Une demande peut se baser sur le contenu de la sortie du calcul d'impact d'équipement virtuel. + tags: + - Calculs Application + operationId: calculerImpactApplication + requestBody: + required: true + content: + 'application/json': + schema: + $ref: "#/components/schemas/DemandeCalculApplicationRest" + responses: + "500": + description: Erreur interne du service + content: + 'application/json': + schema: + $ref: "#/components/schemas/ErreurRest" + "400": + description: Indicateur en erreur + content: + 'application/json': + schema: + $ref: "#/components/schemas/IndicateurImpactApplicationRest" + "200": + description: Indicateur calculé. + content: + 'application/json': + schema: + $ref: "#/components/schemas/IndicateurImpactApplicationRest" + /calculs/messagerie: + post: + summary: Endpoint pour le calcul unitaire d'impact écologique pour un élément de messagerie. + description: | + Endpoint pour le calcul unitaire d'impact écologique pour un élément de messagerie + tags: + - Calculs Messagerie + operationId: calculerImpactMessagerie + requestBody: + required: true + content: + 'application/json': + schema: + $ref: "#/components/schemas/DemandeCalculMessagerieRest" + responses: + "500": + description: Erreur interne du service + content: + 'application/json': + schema: + $ref: "#/components/schemas/ErreurRest" + "400": + description: Indicateur en erreur + content: + 'application/json': + schema: + $ref: "#/components/schemas/IndicateurImpactMessagerieRest" + "200": + description: Indicateur calculé. + content: + 'application/json': + schema: + $ref: "#/components/schemas/IndicateurImpactMessagerieRest" +components: + schemas: + ErreurRest: + description: Objet standard pour les réponses en cas d'erreur d'API + type: object + properties: + code: + description: Code de l'erreur + type: string + message: + description: Message de l'erreur + type: string + status: + description: Code Statut HTTP de la réponse + type: integer + timestamp: + description: Date & Heure de l'erreur + type: string + format: date-time + # Entrées + EquipementPhysiqueRest: + description: Représentation d'un équipement physique dans NumEcoEval + type: object + properties: + nomEquipementPhysique: + description: "" + type: string + modele: + description: "" + type: string + type: + description: "" + type: string + statut: + description: "" + type: string + paysDUtilisation: + description: "" + type: string + utilisateur: + description: "" + type: string + dateAchat: + description: "" + type: string + format: date + dateRetrait: + description: "" + type: string + format: date + nbCoeur: + description: "" + type: string + nomCourtDatacenter: + description: "" + type: string + nbJourUtiliseAn: + description: "" + type: number + format: double + goTelecharge: + description: "" + type: number + format: float + quantite: + description: "" + type: number + format: double + default: 1.0 + consoElecAnnuelle: + description: "" + type: number + format: double + nbEquipementsVirtuels: + description: "Nombre d'équipements virtuels rattachés à l'équipement physique - Utilisé dans les traitements" + type: integer + nbTotalVCPU: + description: "Nombre total de VCPU (correspond à la somme des vCPU définis dans les équipements virtuels) ou null si un des équipements virtuels n'a pas de vCPU - Utilisé dans les traitements" + type: integer + dataCenter: + $ref: "#/components/schemas/DataCenterRest" + DataCenterRest: + description: Représentation d'un Data Center dans NumEcoEval + properties: + nomCourtDatacenter: + description: "" + type: string + nomLongDatacenter: + description: "" + type: string + pue: + description: "" + type: number + format: double + localisation: + description: "" + type: string + EquipementVirtuelRest: + description: Représentation d'un équipement virtuel dans NumEcoEval + properties: + nomEquipementVirtuel: + description: "" + type: string + nomEquipementPhysique: + description: "" + type: string + vCPU: + description: "" + type: integer + cluster: + description: "" + type: string + typeEqv: + description: | + Le type d'équipement virtuel contient "calcul", "stockage" ou null. + Peut être omis entièrement si ce n'est pas applicable. + type: string + capaciteStockage: + description: | + Capacité de stockage de l'équipement virtuel en To. + type: number + format: double + cleRepartition: + description: | + La clé de repartition est exprimée comme une fraction. + type: number + format: double + consoElecAnnuelle: + description: | + Consommation électrique annuelle de l'équipement virtuel. + type: number + format: double + ApplicationRest: + description: Représentation d'une application dans NumEcoEval + properties: + nomApplication: + description: "" + type: string + typeEnvironnement: + description: "" + type: string + nomEquipementVirtuel: + description: "" + type: string + nomEquipementPhysique: + description: "" + type: string + domaine: + description: "" + type: string + sousDomaine: + description: "" + type: string + MessagerieRest: + description: Représentation d'éléments de messagerie dans NumEcoEval + properties: + nombreMailEmis: + description: "" + type: integer + nombreMailEmisXDestinataires: + description: "" + type: integer + volumeTotalMailEmis: + description: "" + type: number + format: double + moisAnnee: + description: "Mois et Année rattachés au données, format MMAAAA" + type: integer + nomEntite: + description: "" + type: string + + + # Référentiels + CorrespondanceRefEquipementRest: + type: object + properties: + modeleEquipementSource: + type: string + description: Modèle de l'équipement + refEquipementCible: + type: string + description: Référence d'équipement correspondant au modèle de l'équipement + description: Référentiel de correspondance entre un modèle d'équipement physique + et une référence d'équipement dans les référentiels. + TypeEquipementRest: + type: object + properties: + type: + type: string + serveur: + type: boolean + commentaire: + type: string + dureeVieDefaut: + type: number + format: double + refEquipementParDefaut: + type: string + description: Référentiel de type d'équipement physique. + HypotheseRest: + type: object + properties: + code: + description: | + Code de l'hypothèse. + type: string + valeur: + description: "Valeur de l'hypothèse" + type: number + format: double + EtapeRest: + type: object + properties: + code: + type: string + libelle: + type: string + CritereRest: + type: object + properties: + nomCritere: + type: string + description: Nom du critère d'impact écologique + unite: + type: string + description: Unité du critère d'impact écologique + MixElectriqueRest: + type: object + properties: + pays: + type: string + critere: + type: string + valeur: + type: number + format: double + description: Référentiel de critère d'impact écologique + ImpactReseauRest: + type: object + properties: + refReseau: + type: string + etapeACV: + type: string + critere: + type: string + unite: + type: string + valeur: + type: number + format: double + consoElecMoyenne: + type: number + format: double + description: Référentiel d'impact écologique pour l'usage du réseau par un équipement + ImpactEquipementRest: + type: object + properties: + refEquipement: + type: string + etape: + type: string + critere: + type: string + type: + type: string + valeur: + type: number + format: double + consoElecMoyenne: + type: number + format: double + description: Référentiel d'impact écologique pour un équipement physique + ImpactMessagerieRest: + type: object + description: Référentiel - L'équation d'impact est une fonction affine de la forme a * x + b + properties: + constanteCoefficientDirecteur: + description: Cette constante correspond à "a" dans la fonction affine + type: number + format: double + constanteOrdonneeOrigine: + description: Cette constante correspond à "b" dans la fonction affine + type: number + format: double + critere: + description: Critère associé à l'impact + type: string + + # Indicateurs + IndicateurImpactEquipementPhysiqueRest: + type: object + properties: + dateCalcul: + type: string + description: "Date et Heure du calcul, même valeur pour tous les indicateurs\ + \ créés avec le même lot d'objets d'entrées" + format: date-time + versionCalcul: + type: string + etapeACV: + type: string + description: "Code de l'étape ACV associé à l'indicateur, peut être null\ + \ pour certains indicateurs" + critere: + type: string + description: "Nom du critère associé à l'indicateur, peut être null pour\ + \ certains indicateurs" + statutIndicateur: + type: string + description: "Statut de l'indicateur, vaut \"OK\" si l'indicateur est calcul\ + \ et \"ERREUR\" si l'indicateur est en erreur" + trace: + type: string + description: Trace du calcul ayant produit l'indicateur + unite: + type: string + description: "Unite du critère associé, peut être null" + nomEquipement: + type: string + typeEquipement: + type: string + impactUnitaire: + type: number + format: double + consoElecMoyenne: + type: number + format: double + quantite: + type: integer + format: int32 + statutEquipementPhysique: + type: string + IndicateurImpactReseauRest: + type: object + properties: + dateCalcul: + type: string + description: "Date et Heure du calcul, même valeur pour tous les indicateurs\ + \ créés avec le même lot d'objets d'entrées" + format: date-time + versionCalcul: + type: string + etapeACV: + type: string + description: "Code de l'étape ACV associé à l'indicateur, peut être null\ + \ pour certains indicateurs" + critere: + type: string + description: "Nom du critère associé à l'indicateur, peut être null pour\ + \ certains indicateurs" + statutIndicateur: + type: string + description: "Statut de l'indicateur, vaut \"OK\" si l'indicateur est calcul\ + \ et \"ERREUR\" si l'indicateur est en erreur" + trace: + type: string + description: Trace du calcul ayant produit l'indicateur + unite: + type: string + description: "Unite du critère associé, peut être null" + impactUnitaire: + type: number + format: double + IndicateurImpactEquipementVirtuelRest: + description: Indicateur d'impact écologique d'un équipement virtuel + type: object + properties: + dateCalcul: + type: string + description: "Date et Heure du calcul, même valeur pour tous les indicateurs\ + \ créés avec le même lot d'objets d'entrées" + format: date-time + versionCalcul: + type: string + etapeACV: + type: string + description: "Code de l'étape ACV associé à l'indicateur, peut être null\ + \ pour certains indicateurs" + critere: + type: string + description: "Nom du critère associé à l'indicateur, peut être null pour\ + \ certains indicateurs" + source: + type: string + statutIndicateur: + type: string + description: "Statut de l'indicateur, vaut \"OK\" si l'indicateur est calcul\ + \ et \"ERREUR\" si l'indicateur est en erreur" + trace: + type: string + description: Trace du calcul ayant produit l'indicateur + unite: + type: string + description: "Unite du critère associé, peut être null" + nomEquipementVirtuel: + type: string + nomEquipement: + type: string + impactUnitaire: + type: number + format: double + cluster: + type: string + consoElecMoyenne: + type: number + format: double + IndicateurImpactApplicationRest: + description: Impact d'application + properties: + dateCalcul: + type: string + description: "Date et Heure du calcul, même valeur pour tous les indicateurs\ + \ créés avec le même lot d'objets d'entrées" + format: date-time + versionCalcul: + type: string + etapeACV: + type: string + description: "Code de l'étape ACV associé à l'indicateur, peut être null\ + \ pour certains indicateurs" + critere: + type: string + description: "Nom du critère associé à l'indicateur, peut être null pour\ + \ certains indicateurs" + statutIndicateur: + type: string + description: "Statut de l'indicateur, vaut \"OK\" si l'indicateur est calcul\ + \ et \"ERREUR\" si l'indicateur est en erreur" + trace: + type: string + description: Trace du calcul ayant produit l'indicateur + unite: + type: string + description: "Unite du critère associé, peut être null" + nomEquipementPhysique: + description: "" + type: string + nomEquipementVirtuel: + description: "" + type: string + nomApplication: + type: string + impactUnitaire: + type: number + format: double + domaine: + type: string + sousDomaine: + type: string + typeEnvironnement: + type: string + consoElecMoyenne: + type: number + format: double + IndicateurImpactMessagerieRest: + description: Indicateur d'impact de messagerie + properties: + dateCalcul: + type: string + description: "Date et Heure du calcul, même valeur pour tous les indicateurs\ + \ créés avec le même lot d'objets d'entrées" + format: date-time + versionCalcul: + type: string + critere: + type: string + description: "Nom du critère associé à l'indicateur, peut être null pour\ + \ certains indicateurs" + statutIndicateur: + type: string + description: "Statut de l'indicateur, vaut \"OK\" si l'indicateur est calcul\ + \ et \"ERREUR\" si l'indicateur est en erreur" + trace: + type: string + description: Trace du calcul ayant produit l'indicateur + unite: + type: string + description: "Unite du critère associé, peut être null" + impactMensuel: + type: number + format: double + moisAnnee: + type: integer + volumeTotalMailEmis: + type: number + format: double + nombreMailEmis: + type: number + format: double + + + # Demandes de calculs unitaires + DemandeCalculImpactEquipementPhysiqueRest: + description: + Demande de calcul d'impact d'équipement physique. + Contient tous les éléments nécéssaire au calcul d'impact (équipement physique, référentiels) + properties: + equipementPhysique: + $ref: "#/components/schemas/EquipementPhysiqueRest" + typeEquipement: + $ref: "#/components/schemas/TypeEquipementRest" + correspondanceRefEquipement: + $ref: "#/components/schemas/CorrespondanceRefEquipementRest" + hypotheses: + type: array + items: + $ref: "#/components/schemas/HypotheseRest" + etape: + $ref: "#/components/schemas/EtapeRest" + critere: + $ref: "#/components/schemas/CritereRest" + mixElectriques: + description: Mix électrique du pays d'utilisation de l'équipement physique + type: array + items: + $ref: "#/components/schemas/MixElectriqueRest" + impactEquipements: + description: | + Référentiels des Critères d'impact d'équipements. + La liste ne peut contenir maximum 2 entrées : + - 1 pour la référence par défaut (typeEquipement.refEquipementParDefaut) + - 1 pour la correspondance cible (correspondanceRefEquipement.refEquipementCible) + type: array + items: + $ref: "#/components/schemas/ImpactEquipementRest" + DemandeCalculImpactReseauEquipementPhysiqueRest: + description: | + Demande de calcul d'impact d'équipement physique. + Contient tous les éléments nécéssaire au calcul d'impact (équipement physique, référentiels) + properties: + equipementPhysique: + $ref: "#/components/schemas/EquipementPhysiqueRest" + etape: + $ref: "#/components/schemas/EtapeRest" + critere: + $ref: "#/components/schemas/CritereRest" + impactsReseau: + description: | + Référentiels des Critères d'impact réseau. + La référence réseau doit être "impactReseauMobileMoyen". + type: array + maxItems: 1 + items: + $ref: "#/components/schemas/ImpactReseauRest" + DemandeCalculEquipementVirtuelRest: + description: Objet regroupant toutes les données nécessaires au calcul unitaire d'indicateur d'impact écologique pour un équipement virtuel. + required: + - equipementVirtuel + - impactEquipement + properties: + equipementVirtuel: + $ref: "#/components/schemas/EquipementVirtuelRest" + nbTotalVCPU: + description: | + Nombre total de CPU virtuels sur l'équipement physique parent. + Si renseigné, sera utilisé en priorité. + Ne sera utilisé que si le type d'équipement virtuel n'est pas "calcul". + type: integer + nbEquipementsVirtuels: + description: | + Nombre total d'équipements virtuels sur l'équipement physique parent. + type: integer + stockageTotalVirtuel: + description: | + Stockage total en To pour tous les équipements virtuels sur l'équipement physique parent. + Ne sera utilisé que si le type d'équipement virtuel n'est pas "stockage". + type: number + format: double + impactEquipement: + $ref: "#/components/schemas/IndicateurImpactEquipementPhysiqueRest" + DemandeCalculApplicationRest: + description: Demande de calcul unitaire d'indicateur d'impact écologique pour une application + properties: + application: + $ref: "#/components/schemas/ApplicationRest" + nbApplications: + description: Nombre d'applications rattachées à l'équipement virtuel portant l'application + type: integer + impactEquipementVirtuel: + $ref: "#/components/schemas/IndicateurImpactEquipementVirtuelRest" + DemandeCalculMessagerieRest: + description: Objet regroupant toutes les données pour le calcul des indicateurs d'impact de messagerie dans NumEcoEval + type: object + properties: + messagerie: + $ref: "#/components/schemas/MessagerieRest" + critere: + $ref: "#/components/schemas/CritereRest" + impactsMessagerie: + type: array + items: + $ref: "#/components/schemas/ImpactMessagerieRest" \ No newline at end of file diff --git a/services/common/src/main/resources/static/api-event-calculs-sync-openapi.yaml b/services/common/src/main/resources/static/api-event-calculs-sync-openapi.yaml new file mode 100644 index 00000000..fc99e3ad --- /dev/null +++ b/services/common/src/main/resources/static/api-event-calculs-sync-openapi.yaml @@ -0,0 +1,92 @@ +openapi: 3.0.1 +info: + title: NumEcoEval - API sync calculs by ids + version: v1 + license: + name: Apache 2.0 + description: | + API permettant de réaliser des calculs synchrones d'impact écologiques à partir d'ids. \ +paths: + /sync/calculs: + post: + summary: Endpoint pour le calcul unitaire d'impact écologique synchrone pour une liste d'id d'équipements physiques et de messageries. + description: | + Endpoint pour le calcul unitaire d'impact écologique pour un équipement physique. + tags: + - Calculs Equipement Physique et Messagerie + operationId: syncCalculByIds + requestBody: + required: true + content: + 'application/json': + schema: + $ref: "#/components/schemas/SyncCalculRest" + responses: + "500": + description: Erreur interne du service + content: + 'application/json': + schema: + $ref: "#/components/schemas/ErreurRest" + "200": + description: Réponse résumée, volume traité. + content: + 'application/json': + schema: + $ref: "#/components/schemas/ReponseCalculRest" +components: + schemas: + ErreurRest: + description: Objet standard pour les réponses en cas d'erreur d'API + type: object + properties: + code: + description: Code de l'erreur + type: string + message: + description: Message de l'erreur + type: string + status: + description: Code Statut HTTP de la réponse + type: integer + timestamp: + description: Date & Heure de l'erreur + type: string + format: date-time + # Entrées + SyncCalculRest: + description: Représentation d'un équipement physique dans NumEcoEval + type: object + properties: + equipementPhysiqueIds: + description: "" + type: array + items: + type: integer + format: int64 + messagerieIds: + description: "" + type: array + items: + type: integer + format: int64 + # Indicateurs + ReponseCalculRest: + type: object + properties: + nbrEquipementPhysique: + type: integer + format: int64 + description: "Nombre total d'équipements physiques traités" + nbrEquipementVirtuel: + type: integer + format: int64 + description: "Nombre total d'équipements virtuels traités" + nbrApplication: + type: integer + format: int64 + description: "Nombre total d'applications traités" + nbrMessagerie: + type: integer + format: int64 + description: "Nombre total de messageries traités" diff --git a/services/common/src/main/resources/static/api-referentiels-openapi.yaml b/services/common/src/main/resources/static/api-referentiels-openapi.yaml new file mode 100644 index 00000000..5aa74073 --- /dev/null +++ b/services/common/src/main/resources/static/api-referentiels-openapi.yaml @@ -0,0 +1,1484 @@ +openapi: 3.0.1 +info: + title: API des référentiels de NumEcoEval + description: | + Endpoints permettant de manipuler les référentiels de NumEcoEval. + Les endpoints CRUD sont générés via Spring DataRest. + + Les endpoints d'export CSV permettent d'exporter l'intégralité d'un référentiel + sous forme de fichier CSV ré-importable via les endpoints d'imports. + + Les endpoints d'import fonctionnent en annule & remplace et supprimeront l'intégralité + du référentiel et utiliseront le contenu du CSV pour peupler le référentiel. + + Les endpoints internes sont utilisés par les différents modules de NumEcoEval. + license: + name: Apache 2.0 + url: https://gitlab-forge.din.developpement-durable.gouv.fr/pub/numeco/m4g/api-referentiel/-/blob/main/LICENSE.txt + version: v0.0.1 +externalDocs: + description: NumEcoEval Documentation + url: https://gitlab-forge.din.developpement-durable.gouv.fr/pub/numeco/m4g/docs +paths: + /referentiel/typeEquipement/csv: + get: + tags: + - Export Référentiels + summary: Exporter les impacts des types d'équipements sous format csv + operationId: exportTypeEquipementCSV + responses: + '200': + description: OK + '400': + description: Bad Request + content: + application/hal+json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + '404': + description: Not Found + content: + application/hal+json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + '500': + description: Internal Server Error + content: + application/hal+json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + post: + tags: + - Import Référentiels + summary: 'Alimentation du référentiel Type Equipement par csv : annule et remplace.' + operationId: importTypeEquipementCSV + requestBody: + content: + multipart/form-data: + schema: + required: + - file + type: object + properties: + file: + type: string + format: binary + responses: + '200': + description: Rapport d'import du fichier CSV + content: + application/hal+json: + schema: + $ref: '#/components/schemas/RapportImportDTO' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + '404': + description: Not Found + content: + application/hal+json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + '500': + description: Internal Server Error + content: + application/hal+json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + /referentiel/mixelecs/csv: + get: + tags: + - Export Référentiels + summary: Exporter les impacts des mix électrique sous format csv + operationId: exportImpactEquipementCSV + responses: + '200': + description: OK + '400': + description: Bad Request + content: + application/hal+json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + '404': + description: Not Found + content: + application/hal+json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + '500': + description: Internal Server Error + content: + application/hal+json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + post: + tags: + - Import Référentiels + summary: 'Alimentation du référentiel ImpactEquipement par csv : annule et remplace.' + operationId: importImpactEquipementCSV + requestBody: + content: + multipart/form-data: + schema: + required: + - file + type: object + properties: + file: + type: string + format: binary + responses: + '200': + description: Rapport d'import du fichier CSV + content: + application/hal+json: + schema: + $ref: '#/components/schemas/RapportImportDTO' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + '404': + description: Not Found + content: + application/hal+json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + '500': + description: Internal Server Error + content: + application/hal+json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + /referentiel/impactreseaux/csv: + get: + tags: + - Export Référentiels + summary: Exporter les impacts réseaux sous format csv + operationId: exportImpactReseauxCSV + responses: + '200': + description: OK + '400': + description: Bad Request + content: + application/hal+json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + '404': + description: Not Found + content: + application/hal+json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + '500': + description: Internal Server Error + content: + application/hal+json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + post: + tags: + - Import Référentiels + summary: 'Alimentation du référentiel ImpactReseau par csv : annule et remplace.' + operationId: importImpactReseauxCSV + requestBody: + content: + multipart/form-data: + schema: + required: + - file + type: object + properties: + file: + type: string + format: binary + responses: + '200': + description: Rapport d'import du fichier CSV + content: + application/hal+json: + schema: + $ref: '#/components/schemas/RapportImportDTO' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + '404': + description: Not Found + content: + application/hal+json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + '500': + description: Internal Server Error + content: + application/hal+json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + /referentiel/impactequipements/csv: + get: + tags: + - Export Référentiels + summary: Exporter les impacts d'équipements sous format csv + operationId: exportImpactEquipementCSV_1 + responses: + '200': + description: OK + '400': + description: Bad Request + content: + application/hal+json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + '404': + description: Not Found + content: + application/hal+json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + '500': + description: Internal Server Error + content: + application/hal+json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + post: + tags: + - Import Référentiels + summary: 'Alimentation du référentiel ImpactEquipement par csv : annule et remplace.' + description: 'Alimentation du référentiel ImpactEquipement par csv : annule et remplace.' + operationId: importImpactEquipementCSV_1 + requestBody: + content: + multipart/form-data: + schema: + required: + - file + type: object + properties: + file: + type: string + format: binary + responses: + '200': + description: Rapport d'import du fichier CSV + content: + application/hal+json: + schema: + $ref: '#/components/schemas/RapportImportDTO' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + '404': + description: Not Found + content: + application/hal+json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + '500': + description: Internal Server Error + content: + application/hal+json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + /referentiel/impactMessagerie/csv: + get: + tags: + - Export Référentiels + summary: Exporter les impacts de messagerie sous format csv + operationId: exportImpactMessagerieCSV + responses: + '200': + description: OK + '400': + description: Bad Request + content: + application/hal+json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + '404': + description: Not Found + content: + application/hal+json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + '500': + description: Internal Server Error + content: + application/hal+json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + post: + tags: + - Import Référentiels + summary: 'Alimentation du référentiel Impact Messagerie par csv : annule et remplace.' + description: 'Alimentation du référentiel Impact Messagerie par csv : annule et remplace.' + operationId: importImpactMessagerieCSV + requestBody: + content: + multipart/form-data: + schema: + required: + - file + type: object + properties: + file: + type: string + format: binary + responses: + '200': + description: Rapport d'import du fichier CSV + content: + application/hal+json: + schema: + $ref: '#/components/schemas/RapportImportDTO' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + '404': + description: Not Found + content: + application/hal+json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + '500': + description: Internal Server Error + content: + application/hal+json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + /referentiel/hypotheses/csv: + get: + tags: + - Export Référentiels + summary: Exporter les hypothèses sous format csv + operationId: exportHypothesesCSV + responses: + '200': + description: OK + '400': + description: Bad Request + content: + application/hal+json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + '404': + description: Not Found + content: + application/hal+json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + '500': + description: Internal Server Error + content: + application/hal+json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + post: + tags: + - Import Référentiels + summary: 'Alimentation des Hypothèses par csv : annule et remplace.' + description: 'Alimentation des Hypothèses par csv : annule et remplace.' + operationId: importHypothesesCSV + requestBody: + content: + multipart/form-data: + schema: + required: + - file + type: object + properties: + file: + type: string + format: binary + responses: + '200': + description: Rapport d'import du fichier CSV + content: + application/hal+json: + schema: + $ref: '#/components/schemas/RapportImportDTO' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + '404': + description: Not Found + content: + application/hal+json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + '500': + description: Internal Server Error + content: + application/hal+json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + /referentiel/etapes/csv: + get: + tags: + - Export Référentiels + summary: Exporter les etapes sous format csv + operationId: exportEtapesCSV + responses: + '200': + description: OK + '400': + description: Bad Request + content: + application/hal+json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + '404': + description: Not Found + content: + application/hal+json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + '500': + description: Internal Server Error + content: + application/hal+json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + post: + tags: + - Import Référentiels + summary: 'Alimentation Etape ACV par csv : annule et remplace.' + description: 'Alimentation Etape ACV par csv : annule et remplace.' + operationId: importEtapesCSV + requestBody: + content: + multipart/form-data: + schema: + required: + - file + type: object + properties: + file: + type: string + format: binary + responses: + '200': + description: Rapport d'import du fichier CSV + content: + application/hal+json: + schema: + $ref: '#/components/schemas/RapportImportDTO' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + '404': + description: Not Found + content: + application/hal+json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + '500': + description: Internal Server Error + content: + application/hal+json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + /referentiel/criteres/csv: + get: + tags: + - Export Référentiels + summary: Exporter les criteres sous format csv + operationId: exportCriteresCSV + responses: + '200': + description: OK + '400': + description: Bad Request + content: + application/hal+json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + '404': + description: Not Found + content: + application/hal+json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + '500': + description: Internal Server Error + content: + application/hal+json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + post: + tags: + - Import Référentiels + summary: Alimentation des Criteres par csv (annule et remplace). + description: Alimentation des Criteres par csv (annule et remplace). + operationId: importCriteresCSV + requestBody: + content: + multipart/form-data: + schema: + required: + - file + type: object + properties: + file: + type: string + format: binary + responses: + '200': + description: Rapport d'import du fichier CSV + content: + application/hal+json: + schema: + $ref: '#/components/schemas/RapportImportDTO' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + '404': + description: Not Found + content: + application/hal+json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + '500': + description: Internal Server Error + content: + application/hal+json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + /referentiel/correspondanceRefEquipement/csv: + get: + tags: + - Export Référentiels + summary: Exporter les correspondances de RefEquipement sous format csv + operationId: exportCorrespondanceRefEquipementCSV + responses: + '200': + description: OK + '400': + description: Bad Request + content: + application/hal+json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + '404': + description: Not Found + content: + application/hal+json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + '500': + description: Internal Server Error + content: + application/hal+json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + post: + tags: + - Import Référentiels + summary: 'Alimentation du référentiel des correspondances de RefEquipement par csv : annule et remplace.' + operationId: importCorrespondanceRefEquipementCSV + requestBody: + content: + multipart/form-data: + schema: + required: + - file + type: object + properties: + file: + type: string + format: binary + responses: + '200': + description: Rapport d'import du fichier CSV + content: + application/hal+json: + schema: + $ref: '#/components/schemas/RapportImportDTO' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + '404': + description: Not Found + content: + application/hal+json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + '500': + description: Internal Server Error + content: + application/hal+json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + /version: + get: + tags: + - Interne NumEcoEval + summary: Endpoint interne à NumEcoEval - Récupération de la version courante + description: | + Endpoint interne utilisé pour connaître la version installée + operationId: getVersion + responses: + '200': + description: Version + content: + application/json: + schema: + $ref: '#/components/schemas/VersionDTO' + '400': + description: Bad Request + content: + application/hal+json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + '404': + description: Not Found + content: + application/hal+json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + '500': + description: Internal Server Error + content: + application/hal+json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + /referentiel/typesEquipement: + get: + tags: + - Interne NumEcoEval + summary: Endpoint interne à NumEcoEval - Récupération de tous les types d'équipement + description: | + Endpoint interne utilisé à la réception de données d'entrées par le module api-expositiondonneesentrees de NumEcoEval. + Renvoie l'intégralité des types d'équipements utilisables par NumEcoEval. + + Les types d'équipement servent notamment à alimenter la durée de vie par défaut des équipements + reçues. + operationId: getAllTypeEquipement + responses: + '200': + description: Types Equipement + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/TypeEquipementDTO' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + '404': + description: Types Equipement non trouvé + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + '500': + description: Internal Server Error + content: + application/hal+json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + /referentiel/typesEquipement/{type}: + get: + tags: + - Interne NumEcoEval + summary: Endpoint interne à NumEcoEval - Récupération d'un type d'équipement via son type + description: | + V2 - Endpoint interne utilisé à l'enrichissement données pour un équipement physique. + operationId: getTypeEquipement + parameters: + - name: type + in: path + description: type recherché + required: true + schema: + type: string + description: type recherché + responses: + '200': + description: Types Equipement + content: + application/json: + schema: + $ref: '#/components/schemas/TypeEquipementDTO' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + '404': + description: Types Equipement non trouvé + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + '500': + description: Internal Server Error + content: + application/hal+json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + /referentiel/mixelecs: + get: + tags: + - Interne NumEcoEval + summary: Endpoint interne à NumEcoEval - Récupération d'un Mix électrique + description: | + Endpoint interne utilisé dans la génération des indicateurs par le module api-calcul de NumEcoEval. + Récupération d'un mix électrique en fonction de paramètres: + <ul> + <li>Le pays de l'équipement: pays</li> + <li>Le critère d'impact: critere</li> + </ul> + . + operationId: getMixElectrique + parameters: + - name: pays + in: query + description: Pays recherché + required: true + schema: + type: string + description: Pays recherché + - name: critere + in: query + description: Nom du critère d'impact écologique + required: true + schema: + type: string + description: Nom du critère d'impact écologique + responses: + '200': + description: mix Electrique trouvé + content: + application/json: + schema: + $ref: '#/components/schemas/MixElectriqueDTO' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + '404': + description: mix Electrique non trouvé + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + '500': + description: Internal Server Error + content: + application/hal+json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + /referentiel/mixelecs/{pays}: + get: + tags: + - Interne NumEcoEval + summary: Endpoint interne à NumEcoEval - Récupération de Mix électriques par pays + description: | + Endpoint interne utilisé dans la génération des indicateurs par le module api-calcul de NumEcoEval. + Récupération de mix électriques en fonction de paramètres: + <ul> + <li>Le pays de l'équipement: pays</li> + </ul> + Cas spécifique avec pays = _all pour retourner tous les Mix electriques. + operationId: getMixElectriqueParPays + parameters: + - name: pays + in: path + description: Pays recherché + required: true + schema: + type: string + description: Pays recherché + responses: + '200': + description: mix Electrique trouvé + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/MixElectriqueDTO' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + '404': + description: mix Electrique non trouvé + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + '500': + description: Internal Server Error + content: + application/hal+json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + /referentiel/impactsMessagerie: + get: + tags: + - Interne NumEcoEval + summary: Endpoint interne à NumEcoEval - Récupération des impacts de messagerie + description: | + Endpoint interne utilisé dans la génération des indicateurs par le module api-calcul de NumEcoEval. + Renvoie l'intégralité des impacts de messagerie. + operationId: getAllImpactMessagerie + responses: + '200': + description: ' Référentiel Impact Messagerie' + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ImpactMessagerieDTO' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + '404': + description: ' Référentiel Impact Messagerie non trouvé' + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + '500': + description: Internal Server Error + content: + application/hal+json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + /referentiel/impactsMessagerie/{critere}: + get: + tags: + - Interne NumEcoEval + summary: Endpoint interne à NumEcoEval - Récupération d'un impact de messagerie + description: | + Endpoint interne utilisé dans l'enrichissement des données pour un calcul dans le module api-enrichissement de NumEcoEval. + Renvoie un élément du référentiel des impacts de messagerie. + operationId: getImpactMessagerie + parameters: + - name: critere + in: path + description: Critère recherché + required: true + schema: + type: string + description: Critère recherché + responses: + '200': + description: ' Référentiel Impact Messagerie' + content: + application/json: + schema: + $ref: '#/components/schemas/ImpactMessagerieDTO' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + '404': + description: ' Référentiel Impact Messagerie non trouvé' + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + '500': + description: Internal Server Error + content: + application/hal+json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + /referentiel/impactreseaux: + get: + tags: + - Interne NumEcoEval + summary: Endpoint interne à NumEcoEval - Récupération d'un Impact Réseau + description: | + Endpoint interne utilisé dans la génération des indicateurs par le module api-calcul de NumEcoEval. + Récupération d'un impact écologique vis à vis de l'usage du réseau en fonction de 3 paramètres: + <ul> + <li>La référence d'impact réseau: refReseau</li> + <li>Le critère d'impact: critere</li> + <li>L'étape ACV: etapeACV</li> + </ul> + . + operationId: getImpactReseau + parameters: + - name: refReseau + in: query + description: Référence de réseau recherché + required: true + schema: + type: string + description: Référence de réseau recherché + - name: critere + in: query + description: Nom du critère d'impact écologique + required: true + schema: + type: string + description: Nom du critère d'impact écologique + - name: etapeacv + in: query + description: Code de l'étape ACV + required: true + schema: + type: string + description: Code de l'étape ACV + responses: + '200': + description: impact reseau trouvé + content: + application/json: + schema: + $ref: '#/components/schemas/ImpactReseauDTO' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + '404': + description: Impact Reseau non trouvé + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + '500': + description: Internal Server Error + content: + application/hal+json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + /referentiel/impactequipements: + get: + tags: + - Interne NumEcoEval + summary: Endpoint interne à NumEcoEval - Récupération d'un Impact Equipement + description: | + Endpoint interne utilisé dans la génération des indicateurs par le module api-calcul de NumEcoEval. + Récupération d'un impact équipement en fonction de 3 paramètres: + <ul> + <li>La référence d'équipement: refEquipement</li> + <li>Le critère d'impact: critere</li> + <li>L'étape ACV: etapeACV</li> + </ul> + . + operationId: getImpactEquipement + parameters: + - name: refEquipement + in: query + description: Référence de l'équipement recherché + required: true + schema: + type: string + description: Référence de l'équipement recherché + - name: critere + in: query + description: Nom du critère d'impact écologique + required: true + schema: + type: string + description: Nom du critère d'impact écologique + - name: etapeacv + in: query + description: Code de l'étape ACV + required: true + schema: + type: string + description: Code de l'étape ACV + responses: + '200': + description: impact equipement trouvé + content: + application/json: + schema: + $ref: '#/components/schemas/ImpactEquipementDTO' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + '404': + description: Impact Equipement non trouvé + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + '500': + description: Internal Server Error + content: + application/hal+json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + /referentiel/hypothese: + get: + tags: + - Interne NumEcoEval + summary: Endpoint interne à NumEcoEval - Récupération d'une hypothèse par son code + description: | + Endpoint interne utilisé dans la génération des indicateurs par le module api-calcul de NumEcoEval. + Renvoie une hypothèse en fonction de sa clé. + operationId: getHypothese + parameters: + - name: cle + in: query + description: Clé de l'hypothèse + required: true + schema: + type: string + description: Clé de l'hypothèse + responses: + '200': + description: Hypothèse trouvé + content: + application/json: + schema: + $ref: '#/components/schemas/HypotheseDTO' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + '404': + description: Hypothèse non trouvé + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + '500': + description: Internal Server Error + content: + application/hal+json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + /referentiel/etapes: + get: + tags: + - Interne NumEcoEval + summary: Endpoint interne à NumEcoEval - Récupération de toutes les étapes ACV + description: | + Endpoint interne utilisé dans la génération des indicateurs par le module api-calcul de NumEcoEval. + Renvoie l'intégralité des étapes du cycle de vie (étapes ACV) des équipements. + operationId: getAllEtapes + responses: + '200': + description: etape acv non trouvé + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/EtapeDTO' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + '404': + description: Impact Reseau non trouvé + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + '500': + description: Internal Server Error + content: + application/hal+json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + /referentiel/criteres: + get: + tags: + - Interne NumEcoEval + summary: Endpoint interne à NumEcoEval - Récupération de tous les critères d'impacts écologiques + description: | + Endpoint interne utilisé dans la génération des indicateurs par le module api-calcul de NumEcoEval. + operationId: getAllCriteres + responses: + '200': + description: impact reseau trouvé + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/CritereDTO' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + '404': + description: Impact Reseau non trouvé + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + '500': + description: Internal Server Error + content: + application/hal+json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + /referentiel/correspondanceRefEquipement: + get: + tags: + - Interne NumEcoEval + summary: Endpoint interne à NumEcoEval - Récupération d'une correspondance de refEquipement à partir d'un modèle d'équipement. + description: | + Endpoint interne utilisé dans l'import de données d'entrées dans NumEcoEval + pour déterminer la référence d'équipement à utiliser dans les référentiels'. + operationId: getCorrespondanceRefEquipement + parameters: + - name: modele + in: query + required: true + schema: + type: string + responses: + '200': + description: Correspondance trouvée + content: + application/json: + schema: + $ref: '#/components/schemas/CorrespondanceRefEquipementDTO' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + '404': + description: Correspondance non trouvée + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' + '500': + description: Internal Server Error + content: + application/hal+json: + schema: + $ref: '#/components/schemas/ErrorResponseDTO' +components: + schemas: + ErrorResponseDTO: + type: object + properties: + code: + type: integer + description: Code de l'erreur + format: int32 + message: + type: string + description: Message de l'erreur + status: + type: string + description: Statut HTTP de la réponse + enum: + - 100 CONTINUE + - 101 SWITCHING_PROTOCOLS + - 102 PROCESSING + - 103 EARLY_HINTS + - 103 CHECKPOINT + - 200 OK + - 201 CREATED + - 202 ACCEPTED + - 203 NON_AUTHORITATIVE_INFORMATION + - 204 NO_CONTENT + - 205 RESET_CONTENT + - 206 PARTIAL_CONTENT + - 207 MULTI_STATUS + - 208 ALREADY_REPORTED + - 226 IM_USED + - 300 MULTIPLE_CHOICES + - 301 MOVED_PERMANENTLY + - 302 FOUND + - 302 MOVED_TEMPORARILY + - 303 SEE_OTHER + - 304 NOT_MODIFIED + - 305 USE_PROXY + - 307 TEMPORARY_REDIRECT + - 308 PERMANENT_REDIRECT + - 400 BAD_REQUEST + - 401 UNAUTHORIZED + - 402 PAYMENT_REQUIRED + - 403 FORBIDDEN + - 404 NOT_FOUND + - 405 METHOD_NOT_ALLOWED + - 406 NOT_ACCEPTABLE + - 407 PROXY_AUTHENTICATION_REQUIRED + - 408 REQUEST_TIMEOUT + - 409 CONFLICT + - 410 GONE + - 411 LENGTH_REQUIRED + - 412 PRECONDITION_FAILED + - 413 PAYLOAD_TOO_LARGE + - 413 REQUEST_ENTITY_TOO_LARGE + - 414 URI_TOO_LONG + - 414 REQUEST_URI_TOO_LONG + - 415 UNSUPPORTED_MEDIA_TYPE + - 416 REQUESTED_RANGE_NOT_SATISFIABLE + - 417 EXPECTATION_FAILED + - 418 I_AM_A_TEAPOT + - 419 INSUFFICIENT_SPACE_ON_RESOURCE + - 420 METHOD_FAILURE + - 421 DESTINATION_LOCKED + - 422 UNPROCESSABLE_ENTITY + - 423 LOCKED + - 424 FAILED_DEPENDENCY + - 425 TOO_EARLY + - 426 UPGRADE_REQUIRED + - 428 PRECONDITION_REQUIRED + - 429 TOO_MANY_REQUESTS + - 431 REQUEST_HEADER_FIELDS_TOO_LARGE + - 451 UNAVAILABLE_FOR_LEGAL_REASONS + - 500 INTERNAL_SERVER_ERROR + - 501 NOT_IMPLEMENTED + - 502 BAD_GATEWAY + - 503 SERVICE_UNAVAILABLE + - 504 GATEWAY_TIMEOUT + - 505 HTTP_VERSION_NOT_SUPPORTED + - 506 VARIANT_ALSO_NEGOTIATES + - 507 INSUFFICIENT_STORAGE + - 508 LOOP_DETECTED + - 509 BANDWIDTH_LIMIT_EXCEEDED + - 510 NOT_EXTENDED + - 511 NETWORK_AUTHENTICATION_REQUIRED + timestamp: + type: string + description: Date & Heure de l'erreur + format: date-time + description: Objet standard pour les réponses en cas d'erreur d'API + RapportImportDTO: + type: object + properties: + fichier: + type: string + erreurs: + type: array + items: + type: string + nbrLignesImportees: + type: integer + format: int64 + VersionDTO: + type: object + properties: + version: + type: string + description: La version + description: Version applicative + TypeEquipementDTO: + type: object + properties: + type: + type: string + description: Type de l'équipment physique, clé du référentiel + serveur: + type: boolean + description: Flag indiquant si l'équipement physique est un serveur + commentaire: + type: string + description: Commentaire de l'entrée dans le référentiel + dureeVieDefaut: + type: number + description: Durée de vie par défaut de ce type d'équipement physique + format: double + source: + type: string + description: Source de l'information du référentiel + refEquipementParDefaut: + type: string + description: Référence de l'équipement par défaut, permet des correspondances en cas d'absence de correspondance direct + description: Référentiel des types d'équipements physiques utilisables dans le système. La clé du référentiel est le champ type. + MixElectriqueDTO: + type: object + properties: + pays: + type: string + description: Pays concerné, fait partie de la clé du référentiel + raccourcisAnglais: + type: string + description: Code du pays concerné en anglais + critere: + type: string + description: Critère d'impact écologique concerné, fait partie de la clé du référentiel + valeur: + type: number + description: Valeur du référentiel + format: double + source: + type: string + description: Source de la valeur du référentiel + description: Référentiel des mix électrique couvrant l'usage de l'électricité vis à vis du pays d'utilisation de l'équipement. La clé du référentiel est composé des champs pays et critere. + ImpactMessagerieDTO: + type: object + properties: + constanteCoefficientDirecteur: + type: number + description: Coefficient directeur de la fonction affine + format: double + constanteOrdonneeOrigine: + type: number + description: Constante de la fonction affine + format: double + critere: + type: string + description: Critère de l'impact écologique d'une messagerie, clé du référentiel + source: + type: string + description: Source de l'impact écologique d'une messagerie + description: Référentiel de l'impact écologique d'une messagerie. La clé est le champ critere. Chaque entrée représente les composants d'une fonction affine (Ax+b). + ImpactReseauDTO: + type: object + properties: + refReseau: + type: string + description: Référence de l'usage du réseau, fait partie de la clé du référentiel + etapeACV: + type: string + description: Étape ACV concerné pour l'usage du réseau, fait partie de la clé du référentiel + critere: + type: string + description: Critère d'impact écologique concerné pour l'usage du réseau, fait partie de la clé du référentiel + unite: + type: string + description: Unité de l'impact écologique concerné pour l'usage du réseau. Champ Déprécié + deprecated: true + source: + type: string + description: Source de l'impact écologique + valeur: + type: number + description: Valeur de l'impact écologique + format: double + consoElecMoyenne: + type: number + description: Consommation électrique moyenne de l'impact écologique + format: double + description: Référentiel de l'impact écologique d'un équipement physique vis à vis de l'usage du réseau dans les référentiels. La clé est composé des champs refReseau, etapeACV et critere. + ImpactEquipementDTO: + type: object + properties: + refEquipement: + type: string + description: Référence de l'équipement physique, fait partie de la clé dans le référentiel + etape: + type: string + description: Étape ACV concernée, fait partie de la clé dans le référentiel + critere: + type: string + description: Critère d'impact écologique concerné, fait partie de la clé dans le référentiel + source: + type: string + description: Source de l'impact écologique pour cette équipement physique + type: + type: string + description: Type de l'équipement physique concerné + valeur: + type: number + description: Valeur de l'impact écologique + format: double + consoElecMoyenne: + type: number + description: Consommation électrique moyenne + format: double + description: + type: string + description: Description de l'entrée dans le référentiel + description: Référentiel de l'impact écologique d'un équipement physique dans les référentiels. La clé est composé des champs refEquipement, etape et critere. + HypotheseDTO: + type: object + properties: + code: + type: string + description: Code de l'hypothèse, clé du référentiel + valeur: + type: string + description: Valeur de l'hypothèse + source: + type: string + description: Source de l'hypothèse + description: Référentiel des hypothèses utilisées pour les calculs + EtapeDTO: + type: object + properties: + code: + pattern: '[A-Z]+' + type: string + description: Code de l'étape. Ne contient que des majuscules, clé du référentiel + libelle: + type: string + description: Libellé de l'étape + description: Référentiel d'étape dans le cycle de vie d'un équipement (Etape ACV) + CritereDTO: + type: object + properties: + nomCritere: + type: string + description: Nom du critère d'impact écologique, clé du référentiel + unite: + type: string + description: Unité du critère d'impact écologique + description: + type: string + description: Description du critère d'impact écologique + description: Référentiel de critère d'impact écologique + CorrespondanceRefEquipementDTO: + type: object + properties: + modeleEquipementSource: + type: string + description: Modèle de l'équipement, clé du référentiel + refEquipementCible: + type: string + description: Référence d'équipement correspondant au modèle de l'équipement + description: Référentiel de correspondance entre un modèle d'équipement physique et une référence d'équipement dans les référentiels. diff --git a/services/common/src/main/resources/static/asyncapi_equipement_physique.yaml b/services/common/src/main/resources/static/asyncapi_equipement_physique.yaml new file mode 100644 index 00000000..704d8b3c --- /dev/null +++ b/services/common/src/main/resources/static/asyncapi_equipement_physique.yaml @@ -0,0 +1,335 @@ +asyncapi: 2.6.0 +info: + title: api-event-calcul-enrichissement-eqp + version: '1.0' + description: | + Lecture d'un équipement physique + puis récupération des référentiels nécessaires au calcul via REST + puis production d'un message pour demande de calcul d'impacts de l'équipement physique +servers: + bootstrap: + url: localhost:9092 + protocol: kafka +channels: + entree_equipementPhysique: + description: Topic d'envoie des données d'entrées pour les équipements physiques à enrichir + publish: + message: + $ref: '#/components/messages/EntreeEquipementPhysique' + calcul_equipementPhysique: + description: Topic d'envoie des données d'entrées pour les équipements physiques à calculer + subscribe: + message: + $ref: '#/components/messages/CalculEquipementPhysique' +components: + messages: + EntreeEquipementPhysique: + name: EntreeEquipementPhysique + title: Message des données d'entrée pour un équipement Physique dans NumEcoEval + summary: Données d'un équipement physique pour un calcul dans NumEcoEval + contentType: application/json + payload: + $ref: "#/components/schemas/EquipementPhysiqueDTO" + CalculEquipementPhysique: + name: CalculEquipementPhysique + title: Message pour le calcul d'impact d'un équipement physique + summary: Intégralité des données permettant le calcul d'un équipement physique pour un calcul dans NumEcoEval + contentType: application/json + payload: + $ref: "#/components/schemas/CalculEquipementPhysiqueDTO" + schemas: + CalculEquipementPhysiqueDTO: + $id: CalculEquipementPhysiqueDTO + description: Objet regroupant toutes les données pour le calcul des indicateurs d'un équipement physique dans NumEcoEval + type: object + properties: + equipementPhysique: + $ref: "#/components/schemas/EquipementPhysiqueDTO" + typeEquipement: + description: Référentiel rattaché au type d'équipement de l'équipement physique + $ref: "#/components/schemas/TypeEquipementDTO" + correspondanceRefEquipement: + description: Correspondance de référence d'équipement actuel pour le modèle de l'équipement + $ref: "#/components/schemas/CorrespondanceRefEquipementDTO" + hypotheses: + description: Hypothèses disponibles pour le calcul + type: array + items: + $ref: "#/components/schemas/HypotheseDTO" + etapes: + description: Référentiels des Étapes ACV + type: array + items: + $ref: "#/components/schemas/EtapeDTO" + criteres: + description: Référentiels des Critères d'impact écologiques + type: array + items: + $ref: "#/components/schemas/CritereDTO" + mixElectriques: + description: Mix électrique du pays d'utilisation de l'équipement physique + type: array + items: + $ref: "#/components/schemas/MixElectriqueDTO" + impactsReseau: + description: Référentiels des Critères d'impact écologiques + type: array + items: + $ref: "#/components/schemas/ImpactReseauDTO" + impactsEquipement: + description: Référentiels d'impacts écologiques pour cet équipement + type: array + items: + $ref: "#/components/schemas/ImpactEquipementDTO" + EquipementPhysiqueDTO: + $id: EquipementPhysiqueDTO + description: Représentation d'un équipement physique dans NumEcoEval + type: object + properties: + id: + description: "" + type: integer + format: int64 + nomEquipementPhysique: + description: "" + type: string + modele: + description: "" + type: string + type: + description: "" + type: string + statut: + description: "" + type: string + paysDUtilisation: + description: "" + type: string + utilisateur: + description: "" + type: string + dateAchat: + description: "" + type: string + format: date + dateRetrait: + description: "" + type: string + format: date + nbCoeur: + description: "" + type: string + modeUtilisation: + description: "" + type: string + tauxUtilisation: + description: "" + type: number + format: double + nomCourtDatacenter: + description: "" + type: string + nbJourUtiliseAn: + description: "" + type: number + format: double + goTelecharge: + description: "" + type: number + format: float + quantite: + description: "" + type: number + format: double + consoElecAnnuelle: + description: "" + type: number + format: double + serveur: + description: "" + type: boolean + nomEntite: + description: "" + type: string + nomSourceDonnee: + description: "Nom de la source de la donnée" + type: string + nomLot: + description: "Nom du lot rattaché" + type: string + dateLot: + description: "" + type: string + format: date + nomOrganisation: + description: "" + type: string + nbEquipementsVirtuels: + description: "Nombre d'équipements virtuels rattachés à l'équipement physique - Utilisé dans les traitements" + type: integer + nbTotalVCPU: + description: "Nombre total de VCPU (correspond à la somme des vCPU définis dans les équipements virtuels) ou null si un des équipements virtuels n'a pas de vCPU - Utilisé dans les traitements" + type: integer + dataCenter: + $ref: "#/components/schemas/DataCenterDTO" + stockageTotalVirtuel: + description: | + Stockage total en To pour tous les équipements virtuels sur l'équipement physique parent. + Ne sera utilisé que si le type d'équipement virtuel n'est pas "stockage". + type: number + format: double + DataCenterDTO: + $id: DataCenterDTO + description: Représentation d'un Data Center dans NumEcoEval + properties: + id: + description: "" + type: integer + format: int64 + nomCourtDatacenter: + description: "" + type: string + nomLongDatacenter: + description: "" + type: string + pue: + description: "" + type: number + format: double + localisation: + description: "" + type: string + nomEntite: + description: "" + type: string + nomSourceDonnee: + description: "Nom de la source de la donnée" + type: string + nomLot: + description: "Nom du lot rattaché" + type: string + dateLot: + description: "" + type: string + format: date + nomOrganisation: + description: "" + type: string + CorrespondanceRefEquipementDTO: + type: object + properties: + modeleEquipementSource: + type: string + description: Modèle de l'équipement + refEquipementCible: + type: string + description: Référence d'équipement correspondant au modèle de l'équipement + description: Référentiel de correspondance entre un modèle d'équipement physique + et une référence d'équipement dans les référentiels. + TypeEquipementDTO: + type: object + properties: + type: + type: string + serveur: + type: boolean + commentaire: + type: string + dureeVieDefaut: + type: number + format: double + source: + type: string + refEquipementParDefaut: + type: string + description: Référentiel de type d'équipement physique. + HypotheseDTO: + type: object + properties: + code: + type: string + valeur: + type: string + source: + type: string + EtapeDTO: + type: object + properties: + code: + type: string + libelle: + type: string + CritereDTO: + type: object + properties: + nomCritere: + type: string + description: Nom du critère d'impact écologique + unite: + type: string + description: Unité du critère d'impact écologique + description: + type: string + description: Description du critère d'impact écologique + MixElectriqueDTO: + type: object + properties: + pays: + type: string + raccourcisAnglais: + type: string + critere: + type: string + valeur: + type: number + format: double + source: + type: string + description: Référentiel de critère d'impact écologique + ImpactReseauDTO: + type: object + properties: + refReseau: + type: string + etapeACV: + type: string + critere: + type: string + unite: + type: string + source: + type: string + valeur: + type: number + format: double + consoElecMoyenne: + type: number + format: double + description: Référentiel d'impact écologique pour l'usage du réseau par un équipement + ImpactEquipementDTO: + type: object + properties: + refEquipement: + type: string + etape: + type: string + critere: + type: string + source: + type: string + type: + type: string + valeur: + type: number + format: double + consoElecMoyenne: + type: number + format: double + description: + type: string + description: Référentiel d'impact écologique pour un équipement physique + operationTraits: + kafka: + bindings: + kafka: + groupId: api-event-calcul-enrichissement-eqp diff --git a/services/common/src/main/resources/static/asyncapi_merge.yaml b/services/common/src/main/resources/static/asyncapi_merge.yaml new file mode 100644 index 00000000..b92b45a0 --- /dev/null +++ b/services/common/src/main/resources/static/asyncapi_merge.yaml @@ -0,0 +1,26 @@ +openapi: 3.0.1 +info: + title: Temporaire - Modeles utilisés pour les échanges AsyncAPI + version: 0.0.1 +paths: + /unused: + get: + operationId: unused + description: "Non utilisé - juste présent pour la compliance" + responses: + default: + description: Types Equipement + content: + application/json: + schema: + type: string +components: + schemas: + CalculEquipementPhysiqueDTO: + $ref: "./asyncapi_equipement_physique.yaml#/components/schemas/CalculEquipementPhysiqueDTO" + EquipementPhysiqueDTO: + $ref: "./asyncapi_equipement_physique.yaml#/components/schemas/EquipementPhysiqueDTO" + DataCenterDTO: + $ref: "./asyncapi_equipement_physique.yaml#/components/schemas/DataCenterDTO" + MessagerieDTO: + $ref: "./asyncapi_messagerie.yaml#/components/schemas/MessagerieDTO" diff --git a/services/common/src/main/resources/static/asyncapi_messagerie.yaml b/services/common/src/main/resources/static/asyncapi_messagerie.yaml new file mode 100644 index 00000000..1fbf243e --- /dev/null +++ b/services/common/src/main/resources/static/asyncapi_messagerie.yaml @@ -0,0 +1,68 @@ +asyncapi: 2.6.0 +info: + title: api-event-donneesEntrees-messagerie + version: '1.0' + description: | + Lecture en base de données des éléments de messagerie au statut A_INGERER, + Mise à jour des données lues au statut INGERE et envoie d'un message par éléments de messagerie pour enrichissement. +servers: + bootstrap: + url: localhost:9092 + protocol: kafka +channels: + entree_messagerie: + description: Topic d'envoie des données de messagerie à enrichir + subscribe: + message: + $ref: '#/components/messages/MessageEntreeMessagerie' +components: + messages: + MessageEntreeMessagerie: + name: MessageEntreeMessagerie + title: Entrée d'un élément de messagerie dans NumEcoEval + summary: Données d'un élément de messagerie pour un calcul dans NumEcoEval + contentType: application/json + payload: + $ref: "#/components/schemas/MessagerieDTO" + schemas: + + MessagerieDTO: + description: Représentation d'éléments de messagerie dans NumEcoEval + properties: + id: + description: "" + type: integer + format: int64 + nombreMailEmis: + description: "" + type: integer + nombreMailEmisXDestinataires: + description: "" + type: integer + volumeTotalMailEmis: + description: "" + type: integer + moisAnnee: + description: "Mois et Année rattachés au données, format MMAAAA" + type: integer + nomEntite: + description: "" + type: string + nomSourceDonnee: + description: "Nom de la source de la donnée" + type: string + nomLot: + description: "Nom du lot rattaché" + type: string + dateLot: + description: "" + type: string + format: date + nomOrganisation: + description: "" + type: string + operationTraits: + kafka: + bindings: + kafka: + groupId: api-event-donneesEntrees-messagerie diff --git a/services/common/src/test/java/org/mte/numecoeval/common/utils/PreparedStatementUtilsTest.java b/services/common/src/test/java/org/mte/numecoeval/common/utils/PreparedStatementUtilsTest.java new file mode 100644 index 00000000..d122b29d --- /dev/null +++ b/services/common/src/test/java/org/mte/numecoeval/common/utils/PreparedStatementUtilsTest.java @@ -0,0 +1,93 @@ +package org.mte.numecoeval.common.utils; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullSource; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +import java.sql.Date; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.sql.Types; +import java.time.LocalDate; +import java.time.LocalDateTime; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +class PreparedStatementUtilsTest { + + @Mock + PreparedStatement preparedStatement; + + @BeforeEach + void setup() { + MockitoAnnotations.openMocks(this); + } + + @ParameterizedTest + @NullSource + void whenNullValue_getDateFromLocalDate_shouldReturnNull(LocalDate value) { + assertNull(PreparedStatementUtils.getDateFromLocalDate(value)); + } + + @Test + void whenValueIsAvailable_getDateFromLocalDate_shouldReturnLocalDate() { + var value = LocalDate.of(2022,1,1); + + var result = PreparedStatementUtils.getDateFromLocalDate(value); + + assertEquals(Date.valueOf("2022-01-01"), result); + } + + @ParameterizedTest + @NullSource + void whenNullValue_getTimestampFromLocalDateTime_shouldReturnNull(LocalDateTime value) { + assertNull(PreparedStatementUtils.getTimestampFromLocalDateTime(value)); + } + + @Test + void whenValueIsAvailable_getTimestampFromLocalDateTime_shouldReturnLocalDate() { + var value = LocalDateTime.of(2022,1,1, 15,15,6); + + var result = PreparedStatementUtils.getTimestampFromLocalDateTime(value); + + assertEquals(Timestamp.valueOf("2022-01-01 15:15:06.000"), result); + } + + @ParameterizedTest + @NullSource + void whenNullValue_setIntValue_shouldSetNull(Integer value) throws SQLException { + PreparedStatementUtils.setIntValue(preparedStatement, 1, value); + + Mockito.verify(preparedStatement, Mockito.times(1)).setNull(1, Types.INTEGER); + } + + @Test + void whenValueIsAvailable_setIntValue_shouldSetIntWithValue() throws SQLException { + var value = 1; + PreparedStatementUtils.setIntValue(preparedStatement, 1, value); + + Mockito.verify(preparedStatement, Mockito.times(1)).setInt(1, value); + } + + @ParameterizedTest + @NullSource + void whenNullValue_setDoubleValue_shouldSetNull(Double value) throws SQLException { + PreparedStatementUtils.setDoubleValue(preparedStatement, 1, value); + + Mockito.verify(preparedStatement, Mockito.times(1)).setNull(1, Types.DOUBLE); + } + + @Test + void whenValueIsAvailable_setDoubleValue_shouldSetDoubleWithValue() throws SQLException { + var value = 1.2; + PreparedStatementUtils.setDoubleValue(preparedStatement, 1, value); + + Mockito.verify(preparedStatement, Mockito.times(1)).setDouble(1, value); + } +} diff --git a/services/common/src/test/java/org/mte/numecoeval/common/utils/ResultSetUtilsTest.java b/services/common/src/test/java/org/mte/numecoeval/common/utils/ResultSetUtilsTest.java new file mode 100644 index 00000000..3a1cdc23 --- /dev/null +++ b/services/common/src/test/java/org/mte/numecoeval/common/utils/ResultSetUtilsTest.java @@ -0,0 +1,110 @@ +package org.mte.numecoeval.common.utils; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.sql.Date; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.time.LocalDate; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.Mockito.when; + +class ResultSetUtilsTest { + + @Mock + ResultSet resultSet; + + @BeforeEach + void setup() { + MockitoAnnotations.openMocks(this); + } + + @ParameterizedTest + @NullAndEmptySource + void whenColumnIsNull_getMethodsUsingCheckColumnInResultSet_shouldReturn(String columnName) throws SQLException { + assertNull(ResultSetUtils.getLocalDate(resultSet, columnName)); + assertNull(ResultSetUtils.getLocalDateTime(resultSet, columnName)); + assertNull(ResultSetUtils.getFloat(resultSet, columnName)); + assertNull(ResultSetUtils.getInteger(resultSet, columnName)); + assertNull(ResultSetUtils.getDouble(resultSet, columnName)); + } + + @Test + void whenResultSetIsNull_getMethodsUsingCheckColumnInResultSet_shouldReturn() throws SQLException { + var columnName = "colonne"; + assertNull(ResultSetUtils.getLocalDate(null, columnName)); + assertNull(ResultSetUtils.getLocalDateTime(null, columnName)); + assertNull(ResultSetUtils.getFloat(null, columnName)); + assertNull(ResultSetUtils.getInteger(null, columnName)); + assertNull(ResultSetUtils.getDouble(null, columnName)); + } + + @Test + void whenValueIsAvailable_getLocalDate_shouldReturnLocalDate() throws SQLException { + var columnName = "test"; + var dateTime = 1683310020L; + when(resultSet.getString(columnName)).thenReturn(new Date(dateTime).toString()); + when(resultSet.getDate(columnName)).thenReturn(new Date(dateTime)); + + var result = ResultSetUtils.getLocalDate(resultSet, columnName); + + assertEquals(LocalDate.of(1970,1,20), result); + } + + @Test + void whenValueIsAvailable_getLocalDateTime_shouldReturnLocalDate() throws SQLException { + var columnName = "test"; + var dateTime = 1683310020L; + when(resultSet.getString(columnName)).thenReturn(new Date(dateTime).toString()); + var originalTimestamp = new Timestamp(dateTime); + when(resultSet.getTimestamp(columnName)).thenReturn(originalTimestamp); + + var result = ResultSetUtils.getLocalDateTime(resultSet, columnName); + + assertEquals(originalTimestamp.toLocalDateTime(), result); + } + + @Test + void whenValueIsAvailable_getFloat_shouldReturnLocalDate() throws SQLException { + var columnName = "test"; + var value = Float.valueOf(6.6f); + when(resultSet.getString(columnName)).thenReturn(value.toString()); + when(resultSet.getFloat(columnName)).thenReturn(value); + + var result = ResultSetUtils.getFloat(resultSet, columnName); + + assertEquals(value, result); + } + + @Test + void whenValueIsAvailable_getDouble_shouldReturnLocalDate() throws SQLException { + var columnName = "test"; + var value = Double.valueOf(6.6); + when(resultSet.getString(columnName)).thenReturn(value.toString()); + when(resultSet.getDouble(columnName)).thenReturn(value); + + var result = ResultSetUtils.getDouble(resultSet, columnName); + + assertEquals(value, result); + } + + @Test + void whenValueIsAvailable_getInteger_shouldReturnLocalDate() throws SQLException { + var columnName = "test"; + var value = Integer.valueOf(6); + when(resultSet.getString(columnName)).thenReturn(value.toString()); + when(resultSet.getInt(columnName)).thenReturn(value); + + var result = ResultSetUtils.getInteger(resultSet, columnName); + + assertEquals(value, result); + } +} diff --git a/services/core/.gitignore b/services/core/.gitignore new file mode 100644 index 00000000..20c8d182 --- /dev/null +++ b/services/core/.gitignore @@ -0,0 +1,32 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/services/core/README.md b/services/core/README.md new file mode 100644 index 00000000..90384e90 --- /dev/null +++ b/services/core/README.md @@ -0,0 +1,3 @@ +# core + +POM parents des modules du projet diff --git a/services/core/ci_settings.xml b/services/core/ci_settings.xml new file mode 100644 index 00000000..5205903f --- /dev/null +++ b/services/core/ci_settings.xml @@ -0,0 +1,16 @@ +<settings xmlns="http://maven.apache.org/SETTINGS/1.1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.1.0 http://maven.apache.org/xsd/settings-1.1.0.xsd"> + <servers> + <server> + <id>gitlab-maven</id> + <configuration> + <httpHeaders> + <property> + <name>Job-Token</name> + <value>${env.CI_JOB_TOKEN}</value> + </property> + </httpHeaders> + </configuration> + </server> + </servers> +</settings> diff --git a/services/core/dependency_check_suppressions.xml b/services/core/dependency_check_suppressions.xml new file mode 100644 index 00000000..fbf9371b --- /dev/null +++ b/services/core/dependency_check_suppressions.xml @@ -0,0 +1,3 @@ +<?xml version="1.0" encoding="UTF-8"?> +<suppressions xmlns="https://jeremylong.github.io/DependencyCheck/dependency-suppression.1.3.xsd"> +</suppressions> diff --git a/services/core/pom.xml b/services/core/pom.xml new file mode 100644 index 00000000..1714c7b8 --- /dev/null +++ b/services/core/pom.xml @@ -0,0 +1,377 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-parent</artifactId> + <version>3.0.7</version> + <relativePath/> <!-- lookup parent from repository --> + </parent> + + <groupId>org.mte.numecoeval</groupId> + <artifactId>core</artifactId> + <version>1.2.3-SNAPSHOT</version> + <name>core</name> + <packaging>pom</packaging> + <description>Projet parent du projet NumEcoEval</description> + + <repositories> + <repository> + <id>gitlab-maven</id> + <url>https://gitlab-forge.din.developpement-durable.gouv.fr/api/v4/projects/20519/packages/maven</url> + </repository> + </repositories> + + <distributionManagement> + <repository> + <id>gitlab-maven</id> + <url>https://gitlab-forge.din.developpement-durable.gouv.fr/api/v4/projects/20519/packages/maven</url> + </repository> + + <snapshotRepository> + <id>gitlab-maven</id> + <url>https://gitlab-forge.din.developpement-durable.gouv.fr/api/v4/projects/20519/packages/maven</url> + </snapshotRepository> + </distributionManagement> + + <properties> + <java.version>17</java.version> + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> + + <!-- Utilitaires --> + <apache-commons-lang3.version>3.12.0</apache-commons-lang3.version> + <apache-commons-collection4.version>4.4</apache-commons-collection4.version> + <apache-commons-io.version>2.11.0</apache-commons-io.version> + <apache-commons-csv.version>1.9.0</apache-commons-csv.version> + <mapstruct.version>1.5.3.Final</mapstruct.version> + + <!-- CVE multiples sur Snakeyaml (CVE-2020-13936, CVE-2022-38749, CVE-2022-38751, CVE-2022-38750, CVE-2022-38752)--> + <snakeyaml.version>1.33</snakeyaml.version> + <!-- CVE sur Postgres (CVE-2022-41946) --> + <postgresql.version>42.5.4</postgresql.version> + + <!-- CVE sur Spring CVE-2023-20863--> + <spring-framework.version>6.0.8</spring-framework.version> + + <!-- Documentation automatique des API REST en OpenAPI 3.0 --> + <springdoc-openapi-ui.version>2.0.4</springdoc-openapi-ui.version> + + <!-- OpenAPI Generator (API REST generator) --> + <openapi-generator-version>6.4.0</openapi-generator-version> + <jackson-databind-nullable-version>0.2.6</jackson-databind-nullable-version> + <!-- scs-multiapi-maven-plugin (Async API generator)--> + <scs-multiapi-maven-plugin.version>4.5.0</scs-multiapi-maven-plugin.version> + + <!-- Tests --> + <meanbean.version>2.0.3</meanbean.version> + <equalsverifier.version>3.10.1</equalsverifier.version> + <wiremock.version>2.34.0</wiremock.version> + <javafaker.version>1.0.2</javafaker.version> + <cucumber-bom.version>7.11.1</cucumber-bom.version> + <jacoco.version>0.8.8</jacoco.version> + <instancio-junit.version>2.2.0</instancio-junit.version> + <surefireArgLine>-Dfile.encoding=UTF-8</surefireArgLine> + <embedded-database-spring-test.version>2.2.0</embedded-database-spring-test.version> + <embedded-postgres.version>2.0.3</embedded-postgres.version> + </properties> + + <dependencyManagement> + <dependencies> + <dependency> + <groupId>org.mte.numecoeval</groupId> + <artifactId>common</artifactId> + <version>${project.version}</version> + </dependency> + + <dependency> + <groupId>org.meanbean</groupId> + <artifactId>meanbean</artifactId> + <version>${meanbean.version}</version> + <scope>test</scope> + </dependency> + + <dependency> + <groupId>org.apache.commons</groupId> + <artifactId>commons-lang3</artifactId> + <version>${commons-lang3.version}</version> + </dependency> + + <dependency> + <groupId>org.apache.commons</groupId> + <artifactId>commons-collections4</artifactId> + <version>${apache-commons-collection4.version}</version> + </dependency> + + <dependency> + <groupId>commons-io</groupId> + <artifactId>commons-io</artifactId> + <version>${apache-commons-io.version}</version> + </dependency> + + <dependency> + <groupId>org.apache.commons</groupId> + <artifactId>commons-csv</artifactId> + <version>${apache-commons-csv.version}</version> + </dependency> + + <dependency> + <groupId>org.mapstruct</groupId> + <artifactId>mapstruct</artifactId> + <version>${mapstruct.version}</version> + </dependency> + + <dependency> + <groupId>org.springdoc</groupId> + <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId> + <version>${springdoc-openapi-ui.version}</version> + </dependency> + + <dependency> + <groupId>org.springdoc</groupId> + <artifactId>springdoc-openapi-starter-common</artifactId> + <version>${springdoc-openapi-ui.version}</version> + </dependency> + + <!-- OpenAPI Generator --> + <dependency> + <groupId>org.openapitools</groupId> + <artifactId>jackson-databind-nullable</artifactId> + <version>${jackson-databind-nullable-version}</version> + </dependency> + + <!-- Cucumber (Tests) --> + <dependency> + <groupId>io.cucumber</groupId> + <artifactId>cucumber-bom</artifactId> + <version>${cucumber-bom.version}</version> + <type>pom</type> + <scope>import</scope> + </dependency> + + <dependency> + <groupId>nl.jqno.equalsverifier</groupId> + <artifactId>equalsverifier</artifactId> + <version>${equalsverifier.version}</version> + <scope>test</scope> + </dependency> + + + <!-- Donnnées aléatoires --> + <dependency> + <groupId>com.github.javafaker</groupId> + <artifactId>javafaker</artifactId> + <version>${javafaker.version}</version> + <scope>test</scope> + </dependency> + + <dependency> + <groupId>org.instancio</groupId> + <artifactId>instancio-junit</artifactId> + <version>${instancio-junit.version}</version> + <scope>test</scope> + </dependency> + <!-- Wiremock--> + <dependency> + <groupId>com.github.tomakehurst</groupId> + <artifactId>wiremock-jre8-standalone</artifactId> + <version>${wiremock.version}</version> + <scope>test</scope> + <exclusions> + <exclusion> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + </exclusion> + </exclusions> + </dependency> + + <!-- Base de données de tests--> + <dependency> + <groupId>io.zonky.test</groupId> + <artifactId>embedded-database-spring-test</artifactId> + <version>${embedded-database-spring-test.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>io.zonky.test</groupId> + <artifactId>embedded-postgres</artifactId> + <version>${embedded-postgres.version}</version> + <scope>test</scope> + </dependency> + + </dependencies> + </dependencyManagement> + + <profiles> + <profile> + <id>FULL</id> + <modules> + <module>../common</module> + <module>../calculs</module> + <module>../api-referentiel</module> + <module>../api-expositiondonneesentrees</module> + <module>../api-event-donneesEntrees</module> + <module>../api-event-calculs</module> + </modules> + </profile> + <!-- Dependency Check en local--> + <profile> + <id>DEPENDENCY-CHECK</id> + <build> + <plugins> + <plugin> + <groupId>org.owasp</groupId> + <artifactId>dependency-check-maven</artifactId> + <version>8.1.2</version> + <executions> + <execution> + <goals> + <goal>check</goal> + </goals> + <configuration> + <suppressionFile>${project.basedir}/dependency_check_suppressions.xml + </suppressionFile> + </configuration> + </execution> + </executions> + </plugin> + </plugins> + </build> + </profile> + <!-- Bypass des tests --> + <profile> + <id>SKIP-ALL-TEST</id> + <properties> + <skip.unit.tests>true</skip.unit.tests> + <skip.integration.tests>true</skip.integration.tests> + <maven.test.failure.ignore>true</maven.test.failure.ignore> + </properties> + </profile> + </profiles> + + <build> + + + <plugins> + <!-- Processors pour les annotations --> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-compiler-plugin</artifactId> + <version>3.8.1</version> + <configuration> + <source>${java.version}</source> <!-- depending on your project --> + <target>${java.version}</target> <!-- depending on your project --> + <encoding>${project.build.sourceEncoding}</encoding> + <annotationProcessorPaths> + <path> + <groupId>org.mapstruct</groupId> + <artifactId>mapstruct-processor</artifactId> + <version>${mapstruct.version}</version> + </path> + <path> + <groupId>org.projectlombok</groupId> + <artifactId>lombok</artifactId> + <version>${lombok.version}</version> + </path> + <path> + <groupId>org.projectlombok</groupId> + <artifactId>lombok-mapstruct-binding</artifactId> + <version>0.2.0</version> + </path> + <path> + <groupId>com.github.therapi</groupId> + <artifactId>therapi-runtime-javadoc-scribe</artifactId> + <version>0.15.0</version> + </path> + <path> + <groupId>org.instancio</groupId> + <artifactId>instancio-processor</artifactId> + <version>2.2.0</version> + </path> + <!-- other annotation processors --> + </annotationProcessorPaths> + </configuration> + </plugin> + + <!-- Correction nécessaire pour la gestion de l'UTF-8 sous Windows --> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-surefire-plugin</artifactId> + <configuration> + <redirectTestOutputToFile>true</redirectTestOutputToFile> + <argLine>@{surefireArgLine}</argLine> + </configuration> + </plugin> + + <!-- Plugin pour le reporting de la couverture du code --> + <plugin> + <groupId>org.jacoco</groupId> + <artifactId>jacoco-maven-plugin</artifactId> + <version>${jacoco.version}</version> + <executions> + <execution> + <id>pre-unit-test</id> + <goals> + <goal>prepare-agent</goal> + </goals> + <configuration> + <!-- Sets the path to the file which contains the execution data. --> + <destFile>${project.build.directory}/jacoco.exec</destFile> + <!-- Exclusion des codes générés et des loaders ayant des méthodes + trop longues --> + <excludes> + <exclude>**/generated/**</exclude> + <exclude>**/generated/*</exclude> + <exclude>**/domain/model/**</exclude> + <exclude>**/domain/exception/**</exclude> + <exclude>**/domain/data/**</exclude> + <exclude>**/infrastructure/client/**</exclude> + <exclude>**/infrastructure/configuration/**</exclude> + <exclude>**/infrastructure/config/**</exclude> + <exclude>**/infrastructure/jpa/entity/**</exclude> + <exclude>**/infrastructure/mapper/**</exclude> + <exclude>**/infrastructure/restapi/dto/**</exclude> + <exclude>**/infrastructure/controller/**</exclude> + <exclude>**/infrastructure/repository/**</exclude> + <exclude>**/infrastructure/kafkalistener/**</exclude> + </excludes> + <propertyName>surefireArgLine</propertyName> + </configuration> + </execution> + <!-- Reporting TUs --> + <execution> + <id>post-unit-test</id> + <phase>verify</phase> + <goals> + <goal>report</goal> + </goals> + <configuration> + <!-- Sets the path to the file which contains the execution data. --> + <dataFile>${project.build.directory}/jacoco.exec</dataFile> + <!-- Sets the output directory for the code coverage report. --> + <outputDirectory>${project.build.directory}/jacoco</outputDirectory> + <!-- Exclusion des codes générés --> + <excludes> + <exclude>**/generated/**</exclude> + <exclude>**/generated/*</exclude> + <exclude>**/domain/model/**</exclude> + <exclude>**/domain/exception/**</exclude> + <exclude>**/domain/data/**</exclude> + <exclude>**/infrastructure/client/**</exclude> + <exclude>**/infrastructure/configuration/**</exclude> + <exclude>**/infrastructure/config/**</exclude> + <exclude>**/infrastructure/jpa/entity/**</exclude> + <exclude>**/infrastructure/mapper/**</exclude> + <exclude>**/infrastructure/restapi/dto/**</exclude> + <exclude>**/infrastructure/controller/**</exclude> + <exclude>**/infrastructure/repository/**</exclude> + <exclude>**/infrastructure/kafkalistener/**</exclude> + </excludes> + </configuration> + </execution> + </executions> + </plugin> + </plugins> + + </build> +</project> -- GitLab