FOSSredder 0.5.0
Loading...
Searching...
No Matches
WorkspaceCommandService.cpp
Go to the documentation of this file.
1
5
7
11
32#include "../../utils/Time.h"
33
34#include <nlohmann/json.hpp>
35
36#include <algorithm>
37#include <optional>
38#include <unordered_set>
39
40namespace {
41
42std::vector<core::domain::Alias> toAliases(const std::vector<core::ports::workspace::AliasSnapshot>& aliases) {
43 std::vector<core::domain::Alias> out;
44 out.reserve(aliases.size());
45 for (const auto& alias : aliases) {
46 out.emplace_back(alias.value, alias.kind, alias.source, alias.createdAt, alias.updatedAt);
47 }
48 return out;
49}
50
51template <typename Entity>
52concept HasTimestamps = requires(Entity entity) {
53 entity.setCreatedAt(std::string{});
54 entity.setUpdatedAt(std::string{});
55};
56
57template <typename Entity>
58void stampCreated(Entity& entity) {
59 if constexpr (HasTimestamps<Entity>) {
60 const auto now = core::utils::currentTimestampUtc();
61 if (entity.createdAt().empty()) {
62 entity.setCreatedAt(now);
63 }
64 if (entity.updatedAt().empty()) {
65 entity.setUpdatedAt(entity.createdAt());
66 }
67 }
68}
69
70template <typename Entity>
71void stampUpdated(Entity& entity) {
72 if constexpr (HasTimestamps<Entity>) {
73 entity.setUpdatedAt(core::utils::currentTimestampUtc());
74 }
75}
76
77template <typename Collection, typename Configure>
78std::string appendEntity(Collection& collection, Configure&& configure) {
79 using Entity = typename Collection::value_type::element_type;
80 auto entity = std::make_shared<Entity>();
81 entity->setId(core::utils::makeStableId());
82 configure(*entity);
83 stampCreated(*entity);
84 const std::string id = entity->id();
85 collection.push_back(std::move(entity));
86 return id;
87}
88
89template <typename Collection, typename Update>
90bool updateEntity(Collection& collection, const std::string& id, Update&& update) {
91 if (id.empty()) {
92 return false;
93 }
94 for (auto& entity : collection) {
95 if (!entity || entity->id() != id) {
96 continue;
97 }
98 update(*entity);
99 stampUpdated(*entity);
100 return true;
101 }
102 return false;
103}
104
105template <typename Collection>
106bool eraseEntity(Collection& collection, const std::string& id) {
107 if (id.empty()) {
108 return false;
109 }
110 const auto originalSize = collection.size();
111 collection.erase(std::remove_if(collection.begin(), collection.end(), [&](const auto& entity) {
112 return entity && entity->id() == id;
113 }), collection.end());
114 return collection.size() != originalSize;
115}
116
117void applyActorDraft(core::domain::Actor& actor, const core::application::ActorInput& input) {
118 actor.rename(input.name);
119 actor.setAliases(input.aliases);
120 actor.setContractIds(input.contractIds);
121}
122
123void applyPropertyDraft(core::domain::Property& property, const core::application::PropertyInput& input) {
124 property.rename(input.name);
125 property.setAliases(input.aliases);
126 property.setContractIds(input.contractIds);
127}
128
129void applyContractDraft(core::domain::Contract& contract, const core::application::ContractInput& input) {
130 std::vector<std::string> actorIds;
131 actorIds.reserve(1);
132 for (const auto& actorId : input.actorIds) {
133 const auto normalized = core::domain::policies::alias::trimCopy(actorId);
134 if (normalized.empty()) {
135 continue;
136 }
137 actorIds.push_back(normalized);
138 break;
139 }
140 contract.rename(input.name);
141 contract.setType(input.type);
142 contract.setAllocatableMode(input.allocatableMode);
143 contract.setActorIds(actorIds);
144 contract.setPropertyIds(input.propertyIds);
145 contract.setAliases(input.aliases);
146}
147
148void applyStatementName(core::domain::Statement& statement, const std::string& name) {
149 statement.rename(name);
150}
151
152std::string serializeSnapshotTransactionsForDomain(
153 const std::vector<core::ports::workspace::TransactionSnapshot>& source);
154core::domain::FilterSpec analysisFilterSpecFromSelection(
156
157void applyAnalysisDraft(core::domain::Analysis& analysis, const core::application::AnalysisInput& input) {
158 analysis.rename(input.name);
159 analysis.setType(input.type);
161 analysis.setFilterSpec(analysisFilterSpecFromSelection(input.filter));
162 analysis.setExportFormat(input.exportFormat);
164 analysis.setExportStateJson({});
166 serializeSnapshotTransactionsForDomain(input.snapshotTransactions));
167 analysis.clearAdjustments();
168 for (const auto& [key, value] : input.adjustments) {
169 analysis.setAdjustment(key, value);
170 }
171}
172
173void applyAnnualDraft(core::domain::Annual& annual,
174 const std::string& name,
175 int year,
176 const std::vector<std::string>& analysisIds) {
177 annual.apply(name, year, analysisIds);
178}
179
180void applyTransactionDraft(core::domain::Transaction& tx, const core::application::TransactionInput& input) {
181 tx.setName(input.name);
182 tx.setBookingDate(input.bookingDate.value());
183 tx.setValuta(input.valuta);
184 tx.setAmount(input.amount);
185 tx.setStatementId(input.statementId);
186 tx.setStatus(input.status);
187 tx.setActorId(input.actorId);
188 tx.setContractId(input.contractId);
189 tx.setAllocatable(input.allocatable);
190 tx.setPropertyIds(input.propertyIds);
191}
192
193std::vector<std::pair<std::string, double>>
194analysisAdjustmentsFromCommand(
196 return command.adjustments;
197}
198
199core::domain::FilterSpec analysisFilterSpecFromSelection(
203}
204
205std::string serializeSnapshotTransactionsForDomain(
206 const std::vector<core::ports::workspace::TransactionSnapshot>& source) {
207 if (source.empty()) {
208 return {};
209 }
210 nlohmann::json rows = nlohmann::json::array();
211 for (const auto& tx : source) {
212 rows.push_back(nlohmann::json{
213 {"id", tx.id},
214 {"transactionId", tx.id},
215 {"name", tx.name},
216 {"transactionName", tx.name},
217 {"bookingDate", tx.bookingDate},
218 {"date", tx.bookingDate},
219 {"valuta", tx.valuta},
220 {"amount", tx.amount},
221 {"status", tx.status},
222 {"contractId", tx.contractId},
223 {"contractType", tx.contractType},
224 {"actorId", tx.actorId},
225 {"statementId", tx.statementId},
226 {"allocatable", tx.allocatable},
227 {"propertyIds", tx.propertyIds},
228 {"propertyNames", tx.propertyNames},
229 });
230 }
231 return rows.dump();
232}
233
234bool isBlank(const std::string& value) {
235 return core::domain::policies::alias::trimCopy(value).empty();
236}
237
238void requireText(core::ports::workspace::ValidationResult& result,
239 const std::string& field,
240 const std::string& value,
241 const std::string& label) {
242 if (isBlank(value)) {
243 result.addError(field, "required", label + " is required.");
244 }
245}
246
247void requireEntityName(core::ports::workspace::ValidationResult& result,
248 const std::string& field,
249 const std::string& value,
250 const std::string& label) {
251 requireText(result, field, value, label);
252 if (!isBlank(value) && !core::domain::EntityName::isValid(value)) {
253 result.addError(field, "invalid", label + " is invalid.");
254 }
255}
256
257void requireId(core::ports::workspace::ValidationResult& result,
258 const std::string& field,
259 const std::string& value,
260 const std::string& label) {
261 if (isBlank(value)) {
262 result.addError(field, "required", label + " is required.");
263 }
264}
265
266bool valid(const core::ports::workspace::ValidationResult& result) {
267 return result.valid();
268}
269
270void appendValidationIssues(core::ports::workspace::ValidationResult& target,
272 target.issues.insert(target.issues.end(), source.issues.begin(), source.issues.end());
273}
274
275std::optional<double> parseTransactionAmount(const core::ports::workspace::TransactionCommand& command) {
277}
278
279core::ports::workspace::ValidationResult validateTransactionCommand(
281 bool requireStatementId) {
283 requireEntityName(result, "name", command.name, "Transaction name");
284 requireText(result, "bookingDate", command.bookingDate, "Booking date");
286 result.addError("bookingDate", "invalid", "Booking date is invalid.");
287 }
288 const auto amount = parseTransactionAmount(command);
290 result.addError("amount", "invalid", "Amount is invalid.");
291 }
292 if (requireStatementId) {
293 requireId(result, "statementId", command.statementId, "Statement");
294 }
295 return result;
296}
297
298core::application::TransactionInput transactionInputFromCommand(
300 double amount) {
302 input.name = command.name;
304 input.valuta = command.valuta;
305 input.amount = amount;
306 input.statementId = command.statementId;
308 input.status = static_cast<core::domain::Transaction::Status>(command.status);
309 input.actorId = command.actorId;
310 input.contractId = command.contractId;
311 input.allocatable = command.allocatable;
312 input.propertyIds = command.propertyIds;
313 return input;
314}
315
316bool syncActorRelations(core::domain::catalog::WorkspaceCatalog& state,
317 const std::string& actorId,
318 const std::vector<std::string>& desiredContractIds) {
319 if (actorId.empty()) {
320 return false;
321 }
322
323 std::unordered_set<std::string> desired;
324 for (const auto& contractId : desiredContractIds) {
325 if (!contractId.empty()) {
326 desired.insert(contractId);
327 }
328 }
329
330 bool changed = false;
331 for (auto& contract : state.contracts()) {
332 if (!contract) {
333 continue;
334 }
335 const bool shouldLink = desired.find(contract->id()) != desired.end();
336 const bool hasLink = contract->containsActorId(actorId);
337 if (shouldLink) {
338 const auto& actorIds = contract->actorIds();
339 const bool alreadySingleOwner = actorIds.size() == 1u && actorIds.front() == actorId;
340 if (!alreadySingleOwner) {
341 contract->setActorIds({actorId});
342 stampUpdated(*contract);
343 changed = true;
344 }
345 } else if (!shouldLink && hasLink) {
346 contract->removeActorId(actorId);
347 stampUpdated(*contract);
348 changed = true;
349 }
350 }
351
352 return changed;
353}
354
355bool syncPropertyRelations(core::domain::catalog::WorkspaceCatalog& state,
356 const std::string& propertyId,
357 const std::vector<std::string>& desiredContractIds) {
358 if (propertyId.empty()) {
359 return false;
360 }
361
362 std::unordered_set<std::string> desired;
363 for (const auto& contractId : desiredContractIds) {
364 if (!contractId.empty()) {
365 desired.insert(contractId);
366 }
367 }
368
369 bool changed = false;
370 for (auto& contract : state.contracts()) {
371 if (!contract) {
372 continue;
373 }
374 const bool shouldLink = desired.find(contract->id()) != desired.end();
375 const bool hasLink = contract->containsPropertyId(propertyId);
376 if (shouldLink && !hasLink) {
377 contract->addPropertyId(propertyId);
378 stampUpdated(*contract);
379 changed = true;
380 } else if (!shouldLink && hasLink) {
381 contract->removePropertyId(propertyId);
382 stampUpdated(*contract);
383 changed = true;
384 }
385 }
386
387 return changed;
388}
389
390void projectActorPropertyLinksFromContracts(core::domain::catalog::WorkspaceCatalog& state) {
391 const auto& contracts = state.contracts();
392
393 for (auto& actor : state.actors()) {
394 if (!actor) continue;
395 std::vector<std::string> contractIds;
396 for (const auto& contract : contracts) {
397 if (contract && contract->containsActorId(actor->id())) {
398 contractIds.push_back(contract->id());
399 }
400 }
401 actor->setContractIds(std::move(contractIds));
402 }
403
404 for (auto& property : state.properties()) {
405 if (!property) continue;
406 std::vector<std::string> contractIds;
407 for (const auto& contract : contracts) {
408 if (contract && contract->containsPropertyId(property->id())) {
409 contractIds.push_back(contract->id());
410 }
411 }
412 property->setContractIds(std::move(contractIds));
413 }
414}
415
416template <typename T>
417std::shared_ptr<T> findById(const std::vector<std::shared_ptr<T>>& items, const std::string& id) {
418 const auto it = std::find_if(items.begin(), items.end(), [&](const auto& item) {
419 return item && item->id() == id;
420 });
421 return it == items.end() ? nullptr : *it;
422}
423
424void removeTransaction(core::domain::catalog::WorkspaceCatalog::StatementList& statements,
426 const std::string& id);
427
428void removeStatement(core::domain::catalog::WorkspaceCatalog::StatementList& statements,
430 const std::string& id) {
431 const auto statement = findById(statements, id);
432 if (!statement) {
433 return;
434 }
435
436 for (const auto& transactionId : statement->transactionIds()) {
437 removeTransaction(statements, transactions, transactionId);
438 }
439
440 statements.erase(std::remove_if(statements.begin(), statements.end(), [&](const auto& item) {
441 return item && item->id() == id;
442 }), statements.end());
443}
444
445void removeTransaction(core::domain::catalog::WorkspaceCatalog::StatementList& statements,
447 const std::string& id) {
448 for (auto& statement : statements) {
449 if (statement) {
450 statement->removeTransaction(id);
451 }
452 }
453
454 transactions.erase(std::remove_if(transactions.begin(), transactions.end(), [&](const auto& item) {
455 return item && item->id() == id;
456 }), transactions.end());
457}
458
459void linkTransactionToStatement(core::domain::catalog::WorkspaceCatalog::StatementList& statements,
460 const std::string& statementId,
461 const std::string& transactionId) {
462 const auto statement = findById(statements, statementId);
463 if (!statement) {
464 return;
465 }
466 statement->addTransaction(transactionId);
467}
468
469void unlinkTransactionFromStatement(core::domain::catalog::WorkspaceCatalog::StatementList& statements,
470 const std::string& statementId,
471 const std::string& transactionId) {
472 const auto statement = findById(statements, statementId);
473 if (!statement) {
474 return;
475 }
476 statement->removeTransaction(transactionId);
477}
478
479class WorkspaceCatalogMutator {
480public:
481 std::string addActor(core::domain::catalog::WorkspaceCatalog& state, const core::application::ActorInput& input) const {
483 return {};
484 }
485 auto actors = state.actors();
486 const auto id = appendEntity(actors, [&](core::domain::Actor& actor) { applyActorDraft(actor, input); });
487 state.setActors(std::move(actors));
488 syncActorRelations(state, id, input.contractIds);
489 projectActorPropertyLinksFromContracts(state);
490 return id;
491 }
492
493 bool updateActor(core::domain::catalog::WorkspaceCatalog& state, const std::string& id, const core::application::ActorInput& input) const {
495 return false;
496 }
497 auto actors = state.actors();
498 const bool updated = updateEntity(actors, id, [&](core::domain::Actor& actor) { applyActorDraft(actor, input); });
499 state.setActors(std::move(actors));
500 if (updated) {
501 syncActorRelations(state, id, input.contractIds);
502 projectActorPropertyLinksFromContracts(state);
503 }
504 return updated;
505 }
506
507 bool deleteActor(core::domain::catalog::WorkspaceCatalog& state, const std::string& id) const {
508 auto actors = state.actors();
509 const bool removed = eraseEntity(actors, id);
510 state.setActors(std::move(actors));
511 if (removed) {
512 syncActorRelations(state, id, std::vector<std::string>{});
513 projectActorPropertyLinksFromContracts(state);
514 }
515 return removed;
516 }
517
518 std::string addProperty(core::domain::catalog::WorkspaceCatalog& state, const core::application::PropertyInput& input) const {
520 return {};
521 }
522 auto properties = state.properties();
523 const auto id = appendEntity(properties, [&](core::domain::Property& property) { applyPropertyDraft(property, input); });
524 state.setProperties(std::move(properties));
525 syncPropertyRelations(state, id, input.contractIds);
526 projectActorPropertyLinksFromContracts(state);
527 return id;
528 }
529
530 bool updateProperty(core::domain::catalog::WorkspaceCatalog& state, const std::string& id, const core::application::PropertyInput& input) const {
532 return false;
533 }
534 auto properties = state.properties();
535 const bool updated = updateEntity(properties, id, [&](core::domain::Property& property) { applyPropertyDraft(property, input); });
536 state.setProperties(std::move(properties));
537 if (updated) {
538 syncPropertyRelations(state, id, input.contractIds);
539 projectActorPropertyLinksFromContracts(state);
540 }
541 return updated;
542 }
543
544 bool deleteProperty(core::domain::catalog::WorkspaceCatalog& state, const std::string& id) const {
545 auto properties = state.properties();
546 const bool removed = eraseEntity(properties, id);
547 state.setProperties(std::move(properties));
548 if (removed) {
549 syncPropertyRelations(state, id, std::vector<std::string>{});
550 projectActorPropertyLinksFromContracts(state);
551 }
552 return removed;
553 }
554
555 std::string addContract(core::domain::catalog::WorkspaceCatalog& state, const core::application::ContractInput& input) const {
557 return {};
558 }
559 auto contracts = state.contracts();
560 const auto id = appendEntity(contracts, [&](core::domain::Contract& contract) { applyContractDraft(contract, input); });
561 state.setContracts(std::move(contracts));
562 projectActorPropertyLinksFromContracts(state);
563 return id;
564 }
565
566 bool updateContract(core::domain::catalog::WorkspaceCatalog& state, const std::string& id, const core::application::ContractInput& input) const {
568 return false;
569 }
570 auto contracts = state.contracts();
571 const bool updated = updateEntity(contracts, id, [&](core::domain::Contract& contract) { applyContractDraft(contract, input); });
572 state.setContracts(std::move(contracts));
573 if (updated) {
574 projectActorPropertyLinksFromContracts(state);
575 }
576 return updated;
577 }
578
579 bool deleteContract(core::domain::catalog::WorkspaceCatalog& state, const std::string& id) const {
580 auto contracts = state.contracts();
581 const bool removed = eraseEntity(contracts, id);
582 state.setContracts(std::move(contracts));
583 if (removed) {
584 projectActorPropertyLinksFromContracts(state);
585 }
586 return removed;
587 }
588
589 std::string addStatement(core::domain::catalog::WorkspaceCatalog& state, const std::string& name) const {
591 return {};
592 }
593 auto statements = state.statements();
594 const auto id = appendEntity(statements, [&](core::domain::Statement& statement) { applyStatementName(statement, name); });
595 state.setStatements(std::move(statements));
596 return id;
597 }
598
599 bool updateStatement(core::domain::catalog::WorkspaceCatalog& state, const std::string& id, const std::string& name) const {
601 return false;
602 }
603 auto statements = state.statements();
604 const bool updated = updateEntity(statements, id, [&](core::domain::Statement& statement) { applyStatementName(statement, name); });
605 state.setStatements(std::move(statements));
606 return updated;
607 }
608
609 bool deleteStatement(core::domain::catalog::WorkspaceCatalog& state, const std::string& id) const {
610 if (id.empty()) {
611 return false;
612 }
613
614 auto statements = state.statements();
615 auto transactions = state.transactions();
616 const bool hadStatement = static_cast<bool>(findById(statements, id));
617 const auto originalTransactionCount = transactions.size();
618 removeStatement(statements, transactions, id);
619 state.setStatements(std::move(statements));
620 state.setTransactions(std::move(transactions));
621 return hadStatement || state.transactions().size() != originalTransactionCount;
622 }
623
624 std::string addTransaction(core::domain::catalog::WorkspaceCatalog& state, const core::application::TransactionInput& input) const {
625 if (isBlank(input.statementId) || input.bookingDate.empty() || !core::domain::MoneyAmount::isValid(input.amount)) {
626 return {};
627 }
628
629 auto statements = state.statements();
630 auto transactions = state.transactions();
631 if (!findById(statements, input.statementId)) {
632 return {};
633 }
634
635 const auto id = appendEntity(transactions, [&](core::domain::Transaction& tx) {
636 applyTransactionDraft(tx, input);
637 });
638
639 const auto statement = findById(statements, input.statementId);
640 if (statement && !input.insertAfterTransactionId.empty()
641 && statement->containsTransaction(input.insertAfterTransactionId)) {
642 statement->insertTransaction(id, statement->indexOfTransaction(input.insertAfterTransactionId) + 1);
643 } else {
644 linkTransactionToStatement(statements, input.statementId, id);
645 }
646 state.setStatements(std::move(statements));
647 state.setTransactions(std::move(transactions));
648
649 return id;
650 }
651
652 bool updateTransaction(core::domain::catalog::WorkspaceCatalog& state, const std::string& id, const core::application::TransactionInput& input) const {
654 return false;
655 }
656
657 if (!input.statementId.empty()) {
658 if (!findById(state.statements(), input.statementId)) {
659 return false;
660 }
661 }
662
663 std::string previousStatementId;
664 std::string previousContractId;
665 bool previousAllocatable = false;
666 auto statements = state.statements();
667 auto transactions = state.transactions();
668 if (const auto transaction = findById(transactions, id)) {
669 previousStatementId = transaction->statementId();
670 previousContractId = transaction->contractId();
671 previousAllocatable = transaction->isAllocatable();
672 }
673
674 const auto updated = updateEntity(transactions, id, [&](core::domain::Transaction& tx) {
675 applyTransactionDraft(tx, input);
676 });
677
678 if (!updated) {
679 return false;
680 }
681
682 if (const auto updatedTx = findById(transactions, id)) {
683 const bool allocatableChanged = updatedTx->isAllocatable() != previousAllocatable;
684 const std::string effectiveContractId = !updatedTx->contractId().empty() ? updatedTx->contractId() : previousContractId;
685 if (allocatableChanged && !effectiveContractId.empty()) {
686 auto contracts = state.contracts();
687 const bool contractModeUpdated = updateEntity(contracts, effectiveContractId, [](core::domain::Contract& contract) {
688 if (contract.allocatableMode() != "mixed") {
689 contract.setAllocatableMode("mixed");
690 stampUpdated(contract);
691 }
692 });
693 if (contractModeUpdated) {
694 state.setContracts(std::move(contracts));
695 }
696 }
697 }
698
699 if (previousStatementId != input.statementId) {
700 if (!previousStatementId.empty()) {
701 unlinkTransactionFromStatement(statements, previousStatementId, id);
702 }
703 if (!input.statementId.empty()) {
704 linkTransactionToStatement(statements, input.statementId, id);
705 }
706 } else if (!input.statementId.empty()) {
707 linkTransactionToStatement(statements, input.statementId, id);
708 }
709
710 state.setStatements(std::move(statements));
711 state.setTransactions(std::move(transactions));
712 return true;
713 }
714
715 bool deleteTransaction(core::domain::catalog::WorkspaceCatalog& state, const std::string& id) const {
716 auto statements = state.statements();
717 auto transactions = state.transactions();
718 const auto originalSize = transactions.size();
719 removeTransaction(statements, transactions, id);
720 state.setStatements(std::move(statements));
721 state.setTransactions(std::move(transactions));
722 return state.transactions().size() != originalSize;
723 }
724
725 std::string addAnalysis(core::domain::catalog::WorkspaceCatalog& state, const core::application::AnalysisInput& input) const {
726 const auto filterSpec = analysisFilterSpecFromSelection(input.filter);
729 return {};
730 }
731 auto analyses = state.analyses();
732 const auto id = appendEntity(analyses, [&](core::domain::Analysis& analysis) { applyAnalysisDraft(analysis, input); });
733 state.setAnalyses(std::move(analyses));
734 return id;
735 }
736
737 bool updateAnalysis(core::domain::catalog::WorkspaceCatalog& state, const std::string& id, const core::application::AnalysisInput& input) const {
738 const auto filterSpec = analysisFilterSpecFromSelection(input.filter);
741 return false;
742 }
743 auto analyses = state.analyses();
744 const bool updated = updateEntity(analyses, id, [&](core::domain::Analysis& analysis) { applyAnalysisDraft(analysis, input); });
745 state.setAnalyses(std::move(analyses));
746 return updated;
747 }
748
749 bool deleteAnalysis(core::domain::catalog::WorkspaceCatalog& state, const std::string& id) const {
750 auto analyses = state.analyses();
751 const bool removed = eraseEntity(analyses, id);
752 state.setAnalyses(std::move(analyses));
753 return removed;
754 }
755
756 std::string addAnnual(core::domain::catalog::WorkspaceCatalog& state,
757 const std::string& name,
758 int year,
759 const std::vector<std::string>& analysisIds) const {
762 return {};
763 }
764 auto annuals = state.annuals();
765 const auto id = appendEntity(annuals, [&](core::domain::Annual& annual) { applyAnnualDraft(annual, name, year, analysisIds); });
766 state.setAnnuals(std::move(annuals));
767 return id;
768 }
769
770 bool updateAnnual(core::domain::catalog::WorkspaceCatalog& state,
771 const std::string& id,
772 const std::string& name,
773 int year,
774 const std::vector<std::string>& analysisIds) const {
777 return false;
778 }
779 auto annuals = state.annuals();
780 const bool updated = updateEntity(annuals, id, [&](core::domain::Annual& annual) { applyAnnualDraft(annual, name, year, analysisIds); });
781 state.setAnnuals(std::move(annuals));
782 return updated;
783 }
784
785 bool deleteAnnual(core::domain::catalog::WorkspaceCatalog& state, const std::string& id) const {
786 auto annuals = state.annuals();
787 const bool removed = eraseEntity(annuals, id);
788 state.setAnnuals(std::move(annuals));
789 return removed;
790 }
791};
792
793const WorkspaceCatalogMutator& catalogMutator() {
794 static const WorkspaceCatalogMutator instance;
795 return instance;
796}
797
798} // namespace
799
800namespace core::application {
801
805
807
809
811
812core::domain::catalog::WorkspaceCatalog& WorkspaceCommandService::mutableCatalogState() noexcept {
813 return session_->mutableCatalogState();
814}
815
816const core::domain::catalog::WorkspaceCatalog& WorkspaceCommandService::catalogState() const noexcept {
817 return session_->catalogState();
818}
819
820void WorkspaceCommandService::commit() {
821 session_->commit();
822}
823
824void WorkspaceCommandService::notifyCatalogChanged() {
825 session_->notifyState();
826}
827
828std::string WorkspaceCommandService::commitCreated(WorkspaceCommandService& service, std::string id) {
829 if (id.empty()) {
830 return {};
831 }
832
833 service.notifyCatalogChanged();
834 return id;
835}
836
837void WorkspaceCommandService::commitIfChanged(WorkspaceCommandService& service, bool changed) {
838 if (changed) {
839 service.notifyCatalogChanged();
840 }
841}
842
845 requireEntityName(result, "name", command.name, "Actor name");
846 return result;
847}
848
851 requireEntityName(result, "name", command.name, "Property name");
852 return result;
853}
854
857 requireEntityName(result, "name", command.name, "Contract name");
858 requireText(result, "type", command.type, "Contract type");
859 if (!isBlank(command.type) && !core::domain::ContractType::isValid(command.type)) {
860 result.addError("type", "invalid", "Contract type is invalid.");
861 }
862 return result;
863}
864
867 requireEntityName(result, "name", command.name, "Statement name");
868 return result;
869}
870
872 return validateTransactionCommand(command, true);
873}
874
877 auto result = validate(command.statement);
878 for (const auto& transaction : command.transactions) {
879 appendValidationIssues(result, validateTransactionCommand(transaction, false));
880 }
881 return result;
882}
883
886 requireEntityName(result, "name", command.name, "Analysis name");
887 requireText(result, "type", command.type, "Analysis type");
888 if (!isBlank(command.type) && !core::domain::AnalysisType::isValid(command.type)) {
889 result.addError("type", "invalid", "Analysis type is invalid.");
890 }
891 if (!isBlank(command.type) && !core::domain::policies::analysis::supportsResultType(command.type)) {
892 result.addError("type", "unsupported", "Analysis type is not supported.");
893 }
894 const auto filterSpec = analysisFilterSpecFromSelection(command.filter);
895 if (!filterSpec.value().empty() && !core::domain::FilterSpec::isValid(filterSpec.value())) {
896 result.addError("filterSpec", "invalid", "Filter specification is invalid.");
897 }
898 if (!isBlank(command.exportFormat) && !core::domain::ExportFormat::isValid(command.exportFormat)) {
899 result.addError("exportFormat", "invalid", "Export format is invalid.");
900 }
901 return result;
902}
903
906 requireEntityName(result, "name", command.name, "Annual name");
908 result.addError("year", "invalid", "Year is invalid.");
909 }
910 return result;
911}
912
914 if (!valid(validate(command))) {
915 return {};
916 }
917 return WorkspaceCommandService::commitCreated(*this, catalogMutator().addActor(mutableCatalogState(), {command.name, toAliases(command.aliases), command.contractIds}));
918}
919
921 auto validation = validate(command);
922 requireId(validation, "id", command.id, "Actor");
923 if (!valid(validation)) {
924 return;
925 }
926 WorkspaceCommandService::commitIfChanged(*this, catalogMutator().updateActor(mutableCatalogState(), command.id, {command.name, toAliases(command.aliases), command.contractIds}));
927}
928
929void WorkspaceCommandService::deleteActor(const std::string& id) {
930 WorkspaceCommandService::commitIfChanged(*this, catalogMutator().deleteActor(mutableCatalogState(), id));
931}
932
934 if (!valid(validate(command))) {
935 return {};
936 }
937 return WorkspaceCommandService::commitCreated(*this, catalogMutator().addProperty(mutableCatalogState(), {command.name, toAliases(command.aliases), command.contractIds}));
938}
939
941 auto validation = validate(command);
942 requireId(validation, "id", command.id, "Property");
943 if (!valid(validation)) {
944 return;
945 }
946 WorkspaceCommandService::commitIfChanged(*this, catalogMutator().updateProperty(mutableCatalogState(), command.id, {command.name, toAliases(command.aliases), command.contractIds}));
947}
948
949void WorkspaceCommandService::deleteProperty(const std::string& id) {
950 WorkspaceCommandService::commitIfChanged(*this, catalogMutator().deleteProperty(mutableCatalogState(), id));
951}
952
954 if (!valid(validate(command))) {
955 return {};
956 }
957 return WorkspaceCommandService::commitCreated(*this, catalogMutator().addContract(mutableCatalogState(), {
958 command.name,
959 command.type,
960 command.allocatableMode,
961 command.actorIds,
962 command.propertyIds,
963 toAliases(command.aliases)
964 }));
965}
966
968 auto validation = validate(command);
969 requireId(validation, "id", command.id, "Contract");
970 if (!valid(validation)) {
971 return;
972 }
973 WorkspaceCommandService::commitIfChanged(*this, catalogMutator().updateContract(mutableCatalogState(),
974 command.id,
975 {command.name,
976 command.type,
977 command.allocatableMode,
978 command.actorIds,
979 command.propertyIds,
980 toAliases(command.aliases)}));
981}
982
983void WorkspaceCommandService::deleteContract(const std::string& id) {
984 WorkspaceCommandService::commitIfChanged(*this, catalogMutator().deleteContract(mutableCatalogState(), id));
985}
986
988 if (!valid(validate(command))) {
989 return {};
990 }
991 return WorkspaceCommandService::commitCreated(*this, catalogMutator().addStatement(mutableCatalogState(), command.name));
992}
993
996 if (!valid(validate(command))) {
997 return {};
998 }
999
1000 std::vector<TransactionInput> inputs;
1001 inputs.reserve(command.transactions.size());
1002 for (const auto& transaction : command.transactions) {
1003 if (!valid(validateTransactionCommand(transaction, false))) {
1004 return {};
1005 }
1006 const auto amount = parseTransactionAmount(transaction);
1007 if (!amount) {
1008 return {};
1009 }
1010 inputs.push_back(transactionInputFromCommand(transaction, *amount));
1011 }
1012
1013 auto& state = mutableCatalogState();
1014 const std::string statementId = catalogMutator().addStatement(state, command.statement.name);
1015 if (statementId.empty()) {
1016 return {};
1017 }
1018 for (auto& input : inputs) {
1019 input.statementId = statementId;
1020 static_cast<void>(catalogMutator().addTransaction(state, input));
1021 }
1022 notifyCatalogChanged();
1023 return statementId;
1024}
1025
1027 auto validation = validate(command);
1028 requireId(validation, "id", command.id, "Statement");
1029 if (!valid(validation)) {
1030 return;
1031 }
1032 WorkspaceCommandService::commitIfChanged(*this, catalogMutator().updateStatement(mutableCatalogState(), command.id, command.name));
1033}
1034
1035void WorkspaceCommandService::deleteStatement(const std::string& id) {
1036 WorkspaceCommandService::commitIfChanged(*this, catalogMutator().deleteStatement(mutableCatalogState(), id));
1037}
1038
1040 if (!valid(validate(command))) {
1041 return {};
1042 }
1043 const auto amount = parseTransactionAmount(command);
1044 if (!amount) {
1045 return {};
1046 }
1047 TransactionInput input = transactionInputFromCommand(command, *amount);
1048 return WorkspaceCommandService::commitCreated(*this, catalogMutator().addTransaction(mutableCatalogState(), input));
1049}
1050
1052 auto validation = validate(command);
1053 requireId(validation, "id", command.id, "Transaction");
1054 if (!valid(validation)) {
1055 return;
1056 }
1057 const auto amount = parseTransactionAmount(command);
1058 if (!amount) {
1059 return;
1060 }
1061 TransactionInput input = transactionInputFromCommand(command, *amount);
1062 WorkspaceCommandService::commitIfChanged(*this, catalogMutator().updateTransaction(mutableCatalogState(), command.id, input));
1063}
1064
1066 WorkspaceCommandService::commitIfChanged(*this, catalogMutator().deleteTransaction(mutableCatalogState(), id));
1067}
1068
1070 if (!valid(validate(command))) {
1071 return {};
1072 }
1073 return WorkspaceCommandService::commitCreated(*this, catalogMutator().addAnalysis(mutableCatalogState(), {
1074 command.name,
1076 command.config,
1077 command.filter,
1080 command.snapshotTransactions,
1081 analysisAdjustmentsFromCommand(command)
1082 }));
1083}
1084
1086 auto validation = validate(command);
1087 requireId(validation, "id", command.id, "Analysis");
1088 if (!valid(validation)) {
1089 return;
1090 }
1091 WorkspaceCommandService::commitIfChanged(*this, catalogMutator().updateAnalysis(mutableCatalogState(), command.id, {
1092 command.name,
1093 core::domain::AnalysisType(command.type),
1094 command.config,
1095 command.filter,
1096 core::domain::ExportFormat(command.exportFormat),
1097 command.includeCalculationAdjustments,
1098 command.snapshotTransactions,
1099 analysisAdjustmentsFromCommand(command)
1100 }));
1101}
1102
1103void WorkspaceCommandService::deleteAnalysis(const std::string& id) {
1104 WorkspaceCommandService::commitIfChanged(*this, catalogMutator().deleteAnalysis(mutableCatalogState(), id));
1105}
1106
1108 if (!valid(validate(command))) {
1109 return {};
1110 }
1111 return WorkspaceCommandService::commitCreated(*this, catalogMutator().addAnnual(mutableCatalogState(), command.name, command.year, command.assignedAnalysisIds));
1112}
1113
1115 auto validation = validate(command);
1116 requireId(validation, "id", command.id, "Annual");
1117 if (!valid(validation)) {
1118 return;
1119 }
1120 WorkspaceCommandService::commitIfChanged(*this, catalogMutator().updateAnnual(mutableCatalogState(), command.id, command.name, command.year, command.assignedAnalysisIds));
1121}
1122
1123void WorkspaceCommandService::deleteAnnual(const std::string& id) {
1124 WorkspaceCommandService::commitIfChanged(*this, catalogMutator().deleteAnnual(mutableCatalogState(), id));
1125}
1126
1127} // namespace core::application
Domain model for actors.
Shared alias normalization and ranking helpers for core entities.
Declares helpers for parsing imported transaction amount strings.
Domain model for persisted analysis definitions.
Shared analysis validation and normalization helpers.
Value object for analysis type labels.
Shared analysis request/config helpers for UI and application layers.
Domain model for annual aggregates.
Shared annual validation helpers.
Value object for normalized booking dates.
Domain model for contracts.
Value object for contract type labels.
Small value object for normalized entity display names.
Value object for export format labels.
Value object for analysis filter specifications.
Value object for monetary transaction amounts.
Domain model for properties.
Declares private stable identifier helpers for core entity creation.
Domain model for statements.
Declares private UTC timestamp helpers for core implementation code.
Domain model for transactions.
Shared transaction transition and normalization helpers.
Applies catalog commands to workspace session state and persists commits.
Declares the mutable workspace session that ties in-memory state to storage.
Value object for annual year validation.
void deleteAnalysis(const std::string &id)
Deletes analysis by id.
void updateTransaction(const core::ports::workspace::TransactionCommand &command)
Updates transaction identified by command id.
void updateStatement(const core::ports::workspace::StatementCommand &command)
Updates statement identified by command id.
WorkspaceCommandService(WorkspaceSession &session)
Creates command service bound to one workspace session.
std::string addTransaction(const core::ports::workspace::TransactionCommand &command)
Adds transaction and returns created id.
void updateContract(const core::ports::workspace::ContractCommand &command)
Updates contract identified by command id.
void updateActor(const core::ports::workspace::ActorCommand &command)
Updates actor identified by command id.
core::ports::workspace::ValidationResult validate(const core::ports::workspace::ActorCommand &command) const
Validates actor command data without mutating state.
void deleteAnnual(const std::string &id)
Deletes annual aggregate by id.
std::string addStatementWithTransactions(const core::ports::workspace::StatementWithTransactionsCommand &command)
Adds statement and transactions in one workspace mutation.
void deleteContract(const std::string &id)
Deletes contract by id.
void deleteProperty(const std::string &id)
Deletes property by id.
void deleteStatement(const std::string &id)
Deletes statement by id.
void deleteTransaction(const std::string &id)
Deletes transaction by id.
std::string addActor(const core::ports::workspace::ActorCommand &command)
Adds actor and returns created id.
std::string addAnnual(const core::ports::workspace::AnnualCommand &command)
Adds annual aggregate and returns created id.
void deleteActor(const std::string &id)
Deletes actor by id.
std::string addContract(const core::ports::workspace::ContractCommand &command)
Adds contract and returns created id.
void updateAnnual(const core::ports::workspace::AnnualCommand &command)
Updates annual aggregate identified by command id.
std::string addProperty(const core::ports::workspace::PropertyCommand &command)
Adds property and returns created id.
void updateAnalysis(const core::ports::workspace::AnalysisCommand &command)
Updates analysis identified by command id.
std::string addAnalysis(const core::ports::workspace::AnalysisCommand &command)
Adds analysis and returns created id.
std::string addStatement(const core::ports::workspace::StatementCommand &command)
Adds statement and returns created id.
void updateProperty(const core::ports::workspace::PropertyCommand &command)
Updates property identified by command id.
Maintains the editable app state for one workspace and persists it through IStorageManager.
void commit()
Persists the session when a file is open and notifies listeners.
const core::domain::catalog::WorkspaceCatalog & catalogState() const noexcept
core::domain::catalog::WorkspaceCatalog & mutableCatalogState() noexcept
void setAliases(std::vector< Alias > value)
Definition Actor.cpp:23
const std::string & id() const noexcept
Definition Actor.cpp:88
void rename(EntityName value)
Renames the actor with normalized entity text.
Definition Actor.cpp:15
void setContractIds(std::vector< std::string > value)
Replaces all linked contract identifiers.
Definition Actor.cpp:28
void rename(EntityName value)
Renames the analysis with normalized entity text.
Definition Analysis.cpp:16
void setAdjustment(std::string key, double value)
Sets or updates a normalized adjustment entry.
Definition Analysis.cpp:111
void setExportStateJson(std::string value)
Stores the export state JSON snapshot.
Definition Analysis.cpp:56
void setType(AnalysisType value)
Sets the analysis type from a value object.
Definition Analysis.cpp:24
void setSnapshotTransactionsJson(std::string value)
Stores the snapshot transactions JSON.
Definition Analysis.cpp:60
void setExportFormat(ExportFormat value)
Sets the export format from a value object.
Definition Analysis.cpp:44
void setFilterSpec(FilterSpec value)
Sets the filter specification from a value object.
Definition Analysis.cpp:36
void setConfigJson(std::string value)
Stores the raw configuration JSON.
Definition Analysis.cpp:32
void clearAdjustments()
Clears all adjustment entries.
Definition Analysis.cpp:107
void setIncludeCalculationAdjustments(bool value)
Enables or disables calculation adjustments.
Definition Analysis.cpp:52
const std::string & value() const noexcept
static bool isValid(const std::string &value)
Checks whether a raw analysis type is valid.
void apply(std::string nameValue, int yearValue, std::vector< std::string > analysisIdsValue)
Applies the complete annual definition in one normalized domain step.
Definition Annual.cpp:32
const std::string & value() const noexcept
bool empty() const noexcept
void setAliases(std::vector< Alias > value)
Definition Contract.cpp:88
void setType(ContractType value)
Sets the contract type from a normalized value object.
Definition Contract.cpp:24
bool containsActorId(const std::string &value) const
Checks whether an actor id exists.
Definition Contract.cpp:130
bool containsPropertyId(const std::string &value) const
Checks whether a property id exists.
Definition Contract.cpp:135
void removePropertyId(const std::string &value)
Removes a property id from the contract.
Definition Contract.cpp:79
const std::string & allocatableMode() const noexcept
Definition Contract.cpp:187
void setActorIds(std::vector< std::string > value)
Replaces the linked actor ids with normalized values.
Definition Contract.cpp:44
void removeActorId(const std::string &value)
Removes an actor id from the contract.
Definition Contract.cpp:57
const std::string & id() const noexcept
Definition Contract.cpp:184
void setPropertyIds(std::vector< std::string > value)
Replaces the linked property ids with normalized values.
Definition Contract.cpp:66
void addPropertyId(std::string value)
Adds a property id when it is not already present.
Definition Contract.cpp:71
void rename(EntityName value)
Renames the contract with normalized entity text.
Definition Contract.cpp:16
void setAllocatableMode(std::string value)
Definition Contract.cpp:32
const std::vector< std::string > & actorIds() const noexcept
Definition Contract.cpp:188
static bool isValid(const std::string &value)
Checks whether a raw contract type is valid.
static bool isValid(const std::string &value)
Checks whether a raw name is valid.
static bool isValid(const std::string &value)
Checks whether a raw export format is valid.
const std::string & value() const noexcept
static bool isValid(const std::string &value)
Checks whether a raw filter specification is valid.
static bool isValid(double value) noexcept
Checks whether a raw amount is finite.
Definition MoneyAmount.h:28
const std::string & id() const noexcept
Definition Property.cpp:88
void addTransaction(std::string value)
Adds a normalized transaction id when it is not already present.
Definition Statement.cpp:24
void removeTransaction(const std::string &value)
Removes a transaction id from the statement.
Definition Statement.cpp:50
const std::vector< std::string > & transactionIds() const noexcept
void rename(EntityName value)
Renames the statement with normalized entity text.
Definition Statement.cpp:16
void setAmount(MoneyAmount value)
Sets the amount from a value object.
void setStatementId(std::string value)
Sets the statement id.
void setActorId(std::string value)
Sets the actor id.
void setName(EntityName value)
Sets the transaction name from a value object.
void setStatus(Status value)
Sets the transaction status.
void setValuta(std::string value)
Sets the valuta text.
void setContractId(std::string value)
Sets the contract id.
const std::string & id() const noexcept
const std::string & name() const noexcept
const std::vector< std::string > & propertyIds() const noexcept
const std::string & statementId() const noexcept
void setPropertyIds(std::vector< std::string > value)
Replaces the property id list with normalized values.
const std::string & actorId() const noexcept
const std::string & bookingDate() const noexcept
const std::string & valuta() const noexcept
const Status & status() const noexcept
void setBookingDate(BookingDate value)
Sets the booking date from a value object.
void setAllocatable(bool value)
Sets the allocatable flag.
const double & amount() const noexcept
const std::string & contractId() const noexcept
static bool isValid(int value) noexcept
Checks whether a raw year is valid.
Definition Year.cpp:30
EntityList< core::domain::Transaction > TransactionList
void setAnnuals(AnnualList value)
Replaces all annuals in the catalog.
const PropertyList & properties() const noexcept
Returns all properties in the catalog.
void setAnalyses(AnalysisList value)
Replaces all analyses in the catalog.
void setProperties(PropertyList value)
Replaces all properties in the catalog.
void setStatements(StatementList value)
Replaces all statements in the catalog.
const AnnualList & annuals() const noexcept
Returns all annuals in the catalog.
EntityList< core::domain::Statement > StatementList
const StatementList & statements() const noexcept
Returns all statements in the catalog.
const TransactionList & transactions() const noexcept
Returns all transactions in the catalog.
void setActors(ActorList value)
Replaces all actors in the catalog.
const ActorList & actors() const noexcept
Returns all actors in the catalog.
void setContracts(ContractList value)
Replaces all contracts 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.
void setTransactions(TransactionList value)
Replaces all transactions in the catalog.
std::string buildAnalysisConfigJson(const core::ports::analysis::AnalysisConfigInput &input)
std::optional< double > parseAmountString(const std::string &s)
std::string trimCopy(std::string value)
Trims leading and trailing whitespace from text.
bool supportsResultType(const std::string &type)
Checks whether an analysis type is supported at all.
bool hasUniqueAnalysisIds(const std::vector< std::string > &analysisIds)
Checks whether all analysis ids are unique.
bool isValidYear(int year)
Checks whether a year is valid for annual aggregates.
bool hasValidBookingDate(const std::string &value)
Checks whether a booking date value is valid.
bool hasValidAmount(double value)
Checks whether a transaction amount value is valid.
std::string buildAnalysisFilterSpec(const AnalysisFilterSelection &selection)
Serializes an analysis filter selection.
std::string makeStableId()
Generates a stable hex identifier for persisted entities.
Definition StableId.cpp:21
std::string currentTimestampUtc()
Returns the current UTC timestamp in ISO-8601 format.
Definition Time.cpp:15
std::vector< core::domain::Alias > aliases
std::vector< std::string > contractIds
core::ports::analysis::AnalysisConfigInput config
core::ports::analysis::AnalysisFilterSelection filter
core::domain::ExportFormat exportFormat
std::vector< std::pair< std::string, double > > adjustments
std::vector< core::ports::workspace::TransactionSnapshot > snapshotTransactions
std::vector< core::domain::Alias > aliases
std::vector< std::string > propertyIds
std::vector< std::string > contractIds
std::vector< core::domain::Alias > aliases
core::domain::Transaction::Status status
std::vector< TransactionCommand > transactions
Transaction creation data; statement ids are assigned by core.
void addError(std::string field, std::string code, std::string message)
Adds an error issue.
bool valid() const noexcept
Reports whether the validation result has no errors.