Skip to content

Commit 87bc2e5

Browse files
Fabienfabiengo
authored andcommitted
feat: add payslip parser and rules validation
1 parent 3d7eeef commit 87bc2e5

File tree

16 files changed

+442
-164
lines changed

16 files changed

+442
-164
lines changed
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,13 @@
1212
@ToString
1313
@NoArgsConstructor
1414
@AllArgsConstructor
15-
public class PublicPayslipFile implements ParsedFile {
15+
public class PayslipFile implements ParsedFile {
1616
@Builder.Default
17-
ParsedFileClassification classification = ParsedFileClassification.PUBLIC_PAYSLIP;
17+
ParsedFileClassification classification = ParsedFileClassification.PAYSLIP;
1818
ParsedStatus status;
1919
String fullname;
2020
@JsonFormat(pattern = "yyyy-MM")
2121
YearMonth month;
22-
double netTaxableIncome;
23-
double cumulativeNetTaxableIncome;
22+
Double netTaxableIncome;
23+
Double cumulativeNetTaxableIncome;
2424
}

dossierfacile-common-library/src/main/java/fr/dossierfacile/common/enums/ParsedFileClassification.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ public enum ParsedFileClassification {
1010
TAX_INCOME(TaxIncomeMainFile.class),
1111
TAX_INCOME_LEAF(TaxIncomeLeaf.class),
1212
GUARANTEE_PROVIDER(GuaranteeProviderFile.class),
13-
PUBLIC_PAYSLIP(PublicPayslipFile.class),
13+
PUBLIC_PAYSLIP(PayslipFile.class),
14+
PAYSLIP(PayslipFile.class),
1415
RENTAL_RECEIPT(RentalReceiptFile.class);
1516

1617
Class<? extends ParsedFile> classificationClass;
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
package fr.dossierfacile.process.file.service.documentrules;
2+
3+
import fr.dossierfacile.common.entity.*;
4+
import fr.dossierfacile.common.entity.ocr.PayslipFile;
5+
import fr.dossierfacile.common.enums.DocumentCategory;
6+
import fr.dossierfacile.common.enums.DocumentSubCategory;
7+
import fr.dossierfacile.common.enums.ParsedFileClassification;
8+
import fr.dossierfacile.process.file.util.PersonNameComparator;
9+
import lombok.RequiredArgsConstructor;
10+
import lombok.extern.slf4j.Slf4j;
11+
import org.springframework.stereotype.Service;
12+
import org.springframework.util.CollectionUtils;
13+
14+
import java.time.LocalDate;
15+
import java.time.YearMonth;
16+
import java.util.Comparator;
17+
import java.util.List;
18+
import java.util.Optional;
19+
import java.util.stream.Collectors;
20+
21+
@Service
22+
@Slf4j
23+
public abstract class AbstractPayslipRulesValidationService implements RulesValidationService {
24+
protected abstract ParsedFileClassification getPayslipClassification();
25+
26+
protected boolean checkQRCode(Document document) {
27+
return true;
28+
}
29+
30+
@Override
31+
public boolean shouldBeApplied(Document document) {
32+
return document.getDocumentCategory() == DocumentCategory.FINANCIAL
33+
&& document.getDocumentSubCategory() == DocumentSubCategory.SALARY
34+
&& !CollectionUtils.isEmpty(document.getFiles())
35+
&& document.getFiles().stream().anyMatch((f) -> f.getParsedFileAnalysis() != null
36+
&& f.getParsedFileAnalysis().getParsedFile() != null
37+
&& f.getParsedFileAnalysis().getParsedFile().getClassification() == getPayslipClassification());
38+
}
39+
40+
protected boolean checkNamesRule(Document document) {
41+
Person documentOwner = Optional.ofNullable((Person) document.getTenant()).orElseGet(() -> document.getGuarantor());
42+
for (File dfFile : document.getFiles()) {
43+
ParsedFileAnalysis analysis = dfFile.getParsedFileAnalysis();
44+
PayslipFile parsedFile = (PayslipFile) analysis.getParsedFile();
45+
46+
String fullName = parsedFile.getFullname().toUpperCase().replaceFirst("^(M. |MR |MME |MLLE |MONSIEUR |MADAME |MADEMOISELLE )", "");
47+
48+
if (!PersonNameComparator.bearlyEqualsTo(fullName, documentOwner.getLastName(), documentOwner.getFirstName())
49+
&& !PersonNameComparator.bearlyEqualsTo(fullName, documentOwner.getPreferredName(), documentOwner.getFirstName())) {
50+
return false;
51+
}
52+
}
53+
return true;
54+
}
55+
56+
private List<List<YearMonth>> getExpectedMonthsLists() {
57+
LocalDate localDate = LocalDate.now();
58+
YearMonth yearMonth = YearMonth.now();
59+
return (localDate.getDayOfMonth() <= 15) ?
60+
List.of(
61+
List.of(yearMonth.minusMonths(1), yearMonth.minusMonths(2), yearMonth.minusMonths(3)),
62+
List.of(yearMonth.minusMonths(2), yearMonth.minusMonths(3), yearMonth.minusMonths(4))) :
63+
List.of(
64+
List.of(yearMonth, yearMonth.minusMonths(1), yearMonth.minusMonths(2)),
65+
List.of(yearMonth.minusMonths(1), yearMonth.minusMonths(2), yearMonth.minusMonths(3)));
66+
}
67+
68+
private boolean checkMonthsValidityRule(Document document) {
69+
List<List<YearMonth>> expectedMonthsList = getExpectedMonthsLists();
70+
71+
List<YearMonth> presentMonths = document.getFiles().stream()
72+
.map(file -> ((PayslipFile) file.getParsedFileAnalysis().getParsedFile()).getMonth())
73+
.toList();
74+
75+
return expectedMonthsList.stream().anyMatch(
76+
expectedMonths -> expectedMonths.stream().allMatch(month -> presentMonths.contains(month))
77+
);
78+
}
79+
80+
private boolean checkAmountValidityRule(Document document) {
81+
List<PayslipFile> recentFiles = document.getFiles().stream()
82+
.map(file -> (PayslipFile) file.getParsedFileAnalysis().getParsedFile())
83+
.sorted(Comparator.comparing(PayslipFile::getMonth).reversed())
84+
.limit(3)
85+
.collect(Collectors.toList());
86+
87+
double monthlyAverage = recentFiles.stream()
88+
.mapToDouble(PayslipFile::getNetTaxableIncome)
89+
.sum() / recentFiles.size();
90+
91+
// Check percentage difference
92+
double diffPercentage = Math.abs((monthlyAverage - document.getMonthlySum()) / document.getMonthlySum());
93+
return (diffPercentage <= 0.2);
94+
}
95+
96+
@Override
97+
public DocumentAnalysisReport process(Document document, DocumentAnalysisReport report) {
98+
99+
try {
100+
if (CollectionUtils.isEmpty(document.getFiles()) || document.getFiles().stream()
101+
.anyMatch(f -> f.getParsedFileAnalysis() == null || f.getParsedFileAnalysis().getParsedFile() == null)
102+
) {
103+
report.setAnalysisStatus(DocumentAnalysisStatus.UNDEFINED);
104+
return report;
105+
}
106+
107+
if (!checkQRCode(document)) {
108+
log.error("Document mismatch to QR CODE :" + document.getId());
109+
report.getBrokenRules().add(DocumentBrokenRule.builder()
110+
.rule(DocumentRule.R_PAYSLIP_QRCHECK)
111+
.message(DocumentRule.R_PAYSLIP_QRCHECK.getDefaultMessage())
112+
.build());
113+
report.setAnalysisStatus(DocumentAnalysisStatus.DENIED);
114+
} else if (!checkNamesRule(document)) {
115+
log.error("Document names mismatches :" + document.getId());
116+
report.getBrokenRules().add(DocumentBrokenRule.builder()
117+
.rule(DocumentRule.R_PAYSLIP_NAME)
118+
.message(DocumentRule.R_PAYSLIP_NAME.getDefaultMessage())
119+
.build());
120+
report.setAnalysisStatus(DocumentAnalysisStatus.DENIED);
121+
} else if (!checkMonthsValidityRule(document)) {
122+
log.error("Document is expired :" + document.getId());
123+
report.getBrokenRules().add(DocumentBrokenRule.builder()
124+
.rule(DocumentRule.R_PAYSLIP_MONTHS)
125+
.message(DocumentRule.R_PAYSLIP_MONTHS.getDefaultMessage())
126+
.build());
127+
report.setAnalysisStatus(DocumentAnalysisStatus.DENIED);
128+
} else if (!checkAmountValidityRule(document)) {
129+
log.error("Amount specified on document mismatch :" + document.getId());
130+
report.getBrokenRules().add(DocumentBrokenRule.builder()
131+
.rule(DocumentRule.R_PAYSLIP_AMOUNT_MISMATCHES)
132+
.message(DocumentRule.R_PAYSLIP_AMOUNT_MISMATCHES.getDefaultMessage())
133+
.build());
134+
report.setAnalysisStatus(DocumentAnalysisStatus.DENIED);
135+
} else {
136+
report.setAnalysisStatus(DocumentAnalysisStatus.CHECKED);
137+
}
138+
139+
} catch (Exception e) {
140+
log.error("Error during the rules validation execution pocess", e);
141+
report.setAnalysisStatus(DocumentAnalysisStatus.UNDEFINED);
142+
}
143+
return report;
144+
}
145+
146+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package fr.dossierfacile.process.file.service.documentrules;
2+
3+
import fr.dossierfacile.common.enums.ParsedFileClassification;
4+
import lombok.RequiredArgsConstructor;
5+
import lombok.extern.slf4j.Slf4j;
6+
import org.springframework.stereotype.Service;
7+
8+
@Service
9+
@RequiredArgsConstructor
10+
@Slf4j
11+
public class PayslipStandardRulesValidationService extends AbstractPayslipRulesValidationService implements RulesValidationService {
12+
13+
@Override
14+
protected ParsedFileClassification getPayslipClassification() {
15+
return ParsedFileClassification.PAYSLIP;
16+
}
17+
18+
}
Lines changed: 18 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
package fr.dossierfacile.process.file.service.documentrules;
22

3-
import fr.dossierfacile.common.entity.*;
4-
import fr.dossierfacile.common.entity.ocr.PublicPayslipFile;
5-
import fr.dossierfacile.common.enums.DocumentCategory;
6-
import fr.dossierfacile.common.enums.DocumentSubCategory;
3+
import fr.dossierfacile.common.entity.BarCodeFileAnalysis;
4+
import fr.dossierfacile.common.entity.Document;
5+
import fr.dossierfacile.common.entity.File;
6+
import fr.dossierfacile.common.entity.ParsedFileAnalysis;
7+
import fr.dossierfacile.common.entity.ocr.PayslipFile;
78
import fr.dossierfacile.common.enums.ParsedFileAnalysisStatus;
89
import fr.dossierfacile.common.enums.ParsedFileClassification;
910
import fr.dossierfacile.process.file.barcode.twoddoc.parsing.TwoDDocDataType;
@@ -12,168 +13,54 @@
1213
import lombok.RequiredArgsConstructor;
1314
import lombok.extern.slf4j.Slf4j;
1415
import org.springframework.stereotype.Service;
15-
import org.springframework.util.CollectionUtils;
1616

17-
import java.time.LocalDate;
1817
import java.time.YearMonth;
19-
import java.util.Comparator;
20-
import java.util.List;
2118
import java.util.Map;
22-
import java.util.Optional;
23-
import java.util.stream.Collectors;
2419

2520
@Service
2621
@RequiredArgsConstructor
2722
@Slf4j
28-
public class PublicPayslipRulesValidationService implements RulesValidationService {
29-
@Override
30-
public boolean shouldBeApplied(Document document) {
31-
return document.getDocumentCategory() == DocumentCategory.FINANCIAL
32-
&& document.getDocumentSubCategory() == DocumentSubCategory.SALARY
33-
&& !CollectionUtils.isEmpty(document.getFiles())
34-
&& document.getFiles().stream().anyMatch((f) -> f.getParsedFileAnalysis() != null
35-
&& f.getParsedFileAnalysis().getParsedFile() != null
36-
&& f.getParsedFileAnalysis().getParsedFile().getClassification() == ParsedFileClassification.PUBLIC_PAYSLIP);
37-
}
38-
39-
private PublicPayslipFile fromQR(BarCodeFileAnalysis barCodeFileAnalysis) {
23+
public class PublicPayslipRulesValidationService extends AbstractPayslipRulesValidationService implements RulesValidationService {
24+
private PayslipFile fromQR(BarCodeFileAnalysis barCodeFileAnalysis) {
4025
Map<String, String> dataWithLabel = (Map<String, String>) barCodeFileAnalysis.getVerifiedData();
41-
return PublicPayslipFile.builder()
26+
return PayslipFile.builder()
27+
.classification(ParsedFileClassification.PUBLIC_PAYSLIP)
4228
.fullname(dataWithLabel.get(TwoDDocDataType.ID_10.getLabel()))
4329
.month(YearMonth.from(TwoDDocUtil.getLocalDateFrom2DDocHexDate(dataWithLabel.get(TwoDDocDataType.ID_54.getLabel()))))
4430
.netTaxableIncome(Double.parseDouble(dataWithLabel.get(TwoDDocDataType.ID_58.getLabel()).replace(" ", "").replace(',', '.')))
4531
.cumulativeNetTaxableIncome(Double.parseDouble(dataWithLabel.get(TwoDDocDataType.ID_59.getLabel()).replace(" ", "").replace(',', '.')))
4632
.build();
4733
}
4834

49-
private boolean checkQRCode(Document document) {
35+
@Override
36+
protected boolean checkQRCode(Document document) {
5037
for (File dfFile : document.getFiles()) {
5138
ParsedFileAnalysis analysis = dfFile.getParsedFileAnalysis();
5239
if (analysis == null || dfFile.getFileAnalysis() == null || analysis.getAnalysisStatus() == ParsedFileAnalysisStatus.FAILED) {
5340
continue;
5441
}
5542
if (analysis.getClassification() == ParsedFileClassification.PUBLIC_PAYSLIP) {
56-
PublicPayslipFile qrDocument = fromQR(dfFile.getFileAnalysis());
57-
PublicPayslipFile parsedDocument = (PublicPayslipFile) analysis.getParsedFile();
43+
PayslipFile qrDocument = fromQR(dfFile.getFileAnalysis());
44+
PayslipFile parsedDocument = (PayslipFile) analysis.getParsedFile();
5845

5946
if (qrDocument == null
6047
|| qrDocument.getFullname() == null
6148
|| qrDocument.getMonth() == null
6249
|| qrDocument.getCumulativeNetTaxableIncome() == 0
6350
|| !PersonNameComparator.equalsWithNormalization(qrDocument.getFullname(), parsedDocument.getFullname())
6451
|| !qrDocument.getMonth().equals(parsedDocument.getMonth())
65-
|| qrDocument.getNetTaxableIncome() != parsedDocument.getNetTaxableIncome()
66-
|| qrDocument.getCumulativeNetTaxableIncome() != parsedDocument.getCumulativeNetTaxableIncome()) {
52+
|| Math.abs(qrDocument.getNetTaxableIncome() - parsedDocument.getNetTaxableIncome()) > 1
53+
|| Math.abs(qrDocument.getCumulativeNetTaxableIncome() - parsedDocument.getCumulativeNetTaxableIncome()) > 1) {
6754
return false;
6855
}
6956
}
7057
}
7158
return true;
7259
}
7360

74-
private boolean checkNamesRule(Document document) {
75-
Person documentOwner = Optional.ofNullable((Person) document.getTenant()).orElseGet(() -> document.getGuarantor());
76-
for (File dfFile : document.getFiles()) {
77-
ParsedFileAnalysis analysis = dfFile.getParsedFileAnalysis();
78-
PublicPayslipFile parsedFile = (PublicPayslipFile) analysis.getParsedFile();
79-
80-
String fullname = parsedFile.getFullname().toUpperCase().replaceFirst("^(MR |MME |MLLE )", "");
81-
82-
if (!PersonNameComparator.bearlyEqualsTo(fullname, documentOwner.getLastName(), documentOwner.getFirstName())
83-
&& !PersonNameComparator.bearlyEqualsTo(fullname, documentOwner.getPreferredName(), documentOwner.getFirstName())) {
84-
return false;
85-
}
86-
}
87-
return true;
88-
}
89-
90-
private List<List<YearMonth>> getExpectedMonthsLists() {
91-
LocalDate localDate = LocalDate.now();
92-
YearMonth yearMonth = YearMonth.now();
93-
return (localDate.getDayOfMonth() <= 15) ?
94-
List.of(
95-
List.of(yearMonth.minusMonths(1), yearMonth.minusMonths(2), yearMonth.minusMonths(3)),
96-
List.of(yearMonth.minusMonths(2), yearMonth.minusMonths(3), yearMonth.minusMonths(4))) :
97-
List.of(
98-
List.of(yearMonth, yearMonth.minusMonths(1), yearMonth.minusMonths(2)),
99-
List.of(yearMonth.minusMonths(1), yearMonth.minusMonths(2), yearMonth.minusMonths(3)));
100-
}
101-
102-
private boolean checkMonthsValidityRule(Document document) {
103-
List<List<YearMonth>> expectedMonthsList = getExpectedMonthsLists();
104-
105-
List<YearMonth> presentMonths = document.getFiles().stream()
106-
.map(file -> ((PublicPayslipFile) file.getParsedFileAnalysis().getParsedFile()).getMonth())
107-
.toList();
108-
109-
return expectedMonthsList.stream().anyMatch(
110-
expectedMonths -> expectedMonths.stream().allMatch(month -> presentMonths.contains(month))
111-
);
112-
}
113-
114-
private boolean checkAmountValidityRule(Document document) {
115-
List<PublicPayslipFile> recentFiles = document.getFiles().stream()
116-
.map(file -> (PublicPayslipFile) file.getParsedFileAnalysis().getParsedFile())
117-
.sorted(Comparator.comparing(PublicPayslipFile::getMonth).reversed())
118-
.limit(3)
119-
.collect(Collectors.toList());
120-
121-
double monthlyAverage = recentFiles.stream()
122-
.mapToDouble(PublicPayslipFile::getNetTaxableIncome)
123-
.sum() / recentFiles.size();
124-
125-
// Check percentage difference
126-
double diffPercentage = Math.abs((monthlyAverage - document.getMonthlySum()) / document.getMonthlySum());
127-
return (diffPercentage <= 0.2);
128-
}
129-
13061
@Override
131-
public DocumentAnalysisReport process(Document document, DocumentAnalysisReport report) {
132-
133-
try {
134-
if (CollectionUtils.isEmpty(document.getFiles()) || document.getFiles().stream()
135-
.anyMatch(f -> f.getParsedFileAnalysis() == null || f.getParsedFileAnalysis().getParsedFile() == null)
136-
) {
137-
report.setAnalysisStatus(DocumentAnalysisStatus.UNDEFINED);
138-
return report;
139-
}
140-
141-
if (!checkQRCode(document)) {
142-
log.error("Document mismatch to QR CODE :" + document.getId());
143-
report.getBrokenRules().add(DocumentBrokenRule.builder()
144-
.rule(DocumentRule.R_PAYSLIP_QRCHECK)
145-
.message(DocumentRule.R_PAYSLIP_QRCHECK.getDefaultMessage())
146-
.build());
147-
report.setAnalysisStatus(DocumentAnalysisStatus.DENIED);
148-
} else if (!checkNamesRule(document)) {
149-
log.error("Document names mismatches :" + document.getId());
150-
report.getBrokenRules().add(DocumentBrokenRule.builder()
151-
.rule(DocumentRule.R_PAYSLIP_NAME)
152-
.message(DocumentRule.R_PAYSLIP_NAME.getDefaultMessage())
153-
.build());
154-
report.setAnalysisStatus(DocumentAnalysisStatus.DENIED);
155-
} else if (!checkMonthsValidityRule(document)) {
156-
log.error("Document is expired :" + document.getId());
157-
report.getBrokenRules().add(DocumentBrokenRule.builder()
158-
.rule(DocumentRule.R_PAYSLIP_MONTHS)
159-
.message(DocumentRule.R_PAYSLIP_MONTHS.getDefaultMessage())
160-
.build());
161-
report.setAnalysisStatus(DocumentAnalysisStatus.DENIED);
162-
} else if (!checkAmountValidityRule(document)) {
163-
log.error("Amount specified on document mismatch :" + document.getId());
164-
report.getBrokenRules().add(DocumentBrokenRule.builder()
165-
.rule(DocumentRule.R_PAYSLIP_AMOUNT_MISMATCHES)
166-
.message(DocumentRule.R_PAYSLIP_AMOUNT_MISMATCHES.getDefaultMessage())
167-
.build());
168-
report.setAnalysisStatus(DocumentAnalysisStatus.DENIED);
169-
} else {
170-
report.setAnalysisStatus(DocumentAnalysisStatus.CHECKED);
171-
}
172-
173-
} catch (Exception e) {
174-
log.error("Error during the rules validation execution pocess", e);
175-
report.setAnalysisStatus(DocumentAnalysisStatus.UNDEFINED);
176-
}
177-
return report;
62+
protected ParsedFileClassification getPayslipClassification() {
63+
return ParsedFileClassification.PUBLIC_PAYSLIP;
17864
}
65+
17966
}

0 commit comments

Comments
 (0)