Run End-of-Day Use Case API
Orchestrates comprehensive end-of-day processing for funds with an 8-phase workflow that handles portfolio valuation, interest accrual, fee calculations, and NAV computation. This use case has been fully migrated to neverthrow Result patterns with custom EODError types.
Status: ✅ Fully migrated to neverthrow - Uses Result<T, EODError> patterns with comprehensive error handling
Migration: Complete with custom error types and structured error propagation
Location: worker/use-cases/operations/run-end-of-day.use-case.ts
Dependencies
AccountingService– Double-entry bookkeeping and journal entry managementFundService– Fund management, fee calculations, and snapshot creationBankAccountService– Cash management and interest accrualFdrService– Fixed deposit management and interest processingEquityPortfolioService– Equity holdings and market data synchronizationBondPortfolioService– Bond holdings and accrued interest calculationsUnrealizedGainLossService– Mark-to-market unrealized gain/loss processingAccountTemplateService– Account validation and setup
Request & Response
RunEndOfDayRequest
interface RunEndOfDayRequest {
fundId: string; // Fund to run EOD for
businessDate: string; // Business date (ISO string)
createdBy?: string; // User ID running the EOD process
}
EODResult
interface EODResult {
success: boolean; // Overall success status
fundId: string; // Fund ID
businessDate: string; // Business date processed
syncErrors: Array<{
// Market data sync errors
entityType: string; // "equity" or "bond"
entityId: string; // Ticker or ISIN
error: string; // Error description
}>;
snapshotCreated: boolean; // Whether snapshot was created
eodDateUpdated: boolean; // Whether EOD date was updated
processingSummary: {
// Phase completion summary
accountsValidated: boolean; // Account validation completed
portfolioValuationCompleted: boolean;
interestProcessingCompleted: boolean;
snapshotCreated: boolean;
};
}
Custom Error Types
The use case implements comprehensive error handling with discriminated unions:
EODError Types
export type EODError =
| ValidationError // Input validation failures
| AccountValidationError // Missing required accounts
| PortfolioValuationError // Portfolio valuation failures
| InterestProcessingError // Interest accrual failures
| SnapshotCreationError // Snapshot creation failures
| SystemError; // System-level failures
ValidationError
interface ValidationError {
type: "ValidationError";
field: string; // Field that failed validation
message: string; // Error message
value?: unknown; // Invalid value
}
// Usage examples
ValidationError.fundId(); // Missing fund ID
ValidationError.businessDate(); // Invalid business date
ValidationError.custom("field", "Custom error message", value);
AccountValidationError
interface AccountValidationError {
type: "AccountValidationError";
missingAccounts: string[]; // Names of missing accounts
message: string; // Error description
}
// Usage
AccountValidationError.missingAccounts(["Cash", "Equity Securities"]);
PortfolioValuationError
interface PortfolioValuationError {
type: "PortfolioValuationError";
phase:
| "equity_sync"
| "bond_sync"
| "equity_snapshot"
| "bond_snapshot"
| "unrealized_gnl"
| "general";
message: string;
syncErrors?: Array<{ entityType: string; entityId: string; error: string }>;
}
// Phase-specific error creation
PortfolioValuationError.equitySyncFailed("DSE API unavailable", syncErrors);
PortfolioValuationError.bondSnapshotFailed("Database timeout");
PortfolioValuationError.unrealizedGnlFailed("Missing market data");
InterestProcessingError
interface InterestProcessingError {
type: "InterestProcessingError";
interestType: "bank" | "fdr" | "bond";
entityId: string; // Bank/FDR/Bond ID
message: string;
}
// Type-specific creation
InterestProcessingError.bankInterestFailed(bankId, "Negative balance");
InterestProcessingError.fdrInterestFailed(fdrId, "Matured FDR");
InterestProcessingError.bondInterestFailed(bondId, "Invalid coupon rate");
SnapshotCreationError
interface SnapshotCreationError {
type: "SnapshotCreationError";
phase: "nav_calculation" | "portfolio_summary" | "snapshot_creation";
message: string;
}
// Phase-specific creation
SnapshotCreationError.navCalculationFailed("Division by zero");
SnapshotCreationError.portfolioSummaryFailed("Portfolio service unavailable");
SnapshotCreationError.snapshotCreationFailed("Storage quota exceeded");
SystemError
interface SystemError {
type: "SystemError";
code: string; // Error code for monitoring
message: string;
originalError?: Error; // Original error for debugging
details?: Record<string, unknown>;
}
// System error creation
SystemError.eodDateUpdateFailed(fundId, "Database constraint violation");
SystemError.custom(
"SERVICE_UNAVAILABLE",
"External service down",
originalError,
);
8-Phase EOD Workflow
Phase 1: Input Validation
Validates request parameters and ensures proper data format.
// Validates fund ID and business date format
const validationResult = this.validateRequest(request);
if (validationResult.isErr()) {
return err(validationResult.error);
}
Validation Rules:
fundIdmust be non-empty stringbusinessDatemust be valid ISO date string- Optional
createdByuser ID for audit trail
Phase 2: Account Validation
Ensures all required EOD accounts exist using account templates.
const accountValidationResult = await this.validateEODAccounts(request.fundId);
if (accountValidationResult.isErr()) {
return err(accountValidationResult.error);
}
Account Categories Validated:
- Asset accounts (Cash, Securities, Receivables)
- Liability accounts (Payables, Accrued expenses)
- Equity accounts (Capital, Reserves)
- Revenue accounts (Interest income, Fee income)
- Expense accounts (Management fees, Operating expenses)
Phase 3: Market Data Synchronization
Syncs equity and bond market data from external sources (DSE API).
const portfolioValuationResult = await this.processPortfolioValuationSafe(
request.fundId,
request.businessDate,
);
Sub-phases:
- Equity Sync: Fetch latest prices and corporate actions
- Bond Sync: Update bond prices and accrued interest data
- Error Handling: Collects sync errors without failing EOD
Error Recovery: Sync errors are collected and stored in the snapshot but don't stop EOD processing.
Phase 4: Portfolio Valuation
Applies market prices to holdings and generates daily snapshots.
Operations:
- Generate equity holdings daily snapshots
- Generate bond holdings daily snapshots
- Create unrealized gain/loss journal entries (daily incremental)
Unrealized Gain/Loss Logic:
// Daily unrealized gain/loss = (Today's MV - Today's Cost) - (Yesterday's MV - Yesterday's Cost)
const dailyUnrealizedGainLossChange =
todaysUnrealizedGainLoss - yesterdaysUnrealizedGainLoss;
Phase 5: Interest Accrual Processing
Calculates and posts daily interest accrual journal entries.
const interestProcessingResult = await this.processAllInterestSafe(
request.fundId,
request.businessDate,
request.createdBy,
);
Interest Types:
-
Bank Account Interest
- Daily interest = Balance × (Annual Rate ÷ 365)
- Journal Entry: Debit Interest Receivable, Credit Interest Income
-
FDR Interest
- Daily interest = Principal × (BPS Rate ÷ 365 ÷ 10,000)
- Journal Entry: Debit FDR Interest Receivable, Credit FDR Interest Income
-
Bond Coupon Interest
- Daily interest = (Quantity × Face Value × Coupon Rate ÷ 10,000) ÷ 365
- ACT/365 day count convention
- Journal Entry: Debit Bond Interest Receivable, Credit Bond Interest Income
Phase 6: Fee Calculation & Accrual
Calculates management fees and creates accrual journal entries.
// Get pre-fee NAV from accounting system
const preFeeNav = await this.accountingService.getNetAssetValueAsOf(
fundId,
businessDate,
);
// Calculate fees based on NAV
const fees = await this.fundService.calculateAllFees(
fundId,
preFeeNav,
preFeeNav,
preFeeNav,
0,
);
// Post fee accrual entries
await this.postFeeAccrualEntries(fundId, businessDate, fees, createdBy);
Fee Types Processed:
- Management Fee (annual basis points on NAV)
- Trustee Fee (regulatory requirement)
- Custodian Fee (asset custody)
- CDBL Fee (central depository services)
- BSEC Fee (securities regulator)
- Formation Fee (one-time setup cost)
Fee Accrual Journal Entry:
Debit: Fee Expense Account (increases expenses)
Credit: Fee Payable Account (increases liabilities)
Phase 7: Asset Management
Processes depreciation for fixed assets.
await this.processDepreciationForFund(
request.fundId,
request.businessDate,
request.createdBy,
);
Depreciation Calculation:
- Daily depreciation = Acquisition Cost ÷ (Useful Life in Years × 365)
- Journal Entry: Debit Depreciation Expense, Credit Accumulated Depreciation
- Only processes active assets with positive depreciation amounts
Phase 8: Finalization
Creates fund snapshot and updates EOD processing date.
// Get final NAV from accounting system (includes all accruals)
const finalNav = await this.accountingService.getNetAssetValueAsOf(
fundId,
businessDate,
);
// Create comprehensive snapshot
const snapshotResult = await this.createFundSnapshotFromAccounting(
request.fundId,
request.businessDate,
finalNav,
fees,
syncErrors,
);
// Update fund's EOD date
const eodUpdateResult = await this.updateEODDateSafe(
request.fundId,
request.businessDate,
);
Snapshot Contents:
- Total assets, liabilities, and equity (from accounting system)
- Portfolio summary (holdings counts and values)
- Fee breakdowns and calculations
- NAV per unit (Total NAV ÷ Total Units)
- Error summaries from market data synchronization
- Processing metadata and audit trail
Account Closing:
// Close income/expense accounts to maintain accounting equation balance
await this.accountingService.closeAccountsForDate(
fundId,
businessDate,
createdBy,
);
Usage Examples
Basic EOD Processing
const runEndOfDayUseCase = app.applicationServices.runEndOfDay;
const request: RunEndOfDayRequest = {
fundId: "fund-123",
businessDate: "2025-01-15T00:00:00.000Z",
createdBy: "user-789",
};
const result = await runEndOfDayUseCase.execute(request);
if (result.isOk()) {
console.log("EOD processing completed successfully:", result.value);
console.log("NAV calculated:", result.value.snapshotCreated);
console.log("Sync errors:", result.value.syncErrors.length);
} else {
const error = result.error;
console.error("EOD processing failed:", error.message);
// Handle specific error types
switch (error.type) {
case "ValidationError":
console.error("Invalid input:", error.field, error.message);
break;
case "AccountValidationError":
console.error("Missing accounts:", error.missingAccounts);
break;
case "PortfolioValuationError":
console.error("Valuation failed in phase:", error.phase);
break;
case "SystemError":
console.error("System error:", error.code);
break;
}
}
Legacy Exception-Based Pattern (Deprecated)
// @deprecated - Use execute() with Result handling instead
try {
await runEndOfDayUseCase.executeLegacy(request);
console.log("EOD completed (legacy method)");
} catch (error) {
console.error("EOD failed (legacy method):", error.message);
}
EOD State Checking
// Check current EOD processing state
const eodState = await runEndOfDayUseCase.getFundEODState(fundId);
if (eodState.lastEodDate === businessDate) {
console.log("EOD already processed for this date");
} else {
console.log("EOD needs to be run for:", businessDate);
// Run EOD processing
}
Error Handling Patterns
Result-Based Error Propagation
// Method signature with Result type
async execute(request: RunEndOfDayRequest): Promise<Result<EODResult, EODError>>
// Error handling in execution flow
const validationResult = this.validateRequest(request);
if (validationResult.isErr()) {
return err(validationResult.error); // Propagate specific error
}
// Continue with success case
const validatedValue = validationResult.value;
Graceful Degradation
EOD processing implements graceful degradation for non-critical failures:
// Fee accrual failures are logged but don't stop EOD
try {
await this.postFeeAccrualEntries(fundId, businessDate, fees, createdBy);
} catch (error) {
logger.warn({
fundId,
businessDate,
error: error.message,
message: "Fee accrual failed - continuing with EOD processing",
});
// Continue processing - ensures EOD date gets updated
}
Error Collection vs. Failure
Collectible Errors (don't stop EOD):
- Market data sync errors (stored in snapshot)
- Individual fee accrual failures (logged and skipped)
- Account creation failures (auto-created with warnings)
Blocking Errors (stop EOD processing):
- Input validation errors
- Missing required accounts
- Portfolio valuation failures
- Snapshot creation failures
- System-level failures
Accounting Principles
Double-Entry Bookkeeping
All EOD operations maintain accounting equation balance:
Assets = Liabilities + Equity
Debits must equal credits in every journal entry
Accrual Basis Accounting
EOD uses accrual accounting principles:
- Interest is accrued daily, not when received
- Fees are accrued when earned, not when paid
- Unrealized gains/losses are recorded daily
- Expenses are recognized when incurred
Chart of Accounts Integration
EOD processing requires properly structured chart of accounts:
- Standard account codes and names
- Proper account type classification
- Consistent naming conventions
- Template-based account validation
Monitoring & Auditing
Structured Logging
// Comprehensive logging throughout EOD process
logger.info(
{
fundId,
businessDate,
phase: "portfolio_valuation",
syncErrors: syncErrors.length,
nav: finalNav,
},
"EOD processing completed",
);
logger.error(
{
fundId,
businessDate,
error: error.message,
phase: "snapshot_creation",
stack: error.stack,
},
"EOD processing failed",
);
Error Metrics
EOD processing emits detailed error metrics:
- Error counts by type and phase
- Processing duration by phase
- Market data sync success rates
- Account validation results
Audit Trail
All EOD operations create comprehensive audit trails:
- Journal entries with system user attribution
- Snapshot creation with processing metadata
- Account auto-creation with audit context
- Error conditions with full context preservation
Performance Considerations
Sequential Processing
EOD processes sequentially to ensure proper accounting flow:
- Valuation posts journal entries first
- NAV calculated from updated ledger
- Fees calculated on accurate NAV
- Final snapshot created with verified data
Database Efficiency
- Uses connection pooling for high-volume operations
- Implements batch processing for journal entries
- Optimizes queries with proper indexing
- Manages transactions appropriately
Error Recovery
- Non-critical failures are logged and processing continues
- Critical failures stop processing to prevent data corruption
- Partial progress is preserved for resume capability
- Error context is maintained for debugging
Testing Patterns
Unit Testing
describe("RunEndOfDayUseCase", () => {
it("should validate request input", async () => {
const invalidRequest = { fundId: "", businessDate: "invalid" };
const result = await useCase.execute(invalidRequest);
expect(result.isErr()).toBe(true);
if (result.isErr()) {
expect(result.error.type).toBe("ValidationError");
expect(result.error.field).toBe("fundId");
}
});
it("should handle account validation failures", async () => {
// Mock missing accounts
mockAccountTemplateService.validateRequiredAccounts.mockResolvedValue({
isValid: false,
missing: ["Cash", "Equity Securities"],
});
const result = await useCase.execute(validRequest);
expect(result.isErr()).toBe(true);
if (result.isErr()) {
expect(result.error.type).toBe("AccountValidationError");
expect(result.error.missingAccounts).toContain("Cash");
}
});
});
Integration Testing
describe("EOD Integration", () => {
it("should process complete EOD workflow", async () => {
const result = await useCase.execute(completeRequest);
expect(result.isOk()).toBe(true);
if (result.isOk()) {
expect(result.value.success).toBe(true);
expect(result.value.snapshotCreated).toBe(true);
expect(result.value.eodDateUpdated).toBe(true);
// Verify accounting system state
const accounts = await accountingService.getAccountsByFundId(fundId);
expect(accounts).toBeDefined();
}
});
});
Best Practices
Error Handling
- Use specific error types for different failure scenarios
- Include context in error objects (IDs, values, business context)
- Log comprehensively with structured data and correlation IDs
- Implement graceful degradation for non-critical failures
- Preserve error context for debugging and audit purposes
Accounting Accuracy
- Maintain balance in all journal entries (debits = credits)
- Use accrual principles for revenue and expenses
- Validate accounts before processing begins
- Close periods properly to maintain accounting equation
- Create audit trails for all financial operations
Performance
- Process sequentially to maintain accounting integrity
- Use batching for high-volume journal entry creation
- Implement proper transaction management for data consistency
- Optimize database queries with appropriate indexing
- Monitor processing times by phase and operation type
Monitoring
- Track success rates by fund and processing phase
- Monitor error frequencies and types
- Log processing durations for performance analysis
- Create alerts for critical failures and prolonged processing
- Generate reports for compliance and operational review
Migration Notes
Pre-Migration (Exception-Based)
// Old pattern - throws exceptions
async executeLegacy(request: RunEndOfDayRequest): Promise<void> {
if (!request.fundId) {
throw new Error("Fund ID is required");
}
// ... processing logic
}
Post-Migration (Result-Based)
// New pattern - returns Results
async execute(request: RunEndOfDayRequest): Promise<Result<EODResult, EODError>> {
const validationResult = this.validateRequest(request);
if (validationResult.isErr()) {
return err(validationResult.error);
}
// ... processing logic with Result handling
return ok(result);
}
Breaking Changes
- Method signature: Changed from
Promise<void>toPromise<Result<EODResult, EODError>> - Error handling: Exception throwing replaced with Result-based error returns
- Caller responsibilities: Must handle
.isOk()/.isErr()patterns - Error objects: Structured error data instead of plain error messages
Benefits of Migration
- Explicit error handling - All error paths are visible and handled
- Type safety - Error types are discriminated unions with full type coverage
- Better testing - Error scenarios can be tested deterministically
- Improved debugging - Rich error context with business meaning
- Compliance - Audit-friendly error handling with full traceability
Last Updated: 2025-01-11 - Complete documentation of RunEndOfDay use case with neverthrow patterns and 8-phase workflow