LedgerJournalHeader Aggregate
Overview
The LedgerJournalHeader aggregate is the core domain model for managing all journal entries in the General Ledger system. It represents a container for multiple journal lines that form balanced accounting transactions, enforcing double-entry bookkeeping rules and coordinating the posting lifecycle.
This aggregate implements the Strategy pattern for journal-type-specific posting logic and serves as the transactional boundary for all journal operations.
Aggregate Structure
LedgerJournalHeader (Aggregate Root)
├── Properties
│ ├── JournalBatchNumber (string)
│ ├── JournalType (JournalType enumeration)
│ ├── JournalName (LedgerJournalName)
│ ├── Status (JournalStatus: Open | Posted)
│ ├── Currency (Currency)
│ ├── PostedDate (DateTimeOffset?)
│ ├── DocumentNumber (string?)
│ ├── TotalCreditAmount (Money)
│ ├── TotalDebitAmount (Money)
│ └── DefaultDimensionId (Guid?)
├── Entities
│ └── Lines (IReadOnlyCollection<LedgerJournalLine>)
└── Behaviors
├── AddLine()
├── UpdateJournalLine()
├── RemoveLine()
├── Post()
├── CreateReversalJournalHeader()
└── MarkAsReversed()
Domain Invariants
1. Balance Requirement
Rule: A journal must be balanced (total debits = total credits) before it can be posted.
Enforcement:
IsBalancedproperty checks:TotalCreditAmount == TotalDebitAmount- Posting strategies validate balance before allowing posting
- Running totals automatically maintained via
UpdateTotalsAfterLineAdded()andUpdateTotalsAfterLineRemoved()
Example:
// Valid - balanced journal
journal.AddLine(new LedgerJournalLine(...) { DebitAmount = 1000 });
journal.AddLine(new LedgerJournalLine(...) { CreditAmount = 1000 });
Assert.True(journal.IsBalanced); // Total DR = 1000, Total CR = 1000
// Invalid - unbalanced journal
journal.AddLine(new LedgerJournalLine(...) { DebitAmount = 1000 });
journal.AddLine(new LedgerJournalLine(...) { CreditAmount = 500 });
Assert.False(journal.IsBalanced); // Total DR = 1000, Total CR = 500
journal.Post(strategyFactory); // ❌ Fails validation
2. Posted Journals Are Immutable
Rule: Once a journal is posted (IsPosted == true), it cannot be modified or deleted.
Enforcement: All mutation methods check IsPosted and throw InvalidOperationException if true.
Protected Methods:
AddLine()- Cannot add lines to posted journalRemoveLine()- Cannot remove lines from posted journalUpdateJournalLine()- Cannot update lines on posted journalUpdate()- Cannot update journal propertiesClearLines()- Cannot clear lines from posted journalSetDefaultDimensions()- Cannot modify default dimensions
Example:
journal.Post(strategyFactory);
Assert.True(journal.IsPosted);
// All these operations throw InvalidOperationException
journal.AddLine(newLine); // ❌ Throws
journal.RemoveLine(lineId); // ❌ Throws
journal.UpdateJournalLine(lineId, ...); // ❌ Throws
journal.Update(...); // ❌ Throws
3. Offset Entry Handling
Rule: Lines with offset accounts automatically create balancing entries, affecting both TotalDebit and TotalCredit.
Logic:
- If line has
OffsetAccountIdand is a debit, system implicitly creates credit to offset account - If line has
OffsetAccountIdand is a credit, system implicitly creates debit to offset account - Totals calculation accounts for these implicit entries
Example:
// Single line with offset account
journal.AddLine(new LedgerJournalLine(...) {
AccountId = cashAccountId,
DebitAmount = 1000,
OffsetAccountId = revenueAccountId,
OffsetAccountType = JournalTransactionAccountType.GLAccount
});
// Result:
// TotalDebitAmount = 1000 (from Cash debit)
// TotalCreditAmount = 1000 (implicit Revenue credit from offset)
Assert.True(journal.IsBalanced); // Automatically balanced
4. Currency Consistency
Rule: All journal lines must use the same currency as the journal header.
Enforcement:
- Journal currency set at creation time
- Cannot change currency if journal has existing lines
- Lines inherit currency from journal
Example:
var journal = new LedgerJournalHeader(..., currency: usdCurrency, ...);
journal.AddLine(new LedgerJournalLine(..., currencyCode: "USD", ...)); // ✓ Valid
// Attempt to change currency after adding lines
journal.Update(
ledgerJournalNameId: nameId,
voucherSeriesId: seriesId,
currencyCode: "EUR"
); // ❌ Throws: "Cannot change currency if journal has lines"
5. Line Numbering
Rule: Journal lines are sequentially numbered starting from 1.
Enforcement:
NextLineNumberproperty automatically calculates next available numberAddLine()automatically assigns line numbersRenumberLines()method re-sequences all lines if gaps exist
Example:
journal.AddLine(line1); // Assigned LineNumber = 1
journal.AddLine(line2); // Assigned LineNumber = 2
journal.AddLine(line3); // Assigned LineNumber = 3
journal.RemoveLine(line2.Id, renumberLines: true);
// Line 1: LineNumber = 1
// Line 3: LineNumber = 2 (renumbered)
Key Properties
Identification Properties
JournalBatchNumber (string)
- Unique identifier for the journal batch
- User-visible reference number
- Used for tracking and reporting
DocumentNumber (string?)
- Optional document number for journal
- Can be auto-generated from number sequence
- Distinct from voucher numbers on lines
Configuration Properties
LedgerJournalNameId (Guid)
- Foreign key to
LedgerJournalNameconfiguration - Determines journal behavior, posting strategy, voucher numbering
- Cannot be changed after journal is posted
JournalTypeId (int?)
- Foreign key to
JournalTypeenumeration - Determines posting strategy to use:
0- Daily (General journals)1- Customer Payment2- Vendor Payment3- Payroll Disbursement4- Tax Settlement
CurrencyId / CurrencyCode (Guid / string)
- Currency for all journal lines
- Immutable once lines are added
- Used for monetary calculations
DefaultDimensionId (Guid?)
- Optional default financial dimension combination
- Applied to new lines if specified
- Can be changed before posting
Status Properties
Status (JournalStatus)
- Current status:
OpenorPosted - Changes from
OpentoPostedduring posting - Determines mutability of journal
PostedDate (DateTimeOffset?)
- Timestamp when journal was posted
nullfor unposted journals- Used for audit trail and period validation
IsPosted (bool)
- Computed property:
PostedDate.HasValue - Convenience check for posted status
Financial Properties
TotalCreditAmount (Money)
- Sum of all credit amounts including implicit offset credits
- Automatically maintained on line add/remove/update
- Must equal
TotalDebitAmountfor posting
TotalDebitAmount (Money)
- Sum of all debit amounts including implicit offset debits
- Automatically maintained on line add/remove/update
- Must equal
TotalCreditAmountfor posting
IsBalanced (bool)
- Computed property:
TotalCreditAmount == TotalDebitAmount - Core posting prerequisite
Reversal Properties
ReverseDate (DateTimeOffset?)
- Date when journal was reversed
nullfor non-reversed journals
ReverseJournalId (Guid?)
- ID of the reversal journal created
- Links original journal to its reversal
Collection Properties
Lines (IReadOnlyCollection<LedgerJournalLine>)
- Read-only collection of journal lines
- Backed by private
List<LedgerJournalLine> - Modified via aggregate methods only
LineCount (int)
- Total number of lines in journal
- Computed from
Lines.Count
NextLineNumber (int)
- Next available line number
- Computed:
Max(LineNumber) + 1or1if empty
Core Behaviors
Line Management
AddLine(LedgerJournalLine line)
Adds a new line to the journal and updates running totals.
Preconditions:
- Journal must not be posted
Effects:
- Assigns sequential line number
- Adds line to collection
- Updates
TotalCreditAmountandTotalDebitAmount - Accounts for offset entry if present
Example:
var line = new LedgerJournalLine(
voucher: "V001",
accountId: cashAccountId,
accountType: JournalTransactionAccountType.GLAccount,
description: "Cash receipt",
creditAmount: 0,
debitAmount: 1000,
offsetAccountType: JournalTransactionAccountType.GLAccount,
offsetAccountId: revenueAccountId,
currencyId: currencyId,
currencyCode: "USD",
transactionDate: DateTimeOffset.Now
);
journal.AddLine(line);
// Line.LineNumber = 1
// TotalDebitAmount = 1000
// TotalCreditAmount = 1000 (from offset)
UpdateJournalLine(...)
Updates an existing journal line with new values and recalculates totals.
Preconditions:
- Journal must not be posted
- Line must exist in journal
Effects:
- Removes old amounts from totals
- Updates line properties
- Adds new amounts to totals
- Raises
LedgerJournalLineUpdatedDomainEvent
Example:
journal.UpdateJournalLine(
lineId: lineId,
voucher: "V001",
accountId: newAccountId,
accountType: JournalTransactionAccountType.GLAccount,
description: "Updated description",
creditAmount: 0,
debitAmount: 1500, // Changed from 1000
currency: usdCurrency,
transactionDate: DateTimeOffset.Now,
mainDimensionCombinationId: dimensionCombId
);
// Domain event added with old vs new values for audit trail
RemoveLine(Guid lineId, bool renumberLines = true)
Removes a line from the journal and updates totals.
Preconditions:
- Journal must not be posted
- Line must exist in journal
Effects:
- Removes line from collection
- Updates
TotalCreditAmountandTotalDebitAmount - Optionally renumbers remaining lines
- Raises
LedgerJournalLineRemovedDomainEvent
Example:
journal.RemoveLine(lineId, renumberLines: true);
// Line removed
// Totals recalculated
// Remaining lines renumbered sequentially
RenumberLines()
Renumbers all lines sequentially starting from 1.
Usage: Called automatically by RemoveLine() or manually after bulk operations.
Example:
// Before: Line numbers 1, 3, 5, 8
journal.RenumberLines();
// After: Line numbers 1, 2, 3, 4
Posting Operations
Post(IJournalStrategyFactory strategyFactory)
Posts the journal using journal-type-specific posting strategy.
Workflow:
- Retrieve posting strategy for journal type
- Validate journal using strategy's validation rules
- Set
PostedDateandStatus = Posted - Mark all lines as posted
- Execute strategy-specific posting logic
- Return success/failure result
Preconditions:
- Journal must be balanced
- Strategy must exist for journal type
- Journal-specific validation must pass
Effects:
- Sets
PostedDate = DateTimeOffset.UtcNow - Sets
Status = JournalStatus.Posted - Calls
SetPosted()on all lines - Raises domain events via strategy
Example:
var result = journal.Post(strategyFactory);
if (result.IsSuccess)
{
Assert.True(journal.IsPosted);
Assert.NotNull(journal.PostedDate);
Assert.Equal(JournalStatus.Posted, journal.Status);
// Domain events dispatched by infrastructure
}
else
{
// Handle validation errors
Console.WriteLine(result.Error);
}
Posting Strategies:
GeneralJournalPostingStrategy- For daily/general journalsCustomerPaymentPostingStrategy- For customer payment journalsVendorPaymentPostingStrategy- For vendor payment journalsTaxSettlementPostingStrategy- For tax settlement journals
Reversal Operations
CreateReversalJournalHeader(string reversalJournalVoucher, string reason)
Creates a new journal header for reversing this journal.
Returns: New LedgerJournalHeader with:
- Same journal name, type, currency
- New journal batch number
- Empty lines (caller must add reversed lines)
Usage: Called by reversal process to create reversal journal structure.
Example:
var reversalJournal = originalJournal.CreateReversalJournalHeader(
reversalJournalVoucher: "REV-V001",
reason: "Correction for incorrect posting"
);
// Caller adds reversed lines using LedgerJournalLine.ReverseLine()
foreach (var line in originalJournal.Lines)
{
var reversedLine = line.ReverseLine(
reversalVoucher: "REV-V001",
reasonForReversal: "Correction",
reversalDate: DateTimeOffset.Now
);
reversalJournal.AddLine(reversedLine);
}
reversalJournal.Post(strategyFactory);
MarkAsReversed(Guid reversalJournalId, DateTimeOffset reverseDate, LedgerJournalHeader reversalJournal)
Marks this journal as reversed and links to reversal journal.
Effects:
- Sets
ReverseJournalIdandReverseDate - Raises
JournalReversedDomainEvent
Example:
originalJournal.MarkAsReversed(
reversalJournalId: reversalJournal.Id,
reverseDate: DateTimeOffset.Now,
reversalJournalForEvent: reversalJournal
);
Assert.NotNull(originalJournal.ReverseDate);
Assert.Equal(reversalJournal.Id, originalJournal.ReverseJournalId);
Voucher Management
GetLastVoucherStatus()
Determines the status of the last voucher in the journal.
Returns: Tuple of (VoucherNumber, IsBalanced)
Purpose: Used by voucher generation logic to decide whether to reuse last voucher or generate new one.
Business Rule:
- If last voucher is balanced: Generate new voucher
- If last voucher is unbalanced: Reuse last voucher
Example:
var (lastVoucher, isBalanced) = journal.GetLastVoucherStatus();
if (lastVoucher == null)
{
// First line - generate new voucher
}
else if (isBalanced)
{
// Last voucher complete - generate new voucher
}
else
{
// Last voucher incomplete - reuse it
}
GetVoucherBalance(string voucherNumber)
Calculates balance for a specific voucher within the journal.
Returns: VoucherBalanceResult containing:
TotalDebit: Sum of all debits for the voucherTotalCredit: Sum of all credits for the voucherIsBalanced: Whether debits equal credits
Accounts For:
- Explicit debit/credit amounts on lines
- Implicit amounts from offset entries
Example:
var balance = journal.GetVoucherBalance("V001");
Console.WriteLine($"Debit: {balance.TotalDebit}");
Console.WriteLine($"Credit: {balance.TotalCredit}");
Console.WriteLine($"Balanced: {balance.IsBalanced}");
Query Operations
AccountJournalTransactions(Guid accountId)
Returns all lines for a specific account.
Returns: IEnumerable<LedgerJournalLine>
Example:
var cashLines = journal.AccountJournalTransactions(cashAccountId);
var totalCashDebits = cashLines.Sum(l => l.DebitAmount);
var totalCashCredits = cashLines.Sum(l => l.CreditAmount);
GetAccountAmounts(Guid accountId)
Returns total credit and debit amounts for a specific account.
Returns: Tuple of (Credit, Debit)
Example:
var (totalCredit, totalDebit) = journal.GetAccountAmounts(cashAccountId);
var netAmount = totalDebit - totalCredit;
GetLedgerJournalLine(Guid lineId)
Retrieves a specific line by ID.
Returns: LedgerJournalLine? - null if not found
Example:
var line = journal.GetLedgerJournalLine(lineId);
if (line != null)
{
Console.WriteLine(line.GetLineSummary());
}
Maintenance Operations
Update(Guid ledgerJournalNameId, Guid? voucherSeriesId, string currencyCode)
Updates journal configuration properties.
Preconditions:
- Journal must not be posted
- Cannot change currency if lines exist
Example:
journal.Update(
ledgerJournalNameId: newJournalNameId,
voucherSeriesId: newSeriesId,
currencyCode: "USD"
);
ClearLines()
Removes all lines and resets totals.
Preconditions:
- Journal must not be posted
Effects:
- Clears line collection
- Resets totals to zero
Example:
journal.ClearLines();
Assert.Equal(0, journal.LineCount);
Assert.Equal(0, journal.TotalDebitAmount.Amount);
Assert.Equal(0, journal.TotalCreditAmount.Amount);
RecalculateTotals()
Recalculates totals from scratch based on current lines.
Usage: Called after bulk line operations or data migrations.
Example:
// After direct line manipulation (in tests or migrations)
journal.RecalculateTotals();
Assert.True(journal.IsBalanced);
SetDefaultDimensions(Guid? defaultDimensionId)
Sets the default financial dimension combination for the journal.
Preconditions:
- Journal must not be posted
Effects:
- Sets
DefaultDimensionId - Raises
JournalHeaderDefaultDimensionsChangedDomainEvent
Example:
journal.SetDefaultDimensions(dimensionSetId);
Assert.Equal(dimensionSetId, journal.DefaultDimensionId);
CanBeDeleted()
Checks if journal can be deleted.
Returns: DeleteResult with success/failure
Business Rule: Posted journals cannot be deleted
Example:
var canDelete = journal.CanBeDeleted();
if (canDelete.IsSuccess)
{
journal.Delete();
}
Domain Events
LedgerJournalPostedDomainEvent
Raised when a journal is successfully posted.
Contains: Reference to posted journal
Handled By: Application layer creates GL transactions, updates subledgers
LedgerJournalLineUpdatedDomainEvent
Raised when a journal line is updated.
Contains:
- Old and new account IDs
- Old and new dimension combinations
- Old and new amounts
- Update metadata (who, when, why)
Purpose: Audit trail and change tracking
LedgerJournalLineRemovedDomainEvent
Raised when a journal line is removed.
Contains: Journal ID and line ID
Purpose: Cleanup and audit trail
JournalReversedDomainEvent
Raised when a journal is marked as reversed.
Contains: Original journal and reversal journal
Purpose: Link journals and notify downstream systems
JournalHeaderDefaultDimensionsChangedDomainEvent
Raised when default dimensions are set/changed.
Contains: Journal ID and new default dimension ID
Purpose: Update UI and tracking
LedgerJournalDeletedDomainEvent
Raised when a journal is deleted.
Contains: Reference to deleted journal
Purpose: Cleanup and audit trail
Strategy Pattern Implementation
The aggregate uses the Strategy pattern to handle journal-type-specific posting logic.
Design
Interface: IJournalPostingStrategy
Methods:
CanHandle(JournalType)- Checks if strategy handles journal typeValidateForPosting(LedgerJournalHeader)- Type-specific validationOnPosting(LedgerJournalHeader)- Type-specific posting logic
Factory: IJournalStrategyFactory - Returns appropriate strategy for journal type
Available Strategies
GeneralJournalPostingStrategy
For: Daily/General journals (JournalType.Daily)
Validation:
- Balanced debits and credits
- All lines have valid accounts
- No payment details on lines
Posting Logic:
- Raises
LedgerJournalPostedDomainEvent - Standard GL transaction creation
CustomerPaymentPostingStrategy
For: Customer payment journals (JournalType.CustomerPayment)
Validation:
- Payment lines must have
PaymentDetails - Settlement amounts must match line amounts
- Customer accounts required
Posting Logic:
- Raises payment-specific domain events
- Updates customer balances
- Records settlements
VendorPaymentPostingStrategy
For: Vendor payment journals (JournalType.VendorPayment)
Validation:
- Payment lines must have
PaymentDetails - Settlement amounts must match line amounts
- Vendor accounts required
Posting Logic:
- Raises payment-specific domain events
- Updates vendor balances
- Records settlements
TaxSettlementPostingStrategy
For: Tax settlement journals (JournalType.TaxSettlement)
Validation:
- Tax-specific validation rules
Posting Logic:
- Settles tax transactions
- Updates tax authority balances
Usage Example
// Strategy automatically selected based on journal type
var result = journal.Post(strategyFactory);
// Factory internally:
// 1. Gets journal type from journal.JournalTypeId
// 2. Returns appropriate strategy (e.g., GeneralJournalPostingStrategy)
// 3. Strategy validates and posts with type-specific logic
Integration with Other Aggregates
LedgerJournalName
- Relationship: Many journals belong to one journal name
- Usage: Provides configuration template for journal behavior
- Properties Used: Journal type, voucher series, offset account defaults
Currency
- Relationship: Each journal has one currency
- Usage: Determines currency for all monetary amounts
- Constraint: Cannot change currency once lines exist
NumberSequence
- Relationship: Optional number sequence for document numbers
- Usage: Auto-generates unique document numbers
- Managed By: Application layer preprocessing
DimensionCombination
- Relationship: Optional default dimension combination
- Usage: Applied to new lines as default financial dimensions
- Managed By:
DefaultDimensionIdproperty
Persistence Considerations
Aggregate Boundary
LedgerJournalHeaderis the aggregate rootLedgerJournalLineentities are always loaded/saved with header- Repository operates on full aggregate (header + lines)
Loading Strategy
// Always load with lines for full aggregate
var journal = await repository.GetByIdAsync(journalId, includeLines: true);
Concurrency
- Use optimistic concurrency with version/timestamp
- Entire aggregate saved/updated in single transaction
- Version check on header prevents lost updates
Cascade Operations
- Deleting journal cascades to delete all lines
- Updating journal does not cascade to lines (explicit line updates required)
Business Rules Summary
| Rule | Enforcement | Impact |
|---|---|---|
| Journal must be balanced to post | Post() method via strategies | Cannot post unbalanced journal |
| Posted journals are immutable | All mutation methods check IsPosted | Cannot modify after posting |
| Lines must use journal currency | Currency validation at line creation | Consistent currency across journal |
| Line numbers are sequential | AddLine() auto-assigns | Easy reference and ordering |
| Offset entries auto-balance | Total calculation includes offsets | Single-line entries can be balanced |
| Cannot change currency with lines | Update() validation | Prevents currency mismatch |
| Cannot delete posted journals | CanBeDeleted() check | Audit trail preservation |
Testing Strategies
Unit Tests
Invariant Tests:
[Fact]
public void AddLine_UpdatesTotals_Correctly()
{
var journal = CreateTestJournal();
var line = CreateDebitLine(amount: 1000);
journal.AddLine(line);
Assert.Equal(1000, journal.TotalDebitAmount.Amount);
}
[Fact]
public void Post_WhenUnbalanced_Fails()
{
var journal = CreateTestJournal();
journal.AddLine(CreateDebitLine(1000));
// No credit line - unbalanced
var result = journal.Post(strategyFactory);
Assert.True(result.IsFailure);
}
State Transition Tests:
[Fact]
public void Post_Changes_StatusToPosted()
{
var journal = CreateBalancedJournal();
journal.Post(strategyFactory);
Assert.Equal(JournalStatus.Posted, journal.Status);
Assert.True(journal.IsPosted);
Assert.NotNull(journal.PostedDate);
}
Mutation Protection Tests:
[Fact]
public void AddLine_WhenPosted_Throws()
{
var journal = CreateAndPostJournal();
var exception = Assert.Throws<InvalidOperationException>(
() => journal.AddLine(CreateDebitLine(1000))
);
Assert.Contains("posted", exception.Message);
}
Integration Tests
Repository Tests:
[Fact]
public async Task Repository_SavesAndLoads_CompleteAggregate()
{
var journal = CreateJournalWithLines();
await repository.AddAsync(journal);
await unitOfWork.SaveChangesAsync();
var loaded = await repository.GetByIdAsync(journal.Id);
Assert.Equal(journal.LineCount, loaded.LineCount);
Assert.Equal(journal.TotalDebitAmount, loaded.TotalDebitAmount);
}
Posting Tests:
[Fact]
public async Task Post_CreatesGeneralJournalEntries()
{
var journal = CreateBalancedJournal();
await repository.AddAsync(journal);
journal.Post(strategyFactory);
await unitOfWork.SaveChangesAsync();
var entries = await glEntryRepository
.GetByJournalIdAsync(journal.Id);
Assert.NotEmpty(entries);
}
Common Patterns and Anti-Patterns
Pattern: Balanced Batch Entry
// ✅ Good: Balanced journal
var journal = new LedgerJournalHeader(...);
journal.AddLine(new LedgerJournalLine(...) { DebitAmount = 1000 });
journal.AddLine(new LedgerJournalLine(...) { CreditAmount = 1000 });
journal.Post(strategyFactory);
Pattern: Single-Line with Offset
// ✅ Good: Auto-balanced single line
journal.AddLine(new LedgerJournalLine(...) {
DebitAmount = 1000,
OffsetAccountId = revenueAccountId
});
// Automatically balanced via offset
Anti-Pattern: Direct Line Collection Mutation
// ❌ Bad: Bypassing aggregate methods
journal.Lines.Add(line); // Compile error - read-only collection
// ✅ Good: Use aggregate methods
journal.AddLine(line);
Anti-Pattern: Manual Total Calculation
// ❌ Bad: Recalculating totals externally
var total = journal.Lines.Sum(l => l.DebitAmount);
// ✅ Good: Trust aggregate's totals
var total = journal.TotalDebitAmount.Amount;
Anti-Pattern: Posting Without Strategy
// ❌ Bad: Bypassing strategy pattern
journal.SetPosted();
journal.PostedDate = DateTimeOffset.Now;
// ✅ Good: Use Post method with strategy
journal.Post(strategyFactory);
References
- Domain Model:
GeneralLedger.Domain/Aggregates/LedgerJournalAggregate/ - Posting Strategies:
GeneralLedger.Domain/Aggregates/LedgerJournalAggregate/Strategy/ - Domain Events:
GeneralLedger.Domain/Aggregates/LedgerJournalAggregate/Events/ - Related Aggregates:
LedgerJournalName.aggregate.mdGeneralJournalEntry.aggregate.mdDimensionCombination.aggregate.md