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>
225 lines
6.4 KiB
C#
225 lines
6.4 KiB
C#
using Books.Api.Domain.Invoices;
|
|
using Books.Api.Invoicing.Services;
|
|
using EventFlow.Commands;
|
|
|
|
namespace Books.Api.Commands.Invoices;
|
|
|
|
/// <summary>
|
|
/// Command handler for creating invoices.
|
|
/// Auto-assigns a sequential invoice number if one is not provided.
|
|
/// </summary>
|
|
public class CreateInvoiceCommandHandler(
|
|
IInvoiceNumberService invoiceNumberService)
|
|
: CommandHandler<InvoiceAggregate, InvoiceId, CreateInvoiceCommand>
|
|
{
|
|
public override async Task ExecuteAsync(
|
|
InvoiceAggregate aggregate,
|
|
CreateInvoiceCommand command,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
// Auto-assign invoice number if not provided
|
|
var invoiceNumber = command.InvoiceNumber;
|
|
if (string.IsNullOrWhiteSpace(invoiceNumber))
|
|
{
|
|
invoiceNumber = await invoiceNumberService.GetNextInvoiceNumberAsync(
|
|
command.CompanyId,
|
|
command.InvoiceDate.Year,
|
|
cancellationToken);
|
|
}
|
|
|
|
aggregate.Create(
|
|
command.CompanyId,
|
|
command.FiscalYearId,
|
|
command.CustomerId,
|
|
command.CustomerName,
|
|
command.CustomerNumber,
|
|
invoiceNumber,
|
|
command.InvoiceDate,
|
|
command.DueDate,
|
|
command.PaymentTermsDays,
|
|
command.Currency,
|
|
command.VatCode,
|
|
command.Notes,
|
|
command.Reference,
|
|
command.CreatedBy);
|
|
}
|
|
}
|
|
|
|
public class AddInvoiceLineCommandHandler
|
|
: CommandHandler<InvoiceAggregate, InvoiceId, AddInvoiceLineCommand>
|
|
{
|
|
public override Task ExecuteAsync(
|
|
InvoiceAggregate aggregate,
|
|
AddInvoiceLineCommand command,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
aggregate.AddLine(
|
|
command.Description,
|
|
command.Quantity,
|
|
command.UnitPrice,
|
|
command.VatCode,
|
|
command.AccountId,
|
|
command.Unit,
|
|
command.DiscountPercent);
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
}
|
|
|
|
public class UpdateInvoiceLineCommandHandler
|
|
: CommandHandler<InvoiceAggregate, InvoiceId, UpdateInvoiceLineCommand>
|
|
{
|
|
public override Task ExecuteAsync(
|
|
InvoiceAggregate aggregate,
|
|
UpdateInvoiceLineCommand command,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
aggregate.UpdateLine(
|
|
command.LineNumber,
|
|
command.Description,
|
|
command.Quantity,
|
|
command.UnitPrice,
|
|
command.VatCode,
|
|
command.AccountId,
|
|
command.Unit,
|
|
command.DiscountPercent);
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
}
|
|
|
|
public class RemoveInvoiceLineCommandHandler
|
|
: CommandHandler<InvoiceAggregate, InvoiceId, RemoveInvoiceLineCommand>
|
|
{
|
|
public override Task ExecuteAsync(
|
|
InvoiceAggregate aggregate,
|
|
RemoveInvoiceLineCommand command,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
aggregate.RemoveLine(command.LineNumber);
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
}
|
|
|
|
public class MarkInvoiceSentCommandHandler
|
|
: CommandHandler<InvoiceAggregate, InvoiceId, MarkInvoiceSentCommand>
|
|
{
|
|
public override Task ExecuteAsync(
|
|
InvoiceAggregate aggregate,
|
|
MarkInvoiceSentCommand command,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
aggregate.Send(
|
|
command.LedgerTransactionId,
|
|
command.SentBy);
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
}
|
|
|
|
public class ReceiveInvoicePaymentCommandHandler
|
|
: CommandHandler<InvoiceAggregate, InvoiceId, ReceiveInvoicePaymentCommand>
|
|
{
|
|
public override Task ExecuteAsync(
|
|
InvoiceAggregate aggregate,
|
|
ReceiveInvoicePaymentCommand command,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
aggregate.ReceivePayment(
|
|
command.Amount,
|
|
command.BankTransactionId,
|
|
command.LedgerTransactionId,
|
|
command.PaymentReference,
|
|
command.PaymentDate,
|
|
command.RecordedBy);
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
}
|
|
|
|
public class VoidInvoiceCommandHandler
|
|
: CommandHandler<InvoiceAggregate, InvoiceId, VoidInvoiceCommand>
|
|
{
|
|
public override Task ExecuteAsync(
|
|
InvoiceAggregate aggregate,
|
|
VoidInvoiceCommand command,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
aggregate.Void(
|
|
command.Reason,
|
|
command.ReversalLedgerTransactionId,
|
|
command.VoidedBy);
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
}
|
|
|
|
// =====================================================
|
|
// CREDIT NOTE COMMAND HANDLERS
|
|
// =====================================================
|
|
|
|
public class CreateCreditNoteCommandHandler
|
|
: CommandHandler<InvoiceAggregate, InvoiceId, CreateCreditNoteCommand>
|
|
{
|
|
public override Task ExecuteAsync(
|
|
InvoiceAggregate aggregate,
|
|
CreateCreditNoteCommand command,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
aggregate.CreateCreditNote(
|
|
command.CompanyId,
|
|
command.FiscalYearId,
|
|
command.CustomerId,
|
|
command.CustomerName,
|
|
command.CustomerNumber,
|
|
command.CreditNoteNumber,
|
|
command.CreditNoteDate,
|
|
command.Currency,
|
|
command.VatCode,
|
|
command.Notes,
|
|
command.Reference,
|
|
command.CreatedBy,
|
|
command.OriginalInvoiceId,
|
|
command.OriginalInvoiceNumber,
|
|
command.CreditReason);
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
}
|
|
|
|
public class IssueCreditNoteCommandHandler
|
|
: CommandHandler<InvoiceAggregate, InvoiceId, IssueCreditNoteCommand>
|
|
{
|
|
public override Task ExecuteAsync(
|
|
InvoiceAggregate aggregate,
|
|
IssueCreditNoteCommand command,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
aggregate.Issue(
|
|
command.LedgerTransactionId,
|
|
command.IssuedBy);
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
}
|
|
|
|
public class ApplyCreditNoteCommandHandler
|
|
: CommandHandler<InvoiceAggregate, InvoiceId, ApplyCreditNoteCommand>
|
|
{
|
|
public override Task ExecuteAsync(
|
|
InvoiceAggregate aggregate,
|
|
ApplyCreditNoteCommand command,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
aggregate.ApplyCredit(
|
|
command.TargetInvoiceId,
|
|
command.TargetInvoiceNumber,
|
|
command.Amount,
|
|
command.AppliedDate,
|
|
command.AppliedBy,
|
|
command.LedgerTransactionId);
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
}
|