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 transactionsInvestorHoldingAggregate– 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
BusinessLogicErrorobjects – repository/processing failures - Legacy transactional flows call
formatErroron 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
Resultobjects and legacy wrappers will be removed.
Last Validated: 2025-11-10 against commit
0342a62e