Security (Phase 1): - Add authentication middleware on /graphql endpoint - Filter company queries by user access (prevent IDOR) - Add role-based authorization on mutations (owner/accountant) - Reduce API key cache TTL from 24h to 5 minutes - Hide exception details in production GraphQL errors - Fix RBAC in frontend companyStore (was hardcoded) Wiring broken features (Phase 2): - Wire Kassekladde submit/void/copy to GraphQL mutations - Wire Kontooversigt account creation to createAccount mutation - Wire Settings save to updateCompany mutation - Wire CreateFiscalYearModal and CloseFiscalYearWizard to mutations - Replace Momsindberetning mock data with real useVatReport query - Remove Dashboard hardcoded percentages and fake VAT deadline - Fix Kreditnotaer invoice selector to use real data - Fix mutation retry from 1 to 0 (prevent duplicate operations) Accounting compliance (Phase 3): - Add balanced entry validation (debit==credit) in JournalEntryDraftAggregate - Add fiscal year boundary enforcement (status, date range checks) - Add PostedAt timestamp to posted events (Bogføringsloven §7) - Add account number uniqueness check within company - Add fiscal year overlap and gap checks - Add sequential invoice auto-numbering - Fix InvoiceLine VAT rate to use canonical VatCodes - Fix SAF-T account type mapping (financial → Expense) - Add DraftLine validation (cannot have both debit and credit > 0) UX improvements (Phase 4): - Fix Danish character encoding across 15+ files (ø, æ, å) - Deploy DemoDataDisclaimer on pages with mock/incomplete data - Adopt PageHeader component universally across all pages - Standardize active/inactive filtering to Switch pattern - Fix dead buttons in Header (Help, Notifications) - Remove hardcoded mock data from Settings - Fix Sidebar controlled state and Kontooversigt navigation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
85 lines
2.6 KiB
C#
85 lines
2.6 KiB
C#
using Books.Api.Domain;
|
|
using Books.Api.Domain.Accounts;
|
|
using Books.Api.EventFlow.Repositories;
|
|
using EventFlow.Commands;
|
|
|
|
namespace Books.Api.Commands.Accounts;
|
|
|
|
/// <summary>
|
|
/// Command handler for creating a new account.
|
|
/// Validates that the account number is unique within the company.
|
|
/// </summary>
|
|
public class CreateAccountCommandHandler(
|
|
IAccountRepository accountRepository)
|
|
: CommandHandler<AccountAggregate, AccountId, CreateAccountCommand>
|
|
{
|
|
public override async Task ExecuteAsync(
|
|
AccountAggregate aggregate,
|
|
CreateAccountCommand command,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
// Check if an account with the same number already exists for this company
|
|
var existingAccount = await accountRepository.GetByCompanyAndNumberAsync(
|
|
command.CompanyId, command.AccountNumber, cancellationToken);
|
|
|
|
if (existingAccount != null)
|
|
{
|
|
throw new DomainException(
|
|
"ACCOUNT_NUMBER_EXISTS",
|
|
$"Account number {command.AccountNumber} already exists for this company",
|
|
$"Kontonummer {command.AccountNumber} eksisterer allerede");
|
|
}
|
|
|
|
aggregate.Create(
|
|
command.CompanyId,
|
|
command.AccountNumber,
|
|
command.Name,
|
|
command.AccountType,
|
|
command.ParentId,
|
|
command.Description,
|
|
command.VatCodeId,
|
|
command.IsSystemAccount,
|
|
command.StandardAccountNumber);
|
|
}
|
|
}
|
|
|
|
public class UpdateAccountCommandHandler : CommandHandler<AccountAggregate, AccountId, UpdateAccountCommand>
|
|
{
|
|
public override Task ExecuteAsync(
|
|
AccountAggregate aggregate,
|
|
UpdateAccountCommand command,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
aggregate.Update(
|
|
command.Name,
|
|
command.ParentId,
|
|
command.Description,
|
|
command.VatCodeId);
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
}
|
|
|
|
public class DeactivateAccountCommandHandler : CommandHandler<AccountAggregate, AccountId, DeactivateAccountCommand>
|
|
{
|
|
public override Task ExecuteAsync(
|
|
AccountAggregate aggregate,
|
|
DeactivateAccountCommand command,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
aggregate.Deactivate();
|
|
return Task.CompletedTask;
|
|
}
|
|
}
|
|
|
|
public class ReactivateAccountCommandHandler : CommandHandler<AccountAggregate, AccountId, ReactivateAccountCommand>
|
|
{
|
|
public override Task ExecuteAsync(
|
|
AccountAggregate aggregate,
|
|
ReactivateAccountCommand command,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
aggregate.Reactivate();
|
|
return Task.CompletedTask;
|
|
}
|
|
}
|