انتقل إلى المحتوى الرئيسي

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

  1. Unique Code: Each TaxGroup must have a unique code within the system
  2. Tax Code Membership: A TaxGroup contains a collection of TaxCode entities that define the actual tax rates and posting accounts
  3. Entity Assignment: TaxGroups are assigned to master data entities (Customers, Vendors) to determine their default tax treatment
  4. 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 SalesTaxGroupId on any Customer record
  • Vendor Assignments: TaxGroup is set as SalesTaxGroupId on 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