Shared Services & Utilities
Shared services provide common utilities and types used across the entire Asset360 application. These services are not tied to specific business domains but provide foundational functionality for error handling, logging, and common data structures.
Services Overview
Domain Error System
Location: @shared/domain-errors.ts
The domain error system provides a comprehensive, type-safe approach to error handling using discriminated unions and the neverthrow Result pattern. This replaces traditional exception-based error handling with explicit error types.
Error Types
export type DomainError =
| ValidationError
| NotFoundError
| AuthorizationError
| BusinessRuleViolation
| ConflictError
| DatabaseError;
Usage Patterns
Creating Results with neverthrow:
import { ok, err, type Result } from "neverthrow";
import type { DomainError } from "@shared/domain-errors";
import {
createValidationError,
createNotFoundError,
createBusinessLogicError,
} from "@shared/domain-errors";
function validateUnits(units: number): Result<number, DomainError> {
if (units <= 0) {
return err(createValidationError("units", "Units must be positive", units));
}
return ok(units);
}
function findInvestor(id: string): Promise<Result<Investor, DomainError>> {
const investor = await investorRepository.findById(id);
if (!investor) {
return err(createNotFoundError("Investor", id));
}
return ok(investor);
}
Handling Results:
const result = await purchaseUnits(request);
if (result.isOk()) {
const transaction = result.value;
logger.info("Units purchased successfully", {
transactionId: transaction.id,
});
} else {
const error = result.error;
logger.error("Failed to purchase units", { error: formatError(error) });
// Handle specific error types
switch (error.type) {
case "InsufficientFunds":
return err({
type: "ValidationError",
message: "Insufficient funds for purchase",
});
case "ValidationError":
return result; // Propagate validation errors
default:
return err({ type: "SystemError", message: "Unexpected error" });
}
}
Error Type Details
ValidationError: Input validation failures
interface ValidationError {
type: "ValidationError";
field: string;
message: string;
details?: { value: string };
}
// Usage
err(createValidationError("email", "Email is invalid", "invalid-email"));
NotFoundError: Entity not found in repository
interface NotFoundError {
type: "NotFoundError";
entity: string;
id: string;
}
// Usage
err(createNotFoundError("Fund", fundId));
AuthorizationError: Access control violations
interface AuthorizationError {
type: "AuthorizationError";
resource: string;
action: string;
message: string;
}
// Usage
err(createAuthorizationError("Fund", "delete"));
BusinessRuleViolation: Business logic constraint violations
interface BusinessRuleViolation {
type: "BusinessRuleViolation";
rule: string;
message: string;
}
// Usage
err(
createBusinessLogicError(
"Fund already has active holdings",
"FUND_HAS_HOLDINGS",
),
);
ConflictError: Resource conflict situations
interface ConflictError {
type: "ConflictError";
message: string;
}
// Usage
err(createConflictError("Fund code already exists"));
DatabaseError: Database operation failures
interface DatabaseError {
type: "DatabaseError";
message: string;
operation: string;
originalError?: unknown;
}
// Usage
err(createDatabaseError("Failed to save entity", "insert", originalError));
Convenience Functions
// Forbidden access (specialized business logic error)
export function createForbiddenError(message: string): DomainError {
return createBusinessLogicError(message, "FORBIDDEN");
}
// Convert unknown errors to business logic errors
export function toBusinessLogicError(
error: unknown,
fallbackMessage: string,
code: string = "BUSINESS_LOGIC_ERROR",
): DomainError {
if (error instanceof Error) {
return createBusinessLogicError(error.message, code);
}
return createBusinessLogicError(fallbackMessage, code);
}
Structured Logging
Location: @shared/logger.ts
Provides structured logging compatible with Cloudflare Workers with contextual information and JSON formatting.
Logger Creation
import { createLogger } from "@shared/logger";
// Create service-specific logger
const accountingLogger = createLogger({
service: "accounting",
version: "1.0.0",
});
// Create operation-specific logger
const eodLogger = accountingLogger.child({
operation: "end-of-day",
fundId: "fund-123",
});
Logging Methods
// All methods accept (message, data) or (data, message) patterns
logger.info("Processing transaction", {
transactionId: "tx-123",
amount: 1000,
});
logger.info(
{ transactionId: "tx-123", amount: 1000 },
"Processing transaction",
);
logger.warn("Deprecated API usage", { endpoint: "/old-api", version: "2.0" });
logger.error("Database connection failed", { error: err.message, retries: 3 });
// Debug logs (output as info level but tagged for filtering)
logger.debug("Processing batch", { batchId: "batch-456", itemCount: 100 });
Log Output Format
All logs are output as JSON with consistent structure:
{
"level": "info",
"timestamp": "2024-01-15T10:30:00.000Z",
"service": "accounting",
"version": "1.0.0",
"transactionId": "tx-123",
"amount": 1000,
"message": "Processing transaction"
}
Financial Constants
Location: @shared/financial-constants.ts
Standardized financial constants and calculations used across the application.
Money Handling
// All monetary values are stored in minor units (cents)
const DOLLAR_TO_CENTS = 100;
const BDT_TO_POISHA = 100;
// Conversion utilities
export function toMinorUnits(amount: number, currency: string): number {
const multiplier = currency === "USD" ? DOLLAR_TO_CENTS : BDT_TO_POISHA;
return Math.round(amount * multiplier);
}
export function fromMinorUnits(minorUnits: number, currency: string): number {
const divisor = currency === "USD" ? DOLLAR_TO_CENTS : BDT_TO_POISHA;
return minorUnits / divisor;
}
Account Constants
// Standard account types and patterns
export const ACCOUNT_TYPES = {
ASSET: "asset",
LIABILITY: "liability",
EQUITY: "equity",
REVENUE: "revenue",
EXPENSE: "expense",
} as const;
// Account code ranges
export const ACCOUNT_CODE_RANGES = {
ASSETS: { min: 1000, max: 1999 },
LIABILITIES: { min: 2000, max: 2999 },
EQUITY: { min: 3000, max: 3999 },
REVENUE: { min: 4000, max: 4999 },
EXPENSES: { min: 5000, max: 5999 },
} as const;
ID Generation
Location: @shared/id.ts
Utilities for generating and validating unique identifiers.
import { generateId, isValidId } from "@shared/id";
// Generate various types of IDs
const fundId = generateId("fund"); // fund_abc123...
const transactionId = generateId("tx"); // tx_def456...
const userId = generateId("user"); // user_ghi789...
// Validate ID format
if (isValidId(fundId, "fund")) {
// Valid fund ID
}
Type Safety
Location: @shared/types/
Common type definitions and interfaces used across domains.
Result Types
import type { DomainError } from "@shared/domain-errors";
// Re-export neverthrow Result with DomainError
export type AppResult<T, E extends DomainError = DomainError> = Result<T, E>;
// Common result patterns
export type EntityResult<T> = AppResult<T>;
export type ServiceResult<T> = AppResult<T>;
export type ValidationResult<T> = AppResult<T, ValidationError>;
Integration Patterns
Service Composition
When creating services that use shared utilities:
import { createLogger } from "@shared/logger";
import type { DomainError } from "@shared/domain-errors";
import { AppResult } from "@shared/types";
export class AccountingService {
private logger = createLogger({ service: "accounting" });
createJournalEntry(
request: CreateJournalEntryRequest,
): AppResult<JournalEntry> {
this.logger.info("Creating journal entry", {
fundId: request.fundId,
description: request.description,
});
// Validation
if (request.lines.length === 0) {
return err({
type: "ValidationError",
message: "Journal entry must have at least one line",
});
}
// Business logic...
return ok(journalEntry);
}
}
Error Handling Patterns
// Repository pattern with Result types
interface Repository<T> {
findById(id: string): Promise<AppResult<T>>;
save(entity: T): Promise<AppResult<T>>;
delete(id: string): Promise<AppResult<void>>;
}
// Use case composition
async function processTransaction(
request: TransactionRequest,
): Promise<AppResult<Receipt>> {
// Validate input
const validated = await validateTransaction(request);
if (validated.isErr()) return validated;
// Check business rules
const businessRules = await checkBusinessRules(validated.value);
if (businessRules.isErr()) return businessRules;
// Persist transaction
const saved = await transactionRepository.save(validated.value);
if (saved.isErr()) return saved;
return ok(createReceipt(saved.value));
}
Testing Support
Error Testing
import {
validationError,
notFoundError,
formatError,
} from "@shared/domain-errors";
describe("Business Logic", () => {
it("should handle validation errors", () => {
const error = validationError("Amount must be positive", "amount");
expect(formatError(error)).toBe(
"Validation failed for amount: Amount must be positive",
);
});
it("should handle not found errors", () => {
const error = notFoundError("Investor", "inv-123");
expect(formatError(error)).toBe("Investor with ID inv-123 not found");
});
});
Logger Testing
import { createLogger } from "@shared/logger";
describe("Service", () => {
it("should log structured data", () => {
const mockConsole = {
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
};
const logger = createLogger({ service: "test" });
logger.info("Test message", { key: "value" });
expect(mockConsole.info).toHaveBeenCalledWith(
expect.stringContaining('"service":"test"'),
);
});
});
Best Practices
Error Handling
- Use specific error types: Choose the most specific DomainError type for each situation
- Provide context: Include relevant IDs, values, and business context in error data
- Format for users: Use
formatError()for user-facing messages - Never swallow errors: Always handle or propagate Result types explicitly
Logging
- Use structured data: Log objects instead of concatenated strings
- Include correlation IDs: Add request/transaction IDs to track operations
- Choose appropriate levels: Use debug for detailed tracing, info for operations, warn for issues, error for failures
- Avoid sensitive data: Don't log passwords, tokens, or PII
Constants
- Centralize financial logic: Use shared constants for all monetary calculations
- Minor units for storage: Store all monetary values in minor units to avoid floating-point issues
- Type-safe enums: Use const assertions for compile-time type safety
Migration Notes
The shared services represent the foundation of the neverthrow migration and error handling improvements introduced in Asset360 v3. When working with these services:
- Always return Result types from domain services
- Use domain errors instead of throwing exceptions
- Handle errors explicitly at service boundaries
- Use structured logging for better observability and debugging