Journal Configuration and Setup
File: /docs/concepts/general-ledger/journal-configuration-setup.concept.md
Module: GeneralLedger
Audience: Architects, Business Analysts, Senior Developers, System Configurators
Overview
This document explains the complete journal configuration and setup workflow in the General Ledger module, from initial configuration through transaction entry and posting. Understanding this workflow is essential for system configurators, developers implementing journal features, and business analysts designing financial processes.
Business Goal
Enable organizations to configure multiple journal templates (LedgerJournalName) that enforce different business rules, automate data entry, and ensure financial control based on the type of transaction being recorded. Each journal template acts as a policy object that determines:
- How transactions are validated
- How voucher numbers are generated
- What default accounts are applied
- Which posting strategy is used
- What data entry shortcuts are available
Key Concepts
| Concept | Definition | Example |
|---|---|---|
| LedgerJournalName | A configuration template that defines journal behavior | "Daily General Journal", "Bank Payments" |
| JournalType | Classification determining posting strategy | Daily, CustomerPayment, VendorPayment, Payroll |
| VoucherGenerationStrategy | Rules for how voucher numbers are allocated | InConnectionWithBalance, Manual, OneVoucherNumberOnly |
| NumberSequence | Automated number generation service | Generates "V-2025-0001", "V-2025-0002", etc. |
| Posting Strategy | Implementation of posting logic for a journal type | GeneralJournalPostingStrategy, PaymentPostingStrategy |
| Default Offset Account | Pre-configured counter-account for quick entry | Bank account for payment journals |
High-Level Architecture
Complete Workflow Diagram
This sequence diagram shows the entire journey from journal name configuration to transaction posting:
Detailed Process Steps
Phase 1: Initial Configuration (Administrator Task)
This is a one-time setup performed by system administrators or accounting managers when configuring the system.
Step 1.1: Create LedgerJournalName
Goal: Define a new journal template for a specific business process.
Inputs Required:
- Name: Unique identifier (e.g., "Bank Payments - Chase Checking")
- Description: Explanation of purpose (e.g., "Vendor payments from main bank account")
- JournalType: Classification determining posting behavior
- VoucherSeries: Reference to a NumberSequence for voucher generation
Example Configuration:
{
"name": "Bank Payments - Chase Checking",
"description": "Vendor payments from Chase checking account",
"journalType": "VendorPayment",
"voucherSeriesCode": "VOUCHER-PAYMENT",
"voucherGenerationStrategy": "OneVoucherNumberOnly",
"defaultOffsetAccountCode": "1010-CORP-US",
"isFixedOffsetAccount": true
}
Business Rationale:
- Separating journal configurations allows different departments to use different templates
- Each template can enforce specific controls (e.g., treasury department always uses fixed bank accounts)
- Configuration changes don't affect historical data (immutability principle)
Step 1.2: Select JournalType
Critical Decision Point: The JournalType determines which posting strategy will be used.
| JournalType | Posting Strategy | Use Case | Key Behavior |
|---|---|---|---|
| Daily | GeneralJournalPostingStrategy | General adjustments, accruals | Direct debit/credit posting, flexible structure |
| CustomerPayment | CustomerPaymentPostingStrategy | AR payments, deposits | Applies payment to customer invoices, updates AR subledger |
| VendorPayment | VendorPaymentPostingStrategy | AP payments, checks | Applies payment to vendor invoices, updates AP subledger |
| Payroll | PayrollPostingStrategy | Payroll processing | Creates employee liability and expense entries |
| TaxSettlement | TaxSettlementPostingStrategy | Sales tax payments | Creates tax liability entries for remittance to authorities |
Architecture Pattern: Strategy Pattern
- Each JournalType maps to a different IPostingStrategy implementation
- This enables polymorphic behavior without modifying the core journal posting service
- New journal types can be added by implementing new strategies
Code Reference:
// In PostingStrategyFactory
IPostingStrategy strategy = journalType switch
{
JournalType.Daily => new GeneralJournalPostingStrategy(),
JournalType.VendorPayment => new VendorPaymentPostingStrategy(),
_ => throw new DomainException($"Unsupported journal type: {journalType}")
};
Step 1.3: Configure VoucherGenerationStrategy
Purpose: Define how voucher numbers are allocated to journal lines.
Strategy Comparison:
| Strategy | When to Use | Example Scenario | Voucher Pattern |
|---|---|---|---|
| InConnectionWithBalance | Multi-line balanced entries | Daily general journal with multiple transactions | V-001 (balanced set), V-002 (next balanced set) |
| OneVoucherNumberOnly | Batch operations as single transaction | Opening balances, payroll batches, payment batches | OB-2025-001 (all lines use same voucher) |
| Manual | External reference tracking | Migrating data with existing voucher numbers | User enters: "MIGRATE-00123" |
Real-World Example - InConnectionWithBalance:
Journal: Daily General Journal
Lines:
Line 1: Debit Cash 1000 (Unbalanced) → Voucher: V-2025-0001
Line 2: Credit Revenue 1000 (Balanced) → Voucher: V-2025-0001
Line 3: Debit Expense 500 (Unbalanced) → Voucher: V-2025-0002
Line 4: Credit Cash 500 (Balanced) → Voucher: V-2025-0002
Real-World Example - OneVoucherNumberOnly:
Journal: Opening Balances 2025
Lines:
Line 1: Debit Asset Account 1 → Voucher: OB-2025-0001
Line 2: Debit Asset Account 2 → Voucher: OB-2025-0001
Line 3: Credit Equity Account → Voucher: OB-2025-0001
All 50 lines use the same voucher number
Step 1.4: Configure Default Offset Account (Optional)
Purpose: Automate data entry for journals with predictable counter-entries.
Configuration Options:
-
No Default Offset Account (Flexible Entry)
- User selects both debit and credit accounts manually
- Use case: Daily general journal with varied transactions
-
Suggested Offset Account (IsFixedOffsetAccount = false)
- System pre-fills offset account, user can change it
- Use case: Payment journal where 90% of payments use main bank, but occasional alternative accounts
-
Fixed Offset Account (IsFixedOffsetAccount = true)
- System pre-fills offset account, user CANNOT change it
- Use case: Petty cash journal where all transactions MUST offset the petty cash account
- Enforces financial control and prevents errors
Business Example - Fixed Offset Account:
Configuration:
Journal Name: "Petty Cash Disbursements"
Default Offset Account: "Cash - Petty Cash" (1050-CORP-US)
Is Fixed: true
User Experience:
User enters: Debit Office Supplies $50
System automatically creates: Credit Petty Cash $50 (locked, cannot change)
If user tries to change offset account → System rejects with error:
"This journal uses a fixed offset account and cannot be changed."
Technical Implementation:
// In AddLedgerJournalTransactionCommandHandler
if (journalName.DefaultOffsetAccountId.HasValue)
{
line.SetOffsetAccount(
journalName.DefaultOffsetAccountId.Value,
journalName.DefaultOffsetAccountType.Value
);
if (journalName.IsFixedOffsetAccount)
{
line.LockOffsetAccount(); // Prevents user modification
}
}
Phase 2: Journal Creation (Daily Operations)
Once journal names are configured, users create journal batches for transaction entry.
Step 2.1: User Selects Journal Name
User Interface Flow:
- User navigates to "Create New Journal"
- System displays dropdown of available LedgerJournalNames
- User selects appropriate template (e.g., "Bank Payments - Chase Checking")
- System retrieves full configuration from repository
Behind the Scenes:
// In CreateJournalCommandHandler
var journalName = await _journalNameRepository.GetByIdAsync(
command.JournalNameId,
includeJournalType: true, // Load posting strategy info
includeVoucherSeries: true // Load number sequence info
);
// Validate journal name is active and available
if (journalName == null || journalName.IsInactive)
{
throw new ValidationException("Selected journal name is not available");
}
Step 2.2: System Generates Journal Batch Number
NumberSequence Integration:
- Each journal gets a unique batch number for tracking
- Batch number comes from a separate NumberSequence (different from voucher sequence)
- Format examples: "GJ-2025-001", "PAY-2025-045", "PR-2025-012"
Code Flow:
// Get journal batch number
var journalBatchNumber = await _numberSequenceService.GetNextNumberAsync(
ModuleFeature.LedgerJournalNumber
);
// Create journal header
var journal = new LedgerJournalHeader(
journalBatchNumber: journalBatchNumber, // "GJ-2025-001"
ledgerJournalNameId: journalName.Id,
journalTypeId: journalName.JournalTypeId, // Inherited from template
currencyId: ledger.AccountingCurrencyId,
voucherSeriesId: journalName.VoucherSeriesId // Inherited from template
);
Step 2.3: Journal Header Created
State at This Point:
- Journal exists but has no lines yet
- Status: Open (can add/modify lines)
- Inherited configuration:
- JournalType (determines posting strategy later)
- VoucherSeries (controls line-level voucher generation)
- VoucherGenerationStrategy (controls allocation behavior)
Data Model:
{
"id": "3a7f5c8d-...",
"journalBatchNumber": "GJ-2025-001",
"ledgerJournalNameId": "a1b2c3d4-...",
"journalType": "VendorPayment",
"voucherSeriesId": "7f8d9ea1-...",
"status": "Open",
"isPosted": false,
"lines": []
}
Phase 3: Transaction Entry (Line-by-Line Processing)
This is where users enter the actual financial transactions.
Step 3.1: User Enters Transaction Details
Required Information:
- Transaction Date
- Account (MainAccount + Dimensions)
- Amount (Debit or Credit, not both)
- Description
- Offset Account (if not auto-filled)
User Experience with Default Offset Account:
Scenario: Payment Journal with Fixed Offset
User sees:
┌─────────────────────────────────────────────┐
│ Date: [01/15/2025] │
│ Account: [Select Vendor Account] │
│ Debit: [5000.00] │
│ Description: [Invoice Payment INV-2024-045] │
│ │
│ Offset Account: [1010-CORP-US] (locked) │
│ Credit: [5000.00] (auto-calculated) │
└─────────────────────────────────────────────┘
Notice: Offset account is pre-filled and locked, credit auto-calculated
Benefit: User only enters 4 fields instead of 7, fewer errors
Step 3.2: System Generates or Assigns Voucher Number
This is where VoucherGenerationStrategy comes into play.
Scenario A: InConnectionWithBalance
Initial State: Journal is empty, no voucher assigned yet
Action: User adds Line 1 (Debit Cash 1000)
Result:
- Journal becomes unbalanced (DR 1000, CR 0)
- System generates NEW voucher: V-2025-0001
- Line 1 gets voucher V-2025-0001
Action: User adds Line 2 (Credit Revenue 1000)
Result:
- Journal becomes balanced (DR 1000, CR 1000)
- Line 2 REUSES existing voucher: V-2025-0001
- Both lines now share voucher V-2025-0001
Action: User adds Line 3 (Debit Expense 500)
Result:
- Journal becomes unbalanced again (DR 1500, CR 1000)
- System generates NEW voucher: V-2025-0002
- Line 3 gets voucher V-2025-0002
Action: User adds Line 4 (Credit Cash 500)
Result:
- Journal becomes balanced (DR 1500, CR 1500)
- Line 4 REUSES existing voucher: V-2025-0002
- Lines 3 and 4 share voucher V-2025-0002
Final State:
Line 1: V-2025-0001 | Debit Cash 1000
Line 2: V-2025-0001 | Credit Revenue 1000
Line 3: V-2025-0002 | Debit Expense 500
Line 4: V-2025-0002 | Credit Cash 500
Scenario B: OneVoucherNumberOnly
Initial State: Journal created, batch voucher not yet generated
Action: User adds Line 1 (Debit Asset 1000)
Result:
- System generates ONE batch voucher: OB-2025-0001
- Line 1 gets voucher OB-2025-0001
- Batch voucher stored in journal header
Action: User adds Line 2 (Debit Asset 2000)
Result:
- Line 2 REUSES batch voucher: OB-2025-0001
Action: User adds Line 3 (Credit Equity 3000)
Result:
- Line 3 REUSES batch voucher: OB-2025-0001
Final State: ALL lines share the same voucher
Line 1: OB-2025-0001 | Debit Asset 1 1000
Line 2: OB-2025-0001 | Debit Asset 2 2000
Line 3: OB-2025-0001 | Credit Equity 3000
Implementation Code:
public async Task<string> DetermineVoucherNumberAsync(
LedgerJournalHeader journal,
VoucherGenerationStrategy strategy)
{
return strategy switch
{
VoucherGenerationStrategy.InConnectionWithBalance =>
await HandleBalanceBasedVoucherAsync(journal),
VoucherGenerationStrategy.OneVoucherNumberOnly =>
await HandleSingleVoucherAsync(journal),
VoucherGenerationStrategy.Manual =>
throw new DomainException("Manual voucher requires user input"),
_ => throw new ArgumentException("Invalid strategy")
};
}
private async Task<string> HandleBalanceBasedVoucherAsync(LedgerJournalHeader journal)
{
if (journal.IsBalanced)
{
// Generate new voucher when balanced
return await _numberSequenceService.GetNextNumberAsync(journal.VoucherSeriesId);
}
else
{
// Reuse last voucher when unbalanced
return journal.LastVoucherNumber
?? await _numberSequenceService.GetNextNumberAsync(journal.VoucherSeriesId);
}
}
private async Task<string> HandleSingleVoucherAsync(LedgerJournalHeader journal)
{
// Generate once, reuse forever
if (journal.JournalBatchVoucherNumber == null)
{
journal.JournalBatchVoucherNumber =
await _numberSequenceService.GetNextNumberAsync(journal.VoucherSeriesId);
}
return journal.JournalBatchVoucherNumber;
}
Step 3.3: Dimension Resolution and Validation
Process:
- User enters MainAccount value (e.g., "4010")
- System resolves appropriate AccountStructure using ConstraintTree
- System validates all required dimensions are provided
- System creates or retrieves DimensionCombination
- Line is linked to immutable DimensionCombination
This process uses the following domain services:
AccountStructureResolutionService- Determines correct structureConstraintOverlapDetectionService- Ensures no ambiguityDimensionValueResolverService- Resolves string values to IDsDimensionCombinationDomainService- Creates/retrieves combinations
Detailed Flow:
Phase 4: Journal Posting (Final Step)
Once all lines are entered and the journal is balanced, it can be posted.
Step 4.1: Pre-Posting Validation
Validation Checks:
- Journal must be balanced (Total Debits = Total Credits)
- All lines must have valid dimension combinations
- Transaction dates must be within open fiscal periods
- No suspended dimension values can be used
- All offset accounts must be properly configured
Code Example:
public async Task<Result> ValidateForPostingAsync(LedgerJournalHeader journal)
{
// Check 1: Balance validation
if (!journal.IsBalanced)
{
return Result.Failure(
$"Journal {journal.JournalBatchNumber} is not balanced. " +
$"Debit: {journal.TotalDebitAmount}, Credit: {journal.TotalCreditAmount}"
);
}
// Check 2: Fiscal period validation
var ledger = await _ledgerRepository.GetCurrentSetupAsync();
foreach (var line in journal.Lines)
{
var period = ledger.GetFiscalPeriodForDate(line.TransactionDate);
if (period?.Status != FiscalPeriodStatus.Open)
{
return Result.Failure(
$"Transaction date {line.TransactionDate:yyyy-MM-dd} is in a closed fiscal period"
);
}
}
// Check 3: Dimension combination validation
foreach (var line in journal.Lines)
{
if (line.LedgerDimensionId == null)
{
return Result.Failure($"Line {line.LineNumber} has no dimension combination");
}
}
return Result.Success();
}
Step 4.2: Posting Strategy Selection
This is where JournalType configuration becomes critical.
Strategy Pattern Implementation:
Each posting strategy implements a common interface but provides different business logic:
public interface IPostingStrategy
{
Task<Result> PostAsync(LedgerJournalHeader journal, Ledger ledger);
}
// Daily Journal: Simple debit/credit posting
public class GeneralJournalPostingStrategy : IPostingStrategy
{
public async Task<Result> PostAsync(LedgerJournalHeader journal, Ledger ledger)
{
foreach (var line in journal.Lines)
{
// Create simple GL entries, no subledger integration
await _glEntryRepository.AddAsync(new GeneralLedgerEntry
{
DimensionCombinationId = line.LedgerDimensionId.Value,
DebitAmount = line.DebitAmountInAccountingCurrency,
CreditAmount = line.CreditAmountInAccountingCurrency,
TransactionDate = line.TransactionDate,
VoucherNumber = line.Voucher
});
}
journal.MarkAsPosted(DateTime.UtcNow);
return Result.Success();
}
}
// Vendor Payment: GL entries + AP subledger update
public class VendorPaymentPostingStrategy : IPostingStrategy
{
public async Task<Result> PostAsync(LedgerJournalHeader journal, Ledger ledger)
{
foreach (var line in journal.Lines)
{
// Create GL entries
await CreateGLEntriesAsync(line);
// Update AP subledger - apply payment to vendor invoices
await _vendorPaymentService.ApplyPaymentAsync(
vendorId: line.AccountId,
paymentAmount: line.DebitAmountInAccountingCurrency,
paymentDate: line.TransactionDate
);
}
journal.MarkAsPosted(DateTime.UtcNow);
return Result.Success();
}
}
Key Difference:
- Daily Journal: Only creates GL entries
- Payment Journals: Creates GL entries AND updates AR/AP subledgers
- Payroll Journal: Creates GL entries AND may integrate with HR for liability tracking
- Tax Settlement: Creates GL entries AND updates tax period settlement status
Step 4.3: GL Entry Creation
For Each Journal Line:
-
Create GeneralLedgerEntry with:
- DimensionCombinationId (account + dimensions)
- Amount (debit or credit)
- Transaction Date
- Voucher Number
- Source Reference (journal batch number)
-
Create mirroring offset entry if configured
-
Update running balances in dimension combination summary tables
Example GL Entries Created:
Journal: PAY-2025-001 (Vendor Payment)
Line 1: Vendor ABC, Debit 5000, Voucher V-2025-001
Generated GL Entries:
Entry 1:
DimensionCombination: 2010-CORP-US (Accounts Payable)
Debit: 5000
Credit: 0
Voucher: V-2025-001
Source: PAY-2025-001
Entry 2:
DimensionCombination: 1010-CORP-US (Bank - Chase Checking)
Debit: 0
Credit: 5000
Voucher: V-2025-001
Source: PAY-2025-001
Step 4.4: Mark Journal as Posted
Final State Changes:
journal.MarkAsPosted(DateTime.UtcNow);
// Journal properties after posting:
// - Status: Posted
// - PostedDate: 2025-01-15T10:30:00Z
// - IsPosted: true
// - Can no longer modify lines
// - Can create reversal journal if needed
Integration Points
1. NumberSequence Integration
Two Separate Number Sequences:
| Sequence Purpose | Scope | Example Format | Used When |
|---|---|---|---|
| Journal Batch Numbers | One per journal header | GJ-2025-001 | Creating journal |
| Voucher Numbers | One per transaction or balanced set | V-2025-0001 | Adding lines (strategy-dependent) |
Configuration Example:
{
"numberSequences": [
{
"feature": "LedgerJournalNumber",
"format": "GJ-{YEAR}-{SEQ:5}",
"description": "Journal batch numbers"
},
{
"feature": "VoucherNumber",
"format": "V-{YEAR}-{SEQ:5}",
"description": "Voucher numbers for daily journals"
},
{
"feature": "PaymentVoucherNumber",
"format": "PAY-{YEAR}-{SEQ:5}",
"description": "Voucher numbers for payment journals"
}
]
}
2. Dimension Resolution Integration
Cross-Aggregate Workflow:
LedgerJournalName (Configuration)
↓
LedgerJournalHeader (Journal Instance)
↓
LedgerJournalLine (Transaction Entry)
↓ (MainAccount + Dimension Values)
Ledger → AccountStructureResolutionService
↓ (Resolves to AccountStructure)
DimensionValueResolverService
↓ (String values → DimensionValue IDs)
DimensionCombinationDomainService
↓ (Creates or retrieves combination)
DimensionCombination (Immutable Account Reference)
Key Integration Points:
- Journal line stores
LedgerDimensionId(FK to DimensionCombination) - DimensionCombination is immutable, created once, reused many times
- Default dimensions from entities (Customer, Vendor) can be inherited
3. Fiscal Calendar Integration
Validation During Posting:
// Check fiscal period status
var fiscalPeriod = ledger.GetFiscalPeriodForDate(line.TransactionDate);
if (fiscalPeriod == null)
{
throw new ValidationException(
$"No fiscal period defined for date {line.TransactionDate:yyyy-MM-dd}"
);
}
if (fiscalPeriod.Status != FiscalPeriodStatus.Open)
{
throw new ValidationException(
$"Cannot post to closed fiscal period: {fiscalPeriod.Name}"
);
}
Key Design Decisions
Decision 1: Configuration Immutability After Journal Creation
Rationale: When a journal is created, it inherits configuration values (JournalType, VoucherSeries) from its LedgerJournalName. These values are COPIED at creation time, not referenced dynamically.
Impact:
- Changes to LedgerJournalName do NOT affect existing journals
- Preserves audit trail integrity
- Historical journals remain consistent with their original configuration
Alternative Considered: Dynamic lookup of configuration on each operation. Rejected because it would break historical data consistency and complicate auditing.
Decision 2: VoucherGenerationStrategy as Enumeration
Rationale: Voucher generation has a finite set of well-defined behaviors. Using an enumeration (rather than a complex strategy object) keeps configuration simple while enabling the three core patterns organizations need.
Benefits:
- Easy to configure via UI
- Clear semantics for users
- Extensible (can add new strategies without schema changes)
Trade-off: Less flexible than a full strategy pattern, but the three strategies cover 99% of real-world needs.
Decision 3: Polymorphic Posting via JournalType
Rationale: Different journal types require fundamentally different posting logic (simple GL entries vs. subledger integration). Using the Strategy pattern allows each type to encapsulate its own logic without coupling.
Benefits:
- Open/Closed Principle: Can add new journal types without modifying existing code
- Single Responsibility: Each strategy handles one posting scenario
- Testability: Each strategy can be tested in isolation
Implementation Pattern:
// Factory creates appropriate strategy based on journal type
var strategy = _strategyFactory.CreateStrategy(journal.JournalType);
await strategy.PostAsync(journal, ledger);
// No if/else chains or switch statements in posting service
// Strategy encapsulates all type-specific logic
Decision 4: Fixed vs. Suggested Offset Accounts
Rationale:
Some business processes require strict control (petty cash, bank payments), while others need flexibility (general journals). The IsFixedOffsetAccount flag provides both capabilities in one design.
Business Value:
- Fixed: Prevents user errors, enforces treasury controls, ensures compliance
- Suggested: Speeds data entry without sacrificing flexibility
Technical Implementation: The domain model enforces this at the aggregate level:
if (line.IsOffsetAccountLocked && userAttemptedToChangeIt)
{
throw new DomainException("Cannot modify fixed offset account");
}
Common Configuration Patterns
Pattern 1: Flexible Daily Journal
Configuration:
Name: "Daily General Journal"
JournalType: Daily
VoucherStrategy: InConnectionWithBalance
DefaultOffsetAccount: None
IsFixedOffsetAccount: false
Use Cases:
- Month-end accruals
- Reclassification entries
- Ad-hoc adjustments
User Experience:
- Full flexibility in account selection
- Each balanced set gets its own voucher
- No automated offset accounts
Pattern 2: Controlled Payment Journal
Configuration:
Name: "Bank Payments - Chase Checking"
JournalType: VendorPayment
VoucherStrategy: OneVoucherNumberOnly
DefaultOffsetAccount: "1010-CORP-US" (Bank - Chase)
IsFixedOffsetAccount: true
Use Cases:
- Paying multiple vendors in one batch
- Check run processing
- ACH payment batches
User Experience:
- User only enters vendor and amount
- System automatically debits AP, credits bank
- All payments in batch share one voucher
- Cannot change bank account (enforced control)
Business Benefit: Reduces data entry by 60%, eliminates offset account selection errors.
Pattern 3: Opening Balance Import
Configuration:
Name: "Opening Balances 2025"
JournalType: Daily
VoucherStrategy: OneVoucherNumberOnly
DefaultOffsetAccount: "3000-CORP-US" (Retained Earnings)
IsFixedOffsetAccount: false
Use Cases:
- System go-live data migration
- Annual opening balance entries
- Closing/opening year transitions
User Experience:
- Import 500 account balances from spreadsheet
- All entries use one voucher: "OB-2025-0001"
- Retained earnings suggested but can be changed
- Journal represents single moment-in-time snapshot
Pattern 4: Payroll Processing
Configuration:
Name: "Payroll Monthly"
JournalType: Payroll
VoucherStrategy: OneVoucherNumberOnly
DefaultOffsetAccount: "2100-CORP-US" (Payroll Clearing)
IsFixedOffsetAccount: false
Use Cases:
- Monthly payroll posting from HR system
- Payroll tax liability entries
- Benefits deduction processing
User Experience:
- System generates journal from HR data
- All salary expenses, taxes, deductions in one journal
- Single voucher for entire payroll run
- Clearing account suggested but can override for special cases
Troubleshooting Guide
Issue 1: "Journal name already exists"
Cause: Attempting to create LedgerJournalName with duplicate name.
Solution:
// Check uniqueness before creation
var exists = await _repository.IsNameUniqueAsync(command.Name);
if (exists)
{
return Result.Failure("Journal name already exists. Please choose a different name.");
}
Prevention: Implement real-time name validation in UI.
Issue 2: "Cannot post journal - not balanced"
Cause: Total debits don't equal total credits.
Diagnosis:
var journal = await _repository.GetByIdAsync(journalId);
Console.WriteLine($"Total Debits: {journal.TotalDebitAmount}");
Console.WriteLine($"Total Credits: {journal.TotalCreditAmount}");
Console.WriteLine($"Difference: {journal.TotalDebitAmount - journal.TotalCreditAmount}");
Solution: Add balancing entry to correct the imbalance.
Issue 3: "Voucher number generation failed"
Cause: NumberSequence not properly configured or depleted.
Diagnosis:
var voucherSeries = await _numberSequenceRepository.GetByIdAsync(journal.VoucherSeriesId);
Console.WriteLine($"Next Number: {voucherSeries.NextNumber}");
Console.WriteLine($"End Number: {voucherSeries.EndNumber}");
Console.WriteLine($"Is Depleted: {voucherSeries.NextNumber > voucherSeries.EndNumber}");
Solution:
- If depleted: Extend number sequence range
- If not configured: Assign valid NumberSequence to LedgerJournalName
Issue 4: "Cannot change offset account - locked"
Cause: LedgerJournalName configured with IsFixedOffsetAccount = true.
Solution (User): This is by design. Contact system administrator if different offset account is truly needed.
Solution (Admin):
- Option 1: Create new LedgerJournalName with different offset account
- Option 2: Modify existing journal name to make offset account flexible (impacts all future journals)
Issue 5: "Transaction date in closed fiscal period"
Cause: Attempting to post to a period that has been closed.
Diagnosis:
var period = ledger.GetFiscalPeriodForDate(transactionDate);
Console.WriteLine($"Period: {period.Name}");
Console.WriteLine($"Status: {period.Status}");
Console.WriteLine($"Closed Date: {period.ClosedDate}");
Solution:
- Option 1: Change transaction date to an open period
- Option 2: Reopen fiscal period (requires proper authorization and audit trail)
Performance Considerations
1. Journal Name Caching
Recommendation: Cache LedgerJournalName configurations since they rarely change.
// Cache for 1 hour
var journalName = await _cache.GetOrCreateAsync(
$"JournalName:{journalNameId}",
async entry =>
{
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1);
return await _repository.GetByIdAsync(journalNameId, includeAll: true);
}
);
Invalidation: Clear cache when journal name is modified.
2. Batch Voucher Number Generation
Problem: Calling NumberSequence service once per line is slow.
Solution: Pre-allocate voucher numbers in batch:
// For OneVoucherNumberOnly: Generate once
var batchVoucher = await _numberSequenceService.GetNextNumberAsync(voucherSeriesId);
// For InConnectionWithBalance: Pre-allocate range
var voucherRange = await _numberSequenceService.AllocateRangeAsync(
voucherSeriesId,
count: estimatedBalancedSets
);
3. Dimension Combination Reuse
Optimization: Always check for existing combinations before creating new ones.
// Calculate hash first
var hash = CalculateHash(segments);
// Check existence
var existing = await _repository.GetByHashAsync(hash);
if (existing != null)
{
return existing.Id; // Reuse
}
// Create only if new
var combination = DimensionCombination.Create(segments, accountStructure);
await _repository.AddAsync(combination);
return combination.Id;
Impact: Can reduce dimension combination records by 70-90% in typical scenarios.
Related Documentation
Domain Aggregates
- LedgerJournalName Aggregate
- LedgerJournalHeader Aggregate
- DimensionCombination Aggregate
- NumberSequence Aggregate
API Endpoints
Related Concepts
Architecture Patterns
- Strategy Pattern (Posting Strategies)
- Factory Pattern (Strategy Factory, Number Sequence)
- Repository Pattern (Data Access)
- Domain-Driven Design (Aggregates, Domain Services)
Last Updated: 2025-01-15 | Version: 1.0 | Status: Active