Skip to main content

General Ledger Domain Documentation

Overview

This section contains domain documentation for the General Ledger sub-module, which serves as the core accounting foundation with multi-dimensional transaction processing capabilities. The domain layer represents pure business logic independent of infrastructure concerns.

Core Aggregates

Ledger Aggregate Complete

Purpose: Central configuration and management of accounting ledgers.

Key Responsibilities:

  • Ledger configuration and setup
  • Chart of accounts management
  • Fiscal calendar integration
  • Currency and exchange rate handling
  • Account structure assignment and validation

Business Rules:

  • Each ledger must have exactly one chart of accounts
  • Fiscal periods must be sequential and non-overlapping
  • Account structures must not have overlapping MainAccount constraints
  • Only one reporting currency per ledger

Domain Events:

  • LedgerCreatedDomainEvent
  • AccountStructureAddedToLedgerDomainEvent
  • FiscalPeriodOpenedDomainEvent
  • FiscalPeriodClosedDomainEvent

Key Methods:

public void AddAccountStructure(DimensionHierarchy accountStructure)
public void OpenFiscalPeriod(FiscalPeriod period)
public void CloseFiscalPeriod(Guid periodId, DateTime closingDate)
public bool IsValidTransactionDate(DateTime transactionDate)

DimensionHierarchy Aggregate Complete

Purpose: Defines account structures (dimensional hierarchies) for financial classification.

Key Responsibilities:

  • Account structure definition and management
  • Dimension level ordering and constraints
  • MainAccount constraint tree management
  • Activation/deactivation lifecycle

Business Rules:

  • Level ordering must be sequential (1, 2, 3, ...)
  • MainAccount constraint ranges cannot overlap within a ledger
  • Mandatory levels must be filled for valid combinations
  • Cannot be deleted if referenced by existing combinations
  • Activation requires constraint overlap validation

Domain Events:

  • DimensionHierarchyCreatedDomainEvent
  • DimensionHierarchyActivatedDomainEvent
  • DimensionHierarchyDeactivatedDomainEvent
  • ConstraintOverlapDetectedDomainEvent

Key Methods:

public void Activate(IConstraintOverlapDetectionService overlapService, Ledger ledger)
public void AddLevel(int level, Guid dimensionAttributeId, bool isMandatory)
public void AddConstraint(ConstraintNode constraintNode)
public bool MatchesMainAccount(string mainAccountValue)

DimensionCombination Aggregate Complete

Purpose: Immutable combinations of financial dimension values representing complete account classifications.

Key Responsibilities:

  • Unique dimension value combinations
  • Display value generation and caching
  • Hash-based identity and deduplication
  • Relationship to account structures

Business Rules:

  • Once created, combinations are immutable
  • Hash-based uniqueness prevents duplicates
  • All mandatory dimensions must be provided
  • Values must exist and be active in their respective dimensions
  • Display values automatically generated for UI presentation

Domain Events:

  • DimensionCombinationCreatedDomainEvent
  • DimensionCombinationReusedDomainEvent

Key Methods:

public static DimensionCombination Create(
IReadOnlyList<DimensionSegment> segments,
DimensionHierarchy accountStructure,
LedgerDimensionType dimensionType)
public string GenerateDisplayValue()
public bool IsValidForAccountStructure(DimensionHierarchy accountStructure)

LedgerJournalHeader Aggregate Pending Documentation

Purpose: Journal management with complete posting lifecycle.

Key Responsibilities:

  • Journal creation and line management
  • Posting validation and execution
  • Reversal journal creation
  • Balance validation (debits = credits)
  • Integration with fiscal periods

Business Rules:

  • Journals must balance before posting
  • Cannot post to closed fiscal periods
  • Reversals create new journals with opposite amounts
  • Posted journals cannot be modified
  • All transactions must have valid dimension combinations

