Skip to main content

Investor Service API

The Investor Service manages investor identities, holdings, and unit transactions. Core read paths have been migrated to return Result objects; transactional helpers still expose legacy promise interfaces until the neverthrow migration completes.

Location: worker/services/investor/service.ts

Dependencies

  • InvestorRepository – persistence for investors, holdings, and transactions
  • InvestorHoldingAggregate – domain aggregate that enforces holding invariants
  • Shared utilities – createId, formatError, money/unit validation helpers

Backward Compatibility Wrappers

getInvestorByIdLegacy(id: InvestorId): Promise<Investor> (deprecated) listAllInvestorsLegacy(options): Promise<{ items: Investor[]; total: number }> (deprecated) getInvestorHoldingsLegacy(investorId: InvestorId): Promise<InvestorHolding[]> (deprecated) : These wrappers call the new Result-based APIs and throw on errors to keep legacy coordinators working. Prefer the Result-returning methods below when writing new code.

Registration & Profile Management

registerInvestor(command: RegisterInvestorCommand, organizationId: OrganizationId | number): Promise<{ investorId: string }> : Creates an investor record with normalized IDs and timestamps. Returns the generated investor ID. Throws on repository failure.

getInvestorById(id: InvestorId): Promise<Result<Investor, DomainError>> : Returns NotFoundError when the investor is missing and BusinessLogicError for repository failures.

updateInvestor(id: InvestorId, command: UpdateInvestorCommand): Promise<Investor> (legacy) : Persists partial updates and returns the investor through the legacy wrapper, throwing on errors or missing investors.

listInvestors(organizationId: OrganizationId | number, limit = 100, offset = 0): Promise<{ items: Investor[]; total: number }> : Paginates investors within an organization. Throws on repository errors.

listAllInvestors(options: { limit: number; offset: number; search?: string }): Promise<Result<{ items: Investor[]; total: number }, DomainError>> : System-level listing that returns BusinessLogicError when repository access fails.

upsertInvestorForMigration(command: UpsertInvestorForMigrationCommand): Promise<void> (legacy) : Supports data migrations with strict validation. Throws descriptive errors when invariants fail (organization mismatch, invalid holdings, bad dates).

Holdings & Transactions

getInvestorHoldings(investorId: InvestorId): Promise<Result<InvestorHolding[], DomainError>> : Returns all holdings for the investor. Repository failures map to BusinessLogicError.

getHoldingsByFund(fundId: string): Promise<InvestorHolding[]> : Legacy promise that forwards directly to the repository.

listInvestorsByFund(params): Promise<{ items: { investor: Investor; holding: InvestorHolding }[]; total: number }> : Lists investors with holdings in a fund. Throws on repository errors.

getInvestorTransactions(params, options?): Promise<{ transactions: InvestorTransactionRecord[]; pagination: Pagination }> (legacy) : Enforces organization access and throws when the investor is missing or in a different organization.

Unit Transactions

purchaseUnits(command: PurchaseUnitsCommand): Promise<{ transactionId: string }> (legacy) : Uses InvestorHoldingAggregate to enforce invariants, validates money inputs, and throws descriptive Errors when validation fails or when organization mismatches occur.

Business Rule: All unit purchase transactions can only be recorded on the calendar day after the last EOD date. If EOD was done for 2025-07-01, transactions can only be recorded for 2025-07-02. This validation is enforced automatically and throws a BusinessRuleViolation error if the transaction date is not allowed.

redeemUnits(command: RedeemUnitsCommand): Promise<{ transactionId: string }> (legacy) : Validates holdings and NAV input, throwing when insufficient units exist or when pricing data is invalid.

Business Rule: All unit redemption transactions can only be recorded on the calendar day after the last EOD date. If EOD was done for 2025-07-01, transactions can only be recorded for 2025-07-02. This validation is enforced automatically and throws a BusinessRuleViolation error if the transaction date is not allowed.

Domain Errors

Result-returning methods leverage inline error construction:

  • createNotFoundError("Investor", id) – missing investors
  • Inline BusinessLogicError objects – repository/processing failures
  • Legacy transactional flows call formatError on aggregate errors before throwing

Usage Example

const investorResult = await services.investor.getInvestorById(investorId);
const investor = investorResult.match(
(value) => value,
(error) => {
if (error.type === "NotFoundError") {
throw new TRPCError({ code: "NOT_FOUND", message: error.message });
}
throw new TRPCError({ code: "BAD_REQUEST", message: error.message });
},
);
// Legacy purchase flow (until migration completes)
try {
const { transactionId } = await services.investor.purchaseUnits(command);
return { success: true, transactionId };
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: error instanceof Error ? error.message : "Purchase failed",
});
}

Migration Notes

  • Transactional helpers (purchaseUnits, redeemUnits, getInvestorTransactions) remain on the legacy promise interface and throw exceptions. Wrap them carefully in coordinators and routers.
  • Once Phase 2 of the neverthrow migration lands, these methods will return Result objects and legacy wrappers will be removed.

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