29#include <unordered_map>
30#include <unordered_set>
34#include <nlohmann/json.hpp>
40double parseNumber(
const std::string &value) {
42 const double parsed = std::strtod(value.c_str(), &end);
43 return end != value.c_str() ? parsed : 0.0;
46void appendUnique(std::vector<std::string> &values,
const std::string &value) {
48 std::find(values.begin(), values.end(), value) != values.end()) {
51 values.push_back(value);
54std::vector<core::ports::analysis::AnalysisTransaction>
55projectAnalysisTransactions(
56 const core::domain::catalog::WorkspaceCatalog &state,
57 const std::vector<std::shared_ptr<core::domain::Transaction>>
59 std::vector<core::ports::analysis::AnalysisTransaction> out;
60 out.reserve(transactions.size());
62 std::unordered_map<std::string, std::shared_ptr<core::domain::Contract>>
64 contractById.reserve(state.
contracts().size());
65 for (
const auto &contract : state.
contracts()) {
69 contractById.emplace(contract->id(), contract);
72 std::unordered_map<std::string, std::string> propertyNameById;
73 propertyNameById.reserve(state.
properties().size());
74 for (
const auto &property : state.
properties()) {
78 propertyNameById.emplace(property->id(), property->name());
81 for (
const auto &transaction : transactions) {
86 core::ports::analysis::AnalysisTransaction projected;
87 projected.
id = transaction->id();
88 projected.
name = transaction->name();
90 projected.
amount = transaction->amount();
91 projected.
contractId = transaction->contractId();
93 if (!transaction->contractId().empty()) {
94 const auto it = contractById.find(transaction->contractId());
95 if (it != contractById.end() && it->second) {
97 for (
const auto &propertyId : it->second->propertyIds()) {
107 for (
const auto &propertyId : projected.
propertyIds) {
108 const auto nameIt = propertyNameById.find(propertyId);
109 projected.
propertyNames.push_back(nameIt != propertyNameById.end() &&
110 !nameIt->second.empty()
114 out.push_back(std::move(projected));
120std::shared_ptr<core::domain::catalog::WorkspaceCatalog> catalogWithAdjustments(
121 const core::domain::catalog::WorkspaceCatalog &source,
122 const std::unordered_map<std::string, double> &adjustments) {
123 if (adjustments.empty()) {
127 auto catalog = std::make_shared<core::domain::catalog::WorkspaceCatalog>();
128 catalog->setContracts(source.
contracts());
130 catalog->setAnalyses(source.
analyses());
139 auto clone = std::make_shared<core::domain::Transaction>(
140 transaction->name(), transaction->bookingDate(), transaction->valuta(),
141 transaction->amount(), transaction->isAllocatable());
142 clone->setId(transaction->id());
143 clone->setStatementId(transaction->statementId());
144 clone->setContractId(transaction->contractId());
145 clone->setActorId(transaction->actorId());
146 clone->setPropertyIds(transaction->propertyIds());
147 clone->setStatus(transaction->status());
148 clone->setCreatedAt(transaction->createdAt());
149 clone->setUpdatedAt(transaction->updatedAt());
150 const auto adjustmentIt = adjustments.find(transaction->id());
151 if (adjustmentIt != adjustments.end()) {
152 clone->setAmount(adjustmentIt->second);
154 transactions.push_back(std::move(clone));
156 catalog->setTransactions(std::move(transactions));
160std::string jsonString(
const nlohmann::json &
object,
const char *key) {
161 const auto it =
object.find(key);
162 if (it ==
object.end() || !it->is_string()) {
165 return it->get<std::string>();
168double jsonDouble(
const nlohmann::json &
object,
const char *key) {
169 const auto it =
object.find(key);
170 if (it ==
object.end() || !it->is_number()) {
173 return it->get<
double>();
176bool jsonBool(
const nlohmann::json &
object,
const char *key) {
177 const auto it =
object.find(key);
178 if (it ==
object.end() || !it->is_boolean()) {
181 return it->get<
bool>();
184std::vector<std::string> jsonStringList(
const nlohmann::json &
object,
187core::ports::analysis::AnalysisConfigInput
188analysisConfigFromJson(
const std::string &raw,
const std::string &type) {
189 core::ports::analysis::AnalysisConfigInput out;
194 nlohmann::json config;
196 config = nlohmann::json::parse(raw);
200 if (!config.is_object()) {
215std::vector<std::string> jsonStringList(
const nlohmann::json &
object,
217 std::vector<std::string> out;
218 const auto it =
object.find(key);
219 if (it ==
object.end() || !it->is_array()) {
222 for (
const auto &item : *it) {
223 if (!item.is_string()) {
226 const auto value = item.get<std::string>();
227 if (!value.empty() &&
228 std::find(out.begin(), out.end(), value) == out.end()) {
229 out.push_back(value);
235std::shared_ptr<core::domain::catalog::WorkspaceCatalog>
236catalogFromAnalysisSnapshot(
const core::domain::Analysis &analysis) {
247 if (rows.is_object()) {
248 const auto transactions = rows.find(
"transactions");
249 rows = transactions != rows.end() ? *transactions : nlohmann::json{};
251 if (!rows.is_array()) {
255 auto catalog = std::make_shared<core::domain::catalog::WorkspaceCatalog>();
259 std::unordered_set<std::string> seenContracts;
260 std::unordered_set<std::string> seenProperties;
262 for (std::size_t index = 0; index < rows.size(); ++index) {
263 const auto &row = rows[index];
264 if (!row.is_object()) {
268 const std::string explicitId = jsonString(row,
"id");
269 const std::string transactionId = jsonString(row,
"transactionId");
270 const std::string
id =
273 : (!transactionId.empty()
275 :
"snapshot-transaction-" + std::to_string(index));
276 const std::string name = jsonString(row,
"name").empty()
277 ? jsonString(row,
"transactionName")
278 : jsonString(row,
"name");
279 const std::string bookingDate = jsonString(row,
"date");
280 const std::string valuta = jsonString(row,
"valuta");
281 const double amount = jsonDouble(row,
"amount");
282 const std::string contractId = jsonString(row,
"contractId");
283 const std::string contractType = jsonString(row,
"contractType");
284 const auto propertyIds = jsonStringList(row,
"propertyIds");
285 const auto propertyNames = jsonStringList(row,
"propertyNames");
287 auto transaction = std::make_shared<core::domain::Transaction>(
288 name, bookingDate, valuta, amount, jsonBool(row,
"allocatable"));
289 transaction->setId(
id);
290 transaction->setStatementId(jsonString(row,
"statementId"));
291 transaction->setContractId(contractId);
292 transaction->setPropertyIds(propertyIds);
293 transactions.push_back(std::move(transaction));
295 if (!contractId.empty() && seenContracts.insert(contractId).second) {
296 auto contract = std::make_shared<core::domain::Contract>();
297 contract->setId(contractId);
298 contract->rename(jsonString(row,
"contractName"));
299 contract->setType(contractType);
300 contract->setPropertyIds(propertyIds);
301 contracts.push_back(std::move(contract));
304 for (std::size_t p = 0; p < propertyIds.size(); ++p) {
305 const auto &propertyId = propertyIds[p];
306 if (propertyId.empty() || !seenProperties.insert(propertyId).second) {
309 auto property = std::make_shared<core::domain::Property>();
310 property->setId(propertyId);
311 property->rename(p < propertyNames.size() && !propertyNames[p].empty()
314 properties.push_back(std::move(property));
318 catalog->setTransactions(std::move(transactions));
319 catalog->setContracts(std::move(contracts));
320 catalog->setProperties(std::move(properties));
324std::filesystem::path analysisPreviewPath(
325 const core::ports::analysis::AnalysisRequest &request,
326 const core::ports::analysis::AnalysisResult &result) {
327 std::ostringstream fingerprint;
332 for (
const auto &row : result.
table) {
333 for (
const auto &column : row) {
334 fingerprint << column <<
'\0';
339 const auto hash = std::hash<std::string>{}(fingerprint.str());
340 std::ostringstream fileName;
341 fileName << std::hex << hash <<
".png";
344 std::filesystem::temp_directory_path() /
"fossredder-analysis-preview";
345 std::error_code error;
346 std::filesystem::create_directories(directory, error);
347 return directory / fileName.str();
356 : imageRenderer_(std::move(imageRenderer)) {}
361 return withRenderedArtifacts(
377 const std::string &analysisId,
const std::string &filterSpec)
const {
386 const std::string effectiveFilter =
387 filterSpec.empty() ?
analysis->filterSpec() : filterSpec;
397 const std::string &filterSpec)
const {
399 const auto frozenCatalog = catalogFromAnalysisSnapshot(
analysis);
400 const auto adjustedCatalog =
401 analysis.includeCalculationAdjustments()
402 ? catalogWithAdjustments(frozenCatalog ? *frozenCatalog : state,
404 : std::shared_ptr<core::domain::catalog::WorkspaceCatalog>{};
405 const auto &sourceState = adjustedCatalog
407 : (frozenCatalog ? *frozenCatalog : state);
411 const std::string key =
analysis.executionType();
415 sourceState, filter);
429 out.
transactions = core::application::analysis::projectAnalysisTransactions(
431 sourceState, filter));
441 if (!imageRenderer_ || !result.
found) {
445 const auto outputPath = analysisPreviewPath(request, result);
446 std::error_code error;
447 if (std::filesystem::exists(outputPath, error) ||
448 imageRenderer_->renderToImage(outputPath, request.
analysisId,
450 result.
artifacts.push_back(outputPath.string());
455std::vector<std::shared_ptr<core::domain::Transaction>>
458 const std::string &filterSpec)
const {
469 const std::string filterSpec =
472 std::unordered_map<std::string, std::shared_ptr<core::domain::Contract>>
474 contractById.reserve(state.
contracts().size());
475 for (
const auto &contract : state.
contracts()) {
476 if (!contract || contract->id().empty()) {
479 contractById.emplace(contract->id(), contract);
482 std::unordered_map<std::string, std::string> propertyNameById;
483 propertyNameById.reserve(state.
properties().size());
484 for (
const auto &property : state.
properties()) {
485 if (!property || property->id().empty()) {
488 propertyNameById.emplace(property->id(), property->name());
491 std::unordered_map<std::string, std::string> actorNameById;
492 actorNameById.reserve(
workspace.actors.size());
493 for (
const auto &actor :
workspace.actors) {
494 if (!actor.id.empty()) {
495 actorNameById.emplace(actor.id, actor.name);
499 std::unordered_map<std::string, std::string> statementNameById;
500 statementNameById.reserve(
workspace.statements.size());
501 for (
const auto &statement :
workspace.statements) {
502 if (!statement.id.empty()) {
503 statementNameById.emplace(statement.id, statement.name);
510 std::unordered_set<std::string> statementIds;
511 double amountSum = 0.0;
513 for (
const auto &transaction : filtered) {
519 row.
id = transaction->id();
520 row.
name = transaction->name();
522 row.
valuta = transaction->valuta();
523 row.
amount = transaction->amount();
525 row.
actorId = transaction->actorId();
530 const auto actorIt = actorNameById.find(row.
actorId);
531 if (actorIt != actorNameById.end()) {
534 const auto statementIt = statementNameById.find(row.
statementId);
535 if (statementIt != statementNameById.end()) {
539 const auto contractIt = contractById.find(row.
contractId);
540 if (contractIt != contractById.end() && contractIt->second) {
543 for (
const auto &propertyId : contractIt->second->propertyIds()) {
553 const auto nameIt = propertyNameById.find(propertyId);
554 row.
propertyNames.push_back(nameIt != propertyNameById.end() &&
555 !nameIt->second.empty()
576 const std::string &dateField,
const std::string &dateMode,
577 const std::string &year,
const std::string &dateFrom,
578 const std::string &dateTo,
const std::vector<std::string> &propertyIds,
580 const std::string &allocatableMode)
const {
582 dateField, dateMode, year, dateFrom, dateTo, propertyIds,
contractTypes,
603 const std::vector<core::ports::analysis::AnalysisAdjustmentTransactionInput>
605 const std::vector<std::string> &selectedTransactionIds,
606 double taxPercent)
const {
607 std::vector<AnalysisAdjustmentTransactionInput> coreTransactions;
608 coreTransactions.reserve(transactions.size());
609 for (
const auto &transaction : transactions) {
610 coreTransactions.push_back({transaction.id, transaction.amount});
614 selectedTransactionIds,
625 const std::string &analysisId,
bool includeCalculationAdjustments,
628 workspace, analysisId, includeCalculationAdjustments, adjustments);
634 bool includeCalculationAdjustments,
635 const std::string &unassignedLabel)
const {
638 std::map<std::string, std::map<std::string, double>> amountsByProperty;
639 std::map<std::string, double> totalsByProperty;
641 auto appendAmount = [&](
const std::string &transactionId,
642 const std::string &contractType,
643 const std::vector<std::string> &propertyNames,
645 const auto adjustmentIt =
646 std::find_if(adjustments.begin(), adjustments.end(),
647 [&](
const auto &adjustment) {
648 return adjustment.first == transactionId;
650 const double amount = includeCalculationAdjustments &&
651 adjustmentIt != adjustments.end()
652 ? adjustmentIt->second
654 const std::string effectiveContract =
655 contractType.empty() ? unassignedLabel : contractType;
660 if (propertyNames.empty()) {
661 amountsByProperty[unassignedLabel][effectiveContract] += amount;
662 totalsByProperty[unassignedLabel] += amount;
665 for (
const auto &propertyName : propertyNames) {
666 const std::string effectiveProperty =
667 propertyName.empty() ? unassignedLabel : propertyName;
668 amountsByProperty[effectiveProperty][effectiveContract] += amount;
669 totalsByProperty[effectiveProperty] += amount;
675 std::vector<std::string> propertyNames = transaction.propertyNames;
676 if (propertyNames.empty()) {
677 propertyNames = transaction.propertyIds;
679 appendAmount(transaction.id, transaction.contractType, propertyNames,
683 for (std::size_t i = 0; i < result.
table.size(); ++i) {
684 const auto &row = result.
table[i];
685 if (row.size() < 3) {
688 appendAmount(
"table-row-" + std::to_string(i), unassignedLabel,
689 {unassignedLabel}, parseNumber(row[2]));
695 for (
const auto &[propertyName, amountsByContract] : amountsByProperty) {
698 row.
total = totalsByProperty[propertyName];
701 const auto it = amountsByContract.find(contractType);
702 row.
amounts.push_back(it != amountsByContract.end() ? it->second : 0.0);
Declares adjustment analysis calculation helpers.
Domain model for persisted analysis definitions.
Declares internal analysis filter predicates and collection helpers.
Defines stable analysis type, configuration, filter, and result keys.
Declares the analysis execution result port contract.
Declares the application service that resolves and executes analyses.
Shared analysis request/config helpers for UI and application layers.
Domain model for contracts.
Declares the analysis rendering port used by the export layer.
Declares plot-oriented analysis aggregation helpers.
Domain model for properties.
Declares tabular analysis result projection helpers.
Domain model for transactions.
Domain aggregate that stores catalog entities for one workspace.
Maps workspace port snapshots into internal catalog state.
Aggregates transaction predicates parsed from one analysis filter specification.
std::string buildFilterSpec(const core::ports::analysis::AnalysisFilterSelection &selection) const override
Serializes a filter selection through the analysis use case.
core::ports::analysis::AnalysisFilterSelection filterSelectionFromFields(const std::string &dateField, const std::string &dateMode, const std::string &year, const std::string &dateFrom, const std::string &dateTo, const std::vector< std::string > &propertyIds, const std::vector< std::string > &contractTypes, const std::string &allocatableMode) const override
Builds a normalized filter selection from raw field values.
core::ports::analysis::AnalysisResult runAnalysisById(const core::domain::catalog::WorkspaceCatalog &state, const std::string &analysisId, const std::string &filterSpec={}) const
Resolves an analysis by identifier and executes it.
core::ports::analysis::AnalysisAdjustmentAmounts buildAnalysisAdjustments(const std::vector< core::ports::analysis::AnalysisAdjustmentTransactionInput > &transactions, const std::vector< std::string > &selectedTransactionIds, double taxPercent) const override
std::optional< double > parseAnalysisPercentText(const std::string &text) const override
Parses user-entered analysis percentage text.
std::vector< std::shared_ptr< core::domain::Transaction > > filterTransactions(const core::domain::catalog::WorkspaceCatalog &state, const std::string &filterSpec={}) const
Returns the transactions matched by one analysis filter specification.
core::ports::analysis::AnalysisResult runAnalysis(const core::domain::catalog::WorkspaceCatalog &state, const core::ports::analysis::AnalysisRequest &request) const
Runs one analysis request against the supplied workspace state.
std::vector< std::string > contractTypes(const core::ports::workspace::WorkspaceSnapshot &workspace) const override
Returns available contract types for analysis filters.
core::ports::analysis::AnalysisResult computeAnalysis(const core::domain::Analysis &analysis, const core::domain::catalog::WorkspaceCatalog &state, const std::string &filterSpec={}) const
Computes a concrete analysis definition against the supplied workspace state.
core::ports::analysis::AnalysisTableState projectTableState(const core::ports::analysis::AnalysisResult &result, const core::ports::analysis::AnalysisAdjustmentAmounts &adjustments, bool includeCalculationAdjustments, const std::string &unassignedLabel) const override
Projects analysis result table state for UI consumption.
core::ports::analysis::AnalysisPreviewResult previewTransactions(const core::ports::workspace::WorkspaceSnapshot &workspace, const core::ports::analysis::AnalysisFilterSelection &filter) const override
Returns port-native transaction preview rows for one filter specification.
AnalysisService()=default
void applyAnalysisPreviewOverrides(core::ports::workspace::WorkspaceSnapshot &workspace, const std::string &analysisId, bool includeCalculationAdjustments, const core::ports::analysis::AnalysisAdjustmentAmounts &adjustments) const override
Applies preview-only analysis fields to a workspace snapshot.
core::ports::analysis::AnalysisFilterSelection parseFilterSpec(const std::string &filterSpec) const override
Parses a serialized filter spec through the analysis use case.
std::string buildAnalysisConfigJson(const core::ports::analysis::AnalysisConfigInput &input) const override
Builds serialized analysis configuration from typed input.
const std::string & snapshotTransactionsJson() const noexcept
bool hasSnapshotTransactions() const noexcept
Checks whether snapshot transactions are present.
EntityList< core::domain::Contract > ContractList
EntityList< core::domain::Transaction > TransactionList
const PropertyList & properties() const noexcept
Returns all properties in the catalog.
EntityList< core::domain::Property > PropertyList
const StatementList & statements() const noexcept
Returns all statements in the catalog.
const TransactionList & transactions() const noexcept
Returns all transactions in the catalog.
const ContractList & contracts() const noexcept
Returns all contracts in the catalog.
const AnalysisList & analyses() const noexcept
Returns all analyses in the catalog.
Renders analysis results to image artifacts.
constexpr std::string_view kPercentKey
constexpr std::string_view kTypeCalculation
constexpr std::string_view kTypePlot
constexpr std::string_view kPlotTypeKey
constexpr std::string_view kTypeTab
constexpr std::string_view kPropertiesKey
constexpr std::string_view kPlotMeasureKey
constexpr std::string_view kContractTypesKey
std::vector< std::string > contractTypesFromSnapshot(const core::ports::workspace::WorkspaceSnapshot &workspace)
core::ports::analysis::AnalysisFilterSelection filterSelectionFromFields(const std::string &dateField, const std::string &dateMode, const std::string &year, const std::string &dateFrom, const std::string &dateTo, const std::vector< std::string > &propertyIds, const std::vector< std::string > &contractTypes, const std::string &allocatableMode)
core::ports::analysis::AnalysisResult computeTableAnalysis(const core::domain::Analysis &analysis, const core::domain::catalog::WorkspaceCatalog &state, const AnalysisFilter &filter)
Computes table rows for transactions matched by one analysis.
std::string buildAnalysisAdjustmentsJson(const std::vector< AnalysisAdjustmentTransactionInput > &transactions, const std::vector< std::string > &selectedTransactionIds, double taxPercent)
core::ports::analysis::AnalysisResult computePlotAnalysis(const core::domain::Analysis &analysis, const core::domain::catalog::WorkspaceCatalog &state, const AnalysisFilter &filter)
Computes plot metrics and table rows for one analysis definition.
std::vector< std::shared_ptr< core::domain::Transaction > > collectAnalysisTransactions(const core::domain::catalog::WorkspaceCatalog &state, const AnalysisFilter &filter)
Collects transactions that satisfy the supplied filter.
std::optional< double > parseAnalysisPercentText(const std::string &text)
Parses a user-entered analysis percentage.
core::ports::analysis::AnalysisResult computeAdjustmentAnalysis(const core::domain::Analysis &analysis, const core::domain::catalog::WorkspaceCatalog &state, const AnalysisFilter &filter)
Computes adjusted transaction rows for one analysis definition.
AnalysisFilter parseAnalysisFilterSpec(const std::string &spec)
Parses a raw filter specification into predicate form.
void applyAnalysisPreviewOverrides(core::ports::workspace::WorkspaceSnapshot &workspace, const std::string &analysisId, bool includeCalculationAdjustments, const std::vector< std::pair< std::string, double > > &adjustments)
std::vector< std::pair< std::string, double > > parseAnalysisAdjustmentsJson(const std::string &adjustmentsJson)
std::string buildAnalysisConfigJson(const core::ports::analysis::AnalysisConfigInput &input)
core::domain::catalog::WorkspaceCatalog toWorkspaceCatalog(const core::ports::workspace::WorkspaceSnapshot &snapshot)
AnalysisFilterSelection parseAnalysisFilterSelection(const std::string &filterSpec)
Parses a serialized analysis filter specification.
std::string buildAnalysisFilterSpec(const AnalysisFilterSelection &selection)
Serializes an analysis filter selection.
std::vector< std::pair< std::string, double > > AnalysisAdjustmentAmounts
Adjustment amount pairs keyed by transaction id.
AnalysisPreviewMetrics metrics
std::vector< AnalysisPreviewTransaction > transactions
std::string statementName
std::vector< std::string > propertyIds
std::vector< std::string > propertyNames
AnalysisFilterSelection filter
std::vector< std::string > artifacts
std::vector< AnalysisTransaction > transactions
AnalysisConfigInput config
std::vector< std::vector< std::string > > table
std::vector< double > amounts
std::vector< std::string > contractTypes
std::vector< AnalysisTablePropertyRow > propertyRows
std::vector< std::string > propertyIds
std::vector< std::string > propertyNames