101 const std::string &annualId)
const {
104 annual_ports::AnnualResult out;
105 out.annualId = annualId;
106 std::unordered_set<std::string> missingLiveKeys;
109 for (
const auto &row :
workspace.annuals) {
110 if (row.id == annualId) {
118 out.annualName =
annual->name;
120 out.stats.assignedAnalysisCount =
121 static_cast<int>(
annual->analysisIds.size());
123 std::unordered_map<std::string, AnalysisSnapshot> analysisById;
124 analysisById.reserve(
workspace.analyses.size());
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);
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);
141 std::vector<SnapshotTx> allSnapshotRows;
142 for (
const auto &analysisId :
annual->analysisIds) {
143 const auto it = analysisById.find(analysisId);
144 if (it == analysisById.end())
146 for (
const auto &row : it->second.snapshotTransactions) {
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));
161 out.stats.snapshotTransactionCount =
static_cast<int>(allSnapshotRows.size());
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());
168 for (
const auto &tx : allSnapshotRows) {
169 byExact[exactKey(tx)].push_back(tx);
170 byDateContract[tx.bookingDate +
"|" + tx.contractId +
"|" + tx.statementId]
174 auto buildRow = [&](
const std::vector<SnapshotTx> &group,
bool similar,
176 annual_ports::AnnualRowResult row;
179 const auto &base = group.front();
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)
196 row.sourceAnalysisIds.push_back(item.sourceAnalysisId);
197 row.sourceAnalysisNames.push_back(item.sourceAnalysisName);
199 const auto contractIt = contractTypeById.find(row.contractId);
200 if (contractIt != contractTypeById.end())
201 row.contractType = contractIt->second;
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;
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 :
"";
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);
229 out.stats.mixedYear += 1;
232 std::unordered_set<std::string> snapshotIds;
233 for (
const auto &tx : allSnapshotRows)
235 snapshotIds.insert(tx.id);
236 for (
const auto &live :
workspace.transactions) {
237 if (snapshotIds.count(live.id) > 0)
239 if (out.year > 0 && bookingYear(live.bookingDate) != out.year)
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;
257 auto updateStatus = [&](
const annual_ports::AnnualRowResult &row) {
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;
265 out.stats.neutral += 1;
267 for (
const auto &row : out.deduplicated)
272 std::unordered_set<std::string> consumedExact;
273 consumedExact.reserve(byExact.size());
274 for (
const auto &[key, group] : byExact) {
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);
286 out.stats.mixedYear += 1;
287 consumedExact.insert(key);
291 for (
const auto &[clusterKey, cluster] : byDateContract) {
292 if (cluster.size() < 2)
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)
300 std::vector<SnapshotTx> similarGroup;
301 for (
const auto &[amount, items] : byAmount) {
303 for (
const auto &tx : items) {
304 if (consumedExact.count(exactKey(tx)) > 0)
306 similarGroup.push_back(tx);
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);
317 out.stats.mixedYear += 1;
318 for (
const auto &tx : similarGroup)
319 consumedExact.insert(exactKey(tx));
323 for (
const auto &tx : allSnapshotRows) {
324 const auto key = exactKey(tx);
325 if (consumedExact.count(key) > 0)
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);
334 out.stats.mixedYear += 1;
337 std::unordered_set<std::string> snapshotIds;
338 for (
const auto &tx : allSnapshotRows)
340 snapshotIds.insert(tx.id);
341 for (
const auto &live :
workspace.transactions) {
342 if (snapshotIds.count(live.id) > 0)
344 if (out.year > 0 && bookingYear(live.bookingDate) != out.year)
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;
362 auto updateStatus = [&](
const annual_ports::AnnualRowResult &row) {
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;
370 out.stats.neutral += 1;
372 for (
const auto &row : out.deduplicated)
374 for (
const auto &row : out.similar)
376 for (
const auto &row : out.divergent)