Skip to main content

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:

  1. Register investor
  2. Purchase units
  3. Create holding with weighted average cost
  4. 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​

  1. Validation First: Validate all input before creating any resources
  2. Ordered Execution: Create resources in dependency order (fund → accounts → ledger)
  3. Error Context: Provide meaningful error messages with context
  4. Idempotency: Consider checking for existing resources
  5. 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​