Compare commits

..

55 commits

Author SHA1 Message Date
ialbert
4cfdd75dd9 Merge remote-tracking branch 'origin/dev' into dev 2025-11-18 12:17:53 +03:00
ialbert
28ec19775e handler concurrency & logs 2025-11-18 12:17:38 +03:00
etreschenkov
3ecb5282cb add new tables 2025-11-17 16:44:58 +03:00
etreschenkov
8ce1948b07 add batch size as parameter 2025-11-13 13:37:24 +03:00
ialbert
945694e50d pipeline bugfixes 2025-11-11 16:59:18 +03:00
etreschenkov
395613ae01 flag calc 2025-11-11 16:47:58 +03:00
etreschenkov
53a2ab77a9 flag calc 2025-11-11 14:36:46 +03:00
etreschenkov
aaf91195b1 flag calc 2025-11-11 14:23:12 +03:00
ialbert
bf693c3fbc pipeline blocking bugfix 2025-11-11 14:02:14 +03:00
ialbert
b487fc832f Merge remote-tracking branch 'origin/dev' into dev 2025-11-11 13:37:14 +03:00
ialbert
a67cc4056a . 2025-11-11 13:37:00 +03:00
etreschenkov
6da5991483 flag calc 2025-11-11 13:11:13 +03:00
etreschenkov
0f04ab3e42 flag calc 2025-11-11 13:01:00 +03:00
etreschenkov
4e6ab1a862 flag calc 2025-11-11 12:52:01 +03:00
etreschenkov
7176a49814 flag calc 2025-11-11 12:23:52 +03:00
etreschenkov
881c31becd flag calc 2025-11-11 09:43:38 +03:00
etreschenkov
83a452fed6 flag calc 2025-11-11 09:24:08 +03:00
etreschenkov
00662f9685 flag calc 2025-11-10 19:17:29 +03:00
etreschenkov
f10540e80a implement terminator 2025-11-10 16:51:03 +03:00
ialbert
525de91285 interrupt 2025-11-10 16:37:30 +03:00
etreschenkov
786004e3c8 attempt to remove unused classes 2025-11-10 11:32:12 +03:00
etreschenkov
74084f93c8 attempt to remove unused classes 2025-11-10 00:17:46 +03:00
ialbert
9813fe4053 pipeline futures 2025-11-05 13:58:28 +03:00
ialbert
d24cd03656 pipeline builder refactor 2025-11-01 15:18:58 +03:00
ialbert
8e53f24aeb added pipeline builder
added queue size
commented out merging
stage bugfix
2025-11-01 13:58:10 +03:00
etreschenkov
9ce437a438 FidIdEnricher: rename and chane payload type 2025-11-01 12:30:43 +03:00
etreschenkov
9cb5a16490 some refactoring :] 2025-10-31 13:25:26 +03:00
etreschenkov
f2df1c0183 some refactoring :] 2025-10-30 18:57:03 +03:00
etreschenkov
5d3507e12b some refactoring :/ 2025-10-30 15:04:58 +03:00
ialbert
999ac42f48 find packages method; global exceptions 2025-10-29 17:44:09 +03:00
etreschenkov
e7d5ff82f4 remove useless classes 2025-10-29 14:18:47 +03:00
etreschenkov
bb7606125a ./ 2025-10-16 17:42:03 +03:00
Mikhail Trofimov
76489609c7 Fix 2025-10-16 17:19:04 +03:00
Mikhail Trofimov
386ea4c18e Fix after merging master 2025-10-16 16:45:23 +03:00
Mikhail Trofimov
438956f8b5 Merge branch 'master' into dev
# Conflicts:
#	pom.xml
#	src/main/java/ru/nbch/credit_tracker/entities/dto/PhoneFidMap.java
#	src/main/java/ru/nbch/credit_tracker/mapper/signal/PhoneFidMapMapper.java
#	src/main/java/ru/nbch/credit_tracker/service/SignalService.java
#	src/main/java/ru/nbch/credit_tracker/service/signal/PhoneFidMapService.java
2025-10-16 16:33:01 +03:00
Mikhail Trofimov
1680abbec9 Fix fids finding 2025-10-16 12:28:23 +03:00
Mikhail Trofimov
7fe066c42d Minor fix: put phoneFid to map not only if name found 2025-10-15 18:03:01 +03:00
Mikhail Trofimov
bc34f3f89c Added FlagsEnricher; Added id to SubjectProfile to use it as uid in PersonFactoryBuilderV2 2025-10-12 20:43:25 +03:00
Mikhail Trofimov
e0d96d9246 Minor fix 2025-10-09 16:30:07 +03:00
Mikhail Trofimov
fc830f8eae Fix after merging master into dev 2025-10-09 15:55:02 +03:00
Mikhail Trofimov
c013b616c7 Merge branch 'master' into dev
# Conflicts:
#	credit-tracker.properties
#	pom.xml
#	src/main/java/ru/nbch/credit_tracker/config/datasource/DatabaseConfig.java
#	src/main/java/ru/nbch/credit_tracker/config/datasource/IndicDataSourceConfig.java
#	src/main/java/ru/nbch/credit_tracker/config/datasource/ScoringDataSourceConfig.java
#	src/main/java/ru/nbch/credit_tracker/config/datasource/SignalDataSourceConfig.java
#	src/main/java/ru/nbch/credit_tracker/config/rest/GlobalExceptionHandler.java
#	src/main/java/ru/nbch/credit_tracker/config/rest/filters/HttpLoggingFilter.java
#	src/main/java/ru/nbch/credit_tracker/facade/SignalFacade.java
#	src/main/java/ru/nbch/credit_tracker/utils/ValidationService.java
#	src/main/java/ru/nbch/credit_tracker/utils/error/ExceptionUtils.java
#	src/main/resources/logback.xml
2025-10-09 15:39:53 +03:00
Mikhail Trofimov
b272123802 Fixed XmlMergeCoordinator::mergeBatch 2025-10-09 15:30:36 +03:00
etreschenkov
e5945a1ef2 TaskConsumerV2 with poison pill 2025-10-09 11:42:41 +03:00
etreschenkov
7fc7bacbc8 конвейер очередей, тестовый вариант 2025-10-08 17:59:43 +03:00
etreschenkov
194828d0b7 some refactoring and new Facade classes 2025-10-07 10:44:27 +03:00
Mikhail Trofimov
f4b49c06ed Added processSubjects method to SignalService; Load Name data with fids in PhoneService2 2025-10-06 10:21:37 +03:00
Mikhail Trofimov
fcca92e6a5 Added code for compare 2 algorithms for fids finding 2025-10-02 17:58:29 +03:00
Mikhail Trofimov
c31d515c84 Moved phone jdbc calls to repository; Added RepositoryTimingAspect 2025-10-02 17:35:38 +03:00
Mikhail Trofimov
f95b547a81 Fix mereger function, moved it to PhoneService; Use previous logic for PhoneService2 2025-10-02 17:19:08 +03:00
Mikhail Trofimov
daceff96af Update tests; Used new merger function for PhoneService2 2025-10-02 13:29:38 +03:00
etreschenkov
978b45eda8 fix merging fids and phones 2025-10-02 11:51:48 +03:00
etreschenkov
769fabf79a fix merging fids and phones 2025-10-02 11:50:56 +03:00
Mikhail Trofimov
ae7260d790 http://jira.mfd.msk:8088/browse/BKI-3213 Optimize fids searching 2025-10-02 10:33:36 +03:00
Mikhail Trofimov
ae18c5d568 Merge branch 'query_optimization' into 'dev'
http://jira.mfd.msk:8088/browse/BKI-3213 Optimized flags calculating

See merge request nbki/credit_tracker!2
2025-09-30 08:03:39 +00:00
Mikhail Trofimov
8e90a7624c http://jira.mfd.msk:8088/browse/BKI-3213 Optimized flags calculating 2025-09-30 08:03:39 +00:00
98 changed files with 4893 additions and 848 deletions

View file