Planned Domain Events:

  • LedgerJournalCreatedDomainEvent
  • LedgerJournalLineAddedDomainEvent
  • LedgerJournalPostedDomainEvent
  • LedgerJournalReversedDomainEvent

LedgerJournalLine Entity Pending Documentation

Purpose: Individual transaction lines within journals.

Key Responsibilities:

  • Transaction amount and description management
  • Dimension combination linkage
  • Currency handling
  • Date validation

Business Rules:

  • Either debit or credit amount (not both)
  • Must reference valid dimension combination
  • Transaction date must be within journal's allowed range
  • Cannot be modified after journal posting

Domain Services

AccountStructureResolutionService Complete

Purpose: Resolves the appropriate account structure based on MainAccount value.

Key Operations:

Task<AccountStructureResolutionResult> ResolveAccountStructureAsync(
string mainAccountValue,
Ledger ledger)

Business Logic:

  • Searches all active account structures assigned to ledger
  • Matches MainAccount value against constraint trees
  • Prevents ambiguous matches (Golden Rule enforcement)
  • Returns success/failure with detailed messages

Integration Points:

  • Used by journal entry processes
  • Called from dimension resolution APIs
  • Validates account structure assignments

ConstraintOverlapDetectionService Complete

Purpose: Prevents MainAccount constraint overlaps that would cause ambiguity.

Key Operations:

Task<OverlapDetectionResult> DetectOverlapsAsync(
DimensionHierarchy dimensionHierarchy,
Ledger ledger)

Business Logic:

  • Critical overlaps prevent account structure activation
  • Warning overlaps generate notifications but allow activation
  • Uses constraint type logic from ConstraintType entity
  • Analyzes constraint trees for all possible conflicts

Enforcement Points:

  • DimensionHierarchy activation
  • Adding DimensionHierarchy to Ledger
  • MainAccount constraint modification

DimensionCombinationDomainService Complete

Purpose: Creates and manages dimension combinations with deduplication.

Key Operations:

Task<DimensionCombination> CreateOrReuseCombinationAsync(
IReadOnlyList<DimensionSegment> segments,
DimensionHierarchy accountStructure,
LedgerDimensionType dimensionType)

Business Logic:

  • Hash-based deduplication ensures reuse of identical combinations
  • Validates completeness against mandatory dimension levels
  • Generates display values for UI presentation
  • Maintains referential integrity for posted transactions

Performance Considerations:

  • Hash computation optimized for quick lookups
  • Display value generation cached
  • Reuse patterns minimize storage overhead

JournalPostingDomainService Planned

Purpose: Orchestrates journal posting with all validations.

Planned Operations:

Task<PostingResult> PostJournalAsync(LedgerJournalHeader journal, DateTime postingDate)
Task<LedgerJournalHeader> ReverseJournalAsync(LedgerJournalHeader originalJournal, string reason)

Planned Business Logic:

  • Balance validation (debits = credits)
  • Fiscal period validation
  • Dimension combination validation
  • General ledger entry creation
  • Audit trail generation

Value Objects

DimensionSegment

Purpose: Represents a single dimension value within a combination.

public record DimensionSegment(
Guid DimensionAttributeId,
Guid DimensionAttributeValueId,
int Level)
{
public bool IsValid => DimensionAttributeId != Guid.Empty &&
DimensionAttributeValueId != Guid.Empty &&
Level > 0;
}

ConstraintNode

Purpose: Represents a node in the MainAccount constraint tree.

public record ConstraintNode(
ConstraintType ConstraintType,
string Value,
IReadOnlyList<ConstraintNode> Children)
{
public bool Matches(string mainAccountValue) => ConstraintType.Evaluate(Value, mainAccountValue);
}

Money

Purpose: Ensures currency consistency across operations.

public record Money(decimal Amount, string CurrencyCode)
{
public Money Add(Money other)
{
if (CurrencyCode != other.CurrencyCode)
throw new InvalidOperationException("Cannot add amounts in different currencies");
return new Money(Amount + other.Amount, CurrencyCode);
}
}

