Financial Dimensions Domain Documentation
Overview
This section contains domain documentation for the Financial Dimensions sub-module, which provides shared dimensional infrastructure used across all finance modules. This foundational module enables multi-dimensional accounting by managing dimension attributes, values, and default dimension sets.
Core Aggregates
DimensionAttribute Aggregate Pending Documentation
Purpose: Defines dimension types and their metadata for use across finance modules.
Key Responsibilities:
- Dimension attribute definition and lifecycle management
- Type-specific validation rules (CustomList, DynamicEntity, FinancialDimension)
- Activation/deactivation workflow
- Integration with backing entity systems
Business Rules:
- Dimension names must be unique within the system
- Cannot be deleted if referenced by existing combinations
- Custom dimensions require at least one active value
- Entity-backed dimensions must specify valid backing entity configuration
- Financial dimensions (MainAccount) are system-required and cannot be deactivated
Planned Domain Events:
DimensionAttributeCreatedDomainEventDimensionAttributeActivatedDomainEventDimensionAttributeDeactivatedDomainEventDimensionAttributeValueAddedDomainEvent
Planned Key Methods:
public void Activate(IDimensionValidationService validationService)
public void Deactivate()
public void AddValue(DimensionAttributeValue value)
public bool CanBeDeleted()
public ValidationResult ValidateValue(string value)
DimensionAttributeValue Aggregate Pending Documentation
Purpose: Individual dimension values with lifecycle management and validation.
Key Responsibilities:
- Dimension value creation and management
- Suspension/activation lifecycle
- Group dimension support for reporting
- Integration with backing entity data sources
Business Rules:
- Values must be unique within their dimension attribute
- Suspended values cannot be used in new transactions
- Cannot be deleted if referenced by existing combinations
- Display values automatically generated for entity-backed dimensions
- Group dimensions enable hierarchical reporting
Planned Domain Events:
DimensionAttributeValueCreatedDomainEventDimensionAttributeValueSuspendedDomainEventDimensionAttributeValueActivatedDomainEventDimensionAttributeValueUpdatedDomainEvent
Planned Key Methods:
public void Suspend(string reason)
public void Activate()
public void UpdateDisplayValue(string newDisplayValue)
public bool CanBeDeleted()
DimensionAttributeValueSet Aggregate Pending Documentation
Purpose: Collections of default dimension values for master data integration.
Key Responsibilities:
- Default dimension set creation and management
- Hash-based uniqueness for efficient reuse
- Master data integration (Customer, Vendor, etc.)
- Cross-module dimension inheritance
Business Rules:
- Hash-based identity ensures identical sets are reused
- Cannot be modified once created (create new set instead)
- Must contain only valid, active dimension values
- Used for inheriting dimensions from master data to transactions
Planned Domain Events:
DimensionAttributeValueSetCreatedDomainEventDimensionAttributeValueSetReusedDomainEventDimensionAttributeValueSetAssignedDomainEvent
Planned Key Methods:
public static DimensionAttributeValueSet Create(
IReadOnlyList<DimensionAttributeValueSetItem> items,
IDimensionAttributeValueSetDomainService domainService)
public string GenerateHash()
public bool IsEquivalentTo(DimensionAttributeValueSet other)
DimensionAttributeValueSetItem Entity Pending Documentation
Purpose: Individual items within a dimension value set.
Key Responsibilities:
- Links specific dimension values to value sets
- Maintains referential integrity
- Provides efficient querying capabilities
Business Rules:
- Must reference valid, active dimension attribute and value
- Cannot have duplicate dimension attributes within the same set
- Immutable once created
Domain Services
DimensionValueResolverService Complete
Purpose: Converts string dimension values to stable IDs using get-or-create pattern.
Key Operations:
Task<IReadOnlyList<DimensionSegment>> ResolveSegmentValuesAsync(
IList<DimensionSegmentInput> segmentInputs,
CancellationToken cancellationToken = default)
Business Logic:
- Validates dimension attribute exists and is active
- For CustomList dimensions: finds existing value or creates new one
- For DynamicEntity dimensions: validates value exists in backing entity
- For FinancialDimension dimensions: validates against chart of accounts
- Returns stable IDs for use in dimension combinations
Error Handling:
- Throws if dimension attribute not found or inactive
- Throws if value is suspended and cannot be used
- Throws if backing entity validation fails
DimensionAttributeValueSetDomainService Planned
Purpose: Manages dimension value set creation with hash-based deduplication.
Planned Operations:
Task<DimensionAttributeValueSet> GetOrCreateAsync(
IList<DimensionSegmentInput> segmentInputs,
string description = null)
Task<string> GenerateHashAsync(IReadOnlyList<(Guid DimensionAttributeId, Guid ValueId)> pairs)
Task<DimensionAttributeValueSet> FindByHashAsync(string hash)
Planned Business Logic:
- Resolves input values to stable IDs
- Calculates hash based on sorted (DimensionAttributeId, ValueId) pairs
- Checks for existing set with same hash
- Creates new set only if no existing match found
- Updates usage statistics for analytics
DimensionValidationService Planned
Purpose: Cross-cutting validation logic for dimensional consistency.
Planned Operations:
Task<ValidationResult> ValidateDimensionValueAsync(
Guid dimensionAttributeId,
string value)
Task<ValidationResult> ValidateDimensionCombinationAsync(
IList<DimensionSegmentInput> segments,
DimensionHierarchy accountStructure)
Task<bool> IsValueActiveAsync(Guid dimensionAttributeId, Guid valueId)
Planned Business Logic:
- Validates individual dimension values
- Checks cross-dimensional business rules
- Validates against account structure requirements
- Provides detailed validation messages for UI feedback
DynamicDimensionValueProviderRegistry Planned
Purpose: Registry for entity-backed dimension value providers.
Planned Operations:
void RegisterProvider(string backingEntityKey, IDynamicDimensionValueProvider provider)
Task<IEnumerable<DimensionAttributeValueViewModel>> GetValuesAsync(
DimensionAttributeMetadata metadata,
DimensionValueSearchContext searchContext)
Task<DimensionAttributeValueViewModel> GetValueByKeyAsync(
DimensionAttributeMetadata metadata,
string key)
Planned Business Logic:
- Routes dimension value requests to appropriate providers
- Handles caching and performance optimization
- Manages provider lifecycle and error handling
- Enables consistent interface for entity-backed dimensions
Value Objects
DimensionSegmentInput
Purpose: User input for dimension values during interactive entry.
public record DimensionSegmentInput(
Guid DimensionAttributeId,
string Value)
{
public bool IsValid => DimensionAttributeId != Guid.Empty && !string.IsNullOrWhiteSpace(Value);
}
DimensionAttributeMetadata
Purpose: Configuration metadata for entity-backed dimensions.
public record DimensionAttributeMetadata(
string BackingEntityKey,
string ValueAttribute,
string NameAttribute,
string KeyAttribute,
string ViewName = null)
{
public bool IsValid => !string.IsNullOrWhiteSpace(BackingEntityKey) &&
!string.IsNullOrWhiteSpace(ValueAttribute) &&
!string.IsNullOrWhiteSpace(KeyAttribute);
}
DimensionValueSearchContext
Purpose: Search and filtering context for dimension value lookups.
public record DimensionValueSearchContext(
string SearchTerm = null,
int? Limit = null,
int? Skip = null,
bool ExcludeSuspended = true,
IReadOnlyDictionary<Guid, string> ContextualFilters = null)
{
public bool HasSearchCriteria => !string.IsNullOrWhiteSpace(SearchTerm) ||
ContextualFilters?.Any() == true;
}
Key Business Rules
Dimension Attribute Uniqueness
Rule: Dimension attribute names must be unique across the entire system.
Rationale: Prevents confusion and ensures consistent dimensional classification.
Enforcement:
public class DimensionAttribute
{
public static DimensionAttribute Create(
string name,
IDimensionAttributeRepository repository)
{
if (await repository.ExistsByNameAsync(name))
{
throw new DomainException($"Dimension attribute with name '{name}' already exists");
}
return new DimensionAttribute(name);
}
}
Value Set Immutability
Rule: DimensionAttributeValueSet entities cannot be modified once created.
Rationale:
- Enables safe reuse across multiple master data records
- Maintains referential integrity for historical data
- Supports hash-based deduplication
Implementation: Create new value set for any changes
Dimension Value Suspension Rules
Rule: Suspended dimension values cannot be used in new transactions but remain visible in historical data.
Enforcement:
public void ValidateValueForNewTransaction(DimensionAttributeValue value)
{
if (value.IsSuspended)
{
throw new DomainException(
$"Suspended dimension value '{value.Value}' cannot be used in new transactions");
}
}
Hash-Based Deduplication
Rule: Dimension value sets with identical content share the same hash and are reused.
Implementation:
public string GenerateHash()
{
var sortedPairs = Items
.Select(item => $"{item.DimensionAttributeId}:{item.DimensionAttributeValueId}")
.OrderBy(pair => pair)
.ToArray();
var concatenated = string.Join("|", sortedPairs);
return ComputeSHA256Hash(concatenated);
}
Cross-Module Integration Patterns
With General Ledger Module
// GL requests dimension resolution from FD
public class LedgerJournalTransactionHandler
{
private readonly IDimensionValueResolverService _dimensionResolver;
public async Task<LedgerJournalLine> CreateTransactionLineAsync(
AddLedgerJournalTransactionCommand command)
{
// 1. Resolve dimension values using FD service
var resolvedSegments = await _dimensionResolver
.ResolveSegmentValuesAsync(command.DimensionSegments);
// 2. GL creates dimension combination using resolved segments
var combination = await _dimensionCombinationService
.CreateOrReuseCombinationAsync(resolvedSegments, accountStructure);
// 3. Create journal line with stable dimension combination ID
return new LedgerJournalLine(
combination.Id,
command.Amount,
command.Description);
}
}
With Accounts Receivable Module (Future)
// Customer setup with default dimensions
public class Customer
{
public Guid? DefaultDimensionId { get; private set; }
public async Task SetDefaultDimensionsAsync(
IList<DimensionSegmentInput> dimensionInputs,
IDimensionAttributeValueSetDomainService valueSetService)
{
if (dimensionInputs?.Any() == true)
{
var valueSet = await valueSetService.GetOrCreateAsync(
dimensionInputs,
$"Default dimensions for customer {Name}");
DefaultDimensionId = valueSet.Id;
}
else
{
DefaultDimensionId = null;
}
AddDomainEvent(new CustomerDefaultDimensionsChangedDomainEvent(this));
}
}
// Invoice inherits customer default dimensions
public class CustomerInvoice
{
public async Task InheritCustomerDimensionsAsync(
Customer customer,
IDimensionAttributeValueSetService valueSetService)
{
if (customer.DefaultDimensionId.HasValue)
{
var defaultSet = await valueSetService
.GetByIdAsync(customer.DefaultDimensionId.Value);
// Apply default dimensions to invoice lines
foreach (var line in Lines.Where(l => !l.HasOverrideDimensions))
{
line.ApplyDimensionSet(defaultSet);
}
}
}
}
With CRM Module (Future)
// Customer dimension provider for entity-backed dimensions
public class CustomerDimensionValueProvider : IDynamicDimensionValueProvider
{
public string BackingEntityTypeKey => "customer";
public async Task<IEnumerable<DimensionAttributeValueViewModel>> GetValuesAsync(
DimensionAttributeMetadata metadata,
DimensionValueSearchContext searchContext,
CancellationToken cancellationToken)
{
var customers = await _customerRepository.SearchAsync(
searchContext.SearchTerm,
searchContext.Limit ?? 50,
cancellationToken);
return customers.Select(customer => new DimensionAttributeValueViewModel
{
Id = customer.Id,
Value = customer.CustomerNumber,
DisplayValue = customer.Name,
IsSuspended = !customer.IsActive,
GroupDimension = customer.CustomerCategory
});
}
}
Domain Event Flows
Dimension Value Set Creation
Dynamic Dimension Value Resolution
Performance Considerations
Caching Strategies
// Dimension metadata caching
public class CachedDimensionAttributeService
{
private readonly IMemoryCache _cache;
public async Task<DimensionAttribute> GetByIdAsync(Guid id)
{
return await _cache.GetOrCreateAsync(
$"dimension-attribute:{id}",
async entry =>
{
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(24);
return await _repository.GetByIdAsync(id);
});
}
}
// Value lookup caching for dynamic entities
public class CachedDynamicDimensionValueProvider
{
private readonly IMemoryCache _cache;
public async Task<DimensionAttributeValueViewModel> GetValueByKeyAsync(
DimensionAttributeMetadata metadata,
string key)
{
var cacheKey = $"dynamic-value:{metadata.BackingEntityKey}:{key}";
return await _cache.GetOrCreateAsync(cacheKey, async entry =>
{
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(15);
return await _baseProvider.GetValueByKeyAsync(metadata, key);
});
}
}
Bulk Operations Optimization
// Efficient bulk dimension value creation
public class BulkDimensionValueService
{
public async Task<BulkOperationResult> CreateValuesAsync(
Guid dimensionAttributeId,
IEnumerable<DimensionValueCreateInput> values)
{
var batches = values.Chunk(100); // Process in batches
var results = new List<DimensionAttributeValue>();
foreach (var batch in batches)
{
var batchResults = await ProcessBatchAsync(dimensionAttributeId, batch);
results.AddRange(batchResults);
}
return new BulkOperationResult(results);
}
}
Hash Computation Optimization
// Optimized hash generation for dimension value sets
public class OptimizedHashGenerator
{
private static readonly SHA256 _hasher = SHA256.Create();
public string GenerateHash(IReadOnlyList<(Guid AttributeId, Guid ValueId)> pairs)
{
// Pre-sort to ensure consistent hash regardless of input order
var sortedPairs = pairs.OrderBy(p => p.AttributeId).ThenBy(p => p.ValueId);
using var stream = new MemoryStream();
using var writer = new BinaryWriter(stream);
foreach (var (attributeId, valueId) in sortedPairs)
{
writer.Write(attributeId.ToByteArray());
writer.Write(valueId.ToByteArray());
}
var hashBytes = _hasher.ComputeHash(stream.ToArray());
return Convert.ToBase64String(hashBytes);
}
}
Testing Strategies
Aggregate Testing
[TestFixture]
public class DimensionAttributeValueSetTests
{
[Test]
public void Create_Should_Generate_Consistent_Hash()
{
// Arrange
var items = new[]
{
new DimensionAttributeValueSetItem(attrId1, valueId1),
new DimensionAttributeValueSetItem(attrId2, valueId2)
};
// Act
var set1 = DimensionAttributeValueSet.Create(items, mockService.Object);
var set2 = DimensionAttributeValueSet.Create(items.Reverse(), mockService.Object);
// Assert
Assert.AreEqual(set1.Hash, set2.Hash);
}
[Test]
public void Create_Should_Validate_All_Items()
{
// Arrange
var invalidItems = new[]
{
new DimensionAttributeValueSetItem(Guid.Empty, valueId1)
};
// Act & Assert
Assert.Throws<DomainException>(
() => DimensionAttributeValueSet.Create(invalidItems, mockService.Object));
}
}
Domain Service Testing
[TestFixture]
public class DimensionValueResolverServiceTests
{
[Test]
public async Task Should_Create_New_Value_When_Not_Exists()
{
// Arrange
var service = new DimensionValueResolverService(mockRepo.Object, mockProviders.Object);
var input = new DimensionSegmentInput(customListDimId, "NEW_VALUE");
mockRepo.Setup(r => r.FindValueAsync(customListDimId, "NEW_VALUE"))
.ReturnsAsync((DimensionAttributeValue)null);
// Act
var result = await service.ResolveSegmentValuesAsync(new[] { input });
// Assert
Assert.AreEqual(1, result.Count);
mockRepo.Verify(r => r.AddValueAsync(It.IsAny<DimensionAttributeValue>()), Times.Once);
}
[Test]
public async Task Should_Reuse_Existing_Value_When_Exists()
{
// Arrange
var existingValue = new DimensionAttributeValue(customListDimId, "EXISTING_VALUE");
mockRepo.Setup(r => r.FindValueAsync(customListDimId, "EXISTING_VALUE"))
.ReturnsAsync(existingValue);
// Act
var result = await service.ResolveSegmentValuesAsync(
new[] { new DimensionSegmentInput(customListDimId, "EXISTING_VALUE") });
// Assert
Assert.AreEqual(existingValue.Id, result.First().DimensionAttributeValueId);
mockRepo.Verify(r => r.AddValueAsync(It.IsAny<DimensionAttributeValue>()), Times.Never);
}
}
Integration Testing
[TestFixture]
public class CrossModuleIntegrationTests
{
[Test]
public async Task Customer_Default_Dimensions_Should_Be_Inherited_By_Invoice()
{
// Arrange
var customer = await CreateCustomerWithDefaultDimensions();
var invoice = new CustomerInvoice(customer.Id);
// Act
await invoice.InheritCustomerDimensionsAsync(customer, valueSetService);
// Assert
Assert.IsTrue(invoice.Lines.All(l => l.DimensionSegments.Any()));
var inheritedSegments = invoice.Lines.First().DimensionSegments;
Assert.Contains(departmentSegment, inheritedSegments);
}
}
Future Enhancements
Advanced Features
- Machine Learning Integration: Intelligent dimension value suggestions based on transaction patterns
- Advanced Validation Rules: Complex cross-dimensional business rules engine
- Hierarchical Dimensions: Support for dimension value hierarchies and rollups
- Temporal Dimensions: Time-based dimension values for seasonal or project-based accounting
Performance Optimizations
- Distributed Caching: Redis-based caching for multi-instance deployments
- Read Replicas: Separate read/write paths for dimension lookups
- Bulk Import Optimization: Optimized bulk dimension value import processes
- Search Indexing: Elasticsearch integration for fast dimension value search
Integration Enhancements
- External System Integration: Connectors for external master data systems
- Real-time Synchronization: Event-driven synchronization with external systems
- API Rate Limiting: Advanced rate limiting for dimension lookup APIs
- Multi-Tenant Isolation: Enhanced support for multi-tenant dimension management
Related Documentation
Implementation References
- Financial Dimensions API Documentation - REST endpoints exposing domain operations
- Default Dimensions Concept - Master data integration workflow
- Cross-Module Integration - Integration patterns
Cross-Module References
- General Ledger Domain - Primary consumer of dimension services
- Finance Domain Overview - Complete finance module domain documentation
Last Updated: [Current Date] | Version: 1.0 | Status: Planning Phase
Priority Items:
- Complete DimensionAttribute aggregate implementation and documentation
- Implement DimensionAttributeValueSet aggregate and domain service
- Create comprehensive integration testing framework
- Implement dynamic dimension value provider registry
- Performance optimization for high-volume dimension lookups