@ -8,9 +8,9 @@ server.tomcat.accesslog.pattern=%h %l %u %t "%r" %s %b
# datasources
# indic
ct.indic-data-source-settings.driver=com.ibm.db2.jcc.DB2Driver
ct.indic-data-source-settings.url=${DB_URL_INDIC:jdbc:db2://10.230.227.101:2668/cprosd22:currentSchema=INDIC;}
ct.indic-data-source-settings.username=${USERNAME_INDIC:tester}
ct.indic-data-source-settings.password=${PASSWORD_INDIC:tester}
ct.indic-data-source-settings.url=${DB_URL_INDIC:jdbc:db2://10.230.227.100:2668/cprosd22:currentSchema=INDIC;}
ct.indic-data-source-settings.username=${USERNAME_INDIC:ros9appl}
ct.indic-data-source-settings.password=${PASSWORD_INDIC:R0s9Appl}
ct.indic-data-source-settings.maximum-pool-size=${MAXIMUM_POOL_SIZE_INDIC:10}
ct.indic-data-source-settings.minimum-idle=${MINIMUM_IDLE_INDIC:10}
ct.indic-data-source-settings.connection-timeout=${CONNECTION_TIMEOUT_INDIC:10000}
@ -60,13 +60,18 @@ ct.signals-root-uri=${SIGNALS_ROOT_URI:http://10.230.227.102:11094/signalRUalfa/
ct.signals-token=${SIGNALS_TOKEN:gttcHxwcGojoAvZK}
ct.signals-user=${SIGNALS_USER:0001ZZ000001}
ct.load-dir=/opt/app/reports
#ct.load-dir=D:\\folder_name\\signals
#ct.load-dir=/opt/app/reports
ct.load-dir=D:\\folder_name\\signals
# min
ct.monitoring-interval=${MONITORING_INTERVAL:15}
# days
ct.update-flags-cron-expression=${UPDATE_FLAGS_CRON_EXPRESSION:0 0 22 * * *}
ct.update-flags-interval=${UPDATE_FLAGS_INTERVAL:1}
ct.days-to-update-flags=${DAYS_TO_UPDATE_FLAGS:30}
ct.encoding=windows-1251
#Batch size and global pipeline timeout
ct.batchSize=1000
#in minute
ct.timeout=30

View file

@ -1,5 +1,5 @@
#commons
APP_VERSION=1.24
APP_VERSION=1.20
REGISTRY_URL=rh8-vm3.nbki.msk:5000
JMX_PORT=
JMX_HOSTNAME=
@ -71,7 +71,7 @@ SIGNALS_USER=0001ZZ000001
# min
MONITORING_INTERVAL=15
# days
UPDATE_FLAGS_CRON_EXPRESSION=0 22 * * *
UPDATE_FLAGS_INTERVAL=1
DAYS_TO_UPDATE_FLAGS=30

View file

@ -10,12 +10,12 @@
</parent>
<groupId>ru.nbch</groupId>
<artifactId>credit-tracker</artifactId>
<version>1.24</version>
<version>2.0</version>
<name>credit_tracker</name>
<description>Credit Tracker Application</description>
<properties>
<java.version>17</java.version>
<java.version>21</java.version>
<git.info.prefix>git_info</git.info.prefix>
<git.info.filename>git.properties</git.info.filename>
<registry.url>10.230.238.56:5000</registry.url>
@ -41,6 +41,10 @@
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
<dependency>
<groupId>com.ibm.db2</groupId>
<artifactId>jcc</artifactId>

View file

@ -3,21 +3,18 @@ package ru.nbch.credit_tracker;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import ru.nbch.credit_tracker.service.MonitoringService;
import ru.nbch.credit_tracker.service.UpdateProcessingPackagesService;
import ru.nbch.credit_tracker.service.UpdatingFlagsService;
@SpringBootApplication
public class CreditTrackerApplication {
public static void main(String[] args) {
ConfigurableApplicationContext ctx = SpringApplication.run(CreditTrackerApplication.class, args);
UpdateProcessingPackagesService updateProcessingPackagesService = ctx.getBean(UpdateProcessingPackagesService.class);
MonitoringService monitoringService = ctx.getBean(MonitoringService.class);
UpdatingFlagsService updatingFlagsService = ctx.getBean(UpdatingFlagsService.class);
monitoringService.run();
updatingFlagsService.run();
updateProcessingPackagesService.run();
// UpdateProcessingPackagesService updateProcessingPackagesService = ctx.getBean(UpdateProcessingPackagesService.class);
// MonitoringService monitoringService = ctx.getBean(MonitoringService.class);
// UpdatingFlagsService updatingFlagsService = ctx.getBean(UpdatingFlagsService.class);
// updateProcessingPackagesService.run();
// monitoringService.run();
// updatingFlagsService.run();
}
}

View file

@ -0,0 +1,122 @@
package ru.nbch.credit_tracker.builder;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import ru.nbch.credit_tracker.entities.external_request.signals.Person;
import ru.nbch.credit_tracker.model.IdData;
import ru.nbch.credit_tracker.model.NameData;
import ru.nbch.credit_tracker.model.SubjectProfile;
import ru.nbch.credit_tracker.utils.CTUtil;
@Slf4j
public class PersonFactoryBuilderV2 {
private final static DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("dd.MM.yyyy");
private final static String DEFAULT_DATE = "01.01.2000";
private final static String IP = "0";
private final static String OKSM = "999";
private final static String CONSENT_PURPOSE = "16";
private final static String REPORT_USER = "АО «НБКИ»";
private final static String LIABILITY = "1";
private final static String CONSENT_PERIOD = "3";
private final static String REPORT_USER_REG_NUM = "1057746710713";
private final static String REPORT_USER_TAX_ID = "7703548386";
private final static String CONSENT_HASH = "hash";
private final static String CONSENT_DIV_CODE = "000-000";
private final static String INQ_PURPOSE = "16";
private final static String ODT = "Иной документ";
private final static String CONSENT_ISSUE_AUTHORITY = "Кем выдан";
private SubjectProfile personData;
private Long uid;
public static PersonFactoryBuilderV2 builder() {
return new PersonFactoryBuilderV2();
}
public PersonFactoryBuilderV2 personData(SubjectProfile personData) {
this.personData = personData;
//todo откуда брать этот id
// this.uid = personData.getIdentifier();
return this;
}
public Person build() {
if (personData == null) {
return null;
}
NameData nameData = personData.getNameData();
IdData passportData = personData.getIdData();
Person person = new Person();
person.setS(nameData.getLastName());
person.setN(nameData.getFirstName());
person.setM(StringUtils.isNotBlank(nameData.getMiddleName()) ? nameData.getMiddleName() : null);
person.setD(DATE_FORMAT.format(nameData.getBirthDate()));
person.setPn(getPn(passportData.getSerNum(), passportData.getIdNum()));
person.setPd(DATE_FORMAT.format(LocalDate.now())); // Do not use passport iss_date. Signal service could reject the package
person.setDt(getDt(passportData.getIdType(), passportData.getIdType2()));
if (person.getDt() != null && person.getDt().equals("999")) {
person.setOdt(ODT);
person.setConsentOdt(ODT);
}
person.setIp(IP);
person.setUid(uid);
person.setOksm(OKSM);
person.setConsentDate(DATE_FORMAT.format(LocalDate.now()));
person.setConsentPurpose(CONSENT_PURPOSE);
person.setReportUser(REPORT_USER);
person.setLiability(LIABILITY);
person.setConsentPeriod(CONSENT_PERIOD);
person.setAgrDate(DATE_FORMAT.format(LocalDate.now()));
person.setReportUserRegNum(REPORT_USER_REG_NUM);
person.setReportUserTaxID(REPORT_USER_TAX_ID);
person.setConsentHash(CONSENT_HASH);
person.setCs(person.getS());
person.setConsentN(person.getN());
person.setConsentM(person.getM());
person.setConsentBd(person.getD());
person.setConsentPn(person.getPn());
person.setConsentPd(person.getPd());
person.setConsentDivCode(CONSENT_DIV_CODE);
person.setConsentDt(person.getDt());
person.setConsentIssueAuthority(CONSENT_ISSUE_AUTHORITY);
person.setInqPurpose(INQ_PURPOSE);
return person;
}
private String getPn(String serNum, String idNum) {
String normSerNum = CTUtil.normalize(serNum);
String normIdNum = CTUtil.normalize(idNum);
return StringUtils.isNotBlank(normSerNum) ? normSerNum + normIdNum : normIdNum;
}
private String getDt(String idType, String idType2) {
if (StringUtils.isNotBlank(idType2)) {
return idType2;
} else {
return switch (idType) {
case "21" -> "21";
case "22" -> "22.1";
case "09" -> "22.2";
case "26" -> "23";
case "04" -> "24";
case "07" -> "25";
case "14", "03" -> "27";
case "10" -> "31";
case "15" -> "32";
case "13" -> "37";
case "06", "27" -> "28";
case "01", "02" -> "999";
default -> {
log.warn("ID_TYPE2 is blank and no matching found for ID_TYPE {}", idType);
yield "";
}
};
}
}
}

View file

@ -0,0 +1,95 @@
package ru.nbch.credit_tracker.component;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import ru.nbch.credit_tracker.config.settings.CreditTrackerSettings;
import ru.nbch.credit_tracker.entities.user_request.monitoring_request.MonitoringRequest;
import ru.nbch.credit_tracker.enums.SignalError;
import ru.nbch.credit_tracker.enums.XmlErrorFormat;
import ru.nbch.credit_tracker.exceptions.SignalException;
import ru.nbch.credit_tracker.model.MonitoringValidationError;
import ru.nbch.credit_tracker.model.ValidationResult;
import ru.nbch.credit_tracker.service.xml.stax.StaxXmlProcessor;
import ru.nbch.credit_tracker.utils.ValidationService;
import ru.nbch.credit_tracker.utils.ZipUtils;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Optional;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import static ru.nbch.credit_tracker.utils.error.ExceptionUtils.clientExcp;
import static ru.nbch.credit_tracker.utils.error.ExceptionUtils.serverExcp;
@Component
@Slf4j
public class MonitoringRequestValidator {
private final CreditTrackerSettings config;
private final ValidationService validationService;
private final StaxXmlProcessor staxXmlProcessor;
public MonitoringRequestValidator(CreditTrackerSettings config,
ValidationService validationService,
StaxXmlProcessor staxXmlProcessor) {
this.config = config;
this.validationService = validationService;
this.staxXmlProcessor = staxXmlProcessor;
}
public void validateZipContent(MultipartFile file) {
log.trace("Validating zip content");
if (file.isEmpty() || !ZipUtils.isZipFile(file)) {
throw clientExcp(SignalError.ERROR_001, XmlErrorFormat.MonitoringError);
}
// Валидация Архива на количетсво файлов, размер и расширение
try (ZipInputStream zipInputStream = new ZipInputStream(file.getInputStream())) {
Optional<ZipEntry> xmlFileEntry = ZipUtils.extractValidEntry(zipInputStream);
if (xmlFileEntry.isEmpty()) {
throw clientExcp(SignalError.ERROR_001, XmlErrorFormat.MonitoringError);
}
long maxXmlFileSize = config.getMaxXmlFileSize() * 1024 * 1024;
if (xmlFileEntry.get().getSize() > maxXmlFileSize) {
throw clientExcp(SignalError.ERROR_009, XmlErrorFormat.MonitoringError);
}
} catch (IOException e) {
throw serverExcp(SignalError.ERROR_013, XmlErrorFormat.MonitoringError, e);
}
log.trace("Validated zip content ok!");
}
public ValidationResult readAndValidateMonitoringRequest(String filePath) {
log.debug("Validating monitoring request...");
ValidationResult validationResult = new ValidationResult();
try (BufferedInputStream in = new BufferedInputStream(Files.newInputStream(Path.of(filePath)), 8 * 1024 * 1024)) {
staxXmlProcessor.read(in, MonitoringRequest.class,
request -> {
try {
validationService.validateMonitoringRequest(request);
if (request.getPackageName() != null && validationResult.getPackageName() == null) {
validationResult.setPackageName(request.getPackageName());
}
if (request.getUserId() != null && validationResult.getUserId() == null) {
validationResult.setUserId(request.getUserId());
}
if (request.getPassword() != null && validationResult.getPassword() == null) {
validationResult.setPassword(request.getPassword());
}
request.getSubjects().getSubject().clear();
} catch (SignalException e) {
validationResult.addError(new MonitoringValidationError(-1, "", e));
}
},
request -> request.getSubjects().getSubject().isEmpty(),
40000, "/MonitoringRequest/Subjects/Subject");
} catch (IOException e) {
throw new RuntimeException("Error while parsing and validating monitoring request", e);
}
log.debug("Validating monitoring request {}!", validationResult.getValidationErrors().isEmpty() ? "ok" : "fail");
return validationResult;
}
}

View file

@ -0,0 +1,28 @@
package ru.nbch.credit_tracker.component;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Aspect
@Slf4j
@Component
public class RepositoryTimingAspect {
@Around("within(@org.springframework.stereotype.Repository *)")
public Object timeMapper(ProceedingJoinPoint pjp) throws Throwable {
long start = System.nanoTime();
try {
return pjp.proceed();
} finally {
long tookMs = (System.nanoTime() - start) / 1_000_000;
String method = pjp.getSignature().toShortString();
if (tookMs > 1000) {
log.warn("Slow jdbc template call {} took {} ms", method, tookMs);
} else {
log.debug("Jdbc template call {} took {} ms", method, tookMs);
}
}
}
}

View file

@ -4,6 +4,8 @@ import com.zaxxer.hikari.HikariDataSource;
import org.springframework.boot.jdbc.DataSourceBuilder;
import ru.nbch.credit_tracker.config.settings.DataSourceSettings;
import javax.sql.DataSource;
public class DatabaseConfig {
protected HikariDataSource createDataSource(DataSourceSettings settings) {

View file

@ -10,6 +10,7 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.Resource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import ru.nbch.credit_tracker.config.settings.CreditTrackerSettings;
import ru.nbch.credit_tracker.config.settings.DataSourceSettings;
@ -60,5 +61,10 @@ public class IndicDataSourceConfig extends DatabaseConfig {
sqlSessionFactoryBean.setTypeHandlersPackage("ru.nbch.credit_tracker.config.datasource.handlers");
return sqlSessionFactoryBean.getObject();
}
@Bean(name = "jdbcTemplateIndic")
public NamedParameterJdbcTemplate getNamedParameterJdbcTemplate(@Qualifier("indicDataSource") DataSource dataSource) {
return new NamedParameterJdbcTemplate(dataSource);
}
}

View file

@ -0,0 +1,53 @@
package ru.nbch.credit_tracker.config.datasource.handlers;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.util.UUID;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedTypes;
import org.apache.ibatis.type.TypeHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@MappedTypes({UUID.class})
public class UUIDTypeHandler implements TypeHandler<UUID> {
private static final Logger log = LoggerFactory.getLogger(UUIDTypeHandler.class);
@Override
public void setParameter(PreparedStatement ps, int i, UUID parameter, JdbcType jdbcType) throws SQLException {
if (parameter == null) {
ps.setObject(i, null, Types.OTHER);
} else {
ps.setObject(i, parameter.toString(), Types.OTHER);
}
}
@Override
public UUID getResult(ResultSet rs, String columnName) throws SQLException {
return toUUID(rs.getString(columnName));
}
@Override
public UUID getResult(ResultSet rs, int columnIndex) throws SQLException {
return toUUID(rs.getString(columnIndex));
}
@Override
public UUID getResult(CallableStatement cs, int columnIndex) throws SQLException {
return toUUID(cs.getString(columnIndex));
}
private static UUID toUUID(String val) {
if (val != null && !val.isEmpty() && !val.isBlank()) {
try {
return UUID.fromString(val);
} catch (IllegalArgumentException e) {
log.warn("Bad UUID found: {}", val);
}
}
return null;
}
}

View file

@ -0,0 +1,14 @@
package ru.nbch.credit_tracker.config.mdc;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import ru.nbch.credit_tracker.utils.LogUtil;
// Annotation to only clear the MDC key at method exit
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ClearMdc {
String key() default LogUtil.mdcKey;
}

View file

@ -1,18 +1,27 @@
package ru.nbch.credit_tracker.config.mdc;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.MDC;
import ru.nbch.credit_tracker.entities.dto.UserPackage;
import ru.nbch.credit_tracker.entities.app.UserPackage;
import ru.nbch.credit_tracker.entities.user_request.monitoring_request.MonitoringRequest;
import ru.nbch.credit_tracker.utils.LogUtil;
@Slf4j
@Aspect
public class MdcAspect {
// For methods that only require clearing the MDC at the end
@Around("@annotation(clearMdc)")
public Object clearMdcAdvice(ProceedingJoinPoint pjp, ClearMdc clearMdc) throws Throwable {
try {
return pjp.proceed();
} finally {
LogUtil.removeMdcLogKey(clearMdc.key());
}
}
// For methods that set an MDC value (from a method argument) and then clear it at the end
@Around("@annotation(setAndClearMdc)")
public Object setAndClearMdcAdvice(ProceedingJoinPoint pjp, SetAndClearMdc setAndClearMdc) throws Throwable {
@ -20,30 +29,18 @@ public class MdcAspect {
int index = setAndClearMdc.argIndex();
Object obj = (args != null && args.length > index) ? args[index] : null;
if (obj instanceof MonitoringRequest request) {
if (request.getPackageName() != null) {
putMdcValue(setAndClearMdc.key(), request.getPackageName());
}
LogUtil.addMdcLogKey(setAndClearMdc.key(), request.getPackageName());
} else if (obj instanceof UserPackage userPackage) {
if (userPackage.getPackageName() != null) {
putMdcValue(setAndClearMdc.key(), userPackage.getPackageName());
}
LogUtil.addMdcLogKey(setAndClearMdc.key(), userPackage.getPackageName());
} else if (obj instanceof String packageName) {
putMdcValue(setAndClearMdc.key(), packageName);
LogUtil.addMdcLogKey(setAndClearMdc.key(), packageName);
} else {
log.debug("no packageName found for for {}", pjp.toShortString());
}
try {
return pjp.proceed();
} finally {
MDC.remove(setAndClearMdc.key());
}
}
private void putMdcValue(String key, String value) {
try {
MDC.put(key, "[" + value + "] ");
} catch (Throwable e) {
log.error("{}", ExceptionUtils.getStackTrace(e));
LogUtil.removeMdcLogKey(setAndClearMdc.key());
}
}
}

View file

@ -4,12 +4,13 @@ import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import ru.nbch.credit_tracker.utils.LogUtil;
// Annotation to set an MDC key based on a method argument and clear it after method execution
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface SetAndClearMdc {
String key() default "ru.nbch.credit_tracker.log.mdc.key";
String key() default LogUtil.mdcKey;
// The index of the method argument to use for the MDC value (defaults to the first argument)
int argIndex() default 0;
}

View file

@ -1,5 +1,6 @@
package ru.nbch.credit_tracker.config.rest;
import java.nio.charset.Charset;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
@ -9,15 +10,19 @@ import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.multipart.MaxUploadSizeExceededException;
import ru.nbch.credit_tracker.config.settings.CreditTrackerSettings;
import ru.nbch.credit_tracker.entities.user_response.errors.*;
import ru.nbch.credit_tracker.entities.user_response.errors.CommonError;
import ru.nbch.credit_tracker.entities.user_response.errors.MonitoringActivePackagesError;
import ru.nbch.credit_tracker.entities.user_response.errors.MonitoringAllPackagesError;
import ru.nbch.credit_tracker.entities.user_response.errors.MonitoringDownloadError;
import ru.nbch.credit_tracker.entities.user_response.errors.MonitoringError;
import ru.nbch.credit_tracker.entities.user_response.errors.MonitoringPackageDeleteError;
import ru.nbch.credit_tracker.entities.user_response.errors.MonitoringReportListError;
import ru.nbch.credit_tracker.enums.SignalError;
import ru.nbch.credit_tracker.exceptions.SignalClientException;
import ru.nbch.credit_tracker.exceptions.SignalException;
import ru.nbch.credit_tracker.exceptions.SignalServerException;
import ru.nbch.credit_tracker.utils.error.SimpleMessageResolver;
import java.nio.charset.Charset;
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@ -42,34 +47,33 @@ public class GlobalExceptionHandler {
return new ResponseEntity<>(error, headers, HttpStatus.BAD_REQUEST);
}
@ExceptionHandler(value = SignalException.class, produces = MediaType.APPLICATION_XML_VALUE)
public ResponseEntity<CommonError> handleClientException(SignalException ex) {
@ExceptionHandler(value = SignalClientException.class, produces = MediaType.APPLICATION_XML_VALUE)
public ResponseEntity<CommonError> handleClientException(SignalClientException ex) {
CommonError err = getCommonError(ex);
String packageName = ex.getPackageName();
if(packageName != null ) {
log.warn("Response error: {}. PackageName: {}", err.getErrorText(), packageName);
} else {
log.warn("Response error: {}", err.getErrorText());
}
return new ResponseEntity<>(err, xmlHeaders(), HttpStatus.BAD_REQUEST);
}
@ExceptionHandler(value = SignalServerException.class, produces = MediaType.APPLICATION_XML_VALUE)
public ResponseEntity<CommonError> handleServerException(SignalServerException ex) {
CommonError commonError = getCommonError(ex);
Exception cause = ex.getCause();
log.error(cause.getMessage(), cause);
return new ResponseEntity<>(commonError, xmlHeaders(), HttpStatus.INTERNAL_SERVER_ERROR);
}
private HttpHeaders xmlHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(new MediaType("application", "xml", Charset.forName(encoding)));
String errorText = messageResolver.resolve(ex.getErrorMessage());
CommonError commonError = getCommonError(ex);
commonError.setStatus("error");
commonError.setErrorCode(ex.getErrorMessage().getSubject().getCode());
commonError.setErrorText(errorText);
if (ex instanceof SignalClientException) {
String packageName = ((SignalClientException) ex).getPackageName();
if(packageName != null ) {
log.warn("Response error: {}. PackageName: {}", errorText, packageName);
} else {
log.warn("Response error: {}", errorText);
}
return new ResponseEntity<>(commonError, headers, HttpStatus.BAD_REQUEST);
} else {
Exception cause = ((SignalServerException) ex).getCause();
log.error(cause.getMessage(), cause);
return new ResponseEntity<>(commonError, headers, HttpStatus.INTERNAL_SERVER_ERROR);
}
return headers;
}
private static CommonError getCommonError(SignalException ex) {
private CommonError getCommonError(SignalException ex) {
CommonError commonError;
switch (ex.getErrorFormat()) {
case MonitoringError -> commonError = new MonitoringError();
@ -80,6 +84,10 @@ public class GlobalExceptionHandler {
case MonitoringPackageDeleteError -> commonError = new MonitoringPackageDeleteError();
default -> throw new IllegalStateException("Unsupported error format: " + ex.getErrorFormat());
}
commonError.setStatus("error");
commonError.setErrorCode(ex.getErrorMessage().getSubject().getCode());
String errorText = messageResolver.resolve(ex.getErrorMessage());
commonError.setErrorText(errorText);
return commonError;
}
}

View file

@ -29,8 +29,11 @@ public class CreditTrackerSettings {
private Long maxXmlFileSize;
private String loadDir;
private Long monitoringInterval;
private String updateFlagsCronExpression;
private Long updateFlagsInterval;
private Long daysToUpdateFlags;
private String encoding;
private Integer batchSize;
private Integer timeout;
}

View file

@ -0,0 +1,36 @@
package ru.nbch.credit_tracker.config.task;
import java.util.function.BiFunction;
import java.util.function.Supplier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import ru.nbch.credit_tracker.config.settings.CreditTrackerSettings;
import ru.nbch.credit_tracker.mapper.signal.PhoneFidMapMapper;
import ru.nbch.credit_tracker.model.impl.PartFilePayload;
import ru.nbch.credit_tracker.model.impl.SignalXmlFilePayload;
import ru.nbch.credit_tracker.model.impl.SubjectPayload;
import ru.nbch.credit_tracker.service.indic.FlagsService2;
import ru.nbch.credit_tracker.service.task.impl.FlagsEnricher;
import ru.nbch.credit_tracker.service.task.impl.KickPipeLineImpl;
import ru.nbch.credit_tracker.service.task.impl.XmlMergeCoordinator;
import ru.nbch.credit_tracker.service.task.setup.IPipeLineStarter;
import ru.nbch.credit_tracker.service.task.setup.IPipeLineTerminator;
import ru.nbch.credit_tracker.service.xml.stax.StaxXmlProcessor;
@Configuration
public class PipelineStarterConfig {
@Bean
public BiFunction<Long, String, IPipeLineStarter<SubjectPayload>> pipelineStarterFactory(StaxXmlProcessor xml, CreditTrackerSettings creditTrackerSettings) {
return (packageId, packageName) -> new KickPipeLineImpl(xml, packageId, packageName, creditTrackerSettings);
}
@Bean
public Supplier<IPipeLineTerminator<PartFilePayload, SignalXmlFilePayload>> pipelineTerminatorFactory(CreditTrackerSettings creditTrackerSettings) {
return () -> new XmlMergeCoordinator(creditTrackerSettings);
}
@Bean
public Supplier<FlagsEnricher> flagEnricherFactory(FlagsService2 flagsService, PhoneFidMapMapper mapper) {
return () -> new FlagsEnricher(flagsService, mapper);
}
}

View file

@ -0,0 +1,26 @@
package ru.nbch.credit_tracker.config.task;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import ru.nbch.credit_tracker.model.impl.PartFilePayload;
import ru.nbch.credit_tracker.model.impl.SubjectPayload;
@Configuration
public class StageConfig {
@Bean("saveSubjectQueueFactory")
public StageQueueFactory<SubjectPayload> saveSubjectQueueFactory() {
return new StageQueueFactory<>(5);
}
@Bean("idEnricherQueueFactory")
public StageQueueFactory<SubjectPayload> idEnricherQueueFactory() {
return new StageQueueFactory<>();
}
@Bean("partFileQueueFactory")
public StageQueueFactory<PartFilePayload> partFileQueueFactory() {
return new StageQueueFactory<>(10000);
}
}

View file

@ -0,0 +1,33 @@
package ru.nbch.credit_tracker.config.task;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import org.springframework.beans.factory.FactoryBean;
import ru.nbch.credit_tracker.model.StagePayload;
public class StageQueueFactory<T extends StagePayload> implements FactoryBean<BlockingQueue<T>> {
private final int capacity;
public StageQueueFactory(int capacity) {
this.capacity = capacity;
}
public StageQueueFactory() {
this(10);
}
@Override
public BlockingQueue<T> getObject() throws Exception {
return new LinkedBlockingQueue<>(capacity);
}
@Override
public Class<?> getObjectType() {
return BlockingQueue.class;
}
@Override
public boolean isSingleton() {
return false;
}
}

View file

@ -3,8 +3,14 @@ package ru.nbch.credit_tracker.controller;
import org.springframework.core.io.Resource;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import ru.nbch.credit_tracker.config.mdc.ClearMdc;
import ru.nbch.credit_tracker.entities.user_request.active_packages.MonitoringActivePackagesRequest;
import ru.nbch.credit_tracker.entities.user_request.all_packages.MonitoringAllPackagesRequest;
import ru.nbch.credit_tracker.entities.user_response.active_packages.MonitoringActivePackagesResponse;
@ -12,47 +18,70 @@ import ru.nbch.credit_tracker.entities.user_response.all_packages.MonitoringAllP
import ru.nbch.credit_tracker.entities.user_response.success.MonitoringPackageDeleteResponse;
import ru.nbch.credit_tracker.entities.user_response.success.MonitoringReportListResponse;
import ru.nbch.credit_tracker.entities.user_response.success.MonitoringSetupResponse;
import ru.nbch.credit_tracker.facade.SignalFacade;
import ru.nbch.credit_tracker.enums.SignalError;
import ru.nbch.credit_tracker.enums.XmlErrorFormat;
import ru.nbch.credit_tracker.exceptions.SignalException;
import ru.nbch.credit_tracker.facade.SignalFacadeV3;
import static ru.nbch.credit_tracker.utils.error.ExceptionUtils.serverExcp;
@RestController
@RequestMapping("/api/v1/signal")
public class SignalController {
public class SignalControllerV3 {
private final SignalFacade signalFacade;
private final SignalFacadeV3 signalFacadeV3;
public SignalController(SignalFacade signalFacade) {
this.signalFacade = signalFacade;
public SignalControllerV3(SignalFacadeV3 signalFacadeV3) {
this.signalFacadeV3 = signalFacadeV3;
}
@PostMapping(value = "/set", produces = MediaType.APPLICATION_XML_VALUE)
@ClearMdc
public ResponseEntity<MonitoringSetupResponse> setupMonitoring(@RequestParam("file") MultipartFile file) {
return signalFacade.setupMonitoring(file);
signalFacadeV3.setupMonitoringRequest(file);
return ResponseEntity.ok().body(new MonitoringSetupResponse());
}
@PostMapping(value = "/active_packages", consumes = MediaType.APPLICATION_XML_VALUE, produces = MediaType.APPLICATION_XML_VALUE)
public ResponseEntity<MonitoringActivePackagesResponse> findActivePackages(@RequestBody MonitoringActivePackagesRequest activePackagesRequest) {
return signalFacade.findActivePackages(activePackagesRequest);
try {
return signalFacadeV3.findActivePackages(activePackagesRequest);
} catch (Exception e) {
throwOrWrap(SignalError.ERROR_013, XmlErrorFormat.MonitoringActivePackagesError, e);
return null;
}
}
@PostMapping(value = "/all_packages", consumes = MediaType.APPLICATION_XML_VALUE, produces = MediaType.APPLICATION_XML_VALUE)
public ResponseEntity<MonitoringAllPackagesResponse> findAllPackages(@RequestBody MonitoringAllPackagesRequest allPackagesRequest) {
return signalFacade.findAllPackages(allPackagesRequest);
try {
return signalFacadeV3.findAllPackages(allPackagesRequest);
} catch (Exception e) {
throwOrWrap(SignalError.ERROR_013, XmlErrorFormat.MonitoringAllPackagesError, e);
return null;
}
}
@PostMapping(value = "/delete", params = {"username", "packageName"}, produces = MediaType.APPLICATION_XML_VALUE)
public ResponseEntity<MonitoringPackageDeleteResponse> deletePackage(@RequestParam("username") String username, @RequestParam("packageName") String packageName) {
return signalFacade.deletePackage(username, packageName);
return signalFacadeV3.deletePackage(username, packageName);
}
@GetMapping(value = "/report_list", params = {"username", "packageName"}, produces = MediaType.APPLICATION_XML_VALUE)
public ResponseEntity<MonitoringReportListResponse> getReportList(@RequestParam("username") String username, @RequestParam("packageName") String packageName) {
return signalFacade.getReportList(username, packageName);
return signalFacadeV3.getReportList(username, packageName);
}
@GetMapping(value = "/download", params = "file", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
public ResponseEntity<Resource> downloadFile(@RequestParam("file") String file) {
return signalFacade.downloadFile(file);
return signalFacadeV3.downloadFile(file);
}
private static void throwOrWrap(SignalError error, XmlErrorFormat specific, Exception ex) {
if (ex instanceof SignalException) {
throw (SignalException) ex;
}
throw serverExcp(error, specific, ex);
}
}

View file

@ -1,4 +1,4 @@
package ru.nbch.credit_tracker.entities.dto;
package ru.nbch.credit_tracker.entities.app;
import lombok.Getter;
import lombok.Setter;

View file

@ -0,0 +1,41 @@
package ru.nbch.credit_tracker.entities.app;
import java.util.UUID;
import lombok.*;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class PhoneFidMap {
private Long id;
private Long packageId;
private UUID recUuid;
private String subjectId;
private String phone;
private Integer fid;
private Integer flagDebt5000;
private Integer flag90days;
private LocalDateTime calculatedAt;
private String errorText;
// NAME data
private String firstName;
private String lastName;
private String middleName;
private LocalDate birthDate;
private LocalDate fileSinceDate;
//ID data
private String serNum;
private String idNum;
private LocalDate issDate;
private String idType;
private String idType2;
private LocalDate IdfileSinceDate;
boolean isFullFilled = false;
}

View file

@ -1,4 +1,4 @@
package ru.nbch.credit_tracker.entities.dto;
package ru.nbch.credit_tracker.entities.app;
import lombok.Builder;
import lombok.Getter;

View file

@ -1,22 +0,0 @@
package ru.nbch.credit_tracker.entities.dto;
import lombok.*;
import java.time.LocalDateTime;
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class PhoneFidMap {
private Long id;
private Long packageId;
private String subjectId;
private String phone;
private Integer fid;
private Integer flagDebt5000;
private Integer flag90days;
private LocalDateTime calculatedAt;
private String errorText;
}

View file

@ -0,0 +1,20 @@
package ru.nbch.credit_tracker.entities.indic;
import java.time.LocalDate;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@Builder
public class Name {
private Integer fid;
private String number;
private LocalDate fileSinceDate;
private String first;
private String last;
private String middle;
private LocalDate birthDate;
}

View file

@ -0,0 +1,11 @@
package ru.nbch.credit_tracker.entities.indic;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class PaymentsFlag {
private Integer fid;
private Integer flag;
}

View file

@ -4,4 +4,5 @@ public interface CommonError {
void setStatus(String status);
void setErrorCode(String errorCode);
void setErrorText(String errorText);
String getErrorText();
}

View file

@ -1,436 +0,0 @@
package ru.nbch.credit_tracker.facade;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.aop.framework.AopContext;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.core.io.FileUrlResource;
import org.springframework.core.io.Resource;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import ru.nbch.credit_tracker.config.mdc.SetAndClearMdc;
import ru.nbch.credit_tracker.config.settings.CreditTrackerSettings;
import ru.nbch.credit_tracker.entities.dto.UserPackage;
import ru.nbch.credit_tracker.entities.user_request.active_packages.MonitoringActivePackagesRequest;
import ru.nbch.credit_tracker.entities.user_request.all_packages.MonitoringAllPackagesRequest;
import ru.nbch.credit_tracker.entities.user_request.monitoring_request.MonitoringRequest;
import ru.nbch.credit_tracker.entities.user_response.active_packages.MonitoringActivePackagesResponse;
import ru.nbch.credit_tracker.entities.user_response.active_packages.Package;
import ru.nbch.credit_tracker.entities.user_response.active_packages.Packages;
import ru.nbch.credit_tracker.entities.user_response.all_packages.MonitoringAllPackagesResponse;
import ru.nbch.credit_tracker.entities.user_response.all_packages.PackageAll;
import ru.nbch.credit_tracker.entities.user_response.all_packages.PackagesAll;
import ru.nbch.credit_tracker.entities.user_response.success.MonitoringPackageDeleteResponse;
import ru.nbch.credit_tracker.entities.user_response.success.MonitoringReportListResponse;
import ru.nbch.credit_tracker.entities.user_response.success.MonitoringSetupResponse;
import ru.nbch.credit_tracker.enums.SignalError;
import ru.nbch.credit_tracker.enums.Status;
import ru.nbch.credit_tracker.enums.XmlErrorFormat;
import ru.nbch.credit_tracker.exceptions.SignalClientException;
import ru.nbch.credit_tracker.service.MonitoringErrorProcesingService;
import ru.nbch.credit_tracker.service.SignalService;
import ru.nbch.credit_tracker.service.scoring.AuthorizationService;
import ru.nbch.credit_tracker.service.signal.SignalRestService;
import ru.nbch.credit_tracker.service.xml.stax.StaxXmlProcessor;
import ru.nbch.credit_tracker.utils.CTUtil;
import ru.nbch.credit_tracker.utils.ValidationService;
import ru.nbch.credit_tracker.utils.ZipUtils;
import javax.xml.stream.XMLStreamException;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import static ru.nbch.credit_tracker.utils.error.ExceptionUtils.clientExcp;
import static ru.nbch.credit_tracker.utils.error.ExceptionUtils.serverExcp;
@Slf4j
@Service
@EnableAspectJAutoProxy(exposeProxy = true)
public class SignalFacade {
private SignalFacade self;
private final CreditTrackerSettings config;
private final AuthorizationService authorizationService;
private final SignalService signalService;
private final SignalRestService signalRestService;
private final MonitoringErrorProcesingService monitoringErrorProcesingService;
private final ValidationService validationService;
private final StaxXmlProcessor staxXmlProcessor;
private final ExecutorService executor;
private final long maxXmlFileSize;
public SignalFacade(CreditTrackerSettings config,
AuthorizationService authorizationService,
SignalService signalService,
SignalRestService signalRestService,
MonitoringErrorProcesingService monitoringErrorProcesingService,
ValidationService validationService,
StaxXmlProcessor staxXmlProcessor,
@Qualifier("executorService") ExecutorService executor) {
this.config = config;
this.authorizationService = authorizationService;
this.signalService = signalService;
this.signalRestService = signalRestService;
this.monitoringErrorProcesingService = monitoringErrorProcesingService;
this.validationService = validationService;
this.staxXmlProcessor = staxXmlProcessor;
this.executor = executor;
this.maxXmlFileSize = config.getMaxXmlFileSize() * 1024 * 1024;
}
public ResponseEntity<MonitoringSetupResponse> setupMonitoring(MultipartFile file) {
// первичные валидации
if (file.isEmpty() || !isZipFile(file)) {
throw clientExcp(SignalError.ERROR_001, XmlErrorFormat.MonitoringError);
}
// Валидация Архива на количетсво файлов, размер и расширение
MonitoringRequest monitoringRequest;
Long packageId;
ZipInputStream zipInputStream = null;
String filePath;
try {
zipInputStream = new ZipInputStream(file.getInputStream());
Optional<ZipEntry> xmlFileEntry = ZipUtils.extractValidEntry(zipInputStream);
if (xmlFileEntry.isEmpty()) {
throw clientExcp(SignalError.ERROR_001, XmlErrorFormat.MonitoringError);
}
if (xmlFileEntry.get().getSize() > maxXmlFileSize) {
throw clientExcp(SignalError.ERROR_009, XmlErrorFormat.MonitoringError);
}
zipInputStream.close();
zipInputStream = new ZipInputStream(file.getInputStream());
zipInputStream.getNextEntry();
filePath = writeTmpFile(zipInputStream);
} catch (Exception e) {
if (e instanceof SignalClientException) {
throw (SignalClientException) e;
}
throw serverExcp(SignalError.ERROR_013, XmlErrorFormat.MonitoringError, e);
} finally {
if (zipInputStream != null) {
try {
zipInputStream.close();
} catch (IOException e) {
throw serverExcp(SignalError.ERROR_013, XmlErrorFormat.MonitoringError, e);
}
}
}
// валидация xml
try {
monitoringRequest = readAndValidateMonitoringRequest(filePath);
} catch (Exception e) {
CTUtil.removeFile(filePath);
if (e.getCause() instanceof XMLStreamException) {
throw clientExcp(SignalError.ERROR_011, XmlErrorFormat.MonitoringError);
}
if (e instanceof SignalClientException) {
throw (SignalClientException) e;
}
throw serverExcp(SignalError.ERROR_013, XmlErrorFormat.MonitoringError, e);
}
try {
// проверка авторизации
AuthorizationService.AuthResult authResult = authorizationService.findAuthInfo(monitoringRequest.getUserId(), monitoringRequest.getPassword());
if (!authResult.isSuccess()) {
signalService.rejectPackage(monitoringRequest);
throw clientExcp(authResult.getError(), monitoringRequest.getPackageName(), XmlErrorFormat.MonitoringError);
}
// проверка на уникальность пакета
if (signalService.isPackageNotUnique(monitoringRequest)) {
signalService.rejectPackage(monitoringRequest);
throw clientExcp(SignalError.ERROR_007, monitoringRequest.getPackageName(), XmlErrorFormat.MonitoringError);
}
packageId = signalService.registerPackage(monitoringRequest);
log.info("Package registered. PackageId {}. Status: {}", packageId, Status.PROCESSING.name());
} catch (Exception e) {
CTUtil.removeFile(filePath);
if (e instanceof SignalClientException) {
throw (SignalClientException) e;
}
throw serverExcp(SignalError.ERROR_013, XmlErrorFormat.MonitoringError, e);
}
// в отдельном потоке запускаем регистрацию субъектов, поиск фидов и расчет флагов просрочек
getCurrentProxy().registerFidsAndDebts(packageId, monitoringRequest.getPackageName(), filePath);
return ResponseEntity.ok().body(new MonitoringSetupResponse());
}
private MonitoringRequest readAndValidateMonitoringRequest(String filePath) {
MonitoringRequest monitoringRequest;
try (BufferedInputStream in = new BufferedInputStream(Files.newInputStream(Path.of(filePath)), 8 * 1024 * 1024)) {
monitoringRequest = staxXmlProcessor.read(in, MonitoringRequest.class,
request -> {
try {
validationService.validateMonitoringRequest(request);
} catch (SignalClientException e) {
if (request != null) {
signalService.rejectPackage(request);
}
request = null;
throw e;
}
request.getSubjects().getSubject().clear();
},
request -> request.getSubjects().getSubject().isEmpty()
,
40000, "/MonitoringRequest/Subjects/Subject");
} catch (IOException e) {
throw new RuntimeException("Error while parsing and validating monitoring request", e);
}
return monitoringRequest;
}
private String writeTmpFile(ZipInputStream zipInputStream) throws IOException {
String folderPath = config.getLoadDir() + File.separator + "tmp_folder" + File.separator;
String filePath = folderPath + UUID.randomUUID() + "monitoring_request.xml";
CTUtil.createFolder(folderPath);
try (OutputStream out = Files.newOutputStream(Path.of(filePath))) {
byte[] buffer = new byte[8 * 1024 * 1024];
int bytesRead;
while ((bytesRead = zipInputStream.read(buffer)) > 0) {
out.write(buffer, 0, bytesRead);
}
return filePath;
}
}
@SetAndClearMdc(argIndex = 1)
public void registerFidsAndDebts(Long packageId, String packageName, String filePath) {
Map<String, String> context = MDC.getCopyOfContextMap();
executor.execute(() -> {
if (context != null) {
MDC.setContextMap(context);
}
try {
signalService.registerSubjects(filePath, packageId);
CTUtil.removeFile(filePath);
SignalService.UpdateResult updateResult = signalService.updateSubjects(packageId, null);
if (updateResult.isNoFidsFound()) {
log.info("No fids found for PackageId: {} PackageName: {}. Package status ON_MONITORING. No registration in signal service.", packageId, packageName);
signalService.updatePackageStatus(packageId, Status.ON_MONITORING);
return;
}
if (signalRestService.setupMonitoring(packageId)) {
LocalDateTime monitoringAt = LocalDateTime.now();
log.info("Package was successfully registered in signal service. PackageId: {} PackageName: {}, MonitoringAt: {}", packageId, packageName, monitoringAt);
signalService.updatePackageOnMonitoringSuccess(packageId, monitoringAt);
monitoringErrorProcesingService.findErrors(packageId);
} else {
log.info("Package registration was rejected by signal service. PackageId: {} PackageName: {}", packageId, packageName);
signalService.updatePackageStatus(packageId, Status.ERROR);
}
} catch (Exception e) {
CTUtil.removeFile(filePath);
signalService.updatePackageStatus(packageId, Status.ERROR);
log.error("Ошибка при поиске фидов или расчете флагов просрочек. Package_id {}", packageId, e);
}
});
}
private boolean isZipFile(MultipartFile file) {
return file.getContentType() != null &&
(file.getContentType().equals("application/zip") || file.getContentType().equals("application/x-zip-compressed")) ||
file.getOriginalFilename() != null && file.getOriginalFilename().toLowerCase().endsWith(".zip");
}
public ResponseEntity<MonitoringActivePackagesResponse> findActivePackages(MonitoringActivePackagesRequest activePackagesRequest) {
try {
AuthorizationService.AuthResult authResult = authorizationService.findAuthInfo(activePackagesRequest.getUserId(), activePackagesRequest.getPassword());
if (!authResult.isSuccess()) {
throw clientExcp(authResult.getError(), XmlErrorFormat.MonitoringActivePackagesError);
}
List<UserPackage> activePackages = signalService.findActivePackages(activePackagesRequest.getUserId());
if (!activePackages.isEmpty()) {
MonitoringActivePackagesResponse response = new MonitoringActivePackagesResponse();
response.setStatus("OK");
response.setMembercode(activePackagesRequest.getUserId().substring(0, 6));
Packages packages = new Packages();
activePackages.forEach(data -> {
Package pack = new Package();
pack.setPackageName(data.getPackageName());
pack.setCreatedAt(data.getCreatedAt() != null ? data.getCreatedAt().toString() : null);
packages.getPackages().add(pack);
});
response.setPackages(packages);
return ResponseEntity.ok(response);
} else {
throw clientExcp(SignalError.ERROR_015, XmlErrorFormat.MonitoringActivePackagesError);
}
} catch (Exception e) {
if (e instanceof SignalClientException) {
throw (SignalClientException) e;
}
throw serverExcp(SignalError.ERROR_013, XmlErrorFormat.MonitoringActivePackagesError, e);
}
}
public ResponseEntity<MonitoringAllPackagesResponse> findAllPackages(MonitoringAllPackagesRequest allPackagesRequest) {
try {
AuthorizationService.AuthResult authResult = authorizationService.findAuthInfo(allPackagesRequest.getUserId(), allPackagesRequest.getPassword());
if (!authResult.isSuccess()) {
throw clientExcp(authResult.getError(), XmlErrorFormat.MonitoringAllPackagesError);
}
List<UserPackage> activePackages = signalService.findAllPackages(allPackagesRequest.getUserId());
if (!activePackages.isEmpty()) {
MonitoringAllPackagesResponse response = new MonitoringAllPackagesResponse();
response.setStatus("OK");
response.setMembercode(allPackagesRequest.getUserId().substring(0, 6));
PackagesAll packages = new PackagesAll();
activePackages.forEach(data -> {
PackageAll pack = new PackageAll();
pack.setPackageName(data.getPackageName());
pack.setCreatedAt(data.getCreatedAt() != null ? data.getCreatedAt().toString() : null);
pack.setStatus(data.getStatus());
if (data.getStatus().equals(Status.DELETED.name())) {
pack.setDeactivatedAt(data.getDeactivatedAt() != null ? data.getDeactivatedAt().toString() : null);
}
packages.getPackages().add(pack);
});
response.setPackages(packages);
return ResponseEntity.ok(response);
} else {
throw clientExcp(SignalError.ERROR_019, XmlErrorFormat.MonitoringAllPackagesError);
}
} catch (Exception e) {
if (e instanceof SignalClientException) {
throw (SignalClientException) e;
}
throw serverExcp(SignalError.ERROR_013, XmlErrorFormat.MonitoringAllPackagesError, e);
}
}
public ResponseEntity<MonitoringPackageDeleteResponse> deletePackage(String username, String packageName) {
try {
if (!signalService.isPackageExists(username, packageName)) {
throw clientExcp(SignalError.ERROR_016, XmlErrorFormat.MonitoringPackageDeleteError, packageName);
}
LocalDateTime deactivatedAt = LocalDateTime.now();
signalService.deletePackage(username, packageName, deactivatedAt);
MonitoringPackageDeleteResponse response = new MonitoringPackageDeleteResponse();
response.setStatus("Deleted");
response.setPackageName(packageName);
response.setDeactivatedAt(deactivatedAt.toString());
signalRestService.deletePackage(username, packageName);
log.info("Package {} deleted", packageName);
return ResponseEntity.ok(response);
} catch (Exception e) {
if (e instanceof SignalClientException) {
throw (SignalClientException) e;
}
throw serverExcp(SignalError.ERROR_017, XmlErrorFormat.MonitoringPackageDeleteError, e);
}
}
public ResponseEntity<MonitoringReportListResponse> getReportList(String username, String packageName) {
try {
if (signalService.isPackagesByStatusExist(username, packageName, Status.PROCESSING)) {
throw clientExcp(SignalError.ERROR_018, XmlErrorFormat.MonitoringReportListError, packageName);
}
if (signalService.isPackagesByStatusExist(username, packageName, Status.REJECTED)) {
throw clientExcp(SignalError.ERROR_020, XmlErrorFormat.MonitoringReportListError, packageName);
}
if (signalService.isPackagesByStatusExist(username, packageName, Status.ERROR)) {
throw clientExcp(SignalError.ERROR_021, XmlErrorFormat.MonitoringReportListError, packageName);
}
if (signalService.isPackagesByStatusExist(username, packageName, Status.DELETED)) {
throw clientExcp(SignalError.ERROR_022, XmlErrorFormat.MonitoringReportListError, packageName);
}
if (!signalService.isPackagesByStatusExist(username, packageName, Status.ON_MONITORING)) {
throw clientExcp(SignalError.ERROR_012, XmlErrorFormat.MonitoringReportListError, packageName);
}
String memberCode = username.substring(0, 6);
String filePrefix = (memberCode + "_" + packageName).toUpperCase();
String dirPath = config.getLoadDir() + File.separator + memberCode;
File folder = new File(dirPath);
MonitoringReportListResponse.Reports reports = new MonitoringReportListResponse.Reports();
if (folder.exists() && folder.isDirectory()) {
File[] files = folder.listFiles();
if (files != null) {
for (File file : files) {
if (file.isFile() && file.getName().startsWith(filePrefix)) {
reports.getReports().add(file.getName());
}
}
}
}
MonitoringReportListResponse response = new MonitoringReportListResponse();
response.setPackageName(packageName);
response.setReports(reports);
return ResponseEntity.ok(response);
} catch (Exception e) {
if (e instanceof SignalClientException) {
throw (SignalClientException) e;
}
throw serverExcp(SignalError.ERROR_013, XmlErrorFormat.MonitoringReportListError, e);
}
}
public ResponseEntity<Resource> downloadFile(String fileName) {
try {
String memberCode = CTUtil.getMemberCodeFromFileName(fileName);
if (memberCode == null) {
throw clientExcp(SignalError.ERROR_014, XmlErrorFormat.MonitoringDownloadError, fileName);
}
String dirPath = config.getLoadDir() + File.separator + memberCode;
File folder = new File(dirPath);
if (folder.exists() && folder.isDirectory()) {
File file = new File(dirPath + File.separator + fileName);
if (file.exists() && file.isFile()) {
return ResponseEntity.ok().body(new FileUrlResource(file.getPath()));
}
} else {
throw clientExcp(SignalError.ERROR_014, XmlErrorFormat.MonitoringDownloadError, fileName);
}
throw clientExcp(SignalError.ERROR_014, XmlErrorFormat.MonitoringDownloadError, fileName);
} catch (Exception e) {
if (e instanceof SignalClientException) {
throw (SignalClientException) e;
}
throw serverExcp(SignalError.ERROR_013, XmlErrorFormat.MonitoringDownloadError, e);
}
}
private SignalFacade getCurrentProxy() {
if (self == null) {
self = (SignalFacade) AopContext.currentProxy();
}
return self;
}
}

View file

@ -0,0 +1,280 @@
package ru.nbch.credit_tracker.facade;
import java.io.File;
import java.io.IOException;
import java.time.LocalDateTime;
import java.util.List;
import javax.xml.stream.XMLStreamException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import org.springframework.core.io.FileUrlResource;
import org.springframework.core.io.Resource;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import ru.nbch.credit_tracker.component.MonitoringRequestValidator;
import ru.nbch.credit_tracker.config.settings.CreditTrackerSettings;
import ru.nbch.credit_tracker.entities.app.UserPackage;
import ru.nbch.credit_tracker.entities.user_request.active_packages.MonitoringActivePackagesRequest;
import ru.nbch.credit_tracker.entities.user_request.all_packages.MonitoringAllPackagesRequest;
import ru.nbch.credit_tracker.entities.user_response.active_packages.MonitoringActivePackagesResponse;
import ru.nbch.credit_tracker.entities.user_response.active_packages.Package;
import ru.nbch.credit_tracker.entities.user_response.active_packages.Packages;
import ru.nbch.credit_tracker.entities.user_response.all_packages.MonitoringAllPackagesResponse;
import ru.nbch.credit_tracker.entities.user_response.all_packages.PackageAll;
import ru.nbch.credit_tracker.entities.user_response.all_packages.PackagesAll;
import ru.nbch.credit_tracker.entities.user_response.success.MonitoringPackageDeleteResponse;
import ru.nbch.credit_tracker.entities.user_response.success.MonitoringReportListResponse;
import ru.nbch.credit_tracker.enums.SignalError;
import ru.nbch.credit_tracker.enums.Status;
import ru.nbch.credit_tracker.enums.XmlErrorFormat;
import ru.nbch.credit_tracker.exceptions.SignalClientException;
import ru.nbch.credit_tracker.model.MonitoringValidationError;
import ru.nbch.credit_tracker.model.ValidationResult;
import ru.nbch.credit_tracker.service.SignalServiceV2;
import ru.nbch.credit_tracker.service.scoring.AuthorizationService;
import ru.nbch.credit_tracker.service.signal.SignalRestService;
import ru.nbch.credit_tracker.service.task.PipelinePackageManager;
import ru.nbch.credit_tracker.utils.CTUtil;
import ru.nbch.credit_tracker.utils.LogUtil;
import ru.nbch.credit_tracker.utils.ZipUtils;
import static ru.nbch.credit_tracker.utils.error.ExceptionUtils.clientExcp;
import static ru.nbch.credit_tracker.utils.error.ExceptionUtils.serverExcp;
@Service
@Slf4j
public class SignalFacadeV3 {
private final MonitoringRequestValidator monitoringRequestValidator;
private final CreditTrackerSettings config;
private final SignalServiceV2 signalService;
private final AuthorizationService authorizationService;
private final PipelinePackageManager pipelinePackageManager;
private final SignalRestService signalRestService;
public SignalFacadeV3(MonitoringRequestValidator monitoringRequestValidator,
CreditTrackerSettings config,
SignalServiceV2 signalService,
AuthorizationService authorizationService,
PipelinePackageManager pipelinePackageManager,
SignalRestService signalRestService) {
this.monitoringRequestValidator = monitoringRequestValidator;
this.config = config;
this.signalService = signalService;
this.authorizationService = authorizationService;
this.pipelinePackageManager = pipelinePackageManager;
this.signalRestService = signalRestService;
}
public void setupMonitoringRequest(MultipartFile file) {
log.debug("Start processing monitoring request");
monitoringRequestValidator.validateZipContent(file);
log.trace("Saving monitoring request to tmp file...");
String filePath;
try {
String folderPath = config.getLoadDir() + File.separator + "tmp_folder" + File.separator;
filePath = ZipUtils.writeTmpFile(folderPath, file);
} catch (IOException e) {
throw serverExcp(SignalError.ERROR_013, XmlErrorFormat.MonitoringError, e);
}
log.debug("File saved to {}", filePath);
ValidationResult validationResult = null;
try {
validationResult = monitoringRequestValidator.readAndValidateMonitoringRequest(filePath);
if (!validationResult.getValidationErrors().isEmpty()) {
MonitoringValidationError firstError = validationResult.getValidationErrors().get(0);
signalService.rejectPackage(validationResult.getUserId(), validationResult.getPackageName());
FileUtils.delete(new File(filePath));
throw firstError.getSignalException();
}
} catch (RuntimeException e) {
if (e.getCause() instanceof XMLStreamException) {
throw clientExcp(SignalError.ERROR_011, XmlErrorFormat.MonitoringError);
}
throw serverExcp(SignalError.ERROR_013, XmlErrorFormat.MonitoringError, e);
} catch (IOException e) {
log.warn("Error deleting tmp file", e);
}
String packageName = validationResult.getPackageName();
String userId = validationResult.getUserId();
String password = validationResult.getPassword();
try {
// проверка авторизации
AuthorizationService.AuthResult authResult = authorizationService.findAuthInfo(userId, password);
if (!authResult.isSuccess()) {
signalService.rejectPackage(userId, packageName);
throw clientExcp(authResult.getError(), XmlErrorFormat.MonitoringError);
}
// проверка на уникальность пакета
// if (signalService.isPackageNotUnique(userId, packageName)) {
// signalService.rejectPackage(userId, packageName);
// throw clientExcp(SignalError.ERROR_007, XmlErrorFormat.MonitoringError);
// }
Long packageId = signalService.registerPackage(userId, packageName);
LogUtil.addMdcLogKey("%s %d".formatted(packageName, packageId));
log.info("Package registered. PackageId {}. Status: {}", packageId, Status.PROCESSING.name());
pipelinePackageManager.buildAndRun(packageId, filePath);
} catch (Exception e) {
CTUtil.removeFile(filePath);
if (e instanceof SignalClientException) {
throw (SignalClientException) e;
}
throw serverExcp(SignalError.ERROR_013, XmlErrorFormat.MonitoringError, e);
}
}
public ResponseEntity<MonitoringActivePackagesResponse> findActivePackages(MonitoringActivePackagesRequest activePackagesRequest) throws Exception {
AuthorizationService.AuthResult authResult = authorizationService.findAuthInfo(activePackagesRequest.getUserId(), activePackagesRequest.getPassword());
if (!authResult.isSuccess()) {
throw clientExcp(authResult.getError(), XmlErrorFormat.MonitoringActivePackagesError);
}
List<UserPackage> activePackages = signalService.findActivePackages(activePackagesRequest.getUserId());
if (activePackages.isEmpty()) {
throw clientExcp(SignalError.ERROR_015, XmlErrorFormat.MonitoringActivePackagesError);
}
MonitoringActivePackagesResponse response = new MonitoringActivePackagesResponse();
response.setStatus("OK");
response.setMembercode(activePackagesRequest.getUserId().substring(0, 6));
Packages packages = new Packages();
activePackages.forEach(data -> {
Package pack = new Package();
pack.setPackageName(data.getPackageName());
pack.setCreatedAt(data.getCreatedAt() != null ? data.getCreatedAt().toString() : null);
packages.getPackages().add(pack);
});
response.setPackages(packages);
return ResponseEntity.ok(response);
}
public ResponseEntity<MonitoringAllPackagesResponse> findAllPackages(MonitoringAllPackagesRequest allPackagesRequest) throws Exception {
AuthorizationService.AuthResult authResult = authorizationService.findAuthInfo(allPackagesRequest.getUserId(), allPackagesRequest.getPassword());
if (!authResult.isSuccess()) {
throw clientExcp(authResult.getError(), XmlErrorFormat.MonitoringAllPackagesError);
}
List<UserPackage> activePackages = signalService.findAllPackages(allPackagesRequest.getUserId());
if (activePackages.isEmpty()) {
throw clientExcp(SignalError.ERROR_019, XmlErrorFormat.MonitoringAllPackagesError);
}
MonitoringAllPackagesResponse response = new MonitoringAllPackagesResponse();
response.setStatus("OK");
response.setMembercode(allPackagesRequest.getUserId().substring(0, 6));
PackagesAll packages = new PackagesAll();
activePackages.forEach(data -> {
PackageAll pack = new PackageAll();
pack.setPackageName(data.getPackageName());
pack.setCreatedAt(data.getCreatedAt() != null ? data.getCreatedAt().toString() : null);
pack.setStatus(data.getStatus());
if (data.getStatus().equals(Status.DELETED.name())) {
pack.setDeactivatedAt(data.getDeactivatedAt() != null ? data.getDeactivatedAt().toString() : null);
}
packages.getPackages().add(pack);
});
response.setPackages(packages);
return ResponseEntity.ok(response);
}
public ResponseEntity<MonitoringPackageDeleteResponse> deletePackage(String username, String packageName) {
try {
if (!signalService.isPackageExists(username, packageName)) {
throw clientExcp(SignalError.ERROR_016, XmlErrorFormat.MonitoringPackageDeleteError, packageName);
}
LocalDateTime deactivatedAt = LocalDateTime.now();
signalService.deletePackage(username, packageName, deactivatedAt);
MonitoringPackageDeleteResponse response = new MonitoringPackageDeleteResponse();
response.setStatus("Deleted");
response.setPackageName(packageName);
response.setDeactivatedAt(deactivatedAt.toString());
signalRestService.deletePackage(username, packageName);
log.info("Package {} deleted", packageName);
return ResponseEntity.ok(response);
} catch (Exception e) {
if (e instanceof SignalClientException) {
throw (SignalClientException) e;
}
throw serverExcp(SignalError.ERROR_017, XmlErrorFormat.MonitoringPackageDeleteError, e);
}
}
public ResponseEntity<MonitoringReportListResponse> getReportList(String username, String packageName) {
try {
if (signalService.isPackagesByStatusExist(username, packageName, Status.PROCESSING)) {
throw clientExcp(SignalError.ERROR_018, XmlErrorFormat.MonitoringReportListError, packageName);
}
if (signalService.isPackagesByStatusExist(username, packageName, Status.REJECTED)) {
throw clientExcp(SignalError.ERROR_020, XmlErrorFormat.MonitoringReportListError, packageName);
}
if (signalService.isPackagesByStatusExist(username, packageName, Status.ERROR)) {
throw clientExcp(SignalError.ERROR_021, XmlErrorFormat.MonitoringReportListError, packageName);
}
if (signalService.isPackagesByStatusExist(username, packageName, Status.DELETED)) {
throw clientExcp(SignalError.ERROR_022, XmlErrorFormat.MonitoringReportListError, packageName);
}
if (!signalService.isPackagesByStatusExist(username, packageName, Status.ON_MONITORING)) {
throw clientExcp(SignalError.ERROR_012, XmlErrorFormat.MonitoringReportListError, packageName);
}
String memberCode = username.substring(0, 6);
String filePrefix = (memberCode + "_" + packageName).toUpperCase();
String dirPath = config.getLoadDir() + File.separator + memberCode;
File folder = new File(dirPath);
MonitoringReportListResponse.Reports reports = new MonitoringReportListResponse.Reports();
if (folder.exists() && folder.isDirectory()) {
File[] files = folder.listFiles();
if (files != null) {
for (File file : files) {
if (file.isFile() && file.getName().startsWith(filePrefix)) {
reports.getReports().add(file.getName());
}
}
}
}
MonitoringReportListResponse response = new MonitoringReportListResponse();
response.setPackageName(packageName);
response.setReports(reports);
return ResponseEntity.ok(response);
} catch (Exception e) {
if (e instanceof SignalClientException) {
throw (SignalClientException) e;
}
throw serverExcp(SignalError.ERROR_013, XmlErrorFormat.MonitoringReportListError, e);
}
}
public ResponseEntity<Resource> downloadFile(String fileName) {
try {
String memberCode = CTUtil.getMemberCodeFromFileName(fileName);
if (memberCode == null) {
throw clientExcp(SignalError.ERROR_014, XmlErrorFormat.MonitoringDownloadError, fileName);
}
String dirPath = config.getLoadDir() + File.separator + memberCode;
File folder = new File(dirPath);
if (folder.exists() && folder.isDirectory()) {
File file = new File(dirPath + File.separator + fileName);
if (file.exists() && file.isFile()) {
return ResponseEntity.ok().body(new FileUrlResource(file.getPath()));
}
} else {
throw clientExcp(SignalError.ERROR_014, XmlErrorFormat.MonitoringDownloadError, fileName);
}
throw clientExcp(SignalError.ERROR_014, XmlErrorFormat.MonitoringDownloadError, fileName);
} catch (Exception e) {
if (e instanceof SignalClientException) {
throw (SignalClientException) e;
}
throw serverExcp(SignalError.ERROR_013, XmlErrorFormat.MonitoringDownloadError, e);
}
}
}

View file

@ -1,19 +1,22 @@
package ru.nbch.credit_tracker.mapper.indic;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import ru.nbch.credit_tracker.service.indic.FlagsService;
import java.time.LocalDate;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import ru.nbch.credit_tracker.entities.indic.PaymentsFlag;
import ru.nbch.credit_tracker.service.indic.FlagsService;
@Mapper
public interface FlagsMapper {
List<FlagsService.RuPastDuarearData> findRuPastDuarearData(@Param("fids") List<Integer> fids);
List<FlagsService.FlagFidData> findFlagsDebt5000(@Param("fids") List<Integer> fids);
List<FlagsService.RuPaymtData> findRuPaymtData(@Param("fids") List<Integer> fids, @Param("reportedDate") LocalDate reportedDate);
List<FlagsService.FlagFidData> findFlag90ByPastDueArrear(@Param("fids") List<Integer> fids, @Param("reportedDate") LocalDate reportedDate);
List<PaymentsFlag> findFlagsDebt5000(@Param("fids") List<Integer> fids);
List<FlagsService.FlagFidData> findFlag90ByRuPaymt(@Param("fids") List<Integer> fids, @Param("reportedDate") LocalDate reportedDate);
List<PaymentsFlag> findFlag90ByPastDueArrear(@Param("fids") List<Integer> fids, @Param("reportedDate") LocalDate reportedDate);
List<PaymentsFlag> findFlag90ByRuPaymt(@Param("fids") List<Integer> fids, @Param("reportedDate") LocalDate reportedDate);
}

View file

@ -1,12 +0,0 @@
package ru.nbch.credit_tracker.mapper.indic;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import ru.nbch.credit_tracker.service.indic.PhoneService;
import java.util.List;
@Mapper
public interface PhoneMapper {
List<PhoneService.FidsData> findFids(@Param("phones") List<String> phones);
}

View file

@ -2,7 +2,7 @@ package ru.nbch.credit_tracker.mapper.signal;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import ru.nbch.credit_tracker.entities.dto.Hit;
import ru.nbch.credit_tracker.entities.app.Hit;
import java.util.List;

View file

@ -2,24 +2,29 @@ package ru.nbch.credit_tracker.mapper.signal;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import ru.nbch.credit_tracker.entities.dto.PhoneFidMap;
import ru.nbch.credit_tracker.entities.app.PhoneFidMap;
import ru.nbch.credit_tracker.entities.user_request.monitoring_request.Subject;
import java.util.Collection;
import java.util.List;
@Mapper
public interface PhoneFidMapMapper {
void registerSubjects(@Param("subjects") List<Subject> subjects, @Param("packageId") Long packageId);
void register(@Param("phoneFidMap") List<PhoneFidMap> phoneFidMap);
void registerJson(@Param("json") String json);
void registerWithId(@Param("phoneFidMap") List<PhoneFidMap> phoneFidMap);
List<PhoneFidMap> findFullPhoneFidMap(@Param("packageId") Long packageId, @Param("offset") int offset, @Param("limit") int limit);
void updatePhoneFidMaps(@Param("subjects") List<PhoneFidMap> subjects);
void updatePhoneFidMaps(@Param("subjects") Collection<PhoneFidMap> subjects);
void updateOne(@Param("subject") PhoneFidMap subject);
void updateFid(@Param("subjects") Collection<PhoneFidMap> subjects);
void updatePhoneFidMapsErrors(@Param("subjects") List<PhoneFidMap> subjects);
PhoneFidMap findPhoneFidMapById(@Param("id") Long packageId);
boolean hasNonNullFids(@Param("packageId") Long packageId);
boolean hasOldRow(@Param("packageId") Long packageId, @Param("daysCount") Integer daysCount);
}

View file

@ -2,7 +2,7 @@ package ru.nbch.credit_tracker.mapper.signal;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import ru.nbch.credit_tracker.entities.dto.UserPackage;
import ru.nbch.credit_tracker.entities.app.UserPackage;
import java.time.LocalDateTime;
import java.util.List;

View file

@ -0,0 +1,16 @@
package ru.nbch.credit_tracker.model;
public class DummyPayload implements StagePayload {
private final boolean poison;
public static DummyPayload POISON = new DummyPayload(true);
public DummyPayload(boolean poison) {
this.poison = poison;
}
@Override
public boolean isPoison() {
return poison;
}
}

View file

@ -0,0 +1,37 @@
package ru.nbch.credit_tracker.model;
import ru.nbch.credit_tracker.entities.app.PhoneFidMap;
import java.util.List;
public class FlagsPayload implements StagePayload {
private final Long packageId;
private final List<PhoneFidMap> phoneFidMaps;
private final boolean poison;
public FlagsPayload(Long packageId, List<PhoneFidMap> fidsMapByBatch) {
this.packageId = packageId;
this.phoneFidMaps = fidsMapByBatch;
this.poison = false;
}
public FlagsPayload(boolean poison) {
this.packageId = null;
this.phoneFidMaps = null;
this.poison = poison;
}
public Long getPackageId() {
return packageId;
}
public List<PhoneFidMap> getPhoneFidMaps() {
return phoneFidMaps;
}
@Override
public boolean isPoison() {
return poison;
}
}

View file

@ -0,0 +1,21 @@
package ru.nbch.credit_tracker.model;
import java.time.LocalDate;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@Builder
//todo подумать над неймингом класс
public class IdData {
private Integer fid;
private String serNum;
private String idNum;
private LocalDate issDate;
private String idType;
private String idType2;
private LocalDate fileSinceDate;
}

View file

@ -0,0 +1,30 @@
package ru.nbch.credit_tracker.model;
import ru.nbch.credit_tracker.exceptions.SignalException;
public class MonitoringValidationError {
private final int line;
private final String message;
private final SignalException signalException;
public MonitoringValidationError(int line,
String message,
SignalException signalException) {
this.line = line;
this.message = message;
this.signalException = signalException;
}
public int getLine() {
return line;
}
public String getMessage() {
return message;
}
public SignalException getSignalException() {
return signalException;
}
}

View file

@ -0,0 +1,20 @@
package ru.nbch.credit_tracker.model;
import java.time.LocalDate;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@Builder
//todo подумать над неймингом класс
public class NameData {
private Integer fid;
private String firstName;
private String lastName;
private String middleName;
private LocalDate birthDate;
private LocalDate fileSinceDate;
}

View file

@ -0,0 +1,16 @@
package ru.nbch.credit_tracker.model;
public class SendSignalsPayload implements StagePayload {
private String signalXmlPath;
public static final SendSignalsPayload POISON = new SendSignalsPayload(true);
private boolean poison;
public SendSignalsPayload(boolean poison) {
this.poison = poison;
}
@Override
public boolean isPoison() {
return poison;
}
}

View file

@ -0,0 +1,5 @@
package ru.nbch.credit_tracker.model;
public interface StagePayload {
boolean isPoison();
}

View file

@ -0,0 +1,17 @@
package ru.nbch.credit_tracker.model;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@Builder
public class SubjectProfile {
private String identifier;
private String phone;
private Integer fid;
private NameData nameData;
private IdData idData;
}

View file

@ -0,0 +1,4 @@
package ru.nbch.credit_tracker.model;
public interface TerminatePayload {
}

View file

@ -0,0 +1,51 @@
package ru.nbch.credit_tracker.model;
import java.util.ArrayList;
import java.util.List;
public class ValidationResult {
private String packageName;
private String userId;
private String password;
private List<MonitoringValidationError> validationErrors = new ArrayList<>();
public void setPackageName(String packageName) {
this.packageName = packageName;
}
public void setUserId(String userId) {
this.userId = userId;
}
public void setValidationErrors(List<MonitoringValidationError> validationErrors) {
this.validationErrors = validationErrors;
}
public ValidationResult() {
}
public String getPackageName() {
return packageName;
}
public String getUserId() {
return userId;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public List<MonitoringValidationError> getValidationErrors() {
return validationErrors;
}
public void addError(MonitoringValidationError error) {
this.validationErrors.add(error);
}
}

View file

@ -0,0 +1,30 @@
package ru.nbch.credit_tracker.model.impl;
import java.nio.file.Path;
import lombok.Getter;
import ru.nbch.credit_tracker.model.StagePayload;
@Getter
public class PartFilePayload implements StagePayload {
private final Path partPath;
private final Long packageId;
private final boolean poison;
public static final PartFilePayload POISON = new PartFilePayload(true);
public PartFilePayload(boolean poison) {
this.poison = poison;
this.partPath = null;
this.packageId = null;
}
public PartFilePayload(Path partPath, Long packageId) {
this.partPath = partPath;
this.packageId = packageId;
this.poison = false;
}
@Override
public boolean isPoison() {
return poison;
}
}

View file

@ -0,0 +1,16 @@
package ru.nbch.credit_tracker.model.impl;
import java.nio.file.Path;
import ru.nbch.credit_tracker.model.TerminatePayload;
public class SignalXmlFilePayload implements TerminatePayload {
private final Path signalXmlFilePath;
public SignalXmlFilePayload(Path signalXmlFilePath) {
this.signalXmlFilePath = signalXmlFilePath;
}
public Path getSignalXmlFilePath() {
return signalXmlFilePath;
}
}

View file

@ -0,0 +1,38 @@
package ru.nbch.credit_tracker.model.impl;
import java.util.List;
import ru.nbch.credit_tracker.model.StagePayload;
import ru.nbch.credit_tracker.model.SubjectProfile;
public class SubjectPayload implements StagePayload {
private final Long packageId;
private final List<SubjectProfile> subjects;
public static final SubjectPayload POISON = new SubjectPayload(true);
private final boolean poison;
public SubjectPayload(boolean poison) {
this.packageId = null;
this.subjects = null;
this.poison = poison;
}
public SubjectPayload(Long packageId, List<SubjectProfile> subjects) {
this.packageId = packageId;
this.subjects = subjects;
this.poison = false;
}
public Long getPackageId() {
return packageId;
}
public List<SubjectProfile> getSubjects() {
return subjects;
}
@Override
public boolean isPoison() {
return poison;
}
}

View file

@ -0,0 +1,150 @@
package ru.nbch.credit_tracker.repository;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.LocalDate;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.stereotype.Repository;
import ru.nbch.credit_tracker.entities.indic.Name;
@Repository
public class PhoneRepository {
private static final String SELCT_FIDS = """
SELECT
pn.number,
pn.fid,
pn.file_since_dt
FROM PHONE_NORM pn
JOIN (SELECT DISTINCT fid FROM NAME) nf
ON nf.fid = pn.fid
WHERE number in (:phones)
ORDER BY
pn.number ASC,
pn.file_since_dt DESC,
pn.fid DESC
WITH UR
""";
private static final String SELCT_FIDS_WITH_NAME = """
SELECT
pn.number,
pn.fid,
pn.file_since_dt,
nf.first,
nf.middle,
nf.name_1,
nf.birth_dt,
nf.file_since_dt as file_since_dt_name
FROM PHONE_NORM pn
JOIN NAME nf
ON nf.fid = pn.fid
WHERE number in (:phones) WITH UR
""";
private final NamedParameterJdbcTemplate jdbcTemplate;
public PhoneRepository(@Qualifier("jdbcTemplateIndic") NamedParameterJdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public List<FidsData> findFids(List<String> phones) {
MapSqlParameterSource params = new MapSqlParameterSource();
params.addValue("phones", phones);
return jdbcTemplate.query(SELCT_FIDS, params, (rs, rowNum) ->
new PhoneRepository.FidsData(rs.getInt("fid"), rs.getString("number"), rs.getObject("file_since_dt", LocalDate.class))
);
}
@Deprecated
public List<FidsDataWithName> findFidsWithName(List<String> phones) {
MapSqlParameterSource params = new MapSqlParameterSource();
params.addValue("phones", phones);
return jdbcTemplate.query(SELCT_FIDS_WITH_NAME, params, (rs, rowNum) ->
new PhoneRepository.FidsDataWithName(
rs.getInt("FID"),
rs.getString("NUMBER"),
getLocalDate(rs, "FILE_SINCE_DT"),
rs.getString("FIRST"),
rs.getString("NAME_1"),
rs.getString("MIDDLE"),
getLocalDate(rs, "BIRTH_DT"),
getLocalDate(rs, "FILE_SINCE_DT_NAME")
));
}
public List<Name> findNameByPhones(List<String> phones) {
MapSqlParameterSource params = new MapSqlParameterSource();
params.addValue("phones", phones);
return jdbcTemplate.query(SELCT_FIDS_WITH_NAME, params, (rs, rowNum) ->
Name.builder()
.fid(rs.getInt("FID"))
.number(rs.getString("NUMBER"))
.fileSinceDate(getLocalDate(rs, "FILE_SINCE_DT_NAME"))
.first(rs.getString("FIRST"))
.middle(rs.getString("MIDDLE"))
.last(rs.getString("NAME_1"))
.birthDate(getLocalDate(rs, "BIRTH_DT"))
.build());
}
private static LocalDate getLocalDate(ResultSet rs, String col) throws SQLException {
java.sql.Date d = rs.getDate(col);
return d != null ? d.toLocalDate() : null;
}
public List<FidsDataLong> findFidsLong(List<String> phones) {
MapSqlParameterSource params = new MapSqlParameterSource();
params.addValue("phones", phones);
return jdbcTemplate.query(SELCT_FIDS, params, (rs, rowNum) ->
new PhoneRepository.FidsDataLong(rs.getInt("fid"), Long.parseLong(rs.getString("number")), rs.getObject("file_since_dt", LocalDate.class))
);
}
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public static class FidsData {
private Integer fid;
private String phone;
private LocalDate fileSinceDate;
}
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public static class FidsDataWithName {
private Integer fid;
private String phone;
private LocalDate fileSinceDate;
private String firstName;
private String lastName;
private String middleName;
private LocalDate birthDate;
private LocalDate fileSinceDateName;
}
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public static class FidsDataLong {
private Integer fid;
private Long phone;
private LocalDate fileSinceDate;
}
}

View file

@ -6,7 +6,7 @@ import org.springframework.aop.framework.AopContext;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.stereotype.Service;
import ru.nbch.credit_tracker.config.mdc.SetAndClearMdc;
import ru.nbch.credit_tracker.entities.dto.UserPackage;
import ru.nbch.credit_tracker.entities.app.UserPackage;
import ru.nbch.credit_tracker.service.signal.SignalRestService;
import ru.nbch.credit_tracker.utils.CTUtil;

View file

@ -6,9 +6,9 @@ import org.slf4j.MDC;
import org.springframework.scheduling.concurrent.CustomizableThreadFactory;
import org.springframework.stereotype.Service;
import ru.nbch.credit_tracker.config.settings.CreditTrackerSettings;
import ru.nbch.credit_tracker.entities.dto.Hit;
import ru.nbch.credit_tracker.entities.dto.PhoneFidMap;
import ru.nbch.credit_tracker.entities.dto.UserPackage;
import ru.nbch.credit_tracker.entities.app.Hit;
import ru.nbch.credit_tracker.entities.app.PhoneFidMap;
import ru.nbch.credit_tracker.entities.app.UserPackage;
import ru.nbch.credit_tracker.entities.external_response.online_download.Inquiry;
import ru.nbch.credit_tracker.entities.external_response.online_download.Person;
import ru.nbch.credit_tracker.entities.external_response.online_download.Persons;

View file

@ -6,18 +6,16 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import ru.nbch.credit_tracker.builder.PersonFactoryBuilder;
import ru.nbch.credit_tracker.config.settings.CreditTrackerSettings;
import ru.nbch.credit_tracker.entities.dto.Hit;
import ru.nbch.credit_tracker.entities.dto.PhoneFidMap;
import ru.nbch.credit_tracker.entities.dto.UserPackage;
import ru.nbch.credit_tracker.entities.app.Hit;
import ru.nbch.credit_tracker.entities.app.PhoneFidMap;
import ru.nbch.credit_tracker.entities.app.UserPackage;
import ru.nbch.credit_tracker.entities.external_request.signals.Auth;
import ru.nbch.credit_tracker.entities.external_request.signals.Person;
import ru.nbch.credit_tracker.entities.external_request.signals.Signals;
import ru.nbch.credit_tracker.entities.external_response.monitoring_report_error.MonitoringReportErrors;
import ru.nbch.credit_tracker.entities.user_request.monitoring_request.MonitoringRequest;
import ru.nbch.credit_tracker.enums.Status;
import ru.nbch.credit_tracker.service.indic.FlagsService;
import ru.nbch.credit_tracker.service.indic.PersonService;
import ru.nbch.credit_tracker.service.indic.PhoneService;
import ru.nbch.credit_tracker.service.indic.*;
import ru.nbch.credit_tracker.service.signal.HitsService;
import ru.nbch.credit_tracker.service.signal.PhoneFidMapService;
import ru.nbch.credit_tracker.service.signal.UserPackageService;
@ -37,12 +35,15 @@ import java.util.function.Predicate;
@Slf4j
@Service
@Deprecated
//todo оставил для временного подсмотреть на старую логику
public class SignalService {
private final CreditTrackerSettings config;
private final HitsService hitsService;
private final UserPackageService userPackageService;
private final PhoneFidMapService phoneFidMapService;
private final PhoneService2 phoneService2;
private final PhoneService phoneService;
private final FlagsService flagsService;
private final PersonService personService;
@ -54,6 +55,7 @@ public class SignalService {
HitsService hitsService,
UserPackageService userPackageService,
PhoneFidMapService phoneFidMapService,
PhoneService2 phoneService2,
PhoneService phoneService,
FlagsService flagsService,
PersonService personService,
@ -62,6 +64,7 @@ public class SignalService {
this.hitsService = hitsService;
this.userPackageService = userPackageService;
this.phoneFidMapService = phoneFidMapService;
this.phoneService2 = phoneService2;
this.phoneService = phoneService;
this.flagsService = flagsService;
this.personService = personService;
@ -72,6 +75,10 @@ public class SignalService {
return userPackageService.isPackageNotUnique(request.getUserId(), request.getPackageName());
}
public boolean isPackageNotUnique(String userId, String packageName) {
return userPackageService.isPackageNotUnique(userId, packageName);
}
public Long registerPackage(MonitoringRequest request) {
UserPackage userPackage = UserPackage.builder()
.userId(request.getUserId())
@ -82,6 +89,16 @@ public class SignalService {
return userPackageService.insertPackage(userPackage);
}
public Long registerPackage(String userId, String packageName) {
UserPackage userPackage = UserPackage.builder()
.userId(userId)
.packageName(packageName)
.createdAt(LocalDateTime.now())
.status(Status.PROCESSING.name())
.build();
return userPackageService.insertPackage(userPackage);
}
public void registerSubjects(String filePath, Long packageId) {
log.info("Start registering subjects. PackageId: {}", packageId);
try (BufferedInputStream in = new BufferedInputStream(Files.newInputStream(Path.of(filePath)), 8 * 1024 * 1024)) {
@ -102,6 +119,25 @@ public class SignalService {
log.info("Subjects were registered. PackageId: {}", packageId);
}
/**
* Выгружает {@link PhoneFidMap} батчами. Передает обогащенные фидами и данными из Name {@link PhoneFidMap}
* на поиск флагов и формирование отчета в оф. сервис сигналов
* @param packageId id пакета, по которому идет выборка {@link PhoneFidMap}
*/
public void processSubjects(Long packageId) {
int offset = 0;
List<PhoneFidMap> batch;
do {
batch = phoneFidMapService.findFullPhoneFidMap(packageId, offset, batchSize);
if (!batch.isEmpty()) {
Map<Integer, PhoneFidMap> fidsMapByBatch = phoneService.defineFidsAndNameByPhone(batch);
// TODO process fidsMapByBatch here
}
offset += batchSize;
} while (!batch.isEmpty());
}
/**
* Обрабатывает все PhoneFidMap по packageId, выгружая их пачками и фильтруя по переданному фильтру
*/
@ -110,6 +146,7 @@ public class SignalService {
boolean noFidsFound = true;
int offset = 0;
List<PhoneFidMap> batch;
Map<Integer, PhoneFidMap> phonesByFidToProcessing = new HashMap<>();
do {
batch = phoneFidMapService.findFullPhoneFidMap(packageId, offset, batchSize);
@ -125,15 +162,32 @@ public class SignalService {
}
if (!filteredItems.isEmpty()) {
boolean fidsFound = updateFlags(filteredItems, packageId);
Map<Integer, PhoneFidMap> fidsMapByBatch = phoneService.defineFidByPhone(filteredItems);
phoneService.defineFidByPhone(filteredItems);
log.debug("Actual fids size by phone's batch: {}", fidsMapByBatch.size());
if (fidsMapByBatch.size() + phonesByFidToProcessing.size() <= batchSize) {
phonesByFidToProcessing.putAll(fidsMapByBatch);
} else {
//todo execute find name and id -> forming xml
boolean fidsFound = updateFlags(phonesByFidToProcessing, packageId);
if (noFidsFound) {
noFidsFound = !fidsFound;
}
phonesByFidToProcessing = new HashMap<>(fidsMapByBatch);
}
}
offset += batchSize;
} while (!batch.isEmpty());
if (!phonesByFidToProcessing.isEmpty()) {
boolean fidsFound = updateFlags(phonesByFidToProcessing, packageId);
if (noFidsFound) {
noFidsFound = !fidsFound;
}
}
return new UpdateResult(updated, noFidsFound);
}
@ -141,50 +195,37 @@ public class SignalService {
* Обрабатывает пачку PhoneFidMap: обновляет фиды и флаги просрочек
* @return fidsFound true - если хотя бы по одному номеру был найден фид, false - в противном случае
*/
public boolean updateFlags(List<PhoneFidMap> subjects, Long packageId) {
log.info("Start calculating fids and flags for subjects batch. PackageId: {}", packageId);
Map<String, Integer> fidsMap = phoneService.findFidsPhoneData(subjects);
LocalDateTime calculatedAt = LocalDateTime.now();
boolean fidsFound = fidsMap.values().stream().anyMatch(Objects::nonNull);
Map<Integer, FlagsService.FlagsData> flagsDataMap = flagsService.findFlags(fidsMap.values().stream().filter(Objects::nonNull).toList());
// TODO Добавить отдельный метод. Если не нашелся фид, надо обновить в базе fid = null и флаги = null calculated_at=now()
public boolean updateFlags(Map<Integer, PhoneFidMap> fidsMap, Long packageId) {
log.info("Start calculating flags for subjects batch. PackageId: {}", packageId);
Map<Integer, FlagsService.FlagsData> flagsDataMap = flagsService.findFlags(fidsMap.keySet().stream().toList());
List<PhoneFidMap> subjectToUpdate = new ArrayList<>();
for (Map.Entry<Integer, FlagsService.FlagsData> entry : flagsDataMap.entrySet()) {
Integer fid = entry.getKey();
FlagsService.FlagsData flagsData = entry.getValue();
PhoneFidMap phoneFidMap = fidsMap.get(fid);
subjects.forEach(subject -> {
Integer fid = fidsMap.get("9" + subject.getPhone());
if (fid != null) {
FlagsService.FlagsData flagsData = flagsDataMap.get(fid);
subject = PhoneFidMap.builder()
.id(subject.getId())
.packageId(packageId)
.subjectId(subject.getSubjectId())
.phone(subject.getPhone())
.fid(fid)
.flagDebt5000(flagsData.getFlagDebt5000())
.flag90days(flagsData.getFlag90days())
.calculatedAt(calculatedAt)
.build();
} else {
subject = PhoneFidMap.builder()
.id(subject.getId())
.packageId(packageId)
.subjectId(subject.getSubjectId())
.phone(subject.getPhone())
.calculatedAt(calculatedAt)
.build();
phoneFidMap.setFid(fid);
phoneFidMap.setFlagDebt5000(flagsData.getFlagDebt5000());
phoneFidMap.setFlag90days(flagsData.getFlag90days());
phoneFidMap.setCalculatedAt(LocalDateTime.now());
subjectToUpdate.add(phoneFidMap);
}
subjectToUpdate.add(subject);
});
int updateBatchSize = batchSize / 9; // 9 параметров на одну запись для обновления. Максимальное количество параметров ~65000
log.debug("Start updating subject's batch");
if (!subjectToUpdate.isEmpty()) {
int updateBatchSize = 7000; // 9 параметров на одну запись для обновления. Максимальное количество параметров ~65000
for (int j = 0; j < subjectToUpdate.size(); j += updateBatchSize) {
int endIndex = Math.min(j + updateBatchSize, subjectToUpdate.size());
phoneFidMapService.updatePhoneFidMaps(subjectToUpdate.subList(j, endIndex));
log.trace("Updated fids and flags for subject's batch from: {} to: {}. Total count: {}. PackageId: {}", j, endIndex, subjectToUpdate.size(), packageId);
}
}
log.debug("Complete updating subject's batch");
log.info("Finish calculating fids and flags for subjects. PackageId: {}. Batch size: {}", packageId, subjects.size());
return fidsFound;
log.info("Finish calculating flags for subjects. PackageId: {}. Batch size: {}", packageId, fidsMap.size());
return !fidsMap.isEmpty();
}
public void rejectPackage(MonitoringRequest request) {
@ -197,6 +238,16 @@ public class SignalService {
userPackageService.insertPackage(userPackage);
}
public void rejectPackage(String userId, String packageName) {
UserPackage userPackage = UserPackage.builder()
.userId(userId)
.packageName(packageName)
.createdAt(LocalDateTime.now())
.status(Status.REJECTED.name())
.build();
userPackageService.insertPackage(userPackage);
}
public void updatePackageStatus(Long packageId, Status status) {
userPackageService.updatePackageStatus(packageId, status);
}
@ -226,7 +277,7 @@ public class SignalService {
return filePath;
}
private Signals buildSignals(Long packageId) {
public Signals buildSignals(Long packageId) {
UserPackage userPackage = userPackageService.findPackage(packageId);
Signals signals = new Signals();
signals.setOnline("2"); // Всегда=2 (только онлайн-сигналы)

View file

@ -0,0 +1,70 @@
package ru.nbch.credit_tracker.service;
import java.time.LocalDateTime;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import ru.nbch.credit_tracker.entities.app.UserPackage;
import ru.nbch.credit_tracker.entities.user_request.monitoring_request.MonitoringRequest;
import ru.nbch.credit_tracker.enums.Status;
import ru.nbch.credit_tracker.service.signal.UserPackageService;
@Slf4j
@Service
public class SignalServiceV2 {
private final UserPackageService userPackageService;
public SignalServiceV2(UserPackageService userPackageService) {
this.userPackageService = userPackageService;
}
public boolean isPackageNotUnique(MonitoringRequest request) {
return userPackageService.isPackageNotUnique(request.getUserId(), request.getPackageName());
}
public boolean isPackageNotUnique(String userId, String packageName) {
return userPackageService.isPackageNotUnique(userId, packageName);
}
public Long registerPackage(String userId, String packageName) {
UserPackage userPackage = UserPackage.builder()
.userId(userId)
.packageName(packageName)
.createdAt(LocalDateTime.now())
.status(Status.PROCESSING.name())
.build();
return userPackageService.insertPackage(userPackage);
}
public void rejectPackage(String userId, String packageName) {
UserPackage userPackage = UserPackage.builder()
.userId(userId)
.packageName(packageName)
.createdAt(LocalDateTime.now())
.status(Status.REJECTED.name())
.build();
userPackageService.insertPackage(userPackage);
}
public List<UserPackage> findActivePackages(String userId) {
return userPackageService.findUserPackages(userId, Status.ON_MONITORING);
}
public List<UserPackage> findAllPackages(String userId) {
return userPackageService.findAllPackagesByUserId(userId);
}
public boolean isPackageExists(String username, String packageName) {
return userPackageService.isPackageExists(username, packageName, Status.ON_MONITORING);
}
public boolean isPackagesByStatusExist(String username, String packageName, Status status) {
return userPackageService.hasActivePackages(username, packageName, status);
}
public void deletePackage(String username, String packageName, LocalDateTime deactivatedAt) {
userPackageService.deletePackage(username, packageName, Status.DELETED, Status.ON_MONITORING, deactivatedAt);
}
}

View file

@ -5,7 +5,7 @@ import org.springframework.aop.framework.AopContext;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.stereotype.Service;
import ru.nbch.credit_tracker.config.mdc.SetAndClearMdc;
import ru.nbch.credit_tracker.entities.dto.UserPackage;
import ru.nbch.credit_tracker.entities.app.UserPackage;
import ru.nbch.credit_tracker.enums.Status;
import ru.nbch.credit_tracker.service.signal.SignalRestService;

View file

@ -1,18 +1,22 @@
package ru.nbch.credit_tracker.service;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.scheduling.concurrent.CustomizableThreadFactory;
import org.springframework.stereotype.Service;
import ru.nbch.credit_tracker.config.settings.CreditTrackerSettings;
import ru.nbch.credit_tracker.entities.app.PhoneFidMap;
import ru.nbch.credit_tracker.entities.app.UserPackage;
import ru.nbch.credit_tracker.enums.Status;
import ru.nbch.credit_tracker.service.signal.SignalRestService;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.scheduling.support.CronTrigger;
import org.springframework.stereotype.Service;
import ru.nbch.credit_tracker.config.settings.CreditTrackerSettings;
import ru.nbch.credit_tracker.entities.dto.UserPackage;
import ru.nbch.credit_tracker.enums.Status;
import ru.nbch.credit_tracker.mapper.signal.PhoneFidMapMapper;
import ru.nbch.credit_tracker.service.signal.SignalRestService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
/**
* Сервис периодического обновления фидов и флагов просрочек по пакетам находящимся в статусе ON_MONITORING
@ -26,40 +30,31 @@ public class UpdatingFlagsService {
private final SignalRestService signalRestService;
private final MonitoringErrorProcesingService monitoringErrorProcesingService;
private final Long daysToUpdateFlags;
private final String updateFlagsCronExpression;
private final PhoneFidMapMapper phoneFidMapMapper;
private final Long updateFlagsInterval;
public UpdatingFlagsService(SignalService signalService,
SignalRestService signalRestService,
MonitoringErrorProcesingService monitoringErrorProcesingService,
CreditTrackerSettings config,
PhoneFidMapMapper phoneFidMapMapper) {
CreditTrackerSettings config) {
this.signalService = signalService;
this.signalRestService = signalRestService;
this.monitoringErrorProcesingService = monitoringErrorProcesingService;
this.daysToUpdateFlags = config.getDaysToUpdateFlags() != null ? config.getDaysToUpdateFlags() : 1;
this.updateFlagsCronExpression = config.getUpdateFlagsCronExpression() != null ? config.getUpdateFlagsCronExpression() : "*/10 * * * * *";
this.phoneFidMapMapper = phoneFidMapMapper;
this.updateFlagsInterval = config.getUpdateFlagsInterval() != null ? config.getUpdateFlagsInterval() : 1;
}
public void run() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(1);
scheduler.initialize();
scheduler.schedule(this::update, new CronTrigger(updateFlagsCronExpression));
ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(new CustomizableThreadFactory("updating-flags-"));
scheduler.scheduleAtFixedRate(this::update, 0, updateFlagsInterval, TimeUnit.DAYS);
}
public void update() {
private void update() {
log.info("Start updating flags");
List<UserPackage> packages = signalService.findPackagesOnMonitoring();
for (UserPackage userPackage : packages) {
log.info("Updating flags for Package {}", userPackage.getPackageName());
MDC.put("ru.nbch.credit_tracker.log.mdc.key", "[" + userPackage.getPackageName() +"]");
log.info("Check on updating flags for Package {}", userPackage.getPackageName());
boolean hasOldRow = phoneFidMapMapper.hasOldRow(userPackage.getId(), daysToUpdateFlags.intValue());
log.info("There are {} old rows", hasOldRow ? "exist" : "doesn't exist");
if (hasOldRow) {
updateFlags(userPackage);
}
MDC.remove("ru.nbch.credit_tracker.log.mdc.key");
}
log.info("Finish updating flags");
@ -70,7 +65,8 @@ public class UpdatingFlagsService {
String packageName = userPackage.getPackageName();
try {
signalService.updatePackageStatus(packageId, Status.PROCESSING);
SignalService.UpdateResult updateResult = signalService.updateSubjects(packageId, null);
Predicate<PhoneFidMap> filter = subject -> subject.getCalculatedAt() != null && hasDaysPassed(subject.getCalculatedAt());
SignalService.UpdateResult updateResult = signalService.updateSubjects(packageId, filter);
if (updateResult.isUpdated()) {
if (signalRestService.setupMonitoring(packageId)) {
LocalDateTime monitoringAt = userPackage.getMonitoringAt() != null ? userPackage.getMonitoringAt() : LocalDateTime.now();

View file

@ -1,35 +1,70 @@
package ru.nbch.credit_tracker.service.indic;
import java.sql.ResultSet;
import java.sql.SQLException;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.stereotype.Service;
import ru.nbch.credit_tracker.mapper.indic.FlagsMapper;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.time.LocalDate;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.*;
import ru.nbch.credit_tracker.model.SubjectProfile;
@Service
public class FlagsService {
private static final Logger log = LoggerFactory.getLogger(FlagsService.class);
private final FlagsMapper mapper;
public FlagsService(FlagsMapper mapper) {
private static final String SELECT_RU_PASDUEARREAR_DATA =
"SELECT FID, ACC_SERIAL_NUM, REPORTED_DT, AMT_PAST_DUE, DAYS_PAST_DUE " +
"FROM RU_PASTDUEARREAR " +
"WHERE FID IN (:fids) " +
"ORDER BY FID, REPORTED_DT DESC " +
"WITH UR";
private static final String SELECT_RU_PAYMT_DATA =
"SELECT FID, DAYS_PAST_DUE, REPORTED_DT " +
"FROM RU_PAYMT " +
"WHERE FID IN (:fids) " +
"AND REPORTED_DT >= :reportedDate " +
"WITH UR";
private final NamedParameterJdbcTemplate jdbcTemplate;
public FlagsService(FlagsMapper mapper,
@Qualifier("jdbcTemplateIndic") NamedParameterJdbcTemplate jdbcTemplate) {
this.mapper = mapper;
this.jdbcTemplate = jdbcTemplate;
}
public Map<Integer, FlagsData> findFlags(List<Integer> fids) {
if (fids.isEmpty()) {
return new HashMap<>();
}
Map<Integer, FlagsData> result = new HashMap<>();
Map<Integer, Integer> flags5000 = calcFlag5000(fids);
Map<Integer, Integer> flags90DaysByPastDueArrear = calcFlag90DaysByPastDueArrear(fids);
Map<Integer, Integer> flags90DaysByRuPaymt = calcFlag90DaysByRuPaymt(fids);
List<Integer> distinctFids = fids.stream()
.distinct()
.sorted(Comparator.naturalOrder())
.toList();
log.debug("Fids count: {}", distinctFids.size());
Map<Integer, Integer> flags5000 = new HashMap<>();
Map<Integer, Integer> flags90DaysByPastDueArrear = new HashMap<>();
Map<Integer, Integer> flags90DaysByRuPaymt = new HashMap<>();
findFlagsRuPastDueArrear(distinctFids, flags5000, flags90DaysByPastDueArrear);
findFlagsRuPaymt(distinctFids, flags90DaysByRuPaymt);
Map<Integer, FlagsData> result = new HashMap<>();
fids.forEach(fid -> {
Integer flagDebt5000 = flags5000.get(fid);
if (flagDebt5000 == null) {
@ -37,7 +72,7 @@ public class FlagsService {
}
Integer flag90days = flags90DaysByPastDueArrear.get(fid);
if (flag90days == null) {
if (flag90days == null || flag90days == 0) {
flag90days = flags90DaysByRuPaymt.get(fid);
if (flag90days == null) {
flag90days = 0;
@ -49,52 +84,352 @@ public class FlagsService {
return result;
}
// TODO optimize
public void fillFlagsData(List<SubjectProfile> subjects) {
List<Integer> fids = subjects.stream().map(SubjectProfile::getFid).filter(Objects::nonNull).toList();
Map<Integer, FlagsService.FlagsData> fidsflagsDataMap = findFlags2(fids);
fidsflagsDataMap.forEach((fid, data) -> {
subjects.stream().filter(s-> s.getFid() != null && s.getFid().equals(fid)).forEach(phoneFidMap -> {
// phoneFidMap.setFlagDebt5000(data.getFlagDebt5000());
// phoneFidMap.setFlag90days(data.getFlag90days());
// phoneFidMap.setCalculatedAt(LocalDateTime.now());
});
});
subjects.stream().filter(s-> s.getFid() == null).forEach(phoneFidMap -> {
// phoneFidMap.setCalculatedAt(LocalDateTime.now());
});
}
/**
* Выбирает данные из RU_PASTDUEARREAR и ищет по ним флаги просрочек
* По FID из RU_PASTDUEARREAR выбираются все уникальные ACC_SERIAL_NUM с максимальной датой REPORTED_DT.
* Если по хотя бы одной такой записи AMT_PAST_DUE > 5000, то флаг устанавливается в 1.
* Если по хотя бы одной такой записи AMT_PAST_DUE > 5000, то flag500 устанавливается в 1.
* Если ни по одному счёту условие не выполняется или счетов вообще не найдено, флаг = 0.
*
* flag90Days = 1, если выполняется хотя бы одно из условий:
* В INDIC.RU_PASTDUEARREAR у данного FID есть запись с DAYS_PAST_DUE 90 и REPORTED_DT >= текущая дата 12 месяцев.
* Если ни по одной записи условия не выполняются или записей вообще не найдено, флаг = 0.
* @param fids - отсортирован по возрастанию фидов
* @param flag5000Map - рещультирующая мапа флагов просрочек debt5000
* @param flag90Days - рещультирующая мапа флагов просрочек 90_days
*/
private Map<Integer, Integer> calcFlag5000(List<Integer> fids) {
return mapper.findFlagsDebt5000(fids).stream()
.collect(Collectors.toMap(FlagFidData::getFid, FlagFidData::getFlag,
(existing, replacement) -> {
if (existing == 1) {
return existing;
} else {
return replacement;
private void findFlagsRuPastDueArrear(List<Integer> fids, Map<Integer, Integer> flag5000Map, Map<Integer, Integer> flag90Days) {
log.debug("Start calculating flag 5000 and 90 days by RU_PASTDUEARREAR");
List<RuPastDuarearData> ruPastDuarearDataList = mapper.findRuPastDuarearData(fids);
ruPastDuarearDataList.sort(Comparator.comparing(FlagsService.RuPastDuarearData::getFid) // сортируем сначала по фиду
.thenComparing(FlagsService.RuPastDuarearData::getReportedDate, Comparator.reverseOrder())); // Затем сортируем по REPORTED_DT по убыванию
LocalDate yearAgo = LocalDate.now().minusYears(1);
for (Integer fid : fids) {
Iterator<RuPastDuarearData> iterator = ruPastDuarearDataList.iterator();
BigInteger prevAccSerialNum = null;
while (iterator.hasNext()) {
FlagsService.RuPastDuarearData data = iterator.next();
if (fid.equals(data.getFid())) {
if (flag5000Map.get(fid) == null) { // Первая запись в списке с макс REPORTED_DT. Нас интересует только она для флага 5000
if (prevAccSerialNum == null || !prevAccSerialNum.equals(data.getAccSerialNum())) {
if (data.getAmtPastDue() != null && data.getAmtPastDue().compareTo(BigDecimal.valueOf(5000)) >= 0) {
flag5000Map.put(fid, 1);
}
}));
prevAccSerialNum = data.getAccSerialNum();
}
}
Integer flag90 = flag90Days.get(fid);
if (flag90 != null && flag90 == 1) {
iterator.remove();
break;
}
if (flag90 == null || flag90 == 0) {
if (data.getDaysPastDue() != null) {
boolean has90daysFlag = data.getDaysPastDue().compareTo(BigInteger.valueOf(90)) >= 0 && !data.getReportedDate().isBefore(yearAgo);
if (has90daysFlag) {
flag90Days.put(fid, 1);
iterator.remove();
break;
} else {
flag90Days.put(fid, 0);
iterator.remove();
}
} else {
iterator.remove();
}
}
}
}
// если мы так и не нашли флаги для этих фидов - выставляем их в 0
flag5000Map.putIfAbsent(fid, 0);
flag90Days.putIfAbsent(fid, 0);
}
log.debug("Complete calculating flag 5000 and 90 days by RU_PASTDUEARREAR");
}
/**
* Флаг = 1, если выполняется хотя бы одно из условий:
* В INDIC.RU_PASTDUEARREAR или RU_PAYMT у данного FID есть запись с DAYS_PAST_DUE 90 и REPORTED_DT >= текущая дата 12 месяцев.
* Выбирает данные из RU_PAYMT и ищет по ним флаги просрочек 90Days
*
* flag90Days = 1, если выполняется хотя бы одно из условий:
* В INDIC.RU_PAYMT у данного FID есть запись с DAYS_PAST_DUE 90 и REPORTED_DT >= текущая дата 12 месяцев.
* Если ни по одной записи условия не выполняются или записей вообще не найдено, флаг = 0.
* @param fids - отсортирован по возрастанию фидов
* @param flag90Days - рещультирующая мапа флагов просрочек 90_days
*/
private Map<Integer, Integer> calcFlag90DaysByPastDueArrear(List<Integer> fids) {
private void findFlagsRuPaymt(List<Integer> fids, Map<Integer, Integer> flag90Days) {
log.debug("Start calculating flag 90 days by RU_PAPAYMT");
LocalDate yearAgo = LocalDate.now().minusYears(1);
return mapper.findFlag90ByPastDueArrear(fids, yearAgo).stream()
.collect(Collectors.toMap(FlagFidData::getFid, FlagFidData::getFlag,
(existing, replacement) -> {
if (existing == 1) {
return existing;
} else {
return replacement;
}
}));
List<RuPaymtData> ruPaymtDataList = mapper.findRuPaymtData(fids, yearAgo);
ruPaymtDataList.sort(Comparator.comparing(RuPaymtData::getFid, Comparator.naturalOrder()));
for (Integer fid : fids) {
Iterator<RuPaymtData> iterator = ruPaymtDataList.iterator();
while (iterator.hasNext()) {
RuPaymtData data = iterator.next();
if (fid.equals(data.getFid())) {
Integer flag90 = flag90Days.get(fid);
if (flag90 != null && flag90 == 1) {
iterator.remove();
break;
}
private Map<Integer, Integer> calcFlag90DaysByRuPaymt(List<Integer> fids) {
LocalDate yearAgo = LocalDate.now().minusYears(1);
return mapper.findFlag90ByRuPaymt(fids, yearAgo).stream()
.collect(Collectors.toMap(FlagFidData::getFid, FlagFidData::getFlag,
(existing, replacement) -> {
if (existing == 1) {
return existing;
if (flag90 == null || flag90 == 0) {
if (data.getDaysPastDue() != null) {
boolean has90daysFlag = data.getDaysPastDue().compareTo(BigInteger.valueOf(90)) >= 0 && !data.getReportedDate().isBefore(yearAgo);
if (has90daysFlag) {
flag90Days.put(fid, 1);
iterator.remove();
break;
} else {
return replacement;
flag90Days.put(fid, 0);
iterator.remove();
}
} else {
iterator.remove();
}
}
}
}
flag90Days.putIfAbsent(fid, 0);
}
log.debug("Complete calculating flag 90 days by RU_PAPAYMT");
}
public Map<Integer, FlagsService.FlagsData> findFlags2(List<Integer> fids) {
if (fids.isEmpty()) {
return new HashMap<>();
}
List<Integer> distinctFids = fids.stream()
.distinct()
.sorted(Comparator.naturalOrder())
.toList();
log.debug("Fids count: {}", distinctFids.size());
Map<Integer, Integer> flags5000 = new HashMap<>();
Map<Integer, Integer> flags90DaysByPastDueArrear = new HashMap<>();
Map<Integer, Integer> flags90DaysByRuPaymt = new HashMap<>();
findFlagsRuPastDueArrear2(distinctFids, flags5000, flags90DaysByPastDueArrear);
findFlagsRuPaymt2(distinctFids, flags90DaysByRuPaymt);
Map<Integer, FlagsService.FlagsData> result = new HashMap<>();
fids.forEach(fid -> {
Integer flagDebt5000 = flags5000.get(fid);
if (flagDebt5000 == null) {
flagDebt5000 = 0;
}
Integer flag90days = flags90DaysByPastDueArrear.get(fid);
if (flag90days == null || flag90days == 0) {
flag90days = flags90DaysByRuPaymt.get(fid);
if (flag90days == null) {
flag90days = 0;
}
}
result.put(fid, new FlagsService.FlagsData(flagDebt5000, flag90days));
});
return result;
}
/**
* Выбирает данные из RU_PASTDUEARREAR и ищет по ним флаги просрочек
* По FID из RU_PASTDUEARREAR выбираются все уникальные ACC_SERIAL_NUM с максимальной датой REPORTED_DT.
* Если по хотя бы одной такой записи AMT_PAST_DUE > 5000, то flag500 устанавливается в 1.
* Если ни по одному счёту условие не выполняется или счетов вообще не найдено, флаг = 0.
* <p>
* flag90Days = 1, если выполняется хотя бы одно из условий:
* В INDIC.RU_PASTDUEARREAR у данного FID есть запись с DAYS_PAST_DUE 90 и REPORTED_DT >= текущая дата 12 месяцев.
* Если ни по одной записи условия не выполняются или записей вообще не найдено, флаг = 0.
*
* @param fids - отсортирован по возрастанию фидов
* @param flag5000Map - рещультирующая мапа флагов просрочек debt5000
* @param flag90Days - рещультирующая мапа флагов просрочек 90_days
*/
private void findFlagsRuPastDueArrear2(List<Integer> fids, Map<Integer, Integer> flag5000Map, Map<Integer, Integer> flag90Days) {
log.debug("Start calculating flag 5000 and 90 days by RU_PASTDUEARREAR");
MapSqlParameterSource params = new MapSqlParameterSource();
params.addValue("fids", fids);
jdbcTemplate.query(SELECT_RU_PASDUEARREAR_DATA, params, (ResultSet rs) -> {
Integer currentFid = null;
BigInteger prevAccSerialNum = null;
boolean processed5000ForCurrentId = false;
boolean processed90DaysForCurrentId = false;
LocalDate yearAgo = LocalDate.now().minusYears(1);
while (rs.next()) {
Integer fid = rs.getInt("FID");
// Переход к новому fid. Сброс состояния
if (!fid.equals(currentFid)) {
currentFid = fid;
prevAccSerialNum = null;
processed5000ForCurrentId = false;
processed90DaysForCurrentId = false;
// Инициализируем значения по умолчанию
flag5000Map.putIfAbsent(fid, 0);
flag90Days.putIfAbsent(fid, 0);
}
// Если для текущего FID уже обработали оба флага, пропускаем остальные записи
if (processed5000ForCurrentId && processed90DaysForCurrentId) {
continue;
}
// Обработка флага 5000
if (!processed5000ForCurrentId && flag5000Map.get(fid) == 0) {
BigInteger currentAccSerialNum = getBigInteger(rs, "ACC_SERIAL_NUM");
BigDecimal amtPastDue = getBigDecimal(rs, "AMT_PAST_DUE");
if (prevAccSerialNum == null || !prevAccSerialNum.equals(currentAccSerialNum)) {
if (amtPastDue != null && amtPastDue.compareTo(BigDecimal.valueOf(5000)) >= 0) {
flag5000Map.put(fid, 1);
processed5000ForCurrentId = true;
}
prevAccSerialNum = currentAccSerialNum;
}
}
// Обработка флага 90 дней
if (!processed90DaysForCurrentId) {
Integer currentFlag90 = flag90Days.get(fid);
if (currentFlag90 != null && currentFlag90 == 1) {
processed90DaysForCurrentId = true;
continue;
}
if (currentFlag90 == null || currentFlag90 == 0) {
BigInteger days = getBigInteger(rs, "DAYS_PAST_DUE");
LocalDate reportedDate = rs.getObject("REPORTED_DT", LocalDate.class);
if (days != null) {
boolean has90daysFlag = days.compareTo(BigInteger.valueOf(90)) >= 0
&& reportedDate != null && !reportedDate.isBefore(yearAgo);
if (has90daysFlag) {
flag90Days.put(fid, 1);
processed90DaysForCurrentId = true;
} else {
flag90Days.put(fid, 0);
}
} else {
flag90Days.put(fid, 0);
}
}
}
}
return null;
});
log.debug("Complete calculating flag 5000 and 90 days by RU_PASTDUEARREAR");
}
/**
* Выбирает данные из RU_PAYMT и ищет по ним флаги просрочек 90Days
* <p>
* flag90Days = 1, если выполняется хотя бы одно из условий:
* В INDIC.RU_PAYMT у данного FID есть запись с DAYS_PAST_DUE 90 и REPORTED_DT >= текущая дата 12 месяцев.
* Если ни по одной записи условия не выполняются или записей вообще не найдено, флаг = 0.
*
* @param fids - отсортирован по возрастанию фидов
* @param flag90Days - рещультирующая мапа флагов просрочек 90_days
*/
private void findFlagsRuPaymt2(List<Integer> fids, Map<Integer, Integer> flag90Days) {
log.debug("Start calculating flag 90 days by RU_PAPAYMT");
LocalDate yearAgo = LocalDate.now().minusYears(1);
MapSqlParameterSource params = new MapSqlParameterSource();
params.addValue("fids", fids);
params.addValue("reportedDate", yearAgo);
jdbcTemplate.query(SELECT_RU_PAYMT_DATA, params, rs -> {
Integer currentFid = null;
boolean processed90DaysForCurrentId = false;
while (rs.next()) {
Integer fid = rs.getInt("FID");
// Переход к новому fid. Сброс состояния
if (!fid.equals(currentFid)) {
currentFid = fid;
processed90DaysForCurrentId = false;
// Инициализируем значения по умолчанию
flag90Days.putIfAbsent(fid, 0);
}
// Обработка флага 90 дней
if (!processed90DaysForCurrentId) {
Integer currentFlag90 = flag90Days.get(fid);
if (currentFlag90 != null && currentFlag90 == 1) {
processed90DaysForCurrentId = true;
continue;
}
if (currentFlag90 == null || currentFlag90 == 0) {
BigInteger days = getBigInteger(rs, "DAYS_PAST_DUE");
LocalDate date = rs.getObject("REPORTED_DT", LocalDate.class);
if (days != null) {
boolean has90daysFlag = days.compareTo(BigInteger.valueOf(90)) >= 0
&& date != null && !date.isBefore(yearAgo);
if (has90daysFlag) {
flag90Days.put(fid, 1);
processed90DaysForCurrentId = true;
} else {
flag90Days.put(fid, 0);
}
} else {
flag90Days.put(fid, 0);
}
}
}
}
return null;
});
log.debug("Complete calculating flag 90 days by RU_PAPAYMT");
}
private BigInteger getBigInteger(ResultSet rs, String columnName) {
try {
String value = rs.getString(columnName);
return value != null ? new BigInteger(value) : null;
} catch (SQLException e) {
return null;
}
}
private BigDecimal getBigDecimal(ResultSet rs, String columnName) {
try {
return rs.getBigDecimal(columnName);
} catch (SQLException e) {
return null;
}
}));
}
@Getter
@ -107,8 +442,23 @@ public class FlagsService {
@Getter
@Setter
public static class FlagFidData {
@AllArgsConstructor
@NoArgsConstructor
public static class RuPastDuarearData {
private Integer fid;
private Integer flag;
private BigInteger accSerialNum;
private LocalDate reportedDate;
private BigDecimal amtPastDue;
private BigInteger daysPastDue;
}
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public static class RuPaymtData {
private Integer fid;
private BigInteger daysPastDue;
private LocalDate reportedDate;
}
}

View file

@ -0,0 +1,37 @@
package ru.nbch.credit_tracker.service.indic;
import java.time.LocalDate;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import ru.nbch.credit_tracker.entities.indic.PaymentsFlag;
import ru.nbch.credit_tracker.mapper.indic.FlagsMapper;
@Slf4j
@Service
public class FlagsService2 {
private final FlagsMapper flagsMapper;
public FlagsService2(FlagsMapper flagsMapper) {
this.flagsMapper = flagsMapper;
}
public Map<Integer, Integer> findFlagsDebt5000(List<Integer> fids) {
return flagsMapper.findFlagsDebt5000(fids).stream()
.collect(Collectors.toMap(PaymentsFlag::getFid, PaymentsFlag::getFlag));
}
public Map<Integer, Integer> findFlag90ByPastDueArrear(List<Integer> fids, LocalDate date) {
return flagsMapper.findFlag90ByPastDueArrear(fids, date).stream()
.collect(Collectors.toMap(PaymentsFlag::getFid, PaymentsFlag::getFlag));
}
public Map<Integer, Integer> findFlag90ByRuPaymt(List<Integer> fids, LocalDate date) {
return flagsMapper.findFlag90ByRuPaymt(fids, date).stream()
.collect(Collectors.toMap(PaymentsFlag::getFid, PaymentsFlag::getFlag));
}
}

View file

@ -1,19 +1,20 @@
package ru.nbch.credit_tracker.service.indic;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import ru.nbch.credit_tracker.mapper.indic.PersonMapper;
import java.time.LocalDate;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import ru.nbch.credit_tracker.mapper.indic.PersonMapper;
import ru.nbch.credit_tracker.model.IdData;
import ru.nbch.credit_tracker.model.SubjectProfile;
@Slf4j
@Service
@ -87,6 +88,37 @@ public class PersonService {
return resultMap;
}
public void filledPassportsData(List<SubjectProfile> subjectProfiles) {
List<Integer> fids = subjectProfiles.stream().map(SubjectProfile::getFid).collect(Collectors.toList());
if (fids.isEmpty()) {
return;
}
List<PassportData> passports = mapper.findPassportData(fids);
Map<Integer, List<PassportData>> passportsMap = passports.stream()
.collect(Collectors.groupingBy(PassportData::getFid));
subjectProfiles.forEach(subjectProfile -> {
PassportData passport = findPassport(passportsMap.get(subjectProfile.getFid()));
if (passport != null) {
IdData idProfile = IdData.builder()
.serNum(passport.getSerNum())
.idNum(passport.getIdNum())
.issDate(passport.getIssDate())
.idType(passport.getIdType())
.idType2(passport.getIdType2())
.fileSinceDate(passport.getFileSinceDate())
.build();
subjectProfile.setIdData(idProfile);
}
});
}
private boolean isRequiredPassportDataPresent(PassportData passportData) {
boolean isDocTypeNotNull = StringUtils.isNotBlank(passportData.idType) || StringUtils.isNotBlank(passportData.idType2);
return StringUtils.isNotBlank(passportData.idNum) && isDocTypeNotNull;
}
private PassportData findPassport(List<PassportData> passports) {
if (passports == null || passports.isEmpty()) {
return null;

View file

@ -0,0 +1,110 @@
package ru.nbch.credit_tracker.service.indic;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import ru.nbch.credit_tracker.mapper.indic.PersonMapper;
import ru.nbch.credit_tracker.model.IdData;
import ru.nbch.credit_tracker.model.SubjectProfile;
@Slf4j
@Service
public class PersonServiceV2 {
private final PersonMapper mapper;
public PersonServiceV2(PersonMapper mapper) {
this.mapper = mapper;
}
public void filledPassportsData(List<SubjectProfile> subjectProfiles) {
List<Integer> fids = subjectProfiles.stream().map(SubjectProfile::getFid).collect(Collectors.toList());
if (fids.isEmpty()) {
return;
}
List<PersonService.PassportData> passports = mapper.findPassportData(fids);
Map<Integer, List<PersonService.PassportData>> passportsMap = passports.stream()
.collect(Collectors.groupingBy(PersonService.PassportData::getFid));
subjectProfiles.forEach(subjectProfile -> {
PersonService.PassportData passport = findPassport(passportsMap.get(subjectProfile.getFid()));
if (passport != null) {
IdData idProfile = IdData.builder()
.serNum(passport.getSerNum())
.idNum(passport.getIdNum())
.issDate(passport.getIssDate())
.idType(passport.getIdType())
.idType2(passport.getIdType2())
.fileSinceDate(passport.getFileSinceDate())
.build();
subjectProfile.setIdData(idProfile);
}
});
}
//todo подлежит рефакторингу
private PersonService.PassportData findPassport(List<PersonService.PassportData> passports) {
if (passports == null || passports.isEmpty()) {
return null;
}
List<PersonService.PassportData> documents = passports.stream()
.filter(this::isDUL)
.toList(); // берем только ДУЛ
if (documents.isEmpty()) {
return null;
}
// Отдаем приоритет паспортам РФ
PersonService.PassportData passport = documents.stream()
.filter(p -> "21".equals(p.getIdType2()) || "21".equals(p.getIdType()))
.sorted(
Comparator.comparing(
PersonService.PassportData::getIssDate,
Comparator.nullsLast(Comparator.reverseOrder())
).thenComparing(
PersonService.PassportData::getFileSinceDate,
Comparator.nullsLast(Comparator.reverseOrder())
)
).findFirst().orElse(null);
if (passport != null) {
return passport;
}
// Если паспорт РФ не найден, смотри остальные ДУЛ'ы и берем первый
passport = documents.stream()
.sorted(
Comparator.comparing(
PersonService.PassportData::getIssDate,
Comparator.nullsLast(Comparator.reverseOrder())
).thenComparing(
PersonService.PassportData::getFileSinceDate,
Comparator.nullsLast(Comparator.reverseOrder())
)
).findFirst().orElse(null);
return passport;
}
//todo подлежит рефакторингу;
private boolean isDUL(PersonService.PassportData p) {
Integer idType;
Double idType2;
try {
idType = p.getIdType() != null ? Integer.parseInt(p.getIdType()) : null;
} catch (NumberFormatException e) {
idType = null;
}
try {
idType2 = p.getIdType2() != null ? Double.valueOf(p.getIdType2()) : null;
} catch (NumberFormatException e) {
idType2 = null;
}
return (idType != null && idType <= 30 && idType != 12) || (idType2 != null && (idType2 >= 1 || idType2 <= 999));
}
}

View file

@ -1,58 +1,269 @@
package ru.nbch.credit_tracker.service.indic;
import lombok.Getter;
import lombok.Setter;
import org.springframework.stereotype.Service;
import ru.nbch.credit_tracker.entities.dto.PhoneFidMap;
import ru.nbch.credit_tracker.mapper.indic.PhoneMapper;
import java.time.LocalDate;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import ru.nbch.credit_tracker.entities.app.PhoneFidMap;
import ru.nbch.credit_tracker.entities.indic.Name;
import ru.nbch.credit_tracker.repository.PhoneRepository;
@Slf4j
@Service
@Deprecated
//todo оставил для временного подсмотреть на старую логику
public class PhoneService {
private final PhoneMapper mapper;
private final PhoneRepository phoneRepository;
public PhoneService(PhoneMapper mapper) {
this.mapper = mapper;
public PhoneService(PhoneRepository phoneRepository) {
this.phoneRepository = phoneRepository;
}
/**
* Ищет фиды по номерам телефона
* @return Map: key - номер телефона; value - fid
*
* @return Map: key - fid; value - {@link PhoneFidMap}
*/
public Map<String, Integer> findFidsPhoneData(List<PhoneFidMap> subjects) {
List<String> phones = subjects.stream().map(subject -> "9" + subject.getPhone()).toList();
return convertFidsDataToMap(mapper.findFids(phones));
}
public Map<Integer, PhoneFidMap> defineFidByPhone(List<PhoneFidMap> subjects) {
List<String> phones = subjects.stream().
map(subject -> "9" + subject.getPhone())
.toList();
private Map<String, Integer> convertFidsDataToMap(List<FidsData> fidsData) {
if (fidsData == null || fidsData.isEmpty()) {
return new HashMap<>();
}
return fidsData.stream()
.collect(Collectors.groupingBy(
FidsData::getPhone,
Collectors.collectingAndThen(
Collectors.maxBy(
Comparator.comparing(FidsData::getFileSinceDate)
.thenComparing(FidsData::getFid)
),
data -> data.map(FidsData::getFid).orElse(null)
subjects.sort(
Comparator.comparing(
PhoneFidMap::getPhone,
Comparator.naturalOrder()
)
));
);
List<PhoneRepository.FidsDataLong> phonesByFid = phoneRepository.findFidsLong(phones);
log.debug("Start merging fids");
Map<Integer, PhoneFidMap> phoneFidMap = merger(subjects, phonesByFid);
log.debug("End merging fids");
return phoneFidMap;
}
@Getter
@Setter
public static class FidsData {
private Integer fid;
private String phone;
private LocalDate fileSinceDate;
private Map<Integer, PhoneFidMap> merger(List<PhoneFidMap> subjects, List<PhoneRepository.FidsDataLong> phonesByFid) {
Map<Integer, PhoneFidMap> phoneFidMap = new HashMap<>();
if (phonesByFid.isEmpty()) {
return phoneFidMap;
}
int index = 0;
for (PhoneFidMap subject : subjects) {
Long normalizedPhone = Long.parseLong("9" + subject.getPhone());
while (index < phonesByFid.size()) {
PhoneRepository.FidsDataLong currentFidData = phonesByFid.get(index);
if (currentFidData.getPhone() < normalizedPhone) {
index++;
continue;
}
if (currentFidData.getPhone().equals(normalizedPhone)) {
phoneFidMap.put(currentFidData.getFid(), subject);
index++;
break;
}
break;
}
if (index >= phonesByFid.size()) {
break;
}
}
return phoneFidMap;
}
public void defineFidsAndNameByPhone2(List<PhoneFidMap> subjects) {
List<String> phones = subjects.stream().
map(subject -> "9" + subject.getPhone())
.toList();
subjects.sort(
Comparator.comparing(
PhoneFidMap::getPhone,
Comparator.naturalOrder()
)
);
List<PhoneRepository.FidsDataWithName> phonesByFid = phoneRepository.findFidsWithName(phones);
log.debug("Start merging fids and names");
Map<Integer, List<PhoneRepository.FidsDataWithName>> fidFidsDataMap = phonesByFid.stream()
.collect(Collectors.groupingBy(PhoneRepository.FidsDataWithName::getFid));
Map<String, Integer> phoneToFidsMap = new HashMap<>();
for (PhoneRepository.FidsDataWithName fidsData : phonesByFid) {
if (!phoneToFidsMap.containsKey(fidsData.getPhone())) {
phoneToFidsMap.put(fidsData.getPhone(), fidsData.getFid());
}
}
for (PhoneFidMap phoneFid : subjects) {
String phone = "9" + phoneFid.getPhone();
Integer fid = phoneToFidsMap.get(phone);
if (fid != null) {
phoneFid.setFid(fid);
List<PhoneRepository.FidsDataWithName> fidsDataWithNames = fidFidsDataMap.get(fid);
if (fidsDataWithNames != null) {
PhoneRepository.FidsDataWithName nameData = defineNameData(fidsDataWithNames);
if (nameData != null) {
phoneFid.setFirstName(nameData.getFirstName());
phoneFid.setMiddleName(nameData.getMiddleName());
phoneFid.setLastName(nameData.getLastName());
phoneFid.setBirthDate(nameData.getBirthDate());
phoneFid.setFileSinceDate(nameData.getFileSinceDateName());
}
}
phoneToFidsMap.remove(phone);
}
}
log.debug("End merging fids and names");
}
@Deprecated
private PhoneRepository.FidsDataWithName defineNameData(List<PhoneRepository.FidsDataWithName> dataList) {
PhoneRepository.FidsDataWithName currentData = null;
for (PhoneRepository.FidsDataWithName fidsData : dataList) {
if (currentData == null) {
currentData = fidsData;
continue;
}
LocalDate currentDate = currentData.getFileSinceDateName();
LocalDate newDate = fidsData.getFileSinceDateName();
if (currentDate != null && newDate != null) {
if (currentDate.isBefore(newDate)) {
currentData = fidsData;
}
} else if (currentDate == null) {
currentData = fidsData;
}
}
return currentData;
}
private Name defineNameDataV2(List<Name> dataList) {
Name currentData = null;
for (Name fidsData : dataList) {
if (currentData == null) {
currentData = fidsData;
continue;
}
LocalDate currentDate = currentData.getFileSinceDate();
LocalDate newDate = fidsData.getFileSinceDate();
if (currentDate != null && newDate != null) {
if (currentDate.isBefore(newDate)) {
currentData = fidsData;
}
} else if (currentDate == null) {
currentData = fidsData;
}
}
return currentData;
}
private Map<Integer, PhoneFidMap> merger2(List<PhoneFidMap> subjects, List<PhoneRepository.FidsData> phonesByFid) {
Map<Integer, PhoneFidMap> phoneFidMap = new HashMap<>();
Iterator<PhoneRepository.FidsData> iterator = phonesByFid.iterator();
String lastUniquePhone = null;
PhoneRepository.FidsData fidsData = null;
for (PhoneFidMap phoneFid : subjects) {
String normalizedPhone = 9 + phoneFid.getPhone();
while (iterator.hasNext()) {
fidsData = iterator.next();
if (StringUtils.isNotEmpty(lastUniquePhone) && lastUniquePhone.equals(fidsData.getPhone())) {
iterator.remove();
continue;
}
if (fidsData.getPhone().equals(normalizedPhone) || normalizedPhone.equals(lastUniquePhone)) {
lastUniquePhone = fidsData.getPhone();
iterator.remove();
break;
}
lastUniquePhone = fidsData.getPhone();
break;
}
if (fidsData != null && normalizedPhone.equals(lastUniquePhone)) {
phoneFidMap.put(fidsData.getFid(), phoneFid);
}
}
return phoneFidMap;
}
/**
* Ищет фиды по номерам телефона. Обогащает {@link PhoneFidMap} для найденных фидов данными из Name и фидом.
* @return Map: key - FID, value - {@link PhoneFidMap}
*/
public Map<Integer, PhoneFidMap> defineFidsAndNameByPhone(List<PhoneFidMap> subjects) {
Map<Integer, PhoneFidMap> phoneFidMap = new HashMap<>();
List<String> phones = subjects.stream().
map(subject -> "9" + subject.getPhone())
.toList();
subjects.sort(
Comparator.comparing(
PhoneFidMap::getPhone,
Comparator.naturalOrder()
)
);
List<PhoneRepository.FidsDataWithName> phonesByFid = phoneRepository.findFidsWithName(phones);
log.debug("Start merging fids and names");
Map<Integer, List<PhoneRepository.FidsDataWithName>> fidFidsDataMap = phonesByFid.stream()
.collect(Collectors.groupingBy(PhoneRepository.FidsDataWithName::getFid));
Map<String, Integer> phoneToFidsMap = new HashMap<>();
for (PhoneRepository.FidsDataWithName fidsData : phonesByFid) {
if (!phoneToFidsMap.containsKey(fidsData.getPhone())) {
phoneToFidsMap.put(fidsData.getPhone(), fidsData.getFid());
}
}
for (PhoneFidMap phoneFid : subjects) {
String phone = "9" + phoneFid.getPhone();
Integer fid = phoneToFidsMap.get(phone);
if (fid != null) {
phoneFid.setFid(fid);
List<PhoneRepository.FidsDataWithName> fidsDataWithNames = fidFidsDataMap.get(fid);
if (fidsDataWithNames != null) {
PhoneRepository.FidsDataWithName nameData = defineNameData(fidsDataWithNames);
if (nameData != null) {
phoneFid.setFirstName(nameData.getFirstName());
phoneFid.setMiddleName(nameData.getMiddleName());
phoneFid.setLastName(nameData.getLastName());
phoneFid.setBirthDate(nameData.getBirthDate());
phoneFid.setFileSinceDate(nameData.getFileSinceDateName());
}
fidFidsDataMap.remove(fid);
}
phoneFidMap.put(fid, phoneFid); // TODO можно перезатереть запись если у разных phone нашлись одинаковые фиды
phoneToFidsMap.remove(phone);
}
}
log.debug("End merging fids and names");
return phoneFidMap;
}
}

View file

@ -0,0 +1,49 @@
package ru.nbch.credit_tracker.service.indic;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import ru.nbch.credit_tracker.entities.indic.Name;
import ru.nbch.credit_tracker.model.SubjectProfile;
import ru.nbch.credit_tracker.repository.PhoneRepository;
import ru.nbch.credit_tracker.utils.CTUtil;
@Slf4j
@Service
public class PhoneService2 {
private final PhoneRepository phoneRepository;
private final Comparator<Name> byDateThenIdNullSafe =
Comparator.comparing(Name::getFileSinceDate, Comparator.nullsFirst(Comparator.naturalOrder()))
.thenComparing(Name::getFid);
public PhoneService2(PhoneRepository phoneRepository) {
this.phoneRepository = phoneRepository;
}
/**
* Поиск данных по таблицам PHONE_NORM и NAME по списку номеров телефонов
* @param subjects
* @return - отображение NAME к номеру телефону, отфильтрованному по логике
* FID с максимальным значением FILE_SINCE_DT. Если FILE_SINCE_DT оказался одинаковый, берется максимальный FID
*/
public Map<String, Name> findNameByNumber(List<SubjectProfile> subjects) {
List<String> phones = subjects.stream()
.map(subject -> CTUtil.normalizePhoneNumber(subject.getPhone()))
.toList();
List<Name> names = phoneRepository.findNameByPhones(phones);
return names.stream()
.collect(Collectors.toMap(
Name::getNumber,
Function.identity(),
BinaryOperator.maxBy(byDateThenIdNullSafe)
));
}
}

View file

@ -1,7 +1,7 @@
package ru.nbch.credit_tracker.service.signal;
import org.springframework.stereotype.Service;
import ru.nbch.credit_tracker.entities.dto.Hit;
import ru.nbch.credit_tracker.entities.app.Hit;
import ru.nbch.credit_tracker.mapper.signal.HitsMapper;
import java.util.List;

View file

@ -1,33 +1,84 @@
package ru.nbch.credit_tracker.service.signal;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.apache.ibatis.session.ExecutorType;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
import ru.nbch.credit_tracker.entities.dto.PhoneFidMap;
import ru.nbch.credit_tracker.entities.app.PhoneFidMap;
import ru.nbch.credit_tracker.entities.indic.Name;
import ru.nbch.credit_tracker.entities.user_request.monitoring_request.Subject;
import ru.nbch.credit_tracker.mapper.signal.PhoneFidMapMapper;
import java.util.List;
import ru.nbch.credit_tracker.model.SubjectProfile;
import ru.nbch.credit_tracker.utils.CTUtil;
@Service
public class PhoneFidMapService {
private final PhoneFidMapMapper mapper;
private final SqlSessionFactory sqlSessionFactory;
public PhoneFidMapService(PhoneFidMapMapper mapper) {
public PhoneFidMapService(PhoneFidMapMapper mapper,
@Qualifier("sqlSessionSignal") SqlSessionFactory sqlSessionFactory) {
this.mapper = mapper;
this.sqlSessionFactory = sqlSessionFactory;
}
@Deprecated
public void registerSubjects(List<Subject> subjects, Long packageId) {
mapper.registerSubjects(subjects, packageId);
}
/**
* Регистрирует в базе список субъектов переданных в запросе
*
* @param subjects - список субъектов
* @param packageId - уникальный идентификатор пакета
*/
public void register(List<SubjectProfile> subjects, Map<String, Name> nameByPhone, Long packageId) {
List<PhoneFidMap> phoneFidMaps = subjects.stream().map(subjectProfile -> {
Integer fid = Optional
.ofNullable(nameByPhone.get(CTUtil.normalizePhoneNumber(subjectProfile.getPhone())))
.map(Name::getFid)
.orElse(null);
return PhoneFidMap.builder()
.phone(subjectProfile.getPhone())
.subjectId(subjectProfile.getIdentifier())
.packageId(packageId)
.fid(fid)
.build();
}
).toList();
mapper.register(phoneFidMaps);
}
public List<PhoneFidMap> findFullPhoneFidMap(Long packageId, int offset, int limit) {
return mapper.findFullPhoneFidMap(packageId, offset, limit);
}
public void updatePhoneFidMaps(List<PhoneFidMap> subjects) {
public void updatePhoneFidMaps(Collection<PhoneFidMap> subjects) {
mapper.updatePhoneFidMaps(subjects);
}
public void updatePhoneFidMaps2(Collection<PhoneFidMap> subjects) {
final int batchSize = 1000;
try (SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH)) {
PhoneFidMapMapper mapper = session.getMapper(PhoneFidMapMapper.class);
int i = 0;
for (PhoneFidMap s : subjects) {
mapper.updateOne(s);
if (++i % batchSize == 0) session.flushStatements();
}
session.flushStatements();
session.commit();
}
}
public void updatePhoneFidMapsErrors(List<PhoneFidMap> subjects) {
mapper.updatePhoneFidMapsErrors(subjects);
}

View file

@ -70,6 +70,10 @@ public class SignalRestService {
*/
public boolean setupMonitoring(Long packageId) throws Exception {
String path = signalService.writeSignals(packageId);
return setupMonitoring(path);
}
public boolean setupMonitoring(String path) throws Exception {
Resource request = compressFile(path);
MultiValueMap<String, Resource> body = new LinkedMultiValueMap<>();
@ -95,8 +99,6 @@ public class SignalRestService {
} else {
log.error("неизвестная ошибка при постановке пакета на мониторинг в официальный сервис сигналов: {}", response.getBody());
}
CTUtil.removeFile(path);
CTUtil.removeFile(path + ".gz");
return false;
}
return false;

View file

@ -1,7 +1,7 @@
package ru.nbch.credit_tracker.service.signal;
import org.springframework.stereotype.Service;
import ru.nbch.credit_tracker.entities.dto.UserPackage;
import ru.nbch.credit_tracker.entities.app.UserPackage;
import ru.nbch.credit_tracker.enums.Status;
import ru.nbch.credit_tracker.mapper.signal.UserPackageMapper;

View file

@ -0,0 +1,6 @@
package ru.nbch.credit_tracker.service.task;
public interface Handler<X, Z> {
Z handle(X taskPayload);
default int preferredConcurrency() {return 10;}
}

View file

@ -0,0 +1,41 @@
package ru.nbch.credit_tracker.service.task;
import java.util.List;
import ru.nbch.credit_tracker.service.task.setup.IPipeLineTerminator;
public class Pipeline {
private Runnable starter;
private IPipeLineTerminator<?,?> terminator;
private List<Stage<?, ?>> stages;
public Pipeline() {
}
public Stage<?, ?> getLast() {
return stages.getLast();
}
public Runnable getStarter() {
return starter;
}
public void setStarter(Runnable starter) {
this.starter = starter;
}
public IPipeLineTerminator<?, ?> getTerminator() {
return terminator;
}
public void setTerminator(IPipeLineTerminator<?, ?> terminator) {
this.terminator = terminator;
}
public List<Stage<?, ?>> getStages() {
return stages;
}
public void setStages(List<Stage<?, ?>> stages) {
this.stages = stages;
}
}

View file

@ -0,0 +1,117 @@
package ru.nbch.credit_tracker.service.task;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.function.BiFunction;
import java.util.function.Supplier;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
import ru.nbch.credit_tracker.config.settings.CreditTrackerSettings;
import ru.nbch.credit_tracker.config.task.StageQueueFactory;
import ru.nbch.credit_tracker.model.impl.PartFilePayload;
import ru.nbch.credit_tracker.model.impl.SignalXmlFilePayload;
import ru.nbch.credit_tracker.model.impl.SubjectPayload;
import ru.nbch.credit_tracker.service.signal.SignalRestService;
import ru.nbch.credit_tracker.service.task.impl.FidIdEnricher;
import ru.nbch.credit_tracker.service.task.impl.FlagsEnricherV2;
import ru.nbch.credit_tracker.service.task.impl.SaveSubjectHandler;
import ru.nbch.credit_tracker.service.task.setup.IPipeLineStarter;
import ru.nbch.credit_tracker.service.task.setup.IPipeLineTerminator;
import ru.nbch.credit_tracker.service.task.setup.PipeLineBuilderV2;
import ru.nbch.credit_tracker.utils.LogUtil;
import ru.nbch.credit_tracker.utils.error.ExceptionUtils;
@Component
@Slf4j
public class PipelinePackageManager {
private final SignalRestService signalRestService;
private final FlagsEnricherV2 flagsEnricherV2;
private final StageQueueFactory<SubjectPayload> saveSubjectQueueFactory;
private final StageQueueFactory<SubjectPayload> idEnricherQueueFactory;
private final StageQueueFactory<PartFilePayload> sendSignalQueueFactory;
private final SaveSubjectHandler saveSubjectHandler;
private final FidIdEnricher fidIdEnricherHandler;
private final Supplier<IPipeLineTerminator<PartFilePayload, SignalXmlFilePayload>> pipelineTerminatorFactory;
private final BiFunction<Long, String, IPipeLineStarter<SubjectPayload>> pipelineStarterFactory;
private final CreditTrackerSettings creditTrackerSettings;
private final ExecutorService executorService = Executors.newFixedThreadPool(10, r -> {
Thread t = new Thread(r);
t.setName("pipe-starter-%d".formatted(t.threadId()));
return t;
});
private final Semaphore semaphore;
private final static int MAX_PARALLEL_REQUESTS = 3;
public PipelinePackageManager(SignalRestService signalRestService,
@Qualifier("saveSubjectQueueFactory") StageQueueFactory<SubjectPayload> saveSubjectQueueFactory,
@Qualifier("idEnricherQueueFactory") StageQueueFactory<SubjectPayload> idEnricherQueueFactory,
@Qualifier("partFileQueueFactory") StageQueueFactory<PartFilePayload> sendSignalQueueFactory,
SaveSubjectHandler saveSubjectHandler, FidIdEnricher fidIdEnricherHandler,
Supplier<IPipeLineTerminator<PartFilePayload, SignalXmlFilePayload>> pipelineTerminatorFactory,
BiFunction<Long, String, IPipeLineStarter<SubjectPayload>> pipelineStarterFactory,
FlagsEnricherV2 flagsEnricherV2,
CreditTrackerSettings creditTrackerSettings) {
this.signalRestService = signalRestService;
this.saveSubjectQueueFactory = saveSubjectQueueFactory;
this.idEnricherQueueFactory = idEnricherQueueFactory;
this.sendSignalQueueFactory = sendSignalQueueFactory;
this.saveSubjectHandler = saveSubjectHandler;
this.fidIdEnricherHandler = fidIdEnricherHandler;
this.pipelineTerminatorFactory = pipelineTerminatorFactory;
this.pipelineStarterFactory = pipelineStarterFactory;
this.flagsEnricherV2 = flagsEnricherV2;
this.creditTrackerSettings = creditTrackerSettings;
this.semaphore = new Semaphore(MAX_PARALLEL_REQUESTS);
}
public void buildAndRun(Long packageId, String pkgPath) throws Exception {
if (!semaphore.tryAcquire()) {
throw new RuntimeException("Too many concurrent requests");
}
log.info("throttler permits after acquire: {}", semaphore.availablePermits());
try {
Pipeline pipeline = PipeLineBuilderV2.first(saveSubjectQueueFactory.getObject(), idEnricherQueueFactory.getObject())
.handler(saveSubjectHandler)
.poison(SubjectPayload.POISON)
.handlerResult(flagsEnricherV2)
.andThen()
.stage(sendSignalQueueFactory.getObject())
.handler(fidIdEnricherHandler)
.poison(PartFilePayload.POISON)
.andThen()
.fillInitialQueue(pipelineStarterFactory.apply(packageId, pkgPath))
.pollFinishingQueue(pipelineTerminatorFactory.get())
.build();
executorService.submit(LogUtil.wrap(LogUtil.extractMdcMap(), pipeline.getStarter()));
executorService.submit(LogUtil.wrap(LogUtil.extractMdcMap(), pipeline.getTerminator()));
pipeline.getTerminator()
.doneFuture()
.orTimeout(creditTrackerSettings.getTimeout(), TimeUnit.MINUTES)
.whenComplete((v, e) -> {
try {
if (e != null) {
log.error(ExceptionUtils.getStackTrace(e));
return;
}
SignalXmlFilePayload res = (SignalXmlFilePayload) v;
log.trace("Signal file by path: {} is completed", res.getSignalXmlFilePath());
// signalRestService.setupMonitoring(res.getSignalXmlFilePath().toString());
} catch (Exception ex) {
log.error("pipeline finished with error{}", ExceptionUtils.getStackTrace(ex));
} finally {
log.info("packageId={}: pipeline finished", packageId);
pipeline.getStages().forEach(Stage::close);
semaphore.release();
}
});
} catch (Exception e) {
semaphore.release();
throw e;
}
}
}

View file

@ -0,0 +1,174 @@
package ru.nbch.credit_tracker.service.task;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.exception.ExceptionUtils;
import ru.nbch.credit_tracker.model.StagePayload;
import ru.nbch.credit_tracker.utils.LogUtil;
@Slf4j
public class Stage<X extends StagePayload, Z extends StagePayload> implements Runnable {
private final Handler<X, Z> dispatcher;
private final BlockingQueue<X> in;
private final BlockingQueue<Z> out;
private final Z POISON;
private final ExecutorService ioPool;
private Consumer<Z> consumer = null;
private final String dispatcherName;
private static final ThreadPoolExecutor.CallerRunsPolicy callerRunsPolicy
= new ThreadPoolExecutor.CallerRunsPolicy();
public Stage(Handler<X, Z> dispatcher,
BlockingQueue<X> in,
BlockingQueue<Z> out,
Z POISON) {
this.dispatcher = dispatcher;
this.in = in;
this.out = out;
this.dispatcherName = dispatcher.getClass().getSimpleName();
var mdcMap = LogUtil.extractMdcMap();
this.ioPool = new ThreadPoolExecutor(dispatcher.preferredConcurrency(),
dispatcher.preferredConcurrency(),
0L, TimeUnit.MINUTES,
new LinkedBlockingQueue<>(1),
r -> {
Thread t = new Thread(LogUtil.wrap(mdcMap, r));
t.setName("pipe-handler-%s-%d".formatted(dispatcherName, t.threadId()));
return t;
},
(r, executor) -> {
log.info("stage {}: all threads are working; switch to CallerRunsPolicy", dispatcherName);
callerRunsPolicy.rejectedExecution(r, executor);
}
);
this.POISON = POISON;
}
public void startRunning() {
ioPool.submit(this);
}
@Override
public void run() {
List<Future<Z>> inflight = new ArrayList<>();
try {
int counter = 0;
while (true) {
X b = takeQuiet(in, Duration.ofSeconds(5));
if (wasInterrupted()) {
break;
}
if (b == null) continue;
if (b.isPoison()) {
log.info("Got poison");
// дождаться всех in-flight задач
for (Future<Z> f : inflight) {
if (wasInterrupted()) {
log.info("stage {} was interrupted while waiting for tasks to finish",
dispatcher.getClass().getSimpleName());
break;
}
f.get();
}
putQuiet(out, POISON);
log.info("stage {} finished.", dispatcher.getClass().getSimpleName());
return;
}
counter++;
int finalCounter = counter;
Future<Z> f = ioPool.submit(() -> {
try {
if (wasInterrupted()) {
log.info("stage {} task {} interrupted...", dispatcher.getClass().getSimpleName(), finalCounter);
return null;
}
log.info("stage {} task {} started.", dispatcher.getClass().getSimpleName(), finalCounter);
Z p = dispatcher.handle(b);
putQuiet(out, p);
log.info("stage {} task {} ended.", dispatcher.getClass().getSimpleName(), finalCounter);
return p;
} catch (Exception e) {
log.error("{} ERROR: {}", dispatcherName, ExceptionUtils.getStackTrace(e));
return null;
}
}
);
inflight.add(f);
if ((counter % 20) == 0) {
inflight.removeIf(Future::isDone);
}
}
} catch (Exception e) {
log.error("Error for {}: {}; {}", dispatcherName, info(), ExceptionUtils.getStackTrace(e));
putQuiet(out, POISON);
}
}
private String info() {
ThreadPoolExecutor casted = (ThreadPoolExecutor) ioPool;
return "executor active tasks: %d; queue size: %d, completed tasks: %d".formatted(
casted.getActiveCount(),
casted.getQueue().size(),
casted.getCompletedTaskCount()
);
}
private boolean wasInterrupted() {
if (Thread.currentThread().isInterrupted()) {
log.warn("pipeline was interrupted...");
putQuiet(out, POISON);
return true;
}
return false;
}
public void close() {
log.info("Stage {} is getting shutdown", dispatcher.getClass().getSimpleName());
ioPool.shutdownNow(); // прерываем обработчики
}
void putQuiet(BlockingQueue<Z> q, Z b) {
try {
if (consumer != null) {
consumer.accept(b);
}
q.put(b);
} catch (InterruptedException e) {
log.error("Error for {}: {}; {}", dispatcherName, info(), ExceptionUtils.getStackTrace(e));
Thread.currentThread().interrupt();
}
}
X takeQuiet(BlockingQueue<X> q, Duration timeout) {
try {
return q.poll(timeout.toMillis(), TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
log.error("Error for {}: {}; {}", dispatcherName, info(), ExceptionUtils.getStackTrace(e));
Thread.currentThread().interrupt();
return null;
}
}
public BlockingQueue<X> getIn() {
return in;
}
public Consumer<Z> getConsumer() {
return consumer;
}
public void setConsumer(Consumer<Z> consumer) {
this.consumer = consumer;
}
}

View file

@ -0,0 +1,82 @@
package ru.nbch.credit_tracker.service.task.impl;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.util.List;
import java.util.UUID;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import ru.nbch.credit_tracker.builder.PersonFactoryBuilderV2;
import ru.nbch.credit_tracker.config.settings.CreditTrackerSettings;
import ru.nbch.credit_tracker.entities.external_request.signals.Person;
import ru.nbch.credit_tracker.model.impl.PartFilePayload;
import ru.nbch.credit_tracker.model.SubjectProfile;
import ru.nbch.credit_tracker.model.impl.SubjectPayload;
import ru.nbch.credit_tracker.service.indic.PersonServiceV2;
import ru.nbch.credit_tracker.service.task.Handler;
import ru.nbch.credit_tracker.service.xml.stax.SignalsStaxXmlWriterV2;
@Service
@Slf4j
public class FidIdEnricher implements Handler<SubjectPayload, PartFilePayload> {
private final PersonServiceV2 personService;
private final CreditTrackerSettings config;
public FidIdEnricher(PersonServiceV2 personService,
CreditTrackerSettings config) {
this.personService = personService;
this.config = config;
}
@Override
public PartFilePayload handle(SubjectPayload taskPayload) {
List<SubjectProfile> subjectsProfile = taskPayload.getSubjects();
log.debug("FidIdEnricher handle by packageId: {}; size: {}", taskPayload.getPackageId(), subjectsProfile.size());
personService.filledPassportsData(subjectsProfile);
List<Person> personOnProcessing = subjectsProfile.stream()
.filter(subjectProfile -> subjectProfile.getIdData() != null && subjectProfile.getNameData() != null)
.map(subjectProfile -> PersonFactoryBuilderV2.builder().personData(subjectProfile).build())
.toList();
log.debug("personOnProcessing size: {}", personOnProcessing.size());
Long packageId = taskPayload.getPackageId();
Path partPath = writeSignals(personOnProcessing, packageId);
return new PartFilePayload(partPath, packageId);
}
private Path writeSignals(List<Person> persons, Long packageId) {
Path dir = Paths.get(config.getLoadDir(), "tmp_folder");
try {
Files.createDirectories(dir);
} catch (IOException e) {
throw new RuntimeException(e);
}
String base = UUID.randomUUID() + "_output.xml";
log.debug("Write tmp xml by packageId: {}", packageId);
Path tmp = dir.resolve(base + ".part");
Path done = dir.resolve(base);
try (OutputStream os = Files.newOutputStream(tmp, StandardOpenOption.CREATE_NEW)) {
SignalsStaxXmlWriterV2 writer = new SignalsStaxXmlWriterV2(persons, null, config.getEncoding());
writer.writeWithStax(os); // перегрузи на OutputStream, если нужно
} catch (IOException e) {
throw new RuntimeException(e);
}
try {
Files.move(tmp, done, StandardCopyOption.ATOMIC_MOVE);
} catch (IOException e) {
throw new RuntimeException(e);
}
return done;
// try {
// mergerQueue.put(new PartFilePayload(done, packageId, null));
// } catch (InterruptedException e) {
// throw new RuntimeException(e);
// }
}
}

View file

@ -0,0 +1,139 @@
package ru.nbch.credit_tracker.service.task.impl;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.exception.ExceptionUtils;
import ru.nbch.credit_tracker.mapper.signal.PhoneFidMapMapper;
import ru.nbch.credit_tracker.model.SubjectProfile;
import ru.nbch.credit_tracker.model.impl.SubjectPayload;
import ru.nbch.credit_tracker.service.indic.FlagsService2;
@Slf4j
public class FlagsEnricher implements Runnable {
private final FlagsService2 flagsService;
private final PhoneFidMapMapper phoneFidMapMapper;
private BlockingQueue<SubjectPayload> in;
private final CompletableFuture<Void> done = new CompletableFuture<>();
private final ExecutorService ioPool;
private final ObjectMapper mapper = new ObjectMapper()
.registerModule(new JavaTimeModule())
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.setSerializationInclusion(JsonInclude.Include.NON_NULL);
public FlagsEnricher(FlagsService2 flagsService,
PhoneFidMapMapper phoneFidMapMapper) {
this.flagsService = flagsService;
this.phoneFidMapMapper = phoneFidMapMapper;
this.ioPool = new ThreadPoolExecutor(10,
10,
0L, TimeUnit.MINUTES,
new LinkedBlockingQueue<>(),
r -> {
Thread t = new Thread(r);
t.setName("queue-handler-%s-%d".formatted(this.getClass().getSimpleName(), t.threadId()));
return t;
}
);
}
@Override
public void run() {
List<Future<?>> inFlight = new ArrayList<>();
while (!Thread.currentThread().isInterrupted()) {
try {
SubjectPayload subjectPayload = in.poll(5, TimeUnit.SECONDS);
if (subjectPayload == null) {
continue;
}
if (subjectPayload.isPoison()) {
log.debug("Got poison");
for (Future<?> f : inFlight) {
f.get();
}
done.complete(null);
return;
}
inFlight.add(ioPool.submit(() -> process(subjectPayload)));
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
} catch (Exception e) {
log.error("Error: {}", ExceptionUtils.getStackTrace(e));
}
}
}
public void process(SubjectPayload subjectPayload) {
List<SubjectProfile> subjects = subjectPayload.getSubjects();
log.debug("FlagsEnricher handle by packageId: {}, size: {}", subjectPayload.getPackageId(), subjects.size());
List<Integer> fids = subjects.stream()
.map(SubjectProfile::getFid)
.filter(Objects::nonNull)
.toList();
LocalDate yearAgo = LocalDate.now().minusYears(1);
Map<Integer, Integer> flagsDebt5000 = flagsService.findFlagsDebt5000(fids);
Map<Integer, Integer> flag90ByPastDueArrear = flagsService.findFlag90ByPastDueArrear(fids, yearAgo);
Map<Integer, Integer> flag90ByRuPaymt = flagsService.findFlag90ByRuPaymt(fids, yearAgo);
String json;
try {
json = mapper.writeValueAsString(subjects.stream().map(subject -> {
Integer debt5000 = null;
Integer flagDays90 = null;
LocalDateTime calculatedAt = null;
if (subject.getFid() != null) {
debt5000 = flagsDebt5000.getOrDefault(subject.getFid(), 0);
int pastDue90 = flag90ByPastDueArrear.getOrDefault(subject.getFid(), 0);
int paymt90 = flag90ByRuPaymt.getOrDefault(subject.getFid(), 0);
flagDays90 = Math.max(pastDue90, paymt90);
calculatedAt = LocalDateTime.now();
}
Integer finalDebt500 = debt5000;
Integer finalFlagDays9 = flagDays90;
LocalDateTime finalCalculatedAt = calculatedAt;
return new HashMap<String, Object>() {{
put("subject_id", subject.getIdentifier());
put("fid", subject.getFid());
put("phone", subject.getPhone());
put("package_id", subjectPayload.getPackageId());
put("flag_debt5000", finalDebt500);
put("flag_90_12", finalFlagDays9);
put("calculated_at", finalCalculatedAt);
}};
}).toList());
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
phoneFidMapMapper.registerJson(json);
}
public void setPollingQueue(BlockingQueue<SubjectPayload> queue) {
this.in = queue;
}
public CompletableFuture<Void> doneFuture() {
return done;
}
}

View file

@ -0,0 +1,111 @@
package ru.nbch.credit_tracker.service.task.impl;
import com.fasterxml.jackson.annotation.JsonInclude;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import ru.nbch.credit_tracker.mapper.signal.PhoneFidMapMapper;
import ru.nbch.credit_tracker.model.SubjectProfile;
import ru.nbch.credit_tracker.model.impl.SubjectPayload;
import ru.nbch.credit_tracker.service.indic.FlagsService2;
@Slf4j
@Service
public class FlagsEnricherV2 implements Consumer<SubjectPayload> {
private final FlagsService2 flagsService;
private final PhoneFidMapMapper phoneFidMapMapper;
private final ExecutorService ioPool;
private final ObjectMapper mapper = new ObjectMapper()
.registerModule(new JavaTimeModule())
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.setSerializationInclusion(JsonInclude.Include.NON_NULL);
public FlagsEnricherV2(FlagsService2 flagsService,
PhoneFidMapMapper phoneFidMapMapper) {
this.flagsService = flagsService;
this.phoneFidMapMapper = phoneFidMapMapper;
this.ioPool = new ThreadPoolExecutor(10,
10,
0L, TimeUnit.MINUTES,
new LinkedBlockingQueue<>(100),
r -> {
Thread t = new Thread(r);
t.setName("flags-enricher-%d".formatted(t.threadId()));
return t;
},
new ThreadPoolExecutor.CallerRunsPolicy()
);
}
public void process(SubjectPayload subjectPayload) {
List<SubjectProfile> subjects = subjectPayload.getSubjects();
log.debug("FlagsEnricher handle by packageId: {}, size: {}", subjectPayload.getPackageId(), subjects.size());
List<Integer> fids = subjects.stream()
.map(SubjectProfile::getFid)
.filter(Objects::nonNull)
.toList();
LocalDate yearAgo = LocalDate.now().minusYears(1);
Map<Integer, Integer> flagsDebt5000 = flagsService.findFlagsDebt5000(fids);
Map<Integer, Integer> flag90ByPastDueArrear = flagsService.findFlag90ByPastDueArrear(fids, yearAgo);
Map<Integer, Integer> flag90ByRuPaymt = flagsService.findFlag90ByRuPaymt(fids, yearAgo);
String json;
try {
json = mapper.writeValueAsString(subjects.stream().map(subject -> {
Integer debt5000 = null;
Integer flagDays90 = null;
LocalDateTime calculatedAt = null;
if (subject.getFid() != null) {
debt5000 = flagsDebt5000.getOrDefault(subject.getFid(), 0);
int pastDue90 = flag90ByPastDueArrear.getOrDefault(subject.getFid(), 0);
int paymt90 = flag90ByRuPaymt.getOrDefault(subject.getFid(), 0);
flagDays90 = Math.max(pastDue90, paymt90);
calculatedAt = LocalDateTime.now();
}
Integer finalDebt500 = debt5000;
Integer finalFlagDays9 = flagDays90;
LocalDateTime finalCalculatedAt = calculatedAt;
return new HashMap<String, Object>() {{
put("subject_id", subject.getIdentifier());
put("fid", subject.getFid());
put("phone", subject.getPhone());
put("package_id", subjectPayload.getPackageId());
put("flag_debt5000", finalDebt500);
put("flag_90_12", finalFlagDays9);
put("calculated_at", finalCalculatedAt);
}};
}).toList());
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
phoneFidMapMapper.registerJson(json);
}
@Override
public void accept(SubjectPayload subjectPayload) {
ThreadPoolExecutor casted = (ThreadPoolExecutor) ioPool;
log.info("submitting payload. active tasks number: {}; queue size: {}", casted.getActiveCount(), casted.getQueue().size());
this.ioPool.submit(() -> {
process(subjectPayload);
});
}
}

View file

@ -0,0 +1,78 @@
package ru.nbch.credit_tracker.service.task.impl;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ru.nbch.credit_tracker.config.settings.CreditTrackerSettings;
import ru.nbch.credit_tracker.entities.user_request.monitoring_request.MonitoringRequest;
import ru.nbch.credit_tracker.model.SubjectProfile;
import ru.nbch.credit_tracker.model.impl.SubjectPayload;
import ru.nbch.credit_tracker.service.task.setup.IPipeLineStarter;
import ru.nbch.credit_tracker.service.xml.stax.StaxXmlProcessor;
import ru.nbch.credit_tracker.utils.error.ExceptionUtils;
public class KickPipeLineImpl implements IPipeLineStarter<SubjectPayload> {
private final Logger log = LoggerFactory.getLogger(getClass());
private final Long packageId;
private final String pkgPath;
private final StaxXmlProcessor xmlProcessor;
private final int BATCH_SIZE;
public KickPipeLineImpl(StaxXmlProcessor xmlProcessor, Long packageId, String pkgPath,
CreditTrackerSettings creditTrackerSettings) {
this.packageId = packageId;
this.pkgPath = pkgPath;
this.xmlProcessor = xmlProcessor;
this.BATCH_SIZE = creditTrackerSettings.getBatchSize();
}
@Override
public void startFillingQueue(BlockingQueue<SubjectPayload> firstQueue) {
log.info("Start registering subjects. PackageId: {}", packageId);
AtomicInteger counter = new AtomicInteger(0);
try (BufferedInputStream in = new BufferedInputStream(Files.newInputStream(Path.of(pkgPath)), 8 * 1024 * 1024)) {
xmlProcessor.read(in, MonitoringRequest.class,
request -> {
if (!request.getSubjects().getSubject().isEmpty()) {
List<SubjectProfile> subjectProfileBatch = request.getSubjects().getSubject().stream().map(subject ->
SubjectProfile.builder()
.identifier(subject.getId())
.phone(subject.getClientId())
.build()
).collect(Collectors.toList());
int count = counter.incrementAndGet();
log.trace("Reading subject's {} batch. Total count: {}. PackageId: {}", count, request.getSubjects().getSubject().size(), packageId);
try {
firstQueue.put(new SubjectPayload(packageId, subjectProfileBatch));
} catch (InterruptedException e) {
log.error("{}", ExceptionUtils.getStackTrace(e));
throw new RuntimeException(e);
}
request.getSubjects().getSubject().clear();
}
},
request -> request.getSubjects().getSubject().isEmpty()
,
BATCH_SIZE, "/MonitoringRequest/Subjects/Subject");
} catch (IOException e) {
log.error("{}", ExceptionUtils.getStackTrace(e));
} finally {
log.debug("Send poison pill");
try {
firstQueue.put(new SubjectPayload(true));
} catch (InterruptedException e) {
log.error("couldn't send poison: {}", ExceptionUtils.getStackTrace(e));
}
}
}
}

View file

@ -0,0 +1,59 @@
package ru.nbch.credit_tracker.service.task.impl;
import java.util.List;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import ru.nbch.credit_tracker.entities.indic.Name;
import ru.nbch.credit_tracker.mapper.signal.PhoneFidMapMapper;
import ru.nbch.credit_tracker.model.NameData;
import ru.nbch.credit_tracker.model.SubjectProfile;
import ru.nbch.credit_tracker.model.impl.SubjectPayload;
import ru.nbch.credit_tracker.service.indic.PhoneService2;
import ru.nbch.credit_tracker.service.task.Handler;
import ru.nbch.credit_tracker.utils.CTUtil;
/**
* Стадия для сопоставления FID к номеру телефона и регистрации пакета в базе данных.
* Так же выполняется поиск и обогащение перс.данными из NAME для дальнейшей обработки.
*/
@Service
@Slf4j
//todo опять нейминг класса, варианты: ResolveSubject; DefineIcrsSubject; IcrsMapping/Resolving; PackageSaver; Filler...
public class SaveSubjectHandler implements Handler<SubjectPayload, SubjectPayload> {
private final PhoneService2 phoneService2;
private final PhoneFidMapMapper phoneFidMapMapper;
public SaveSubjectHandler(PhoneService2 phoneService2,
PhoneFidMapMapper phoneFidMapMapper) {
this.phoneService2 = phoneService2;
this.phoneFidMapMapper = phoneFidMapMapper;
}
@Override
public SubjectPayload handle(SubjectPayload taskPayload) {
List<SubjectProfile> subjects = taskPayload.getSubjects();
Long packageId = taskPayload.getPackageId();
log.debug("SaveSubjectStage handle by packageId: {}, size of bucket for save: {}", taskPayload.getPackageId(), subjects.size());
Map<String, Name> nameByPhone = phoneService2.findNameByNumber(subjects);
//todo сделать один проход для установки FID для вставки в PHONE_FID_MAP и одновременного заполнения инфо из Name для дальнейшей обработки
subjects.forEach(subject -> {
Name name = nameByPhone.get(CTUtil.normalizePhoneNumber(subject.getPhone()));
if (name != null) {
NameData nameData = NameData.builder()
.middleName(name.getMiddle())
.firstName(name.getFirst())
.lastName(name.getLast())
.fileSinceDate(name.getFileSinceDate())
.birthDate(name.getBirthDate())
.build();
subject.setFid(name.getFid());
subject.setNameData(nameData);
}
});
//todo стоит ли делать имутабл для subject?
return new SubjectPayload(taskPayload.getPackageId(), subjects);
}
}

View file

@ -0,0 +1,148 @@
package ru.nbch.credit_tracker.service.task.impl;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import javax.xml.stream.XMLStreamWriter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.exception.ExceptionUtils;
import ru.nbch.credit_tracker.config.settings.CreditTrackerSettings;
import ru.nbch.credit_tracker.model.impl.PartFilePayload;
import ru.nbch.credit_tracker.model.impl.SignalXmlFilePayload;
import ru.nbch.credit_tracker.service.task.setup.IPipeLineTerminator;
@Slf4j
public final class XmlMergeCoordinator implements IPipeLineTerminator<PartFilePayload, SignalXmlFilePayload> {
private final Path mergedDir;
private final Charset encoding = Charset.forName("windows-1251");
private final int batchSize = 10;
private final CompletableFuture<SignalXmlFilePayload> done = new CompletableFuture<>();
private Path out;
private BlockingQueue<PartFilePayload> inQueue;
public XmlMergeCoordinator(CreditTrackerSettings config) {
this.mergedDir = Path.of(config.getLoadDir() + File.separator + "tmp_folder" + File.separator + "merged" + File.separator);
}
@Override
public void run() {
try {
Files.createDirectories(mergedDir);
} catch (IOException e) {
throw new RuntimeException(e);
}
while (!Thread.currentThread().isInterrupted()) {
try {
PartFilePayload part = inQueue.poll(5, TimeUnit.SECONDS);
if (part == null) {
continue;
}
if (part.isPoison()) {
log.debug("Got poison");
done.complete(new SignalXmlFilePayload(out));
return;
}
mergeBatch(Collections.singletonList(part));
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
} catch (Exception e) {
log.error("Error: {}", ExceptionUtils.getStackTrace(e));
}
}
}
private void mergeBatch(List<PartFilePayload> inputs) throws Exception {
log.debug("Starting merge batch; size: {}", inputs.size());
Long packageId = inputs.stream().findFirst().get().getPackageId();
String outName = "merged_" + packageId + ".xml";
if (out == null) {
out = mergedDir.resolve(outName);
}
XMLOutputFactory xof = XMLOutputFactory.newFactory();
try (OutputStream os = Files.newOutputStream(out, StandardOpenOption.CREATE, StandardOpenOption.APPEND)) {
XMLStreamWriter w = xof.createXMLStreamWriter(os, encoding.name());
for (PartFilePayload in : inputs) {
log.debug("Copy person from file {}", in.getPartPath());
copyPersonsChildren(in.getPartPath(), w);
}
w.flush();
w.close();
}
for (PartFilePayload p : inputs) Files.deleteIfExists(p.getPartPath());
// inputs.clear();
}
/**
* Копирует всё содержимое внутри <Persons>...</Persons> входного файла
* в текущий writer, но без повторного открытия/закрытия корневого тега.
*/
private void copyPersonsChildren(Path input, XMLStreamWriter w) throws Exception {
XMLInputFactory xif = XMLInputFactory.newFactory();
try (InputStream is = Files.newInputStream(input)) {
XMLStreamReader r = xif.createXMLStreamReader(is, encoding.name());
while (r.hasNext()) {
int t = r.next();
if (t == XMLStreamConstants.START_ELEMENT && "Persons".equals(r.getLocalName())) {
break;
}
}
int depth = 0; // 0 внутри корневого контейнера
while (r.hasNext()) {
int t = r.next();
if (t == XMLStreamConstants.START_ELEMENT) {
depth++;
writeStart(w, r);
} else if (t == XMLStreamConstants.CHARACTERS) {
if (!r.isWhiteSpace()) w.writeCharacters(r.getText());
} else if (t == XMLStreamConstants.CDATA) {
w.writeCData(r.getText());
} else if (t == XMLStreamConstants.END_ELEMENT) {
if (depth > 0) {
w.writeEndElement();
depth--;
} else if ("Persons".equals(r.getLocalName())) {
break;
}
}
}
r.close();
}
}
private void writeStart(XMLStreamWriter w, XMLStreamReader r) throws XMLStreamException {
w.writeStartElement(r.getLocalName());
for (int i = 0; i < r.getAttributeCount(); i++) {
w.writeAttribute(r.getAttributeLocalName(i), r.getAttributeValue(i));
}
}
@Override
public void setPollingQueue(BlockingQueue<PartFilePayload> mergerQueue) {
this.inQueue = mergerQueue;
}
@Override
public CompletableFuture<SignalXmlFilePayload> doneFuture() {
return done;
}
}

View file

@ -0,0 +1,8 @@
package ru.nbch.credit_tracker.service.task.setup;
import java.util.concurrent.BlockingQueue;
import ru.nbch.credit_tracker.model.StagePayload;
public interface IPipeLineStarter<Q extends StagePayload> {
void startFillingQueue(BlockingQueue<Q> queue);
}

View file

@ -0,0 +1,11 @@
package ru.nbch.credit_tracker.service.task.setup;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CompletableFuture;
import ru.nbch.credit_tracker.model.StagePayload;
import ru.nbch.credit_tracker.model.TerminatePayload;
public interface IPipeLineTerminator<Q extends StagePayload, X extends TerminatePayload> extends Runnable {
void setPollingQueue(BlockingQueue<Q> queue);
CompletableFuture<X> doneFuture();
}

View file

@ -0,0 +1,77 @@
package ru.nbch.credit_tracker.service.task.setup;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.BlockingQueue;
import ru.nbch.credit_tracker.model.StagePayload;
import ru.nbch.credit_tracker.service.task.Pipeline;
import ru.nbch.credit_tracker.service.task.Stage;
import ru.nbch.credit_tracker.service.task.setup.step.HandlerStep;
public class PipeLineBuilderV2<FIRST extends StagePayload, LAST extends StagePayload> {
private final List<Stage<?, ?>> stages;
private Stage<FIRST, ?> firstStage;
private BlockingQueue<LAST> lastQueue;
private IPipeLineStarter<FIRST> pipeLineStarter;
private IPipeLineTerminator<LAST, ?> pipeLineTerminator;
private PipeLineBuilderV2() {
this.stages = new ArrayList<>();
}
public static <I extends StagePayload, O extends StagePayload>
HandlerStep<I, I, O> first(BlockingQueue<I> inputQueue, BlockingQueue<O> outputQueue) {
PipeLineBuilderV2<I, O> builder = new PipeLineBuilderV2<>();
builder.lastQueue = outputQueue;
HandlerStep<I, I, O> step = StageBuilderV2.create(inputQueue, outputQueue);
((StageBuilderV2<I, I, O>) step).setParent(builder);
return step;
}
@SuppressWarnings("unchecked")
public <NEXT extends StagePayload>
HandlerStep<FIRST, LAST, NEXT> stage(BlockingQueue<NEXT> outputQueue) {
HandlerStep<FIRST, LAST, NEXT> step = StageBuilderV2.create(lastQueue, outputQueue);
lastQueue = (BlockingQueue<LAST>) outputQueue;
((StageBuilderV2<FIRST, LAST, NEXT>) step).setParent((PipeLineBuilderV2<FIRST, NEXT>) this);
return step;
}
public PipeLineBuilderV2<FIRST, LAST> fillInitialQueue(IPipeLineStarter<FIRST> pipeLineStarter) {
Objects.requireNonNull(pipeLineStarter, "pipeLineStarter is required");
if (firstStage == null) {
throw new IllegalStateException("Pipeline has no stages");
}
this.pipeLineStarter = pipeLineStarter;
return this;
}
public PipeLineBuilderV2<FIRST, LAST> pollFinishingQueue(IPipeLineTerminator<LAST, ?> pipeLineTerminator) {
Objects.requireNonNull(pipeLineTerminator, "pipeLineTerminator is required");
this.pipeLineTerminator = pipeLineTerminator;
return this;
}
public Pipeline build() {
Pipeline p = new Pipeline();
p.setStarter(() -> {
stages.forEach(Stage::startRunning);
pipeLineStarter.startFillingQueue(firstStage.getIn());
});
pipeLineTerminator.setPollingQueue(lastQueue);
p.setTerminator(pipeLineTerminator);
p.setStages(stages);
return p;
}
@SuppressWarnings("unchecked")
<I extends StagePayload, O extends StagePayload>
void addStage(Stage<I, O> stage) {
Objects.requireNonNull(stage, "stage is required");
if (stages.isEmpty()) {
this.firstStage = (Stage<FIRST, ?>) stage;
}
stages.add(stage);
}
}

View file

@ -0,0 +1,84 @@
package ru.nbch.credit_tracker.service.task.setup;
import java.util.Objects;
import java.util.concurrent.BlockingQueue;
import java.util.function.Consumer;
import ru.nbch.credit_tracker.model.StagePayload;
import ru.nbch.credit_tracker.service.task.Handler;
import ru.nbch.credit_tracker.service.task.Stage;
import ru.nbch.credit_tracker.service.task.setup.step.BuildStep;
import ru.nbch.credit_tracker.service.task.setup.step.HandlerStep;
import ru.nbch.credit_tracker.service.task.setup.step.PoisonStep;
/**
* Step Builder для Stage:
* Обязательная последовательность: handler(...) -> poison(...) -> buildStage()
*/
public class StageBuilderV2<FIRST extends StagePayload, I extends StagePayload, O extends StagePayload>
implements HandlerStep<FIRST, I, O>, PoisonStep<FIRST, I, O>, BuildStep<FIRST, O> {
private PipeLineBuilderV2<FIRST, O> parent;
private final BlockingQueue<I> inputQueue;
private final BlockingQueue<O> outputQueue;
private Handler<I, O> handler;
private O poison;
Consumer<O> consumer = null;
private StageBuilderV2(BlockingQueue<I> inputQueue, BlockingQueue<O> outputQueue) {
this.inputQueue = Objects.requireNonNull(inputQueue, "inputQueue is required");
this.outputQueue = Objects.requireNonNull(outputQueue, "outputQueue is required");
}
/** Стартовая фабрика возвращает первый шаг — HandlerStep */
public static <FIRST extends StagePayload, I extends StagePayload, O extends StagePayload>
HandlerStep<FIRST, I, O> create(BlockingQueue<I> inputQueue, BlockingQueue<O> outputQueue) {
return new StageBuilderV2<>(inputQueue, outputQueue);
}
void setParent(PipeLineBuilderV2<FIRST, O> parent) {
this.parent = Objects.requireNonNull(parent, "parent is required");
}
// ===================== ШАГИ =====================
/** Шаг 1: задать обработчик */
@Override
public PoisonStep<FIRST, I, O> handler(Handler<I, O> handler) {
this.handler = Objects.requireNonNull(handler, "handler is required");
return this;
}
/** Шаг 2: задать poison pill */
@Override
public BuildStep<FIRST, O> poison(O poison) {
this.poison = Objects.requireNonNull(poison, "poison is required");
return this;
}
public BuildStep<FIRST, O> handlerResult(Consumer<O> consumer) {
this.consumer = Objects.requireNonNull(consumer, "consumer is required");
return this;
}
/** Шаг 3: собрать Stage и вернуться к PipeLineBuilder */
@Override
public PipeLineBuilderV2<FIRST, O> andThen() {
if (parent == null) {
throw new IllegalStateException("parent PipeLineBuilder is not set");
}
if (handler == null) {
throw new IllegalStateException("handler must be set before buildStage()");
}
if (poison == null) {
throw new IllegalStateException("poison must be set before buildStage()");
}
Stage<I, O> stage = new Stage<>(handler, inputQueue, outputQueue, poison);
stage.setConsumer(consumer);
parent.addStage(stage);
return parent;
}
// ===================== Step-interfaces =====================
}

View file

@ -0,0 +1,10 @@
package ru.nbch.credit_tracker.service.task.setup.step;
import java.util.function.Consumer;
import ru.nbch.credit_tracker.model.StagePayload;
import ru.nbch.credit_tracker.service.task.setup.PipeLineBuilderV2;
public interface BuildStep<FIRST extends StagePayload, O extends StagePayload> {
PipeLineBuilderV2<FIRST, O> andThen();
BuildStep<FIRST, O> handlerResult(Consumer<O> consumer);
}

View file

@ -0,0 +1,8 @@
package ru.nbch.credit_tracker.service.task.setup.step;
import ru.nbch.credit_tracker.model.StagePayload;
import ru.nbch.credit_tracker.service.task.Handler;
public interface HandlerStep<FIRST extends StagePayload, I extends StagePayload, O extends StagePayload> {
PoisonStep<FIRST, I, O> handler(Handler<I, O> handler);
}

View file

@ -0,0 +1,7 @@
package ru.nbch.credit_tracker.service.task.setup.step;
import ru.nbch.credit_tracker.model.StagePayload;
public interface PoisonStep<FIRST extends StagePayload, I extends StagePayload, O extends StagePayload> {
BuildStep<FIRST, O> poison(O poison);
}

View file

@ -0,0 +1,114 @@
package ru.nbch.credit_tracker.service.xml.stax;
import java.io.OutputStream;
import java.util.List;
import javax.xml.stream.XMLEventFactory;
import javax.xml.stream.XMLEventWriter;
import javax.xml.stream.XMLOutputFactory;
import lombok.extern.slf4j.Slf4j;
import ru.nbch.credit_tracker.entities.external_request.signals.Auth;
import ru.nbch.credit_tracker.entities.external_request.signals.Person;
import ru.nbch.credit_tracker.entities.external_request.signals.Signals;
@Slf4j
//todo сделать с этим что-то!!!
public class SignalsStaxXmlWriterV2 {
private final List<Person> personList;
private final Signals signals;
private final String charsetName;
public SignalsStaxXmlWriterV2(List<Person> personList,
Signals signals,
String charsetName) {
this.personList = personList;
this.signals = signals;
this.charsetName = charsetName;
}
public void writeWithStax(OutputStream ou) {
try {
XMLOutputFactory output = XMLOutputFactory.newInstance();
XMLEventFactory xmlEventFactory = XMLEventFactory.newInstance();
XMLEventWriter writer = output.createXMLEventWriter(ou, charsetName);
writer = new IndentingXMLEventWriter(writer);
WriterWrapper writerWrapper = new WriterWrapper(xmlEventFactory, writer);
// writerWrapper.addStartDocument(charsetName);
//
// writerWrapper.addStartElement("Signals");
// writerWrapper.addAttribute("online", signals.getOnline());
// writerWrapper.addAttribute("processWithErrors", signals.getProcessWithErrors());
// writerWrapper.addSimplelement("Id", signals.getId());
// writeAuth(writerWrapper, signals.getAuth());
//
// writerWrapper.addStartElement("Conditions");
writerWrapper.addStartElement("Persons");
writePersons(writerWrapper, personList);
writerWrapper.addEndElement("Persons");
// writerWrapper.addEndElement("Conditions");
// writerWrapper.addEndElement("Signals");
writerWrapper.addEndDocument();
writerWrapper.flush();
System.gc();
} catch (Throwable e) {
log.error("Error serializing object to XML string: {}", e.getMessage(), e);
throw new IllegalStateException("cannot serialize object to XML");
}
}
private void writePersons(WriterWrapper writerWrapper, List<Person> persons) throws Exception {
writerWrapper.writeCollection("p", persons, (obj, name) -> writePerson(writerWrapper, obj, name));
}
private void writeAuth(WriterWrapper writerWrapper, Auth auth) throws Exception {
writerWrapper.addStartElement("Auth");
writerWrapper.addSimplelement("User", auth.getUser());
writerWrapper.addEndElement("Auth");
}
public void writePerson(WriterWrapper writerWrapper, Person person, String name) throws Exception {
if (person == null) {
return;
}
writerWrapper.addStartElement(name);
writerWrapper.addAttribute("uid", String.valueOf(person.getUid()));
writerWrapper.addAttribute("oksm", person.getOksm());
writerWrapper.addAttribute("s", person.getS());
writerWrapper.addAttribute("n", person.getN());
writerWrapper.addAttribute("m", person.getM());
writerWrapper.addAttribute("d", person.getD());
writerWrapper.addAttribute("pn", person.getPn());
writerWrapper.addAttribute("pd", person.getPd());
writerWrapper.addAttribute("dt", person.getDt());
writerWrapper.addAttribute("odt", person.getOdt());
writerWrapper.addAttribute("ip", person.getIp());
writerWrapper.addAttribute("consentDate", person.getConsentDate());
writerWrapper.addAttribute("consentPurpose", person.getConsentPurpose());
writerWrapper.addAttribute("reportUser", person.getReportUser());
writerWrapper.addAttribute("liability", person.getLiability());
writerWrapper.addAttribute("consentPeriod", person.getConsentPeriod());
writerWrapper.addAttribute("agrDate", person.getAgrDate());
writerWrapper.addAttribute("reportUserRegNum", person.getReportUserRegNum());
writerWrapper.addAttribute("reportUserTaxID", person.getReportUserTaxID());
writerWrapper.addAttribute("consentHash", person.getConsentHash());
writerWrapper.addAttribute("cS", person.getCs());
writerWrapper.addAttribute("consentN", person.getConsentN());
writerWrapper.addAttribute("consentM", person.getConsentM());
writerWrapper.addAttribute("consentBd", person.getConsentBd());
writerWrapper.addAttribute("consentPn", person.getConsentPn());
writerWrapper.addAttribute("consentPd", person.getConsentPd());
writerWrapper.addAttribute("consentDivCode", person.getConsentDivCode());
writerWrapper.addAttribute("consentDt", person.getConsentDt());
writerWrapper.addAttribute("consentOdt", person.getConsentOdt());
writerWrapper.addAttribute("consentIssueAuthority", person.getConsentIssueAuthority());
writerWrapper.addAttribute("inqPurpose", person.getInqPurpose());
writerWrapper.addEndElement(name);
}
}

View file

@ -104,7 +104,11 @@ public class XmlProcessorStaxImpl implements StaxXmlProcessor {
}
@Override
public <Z> Z read(InputStream inputStream, Class<Z> clazz, Consumer<Z> objProcessor, Function<Z, Boolean> emptyBatchCheckFunction, Integer batchSize, String endBatchElementPath) {
public <Z> Z read(InputStream inputStream,
Class<Z> clazz, Consumer<Z> objProcessor,
Function<Z, Boolean> emptyBatchCheckFunction,
Integer batchSize,
String endBatchElementPath) {
try {
XMLStreamReader xmlStreamReader = xmlInputFactory.createXMLStreamReader(inputStream);
Supplier<StaxReader<?>> staxReaderSupplier = staxReaders.get(clazz);

View file

@ -101,4 +101,8 @@ public class CTUtil {
log.warn("File {} hasn't been deleted", filePath);
}
}
public static String normalizePhoneNumber(String phoneNumber) {
return "9" + phoneNumber;
}
}

View file

@ -0,0 +1,85 @@
package ru.nbch.credit_tracker.utils;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import ru.nbch.credit_tracker.utils.error.ExceptionUtils;
public class LogUtil {
private final static Logger log = LoggerFactory.getLogger(LogUtil.class);
public static final String mdcKey = "ru.nbch.credit_tracker.log.mdc.key";
public static void addMdcLogKey(String uuid) {
addMdcLogKey(mdcKey, uuid);
}
public static void removeMdcLogKey() {
removeMdcLogKey(mdcKey);
}
public static void removeMdcLogKey(String key) {
try {
MDC.remove(key);
} catch (Throwable e) {
log.error(ExceptionUtils.getStackTrace(e));
}
}
public static void addMdcLogKey(String mdcKey, String uuid) {
if (uuid != null) {
try {
MDC.put(mdcKey, "[" + uuid + "] ");
} catch (Throwable e) {
log.error("{}", ExceptionUtils.getStackTrace(e));
}
}
}
public static Map<String, String> extractMdcMap() {
try {
return MDC.getCopyOfContextMap();
} catch (Throwable e) {
log.error(ExceptionUtils.getStackTrace(e));
return null;
}
}
public static Runnable wrap(Map<String, String> contextMap, Runnable task) {
return () -> {
Map<String, String> previous = null;
try {
try {
previous = MDC.getCopyOfContextMap();
} catch (Throwable e) {
log.error(ExceptionUtils.getStackTrace(e));
}
// ставим захваченный контекст (может быть null)
try {
if (contextMap == null) {
MDC.clear();
} else {
MDC.setContextMap(contextMap);
}
} catch (Throwable e) {
log.error(ExceptionUtils.getStackTrace(e));
}
task.run();
} finally {
// восстанавливаем предыдущий (может быть null)
try {
if (previous == null) {
MDC.clear();
} else {
MDC.setContextMap(previous);
}
} catch (Throwable e) {
log.error(ExceptionUtils.getStackTrace(e));
}
}
};
}
}

View file

@ -1,10 +1,15 @@
package ru.nbch.credit_tracker.utils;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Optional;
import java.util.UUID;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.multipart.MultipartFile;
@Slf4j
public class ZipUtils {
@ -25,4 +30,27 @@ public class ZipUtils {
}
return zipEntry;
}
public static boolean isZipFile(MultipartFile file) {
return file.getContentType() != null &&
(file.getContentType().equals("application/zip") || file.getContentType().equals("application/x-zip-compressed")) ||
file.getOriginalFilename() != null && file.getOriginalFilename().toLowerCase().endsWith(".zip");
}
public static String writeTmpFile(String path, MultipartFile file) throws IOException {
CTUtil.createFolder(path);
String filePath = path + UUID.randomUUID() + "monitoring_request.xml";
try (OutputStream out = Files.newOutputStream(Path.of(filePath))) {
ZipInputStream zipInputStream = new ZipInputStream(file.getInputStream());
zipInputStream.getNextEntry();
byte[] buffer = new byte[8 * 1024 * 1024];
int bytesRead;
while ((bytesRead = zipInputStream.read(buffer)) > 0) {
out.write(buffer, 0, bytesRead);
}
return filePath;
}
}
}

View file

@ -1,5 +1,7 @@
package ru.nbch.credit_tracker.utils.error;
import java.io.PrintWriter;
import java.io.StringWriter;
import ru.nbch.credit_tracker.enums.EnumKeyMessage;
import ru.nbch.credit_tracker.enums.IEnumKeyCode;
import ru.nbch.credit_tracker.enums.XmlErrorFormat;
@ -8,6 +10,13 @@ import ru.nbch.credit_tracker.exceptions.SignalServerException;
public class ExceptionUtils {
public static String getStackTrace(Throwable throwable) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw, true);
throwable.printStackTrace(pw);
return sw.getBuffer().toString();
}
public static SignalClientException clientExcp(IEnumKeyCode enumKeyCode, XmlErrorFormat xmlErrorFormat, Object... args) {
return clientExcp(enumKeyCode, null, xmlErrorFormat, args);
}

View file

@ -63,12 +63,18 @@
</appender>
<!--SQL запросы и результаты их выполнения-->
<logger name="ru.nbch.credit_tracker.mapper" level="INFO" additivity="false">
<logger name="ru.nbch.credit_tracker.mapper" level="DEBUG" additivity="false">
<appender-ref ref="FILE"/>
<appender-ref ref="FILE-JSON"/>
</logger>
<logger name="ru.nbch.credit_tracker.service" level="INFO" additivity="false">
<logger name="ru.nbch.credit_tracker.component" level="DEBUG" additivity="false">
<appender-ref ref="FILE"/>
<appender-ref ref="FILE-JSON"/>
<appender-ref ref="CONSOLE"/>
</logger>
<logger name="ru.nbch.credit_tracker.service" level="TRACE" additivity="false">
<appender-ref ref="FILE"/>
<appender-ref ref="FILE-JSON"/>
<appender-ref ref="CONSOLE"/>

View file

@ -5,12 +5,47 @@
<mapper namespace="ru.nbch.credit_tracker.mapper.indic.FlagsMapper">
<resultMap id="FlagsFidDataResultMap" type="ru.nbch.credit_tracker.service.indic.FlagsService$FlagFidData">
<resultMap id="RuPastDuarearResultMap" type="ru.nbch.credit_tracker.service.indic.FlagsService$RuPastDuarearData">
<result property="fid" column="fid" />
<result property="accSerialNum" column="ACC_SERIAL_NUM" />
<result property="reportedDate" column="REPORTED_DT" />
<result property="amtPastDue" column="AMT_PAST_DUE" />
<result property="daysPastDue" column="DAYS_PAST_DUE" />
</resultMap>
<resultMap id="RuPaymtResultMap" type="ru.nbch.credit_tracker.service.indic.FlagsService$RuPaymtData">
<result property="fid" column="fid" />
<result property="daysPastDue" column="DAYS_PAST_DUE" />
<result property="reportedDate" column="REPORTED_DT" />
</resultMap>
<resultMap id="PaymentsFlagResult" type="ru.nbch.credit_tracker.entities.indic.PaymentsFlag">
<result property="fid" column="fid" />
<result property="flag" column="flag" />
</resultMap>
<select id="findFlagsDebt5000" resultMap="FlagsFidDataResultMap">
<select id="findRuPastDuarearData" resultMap="RuPastDuarearResultMap">
SELECT FID, ACC_SERIAL_NUM, REPORTED_DT, AMT_PAST_DUE, DAYS_PAST_DUE
FROM RU_PASTDUEARREAR
WHERE FID IN
<foreach item="fid" collection="fids" open="(" separator="," close=")">
#{fid}
</foreach>
with UR
</select>
<select id="findRuPaymtData" resultMap="RuPaymtResultMap">
SELECT FID, DAYS_PAST_DUE, REPORTED_DT
FROM RU_PAYMT
WHERE FID IN
<foreach item="fid" collection="fids" open="(" separator="," close=")">
#{fid}
</foreach>
AND REPORTED_DT >= #{reportedDate}
with UR
</select>
<select id="findFlagsDebt5000" resultMap="PaymentsFlagResult">
SELECT
FID as fid,
(case when (sum(case when AMT_PAST_DUE > 5000 then 1 else 0 end) = 0) then 0 else 1 end) as flag
@ -27,7 +62,7 @@
WITH UR
</select>
<select id="findFlag90ByPastDueArrear" resultMap="FlagsFidDataResultMap">
<select id="findFlag90ByPastDueArrear" resultMap="PaymentsFlagResult">
SELECT
FID as fid,
(case when (sum(case when (REPORTED_DT >= #{reportedDate} and DAYS_PAST_DUE > 90) then 1 else 0 end) = 0) then 0 else 1 end) as flag
@ -40,7 +75,7 @@
WITH UR
</select>
<select id="findFlag90ByRuPaymt" resultMap="FlagsFidDataResultMap">
<select id="findFlag90ByRuPaymt" resultMap="PaymentsFlagResult">
SELECT
FID as fid,
(case when (sum(case when (REPORTED_DT >= #{reportedDate} and DAYS_PAST_DUE > 90) then 1 else 0 end) = 0) then 0 else 1 end) as flag

View file

@ -1,25 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="ru.nbch.credit_tracker.mapper.indic.PhoneMapper">
<resultMap id="fidsDataResultMap" type="ru.nbch.credit_tracker.service.indic.PhoneService$FidsData">
<result property="phone" column="number" />
<result property="fid" column="fid" />
<result property="fileSinceDate" column="fileSinceDate" />
</resultMap>
<select id="findFids" resultMap="fidsDataResultMap">
SELECT pn.NUMBER AS number, pn.FID AS fid, pn.FILE_SINCE_DT AS fileSinceDate
FROM PHONE_NORM pn
INNER JOIN NAME n ON n.FID = pn.FID
WHERE number in
<foreach item="phone" collection="phones" open="(" separator="," close=")">
#{phone}
</foreach>
ORDER BY fileSinceDate DESC, fid DESC
WITH UR
</select>
</mapper>

View file

@ -5,7 +5,7 @@
<mapper namespace="ru.nbch.credit_tracker.mapper.signal.PhoneFidMapMapper">
<resultMap id="PhoneFidMapResultMap" type="ru.nbch.credit_tracker.entities.dto.PhoneFidMap">
<resultMap id="PhoneFidMapResultMap" type="ru.nbch.credit_tracker.entities.app.PhoneFidMap">
<result property="id" column="id"/>
<result property="packageId" column="package_id"/>
<result property="subjectId" column="subject_id"/>
@ -25,6 +25,36 @@
</foreach>
</insert>
<insert id="register" parameterType="java.util.List">
INSERT INTO signals_phone_fid_map (subject_id, fid, phone, package_id, flag_debt5000, flag_90_12, calculated_at)
values
<foreach collection="phoneFidMap" item="phoneFid" separator=",">
(#{phoneFid.subjectId}, #{phoneFid.fid}, #{phoneFid.phone}, #{phoneFid.packageId}, #{phoneFid.flagDebt5000}, #{phoneFid.flag90days}, #{phoneFid.calculatedAt})
</foreach>
</insert>
<insert id="registerJson" parameterType="string">
INSERT INTO signals_phone_fid_map
(subject_id, fid, phone, package_id, flag_debt5000, flag_90_12, calculated_at)
SELECT subject_id, fid, phone, package_id, flag_debt5000, flag_90_12, calculated_at
FROM json_to_recordset(#{json}::json) AS t(
subject_id varchar(255),
fid int,
phone text,
package_id bigint,
flag_debt5000 smallint,
flag_90_12 smallint,
calculated_at timestamp)
</insert>
<insert id="registerWithId" parameterType="java.util.List" useGeneratedKeys="true" keyColumn="id" keyProperty="id">
INSERT INTO signals_phone_fid_map (subject_id, phone, package_id)
values
<foreach collection="phoneFidMap" item="phoneFid" separator=",">
(#{phoneFid.subjectId}, #{phoneFid.phone}, #{phoneFid.packageId})
</foreach>
</insert>
<update id="updatePhoneFidMaps" parameterType="java.util.List">
<foreach collection="subjects" item="subject" separator=";">
@ -42,12 +72,35 @@
</foreach>
</update>
<update id="updateOne" parameterType="ru.nbch.credit_tracker.entities.app.PhoneFidMap">
UPDATE signals_phone_fid_map
SET
package_id = #{subject.packageId},
subject_id = #{subject.subjectId},
phone = #{subject.phone},
fid = #{subject.fid2},
flag_debt5000 = #{subject.flagDebt5000},
flag_90_12 = #{subject.flag90days},
calculated_at = #{subject.calculatedAt},
error_text = #{subject.errorText}
WHERE id = #{subject.id}
</update>
<update id="updateFid" parameterType="java.util.List">
<foreach collection="subjects" item="subject" separator=";">
UPDATE signals_phone_fid_map
SET
fid = #{subject.fid}
WHERE rec_uuid = #{subject.id}
</foreach>
</update>
<update id="updatePhoneFidMapsErrors" parameterType="java.util.List">
<foreach collection="subjects" item="subject" separator=";">
UPDATE signals_phone_fid_map
SET
error_text = #{subject.errorText}
WHERE id = #{subject.id}
WHERE id = #{subject.recUuid}
</foreach>
</update>
@ -67,7 +120,7 @@
LIMIT #{limit} OFFSET #{offset}
</select>
<select id="findPhoneFidMapById" resultMap="PhoneFidMapResultMap" resultType="ru.nbch.credit_tracker.entities.dto.PhoneFidMap">
<select id="findPhoneFidMapById" resultMap="PhoneFidMapResultMap" resultType="ru.nbch.credit_tracker.entities.app.PhoneFidMap">
SELECT id,
package_id,
subject_id,
@ -88,11 +141,5 @@
where package_id = #{packageId} AND fid is not null
</select>
<select id="hasOldRow" resultType="java.lang.Boolean">
select EXISTS(SELECT 1
FROM signals_phone_fid_map
WHERE package_id = #{packageId}
AND calculated_at &lt; now() - make_interval(days => #{daysCount})
)
</select>
</mapper>

View file

@ -5,7 +5,7 @@
<mapper namespace="ru.nbch.credit_tracker.mapper.signal.UserPackageMapper">
<resultMap id="packageDataResultMap" type="ru.nbch.credit_tracker.entities.dto.UserPackage">
<resultMap id="packageDataResultMap" type="ru.nbch.credit_tracker.entities.app.UserPackage">
<result property="id" column="id"/>
<result property="userId" column="membercode"/>
<result property="packageName" column="package_name"/>
@ -53,7 +53,7 @@
</update>
<select id="insertPackage" resultType="java.lang.Long"
parameterType="ru.nbch.credit_tracker.entities.dto.UserPackage">
parameterType="ru.nbch.credit_tracker.entities.app.UserPackage">
INSERT INTO signals_user_packages (membercode, package_name, created_at, status)
values (#{userPackage.userId}, #{userPackage.packageName}, #{userPackage.createdAt}, #{userPackage.status}) RETURNING id
</select>
@ -77,7 +77,7 @@
</update>
<select id="findPackage" resultMap="packageDataResultMap"
resultType="ru.nbch.credit_tracker.entities.dto.UserPackage">
resultType="ru.nbch.credit_tracker.entities.app.UserPackage">
SELECT id, membercode, package_name, created_at, status, deactivated_at, last_downloaded_report, monitoring_at
FROM signals_user_packages
WHERE id = #{id}
@ -85,7 +85,7 @@
</select>
<select id="findUserPackages" resultMap="packageDataResultMap"
resultType="ru.nbch.credit_tracker.entities.dto.UserPackage">
resultType="ru.nbch.credit_tracker.entities.app.UserPackage">
SELECT id, membercode, package_name, created_at, status, deactivated_at, last_downloaded_report, monitoring_at
FROM signals_user_packages
WHERE membercode IS NOT NULL
@ -97,7 +97,7 @@
</select>
<select id="findAllPackagesByUserId" resultMap="packageDataResultMap"
resultType="ru.nbch.credit_tracker.entities.dto.UserPackage">
resultType="ru.nbch.credit_tracker.entities.app.UserPackage">
SELECT id, membercode, package_name, created_at, status, deactivated_at, last_downloaded_report, monitoring_at
FROM signals_user_packages
WHERE membercode IS NOT NULL
@ -105,7 +105,7 @@
</select>
<select id="findAllPackagesByStatus" resultMap="packageDataResultMap"
resultType="ru.nbch.credit_tracker.entities.dto.UserPackage">
resultType="ru.nbch.credit_tracker.entities.app.UserPackage">
SELECT id, membercode, package_name, created_at, status, deactivated_at, last_downloaded_report, monitoring_at
FROM signals_user_packages
WHERE status = #{status}

View file

@ -1,3 +0,0 @@
CREATE INDEX IF NOT EXISTS
idx_signals_phone_fid_map_calculated_at
ON signals_phone_fid_map (calculated_at);

View file

@ -0,0 +1,24 @@
CREATE TABLE signals_user_packages (
DB_VERSION varchar(12)
);
alter table signals_user_packages
alter column LAST_SEARCHED_AT type timestamp;
alter table signals_user_packages
alter column REJECT_STATUS type varchar(255);
alter table signals_phone_fid_map
alter column SEARCHED_AT type timestamp;
CREATE TABLE signals_flags (
ID SERIAL PRIMARY KEY,
MAP_ID bigint,
SUBJECT_ID varchar(255),
PHONE varchar(255),
FID int,
FLAG_DEBT5000 smallint,
FLAG_90_12 smallint,
CALCULATED_AT timestamp
);

View file

@ -0,0 +1,278 @@
package ru.nbch.credit_tracker.service;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import javax.sql.DataSource;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import ru.nbch.credit_tracker.service.indic.FlagsService;
public class FlagsService2Test {
private static final LocalDate INITIAL_DATE = LocalDate.parse("2025-09-25");
private static final String RU_PASTDUEAREAR_CREATE = """
create table RU_PASTDUEARREAR
(
FID INTEGER not null,
SERIAL_NUM BIGINT not null primary key,
ACC_SERIAL_NUM BIGINT not null,
REPORTED_DT DATE not null,
AMT_PAST_DUE DECIMAL(17, 2),
DAYS_PAST_DUE BIGINT
);
""";
private static final String RU_PASTDUEAREAR_DROP = """
DROP TABLE IF EXISTS RU_PASTDUEARREAR;
""";
private static final String RU_PAYMT_DROP = """
DROP TABLE IF EXISTS RU_PAYMT;
""";
private static final String RU_PAYMT_CREATE = """
create table RU_PAYMT
(
FID INTEGER not null,
SERIAL_NUM BIGINT not null primary key,
DAYS_PAST_DUE INTEGER,
REPORTED_DT DATE not null
);
""";
private static final String FILL_RU_PASTDUEARREAR = String.format("""
INSERT INTO RU_PASTDUEARREAR VALUES
(1, 1, 1, '%s', 36000.00, 159), -- просрочка 5000 и 90_days по этой записи
(2, 2, 2, '%s', 3200.00, null), -- не макс reported_date для того же acc_serial_num
(2, 3, 2, '%s', 6700.00, null), -- не макс reported_date для того же acc_serial_num
(2, 4, 2, '%s', 5700.00, null), -- просрочка 5000 по этой записи
(3, 5, 3, '%s', 300000000000.00, null),
(2, 6, 2, '%s', 0.00, null), -- не макс reported_date для того же acc_serial_num
(4, 7, 4, '%s', 4000.00, null), -- нет просрочек
(5, 8, 5, '%s', 2000.31, 182), -- не макс reported_date для того же acc_serial_num
(5, 9, 5, '%s', 2000.31, 212), -- не макс reported_date для того же acc_serial_num
(5, 10, 5, '%s', 2000.31, null), -- не макс reported_date для того же acc_serial_num
(5, 11, 5, '%s', 2000.31, 273), -- не макс reported_date для того же acc_serial_num
(5, 12, 5, '%s', 2000.31, null), -- не макс reported_date для того же acc_serial_num
(5, 13, 5, '%s', 7000.31, 307), -- не макс reported_date для того же acc_serial_num
(5, 14, 5, '%s', 2000.31, 338), -- не макс reported_date для того же acc_serial_num, просрочка 90_days будет взята у этой записи
(5, 15, 5, '%s', 7000.31, 368), -- Просрочка 5000 будет взята у этой записи. Уникальный acc_serial_num с макс reported_dt
(5, 16, 5, '%s', 2000.31, 399), -- не макс reported_date для того же acc_serial_num
(5, 17, 5, '%s', 5000.31, 5), -- не макс reported_date для того же acc_serial_num
(6, 18, 6, '%s', 3000, null), -- Нет просрочки 5000, нет просрочки 90_days (90_days будет в RU_PAYMT)
(7, 19, 7, '%s', 5500, 90), -- не макс reported_date для того же acc_serial_num. 90_days будет взята отсюда
(7, 20, 7, '%s', 3000, null) -- Нет просрочки 5000. Уникальный acc_serial_num с макс reported_dt
""",
calcLocalDateFromInitialDate("2025-05-31"),
calcLocalDateFromInitialDate("2021-12-29"),
calcLocalDateFromInitialDate("2022-01-28"),
calcLocalDateFromInitialDate("2025-04-06"),
calcLocalDateFromInitialDate("2025-01-14"),
calcLocalDateFromInitialDate("2021-11-29"),
calcLocalDateFromInitialDate("2025-12-06"),
calcLocalDateFromInitialDate("2021-04-01"),
calcLocalDateFromInitialDate("2021-05-01"),
calcLocalDateFromInitialDate("2021-05-31"),
calcLocalDateFromInitialDate("2021-07-01"),
calcLocalDateFromInitialDate("2021-07-31"),
calcLocalDateFromInitialDate("2021-08-04"),
calcLocalDateFromInitialDate("2025-09-08"),
calcLocalDateFromInitialDate("2025-09-09"),
calcLocalDateFromInitialDate("2021-11-04"),
calcLocalDateFromInitialDate("2022-01-04"),
calcLocalDateFromInitialDate("2025-09-09"),
calcLocalDateFromInitialDate("2025-09-09"),
calcLocalDateFromInitialDate("2025-09-10"));
private static final String FILL_RU_PAYMT = String.format("""
INSERT INTO RU_PAYMT VALUES
(1, 1, null, '%s'),
(2, 2, 30, '%s'),
(3, 3, null, '%s'),
(3, 4, 100, '%s'),
(4, 5, 10, '%s'),
(5, 6, 10, '%s'),
(6, 7, 90, '%s'),
(6, 8, 80, '%s'),
(7, 9, null, '%s'),
(8, 10, null, '%s'),
(8, 11, 83, '%s'),
(8, 12, 91, '%s'),
(9, 13, 100, '%s')
""",
calcLocalDateFromInitialDate("2024-11-09"),
calcLocalDateFromInitialDate("2024-11-09"),
calcLocalDateFromInitialDate("2024-11-10"),
calcLocalDateFromInitialDate("2024-11-15"),
calcLocalDateFromInitialDate("2024-11-15"),
calcLocalDateFromInitialDate("2023-11-15"),
calcLocalDateFromInitialDate("2024-11-09"),
calcLocalDateFromInitialDate("2024-10-09"),
calcLocalDateFromInitialDate("2024-10-09"),
calcLocalDateFromInitialDate("2024-10-09"),
calcLocalDateFromInitialDate("2024-10-09"),
calcLocalDateFromInitialDate("2024-10-09"),
calcLocalDateFromInitialDate("2024-10-09"));
private DataSource dataSource;
private NamedParameterJdbcTemplate jdbcTemplate;
private FlagsService flagsService;
@BeforeEach
void setUp() {
dataSource = DataSourceBuilder.create()
.url("jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;MODE=DB2;DATABASE_TO_LOWER=TRUE")
.driverClassName("org.h2.Driver")
.username("sa")
.password("")
.build();
jdbcTemplate = new NamedParameterJdbcTemplate(dataSource);
jdbcTemplate.getJdbcTemplate().execute(RU_PASTDUEAREAR_DROP);
jdbcTemplate.getJdbcTemplate().execute(RU_PAYMT_DROP);
jdbcTemplate.getJdbcTemplate().execute(RU_PASTDUEAREAR_CREATE);
jdbcTemplate.getJdbcTemplate().execute(RU_PAYMT_CREATE);
flagsService = new FlagsService(null, jdbcTemplate);
}
@Test
public void testfindFlags() {
jdbcTemplate.getJdbcTemplate().execute(FILL_RU_PASTDUEARREAR);
jdbcTemplate.getJdbcTemplate().execute(FILL_RU_PAYMT);
List<Integer> fids = new ArrayList<>(List.of(
1, // есть просрочка 5000, есть просрочка 90_days в RU_PASTDUEARREAR
2, // есть просрочка 5000, нет просрочки 90_days в RU_PASTDUEARREAR. Нет просрочки 90_days в RU_PAYMT
3, // есть просрочка 5000, нет просрочки 90_days в RU_PASTDUEARREAR. Есть просрочка 90_days в RU_PAYMT
4, // нет никаких просрочек
7, // нет просрочки 5000, есть просрочка 90_days в RU_PASTDUEARREAR
5, // есть просрочка 5000, есть просрочка 90_days в RU_PASTDUEARREAR
6, // нет просрочки 5000, нет просрочки 90_days в RU_PASTDUEARREAR. Есть просрочка 90_days в RU_PAYMT
8, // нету в RU_PASTDUEARREAR, есть в RU_PAYMT
9) // нету в RU_PASTDUEARREAR, нету в RU_PAYMT
);
Map<Integer, FlagsService.FlagsData> resultMap = flagsService.findFlags(fids);
Assertions.assertEquals(9, resultMap.size());
Assertions.assertEquals(1, resultMap.get(1).getFlagDebt5000());
Assertions.assertEquals(1, resultMap.get(2).getFlagDebt5000());
Assertions.assertEquals(1, resultMap.get(3).getFlagDebt5000());
Assertions.assertEquals(0, resultMap.get(4).getFlagDebt5000());
Assertions.assertEquals(1, resultMap.get(5).getFlagDebt5000());
Assertions.assertEquals(0, resultMap.get(6).getFlagDebt5000());
Assertions.assertEquals(0, resultMap.get(7).getFlagDebt5000());
Assertions.assertEquals(0, resultMap.get(8).getFlagDebt5000());
Assertions.assertEquals(0, resultMap.get(9).getFlagDebt5000());
Assertions.assertEquals(1, resultMap.get(1).getFlag90days());
Assertions.assertEquals(0, resultMap.get(2).getFlag90days());
Assertions.assertEquals(1, resultMap.get(3).getFlag90days());
Assertions.assertEquals(0, resultMap.get(4).getFlag90days());
Assertions.assertEquals(1, resultMap.get(5).getFlag90days());
Assertions.assertEquals(1, resultMap.get(6).getFlag90days());
Assertions.assertEquals(1, resultMap.get(7).getFlag90days());
Assertions.assertEquals(1, resultMap.get(8).getFlag90days());
Assertions.assertEquals(1, resultMap.get(9).getFlag90days());
}
@Test
public void testfindFlagsWithoutRuPaymt() {
jdbcTemplate.getJdbcTemplate().execute(FILL_RU_PASTDUEARREAR);
List<Integer> fids = new ArrayList<>(List.of(
1, // есть просрочка 5000, есть просрочка 90_days в RU_PASTDUEARREAR
2, // есть просрочка 5000, нет просрочки 90_days в RU_PASTDUEARREAR. Нет просрочки 90_days в RU_PAYMT
3, // есть просрочка 5000, нет просрочки 90_days в RU_PASTDUEARREAR. Есть просрочка 90_days в RU_PAYMT
4, // нет никаких просрочек
7, // нет просрочки 5000, есть просрочка 90_days в RU_PASTDUEARREAR
5, // есть просрочка 5000, есть просрочка 90_days в RU_PASTDUEARREAR
6, // нет просрочки 5000, нет просрочки 90_days в RU_PASTDUEARREAR. Есть просрочка 90_days в RU_PAYMT
8, // нету в RU_PASTDUEARREAR, есть в RU_PAYMT
9) // нету в RU_PASTDUEARREAR, нету в RU_PAYMT
);
Map<Integer, FlagsService.FlagsData> resultMap = flagsService.findFlags(fids);
Assertions.assertEquals(9, resultMap.size());
Assertions.assertEquals(1, resultMap.get(1).getFlagDebt5000());
Assertions.assertEquals(1, resultMap.get(2).getFlagDebt5000());
Assertions.assertEquals(1, resultMap.get(3).getFlagDebt5000());
Assertions.assertEquals(0, resultMap.get(4).getFlagDebt5000());
Assertions.assertEquals(1, resultMap.get(5).getFlagDebt5000());
Assertions.assertEquals(0, resultMap.get(6).getFlagDebt5000());
Assertions.assertEquals(0, resultMap.get(7).getFlagDebt5000());
Assertions.assertEquals(0, resultMap.get(8).getFlagDebt5000());
Assertions.assertEquals(0, resultMap.get(9).getFlagDebt5000());
Assertions.assertEquals(1, resultMap.get(1).getFlag90days());
Assertions.assertEquals(0, resultMap.get(2).getFlag90days());
Assertions.assertEquals(0, resultMap.get(3).getFlag90days());
Assertions.assertEquals(0, resultMap.get(4).getFlag90days());
Assertions.assertEquals(1, resultMap.get(5).getFlag90days());
Assertions.assertEquals(0, resultMap.get(6).getFlag90days());
Assertions.assertEquals(1, resultMap.get(7).getFlag90days());
Assertions.assertEquals(0, resultMap.get(8).getFlag90days());
Assertions.assertEquals(0, resultMap.get(9).getFlag90days());
}
@Test
public void testfindFlagsWithoutRuPastduearreae() {
jdbcTemplate.getJdbcTemplate().execute(FILL_RU_PAYMT);
List<Integer> fids = new ArrayList<>(List.of(
1, // есть просрочка 5000, есть просрочка 90_days в RU_PASTDUEARREAR
2, // есть просрочка 5000, нет просрочки 90_days в RU_PASTDUEARREAR. Нет просрочки 90_days в RU_PAYMT
3, // есть просрочка 5000, нет просрочки 90_days в RU_PASTDUEARREAR. Есть просрочка 90_days в RU_PAYMT
4, // нет никаких просрочек
7, // нет просрочки 5000, есть просрочка 90_days в RU_PASTDUEARREAR
5, // есть просрочка 5000, есть просрочка 90_days в RU_PASTDUEARREAR
6, // нет просрочки 5000, нет просрочки 90_days в RU_PASTDUEARREAR. Есть просрочка 90_days в RU_PAYMT
8, // нету в RU_PASTDUEARREAR, есть в RU_PAYMT
9) // нету в RU_PASTDUEARREAR, нету в RU_PAYMT
);
Map<Integer, FlagsService.FlagsData> resultMap = flagsService.findFlags(fids);
Assertions.assertEquals(9, resultMap.size());
Assertions.assertEquals(0, resultMap.get(1).getFlagDebt5000());
Assertions.assertEquals(0, resultMap.get(2).getFlagDebt5000());
Assertions.assertEquals(0, resultMap.get(3).getFlagDebt5000());
Assertions.assertEquals(0, resultMap.get(4).getFlagDebt5000());
Assertions.assertEquals(0, resultMap.get(5).getFlagDebt5000());
Assertions.assertEquals(0, resultMap.get(6).getFlagDebt5000());
Assertions.assertEquals(0, resultMap.get(7).getFlagDebt5000());
Assertions.assertEquals(0, resultMap.get(8).getFlagDebt5000());
Assertions.assertEquals(0, resultMap.get(9).getFlagDebt5000());
Assertions.assertEquals(0, resultMap.get(1).getFlag90days());
Assertions.assertEquals(0, resultMap.get(2).getFlag90days());
Assertions.assertEquals(1, resultMap.get(3).getFlag90days());
Assertions.assertEquals(0, resultMap.get(4).getFlag90days());
Assertions.assertEquals(0, resultMap.get(5).getFlag90days());
Assertions.assertEquals(1, resultMap.get(6).getFlag90days());
Assertions.assertEquals(0, resultMap.get(7).getFlag90days());
Assertions.assertEquals(1, resultMap.get(8).getFlag90days());
Assertions.assertEquals(1, resultMap.get(9).getFlag90days());
}
private static LocalDate calcLocalDateFromInitialDate(String date) {
LocalDateTime now = LocalDateTime.now();
long daysPassed = ChronoUnit.DAYS.between(INITIAL_DATE, now);
return LocalDate.parse(date).plusDays(daysPassed);
}
}

View file

@ -0,0 +1,218 @@
package ru.nbch.credit_tracker.service;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import ru.nbch.credit_tracker.mapper.indic.FlagsMapper;
import ru.nbch.credit_tracker.service.indic.FlagsService;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
public class FlagsServiceTest {
private static final LocalDate INITIAL_DATE = LocalDate.parse("2025-09-25");
@Mock
FlagsMapper flagsMapper;
@Test
public void testfindFlags() {
FlagsService flagsService = new FlagsService(flagsMapper, null);
when(flagsMapper.findRuPastDuarearData(Mockito.anyList())).thenReturn(ruPastDuarearDataList);
when(flagsMapper.findRuPaymtData(Mockito.anyList(), Mockito.any())).thenReturn(ruPaymtDataList);
List<Integer> fids = new ArrayList<>(List.of(
1, // есть просрочка 5000, есть просрочка 90_days в RU_PASTDUEARREAR
2, // есть просрочка 5000, нет просрочки 90_days в RU_PASTDUEARREAR. Нет просрочки 90_days в RU_PAYMT
3, // есть просрочка 5000, нет просрочки 90_days в RU_PASTDUEARREAR. Есть просрочка 90_days в RU_PAYMT
4, // нет никаких просрочек
7, // нет просрочки 5000, есть просрочка 90_days в RU_PASTDUEARREAR
5, // есть просрочка 5000, есть просрочка 90_days в RU_PASTDUEARREAR
6, // нет просрочки 5000, нет просрочки 90_days в RU_PASTDUEARREAR. Есть просрочка 90_days в RU_PAYMT
8, // нету в RU_PASTDUEARREAR, есть в RU_PAYMT
9) // нету в RU_PASTDUEARREAR, нету в RU_PAYMT
);
Map<Integer, FlagsService.FlagsData> resultMap = flagsService.findFlags(fids);
Assertions.assertEquals(9, resultMap.size());
Assertions.assertEquals(1, resultMap.get(1).getFlagDebt5000());
Assertions.assertEquals(1, resultMap.get(2).getFlagDebt5000());
Assertions.assertEquals(1, resultMap.get(3).getFlagDebt5000());
Assertions.assertEquals(0, resultMap.get(4).getFlagDebt5000());
Assertions.assertEquals(1, resultMap.get(5).getFlagDebt5000());
Assertions.assertEquals(0, resultMap.get(6).getFlagDebt5000());
Assertions.assertEquals(0, resultMap.get(7).getFlagDebt5000());
Assertions.assertEquals(0, resultMap.get(8).getFlagDebt5000());
Assertions.assertEquals(0, resultMap.get(9).getFlagDebt5000());
Assertions.assertEquals(1, resultMap.get(1).getFlag90days());
Assertions.assertEquals(0, resultMap.get(2).getFlag90days());
Assertions.assertEquals(1, resultMap.get(3).getFlag90days());
Assertions.assertEquals(0, resultMap.get(4).getFlag90days());
Assertions.assertEquals(1, resultMap.get(5).getFlag90days());
Assertions.assertEquals(1, resultMap.get(6).getFlag90days());
Assertions.assertEquals(1, resultMap.get(7).getFlag90days());
Assertions.assertEquals(1, resultMap.get(8).getFlag90days());
Assertions.assertEquals(1, resultMap.get(9).getFlag90days());
}
@Test
public void testfindFlagsWithoutRuPaymt() {
FlagsService flagsService = new FlagsService(flagsMapper, null);
when(flagsMapper.findRuPastDuarearData(Mockito.anyList())).thenReturn(ruPastDuarearDataList);
when(flagsMapper.findRuPaymtData(Mockito.anyList(), Mockito.any())).thenReturn(new ArrayList<>());
List<Integer> fids = new ArrayList<>(List.of(
1, // есть просрочка 5000, есть просрочка 90_days в RU_PASTDUEARREAR
2, // есть просрочка 5000, нет просрочки 90_days в RU_PASTDUEARREAR. Нет просрочки 90_days в RU_PAYMT
3, // есть просрочка 5000, нет просрочки 90_days в RU_PASTDUEARREAR. Есть просрочка 90_days в RU_PAYMT
4, // нет никаких просрочек
7, // нет просрочки 5000, есть просрочка 90_days в RU_PASTDUEARREAR
5, // есть просрочка 5000, есть просрочка 90_days в RU_PASTDUEARREAR
6, // нет просрочки 5000, нет просрочки 90_days в RU_PASTDUEARREAR. Есть просрочка 90_days в RU_PAYMT
8, // нету в RU_PASTDUEARREAR, есть в RU_PAYMT
9) // нету в RU_PASTDUEARREAR, нету в RU_PAYMT
);
Map<Integer, FlagsService.FlagsData> resultMap = flagsService.findFlags(fids);
Assertions.assertEquals(9, resultMap.size());
Assertions.assertEquals(1, resultMap.get(1).getFlagDebt5000());
Assertions.assertEquals(1, resultMap.get(2).getFlagDebt5000());
Assertions.assertEquals(1, resultMap.get(3).getFlagDebt5000());
Assertions.assertEquals(0, resultMap.get(4).getFlagDebt5000());
Assertions.assertEquals(1, resultMap.get(5).getFlagDebt5000());
Assertions.assertEquals(0, resultMap.get(6).getFlagDebt5000());
Assertions.assertEquals(0, resultMap.get(7).getFlagDebt5000());
Assertions.assertEquals(0, resultMap.get(8).getFlagDebt5000());
Assertions.assertEquals(0, resultMap.get(9).getFlagDebt5000());
Assertions.assertEquals(1, resultMap.get(1).getFlag90days());
Assertions.assertEquals(0, resultMap.get(2).getFlag90days());
Assertions.assertEquals(0, resultMap.get(3).getFlag90days());
Assertions.assertEquals(0, resultMap.get(4).getFlag90days());
Assertions.assertEquals(1, resultMap.get(5).getFlag90days());
Assertions.assertEquals(0, resultMap.get(6).getFlag90days());
Assertions.assertEquals(1, resultMap.get(7).getFlag90days());
Assertions.assertEquals(0, resultMap.get(8).getFlag90days());
Assertions.assertEquals(0, resultMap.get(9).getFlag90days());
}
@Test
public void testfindFlagsWithoutRuPastduearreae() {
FlagsService flagsService = new FlagsService(flagsMapper, null);
when(flagsMapper.findRuPastDuarearData(Mockito.anyList())).thenReturn(new ArrayList<>());
when(flagsMapper.findRuPaymtData(Mockito.anyList(), Mockito.any())).thenReturn(ruPaymtDataList);
List<Integer> fids = new ArrayList<>(List.of(
1, // есть просрочка 5000, есть просрочка 90_days в RU_PASTDUEARREAR
2, // есть просрочка 5000, нет просрочки 90_days в RU_PASTDUEARREAR. Нет просрочки 90_days в RU_PAYMT
3, // есть просрочка 5000, нет просрочки 90_days в RU_PASTDUEARREAR. Есть просрочка 90_days в RU_PAYMT
4, // нет никаких просрочек
7, // нет просрочки 5000, есть просрочка 90_days в RU_PASTDUEARREAR
5, // есть просрочка 5000, есть просрочка 90_days в RU_PASTDUEARREAR
6, // нет просрочки 5000, нет просрочки 90_days в RU_PASTDUEARREAR. Есть просрочка 90_days в RU_PAYMT
8, // нету в RU_PASTDUEARREAR, есть в RU_PAYMT
9) // нету в RU_PASTDUEARREAR, нету в RU_PAYMT
);
Map<Integer, FlagsService.FlagsData> resultMap = flagsService.findFlags(fids);
Assertions.assertEquals(9, resultMap.size());
Assertions.assertEquals(0, resultMap.get(1).getFlagDebt5000());
Assertions.assertEquals(0, resultMap.get(2).getFlagDebt5000());
Assertions.assertEquals(0, resultMap.get(3).getFlagDebt5000());
Assertions.assertEquals(0, resultMap.get(4).getFlagDebt5000());
Assertions.assertEquals(0, resultMap.get(5).getFlagDebt5000());
Assertions.assertEquals(0, resultMap.get(6).getFlagDebt5000());
Assertions.assertEquals(0, resultMap.get(7).getFlagDebt5000());
Assertions.assertEquals(0, resultMap.get(8).getFlagDebt5000());
Assertions.assertEquals(0, resultMap.get(9).getFlagDebt5000());
Assertions.assertEquals(0, resultMap.get(1).getFlag90days());
Assertions.assertEquals(0, resultMap.get(2).getFlag90days());
Assertions.assertEquals(1, resultMap.get(3).getFlag90days());
Assertions.assertEquals(0, resultMap.get(4).getFlag90days());
Assertions.assertEquals(0, resultMap.get(5).getFlag90days());
Assertions.assertEquals(1, resultMap.get(6).getFlag90days());
Assertions.assertEquals(0, resultMap.get(7).getFlag90days());
Assertions.assertEquals(1, resultMap.get(8).getFlag90days());
Assertions.assertEquals(1, resultMap.get(9).getFlag90days());
}
private final List<FlagsService.RuPastDuarearData> ruPastDuarearDataList = new ArrayList<>(
List.of(
new FlagsService.RuPastDuarearData(1, BigInteger.valueOf(1), calcLocalDateFromInitialDate("2025-05-31"), BigDecimal.valueOf(36000.00), BigInteger.valueOf(159)), // просрочка 5000 и 90_days по этой записи
new FlagsService.RuPastDuarearData(2, BigInteger.valueOf(2), calcLocalDateFromInitialDate("2021-12-29"), BigDecimal.valueOf(3200.00), null), // не макс reported_date для того же acc_serial_num
new FlagsService.RuPastDuarearData(2, BigInteger.valueOf(2), calcLocalDateFromInitialDate("2022-01-28"), BigDecimal.valueOf(6700.00), null), // не макс reported_date для того же acc_serial_num
new FlagsService.RuPastDuarearData(2, BigInteger.valueOf(2), calcLocalDateFromInitialDate("2025-04-06"), BigDecimal.valueOf(5700.00), null), // просрочка 5000 по этой записи
new FlagsService.RuPastDuarearData(3, BigInteger.valueOf(3), calcLocalDateFromInitialDate("2025-01-14"), BigDecimal.valueOf(300000000000.00), null),
new FlagsService.RuPastDuarearData(2, BigInteger.valueOf(2), calcLocalDateFromInitialDate("2021-11-29"), BigDecimal.valueOf(0.00), null), // не макс reported_date для того же acc_serial_num
new FlagsService.RuPastDuarearData(4, BigInteger.valueOf(4), calcLocalDateFromInitialDate("2025-12-06"), BigDecimal.valueOf(4000.00), null), // нет просрочек
new FlagsService.RuPastDuarearData(5, BigInteger.valueOf(5), calcLocalDateFromInitialDate("2021-04-01"), BigDecimal.valueOf(2000.31), BigInteger.valueOf(182)), // не макс reported_date для того же acc_serial_num
new FlagsService.RuPastDuarearData(5, BigInteger.valueOf(5), calcLocalDateFromInitialDate("2021-05-01"), BigDecimal.valueOf(2000.31), BigInteger.valueOf(212)), // не макс reported_date для того же acc_serial_num
new FlagsService.RuPastDuarearData(5, BigInteger.valueOf(5), calcLocalDateFromInitialDate("2021-05-31"), BigDecimal.valueOf(2000.31), null), // не макс reported_date для того же acc_serial_num
new FlagsService.RuPastDuarearData(5, BigInteger.valueOf(5), calcLocalDateFromInitialDate("2021-07-01"), BigDecimal.valueOf(2000.31), BigInteger.valueOf(273)), // не макс reported_date для того же acc_serial_num
new FlagsService.RuPastDuarearData(5, BigInteger.valueOf(5), calcLocalDateFromInitialDate("2021-07-31"), BigDecimal.valueOf(2000.31), null), // не макс reported_date для того же acc_serial_num
new FlagsService.RuPastDuarearData(5, BigInteger.valueOf(5), calcLocalDateFromInitialDate("2021-08-04"), BigDecimal.valueOf(7000.31), BigInteger.valueOf(307)), // не макс reported_date для того же acc_serial_num
new FlagsService.RuPastDuarearData(5, BigInteger.valueOf(5), calcLocalDateFromInitialDate("2025-09-08"), BigDecimal.valueOf(2000.31), BigInteger.valueOf(338)), // не макс reported_date для того же acc_serial_num, просрочка 90_days будет взята у этой записи
new FlagsService.RuPastDuarearData(5, BigInteger.valueOf(51), calcLocalDateFromInitialDate("2025-09-09"), BigDecimal.valueOf(7000.31), BigInteger.valueOf(368)), // Просрочка 5000 будет взята у этой записи. Уникальный acc_serial_num с макс reported_dt
new FlagsService.RuPastDuarearData(5, BigInteger.valueOf(5), calcLocalDateFromInitialDate("2021-11-04"), BigDecimal.valueOf(2000.31), BigInteger.valueOf(399)), // не макс reported_date для того же acc_serial_num
new FlagsService.RuPastDuarearData(5, BigInteger.valueOf(5), calcLocalDateFromInitialDate("2022-01-04"), BigDecimal.valueOf(5000.31), BigInteger.valueOf(5)), // не макс reported_date для того же acc_serial_num
new FlagsService.RuPastDuarearData(6, BigInteger.valueOf(6), calcLocalDateFromInitialDate("2025-09-09"), BigDecimal.valueOf(3000), null), // Нет просрочки 5000, нет просрочки 90_days (90_days будет в RU_PAYMT)
new FlagsService.RuPastDuarearData(7, BigInteger.valueOf(7), calcLocalDateFromInitialDate("2025-09-09"), BigDecimal.valueOf(5500), BigInteger.valueOf(90)), // не макс reported_date для того же acc_serial_num. 90_days будет взята отсюда
new FlagsService.RuPastDuarearData(7, BigInteger.valueOf(7), calcLocalDateFromInitialDate("2025-09-10"), BigDecimal.valueOf(3000), null) // Нет просрочки 5000. Уникальный acc_serial_num с макс reported_dt
)
);
private final List<FlagsService.RuPaymtData> ruPaymtDataList = new ArrayList<>(
List.of(
new FlagsService.RuPaymtData(1, null, calcLocalDateFromInitialDate("2024-11-09")),
new FlagsService.RuPaymtData(2, BigInteger.valueOf(30), calcLocalDateFromInitialDate("2024-11-09")),
new FlagsService.RuPaymtData(3, null, calcLocalDateFromInitialDate("2024-11-10")),
new FlagsService.RuPaymtData(3, BigInteger.valueOf(100), calcLocalDateFromInitialDate("2024-11-15")),
new FlagsService.RuPaymtData(4, BigInteger.valueOf(10), calcLocalDateFromInitialDate("2024-11-15")),
new FlagsService.RuPaymtData(5, BigInteger.valueOf(10), calcLocalDateFromInitialDate("2023-11-15")),
new FlagsService.RuPaymtData(6, BigInteger.valueOf(90), calcLocalDateFromInitialDate("2024-11-09")),
new FlagsService.RuPaymtData(6, BigInteger.valueOf(80), calcLocalDateFromInitialDate("2024-10-09")),
new FlagsService.RuPaymtData(7, null, calcLocalDateFromInitialDate("2024-10-09")),
new FlagsService.RuPaymtData(8, null, calcLocalDateFromInitialDate("2024-10-09")),
new FlagsService.RuPaymtData(8, BigInteger.valueOf(83), calcLocalDateFromInitialDate("2024-10-09")),
new FlagsService.RuPaymtData(8, BigInteger.valueOf(91), calcLocalDateFromInitialDate("2024-10-09")),
new FlagsService.RuPaymtData(9, BigInteger.valueOf(100), calcLocalDateFromInitialDate("2024-10-09"))
)
);
private LocalDate calcLocalDateFromInitialDate(String date) {
LocalDateTime now = LocalDateTime.now();
long daysPassed = ChronoUnit.DAYS.between(INITIAL_DATE, now);
return LocalDate.parse(date).plusDays(daysPassed);
}
}

View file

@ -0,0 +1,179 @@
package ru.nbch.credit_tracker.service;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import static org.mockito.ArgumentMatchers.any;
import org.mockito.Mock;
import static org.mockito.Mockito.when;
import org.mockito.junit.jupiter.MockitoExtension;
import ru.nbch.credit_tracker.entities.app.PhoneFidMap;
import ru.nbch.credit_tracker.repository.PhoneRepository;
import ru.nbch.credit_tracker.service.indic.PhoneService;
@ExtendWith(MockitoExtension.class)
public class PhoneService2Test {
@Mock
PhoneRepository phoneRepository;
@Test
public void defineFidByPhoneTest() {
PhoneService phoneService = new PhoneService(phoneRepository);
fidsDataWithEqualSinceDt.sort(byDbOrder);
when(phoneRepository.findFids(any())).thenReturn(fidsDataWithEqualSinceDt);
List<PhoneFidMap> phoneFidMap = new ArrayList<>();
phoneFidMap.add(PhoneFidMap.builder().phone("1111").build());
phoneFidMap.add(PhoneFidMap.builder().phone("1112").build());
phoneFidMap.add(PhoneFidMap.builder().phone("1113").build());
phoneFidMap.add(PhoneFidMap.builder().phone("1114").build());
phoneFidMap.add(PhoneFidMap.builder().phone("1115").build());
Map<Integer, PhoneFidMap> res = phoneService.defineFidByPhone(phoneFidMap);
Assertions.assertEquals(4, res.size());
Assertions.assertEquals("1111", res.get(124).getPhone());
Assertions.assertEquals("1112", res.get(125).getPhone());
Assertions.assertEquals("1114", res.get(126).getPhone());
Assertions.assertEquals("1115", res.get(127).getPhone());
}
@Test
public void defineFidByPhoneTest2() {
PhoneService phoneService = new PhoneService(phoneRepository);
fidsDataWithEqualSinceDt.sort(byDbOrder);
when(phoneRepository.findFids(any())).thenReturn(fidsDataWithEqualSinceDt2);
List<PhoneFidMap> phoneFidMap = new ArrayList<>();
phoneFidMap.add(PhoneFidMap.builder().phone("1111").build());
phoneFidMap.add(PhoneFidMap.builder().phone("1112").build());
phoneFidMap.add(PhoneFidMap.builder().phone("1113").build());
phoneFidMap.add(PhoneFidMap.builder().phone("1114").build());
phoneFidMap.add(PhoneFidMap.builder().phone("1115").build());
Map<Integer, PhoneFidMap> res = phoneService.defineFidByPhone(phoneFidMap);
Assertions.assertEquals(3, res.size());
Assertions.assertEquals("1112", res.get(125).getPhone());
Assertions.assertEquals("1114", res.get(126).getPhone());
Assertions.assertEquals("1115", res.get(127).getPhone());
}
@Test
public void defineFidByPhoneTest3() {
PhoneService phoneService = new PhoneService(phoneRepository);
fidsDataWithEqualSinceDt.sort(byDbOrder);
when(phoneRepository.findFids(any())).thenReturn(fidsDataWithEqualSinceDt3);
List<PhoneFidMap> phoneFidMap = new ArrayList<>();
phoneFidMap.add(PhoneFidMap.builder().phone("1111").build());
phoneFidMap.add(PhoneFidMap.builder().phone("1112").build());
phoneFidMap.add(PhoneFidMap.builder().phone("1113").build());
phoneFidMap.add(PhoneFidMap.builder().phone("1114").build());
phoneFidMap.add(PhoneFidMap.builder().phone("1115").build());
Map<Integer, PhoneFidMap> res = phoneService.defineFidByPhone(phoneFidMap);
Assertions.assertEquals(2, res.size());
Assertions.assertEquals("1114", res.get(126).getPhone());
Assertions.assertEquals("1115", res.get(127).getPhone());
}
private final List<PhoneRepository.FidsData> fidsDataWithEqualSinceDt = new ArrayList<>(
List.of(
new PhoneRepository.FidsData(123, "91111", LocalDate.of(2025, 9, 10)),
new PhoneRepository.FidsData(124, "91111", LocalDate.of(2025, 9, 10)),
new PhoneRepository.FidsData(125, "91112", LocalDate.of(2024, 10, 10)),
new PhoneRepository.FidsData(126, "91114", LocalDate.of(2024, 10, 11)),
new PhoneRepository.FidsData(127, "91115", LocalDate.of(2025, 9, 11)),
new PhoneRepository.FidsData(128, "91115", LocalDate.of(2025, 9, 10))
)
);
private final List<PhoneRepository.FidsData> fidsDataWithEqualSinceDt2 = new ArrayList<>(
List.of(
new PhoneRepository.FidsData(125, "91112", LocalDate.of(2024, 10, 10)),
new PhoneRepository.FidsData(126, "91114", LocalDate.of(2024, 10, 11)),
new PhoneRepository.FidsData(127, "91115", LocalDate.of(2025, 9, 11)),
new PhoneRepository.FidsData(128, "91115", LocalDate.of(2025, 9, 10))
)
);
private final List<PhoneRepository.FidsData> fidsDataWithEqualSinceDt3 = new ArrayList<>(
List.of(
new PhoneRepository.FidsData(126, "91114", LocalDate.of(2024, 10, 11)),
new PhoneRepository.FidsData(127, "91115", LocalDate.of(2025, 9, 11)),
new PhoneRepository.FidsData(128, "91115", LocalDate.of(2025, 9, 10))
)
);
Comparator<PhoneRepository.FidsData> byDbOrder =
Comparator.comparing(PhoneRepository.FidsData::getPhone, Comparator.nullsFirst(Comparator.naturalOrder())) // pn.number ASC
.thenComparing(PhoneRepository.FidsData::getFileSinceDate, Comparator.nullsLast(Comparator.reverseOrder())) // file_since_dt DESC
.thenComparing(PhoneRepository.FidsData::getFid, Comparator.reverseOrder());
@Test
void defineFidsAndNameByPhoneTest() {
PhoneService phoneService = new PhoneService(phoneRepository);
// fidsDataWithNameEqualSinceDt.sort(byDbOrderWithName);
when(phoneRepository.findFidsWithName(any())).thenReturn(fidsDataWithNameEqualSinceDt);
List<PhoneFidMap> phoneFidMap = new ArrayList<>();
phoneFidMap.add(PhoneFidMap.builder().phone("1111").build());
phoneFidMap.add(PhoneFidMap.builder().phone("1112").build());
phoneFidMap.add(PhoneFidMap.builder().phone("1113").build());
phoneFidMap.add(PhoneFidMap.builder().phone("1114").build());
phoneFidMap.add(PhoneFidMap.builder().phone("1115").build());
Map<Integer, PhoneFidMap> res = phoneService.defineFidsAndNameByPhone(phoneFidMap);
Assertions.assertEquals(4, res.size());
Assertions.assertEquals("1111", res.get(124).getPhone());
Assertions.assertEquals("1112", res.get(125).getPhone());
Assertions.assertEquals("1114", res.get(126).getPhone());
Assertions.assertEquals("1115", res.get(127).getPhone());
Assertions.assertEquals("found124", res.get(124).getFirstName());
Assertions.assertNull(res.get(125).getFirstName());
Assertions.assertEquals("found126", res.get(126).getFirstName());
Assertions.assertNull(res.get(128));
}
private final List<PhoneRepository.FidsDataWithName> fidsDataWithNameEqualSinceDt = new ArrayList<>(
List.of(
new PhoneRepository.FidsDataWithName(123, "91111", LocalDate.of(2025, 9, 10), "skip", null, null, null, null),
new PhoneRepository.FidsDataWithName(123, "91111", LocalDate.of(2025, 9, 10), "skip", null, null, null, LocalDate.of(2025, 10, 9)),
new PhoneRepository.FidsDataWithName(124, "91111", LocalDate.of(2025, 9, 9), "found124", null, null, null, LocalDate.of(2025, 9, 9)),
new PhoneRepository.FidsDataWithName(124, "91111", LocalDate.of(2025, 9, 10), "skip124", null, null, null, LocalDate.of(2025, 9, 8)),
new PhoneRepository.FidsDataWithName(124, "91111", LocalDate.of(2025, 9, 9), "skip124", null, null, null, LocalDate.of(2025, 9, 8)),
new PhoneRepository.FidsDataWithName(124, "91111", LocalDate.of(2025, 9, 10), "found124", null, null, null, LocalDate.of(2025, 9, 9)),
new PhoneRepository.FidsDataWithName(125, "91112", LocalDate.of(2024, 10, 10), null, null, null, null, null),
new PhoneRepository.FidsDataWithName(126, "91114", LocalDate.of(2024, 10, 11), "skip126", null, null, null, null),
new PhoneRepository.FidsDataWithName(126, "91114", LocalDate.of(2024, 10, 11), "found126", null, null, null, LocalDate.of(2025, 9, 9)),
new PhoneRepository.FidsDataWithName(126, "91114", LocalDate.of(2024, 10, 5), "found126", null, null, null, LocalDate.of(2025, 9, 9)),
new PhoneRepository.FidsDataWithName(126, "91114", LocalDate.of(2024, 10, 5), "skip126", null, null, null, null),
new PhoneRepository.FidsDataWithName(127, "91115", LocalDate.of(2025, 9, 11), null, null, null, null, null),
new PhoneRepository.FidsDataWithName(128, "91115", LocalDate.of(2025, 9, 10), "found128", null, null, null, LocalDate.of(2025, 9, 9))
)
);
//todo понять зачем в тесте использовался этот компоратор
// Comparator<PhoneRepository.FidsDataWithName> byDbOrderWithName =
// Comparator.comparing(PhoneRepository.FidsDataWithName::getPhone, Comparator.nullsFirst(Comparator.naturalOrder())) // pn.number ASC
// .thenComparing(PhoneRepository.FidsDataWithName::getFileSinceDate, Comparator.nullsLast(Comparator.reverseOrder())) // file_since_dt DESC
// .thenComparing(PhoneRepository.FidsDataWithName::getFid, Comparator.reverseOrder());
}

View file

@ -0,0 +1,122 @@
package ru.nbch.credit_tracker.service;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import ru.nbch.credit_tracker.entities.app.PhoneFidMap;
import ru.nbch.credit_tracker.repository.PhoneRepository;
import ru.nbch.credit_tracker.service.indic.PhoneService;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
public class PhoneServiceTest {
@Mock
PhoneRepository phoneRepository;
@Test
public void defineFidByPhoneTest() {
PhoneService phoneService = new PhoneService(phoneRepository);
fidsDataWithEqualSinceDt.sort(byDbOrder);
when(phoneRepository.findFidsLong(any())).thenReturn(fidsDataWithEqualSinceDt);
List<PhoneFidMap> phoneFidMap = new ArrayList<>();
phoneFidMap.add(PhoneFidMap.builder().phone("1111").build());
phoneFidMap.add(PhoneFidMap.builder().phone("1112").build());
phoneFidMap.add(PhoneFidMap.builder().phone("1113").build());
phoneFidMap.add(PhoneFidMap.builder().phone("1114").build());
phoneFidMap.add(PhoneFidMap.builder().phone("1115").build());
Map<Integer, PhoneFidMap> res = phoneService.defineFidByPhone(phoneFidMap);
Assertions.assertEquals(4, res.size());
Assertions.assertEquals("1111", res.get(124).getPhone());
Assertions.assertEquals("1112", res.get(125).getPhone());
Assertions.assertEquals("1114", res.get(126).getPhone());
Assertions.assertEquals("1115", res.get(127).getPhone());
}
@Test
public void defineFidByPhoneTest2() {
PhoneService phoneService = new PhoneService(phoneRepository);
fidsDataWithEqualSinceDt.sort(byDbOrder);
when(phoneRepository.findFidsLong(any())).thenReturn(fidsDataWithEqualSinceDt2);
List<PhoneFidMap> phoneFidMap = new ArrayList<>();
phoneFidMap.add(PhoneFidMap.builder().phone("1111").build());
phoneFidMap.add(PhoneFidMap.builder().phone("1112").build());
phoneFidMap.add(PhoneFidMap.builder().phone("1113").build());
phoneFidMap.add(PhoneFidMap.builder().phone("1114").build());
phoneFidMap.add(PhoneFidMap.builder().phone("1115").build());
Map<Integer, PhoneFidMap> res = phoneService.defineFidByPhone(phoneFidMap);
Assertions.assertEquals(3, res.size());
Assertions.assertEquals("1112", res.get(125).getPhone());
Assertions.assertEquals("1114", res.get(126).getPhone());
Assertions.assertEquals("1115", res.get(127).getPhone());
}
@Test
public void defineFidByPhoneTest3() {
PhoneService phoneService = new PhoneService(phoneRepository);
fidsDataWithEqualSinceDt.sort(byDbOrder);
when(phoneRepository.findFidsLong(any())).thenReturn(fidsDataWithEqualSinceDt3);
List<PhoneFidMap> phoneFidMap = new ArrayList<>();
phoneFidMap.add(PhoneFidMap.builder().phone("1111").build());
phoneFidMap.add(PhoneFidMap.builder().phone("1112").build());
phoneFidMap.add(PhoneFidMap.builder().phone("1113").build());
phoneFidMap.add(PhoneFidMap.builder().phone("1114").build());
phoneFidMap.add(PhoneFidMap.builder().phone("1115").build());
Map<Integer, PhoneFidMap> res = phoneService.defineFidByPhone(phoneFidMap);
Assertions.assertEquals(2, res.size());
Assertions.assertEquals("1114", res.get(126).getPhone());
Assertions.assertEquals("1115", res.get(127).getPhone());
}
private final List<PhoneRepository.FidsDataLong> fidsDataWithEqualSinceDt = new ArrayList<>(
List.of(
new PhoneRepository.FidsDataLong(123, 91111L, LocalDate.of(2025, 9, 10)),
new PhoneRepository.FidsDataLong(124, 91111L, LocalDate.of(2025, 9, 10)),
new PhoneRepository.FidsDataLong(125, 91112L, LocalDate.of(2024, 10, 10)),
new PhoneRepository.FidsDataLong(126, 91114L, LocalDate.of(2024, 10, 11)),
new PhoneRepository.FidsDataLong(127, 91115L, LocalDate.of(2025, 9, 11)),
new PhoneRepository.FidsDataLong(128, 91115L, LocalDate.of(2025, 9, 10))
)
);
private final List<PhoneRepository.FidsDataLong> fidsDataWithEqualSinceDt2 = new ArrayList<>(
List.of(
new PhoneRepository.FidsDataLong(125, 91112L, LocalDate.of(2024, 10, 10)),
new PhoneRepository.FidsDataLong(126, 91114L, LocalDate.of(2024, 10, 11)),
new PhoneRepository.FidsDataLong(127, 91115L, LocalDate.of(2025, 9, 11)),
new PhoneRepository.FidsDataLong(128, 91115L, LocalDate.of(2025, 9, 10))
)
);
private final List<PhoneRepository.FidsDataLong> fidsDataWithEqualSinceDt3 = new ArrayList<>(
List.of(
new PhoneRepository.FidsDataLong(126, 91114L, LocalDate.of(2024, 10, 11)),
new PhoneRepository.FidsDataLong(127, 91115L, LocalDate.of(2025, 9, 11)),
new PhoneRepository.FidsDataLong(128, 91115L, LocalDate.of(2025, 9, 10))
)
);
Comparator<PhoneRepository.FidsDataLong> byDbOrder =
Comparator.comparing(PhoneRepository.FidsDataLong::getPhone, Comparator.nullsFirst(Comparator.naturalOrder())) // pn.number ASC
.thenComparing(PhoneRepository.FidsDataLong::getFileSinceDate, Comparator.nullsLast(Comparator.reverseOrder())) // file_since_dt DESC
.thenComparing(PhoneRepository.FidsDataLong::getFid, Comparator.reverseOrder());
}

View file

@ -6,7 +6,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import ru.nbch.credit_tracker.entities.dto.PhoneFidMap;
import ru.nbch.credit_tracker.entities.app.PhoneFidMap;
import ru.nbch.credit_tracker.entities.external_response.monitoring_report_error.MonitoringReportErrors;
import ru.nbch.credit_tracker.entities.external_response.online_download.Person;
import ru.nbch.credit_tracker.entities.external_response.online_download.Persons;