books/backend/Books.Api/Commands/FiscalYears/FiscalYearCommandHandlers.cs

119 lines
4.1 KiB
C#
Raw Normal View History

Full product audit: fix security, compliance, UX, and wire broken features 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>
2026-02-05 21:35:26 +01:00
using Books.Api.Domain;
using Books.Api.Domain.FiscalYears;
Full product audit: fix security, compliance, UX, and wire broken features 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>
2026-02-05 21:35:26 +01:00
using Books.Api.EventFlow.Repositories;
using EventFlow.Commands;
namespace Books.Api.Commands.FiscalYears;
Full product audit: fix security, compliance, UX, and wire broken features 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>
2026-02-05 21:35:26 +01:00
/// <summary>
/// Command handler for creating a new fiscal year.
/// Validates overlap with existing fiscal years and checks for gaps.
/// </summary>
public class CreateFiscalYearCommandHandler(
IFiscalYearRepository fiscalYearRepository)
: CommandHandler<FiscalYearAggregate, FiscalYearId, CreateFiscalYearCommand>
{
Full product audit: fix security, compliance, UX, and wire broken features 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>
2026-02-05 21:35:26 +01:00
public override async Task ExecuteAsync(
FiscalYearAggregate aggregate,
CreateFiscalYearCommand command,
CancellationToken cancellationToken)
{
Full product audit: fix security, compliance, UX, and wire broken features 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>
2026-02-05 21:35:26 +01:00
// Check for overlapping fiscal years
var hasOverlap = await fiscalYearRepository.HasOverlappingYearAsync(
command.CompanyId,
command.StartDate,
command.EndDate,
excludeId: null,
cancellationToken);
if (hasOverlap)
{
throw new DomainException(
"FISCAL_YEAR_OVERLAP",
"The fiscal year overlaps with an existing fiscal year",
"Regnskabsåret overlapper med et eksisterende regnskabsår");
}
// Check for gaps: verify the new start date follows the latest existing fiscal year
if (!command.IsFirstFiscalYear)
{
var existingYears = await fiscalYearRepository.GetByCompanyIdAsync(
command.CompanyId, cancellationToken);
if (existingYears.Count > 0)
{
// Find the latest end date among existing fiscal years
var latestEndDate = existingYears
.Select(fy => DateOnly.FromDateTime(fy.EndDate))
.Max();
var expectedStartDate = latestEndDate.AddDays(1);
if (command.StartDate != expectedStartDate)
{
throw new DomainException(
"FISCAL_YEAR_GAP",
$"Fiscal year must start on {expectedStartDate:yyyy-MM-dd} (day after previous year ends on {latestEndDate:yyyy-MM-dd}). No gaps are allowed between fiscal years.",
$"Regnskabsåret skal starte den {expectedStartDate:yyyy-MM-dd} (dagen efter forrige år slutter den {latestEndDate:yyyy-MM-dd}). Der må ikke være huller mellem regnskabsår.");
}
}
}
aggregate.Create(
command.CompanyId,
command.Name,
command.StartDate,
command.EndDate,
command.IsFirstFiscalYear,
command.IsReorganization);
}
}
public class CloseFiscalYearCommandHandler : CommandHandler<FiscalYearAggregate, FiscalYearId, CloseFiscalYearCommand>
{
public override Task ExecuteAsync(
FiscalYearAggregate aggregate,
CloseFiscalYearCommand command,
CancellationToken cancellationToken)
{
aggregate.Close(command.ClosedBy);
return Task.CompletedTask;
}
}
public class ReopenFiscalYearCommandHandler : CommandHandler<FiscalYearAggregate, FiscalYearId, ReopenFiscalYearCommand>
{
public override Task ExecuteAsync(
FiscalYearAggregate aggregate,
ReopenFiscalYearCommand command,
CancellationToken cancellationToken)
{
aggregate.Reopen(command.ReopenedBy);
return Task.CompletedTask;
}
}
public class LockFiscalYearCommandHandler : CommandHandler<FiscalYearAggregate, FiscalYearId, LockFiscalYearCommand>
{
public override Task ExecuteAsync(
FiscalYearAggregate aggregate,
LockFiscalYearCommand command,
CancellationToken cancellationToken)
{
aggregate.Lock(command.LockedBy);
return Task.CompletedTask;
}
}
public class MarkOpeningBalancePostedCommandHandler : CommandHandler<FiscalYearAggregate, FiscalYearId, MarkOpeningBalancePostedCommand>
{
public override Task ExecuteAsync(
FiscalYearAggregate aggregate,
MarkOpeningBalancePostedCommand command,
CancellationToken cancellationToken)
{
aggregate.MarkOpeningBalancePosted();
return Task.CompletedTask;
}
}