Skip to main content

Fund Service API

The Fund Service manages the lifecycle of funds, fee configuration, and end-of-day (EOD) snapshot management. Core operations now return Result<*, DomainError> values built with neverthrow and the shared domain error helpers.

Location: worker/services/fund/service.ts

Dependencies

  • FundRepository – persistence for funds, fees, transactions, and snapshots
  • Shared financial helpers – applyBpsToMinor, MONTHS_PER_YEAR
  • Shared domain error builders – createNotFoundError, createValidationError, toBusinessLogicError

Lifecycle & Lookup

createFund(data: CreateFundRequest): Promise<Result<Fund, DomainError>> : Validates against createFundSchema, enforces unique codes, seeds default fees when omitted, and returns the created fund. Errors surface as ValidationError (code) or BusinessLogicError for repository failures.

getFundById(id: string): Promise<Result<Fund, DomainError>> : Parses IDs with fundIdSchema, returns NotFoundError when missing, and maps unexpected errors to BusinessLogicError.

getFundByCode(code: string): Promise<Fund | null> : Schema-validated lookup used for internal workflows; returns null when not found.

getFundByCodeAndOrganization(code: string, organizationId: string): Promise<Result<Fund, DomainError>> : Verifies both identifiers, returning NotFoundError when the fund is absent or belongs to another organization. Validation issues surface as ValidationError for code or organizationId.

updateFund(id: string, data: UpdateFundRequest): Promise<Result<Fund, DomainError>> : Performs guarded partial updates with uniqueness validation when the code changes. Returns NotFoundError if the fund is missing.

deleteFund(id: string): Promise<void> (legacy) : Throws when the fund does not exist. Consumers must keep try/catch until this method is migrated to Result.

getFundsByOrganization(organizationId: string): Promise<Result<Fund[], DomainError>> : Lists funds for an organization, returning BusinessLogicError on repository failures.

getActiveFunds(): Promise<Fund[]> : Returns every active fund. Does not wrap errors yet—callers should guard with try/catch if needed.

getFundTransactions(params, options?): Promise<{ transactions: FundTransactionRecord[]; pagination: Pagination }> (legacy) : Still throws Error for missing funds or org mismatches. Wrap with try/catch when calling from routers/use cases.

createDefaultFund(organizationId: string): Promise<Result<Fund, DomainError>> : Convenience around createFund that derives an opinionated name/code combination.

seedDefaultFund(organizationId: string): Promise<Result<Fund, DomainError> | null> : Creates a default fund only when the organization has none. Returns null when a seed is unnecessary.

Fee Management

updateFundFees(fundId: string, fees: FundFeesJson): Promise<Result<Fund, DomainError>> : Delegates to updateFund and surfaces validation errors from uniqueness checks.

getFundFees(fundId: string): Promise<Result<FundFeesJson, DomainError> | null> : Reuses getFundById. Returns default Bangladesh fees when none are stored.

calculateManagementFee(fundId: string, navAmount: number): Promise<number> : Ensures fees exist (throws via getFeesOrThrow) and calculates tiered or flat management fees in minor units.

calculateTrusteeFee(fundId: string, totalAssets: number): Promise<number> calculateCustodianFee(fundId: string, totalAssets: number): Promise<number> calculateCDBLFee(fundId: string, months: number = MONTHS_PER_YEAR): Promise<number> calculateBSECFee(fundId: string, navAmount: number, initialFundSize: number): Promise<number> calculateFormationFee(fundId: string, monthsPassed: number): Promise<number> : All rely on getFeesOrThrow, which throws if fees are unavailable. Handle exceptions or migrate callers to Results before removing the private helper.

calculateAllFees(...): Promise<CalculatedFeeBreakdown> : Runs the above calculations concurrently and returns a breakdown in minor units.

createDefaultBangladeshFees(): FundFeesJson : Provides the tiered Bangladesh fee template used during onboarding or fallback scenarios.

Snapshot Management

getEODState(fundId: string): Promise<EODState> : Reads the persisted EOD state from the repository.

updateEODDate(fundId: string, businessDate: string): Promise<void> : Persists the next business date for the EOD processor.

createSnapshot(data: CreateFundSnapshotRequest): Promise<FundSnapshotEntity> getSnapshot(fundId: string, businessDate: string): Promise<FundSnapshotEntity | null> getLatestSnapshot(fundId: string): Promise<FundSnapshotEntity | null> : Straight pass-throughs to repository queries.

getSnapshotsByFund(fundId: string, page = 1, limit = 10): Promise<{ snapshots: FundSnapshotEntity[]; pagination: Pagination }> : Enforces pagination bounds and delegates to the repository for retrieval.

Domain Errors

Most Result-returning methods use the shared error helpers:

  • createValidationError when schema parsing or business rules fail (duplicate codes, invalid IDs)
  • createNotFoundError when a fund is missing
  • toBusinessLogicError for repository or system failures

See worker/services/shared/domain-errors.ts for constructors and extend the helpers when adding new error shapes.

Usage Examples

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

export const fundRouter = router({
create: publicProcedure
.input(createFundSchema)
.mutation(async ({ input, ctx }) => {
const fund = await executeResult(
ctx.domainServices.fund.createFund({
...input,
organizationId: ctx.user?.organizationId ?? "demo-org",
}),
);
return { success: true, fund };
}),

list: publicProcedure.input(listFundsInput).query(async ({ input, ctx }) => {
const organization = await executeResult(
ctx.domainServices.auth.getOrganizationBySlug(input.orgSlug),
);
const funds = await executeResult(
ctx.domainServices.fund.getFundsByOrganization(organization.id),
);
return paginateFunds(funds, input);
}),
});

Legacy Notes & Migration Status

  • deleteFund and getFundTransactions still throw exceptions. Wrap calls in try/catch until they are converted to return Result values.
  • Fee calculation helpers rely on getFeesOrThrow, which throws if fee configurations are missing. Callers should continue handling exceptions or migrate to a Result-based wrapper during Phase 2 of the migration.

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