19#include <unordered_map>
21#include <nlohmann/json.hpp>
27using ContractTypeIndex = std::unordered_map<std::string, std::string>;
28using ContractPropertyIndex = std::unordered_map<std::string, std::vector<std::string>>;
29using PropertyNameIndex = std::unordered_map<std::string, std::string>;
34 std::vector<std::string> propertyAnalysisFilter;
35 std::vector<std::string> contractTypeAnalysisFilter;
38std::string normalizeValue(
const std::string& value);
39std::string normalizePlotMeasure(
const std::string& value);
41PlotConfig parsePlotConfig(
const core::domain::Analysis& analysis)
50 if (!
json.is_object()) {
55 if (plotTypeIt !=
json.end() && plotTypeIt->is_string()) {
56 config.plotType = normalizeValue(plotTypeIt->get<std::string>());
60 if (plotMeasureIt !=
json.end() && plotMeasureIt->is_string()) {
61 config.plotMeasure = normalizePlotMeasure(plotMeasureIt->get<std::string>());
65 if (propertiesIt !=
json.end() && propertiesIt->is_array()) {
66 for (
const auto& entry : *propertiesIt) {
67 if (entry.is_string()) {
68 config.propertyAnalysisFilter.push_back(entry.get<std::string>());
74 if (contractTypesIt !=
json.end() && contractTypesIt->is_array()) {
75 for (
const auto& entry : *contractTypesIt) {
76 if (entry.is_string()) {
77 config.contractTypeAnalysisFilter.push_back(entry.get<std::string>());
86std::string normalizeValue(
const std::string& value)
89 if (normalized.empty()) {
93 for (
auto& ch : normalized) {
94 ch =
static_cast<char>(std::tolower(
static_cast<unsigned char>(ch)));
100std::vector<std::string> normalizeList(
const std::vector<std::string>& values)
102 std::vector<std::string> normalizedValues;
103 normalizedValues.reserve(values.size());
104 for (
const auto& value : values) {
105 const auto normalized = normalizeValue(value);
106 if (!normalized.empty()) {
107 normalizedValues.push_back(normalized);
110 return normalizedValues;
113std::string normalizePlotMeasure(
const std::string& value)
115 const auto normalized = normalizeValue(value);
119 if (normalized ==
"averageamount" || normalized ==
"average amount") {
122 if (normalized ==
"totalamount" || normalized ==
"total amount") {
128ContractTypeIndex buildContractTypeIndex(
const core::domain::catalog::WorkspaceCatalog& state,
bool normalize)
130 ContractTypeIndex index;
132 for (
const auto& contract : state.
contracts()) {
136 index.emplace(contract->id(), normalize ? normalizeValue(contract->type()) : contract->type());
141ContractPropertyIndex buildContractPropertyIndex(
const core::domain::catalog::WorkspaceCatalog& state)
143 ContractPropertyIndex index;
145 for (
const auto& contract : state.
contracts()) {
149 index.emplace(contract->id(), contract->propertyIds());
154PropertyNameIndex buildPropertyNameIndex(
const core::domain::catalog::WorkspaceCatalog& state)
156 PropertyNameIndex index;
158 for (
const auto& property : state.
properties()) {
162 index.emplace(property->id(), property->name().empty() ? property->id() : property->name());
167std::string propertyLabelForId(
const PropertyNameIndex& propertyNameById,
168 const std::string& propertyId)
170 const auto it = propertyNameById.find(propertyId);
171 if (it == propertyNameById.end() || it->second.empty()) {
177std::vector<std::string> effectivePropertyIds(
const core::domain::Transaction& transaction,
178 const ContractPropertyIndex& propertyIdsByContractId)
180 std::vector<std::string> ids = transaction.
propertyIds();
185 const auto it = propertyIdsByContractId.find(transaction.
contractId());
186 if (it == propertyIdsByContractId.end()) {
190 for (
const auto& propertyId : it->second) {
191 if (std::find(ids.begin(), ids.end(), propertyId) == ids.end()) {
192 ids.push_back(propertyId);
198bool matchesPropertyAnalysisFilter(
const core::domain::Transaction& transaction,
199 const std::vector<std::string>& propertyAnalysisFilter,
200 const ContractPropertyIndex& propertyIdsByContractId)
202 if (propertyAnalysisFilter.empty()) {
206 const auto propertyIds = effectivePropertyIds(transaction, propertyIdsByContractId);
207 for (
const auto& propertyId : propertyIds) {
208 if (std::find(propertyAnalysisFilter.begin(), propertyAnalysisFilter.end(), propertyId) != propertyAnalysisFilter.end()) {
216bool matchesContractTypeAnalysisFilter(
const core::domain::Transaction& transaction,
217 const std::vector<std::string>& normalizedContractTypeAnalysisFilter,
218 const ContractTypeIndex& normalizedContractTypeById)
220 if (normalizedContractTypeAnalysisFilter.empty()) {
225 return std::find(normalizedContractTypeAnalysisFilter.begin(),
226 normalizedContractTypeAnalysisFilter.end(),
230 const auto it = normalizedContractTypeById.find(transaction.
contractId());
231 if (it == normalizedContractTypeById.end()) {
235 return std::find(normalizedContractTypeAnalysisFilter.begin(),
236 normalizedContractTypeAnalysisFilter.end(),
237 it->second) != normalizedContractTypeAnalysisFilter.end();
240std::vector<std::shared_ptr<core::domain::Transaction>> collectMatchedTransactions(
const core::domain::catalog::WorkspaceCatalog& state,
242 const std::vector<std::string>& propertyAnalysisFilter,
243 const std::vector<std::string>& normalizedContractTypeAnalysisFilter,
244 const ContractTypeIndex& normalizedContractTypeById,
245 const ContractPropertyIndex& propertyIdsByContractId)
247 std::vector<std::shared_ptr<core::domain::Transaction>> matched;
253 if (!filter.empty() && !filter.matches(transaction, state)) {
256 if (!matchesPropertyAnalysisFilter(*transaction, propertyAnalysisFilter, propertyIdsByContractId)) {
259 if (!matchesContractTypeAnalysisFilter(*transaction, normalizedContractTypeAnalysisFilter, normalizedContractTypeById)) {
262 matched.push_back(transaction);
268std::string extractYearMonth(
const std::string& bookingDate)
275 if (value.size() >= 7
276 && std::isdigit(
static_cast<unsigned char>(value[0]))
277 && std::isdigit(
static_cast<unsigned char>(value[1]))
278 && std::isdigit(
static_cast<unsigned char>(value[2]))
279 && std::isdigit(
static_cast<unsigned char>(value[3]))) {
280 const std::string year = value.substr(0, 4);
281 if (value[4] ==
'-') {
282 return year +
"-" + value.substr(5, 2);
285 if (value.size() >= 6) {
286 return year +
"-" + value.substr(4, 2);
290 if (value.find(
'.') != std::string::npos) {
291 std::vector<std::string> parts;
292 std::istringstream stream(value);
294 while (std::getline(stream, token,
'.')) {
298 if (parts.size() == 3) {
299 std::string month = parts[1];
300 if (parts[2].size() == 4 && (month.size() == 1 || month.size() == 2)) {
301 if (month.size() == 1) {
302 month.insert(month.begin(),
'0');
304 return parts[2] +
"-" + month;
312core::ports::analysis::AnalysisResult buildPieResult(
313 const std::vector<std::shared_ptr<core::domain::Transaction>>& matched,
314 const ContractTypeIndex& normalizedContractTypeById,
315 const std::string& plotMeasure)
317 core::ports::analysis::AnalysisResult result;
318 std::map<std::string, double> aggregatedAmounts;
319 std::map<std::string, int> transactionCounts;
321 for (
const auto& transaction : matched) {
324 const auto it = normalizedContractTypeById.find(transaction->
contractId());
325 if (it != normalizedContractTypeById.end() && !it->second.empty()) {
332 aggregatedAmounts[key] += transaction->
amount();
333 transactionCounts[key]++;
336 double totalAmount = 0.0;
337 std::size_t totalCount = 0;
338 for (
const auto& [key, amount] : aggregatedAmounts) {
339 totalAmount += std::fabs(amount);
340 totalCount += transactionCounts[key];
343 for (
const auto& [key, amount] : aggregatedAmounts) {
346 value =
static_cast<double>(transactionCounts[key]);
348 value = transactionCounts[key] > 0 ? std::fabs(amount) / transactionCounts[key] : 0.0;
350 value = std::fabs(amount);
353 result.
table.push_back({key, std::to_string(value)});
361core::ports::analysis::AnalysisResult buildHistogramResult(
362 const std::vector<std::shared_ptr<core::domain::Transaction>>& matched,
363 const core::domain::catalog::WorkspaceCatalog& state,
364 const ContractPropertyIndex& propertyIdsByContractId)
366 core::ports::analysis::AnalysisResult result;
367 if (matched.empty()) {
371 const auto contractTypeByIdRaw = buildContractTypeIndex(state,
false);
372 const auto propertyNameById = buildPropertyNameIndex(state);
373 std::map<std::string, double> monthTotal;
374 std::map<std::string, std::map<std::string, double>> monthByContract;
375 std::map<std::string, std::map<std::string, double>> monthByProperty;
377 for (
const auto& transaction : matched) {
378 const std::string month = extractYearMonth(transaction->
bookingDate());
379 const double amount = std::fabs(transaction->
amount());
380 monthTotal[month] += amount;
384 const auto it = contractTypeByIdRaw.find(transaction->
contractId());
385 if (it != contractTypeByIdRaw.end() && !it->second.empty()) {
386 contractType = it->second;
391 monthByContract[month][contractType] += amount;
393 const auto propertyIds = effectivePropertyIds(*transaction, propertyIdsByContractId);
394 if (!propertyIds.empty()) {
395 for (
const auto& propertyId : propertyIds) {
396 monthByProperty[month][propertyLabelForId(propertyNameById, propertyId)] += amount;
403 std::vector<std::string> months;
404 months.reserve(monthTotal.size());
405 for (
const auto& [month, amount] : monthTotal) {
407 months.push_back(month);
409 std::sort(months.begin(), months.end());
411 double totalAmount = 0.0;
412 for (
const auto& month : months) {
413 nlohmann::json summary;
417 for (
const auto& [label, amount] : monthByContract[month]) {
421 for (
const auto& [label, amount] : monthByProperty[month]) {
425 result.
table.push_back({month, summary.dump()});
426 totalAmount += monthTotal[month];
437core::ports::analysis::AnalysisResult
442 const PlotConfig config = parsePlotConfig(
analysis);
443 const ContractTypeIndex normalizedContractTypeById = buildContractTypeIndex(state,
true);
444 const ContractPropertyIndex propertyIdsByContractId = buildContractPropertyIndex(state);
445 const auto normalizedPropertyFilter = normalizeList(config.propertyAnalysisFilter);
446 const auto normalizedContractTypeFilter = normalizeList(config.contractTypeAnalysisFilter);
447 const auto matched = collectMatchedTransactions(state,
449 normalizedPropertyFilter,
450 normalizedContractTypeFilter,
451 normalizedContractTypeById,
452 propertyIdsByContractId);
456 out = buildHistogramResult(matched, state, propertyIdsByContractId);
458 out = buildPieResult(matched, normalizedContractTypeById, config.plotMeasure);
Declares internal analysis filter predicates and collection helpers.
Defines stable analysis type, configuration, filter, and result keys.
Domain model for contracts.
Declares plot-oriented analysis aggregation helpers.
Domain model for properties.
Declares private string helpers shared across core implementation files.
Domain model for transactions.
Domain aggregate that stores catalog entities for one workspace.
Aggregates transaction predicates parsed from one analysis filter specification.
const std::string & configJson() const noexcept
const std::vector< std::string > & propertyIds() const noexcept
const std::string & bookingDate() const noexcept
const double & amount() const noexcept
const std::string & contractId() const noexcept
const PropertyList & properties() const noexcept
Returns all properties 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.
constexpr std::string_view kUnassigned
constexpr std::string_view kNoProperty
constexpr std::string_view kTotalAmount
constexpr std::string_view kMatchedTransactions
constexpr std::string_view kRowCount
constexpr std::string_view kCount
constexpr std::string_view kAverageAmount
constexpr std::string_view kTotalAmount
constexpr std::string_view kHistogram
constexpr std::string_view kPie
constexpr std::string_view kMonth
constexpr std::string_view kByContract
constexpr std::string_view kByProperty
constexpr std::string_view kTotal
constexpr std::string_view kPlotTypeKey
constexpr std::string_view kPropertiesKey
constexpr std::string_view kPlotMeasureKey
constexpr std::string_view kContractTypesKey
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::string trim(std::string s)
Removes leading and trailing ASCII whitespace from a string.
std::vector< std::vector< std::string > > table
std::map< std::string, double > metrics