TaxGroup
Purpose
The TaxGroup aggregate represents a collection of tax codes that apply to specific types of entities (customers, vendors) or transaction scenarios. It serves as a classification mechanism for determining which taxes should be calculated for a given transaction based on the entity involved.
Business Rules and Invariants
Core Business Rules
- Unique Code: Each TaxGroup must have a unique code within the system
- Tax Code Membership: A TaxGroup contains a collection of TaxCode entities that define the actual tax rates and posting accounts
- Entity Assignment: TaxGroups are assigned to master data entities (Customers, Vendors) to determine their default tax treatment
- Transaction Application: When a transaction involves an entity with a TaxGroup, the system uses the intersection of the TaxGroup's tax codes with the item's TaxItemGroup to determine applicable taxes
Deletion Constraints
A TaxGroup cannot be deleted if it meets any of the following conditions:
1. Master Data Assignments
- Customer Assignments: TaxGroup is set as
SalesTaxGroupIdon any Customer record - Vendor Assignments: TaxGroup is set as
SalesTaxGroupIdon any Vendor record
2. Transaction References
- Posted Transactions: TaxGroup is referenced in any posted general ledger transactions
- Unposted Transactions: TaxGroup is referenced in any unposted ledger journal lines
- Subledger Transactions: TaxGroup is used in any sales invoices, purchase invoices, or return documents
State Management
Domain Events
The TaxGroup aggregate publishes the following domain events:
TaxGroupDeletedDomainEvent
Published when a TaxGroup is successfully deleted.
public record TaxGroupDeletedDomainEvent(
Guid TaxGroupId,
string TaxGroupCode,
DateTime DeletedAt,
string DeletedBy
) : DomainEvent;
Deletion Process
Command: DeleteTaxGroupCommand
public record DeleteTaxGroupCommand(Guid TaxGroupId) : IRequest;
Validation Sequence
Validation Rules Implementation
private async Task ValidateDeletionConstraintsAsync(TaxGroup taxGroup, CancellationToken cancellationToken)
{
var usageViolations = new List<string>();
// 1. Check General Ledger journal line usage
var journalLineCount = await _taxGroupRepository.CountLedgerJournalLinesUsingTaxGroupAsync(
taxGroup.Id, cancellationToken);
if (journalLineCount > 0)
{
usageViolations.Add($"Referenced in {journalLineCount} ledger journal line(s)");
}
// 2. Check cross-module usage via ITaxUsageValidator
await ValidateCrossModuleUsageAsync(taxGroup, usageViolations, cancellationToken);
// 3. Throw exception if usage found
if (usageViolations.Any())
{
throw new TaxGroupInUseException(taxGroup.Id, taxGroup.Code, usageViolations);
}
}
Cross-Module Usage Validation
AccountsReceivable Module Validation
The AR module's ITaxUsageValidator implementation checks:
// Pseudo-code for AR module validation
public async Task<TaxUsageValidationResult> ValidateTaxGroupUsageAsync(Guid taxGroupId)
{
// Check customer assignments
var customerCount = await _customerRepository.CountCustomersWithTaxGroupAsync(taxGroupId);
if (customerCount > 0)
{
return TaxUsageValidationResult.HasUsage(
customerCount,
$"Assigned to {customerCount} customer(s)",
"AccountsReceivable");
}
// Check sales invoice usage
var salesInvoiceCount = await _salesInvoiceRepository.CountInvoicesUsingTaxGroupAsync(taxGroupId);
if (salesInvoiceCount > 0)
{
return TaxUsageValidationResult.HasUsage(
salesInvoiceCount,
$"Used in {salesInvoiceCount} sales invoice(s)",
"AccountsReceivable");
}
return TaxUsageValidationResult.NoUsage("AccountsReceivable");
}
AccountsPayable Module Validation
The AP module checks vendor assignments and purchase invoices similarly to the AR module.
Error Scenarios and Messages
Successful Deletion
INFO: Successfully deleted tax group: TG001
Blocked Deletion - Customer Assignment
ERROR: Cannot delete tax group 'VAT-DOMESTIC' because it is currently being used.
Usage found: AccountsReceivable: Assigned to 3 customer(s): CUST001, CUST002, CUST003
Blocked Deletion - Multiple Usage Types
ERROR: Cannot delete tax group 'VAT-STANDARD' because it is currently being used.
Usage found: AccountsReceivable: Assigned to 5 customer(s): CUST001, CUST002 and 3 others;
AccountsPayable: Used in 12 purchase invoice(s);
GeneralLedger: Referenced in 8 ledger journal line(s)
Validation Error Safety
ERROR: Cannot delete tax group 'VAT-EXPORT' because it is currently being used.
Usage found: AccountsReceivable: Validation error occurred - assuming usage exists for safety;
GeneralLedger: No usage found
Repository Requirements
The TaxGroup repository must support the following additional methods for deletion validation:
public partial interface ITaxGroupRepository
{
// Existing methods...
/// <summary>
/// Counts ledger journal lines that reference this tax group
/// </summary>
Task<int> CountLedgerJournalLinesUsingTaxGroupAsync(
Guid taxGroupId,
CancellationToken cancellationToken = default);
/// <summary>
/// Soft deletes a tax group by marking it as inactive
/// </summary>
void Delete(TaxGroup taxGroup);
}
Key Design Decisions
Why Cross-Module Validation?
- Referential Integrity: Ensures no orphaned references across module boundaries
- Business Continuity: Prevents disruption of ongoing business processes
- Audit Trail: Maintains historical transaction integrity for regulatory compliance
Why Soft Delete?
- Audit Requirements: Tax configurations often need to be preserved for audit trails
- Historical Reference: Posted transactions maintain valid references to tax configurations
- Reversibility: Accidental deletions can be reversed by reactivating the tax group
Why Domain Exceptions?
- Business Rule Enforcement: Exceptions represent violated business rules rather than technical failures
- Rich Context: Provide detailed information about what prevents deletion
- User Experience: Enable meaningful error messages in the user interface
- Consistent Handling: Standardized approach across all tax entity deletions