Skip to main content

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 management
  • FundService – Fund management, fee calculations, and snapshot creation
  • BankAccountService – Cash management and interest accrual
  • FdrService – Fixed deposit management and interest processing
  • EquityPortfolioService – Equity holdings and market data synchronization
  • BondPortfolioService – Bond holdings and accrued interest calculations
  • UnrealizedGainLossService – Mark-to-market unrealized gain/loss processing
  • AccountTemplateService – 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:

  • fundId must be non-empty string
  • businessDate must be valid ISO date string
  • Optional createdBy user 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:

  1. Equity Sync: Fetch latest prices and corporate actions
  2. Bond Sync: Update bond prices and accrued interest data
  3. 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:

  1. Generate equity holdings daily snapshots
  2. Generate bond holdings daily snapshots
  3. 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:

  1. Bank Account Interest

    • Daily interest = Balance × (Annual Rate ÷ 365)
    • Journal Entry: Debit Interest Receivable, Credit Interest Income
  2. FDR Interest

    • Daily interest = Principal × (BPS Rate ÷ 365 ÷ 10,000)
    • Journal Entry: Debit FDR Interest Receivable, Credit FDR Interest Income
  3. 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:

  1. Valuation posts journal entries first
  2. NAV calculated from updated ledger
  3. Fees calculated on accurate NAV
  4. 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

  1. Use specific error types for different failure scenarios
  2. Include context in error objects (IDs, values, business context)
  3. Log comprehensively with structured data and correlation IDs
  4. Implement graceful degradation for non-critical failures
  5. Preserve error context for debugging and audit purposes

Accounting Accuracy

  1. Maintain balance in all journal entries (debits = credits)
  2. Use accrual principles for revenue and expenses
  3. Validate accounts before processing begins
  4. Close periods properly to maintain accounting equation
  5. Create audit trails for all financial operations

Performance

  1. Process sequentially to maintain accounting integrity
  2. Use batching for high-volume journal entry creation
  3. Implement proper transaction management for data consistency
  4. Optimize database queries with appropriate indexing
  5. Monitor processing times by phase and operation type

Monitoring

  1. Track success rates by fund and processing phase
  2. Monitor error frequencies and types
  3. Log processing durations for performance analysis
  4. Create alerts for critical failures and prolonged processing
  5. 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> to Promise<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

  1. Explicit error handling - All error paths are visible and handled
  2. Type safety - Error types are discriminated unions with full type coverage
  3. Better testing - Error scenarios can be tested deterministically
  4. Improved debugging - Rich error context with business meaning
  5. 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