Skip to main content

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

  1. Use specific error types: Choose the most specific DomainError type for each situation
  2. Provide context: Include relevant IDs, values, and business context in error data
  3. Format for users: Use formatError() for user-facing messages
  4. Never swallow errors: Always handle or propagate Result types explicitly

Logging

  1. Use structured data: Log objects instead of concatenated strings
  2. Include correlation IDs: Add request/transaction IDs to track operations
  3. Choose appropriate levels: Use debug for detailed tracing, info for operations, warn for issues, error for failures
  4. Avoid sensitive data: Don't log passwords, tokens, or PII

Constants

  1. Centralize financial logic: Use shared constants for all monetary calculations
  2. Minor units for storage: Store all monetary values in minor units to avoid floating-point issues
  3. 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:

  1. Always return Result types from domain services
  2. Use domain errors instead of throwing exceptions
  3. Handle errors explicitly at service boundaries
  4. Use structured logging for better observability and debugging