Skip to content

Commit

Permalink
Feature/auto france identite num (#707)
Browse files Browse the repository at this point in the history
* feat(process-file): Add France Identité Numerique API project

* chore: 🤖 Add api france identite project and readme to deploy

* chore: 🤖 add default value for france_identite_api_url
  • Loading branch information
mattboll authored Feb 16, 2024
1 parent 87bc2e5 commit d87a4e9
Show file tree
Hide file tree
Showing 15 changed files with 243 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ public enum DocumentRule {

R_RENT_RECEIPT_NAME(Level.CRITICAL, "Nom/prénoms ne correspondent pas"),
R_RENT_RECEIPT_MONTHS(Level.CRITICAL, "Les trois dernières quittances doivent être fournies (la plus récente doit être au pire M-2)"),
R_RENT_RECEIPT_ADDRESS_SALARY(Level.WARN, "TODO. L'adresse de la location semble ne pas correspondre à l'adresse des bulletins de payes");
R_RENT_RECEIPT_ADDRESS_SALARY(Level.WARN, "TODO. L'adresse de la location semble ne pas correspondre à l'adresse des bulletins de payes"),

R_FRANCE_IDENTITE_NAMES(Level.CRITICAL, "Les noms et prénoms ne correspondent pas"),
R_FRANCE_IDENTITE_STATUS(Level.CRITICAL, "Ce document n'a pas pu être validé par France Identité");

public enum Level {
CRITICAL, WARN
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package fr.dossierfacile.common.entity;

import fr.dossierfacile.common.entity.ocr.ParsedFile;
import fr.dossierfacile.common.enums.ParsedFileClassification;
import fr.dossierfacile.common.enums.ParsedStatus;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;

@Data
@Builder
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class FranceIdentiteApiResult implements ParsedFile {
@Builder.Default
ParsedFileClassification classification = ParsedFileClassification.FRANCE_IDENTITE;
ParsedStatus parsedStatus;
String status;
String familyName;
String givenName;
String birthDate;
String birthPlace;
String gender;
String validityDate;
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
package fr.dossierfacile.common.enums;

import fr.dossierfacile.common.entity.ocr.*;
import fr.dossierfacile.common.entity.FranceIdentiteApiResult;
import fr.dossierfacile.common.entity.ocr.GuaranteeProviderFile;
import fr.dossierfacile.common.entity.ocr.ParsedFile;
import fr.dossierfacile.common.entity.ocr.PayslipFile;
import fr.dossierfacile.common.entity.ocr.RentalReceiptFile;
import fr.dossierfacile.common.entity.ocr.TaxIncomeLeaf;
import fr.dossierfacile.common.entity.ocr.TaxIncomeMainFile;
import lombok.AllArgsConstructor;
import lombok.Getter;

Expand All @@ -12,7 +18,8 @@ public enum ParsedFileClassification {
GUARANTEE_PROVIDER(GuaranteeProviderFile.class),
PUBLIC_PAYSLIP(PayslipFile.class),
PAYSLIP(PayslipFile.class),
RENTAL_RECEIPT(RentalReceiptFile.class);
RENTAL_RECEIPT(RentalReceiptFile.class),
FRANCE_IDENTITE(FranceIdentiteApiResult.class);

Class<? extends ParsedFile> classificationClass;
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,9 @@
import fr.dossierfacile.common.entity.ParsedFileAnalysis;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;
import java.util.Optional;

public interface ParsedFileAnalysisRepository extends JpaRepository<ParsedFileAnalysis, Long> {
List<ParsedFileAnalysis> findByFileId(Long fileId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,6 @@ public void processFile(Long fileId) {
fileRepository.findById(fileId)
.map(barCodeFileProcessor::process)
.map(fileParserProcessor::process);

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public class StorageFileLoaderService {
public File getTemporaryFilePath(StorageFile storageFile) {
try {
try (InputStream in = fileStorageService.download(storageFile)) {
Path temporaryFile = Files.createTempFile("tax-" + storageFile.getId()
Path temporaryFile = Files.createTempFile("temp-" + storageFile.getId()
+ "-" + UUID.randomUUID(),
MediaType.APPLICATION_PDF_VALUE.equalsIgnoreCase(storageFile.getContentType()) ? ".pdf" : "");
// actually we only need first file
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package fr.dossierfacile.process.file.service.documentrules;

import fr.dossierfacile.common.entity.Document;
import fr.dossierfacile.common.entity.DocumentAnalysisReport;
import fr.dossierfacile.common.entity.DocumentAnalysisStatus;
import fr.dossierfacile.common.entity.DocumentBrokenRule;
import fr.dossierfacile.common.entity.DocumentRule;
import fr.dossierfacile.common.entity.File;
import fr.dossierfacile.common.entity.FranceIdentiteApiResult;
import fr.dossierfacile.common.entity.ParsedFileAnalysis;
import fr.dossierfacile.common.entity.Person;
import fr.dossierfacile.common.enums.ParsedFileAnalysisStatus;
import fr.dossierfacile.common.enums.ParsedFileClassification;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.util.LinkedList;
import java.util.List;
import java.util.Optional;

import static fr.dossierfacile.common.enums.DocumentSubCategory.FRANCE_IDENTITE;
import static fr.dossierfacile.process.file.util.NameUtil.normalizeName;

@Service
@RequiredArgsConstructor
@Slf4j
public class FranceIdentiteNumeriqueRulesValidationService implements RulesValidationService {

@Override
public boolean shouldBeApplied(Document document) {
return document.getDocumentSubCategory() == FRANCE_IDENTITE;
}

@Override
public DocumentAnalysisReport process(Document document, DocumentAnalysisReport report) {
List<DocumentBrokenRule> brokenRules = Optional.ofNullable(report.getBrokenRules())
.orElseGet(() -> {
report.setBrokenRules(new LinkedList<>());
return report.getBrokenRules();
});
for (File dfFile : document.getFiles()) {
ParsedFileAnalysis analysis = dfFile.getParsedFileAnalysis();
if (analysis == null || analysis.getAnalysisStatus() == ParsedFileAnalysisStatus.FAILED) {
continue;
}
if (analysis.getClassification() == ParsedFileClassification.FRANCE_IDENTITE) {
FranceIdentiteApiResult parsedDocument = (FranceIdentiteApiResult) analysis.getParsedFile();

// Parse Rule
if (parsedDocument == null
|| parsedDocument.getStatus() == null
|| parsedDocument.getFamilyName() == null
|| parsedDocument.getGivenName() == null
|| parsedDocument.getValidityDate() == null) {
brokenRules.add(DocumentBrokenRule.builder()
.rule(DocumentRule.R_FRANCE_IDENTITE_STATUS)
.message(DocumentRule.R_FRANCE_IDENTITE_STATUS.getDefaultMessage())
.build());
continue;
}

// Fake Rule
if (!("VALID".equals(parsedDocument.getStatus()))) {
brokenRules.add(DocumentBrokenRule.builder()
.rule(DocumentRule.R_FRANCE_IDENTITE_STATUS)
.message(DocumentRule.R_FRANCE_IDENTITE_STATUS.getDefaultMessage())
.build());
continue;
}

// TODO : check that France Identité verifies that names on pdf matches qrcode
Person documentOwner = Optional.ofNullable((Person) document.getTenant()).orElseGet(document::getGuarantor);
String firstName = documentOwner.getFirstName();
String lastName = document.getName();
if (!(normalizeName(parsedDocument.getGivenName()).contains(normalizeName(firstName))
&& (normalizeName(parsedDocument.getFamilyName()).contains(normalizeName(lastName)))
)) {
log.error("Le nom/prenom ne correpond pas à l'utilisateur tenantId:" + document.getTenant().getId() + " firstname: " + firstName);
brokenRules.add(DocumentBrokenRule.builder()
.rule(DocumentRule.R_FRANCE_IDENTITE_NAMES)
.message(DocumentRule.R_FRANCE_IDENTITE_NAMES.getDefaultMessage())
.build());
}
}
}
if (brokenRules.isEmpty()) {
report.setAnalysisStatus(DocumentAnalysisStatus.CHECKED);
} else if (brokenRules.stream().anyMatch(r -> r.getRule().getLevel() == DocumentRule.Level.CRITICAL)) {
report.setAnalysisStatus(DocumentAnalysisStatus.DENIED);
} else {
report.setAnalysisStatus(DocumentAnalysisStatus.UNDEFINED);
}
return report;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package fr.dossierfacile.process.file.service.parsers;

import fr.dossierfacile.common.entity.FranceIdentiteApiResult;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.MultipartBodyBuilder;
import org.springframework.stereotype.Service;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate;

import java.io.File;

import static fr.dossierfacile.common.enums.DocumentSubCategory.FRANCE_IDENTITE;

@Service
@Slf4j
@RequiredArgsConstructor
public class FranceIdentiteParser implements FileParser<FranceIdentiteApiResult> {
private final RestTemplate restTemplate;
private static final String CALL_BACK_RESPONSE = "France Identité callback responseStatus: {}";

@Value("${france.identite.api.url:https://dossierfacile-france-identite-numerique-api.osc-secnum-fr1.scalingo.io/api/validation/v1/check-doc-valid}")
private String urlCallback;

@Override
public FranceIdentiteApiResult parse(File file) {
ResponseEntity<FranceIdentiteApiResult> response;
try {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.MULTIPART_FORM_DATA);

Resource resource = new FileSystemResource(file.getPath());
MultipartBodyBuilder multipartBodyBuilder = new MultipartBodyBuilder();
multipartBodyBuilder.part("file", resource, MediaType.APPLICATION_PDF);

HttpEntity<MultiValueMap<String, HttpEntity<?>>> request = new HttpEntity<>(multipartBodyBuilder.build(), headers);
response = restTemplate.exchange(urlCallback, HttpMethod.POST, request, FranceIdentiteApiResult.class);
log.info(CALL_BACK_RESPONSE, response.getStatusCode());
} catch (RestClientException e) {
log.error("Unable to parse");
throw new RuntimeException(e);
}
if ( HttpStatus.OK != response.getStatusCode() && HttpStatus.ACCEPTED != response.getStatusCode()) {
log.error("Failure on France Identité check:" + urlCallback + "- Status:" + response.getStatusCode());
}
return response.getBody();
}


@Override
public boolean shouldTryToApply(fr.dossierfacile.common.entity.File file) {
return file.getDocument().getDocumentSubCategory() == FRANCE_IDENTITE
&& MediaType.APPLICATION_PDF_VALUE.equalsIgnoreCase(file.getStorageFile().getContentType());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package fr.dossierfacile.process.file.util;

import java.text.Normalizer;

public class NameUtil {
public static String normalizeName(String name) {
if (name == null)
return null;
String normalized = Normalizer.normalize(name, Normalizer.Form.NFD);
return normalized.replace('-', ' ')
.replaceAll("[\\p{InCombiningDiacriticalMarks}]", "").toUpperCase();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,6 @@ logging.logstash.destination=
application.name=process-file
environment=

ants.tsl.uri=https://ants.gouv.fr/files/25362bbf-a54e-4ed9-b98a-71e2382b54e0/tsl_signed.xml
ants.tsl.uri=https://ants.gouv.fr/files/25362bbf-a54e-4ed9-b98a-71e2382b54e0/tsl_signed.xml

france.identite.api.url=https://dossierfacile-france-identite-numerique-api.osc-secnum-fr1.scalingo.io/api/validation/v1/check-doc-valid
1 change: 1 addition & 0 deletions france-identite-numerique-api/Procfile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
web: java $JAVA_OPTS -jar attest-val-partenaires-api-1.0.3.jar
15 changes: 15 additions & 0 deletions france-identite-numerique-api/README
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
* Comment déployer le projet

Se mettre à la racine du projet et faire un tar.gz du projet :

```
tar czvf france-identite-numerique-api.tar.gz france-identite-numerique-api
```

Puis déployer directement sur scalingo :

```
scalingo --app nom-app deploy ./france-identite-numerique-api.tar.gz
```

Pour le moment le code source de france-identite-numerique, même s'il est censé être libre et même si nous y avons accès, n'a pas de license et donc n'est pas partageable sur un dépôt publique.
Binary file not shown.
1 change: 1 addition & 0 deletions france-identite-numerique-api/system.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
java.runtime.version=11
1 change: 1 addition & 0 deletions system.properties
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
java.runtime.version=21
maven.version=3.9.5

0 comments on commit d87a4e9

Please sign in to comment.