This commit is contained in:
akulikov 2023-07-07 12:38:45 +03:00
parent 6ff6d246a4
commit 7179c09c29
6 changed files with 493 additions and 2 deletions

View file

@ -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 <P> тип параметров, используемых при построении отчета
*/
public abstract class CSVReportBuilder_<P, R> {
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<R> getRows();
/**
* Название отчета, используется для формирования имени отчета (без указания формата)
*
* @param params Параметры формирования отчета
*/
protected abstract String getReportFilename(P params);
/**
* Тип отчета по {@link ReportType}
*/
public abstract ReportType getReportType();
/**
* Тип параметров для построения отчетов
*/
public abstract Class<P> getParamsClass();
public abstract Class<R> 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<R> lines = getRows();
assert lines != null;
try (Writer writer = new FileWriter(reportFile)) {
HeaderColumnNameMappingStrategy<R> headerColumnNameMappingStrategy = new HeaderColumnNameMappingStrategy<>();
headerColumnNameMappingStrategy.setType(getReportClass());
StatefulBeanToCsv<R> sbc = new StatefulBeanToCsvBuilder<R>(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;
}
}

View file

@ -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;
}
}

View file

@ -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_<SessionIdParam, UnfulfilledDealReport> {
private final Imdg<ExecutionFond> executionFondImdg;
private final Imdg<Company> companyImdg;
private final Imdg<STrades> 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<UnfulfilledDealReport> rows = null;
@Override
protected List<UnfulfilledDealReport> 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<SessionIdParam> getParamsClass() {
return SessionIdParam.class;
}
@Override
public Class<UnfulfilledDealReport> 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<ExecutionFond> executionFonds = executionFondImdg.getCollectionObjectsByPredicate(predicate);
Map<Long, Boolean> 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);
}
}
}
}

View file

@ -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<Registry> registryImdg;
private Imdg<Company> companyImdg;
private Imdg<RegistryCodeDictionary> registryCodeDictionaryImdg;
private Imdg<ExecutionDeposit> executionDepositImdg;
private Imdg<ExecutionFond> executionFondImdg;
private Imdg<Session> sessionImdg;
private Imdg<CompanySymbols> companySymbolsImdg;
private Imdg<ProfileDocument> profileDocumentImdg;
private Long COMPANY_ID;
private Imdg<Listing> listingImdg;
private Imdg<FixedIncomeSecurity> fixedIncomeSecurityImdg;
private Imdg<TradingClearingRegistry> tradingClearingRegistryImdg;
private Imdg<PaymentInstruction> paymentInstructionImdg;
private Imdg<Statement> statementImdg;
private Imdg<STrades> 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<String> 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<String, Integer> expectedValueForColumn = new HashMap<>();
Map<String, Integer> 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();
}
}

View file

@ -1,2 +1,2 @@
securityId,tradeNo,tradeDate,settleCode
SECURITYID,TRADENO,TRADEDATE,SETTLECODE
"3007","777","20.06.2023","test-class-code"

1 securityId SECURITYID tradeNo TRADENO tradeDate TRADEDATE settleCode SETTLECODE
2 3007 3007 777 777 20.06.2023 20.06.2023 test-class-code test-class-code

View file

@ -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;