diff --git a/clearing-parent/reports-service/src/main/java/ru/spcex/clearing/reports/reports/CSVReportBuilder_.java b/clearing-parent/reports-service/src/main/java/ru/spcex/clearing/reports/reports/CSVReportBuilder_.java new file mode 100644 index 000000000..fc106b873 --- /dev/null +++ b/clearing-parent/reports-service/src/main/java/ru/spcex/clearing/reports/reports/CSVReportBuilder_.java @@ -0,0 +1,108 @@ +package ru.spcex.clearing.reports.reports; + +import com.opencsv.CSVWriterBuilder; +import com.opencsv.ICSVWriter; +import com.opencsv.bean.HeaderColumnNameMappingStrategy; +import com.opencsv.bean.StatefulBeanToCsv; +import com.opencsv.bean.StatefulBeanToCsvBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import ru.spcex.platform.enumeration.ReportType; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.Writer; +import java.time.format.DateTimeFormatter; +import java.util.List; + +/** + * Класс для построения отчетов в формате CSV + * + * @param

тип параметров, используемых при построении отчета + */ +public abstract class CSVReportBuilder_ { + protected static final Logger log = LoggerFactory.getLogger(CSVReportBuilder_.class); + protected DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy'T'HH:mm:ss"); + /** + * Возвращает списком значения CSV отчета. Каждый элемент списка - строка, элемент массива внутри списка - столбец. + */ + protected abstract List getRows(); + + /** + * Название отчета, используется для формирования имени отчета (без указания формата) + * + * @param params Параметры формирования отчета + */ + protected abstract String getReportFilename(P params); + + /** + * Тип отчета по {@link ReportType} + */ + public abstract ReportType getReportType(); + + /** + * Тип параметров для построения отчетов + */ + public abstract Class

getParamsClass(); + + public abstract Class getReportClass(); + + /** + * Формирование отчета на основе параметров + * + * @param params Параметры формирования отчета + */ + protected abstract void collect(P params); + + /** + * Создает CSVWriter. Если требуется изменить параметры выходного CSV файла, этот метод следует переопределить. + * + * @param file Файл отчета + * @return Созданный CSVWriter + * + * todo check it + */ + protected ICSVWriter createCsvWriter(File file) throws IOException { + Writer writer = new FileWriter(file); + CSVWriterBuilder csvWriterBuilder = new CSVWriterBuilder(writer) + .withLineEnd("\n") + .withSeparator(',') + .withQuoteChar('\"'); + return csvWriterBuilder.build(); + } + + public File createReport(P params, File outFolder) { + assert outFolder != null && outFolder.isDirectory(); + + File reportFile = new File(outFolder, normalizeFilenameForOS(getReportFilename(params) + ".csv")); + + collect(params); + List lines = getRows(); + + assert lines != null; + + try (Writer writer = new FileWriter(reportFile)) { + HeaderColumnNameMappingStrategy headerColumnNameMappingStrategy = new HeaderColumnNameMappingStrategy<>(); + headerColumnNameMappingStrategy.setType(getReportClass()); + StatefulBeanToCsv sbc = new StatefulBeanToCsvBuilder(writer) + .withMappingStrategy(headerColumnNameMappingStrategy) + .withLineEnd("\n") + .withSeparator(',') + .withQuotechar('\"') + .build(); + sbc.write(lines); + } catch (Exception e) { + log.error("Can't create report", e); + return null; + } + + return reportFile; + } + + protected String normalizeFilenameForOS(String filename) { + if (System.getProperty("os.name").toLowerCase().contains("windows")) + return filename.replace(":", "_"); + return filename; + } +} diff --git a/clearing-parent/reports-service/src/main/java/ru/spcex/clearing/reports/reports/bean/UnfulfilledDealReport.java b/clearing-parent/reports-service/src/main/java/ru/spcex/clearing/reports/reports/bean/UnfulfilledDealReport.java new file mode 100644 index 000000000..f50e2540b --- /dev/null +++ b/clearing-parent/reports-service/src/main/java/ru/spcex/clearing/reports/reports/bean/UnfulfilledDealReport.java @@ -0,0 +1,63 @@ +package ru.spcex.clearing.reports.reports.bean; + +import com.opencsv.bean.CsvBindByName; +import com.opencsv.bean.CsvBindByPosition; +import com.opencsv.bean.CsvDate; +import com.opencsv.bean.CsvNumber; + +import java.math.BigDecimal; +import java.time.LocalDate; + +public class UnfulfilledDealReport { + + @CsvBindByName(column = "securityId") + @CsvBindByPosition(position = 0) + @CsvNumber("#0") + private BigDecimal securityId; + + @CsvBindByName(column = "tradeNo") + @CsvBindByPosition(position = 1) + private Long tradeNo; + + @CsvBindByName(column = "tradeDate") + @CsvBindByPosition(position = 2) + @CsvDate(value = "dd.MM.yyyy") + private LocalDate tradeDate; + + @CsvBindByName(column = "settleCode") + @CsvBindByPosition(position = 3) + private String settleCode; + + + public BigDecimal getSecurityId() { + return securityId; + } + + public void setSecurityId(BigDecimal securityId) { + this.securityId = securityId; + } + + public Long getTradeNo() { + return tradeNo; + } + + public void setTradeNo(Long tradeNo) { + this.tradeNo = tradeNo; + } + + public LocalDate getTradeDate() { + return tradeDate; + } + + public void setTradeDate(LocalDate tradeDate) { + this.tradeDate = tradeDate; + } + + public String getSettleCode() { + return settleCode; + } + + public void setSettleCode(String settleCode) { + this.settleCode = settleCode; + } +} diff --git a/clearing-parent/reports-service/src/main/java/ru/spcex/clearing/reports/reports/bp/UnfulfilledDealReportBuilder_.java b/clearing-parent/reports-service/src/main/java/ru/spcex/clearing/reports/reports/bp/UnfulfilledDealReportBuilder_.java new file mode 100644 index 000000000..f765cb77b --- /dev/null +++ b/clearing-parent/reports-service/src/main/java/ru/spcex/clearing/reports/reports/bp/UnfulfilledDealReportBuilder_.java @@ -0,0 +1,107 @@ +package ru.spcex.clearing.reports.reports.bp; + +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; +import ru.clearing.classes.statics.data.company.Company; +import ru.clearing.classes.statics.data.execution.ExecutionFond; +import ru.clearing.classes.statics.data.misc.STrades; +import ru.spcex.clearing.imdg.IMDGDistributedNames; +import ru.spcex.clearing.reports.reports.CSVReportBuilder_; +import ru.spcex.clearing.reports.reports.SessionIdParam; +import ru.spcex.clearing.reports.reports.bean.UnfulfilledDealReport; +import ru.spcex.platform.enumeration.Allowed; +import ru.spcex.platform.enumeration.ReportType; +import ru.spcex.platform.enumeration.WorkflowStatus; +import ru.spcex.platform.imdg.api.Imdg; +import ru.spcex.platform.imdg.api.ImdgProvider; +import ru.spcex.platform.imdg.api.predicate.ImdgPredicate; +import ru.spcex.platform.imdg.api.predicate.ImdgPredicateBuilder; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.*; + +@Component +@Scope(value = ConfigurableBeanFactory.SCOPE_SINGLETON) +public class UnfulfilledDealReportBuilder_ extends CSVReportBuilder_ { + private final Imdg executionFondImdg; + private final Imdg companyImdg; + private final Imdg sTradesImdg; + + public UnfulfilledDealReportBuilder_(ImdgProvider imdgProvider) { + executionFondImdg = imdgProvider.getImdg(IMDGDistributedNames.Map_ExecutionFond, ExecutionFond.class); + companyImdg = imdgProvider.getImdg(IMDGDistributedNames.Map_Company, Company.class); + sTradesImdg = imdgProvider.getImdg(IMDGDistributedNames.Map_STrades, STrades.class); + } + + private List rows = null; + + @Override + protected List getRows() { + return rows; + } + + @Override + protected String getReportFilename(SessionIdParam params) { + return "unfulfilled_deal_report.%s".formatted(dateTimeFormatter.format(LocalDateTime.now())); + } + + @Override + public ReportType getReportType() { + return ReportType.UNFULFILLED_DEAL_REPORT; + } + + @Override + public Class getParamsClass() { + return SessionIdParam.class; + } + + @Override + public Class getReportClass() { + return UnfulfilledDealReport.class; + } + + @Override + protected void collect(SessionIdParam params) { + ImdgPredicateBuilder pb = executionFondImdg.predicateBuilder(); + ImdgPredicate predicate = pb.and( + pb.equals("sessionId", params.getSessionId()), + pb.equals("coverageStatus", Allowed.DENIED.getKey()) + ); + Collection executionFonds = executionFondImdg.getCollectionObjectsByPredicate(predicate); + + Map activeCompanyForId = new HashMap<>(); + rows = new ArrayList<>(executionFonds.size()); + for (ExecutionFond executionFond : executionFonds) { + try { + Boolean activeCompany = activeCompanyForId.get(executionFond.getCompanyId()); + if (activeCompany == null) { + Company company = companyImdg.getSingleObjectByID(executionFond.getCompanyId()); + activeCompany = WorkflowStatus.Active.equalsByKey(company.getWorkflowStatus()); + activeCompanyForId.put(executionFond.getCompanyId(), activeCompany); + } + if (!activeCompany) continue; + + STrades sTrade = sTradesImdg.getSingleObjectByFieldValues( + Map.of( + "settleCode", executionFond.getSettlementCode(), + "operation", executionFond.getSide() + ) + ); + + UnfulfilledDealReport unfulfilledDealReport = new UnfulfilledDealReport(); + // todo replace to long (example for test) + unfulfilledDealReport.setSecurityId(BigDecimal.valueOf(executionFond.getSecurityId())); + unfulfilledDealReport.setTradeNo(executionFond.getExchangeExecutionId()); + unfulfilledDealReport.setTradeDate(executionFond.getTradingDate()); + unfulfilledDealReport.setSettleCode(sTrade != null ? sTrade.getClassCode() : null); + + rows.add(unfulfilledDealReport); + } catch (Throwable e) { + log.error("Can't create report row for execution_fond (id = %d), skipped".formatted(executionFond.getId()), e); + } + } + } + +} diff --git a/clearing-parent/reports-service/src/test/java/ru/spcex/clearing/reports/services/PrototypeTest.java b/clearing-parent/reports-service/src/test/java/ru/spcex/clearing/reports/services/PrototypeTest.java new file mode 100644 index 000000000..268810ebf --- /dev/null +++ b/clearing-parent/reports-service/src/test/java/ru/spcex/clearing/reports/services/PrototypeTest.java @@ -0,0 +1,211 @@ +package ru.spcex.clearing.reports.services; + +import com.opencsv.CSVParser; +import com.opencsv.CSVParserBuilder; +import com.opencsv.CSVReader; +import com.opencsv.CSVReaderBuilder; +import com.opencsv.exceptions.CsvValidationException; +import org.apache.commons.io.FileUtils; +import org.junit.jupiter.api.*; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import ru.clearing.classes.statics.data.company.Company; +import ru.clearing.classes.statics.data.company.CompanySymbols; +import ru.clearing.classes.statics.data.execution.ExecutionDeposit; +import ru.clearing.classes.statics.data.execution.ExecutionFond; +import ru.clearing.classes.statics.data.instrument.issue.FixedIncomeSecurity; +import ru.clearing.classes.statics.data.misc.Listing; +import ru.clearing.classes.statics.data.misc.STrades; +import ru.clearing.classes.statics.data.misc.Session; +import ru.clearing.classes.statics.data.payment.PaymentInstruction; +import ru.clearing.classes.statics.data.profile.ProfileDocument; +import ru.clearing.classes.statics.data.registry.Registry; +import ru.clearing.classes.statics.data.registry.TradingClearingRegistry; +import ru.clearing.classes.statics.data.statement.Statement; +import ru.clearing.platform.dictionary.RegistryCodeDictionary; +import ru.spcex.clearing.imdg.IMDGDistributedNames; +import ru.spcex.clearing.reports.config.BeanConfiguration; +import ru.spcex.clearing.reports.config.ReportBuildersConfig; +import ru.spcex.clearing.reports.config.TestConfig; +import ru.spcex.clearing.reports.config.validation.ValidationConfig; +import ru.spcex.clearing.reports.reports.SessionIdParam; +import ru.spcex.clearing.reports.reports.bp.UnfulfilledDealReportBuilder_; +import ru.spcex.clearing.test.config.ImdgTestConfig; +import ru.spcex.clearing.test.config.KafkaTestConfig; +import ru.spcex.platform.enumeration.Allowed; +import ru.spcex.platform.enumeration.Side; +import ru.spcex.platform.enumeration.WorkflowStatus; +import ru.spcex.platform.imdg.api.Imdg; +import ru.spcex.platform.imdg.api.ImdgProvider; + +import javax.annotation.PostConstruct; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.time.LocalDate; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@ExtendWith(SpringExtension.class) +@ContextConfiguration(classes = { + BeanConfiguration.class, + ValidationConfig.class, + ReportService.class, + ImdgTestConfig.class, + KafkaTestConfig.class, + ReportBuildersConfig.class, + TestConfig.class +}) +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class PrototypeTest { + private static File outFolder; + private Imdg registryImdg; + private Imdg companyImdg; + private Imdg registryCodeDictionaryImdg; + private Imdg executionDepositImdg; + private Imdg executionFondImdg; + private Imdg sessionImdg; + private Imdg companySymbolsImdg; + private Imdg profileDocumentImdg; + + private Long COMPANY_ID; + private Imdg

listingImdg; + private Imdg fixedIncomeSecurityImdg; + private Imdg tradingClearingRegistryImdg; + private Imdg paymentInstructionImdg; + private Imdg statementImdg; + private Imdg sTradeImdg; + + @Autowired + @Qualifier("hazelcastServiceTest") + private ImdgProvider imdgProvider; + + @Autowired + private UnfulfilledDealReportBuilder_ reportBuilder; + + @BeforeAll + public static void createTestFolder() { + outFolder = new File("out"); + outFolder.mkdir(); + } + + @AfterAll + public static void deleteTestFolder() throws IOException { + FileUtils.deleteDirectory(outFolder); + } + + + @Test + @Order(1) + public void test() throws IOException, CsvValidationException { + clearTestDir(); + + Company company = new Company(); + company.setWorkflowStatus(WorkflowStatus.Active.getKey()); + Long id = companyImdg.insert(company); + + Session session = new Session(); + Long sessionId = sessionImdg.insert(session); + + ExecutionFond executionFond = new ExecutionFond(); + executionFond.setSessionId(sessionId); + executionFond.setCompanyId(id); + executionFond.setCoverageStatus(Allowed.DENIED.getKey()); + executionFond.setSettlementCode("SETCODE"); + executionFond.setSide(Side.SELL.getKey()); + + STrades sTrades = new STrades(); + sTrades.setSettleCode("SETCODE"); + sTrades.setOperation(Side.SELL.getKey()); + sTrades.setClassCode("test-class-code"); + sTradeImdg.insert(sTrades); + + executionFond.setSecurityId(3007L); + executionFond.setExchangeExecutionId(777L); + executionFond.setTradingDate(LocalDate.of(2023, 6, 20)); + + executionFondImdg.insert(executionFond); + + SessionIdParam sessionIdParam = new SessionIdParam(); + sessionIdParam.setSessionId(sessionId); + + File outFile = reportBuilder.createReport(sessionIdParam, outFolder); + + File expectedFile = new File(getClass().getClassLoader().getResource("expected_reports_csv/bp/unfulfilled_deal_report_expected.csv").getFile()); + compareCSVFiles(expectedFile, outFile, Collections.emptyList()); + } + + private void compareCSVFiles(File expectedFile, File actualFile, List columnsForSkip) throws IOException, CsvValidationException { + if (columnsForSkip == null) columnsForSkip = Collections.emptyList(); + CSVParser csvParser = new CSVParserBuilder().withSeparator(',').withQuoteChar('\"').build(); + CSVReader expectedCSVReader = new CSVReaderBuilder(new FileReader(expectedFile)).withCSVParser(csvParser).build(); + CSVReader actualCSVReader = new CSVReaderBuilder(new FileReader(actualFile)).withCSVParser(csvParser).build(); + + Map expectedValueForColumn = new HashMap<>(); + Map actualValueForColumn = new HashMap<>(); + + String[] expectedHeaders = expectedCSVReader.readNext(); + String[] actualHeaders = actualCSVReader.readNext(); + assertEquals(expectedHeaders.length, actualHeaders.length); + + for (int headerIdx = 0; headerIdx < expectedHeaders.length; headerIdx++) { + String expectedHeader = expectedHeaders[headerIdx]; + String actualHeader = actualHeaders[headerIdx]; + if (!columnsForSkip.contains(expectedHeader)) expectedValueForColumn.put(expectedHeader, headerIdx); + if (!columnsForSkip.contains(actualHeader)) actualValueForColumn.put(actualHeader, headerIdx); + } + + do { + String[] expectedLines = expectedCSVReader.readNext(); + String[] actualLines = actualCSVReader.readNext(); + if (expectedLines == null || actualLines == null) { + if (expectedLines == null && actualLines == null) { + break; + } + Assertions.fail("expected and actual lines not equal"); + } + for (String expectedHeader : expectedValueForColumn.keySet()) { + int expectedIdx = expectedValueForColumn.get(expectedHeader); + int actualIdx = actualValueForColumn.get(expectedHeader); + String expected = expectedLines[expectedIdx]; + String actual = actualLines[actualIdx]; + assertEquals(expected, actual, expectedHeader); + } + } while (true); + + expectedCSVReader.close(); + actualCSVReader.close(); + } + + @PostConstruct + private void init() { + ImdgTestConfig.waitAvailableImdgProviderAndAddAdminWithDefaultId(); + registryImdg = imdgProvider.getImdg(IMDGDistributedNames.Map_Registry, Registry.class); + registryCodeDictionaryImdg = imdgProvider.getImdg(IMDGDistributedNames.Map_RegistryCodeDictionary, RegistryCodeDictionary.class); + + companyImdg = imdgProvider.getImdg(IMDGDistributedNames.Map_Company, Company.class); + Company company = new Company(); + company.setWorkflowStatus(WorkflowStatus.Active.getKey()); + company.setTradingCode("TRADING_CODE"); + COMPANY_ID = companyImdg.insert(company); + + executionDepositImdg = imdgProvider.getImdg(IMDGDistributedNames.Map_ExecutionDeposit, ExecutionDeposit.class); + executionFondImdg = imdgProvider.getImdg(IMDGDistributedNames.Map_ExecutionFond, ExecutionFond.class); + sessionImdg = imdgProvider.getImdg(IMDGDistributedNames.Map_Session, Session.class); + companySymbolsImdg = imdgProvider.getImdg(IMDGDistributedNames.Map_CompanySymbols, CompanySymbols.class); + profileDocumentImdg = imdgProvider.getImdg(IMDGDistributedNames.Map_ProfileDocument, ProfileDocument.class); + sTradeImdg = imdgProvider.getImdg(IMDGDistributedNames.Map_STrades, STrades.class); + } + + private void clearTestDir() throws IOException { + FileUtils.deleteDirectory(outFolder); + outFolder.mkdir(); + } +} diff --git a/clearing-parent/reports-service/src/test/resources/expected_reports_csv/bp/unfulfilled_deal_report_expected.csv b/clearing-parent/reports-service/src/test/resources/expected_reports_csv/bp/unfulfilled_deal_report_expected.csv index c917aaa25..b3881299a 100644 --- a/clearing-parent/reports-service/src/test/resources/expected_reports_csv/bp/unfulfilled_deal_report_expected.csv +++ b/clearing-parent/reports-service/src/test/resources/expected_reports_csv/bp/unfulfilled_deal_report_expected.csv @@ -1,2 +1,2 @@ -securityId,tradeNo,tradeDate,settleCode +SECURITYID,TRADENO,TRADEDATE,SETTLECODE "3007","777","20.06.2023","test-class-code" diff --git a/platform-parent/platform-enum/src/main/java/ru/spcex/platform/enumeration/ReportType.java b/platform-parent/platform-enum/src/main/java/ru/spcex/platform/enumeration/ReportType.java index 4bf2bb6bf..16dfdc33f 100644 --- a/platform-parent/platform-enum/src/main/java/ru/spcex/platform/enumeration/ReportType.java +++ b/platform-parent/platform-enum/src/main/java/ru/spcex/platform/enumeration/ReportType.java @@ -22,7 +22,9 @@ public enum ReportType implements IEnumKey { KS_REP_TRADES("ks_rep_trades"), KS_REP_CASH_NETTO("ks_rep_cash_netto"), KS_REP_DEPO_NETTO("ks_rep_depo_netto"), - KS_REP_FIRM_DETAILS("ks_rep_firm_details") + KS_REP_FIRM_DETAILS("ks_rep_firm_details"), + + SIMPLE_REPORT("simple_report") ; private final String key;