Fund Setup Use Case
Orchestrates the complete initialization of a new investment fund, including bank accounts, chart of accounts, and optional initial investors. This use case is partially migrated to neverthrow - it consumes Result types from domain services but still throws exceptions for validation failures.
Status: 🔄 Partially migrated to neverthrow - Consumes Result<T, DomainError> from services but throws exceptions for use case validation
Migration Status: Mixed pattern - service calls use Results but main execute() method still throws exceptions
Location: worker/use-cases/fund/setup-fund.use-case.ts
Dependencies​
export interface SetupFundDependencies {
fundService: FundService;
bankAccountService: BankAccountService;
investorService: InvestorService;
accountingService: IAccountingService;
/** Application service to orchestrate unit purchases for consistency */
purchaseUnitsUseCase: PurchaseUnitsUseCase;
}
Workflow Diagram​
Request & Response​
SetupFundRequest​
export interface SetupFundRequest {
organizationId: string;
fund: {
name: string;
code: string;
fundType?: string;
baseCurrency?: string;
};
bankAccounts: SetupFundBankAccountRequest[];
effectiveDate?: string;
initialInvestors?: InitialInvestorContribution[];
}
export interface SetupFundBankAccountRequest {
bank: string;
accountNumber: string;
routingNumber: string;
accountName: string;
branchName?: string;
interestRate?: number;
minimumBalanceAmount?: number;
}
// All fund bank accounts are provisioned as current BDT accounts and must
// provide a routing number directly from the bank.
export interface InitialInvestorContribution {
investor: {
fullName: string;
email: string;
mobilePhone: string;
};
units: number;
navPerUnit: InvestorMoney;
contributionDate?: string;
}
SetupFundResult​
export interface SetupFundResult {
fund: Fund;
bankAccounts: BankAccountEntity[];
processedInvestors?: Array<{
investor: Investor;
holding: InvestorHolding;
transaction: { transactionId: string };
}>;
}
Current Error Handling Pattern​
The use case currently follows a mixed pattern - consuming Result types from services while throwing exceptions for use case-level validation:
async execute(request: SetupFundRequest): Promise<SetupFundResult> {
// Use case validation still throws exceptions
if (!request.bankAccounts || request.bankAccounts.length === 0) {
throw new Error("At least one bank account is required for fund setup");
}
// Services are called with Result handling
const fundResult = await this.fundService.createFund({...});
if (fundResult.isErr()) {
throw new Error(`Fund creation failed: ${fundResult.error.message}`);
}
const fund = fundResult._unsafeUnwrap(); // Unsafe unwrap after error check
// ... more processing
return result;
}
Process Steps​
1. Validation​
async setupFund(request: FundSetupRequest): Promise<FundSetupResult> {
if (!request.bankAccounts || request.bankAccounts.length === 0) {
throw new Error("At least one bank account is required for fund setup");
}
// ...
}
2. Fund Creation​
const fund = await this.fundService.createFund({
organizationId: request.organizationId,
name: request.fund.name,
code: request.fund.code,
startDate: request.effectiveDate ?? new Date(),
});
3. Bank Account Creation​
const createdAccounts: BankAccountEntity[] = [];
for (const account of request.bankAccounts) {
const bankAccount = await this.bankAccountService.createBankAccount({
accountNumber: account.accountNumber,
bankName: account.bank,
routingNumber: account.routingNumber,
accountName: account.accountName,
accountType: "current",
currency: "BDT",
fundId: fund.id,
interestRate: account.interestRate ?? 0,
minimumBalance: account.minimumBalanceAmount ?? 0,
});
createdAccounts.push(bankAccount);
}
// Every fund bank account is provisioned as a Bangladeshi Taka current account.
4. Chart of Accounts Setup​
The use case creates a complete chart of accounts with standard categories:
Asset Accounts​
- Issue & Formation
- Accrued Interest Receivable (Bank, FDR, T.Bond, Bond)
- Investment in Equity Securities
- Investment in T.Bonds
- Investment in Open-end MF
- Dividend Receivable
- Advance BSEC Fee
- Bank Account - {account number} (for each bank account)
Liability Accounts​
- Payable accounts (Management Fee, Trustee Fee, Custodian Fee, etc.)
- Payable - Unit Redemption
- Accrued Interest Payable (Bonds)
Equity Accounts​
- Unit Capital
- Unit Premium
- Retained Earnings
Revenue Accounts​
- Capital Gains/Losses on securities
- Dividend Income
- Bank Interest Income
- T.Bond Interest Income
- FDR Interest Income
- Unrealized Gain/Loss on Equity Securities
- Unrealized Gain/Loss on Bond Securities
Expense Accounts​
- Amortization - Issue & Formation Expenses
- Bank Charges
- Management Fee
- Trustee Fee
- Custodian Fee
- BO A/C Maintenance Fee
- Publication Expense
- CDBL Connectivity Fee
- CDBL - CDS Charge
- ESS Registration Fee
- Excise Duty
- Audit Fees
private async ensureFundAccounts(
fundId: number,
bankAccounts: FundSetupBankAccountRequest[],
): Promise<void> {
const definitions: Array<{ type: AccountType; names: string[] }> = [
{
type: "revenue",
names: [
"Capital Gains/Losses on securities",
"Dividend Income",
"Bank Interest Income",
// ... more accounts
],
},
// ... other account types
];
for (const { type, names } of definitions) {
for (const name of names) {
await this.accountingService.createAccount({
fundId,
name,
type,
code: `${type.slice(0, 3).toUpperCase()}-${counter}`,
});
}
}
}
5. Default Fee Configuration​
// Set up Bangladesh fee package
const defaultFees = this.fundService.createDefaultBangladeshFees();
await this.fundService.updateFundFees(fund.id, defaultFees);
Default fees include:
- Management Fee: Tiered structure (2.5% → 2.0% → 1.5% → 1.0%)
- Trustee Fee: 0.5%
- Custodian Fee: 0.25%
- CDBL Fee: BDT 50,000 annually
- BSEC Fees: 0.1% registration + 0.05% NAV
- Formation Fee: BDT 5M amortized over 5 years
6. Initial Investor Processing (Optional)​
if (request.initialInvestors && request.initialInvestors.length > 0) {
processedInvestors = await this.processInitialInvestors(
fund.id,
request.organizationId,
request.initialInvestors,
);
}
For each initial investor:
- Register investor
- Purchase units
- Create holding with weighted average cost
- Record transaction
Usage Example​
const result = await fundSetupUseCase.setupFund({
organizationId: "org-1",
fund: {
name: "Bangladesh Growth Fund",
code: "BGF01",
},
bankAccounts: [
{
bank: "Prime Bank",
accountNumber: "1234567890",
routingNumber: "PRIME-ROUTE-001",
accountName: "Fund Operating Account",
branchName: "Gulshan Branch",
interestRate: 3.5,
minimumBalanceAmount: 10000000,
},
{
bank: "Prime Bank",
accountNumber: "0987654321",
routingNumber: "PRIME-ROUTE-002",
accountName: "Fund Settlement Account",
branchName: "Gulshan Branch",
interestRate: 3.5,
},
],
effectiveDate: new Date("2024-01-01"),
initialInvestors: [
{
investor: {
fullName: "John Doe",
email: "[email protected]",
mobilePhone: "+8801712345678",
},
units: 10000,
navPerUnit: { amount: 1000, currency: "BDT" }, // 10 BDT per unit
},
],
});
logger.info("Fund setup completed", {
fundId: result.fund.id,
bankAccountCount: result.bankAccounts.length,
investorCount: result.processedInvestors?.length ?? 0,
});
Error Handling​
The use case handles errors at each step:
try {
const fund = await this.fundService.createFund({...});
try {
// Create bank accounts
for (const account of request.bankAccounts) {
const bankAccount = await this.bankAccountService.createBankAccount({...});
createdAccounts.push(bankAccount);
}
} catch (error) {
// If bank account creation fails, fund is already created
console.error('Bank account creation failed:', error);
throw new Error(`Failed to create bank accounts for fund ${fund.id}: ${error.message}`);
}
} catch (error) {
throw new Error(`Fund setup failed: ${error.message}`);
}
Testing​
Unit Test​
describe('FundSetupUseCase', () => {
it('creates fund with bank accounts', async () => {
const mockFundService = {
createFund: vi.fn().mockResolvedValue({ id: 1, name: 'Test Fund' }),
updateFundFees: vi.fn(),
createDefaultBangladeshFees: vi.fn().mockReturnValue({}),
};
const mockBankService = {
createBankAccount: vi.fn().mockResolvedValue({ id: '1', fundId: 1 }),
};
const use case = new FundSetupUseCase({
fundService: mockFundService as any,
bankAccountService: mockBankService as any,
investorService: {} as any,
accountingService: { createAccount: vi.fn() } as any,
});
await use case.setupFund({
organizationId: 'org-1',
fund: { name: 'Test', code: 'TEST' },
bankAccounts: [
{
bank: 'Test Bank',
accountNumber: '123',
routingNumber: 'ROUTE-123',
accountName: 'Operating Account',
},
],
});
expect(mockFundService.createFund).toHaveBeenCalledOnce();
expect(mockBankService.createBankAccount).toHaveBeenCalledOnce();
});
});
Integration Test​
describe('FundSetupUseCase Integration', () => {
it('sets up complete fund', async () => {
const services = createDomainServices({ db: await getTestDB(), env: {} as Env });
const use cases = createApplicationServices(services);
const org = await services.organization.createOrganization({
name: 'Test Org',
slug: 'test',
});
const result = await use cases.fundSetup.setupFund({
organizationId: org.id,
fund: { name: 'Test Fund', code: 'TEST01' },
bankAccounts: [
{
bank: 'Test',
accountNumber: '123',
routingNumber: 'ROUTE-123',
accountName: 'Operating Account',
},
],
});
expect(result.fund.id).toBeDefined();
expect(result.bankAccounts).toHaveLength(1);
// Verify chart of accounts was created
const accounts = await services.accounting.getAccountsByFundId(result.fund.id);
expect(accounts.length).toBeGreaterThan(30); // Should have full chart
});
});
Best Practices​
- Validation First: Validate all input before creating any resources
- Ordered Execution: Create resources in dependency order (fund → accounts → ledger)
- Error Context: Provide meaningful error messages with context
- Idempotency: Consider checking for existing resources
- Complete Setup: Ensure all required accounts are created
Common Issues​
Issue: Bank account creation fails​
Cause: Fund ID doesn't exist or invalid account number
Solution: Ensure fund is created before bank accounts, validate input
Issue: Chart of accounts incomplete​
Cause: Account creation failed partway through
Solution: Check account type enums, ensure valid account codes
Issue: Initial investor setup fails​
Cause: Invalid NAV or investor data
Solution: Validate investor data before processing
See Also​
- Fund Service API - Fund creation details
- Bank Account Service API - Bank account operations
- Investor Service API - Investor registration
- [Unit Purchase UseCase](/use cases/unit-purchase) - Related workflow