AccountingPeriod

Purpose: Represents fiscal periods with status management.

public record AccountingPeriod(
DateTime StartDate,
DateTime EndDate,
PeriodStatus Status)
{
public bool IsOpen => Status == PeriodStatus.Open;
public bool Contains(DateTime date) => date >= StartDate && date <= EndDate;
}

Key Business Rules

Golden Rule: MainAccount Uniqueness

Rule: A single MainAccount value cannot exist in more than one active DimensionHierarchy assigned to the same Ledger.

Rationale: Prevents ambiguous account structure resolution during transaction entry.

Enforcement Points:

  • DimensionHierarchy activation via ConstraintOverlapDetectionService
  • Adding DimensionHierarchy to Ledger
  • MainAccount constraint modification

Implementation:

public async Task<OverlapDetectionResult> DetectOverlapsAsync(
DimensionHierarchy candidateStructure,
Ledger ledger)
{
var existingStructures = ledger.GetActiveAccountStructures();
var overlaps = new List<ConstraintOverlap>();

foreach (var existing in existingStructures)
{
var overlap = DetectConstraintOverlap(candidateStructure, existing);
if (overlap.IsCritical)
{
overlaps.Add(overlap);
}
}

return new OverlapDetectionResult(overlaps);
}

Dimension Combination Immutability

Rule: Once created, DimensionCombination entities cannot be modified.

Rationale:

  • Ensures referential integrity for posted transactions
  • Enables safe reuse of combinations
  • Prevents historical data corruption

Implementation: Private setters and no update methods

Journal Balance Requirement

Rule: Journal total debits must equal total credits before posting.

Enforcement:

public PostingValidationResult ValidateForPosting()
{
var totalDebits = Lines.Sum(l => l.DebitAmount);
var totalCredits = Lines.Sum(l => l.CreditAmount);

if (totalDebits != totalCredits)
{
return PostingValidationResult.Failure("Journal is not balanced");
}

return PostingValidationResult.Success();
}

Fiscal Period Validation

Rule: Transactions can only be posted to open fiscal periods.

Enforcement:

public bool CanPostTransaction(DateTime transactionDate)
{
var period = FiscalCalendar.GetPeriodForDate(transactionDate);
return period?.IsOpen == true;
}

Domain Event Flows

Journal Posting Workflow

Dimension Combination Creation


Integration Points

With Financial Dimensions Module

// GL depends on FD for dimension value resolution
public class LedgerJournalLine
{
public async Task SetDimensionCombinationAsync(
IList<DimensionSegmentInput> segmentInputs,
IDimensionValueResolverService dimensionResolver,
IDimensionCombinationDomainService combinationService)
{
// 1. Resolve segment values using FD service
var resolvedSegments = await dimensionResolver
.ResolveSegmentValuesAsync(segmentInputs);

// 2. Create or reuse combination using GL service
var combination = await combinationService
.CreateOrReuseCombinationAsync(resolvedSegments, accountStructure);

// 3. Link to journal line
LedgerDimensionId = combination.Id;
}
}

With Accounts Receivable Module (Future)

// AR invoices inherit customer default dimensions
public class CustomerInvoicePostingDomainService
{
public async Task<LedgerJournalHeader> CreateJournalFromInvoiceAsync(
CustomerInvoice invoice)
{
var customer = await _customerRepository.GetByIdAsync(invoice.CustomerId);
var defaultDimensions = await _dimensionService
.GetDimensionSetAsync(customer.DefaultDimensionId);

var journal = new LedgerJournalHeader(/* parameters */);

foreach (var invoiceLine in invoice.Lines)
{
var inheritedDimensions = MergeDimensions(
defaultDimensions,
invoiceLine.OverrideDimensions);

var journalLine = new LedgerJournalLine(/* parameters */);
await journalLine.SetDimensionCombinationAsync(inheritedDimensions, /*services*/);

journal.AddLine(journalLine);
}

return journal;
}
}

