FOSSredder 0.5.0
Loading...
Searching...
No Matches
AnnualService.cpp
Go to the documentation of this file.
1
5
6#include "core/pch.h"
7
10
11#include <unordered_map>
12#include <unordered_set>
13
15
16namespace annual_ports = core::ports::annual;
17
18namespace {
19
20struct SnapshotTx {
21 std::string id;
22 std::string name;
23 std::string bookingDate;
24 double amount = 0.0;
25 bool allocatable = false;
26 std::string contractId;
27 std::string statementId;
28 std::vector<std::string> propertyIds;
29 std::string sourceAnalysisId;
30 std::string sourceAnalysisName;
31};
32
33void appendMissingLiveRow(annual_ports::AnnualResult &result,
34 const annual_ports::AnnualRowResult &row,
35 std::unordered_set<std::string> &seenKeys) {
36 if (!row.missingLive) {
37 return;
38 }
39 const std::string key = row.key.empty() ? row.transactionId : row.key;
40 if (key.empty() || !seenKeys.insert(key).second) {
41 return;
42 }
43 result.missingLive.push_back(row);
44}
45
46int bookingYear(const std::string &value) {
47 if (value.size() < 4)
48 return -1;
49
50 auto parseYearAt = [&](size_t index) -> int {
51 if (index + 4 > value.size())
52 return -1;
53 for (size_t i = index; i < index + 4; ++i) {
54 if (value[i] < '0' || value[i] > '9')
55 return -1;
56 }
57 try {
58 const int year = std::stoi(value.substr(index, 4));
59 return (year >= 1900 && year <= 2500) ? year : -1;
60 } catch (...) {
61 return -1;
62 }
63 };
64
65 const int leading = parseYearAt(0);
66 if (leading > 0)
67 return leading;
68
69 for (size_t i = 0; i + 4 <= value.size(); ++i) {
70 const int year = parseYearAt(i);
71 if (year > 0)
72 return year;
73 }
74 return -1;
75}
76
77std::string exactKey(const SnapshotTx &tx) {
78 return tx.bookingDate + "|" + std::to_string(tx.amount) + "|" +
79 tx.contractId + "|" + tx.statementId;
80}
81
82} // namespace
83
84annual_ports::AnnualResult AnnualService::runAnnual(
86 const annual_ports::AnnualRequest &request) const {
87 if (!request.previewAnalysisIds.empty() || request.previewYear != 0) {
88 const auto previewWorkspace =
89 withPreviewAnnual(workspace, request.annualId,
90 request.previewAnalysisIds, request.previewYear);
91 return sortAnnualResult(buildAnnualResult(previewWorkspace,
92 request.annualId.empty()
93 ? std::string(kPreviewAnnualId)
94 : request.annualId));
95 }
96 return sortAnnualResult(buildAnnualResult(workspace, request.annualId));
97}
98
99annual_ports::AnnualResult AnnualService::buildAnnualResult(
101 const std::string &annualId) const {
102 using namespace core::ports::workspace;
103
104 annual_ports::AnnualResult out;
105 out.annualId = annualId;
106 std::unordered_set<std::string> missingLiveKeys;
107
108 const AnnualSnapshot *annual = nullptr;
109 for (const auto &row : workspace.annuals) {
110 if (row.id == annualId) {
111 annual = &row;
112 break;
113 }
114 }
115 if (!annual)
116 return out;
117
118 out.annualName = annual->name;
119 out.year = annual->year;
120 out.stats.assignedAnalysisCount =
121 static_cast<int>(annual->analysisIds.size());
122
123 std::unordered_map<std::string, AnalysisSnapshot> analysisById;
124 analysisById.reserve(workspace.analyses.size());
125 for (const auto &analysis : workspace.analyses) {
126 analysisById.emplace(analysis.id, analysis);
127 }
128
129 std::unordered_map<std::string, TransactionSnapshot> liveById;
130 liveById.reserve(workspace.transactions.size());
131 for (const auto &tx : workspace.transactions) {
132 liveById.emplace(tx.id, tx);
133 }
134
135 std::unordered_map<std::string, std::string> contractTypeById;
136 contractTypeById.reserve(workspace.contracts.size());
137 for (const auto &contract : workspace.contracts) {
138 contractTypeById.emplace(contract.id, contract.type);
139 }
140
141 std::vector<SnapshotTx> allSnapshotRows;
142 for (const auto &analysisId : annual->analysisIds) {
143 const auto it = analysisById.find(analysisId);
144 if (it == analysisById.end())
145 continue;
146 for (const auto &row : it->second.snapshotTransactions) {
147 SnapshotTx tx;
148 tx.id = row.id;
149 tx.name = row.name;
150 tx.bookingDate = row.bookingDate;
151 tx.amount = row.amount;
152 tx.allocatable = row.allocatable;
153 tx.contractId = row.contractId;
154 tx.statementId = row.statementId;
155 tx.propertyIds = row.propertyIds;
156 tx.sourceAnalysisId = it->second.id;
157 tx.sourceAnalysisName = it->second.name;
158 allSnapshotRows.push_back(std::move(tx));
159 }
160 }
161 out.stats.snapshotTransactionCount = static_cast<int>(allSnapshotRows.size());
162
163 std::unordered_map<std::string, std::vector<SnapshotTx>> byExact;
164 std::unordered_map<std::string, std::vector<SnapshotTx>> byDateContract;
165 byExact.reserve(allSnapshotRows.size());
166 byDateContract.reserve(allSnapshotRows.size());
167
168 for (const auto &tx : allSnapshotRows) {
169 byExact[exactKey(tx)].push_back(tx);
170 byDateContract[tx.bookingDate + "|" + tx.contractId + "|" + tx.statementId]
171 .push_back(tx);
172 }
173
174 auto buildRow = [&](const std::vector<SnapshotTx> &group, bool similar,
175 bool divergent) {
176 annual_ports::AnnualRowResult row;
177 if (group.empty())
178 return row;
179 const auto &base = group.front();
180 row.key = similar
181 ? ("sim|" + exactKey(base))
182 : (divergent ? ("div|" + exactKey(base)) : exactKey(base));
183 row.transactionId = base.id;
184 row.transactionName = base.name;
185 row.bookingDate = base.bookingDate;
186 row.amount = base.amount;
187 row.allocatable = base.allocatable;
188 row.contractId = base.contractId;
189 row.statementId = base.statementId;
190 row.duplicateCount = static_cast<int>(group.size());
191 row.calcVariant = similar;
192 std::unordered_set<std::string> srcIds;
193 for (const auto &item : group) {
194 if (!srcIds.insert(item.sourceAnalysisId).second)
195 continue;
196 row.sourceAnalysisIds.push_back(item.sourceAnalysisId);
197 row.sourceAnalysisNames.push_back(item.sourceAnalysisName);
198 }
199 const auto contractIt = contractTypeById.find(row.contractId);
200 if (contractIt != contractTypeById.end())
201 row.contractType = contractIt->second;
202
203 auto liveIt = row.transactionId.empty() ? liveById.end()
204 : liveById.find(row.transactionId);
205 const bool hasLive = liveIt != liveById.end();
206 row.missingLive = !hasLive;
207 const int year = bookingYear(row.bookingDate);
208 row.mixedYear = out.year > 0 && year > 0 && year != out.year;
209 if (hasLive) {
210 row.status = static_cast<int>(liveIt->second.status);
211 row.allocatable = liveIt->second.allocatable;
212 row.contractId = liveIt->second.contractId;
213 row.statementId = liveIt->second.statementId;
214 const auto cIt = contractTypeById.find(row.contractId);
215 row.contractType = cIt != contractTypeById.end() ? cIt->second : "";
216 }
217 return row;
218 };
219
220 if (annual->analysisIds.size() <= 1) {
221 for (const auto &tx : allSnapshotRows) {
222 auto row = buildRow({tx}, false, false);
223 out.deduplicated.push_back(row);
224 if (row.missingLive) {
225 out.stats.missingLive += 1;
226 appendMissingLiveRow(out, row, missingLiveKeys);
227 }
228 if (row.mixedYear)
229 out.stats.mixedYear += 1;
230 }
231
232 std::unordered_set<std::string> snapshotIds;
233 for (const auto &tx : allSnapshotRows)
234 if (!tx.id.empty())
235 snapshotIds.insert(tx.id);
236 for (const auto &live : workspace.transactions) {
237 if (snapshotIds.count(live.id) > 0)
238 continue;
239 if (out.year > 0 && bookingYear(live.bookingDate) != out.year)
240 continue;
241 annual_ports::AnnualRowResult row;
242 row.key = "live|" + live.id;
243 row.transactionId = live.id;
244 row.transactionName = live.name;
245 row.bookingDate = live.bookingDate;
246 row.amount = live.amount;
247 row.status = static_cast<int>(live.status);
248 row.allocatable = live.allocatable;
249 row.contractId = live.contractId;
250 row.statementId = live.statementId;
251 const auto cIt = contractTypeById.find(live.contractId);
252 row.contractType = cIt != contractTypeById.end() ? cIt->second : "";
253 out.workspaceOnly.push_back(row);
254 out.stats.missingFromYear += 1;
255 }
256
257 auto updateStatus = [&](const annual_ports::AnnualRowResult &row) {
258 if (row.status == 1)
259 out.stats.unverified += 1;
260 else if (row.status == 2)
261 out.stats.verified += 1;
262 else if (row.status == 3)
263 out.stats.completed += 1;
264 else
265 out.stats.neutral += 1;
266 };
267 for (const auto &row : out.deduplicated)
268 updateStatus(row);
269 return out;
270 }
271
272 std::unordered_set<std::string> consumedExact;
273 consumedExact.reserve(byExact.size());
274 for (const auto &[key, group] : byExact) {
275 if (group.empty())
276 continue;
277 if (group.size() > 1) {
278 auto row = buildRow(group, false, false);
279 out.deduplicated.push_back(row);
280 out.stats.duplicateCount += 1;
281 if (row.missingLive) {
282 out.stats.missingLive += 1;
283 appendMissingLiveRow(out, row, missingLiveKeys);
284 }
285 if (row.mixedYear)
286 out.stats.mixedYear += 1;
287 consumedExact.insert(key);
288 }
289 }
290
291 for (const auto &[clusterKey, cluster] : byDateContract) {
292 if (cluster.size() < 2)
293 continue;
294 std::unordered_map<double, std::vector<SnapshotTx>> byAmount;
295 for (const auto &tx : cluster)
296 byAmount[tx.amount].push_back(tx);
297 if (byAmount.size() < 2)
298 continue;
299
300 std::vector<SnapshotTx> similarGroup;
301 for (const auto &[amount, items] : byAmount) {
302 (void)amount;
303 for (const auto &tx : items) {
304 if (consumedExact.count(exactKey(tx)) > 0)
305 continue;
306 similarGroup.push_back(tx);
307 }
308 }
309 if (similarGroup.size() > 1) {
310 auto row = buildRow(similarGroup, true, false);
311 out.similar.push_back(row);
312 if (row.missingLive) {
313 out.stats.missingLive += 1;
314 appendMissingLiveRow(out, row, missingLiveKeys);
315 }
316 if (row.mixedYear)
317 out.stats.mixedYear += 1;
318 for (const auto &tx : similarGroup)
319 consumedExact.insert(exactKey(tx));
320 }
321 }
322
323 for (const auto &tx : allSnapshotRows) {
324 const auto key = exactKey(tx);
325 if (consumedExact.count(key) > 0)
326 continue;
327 auto row = buildRow({tx}, false, true);
328 out.divergent.push_back(row);
329 if (row.missingLive) {
330 out.stats.missingLive += 1;
331 appendMissingLiveRow(out, row, missingLiveKeys);
332 }
333 if (row.mixedYear)
334 out.stats.mixedYear += 1;
335 }
336
337 std::unordered_set<std::string> snapshotIds;
338 for (const auto &tx : allSnapshotRows)
339 if (!tx.id.empty())
340 snapshotIds.insert(tx.id);
341 for (const auto &live : workspace.transactions) {
342 if (snapshotIds.count(live.id) > 0)
343 continue;
344 if (out.year > 0 && bookingYear(live.bookingDate) != out.year)
345 continue;
346 annual_ports::AnnualRowResult row;
347 row.key = "live|" + live.id;
348 row.transactionId = live.id;
349 row.transactionName = live.name;
350 row.bookingDate = live.bookingDate;
351 row.amount = live.amount;
352 row.status = static_cast<int>(live.status);
353 row.allocatable = live.allocatable;
354 row.contractId = live.contractId;
355 row.statementId = live.statementId;
356 const auto cIt = contractTypeById.find(live.contractId);
357 row.contractType = cIt != contractTypeById.end() ? cIt->second : "";
358 out.workspaceOnly.push_back(row);
359 out.stats.missingFromYear += 1;
360 }
361
362 auto updateStatus = [&](const annual_ports::AnnualRowResult &row) {
363 if (row.status == 1)
364 out.stats.unverified += 1;
365 else if (row.status == 2)
366 out.stats.verified += 1;
367 else if (row.status == 3)
368 out.stats.completed += 1;
369 else
370 out.stats.neutral += 1;
371 };
372 for (const auto &row : out.deduplicated)
373 updateStatus(row);
374 for (const auto &row : out.similar)
375 updateStatus(row);
376 for (const auto &row : out.divergent)
377 updateStatus(row);
378
379 return out;
380}
381
382} // namespace core::application::annual
Builds annual derived snapshots from workspace snapshots.
Shared annual preview helpers for UI and application layers.
core::ports::annual::AnnualResult buildAnnualResult(const core::ports::workspace::WorkspaceSnapshot &workspace, const std::string &annualId) const
core::ports::annual::AnnualResult runAnnual(const core::ports::workspace::WorkspaceSnapshot &workspace, const core::ports::annual::AnnualRequest &request) const override
Computes annual reporting data from a workspace snapshot and request.
Collects common core includes and aliases for precompiled-header builds.
core::ports::workspace::WorkspaceSnapshot withPreviewAnnual(core::ports::workspace::WorkspaceSnapshot workspace, const std::string &annualId, const std::vector< std::string > &analysisIds, int year)
constexpr const char * kPreviewAnnualId
core::ports::annual::AnnualResult sortAnnualResult(const core::ports::annual::AnnualResult &result)