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:
createValidationErrorwhen schema parsing or business rules fail (duplicate codes, invalid IDs)createNotFoundErrorwhen a fund is missingtoBusinessLogicErrorfor 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
deleteFundandgetFundTransactionsstill throw exceptions. Wrap calls intry/catchuntil they are converted to returnResultvalues.- 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