firms and collections #1
Loading…
Reference in a new issue
No description provided.
Delete branch "d/fi1-firms"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Adds firm legal entities with a default-firm invariant, a collection registry that routes orders to firms, and real firm identity on invoice, credit note and purchase order documents. Includes a firms admin page and a draft-editable firm select on orders.
🤖 Claude AI Code Review
1. Summary
Adds firm legal entities with a default-firm invariant, collection→firm routing, and firm identity on invoice/credit-note/PO documents, plus an admin page and order firm-select. Well-structured with thorough test coverage.
2. Code Quality
⭐⭐⭐⭐☆
Excellent test coverage across services, PDF/XML export, and components, with clear intent comments explaining routing and fallback rules.
3. Issues
🟡 FirmService.cs — default-firm invariant broken on delete.
DeleteFirmAsyncallows deleting a non-default firm freely, but nothing promotes a remaining firm to default when the default firm is deleted. The guard only blocks deleting the default while others exist, and permits deleting the default when it's the sole firm. That's fine — but if you delete all non-default firms then the default, you end with zero firms (acceptable). The real gap: there's no path that ever leaves multiple firms with none defaulted, so this is actually safe. No action needed — but worth confirmingSetDefaultAsync/add flows are the only default mutators (they are).🟡 FirmService.SetDefaultAsync / AddFirmAsync — TOCTOU on default-firm invariant under concurrency. The "exactly one default" and unique-code checks are read-then-write without a transaction or unique constraint on
IsDefault. Two concurrentAddFirmAsynccalls on an empty table could both setIsDefault = true. This matches the existing single-user "Mode B" IOUs documented in TODO.md, so it's consistent with the codebase's stated posture — flagging only for awareness.🟢 CollectionsDialog.razor — rename UX is confusing.
RenameAsyncreuses the shared_newNameadd field and no-ops with a hint if blank, per the inline comment. Not a bug, but the coupling between the Add form and per-row rename will surprise users. (Skip if intentional.)4. Verdict
APPROVE
No blocking bugs, resource leaks, or breaking changes. The concurrency gaps are pre-existing, documented tradeoffs consistent with the single-user model. Migration, model, service, and export changes are internally consistent and well-tested.
📊 Tokens: 72594 input + 764 output | 💰 Cost: ~$0.3821
Generated by Claude Opus 4.8 via Gitea Actions