LedgerJournalName Aggregate
File: /docs/domain/general-ledger/aggregates/ledger-journal-name.aggregate.md
Module: GeneralLedger
Namespace: GeneralLedger.Domain.Aggregates.LedgerJournalNameAggregate
Purpose
The LedgerJournalName aggregate serves as a configuration template that defines the behavior, validation rules, and default settings for ledger journals within the General Ledger module. It acts as a policy object that encapsulates journal processing rules, ensuring consistency in transaction entry, improving data quality, and enforcing financial controls across different journal types.
When a user creates a LedgerJournalHeader, they select a LedgerJournalName, which determines:
- The posting strategy to be applied (via JournalType)
- How voucher numbers are generated and managed
- Default offset account behavior for efficient data entry
- Allowed account types and transaction patterns
This design enables organizations to configure multiple journal templates optimized for specific business processes (e.g., daily general entries, customer payments, vendor payments, payroll) without requiring code changes.
Business Context
In enterprise accounting systems, journals are not one-size-fits-all. Different business processes require different entry patterns, controls, and automation:
- Daily General Journals: Flexible entry with balanced lines, each line pair getting its own voucher
- Payment Journals: Streamlined entry with a fixed bank account offset, single voucher per payment
- Subledger Journals: System-generated entries from sales/purchase invoices with specific posting logic
- Payroll Journals: Batch entries where the entire journal represents one logical transaction
LedgerJournalName provides the configuration layer that enables these different patterns while maintaining data integrity and audit requirements.
Aggregate Structure
Root Entity: LedgerJournalName
- Identity:
Guid Id(inherited from AggregateRootBase) - Core Properties:
Name(string, unique): User-friendly identifier displayed in journal creation UIDescription(string): Detailed explanation of the journal's intended use
- Behavioral Configuration:
JournalTypeId(int): Foreign key to JournalType enumerationJournalType(navigation): Determines the posting strategy implementationVoucherSeriesId(Guid): Foreign key to NumberSequence for voucher numberingVoucherSeries(navigation): Number sequence configuration for vouchersVoucherGenerationStrategy(enum): Controls voucher allocation behavior
- Default Account Configuration:
DefaultOffsetAccountId(Guid?): Suggested offset account for new linesDefaultOffsetAccountType(enum?): Type of the offset account (Ledger, Customer, Vendor, etc.)IsFixedOffsetAccount(bool): Enforces the offset account (prevents user changes)
Key Enumerations
JournalType Enum
Defines the primary purpose and posting strategy for journals using this name:
Daily(0): General-purpose entries, uses GeneralJournalPostingStrategyCustomerPayment(1): Customer payment processing, uses CustomerPaymentPostingStrategyVendorPayment(2): Vendor payment processing, uses VendorPaymentPostingStrategyPayroll(3): Payroll transaction processing, uses PayrollPostingStrategyTaxSettlement(4): Tax settlement posting, uses TaxSettlementPostingStrategy
Each JournalType is associated with a specific posting strategy implementation, enabling polymorphic posting behavior.
VoucherGenerationStrategy Enum
Controls how voucher numbers are allocated within a journal batch:
public enum VoucherGenerationStrategy
{
/// <summary>
/// Reuses the last voucher until balanced, then generates new voucher.
/// Ideal for multi-line entries where each balanced set forms one transaction.
/// Example: Debit Cash 1000, Credit Revenue 1000 (Voucher V001)
/// Debit Expense 500, Credit Cash 500 (Voucher V002)
/// </summary>
InConnectionWithBalance = 0,
/// <summary>
/// User must manually enter voucher number on each line.
/// Provides maximum control but requires more data entry effort.
/// Used when voucher numbers must follow external references.
/// </summary>
Manual = 1,
/// <summary>
/// Single voucher for the entire journal batch.
/// Ideal for opening balances or batch imports where all lines
/// represent one logical transaction.
/// Example: All opening balance entries use Voucher OB-2025-001
/// </summary>
OneVoucherNumberOnly = 2
}
Business Rules & Invariants
Core Validation Rules
-
Unique Name Requirement
- Rule:
Namemust be unique across all LedgerJournalName entities - Enforcement: Database unique constraint + domain validation
- Rationale: Users select journals by name, duplicates cause confusion
- Rule:
-
Journal Type Assignment
- Rule: Every LedgerJournalName must have a JournalType assigned
- Enforcement: Required field validation in domain model
- Rationale: JournalType determines posting strategy, cannot be null
-
Voucher Series Configuration
- Rule: Every LedgerJournalName must reference a valid NumberSequence
- Enforcement: Foreign key constraint + domain validation
- Rationale: All journals require voucher number generation capability
-
Fixed Offset Account Integrity
- Rule: If
IsFixedOffsetAccount = true, thenDefaultOffsetAccountIdmust be provided - Enforcement: Domain method
SetFixedOffsetAccount()validates both properties together - Validation Logic:
if (IsFixedOffsetAccount && !DefaultOffsetAccountId.HasValue)
{
throw new DomainException(
"Cannot set fixed offset account without specifying the account");
} - Rationale: A fixed offset without specifying which account makes no business sense
- Rule: If
-
Voucher Strategy Consistency
- Rule:
VoucherGenerationStrategymust be set to a valid enum value - Default:
InConnectionWithBalance(most common scenario) - Rationale: Controls critical behavior in journal line processing
- Rule:
Deletion Constraints
A LedgerJournalName cannot be deleted if:
-
Active Journal References: Any
LedgerJournalHeaderentities reference this LedgerJournalName- Check: Query
LedgerJournalHeadertable forLedgerJournalNameId - Impact: Could orphan historical journals and break audit trail
- Check: Query
-
System-Required Journals: Certain journal names may be marked as system-required
- Examples: "Tax Settlement Posting", "System Generated Adjustments"
- Enforcement:
IsSystemRequiredflag (if implemented)
Deletion Process:
public async Task<Result> DeleteLedgerJournalNameAsync(Guid id)
{
var journalName = await _repository.GetByIdAsync(id);
// Check for journal references
var hasJournals = await _repository.HasJournalReferencesAsync(id);
if (hasJournals)
{
return Result.Failure(
"Cannot delete journal name as it is used by existing journals");
}
_repository.Remove(journalName);
await _unitOfWork.SaveChangesAsync();
return Result.Success();
}
State Management
Lifecycle States
State Properties
The LedgerJournalName aggregate tracks the following key state:
| Property | Type | Purpose | Constraints |
|---|---|---|---|
Name | string | Display name for UI selection | Unique, required, max 100 chars |
Description | string | Detailed usage explanation | Optional, max 500 chars |
JournalTypeId | int | Links to posting strategy | Required, valid JournalType enum |
VoucherSeriesId | Guid | Number sequence for vouchers | Required, must exist in NumberSequence |
VoucherGenerationStrategy | enum | Voucher allocation behavior | Required, defaults to InConnectionWithBalance |
DefaultOffsetAccountId | Guid? | Suggested offset account | Optional unless IsFixedOffsetAccount = true |
DefaultOffsetAccountType | enum? | Type of offset account | Optional, must align with DefaultOffsetAccountId |
IsFixedOffsetAccount | bool | Locks offset account for control | Defaults to false |
Configuration Change Impact
Low-Risk Changes (can be made anytime):
- Update
Description - Change
DefaultOffsetAccountId(if not fixed) - affects only new journals
Medium-Risk Changes (require careful consideration):
- Change
VoucherGenerationStrategy- affects how new journals allocate vouchers - Toggle
IsFixedOffsetAccount- changes data entry behavior
High-Risk Changes (generally prohibited after use):
- Change
JournalType- would alter posting strategy for future journals - Change
VoucherSeriesId- could create numbering conflicts
Best Practice: Create a new LedgerJournalName instead of modifying one that's actively used.
Domain Events
Current State
The LedgerJournalName aggregate currently does not publish domain events. Its state is consumed synchronously by command handlers and services during journal creation and processing.
Future Considerations
As the system evolves, consider implementing the following events:
LedgerJournalNameCreatedDomainEvent
public class LedgerJournalNameCreatedDomainEvent : IDomainEvent
{
public Guid LedgerJournalNameId { get; }
public string Name { get; }
public int JournalTypeId { get; }
// Use Case: Cache invalidation, audit logging, UI refresh
}
LedgerJournalNameConfigurationChangedDomainEvent
public class LedgerJournalNameConfigurationChangedDomainEvent : IDomainEvent
{
public Guid LedgerJournalNameId { get; }
public Dictionary<string, object> ChangedProperties { get; }
// Use Case: Impact analysis, notification to active users, audit trail
}
LedgerJournalNameDeletedDomainEvent
public class LedgerJournalNameDeletedDomainEvent : IDomainEvent
{
public Guid LedgerJournalNameId { get; }
public string Name { get; }
// Use Case: Cleanup caches, notify administrators, audit logging
}
Rationale for Future Events:
- Configuration changes can impact active users
- Audit requirements may necessitate event-based tracking
- Integration with notification systems for process owners
- Cache invalidation in distributed scenarios
Repository Contract
ILedgerJournalNameRepository
Purpose: Provides data access operations for LedgerJournalName aggregate with specialized queries for validation and configuration retrieval.
Core Query Methods
public interface ILedgerJournalNameRepository : IRepository<LedgerJournalName>
{
/// <summary>
/// Retrieves all journal names, optionally including related entities.
/// Used in: Journal name selection dropdowns, configuration screens
/// </summary>
Task<List<LedgerJournalName>> GetAllAsync(
bool includeJournalType = false,
bool includeVoucherSeries = false);
/// <summary>
/// Gets a journal name by ID with optional eager loading.
/// Used in: Journal creation, configuration editing
/// </summary>
Task<LedgerJournalName?> GetByIdAsync(
Guid id,
bool includeJournalType = false,
bool includeVoucherSeries = false);
/// <summary>
/// Finds journal name by unique name.
/// Used in: Name uniqueness validation
/// </summary>
Task<LedgerJournalName?> GetByNameAsync(string name);
/// <summary>
/// Gets all journal names for a specific journal type.
/// Used in: Type-specific journal selection, reporting
/// </summary>
Task<List<LedgerJournalName>> GetByJournalTypeAsync(int journalTypeId);
/// <summary>
/// Checks if a journal name has any associated journals.
/// Used in: Deletion validation
/// </summary>
Task<bool> HasJournalReferencesAsync(Guid journalNameId);
/// <summary>
/// Validates name uniqueness excluding a specific ID.
/// Used in: Update operations to allow keeping the same name
/// </summary>
Task<bool> IsNameUniqueAsync(string name, Guid? excludeId = null);
}
Command Methods
/// <summary>
/// Adds a new journal name to the repository.
/// Validation: Name uniqueness, required fields
/// </summary>
void Add(LedgerJournalName journalName);
/// <summary>
/// Updates an existing journal name.
/// Validation: Name uniqueness, consistency checks
/// </summary>
void Update(LedgerJournalName journalName);
/// <summary>
/// Removes a journal name from the repository.
/// Pre-condition: No journal references exist
/// </summary>
void Remove(LedgerJournalName journalName);
Aggregate Relationships
1. Relationship with LedgerJournalHeader
Nature: One-to-Many (One LedgerJournalName → Many LedgerJournalHeaders)
Interaction Pattern:
// Journal creation inherits configuration from LedgerJournalName
var journal = new LedgerJournalHeader(
journalBatchNumber: "GJ-2025-001",
ledgerJournalNameId: journalName.Id,
journalTypeId: journalName.JournalTypeId, // Inherited
currencyId: accountingCurrency.Id,
voucherSeriesId: journalName.VoucherSeriesId // Inherited
);
Key Points:
- LedgerJournalHeader stores
LedgerJournalNameIdas foreign key - Configuration values (JournalType, VoucherSeries) are copied at creation time
- Changes to LedgerJournalName do not retroactively affect existing journals
- This immutability preserves audit trail integrity
2. Relationship with JournalType (Enumeration)
Nature: Many-to-One Reference (Many LedgerJournalNames → One JournalType value)
Purpose: Determines which posting strategy implementation is used during journal posting.
Posting Strategy Polymorphism:
// In JournalPostingService
IPostingStrategy strategy = _journalType switch
{
JournalType.Daily => new GeneralJournalPostingStrategy(),
JournalType.CustomerPayment => new CustomerPaymentPostingStrategy(),
JournalType.VendorPayment => new VendorPaymentPostingStrategy(),
JournalType.Payroll => new PayrollPostingStrategy(),
JournalType.TaxSettlement => new TaxSettlementPostingStrategy(),
_ => throw new DomainException($"Unsupported journal type: {_journalType}")
};
await strategy.PostAsync(journal, ledger);
3. Relationship with NumberSequence
Nature: Many-to-One (Many LedgerJournalNames → One NumberSequence)
Purpose: Controls voucher number generation for journals created with this name.
Integration Points:
- LedgerJournalName stores
VoucherSeriesIdforeign key - During journal line creation, the system requests the next number from NumberSequence
- VoucherGenerationStrategy determines when to request new numbers:
InConnectionWithBalance: New number when lines balanceManual: No automatic generationOneVoucherNumberOnly: Single number for entire journal
Example Usage:
// In AddLedgerJournalTransactionCommandHandler
string voucherNumber = _voucherGenerationStrategy switch
{
VoucherGenerationStrategy.InConnectionWithBalance =>
await _numberSequenceService.GetNextNumberAsync(journal.VoucherSeriesId),
VoucherGenerationStrategy.Manual =>
command.ManualVoucherNumber, // User-provided
VoucherGenerationStrategy.OneVoucherNumberOnly =>
journal.JournalBatchVoucherNumber, // Same for all lines
_ => throw new DomainException("Invalid voucher strategy")
};
4. Relationship with DimensionCombination (Indirect)
Nature: Indirect reference through DefaultOffsetAccountId
Purpose: When DefaultOffsetAccountId is set, it points to a DimensionCombination that represents the default offset account structure.
Workflow:
- User configures LedgerJournalName with a default offset account (e.g., "Bank - Main Account")
- System resolves this to a full DimensionCombination (MainAccount + Dimensions)
DefaultOffsetAccountIdstores the resulting DimensionCombination.Id- When a user creates a journal line, the system auto-populates the offset account using this combination
Example:
// Configuration time
var bankAccount = await _dimensionService.ResolveAccountAsync("1010-CORP-US");
journalName.SetDefaultOffsetAccount(bankAccount.Id, AccountType.Ledger, isFixed: true);
// Usage time (in journal line creation)
if (journalName.DefaultOffsetAccountId.HasValue)
{
line.SetOffsetAccount(
journalName.DefaultOffsetAccountId.Value,
journalName.DefaultOffsetAccountType.Value
);
if (journalName.IsFixedOffsetAccount)
{
line.LockOffsetAccount(); // User cannot change
}
}
Domain Services
Services That Consume LedgerJournalName
While LedgerJournalName itself is primarily a configuration entity, it is consumed by several domain services:
1. JournalCreationService
Purpose: Orchestrates the creation of new journal headers.
Usage of LedgerJournalName:
public async Task<LedgerJournalHeader> CreateJournalAsync(
Guid ledgerJournalNameId,
Guid currencyId)
{
var journalName = await _journalNameRepository.GetByIdAsync(
ledgerJournalNameId,
includeJournalType: true,
includeVoucherSeries: true);
var journalBatchNumber = await _numberSequenceService
.GetNextNumberAsync(ModuleFeature.LedgerJournalNumber);
var journal = new LedgerJournalHeader(
journalBatchNumber,
journalName.Id,
journalName.JournalTypeId,
currencyId,
journalName.VoucherSeriesId
);
// Apply default dimension if configured
if (journalName.DefaultDimensionId.HasValue)
{
journal.SetDefaultDimension(journalName.DefaultDimensionId.Value);
}
return journal;
}
2. VoucherGenerationService
Purpose: Generates or validates voucher numbers based on the configured strategy.
Strategy Implementation:
public async Task<string> GenerateVoucherNumberAsync(
LedgerJournalHeader journal,
LedgerJournalName journalName,
bool isBalanced)
{
return journalName.VoucherGenerationStrategy switch
{
VoucherGenerationStrategy.InConnectionWithBalance =>
isBalanced
? await _numberSequence.GetNextNumberAsync(journal.VoucherSeriesId)
: journal.LastVoucherNumber, // Reuse until balanced
VoucherGenerationStrategy.OneVoucherNumberOnly =>
journal.JournalBatchVoucherNumber ??=
await _numberSequence.GetNextNumberAsync(journal.VoucherSeriesId),
VoucherGenerationStrategy.Manual =>
throw new DomainException(
"Manual voucher strategy requires user to provide voucher number"),
_ => throw new DomainException("Invalid voucher generation strategy")
};
}
3. JournalPostingService
Purpose: Posts journals to the general ledger.
Uses JournalType from LedgerJournalName:
public async Task PostJournalAsync(LedgerJournalHeader journal)
{
var journalName = await _journalNameRepository.GetByIdAsync(
journal.LedgerJournalNameId,
includeJournalType: true);
// Select posting strategy based on journal type
var strategy = _strategyFactory.CreateStrategy(journalName.JournalType);
await strategy.ExecutePostingAsync(journal, _ledger);
}
Usage Examples
Example 1: Creating a Daily Journal Configuration
// Application Layer - CreateLedgerJournalNameCommand
public async Task<Result<Guid>> Handle(
CreateLedgerJournalNameCommand command,
CancellationToken cancellationToken)
{
// 1. Validate name uniqueness
var exists = await _repository.IsNameUniqueAsync(command.Name);
if (exists)
{
return Result<Guid>.Failure("Journal name already exists");
}
// 2. Create aggregate
var journalName = new LedgerJournalName(
name: command.Name,
description: command.Description,
journalTypeId: JournalType.Daily.Id,
voucherSeriesId: command.VoucherSeriesId
);
// 3. Configure voucher strategy
journalName.SetVoucherGenerationStrategy(
VoucherGenerationStrategy.InConnectionWithBalance);
// 4. No default offset account (flexible entry)
// 5. Persist
_repository.Add(journalName);
await _unitOfWork.SaveChangesAsync(cancellationToken);
return Result<Guid>.Success(journalName.Id);
}
Example 2: Creating a Bank Payment Journal Configuration
public async Task<Result<Guid>> Handle(
CreateBankPaymentJournalCommand command,
CancellationToken cancellationToken)
{
// 1. Resolve the bank account to a DimensionCombination
var bankAccount = await _dimensionService.ResolveAccountAsync("1010-CORP-US");
// 2. Create journal name
var journalName = new LedgerJournalName(
name: "Bank Payments - Main Account",
description: "Customer and vendor payments from main bank account",
journalTypeId: JournalType.VendorPayment.Id,
voucherSeriesId: command.VoucherSeriesId
);
// 3. Set ONE VOUCHER per payment (common in payment processing)
journalName.SetVoucherGenerationStrategy(
VoucherGenerationStrategy.OneVoucherNumberOnly);
// 4. Set FIXED offset account (always the bank)
journalName.SetFixedOffsetAccount(
accountId: bankAccount.Id,
accountType: AccountType.Ledger,
isFixed: true // User cannot change
);
// 5. Persist
_repository.Add(journalName);
await _unitOfWork.SaveChangesAsync(cancellationToken);
return Result<Guid>.Success(journalName.Id);
}
Example 3: Using Journal Name During Transaction Entry
// In AddLedgerJournalTransactionCommandHandler
public async Task<Result<Guid>> Handle(
AddLedgerJournalTransactionCommand command,
CancellationToken cancellationToken)
{
// 1. Get journal with its configuration
var journal = await _journalRepository.GetByIdAsync(
command.JournalHeaderId,
includeJournalName: true);
var journalName = journal.JournalName;
// 2. Generate or validate voucher number
string voucherNumber = await _voucherService.GenerateVoucherNumberAsync(
journal,
journalName,
isBalanced: journal.IsBalanced);
// 3. Create journal line
var line = new LedgerJournalLine(
lineNumber: journal.NextLineNumber,
transactionDate: command.TransactionDate,
accountId: command.AccountId,
accountType: command.AccountType,
debitAmount: command.DebitAmount,
creditAmount: command.CreditAmount,
description: command.Description,
voucherNumber: voucherNumber
);
// 4. Apply default offset account if configured
if (journalName.DefaultOffsetAccountId.HasValue)
{
line.SetOffsetAccount(
journalName.DefaultOffsetAccountId.Value,
journalName.DefaultOffsetAccountType.Value
);
// 5. Lock offset account if fixed
if (journalName.IsFixedOffsetAccount)
{
line.LockOffsetAccount();
}
}
// 6. Add line to journal
journal.AddLine(line);
// 7. Persist
await _unitOfWork.SaveChangesAsync(cancellationToken);
return Result<Guid>.Success(line.Id);
}
Validation Rules
Creation Validation
public class CreateLedgerJournalNameValidator : AbstractValidator<CreateLedgerJournalNameCommand>
{
public CreateLedgerJournalNameValidator(ILedgerJournalNameRepository repository)
{
RuleFor(x => x.Name)
.NotEmpty().WithMessage("Journal name is required")
.MaximumLength(100).WithMessage("Name cannot exceed 100 characters")
.MustAsync(async (name, ct) =>
await repository.IsNameUniqueAsync(name))
.WithMessage("Journal name already exists");
RuleFor(x => x.Description)
.MaximumLength(500).WithMessage("Description cannot exceed 500 characters");
RuleFor(x => x.JournalTypeId)
.Must(BeValidJournalType).WithMessage("Invalid journal type");
RuleFor(x => x.VoucherSeriesId)
.NotEmpty().WithMessage("Voucher series is required");
RuleFor(x => x)
.Must(ValidateFixedOffsetConfiguration)
.WithMessage("Fixed offset account requires specifying the account ID")
.When(x => x.IsFixedOffsetAccount);
}
private bool BeValidJournalType(int journalTypeId)
{
return Enumeration.TryFromValue<JournalType>(journalTypeId, out _);
}
private bool ValidateFixedOffsetConfiguration(CreateLedgerJournalNameCommand command)
{
if (command.IsFixedOffsetAccount && !command.DefaultOffsetAccountId.HasValue)
{
return false; // Fixed offset requires account ID
}
return true;
}
}
Update Validation
public class UpdateLedgerJournalNameValidator : AbstractValidator<UpdateLedgerJournalNameCommand>
{
public UpdateLedgerJournalNameValidator(ILedgerJournalNameRepository repository)
{
RuleFor(x => x.Name)
.NotEmpty()
.MaximumLength(100)
.MustAsync(async (cmd, name, ct) =>
await repository.IsNameUniqueAsync(name, cmd.Id))
.WithMessage("Journal name already exists");
// Additional rules similar to creation validation
// Prevent changing critical properties if journals exist
RuleFor(x => x)
.MustAsync(async (cmd, ct) =>
!await repository.HasJournalReferencesAsync(cmd.Id) ||
!cmd.IsJournalTypeChanged)
.WithMessage("Cannot change journal type when journals exist");
}
}
Performance Considerations
1. Caching Strategy
Since LedgerJournalName is frequently accessed but rarely modified, implement caching:
public class CachedLedgerJournalNameRepository : ILedgerJournalNameRepository
{
private readonly ILedgerJournalNameRepository _inner;
private readonly IMemoryCache _cache;
private const string CacheKeyPrefix = "JournalName:";
private const int CacheMinutes = 60;
public async Task<LedgerJournalName?> GetByIdAsync(Guid id, bool includeRelated = false)
{
var cacheKey = $"{CacheKeyPrefix}{id}:{includeRelated}";
return await _cache.GetOrCreateAsync(cacheKey, async entry =>
{
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(CacheMinutes);
return await _inner.GetByIdAsync(id, includeRelated);
});
}
public async Task<List<LedgerJournalName>> GetAllAsync(bool includeRelated = false)
{
var cacheKey = $"{CacheKeyPrefix}All:{includeRelated}";
return await _cache.GetOrCreateAsync(cacheKey, async entry =>
{
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(CacheMinutes);
return await _inner.GetAllAsync(includeRelated);
});
}
// Invalidate cache on modifications
public void Update(LedgerJournalName journalName)
{
_cache.Remove($"{CacheKeyPrefix}{journalName.Id}:true");
_cache.Remove($"{CacheKeyPrefix}{journalName.Id}:false");
_cache.Remove($"{CacheKeyPrefix}All:true");
_cache.Remove($"{CacheKeyPrefix}All:false");
_inner.Update(journalName);
}
}
2. Query Optimization
Use projection when only basic properties are needed:
// Heavy: Loads entire aggregate with relationships
var journalNames = await _repository.GetAllAsync(includeJournalType: true, includeVoucherSeries: true);
// Lightweight: Projection for dropdown lists
var journalNameList = await _context.LedgerJournalNames
.Select(jn => new LedgerJournalNameListItem
{
Id = jn.Id,
Name = jn.Name,
JournalTypeName = jn.JournalType.Name
})
.ToListAsync();
Common Configuration Patterns
Pattern 1: Flexible General Journal
Use Case: Daily adjusting entries, miscellaneous transactions
Name: "Daily General Journal"
JournalType: Daily
VoucherStrategy: InConnectionWithBalance
DefaultOffsetAccount: None
IsFixedOffsetAccount: false
Behavior: User enters balanced sets of entries, each balanced set gets its own voucher.
Pattern 2: Bank Payment Journal
Use Case: Paying vendors from a specific bank account
Name: "Bank Payments - Chase Checking"
JournalType: VendorPayment
VoucherStrategy: OneVoucherNumberOnly
DefaultOffsetAccount: DimensionCombination for "Cash-Chase-Checking"
IsFixedOffsetAccount: true
Behavior: Every line in the journal offsets the same bank account. One voucher for the entire payment batch.
Pattern 3: Opening Balance Journal
Use Case: Migrating opening balances from legacy system
Name: "Opening Balances 2025"
JournalType: Daily
VoucherStrategy: OneVoucherNumberOnly
DefaultOffsetAccount: None
IsFixedOffsetAccount: false
Behavior: All opening balance entries share a single voucher number (e.g., "OB-2025-001").
Pattern 4: Payroll Journal
Use Case: Monthly payroll posting from HR system
Name: "Payroll Monthly"
JournalType: Payroll
VoucherStrategy: OneVoucherNumberOnly
DefaultOffsetAccount: DimensionCombination for "Payroll Clearing Account"
IsFixedOffsetAccount: false // May need different clearing accounts
Behavior: Entire payroll run uses one voucher. Specific clearing account suggested but can be overridden.
Testing Considerations
Unit Tests (Domain Layer)
[Fact]
public void SetFixedOffsetAccount_WhenAccountIdIsNull_ShouldThrowException()
{
// Arrange
var journalName = LedgerJournalNameFactory.CreateValid();
// Act & Assert
var exception = Assert.Throws<DomainException>(() =>
journalName.SetFixedOffsetAccount(null, AccountType.Ledger, isFixed: true));
Assert.Contains("Cannot set fixed offset account without specifying the account",
exception.Message);
}
[Fact]
public void Create_WithValidProperties_ShouldSucceed()
{
// Arrange & Act
var journalName = new LedgerJournalName(
name: "Test Journal",
description: "Test Description",
journalTypeId: JournalType.Daily.Id,
voucherSeriesId: Guid.NewGuid()
);
// Assert
Assert.NotNull(journalName);
Assert.Equal("Test Journal", journalName.Name);
Assert.Equal(JournalType.Daily.Id, journalName.JournalTypeId);
}
Integration Tests (Application Layer)
[Fact]
public async Task CreateLedgerJournalName_WithDuplicateName_ShouldFail()
{
// Arrange
await SeedJournalName("Existing Journal");
var command = new CreateLedgerJournalNameCommand
{
Name = "Existing Journal",
Description = "Duplicate",
JournalTypeId = JournalType.Daily.Id,
VoucherSeriesId = Guid.NewGuid()
};
// Act
var result = await _handler.Handle(command, CancellationToken.None);
// Assert
Assert.True(result.IsFailure);
Assert.Contains("Journal name already exists", result.Error);
}
Related Documentation
Domain Documentation
- Ledger Aggregate - Ledger configuration and fiscal setup
- LedgerJournalHeader Aggregate - Journal lifecycle management
- DimensionCombination Aggregate - Account combinations for offset accounts
- NumberSequence Aggregate - Voucher number generation
API Documentation
- LedgerJournalName API - REST endpoints for journal name management
- General Journals API - Journal creation using journal names
Conceptual Documentation
- Journal Posting Lifecycle - Complete posting workflow
- Voucher Numbering Strategies - Detailed voucher generation patterns
Cross-Module References
- Posting Strategies Pattern - Strategy pattern for polymorphic posting
Last Updated: 2025-01-15 | Version: 1.0 | Status: Active Development