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;