Skip to main content

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

ConceptDefinitionExample
LedgerJournalNameA configuration template that defines journal behavior"Daily General Journal", "Bank Payments"
JournalTypeClassification determining posting strategyDaily, CustomerPayment, VendorPayment, Payroll
VoucherGenerationStrategyRules for how voucher numbers are allocatedInConnectionWithBalance, Manual, OneVoucherNumberOnly
NumberSequenceAutomated number generation serviceGenerates "V-2025-0001", "V-2025-0002", etc.
Posting StrategyImplementation of posting logic for a journal typeGeneralJournalPostingStrategy, PaymentPostingStrategy
Default Offset AccountPre-configured counter-account for quick entryBank 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.

JournalTypePosting StrategyUse CaseKey Behavior
DailyGeneralJournalPostingStrategyGeneral adjustments, accrualsDirect debit/credit posting, flexible structure
CustomerPaymentCustomerPaymentPostingStrategyAR payments, depositsApplies payment to customer invoices, updates AR subledger
VendorPaymentVendorPaymentPostingStrategyAP payments, checksApplies payment to vendor invoices, updates AP subledger
PayrollPayrollPostingStrategyPayroll processingCreates employee liability and expense entries
TaxSettlementTaxSettlementPostingStrategySales tax paymentsCreates 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:

StrategyWhen to UseExample ScenarioVoucher Pattern
InConnectionWithBalanceMulti-line balanced entriesDaily general journal with multiple transactionsV-001 (balanced set), V-002 (next balanced set)
OneVoucherNumberOnlyBatch operations as single transactionOpening balances, payroll batches, payment batchesOB-2025-001 (all lines use same voucher)
ManualExternal reference trackingMigrating data with existing voucher numbersUser 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:

  1. No Default Offset Account (Flexible Entry)

    • User selects both debit and credit accounts manually
    • Use case: Daily general journal with varied transactions
  2. 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
  3. 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:

  1. User navigates to "Create New Journal"
  2. System displays dropdown of available LedgerJournalNames
  3. User selects appropriate template (e.g., "Bank Payments - Chase Checking")
  4. 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:

  1. User enters MainAccount value (e.g., "4010")
  2. System resolves appropriate AccountStructure using ConstraintTree
  3. System validates all required dimensions are provided
  4. System creates or retrieves DimensionCombination
  5. Line is linked to immutable DimensionCombination

This process uses the following domain services:

  • AccountStructureResolutionService - Determines correct structure
  • ConstraintOverlapDetectionService - Ensures no ambiguity
  • DimensionValueResolverService - Resolves string values to IDs
  • DimensionCombinationDomainService - 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:

  1. Journal must be balanced (Total Debits = Total Credits)
  2. All lines must have valid dimension combinations
  3. Transaction dates must be within open fiscal periods
  4. No suspended dimension values can be used
  5. 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:

  1. Create GeneralLedgerEntry with:

    • DimensionCombinationId (account + dimensions)
    • Amount (debit or credit)
    • Transaction Date
    • Voucher Number
    • Source Reference (journal batch number)
  2. Create mirroring offset entry if configured

  3. 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 PurposeScopeExample FormatUsed When
Journal Batch NumbersOne per journal headerGJ-2025-001Creating journal
Voucher NumbersOne per transaction or balanced setV-2025-0001Adding 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.


Domain Aggregates

API Endpoints

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