Skip to main content

API Reference Introduction

This section documents the domain services that power Asset360 v3. The backend has fully adopted neverthrow for error handling in core flows, so service APIs now expose typed Result<Success, DomainError> responses instead of throwing exceptions. Supporting methods that still return plain promises are explicitly called out.

Service Categories

Core Services

Core services own the primary business operations inside their domains:

Portfolio Services

Specialized services for investment portfolios and pricing:

Documentation Structure

Each service page now includes:

  1. Overview – Responsibility and context
  2. Dependencies – Required repositories/services
  3. Public Methods – Signatures grouped by capability with Result semantics
  4. Domain Errors – Error variants a consumer should handle
  5. Usage Examples – Typical orchestration patterns
  6. Legacy Notes – Methods that still return plain promises (ongoing migration)

Core Patterns

Service Structure with Result

import { ok, err, type Result } from "neverthrow";
import {
createNotFoundError,
toBusinessLogicError,
} from "@worker/services/shared/domain-errors";
import type { DomainError } from "@worker/trpc/utils/error-mapping";

export class ServiceName {
constructor(private readonly repository: RepositoryName) {}

async findById(id: string): Promise<Result<Entity, DomainError>> {
try {
const entity = await this.repository.findById(id);
if (!entity) {
return err(createNotFoundError("Entity", id));
}
return ok(entity);
} catch (error) {
return err(toBusinessLogicError(error, "Failed to fetch entity"));
}
}
}

neverthrow gives exhaustive handling via result.match(...), .map(...), .andThen(...), and keeps error types explicit.

DomainError Model

All domain errors implement the interfaces in @worker/trpc/utils/error-mapping:

  • ValidationError – Invalid input (field, optional value)
  • NotFoundError – Missing resource (resource, id)
  • AuthorizationError – Access denied (reason)
  • BusinessLogicError – Business rule violations (code for machine handling)
  • Coordinator-specific variants (for example PortfolioValuationError) extend DomainError

Shared constructors in worker/services/shared/domain-errors.ts keep error creation consistent across services.

Consuming Services in tRPC Routers

Use executeResult() to bridge Result values to tRPC responses:

import { executeResult } from "@worker/trpc/utils/error-mapping";

export const organizationRouter = router({
getBySlug: publicProcedure
.input(z.object({ slug: z.string() }))
.query(async ({ input, ctx }) => {
return executeResult(
ctx.domainServices.organization.getOrganizationBySlug(input.slug),
);
}),
});

executeResult unwraps the Result and rethrows mapped TRPCErrors with the appropriate HTTP status codes. For procedures that still call legacy promise-based methods, keep defensive try/catch blocks until the migration finishes.

React Integration with TanStack Query

import { useTRPC } from "@/lib/trpc";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";

export function FundList() {
const trpc = useTRPC();
const queryClient = useQueryClient();

const listQuery = trpc.org.funds.list.queryOptions({ orgSlug: "demo-org" });
const { data } = useQuery(listQuery);

const createMutation = trpc.org.funds.create.mutationOptions();
const { mutate: createFund } = useMutation(createMutation, {
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: trpc.org.funds.list.queryKey(),
});
},
});

// …render data + mutation handlers…
}

Testing Result-Based Services

Prefer explicit assertions over thrown-error checks:

it("returns validation error for duplicate code", async () => {
const result = await services.fund.createFund({
name: "Alpha",
code: "ALPHA",
organizationId: "org-1",
});

expect(result.isErr()).toBe(true);
expect(result._unsafeUnwrapErr().type).toBe("ValidationError");
});

Helpers in worker/__tests__/support/helpers/result.ts provide ergonomic matchers to keep tests tidy.

Working with Legacy Methods

Some orchestration flows (for example, complex unit purchase workstreams and certain migration helpers) still return plain promises and throw on failure. These sections are annotated in the individual service docs under Legacy Notes and will be migrated in Phase 2 of the neverthrow plan. When consuming those methods:

  • Keep try/catch with toBusinessLogicError() or custom error formatting
  • Never swallow errors silently—rethrow or map them to DomainError
  • Prefer wrapping legacy calls inside new Result-based helpers when adding features

Null and Optional Returns

Query helpers that are intentionally nullable (for example FundService.getFundByCode, FundService.getLatestSnapshot) continue to return Promise<T | null>. Handle nullability explicitly in the caller even when Results are used elsewhere.

Using Services via the Factory

Services are still produced through createDomainServices:

import { createDomainServices } from "@worker/services";

const { fund, organization } = await createDomainServices({ db, env });

const fundResult = await fund.createFund({
name: "Growth Fund",
code: "GR01",
organizationId: "org-1",
startDate: new Date().toISOString(),
});

const fundEntity = await fundResult.unwrap(); // throws mapped TRPC error if failure

For tests, use the helpers under worker/__tests__/support to spin up isolated service instances with predictable fixtures.

Last Validated: 2025-11-10 against commit 0342a62e