Testing Patterns

Aggregate Testing

[TestFixture]
public class LedgerJournalHeaderTests
{
[Test]
public void Post_Should_Raise_Domain_Event_When_Valid()
{
// Arrange
var journal = CreateBalancedJournal();
var mockValidator = new Mock<IJournalValidator>();
mockValidator.Setup(v => v.ValidateForPosting(journal))
.Returns(PostingValidationResult.Success());

// Act
journal.Post(DateTime.Now, mockValidator.Object);

// Assert
var domainEvent = journal.GetDomainEvents()
.OfType<LedgerJournalPostedDomainEvent>().First();
Assert.IsNotNull(domainEvent);
Assert.AreEqual(JournalStatus.Posted, journal.Status);
}

[Test]
public void Post_Should_Throw_When_Unbalanced()
{
// Arrange
var journal = CreateUnbalancedJournal();
var mockValidator = new Mock<IJournalValidator>();
mockValidator.Setup(v => v.ValidateForPosting(journal))
.Returns(PostingValidationResult.Failure("Unbalanced"));

// Act & Assert
Assert.Throws<JournalPostingException>(
() => journal.Post(DateTime.Now, mockValidator.Object));
}
}

Domain Service Testing

[TestFixture]
public class AccountStructureResolutionServiceTests
{
[Test]
public async Task Should_Resolve_Correct_Structure_For_Assets()
{
// Arrange
var service = new AccountStructureResolutionService();
var ledger = CreateLedgerWithAssetStructure(); // 1000-1999 range

// Act
var result = await service.ResolveAccountStructureAsync("1100", ledger);

// Assert
Assert.IsTrue(result.IsSuccess);
Assert.AreEqual("Assets Account Structure", result.ResolvedStructure.Name);
}

[Test]
public async Task Should_Fail_When_No_Structure_Matches()
{
// Arrange
var service = new AccountStructureResolutionService();
var ledger = CreateLedgerWithAssetStructure(); // 1000-1999 range

// Act
var result = await service.ResolveAccountStructureAsync("9999", ledger);

// Assert
Assert.IsFalse(result.IsSuccess);
Assert.Contains("No matching account structure", result.Message);
}
}

Performance Considerations

Aggregate Size Management

  • LedgerJournalHeader: Use lazy loading for large journals with many lines
  • DimensionCombination: Lightweight and cacheable, optimized for frequent access
  • DimensionHierarchy: Cache resolved structures to avoid repeated constraint evaluation

Event Processing

  • Domain events processed synchronously within transaction boundaries
  • Integration events processed asynchronously via message bus
  • Avoid heavy operations in domain event handlers

Memory Optimization

// Efficient dimension combination lookup
public class CachedDimensionCombinationService
{
private readonly IMemoryCache _cache;

public async Task<DimensionCombination> GetOrCreateAsync(
string combinationHash,
Func<Task<DimensionCombination>> factory)
{
return await _cache.GetOrCreateAsync(
$"combination:{combinationHash}",
async entry =>
{
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(4);
return await factory();
});
}
}

Future Enhancements

Planned Domain Features

  • Advanced Approval Workflows: Configurable journal approval processes
  • Multi-Currency Enhancements: Enhanced foreign currency handling
  • Intercompany Transactions: Cross-company transaction processing
  • Advanced Audit Capabilities: Enhanced audit trail and change tracking

Domain Model Evolution

  • Event Sourcing: Consider for complete audit requirements
  • Microservices Split: Potential bounded context evolution
  • Performance Optimization: Aggregate optimization for high-volume scenarios

Implementation References

Cross-Module References


Last Updated: [Current Date] | Version: 1.0 | Status: Active Development

Priority Items:

  1. Complete LedgerJournalHeader aggregate documentation
  2. Implement JournalPostingDomainService
  3. Add comprehensive integration testing
  4. Performance optimization for high-volume scenarios