Backend Security & Data Integrity: - Block negative debit/credit amounts that bypass balance validation - Require document date at posting (was optional, bypassing fiscal year checks) - Fix event sourcing anti-pattern: timestamps now stored in events, not UtcNow in Apply - Add [Authorize] to BankingController OAuth callback - Add company access check on attachment downloads - Validate CVR in CompanyAggregate.Create and CompanyAggregate.Update - Require company CVR for invoice creation (Momsloven §52) - Delete leftover WeatherForecastController - Fix duplicate migration number 007 (renamed to 007b) - Remove dead code in VatCalculationService (identical if/else branches) Accounting Compliance: - Add missing VAT accounts to StandardDanishAccounts (5610, 5611, 5620) - Populate SAF-T TaxInformation on transaction lines (was always null) - Add AuditFileCountry and TaxRegistrationNumber to SAF-T header Critical Frontend Bugs: - Fix Dashboard <a href> causing full page reloads (now uses React Router Link) - Wire Kassekladde filters to actual data (account, status, date range) - Pre-populate form when editing existing Kassekladde drafts - Add detail drawer for "Vis detaljer" action (was just a toast) - Toggle advanced filters with "Flere filtre" button - CloseFiscalYearWizard now actually posts closing entries via mutations - "Create next year" checkbox now creates the next fiscal year Danish Character Encoding (~50 fixes): - Fix ø/æ/å across Momsindberetning, DocumentUploadModal, Bankafstemning, Kontooversigt, CloseFiscalYearWizard, vatCodes, periodStore, periods, accounting, types/periods Dead Buttons & UX: - Disable Momsindberetning PDF/Export buttons with tooltips - FiscalYearSelector "Administrer" now navigates to Settings - Settings bank tab now uses real BankConnectionsTab component - Bankafstemning save button disabled with development tooltip - Replace hardcoded account options with real API data (Bankafstemning, Fakturaer) - Header help button shows info message, notification bell shows popover Consistency & Quality: - Remove 7 console.log statements from production code - Adopt PageHeader on 6 remaining pages (Kreditnotaer, Settings, Admin, etc.) - Standardize loading states to Skeleton pattern (5 pages) - Replace deprecated bodyStyle prop on Ant Design Cards - Standardize date format to DD-MM-YYYY - Fix sidebar width mismatch in designTokens - Fix Kontooversigt breadcrumb pointing to non-existent route Accessibility: - Add aria-label to sidebar navigation - Add +/- prefix to AmountText for color-blind users - Fix CompanySwitcher permanent skeleton when no companies Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
102 lines
3.2 KiB
C#
102 lines
3.2 KiB
C#
using Books.Api.Domain.UserAccess.Events;
|
|
using EventFlow.Aggregates;
|
|
|
|
namespace Books.Api.Domain.UserAccess;
|
|
|
|
public class UserCompanyAccessAggregate : AggregateRoot<UserCompanyAccessAggregate, UserCompanyAccessId>,
|
|
IEmit<UserCompanyAccessGrantedEvent>,
|
|
IEmit<UserCompanyAccessRoleChangedEvent>,
|
|
IEmit<UserCompanyAccessRevokedEvent>
|
|
{
|
|
public string UserId { get; private set; } = string.Empty;
|
|
public string CompanyId { get; private set; } = string.Empty;
|
|
public CompanyRole Role { get; private set; }
|
|
public string GrantedBy { get; private set; } = string.Empty;
|
|
public DateTimeOffset GrantedAt { get; private set; }
|
|
public bool IsActive { get; private set; }
|
|
public DateTimeOffset? RevokedAt { get; private set; }
|
|
public string? RevokedBy { get; private set; }
|
|
|
|
public UserCompanyAccessAggregate(UserCompanyAccessId id) : base(id) { }
|
|
|
|
/// <summary>
|
|
/// Grant access to a user for a company.
|
|
/// </summary>
|
|
public void GrantAccess(string userId, string companyId, CompanyRole role, string grantedBy)
|
|
{
|
|
if (!IsNew && IsActive)
|
|
{
|
|
throw new DomainException(
|
|
"ACCESS_ALREADY_GRANTED",
|
|
$"User '{userId}' already has access to company '{companyId}'",
|
|
$"Brugeren '{userId}' har allerede adgang til virksomheden");
|
|
}
|
|
|
|
// If previously revoked, we're re-granting
|
|
Emit(new UserCompanyAccessGrantedEvent(userId, companyId, role, grantedBy, DateTimeOffset.UtcNow));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Change the user's role for this company.
|
|
/// </summary>
|
|
public void ChangeRole(CompanyRole newRole, string changedBy)
|
|
{
|
|
if (!IsActive)
|
|
{
|
|
throw new DomainException(
|
|
"ACCESS_NOT_ACTIVE",
|
|
"Cannot change role - access is not active",
|
|
"Kan ikke ændre rolle - adgang er ikke aktiv");
|
|
}
|
|
|
|
if (Role == newRole)
|
|
{
|
|
throw new DomainException(
|
|
"ROLE_UNCHANGED",
|
|
$"User already has role '{newRole}'",
|
|
$"Brugeren har allerede rollen '{newRole}'");
|
|
}
|
|
|
|
Emit(new UserCompanyAccessRoleChangedEvent(Role, newRole, changedBy));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Revoke the user's access to this company.
|
|
/// </summary>
|
|
public void RevokeAccess(string revokedBy)
|
|
{
|
|
if (!IsActive)
|
|
{
|
|
throw new DomainException(
|
|
"ACCESS_ALREADY_REVOKED",
|
|
"Access is already revoked",
|
|
"Adgang er allerede tilbagekaldt");
|
|
}
|
|
|
|
Emit(new UserCompanyAccessRevokedEvent(revokedBy, DateTimeOffset.UtcNow));
|
|
}
|
|
|
|
public void Apply(UserCompanyAccessGrantedEvent e)
|
|
{
|
|
UserId = e.UserId;
|
|
CompanyId = e.CompanyId;
|
|
Role = e.Role;
|
|
GrantedBy = e.GrantedBy;
|
|
GrantedAt = e.GrantedAt;
|
|
IsActive = true;
|
|
RevokedAt = null;
|
|
RevokedBy = null;
|
|
}
|
|
|
|
public void Apply(UserCompanyAccessRoleChangedEvent e)
|
|
{
|
|
Role = e.NewRole;
|
|
}
|
|
|
|
public void Apply(UserCompanyAccessRevokedEvent e)
|
|
{
|
|
IsActive = false;
|
|
RevokedAt = e.RevokedAt;
|
|
RevokedBy = e.RevokedBy;
|
|
}
|
|
}
|