books/backend/Books.Api/Domain/Companies/CompanyAggregate.cs
Nicolaj Hartmann 8096a19081 Audit v4: VAT calc, SAF-T compliance, security hardening, frontend quality
Backend (17 files):
- VAT: REP 25% deductibility (§42), EU reverse charge double-entry (IEUV/IEUY/IVY),
  IVY rate 0%→25%, VatReport Box C/D populated, Basis1 from real revenue
- SAF-T: correct OECD namespace, closing balance net calc, zero-amount fallback,
  credit note auto-numbering (§52)
- Security: BankingController CSRF state token + company auth check,
  attachment canonical path traversal check, discount 0-100% validation,
  deactivated product/customer update guard
- Quality: redact bank API logs, remove dead code (VatCalcService,
  PaymentMatchingService), CompanyAggregate IEmit interfaces, fix URL encoding

Frontend (15 files):
- Fix double "kr." in AmountText and Dashboard Statistic components
- Fix UserSettings Switch defaultChecked desync with Form state
- Remove dual useCompany/useCompanyStore pattern (Dashboard, Moms, Bank)
- Correct SKAT VAT deadline calculation per period type
- Add half-yearly/yearly VAT period options
- Guard console.error with import.meta.env.DEV
- Use shared formatDate in BankConnectionsTab
- Remove dead NONE vatCode check, purge 7 legacy VAT codes from type union
- Migrate S25→U25, K25→I25 across all pages

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-06 01:38:52 +01:00

100 lines
3.2 KiB
C#

using Books.Api.Domain.Companies.Events;
using EventFlow.Aggregates;
namespace Books.Api.Domain.Companies;
public class CompanyAggregate(CompanyId id) : AggregateRoot<CompanyAggregate, CompanyId>(id),
IEmit<CompanyCreatedEvent>,
IEmit<CompanyUpdatedEvent>,
IEmit<CompanyBankDetailsUpdatedEvent>
{
private bool _isCreated;
public void Apply(CompanyCreatedEvent e) => _isCreated = true;
public void Apply(CompanyUpdatedEvent e) { }
public void Apply(CompanyBankDetailsUpdatedEvent e) { }
public void Create(
string name,
string? cvr,
string? address,
string? postalCode,
string? city,
string country,
int fiscalYearStartMonth,
string currency,
bool vatRegistered,
string? vatPeriodFrequency)
{
if (_isCreated)
throw new DomainException("Company already exists");
if (string.IsNullOrWhiteSpace(name))
throw new DomainException("Company name is required");
if (fiscalYearStartMonth < 1 || fiscalYearStartMonth > 12)
throw new DomainException("Fiscal year start month must be between 1 and 12");
// Validate CVR number if provided
if (!string.IsNullOrWhiteSpace(cvr) && !CvrValidator.IsValid(cvr.Trim()))
throw new DomainException(
"INVALID_CVR",
$"CVR number '{cvr}' is not valid. Must be 8 digits with valid checksum.",
$"CVR-nummer '{cvr}' er ugyldigt. Skal være 8 cifre med gyldig kontrolsum.");
Emit(new CompanyCreatedEvent(
name.Trim(),
cvr?.Trim(),
address?.Trim(),
postalCode?.Trim(),
city?.Trim(),
country,
fiscalYearStartMonth,
currency,
vatRegistered,
vatPeriodFrequency));
}
public void Update(
string name,
string? cvr,
string? address,
string? postalCode,
string? city,
string country,
int fiscalYearStartMonth,
string currency,
bool vatRegistered,
string? vatPeriodFrequency)
{
if (!_isCreated)
throw new DomainException("Company does not exist");
if (string.IsNullOrWhiteSpace(name))
throw new DomainException("Company name is required");
if (fiscalYearStartMonth < 1 || fiscalYearStartMonth > 12)
throw new DomainException("Fiscal year start month must be between 1 and 12");
// Validate CVR number if provided
if (!string.IsNullOrWhiteSpace(cvr) && !CvrValidator.IsValid(cvr.Trim()))
throw new DomainException(
"INVALID_CVR",
$"CVR number '{cvr}' is not valid. Must be 8 digits with valid checksum.",
$"CVR-nummer '{cvr}' er ugyldigt. Skal være 8 cifre med gyldig kontrolsum.");
Emit(new CompanyUpdatedEvent(
name.Trim(),
cvr?.Trim(),
address?.Trim(),
postalCode?.Trim(),
city?.Trim(),
country,
fiscalYearStartMonth,
currency,
vatRegistered,
vatPeriodFrequency));
}
}