Purchase Bond Use Case API
Orchestrates the complete bond purchase workflow including transaction recording and accounting journal entries. This use case handles the purchase of bond securities with proper accounting treatment for clean prices and accrued interest.
Status: 🔄 Not yet migrated to neverthrow - Currently uses exception-based error handling Target Migration: Phase 3 of neverthrow rollout
Location: worker/use-cases/portfolio/purchase-bond.use-case.ts
Dependencies
FundService– Validates fund existence and access permissionsBondPortfolioService– Records bond transaction and portfolio holdingsAccountingService– Creates journal entries for bond purchase accounting@shared/account-constants– Standard account names and codes for bond accounting
Request & Response
PurchaseBondRequest
interface PurchaseBondRequest {
fundId: string; // Fund making the purchase
bondId: string; // Bond being purchased
quantity: number; // Number of bonds to purchase (must be positive)
cleanAmountMinorUnits: number; // Clean price amount in minor units (excluding accrued interest)
accruedInterestMinorUnits: number; // Accrued interest in minor units
tradeDate: string; // Trade date (ISO string)
settlementDate: string; // Settlement date (ISO string)
notes?: string | null; // Optional transaction notes
createdBy?: string; // User ID creating this transaction
}
PurchaseBondResult
interface PurchaseBondResult {
transactionId: string; // Transaction ID for tracking
fundId: string; // Fund ID
bondId: string; // Bond ID
quantity: number; // Number of bonds purchased
cleanPriceMinorUnits: number; // Clean price per bond in minor units
accruedInterestMinorUnits: number; // Accrued interest per bond in minor units
dirtyPriceMinorUnits: number; // Total dirty price (clean + accrued)
totalCleanAmount: number; // Total clean amount for all bonds
totalDirtyAmount: number; // Total dirty amount for all bonds
}
Workflow
1. Input Validation
- Validate purchase quantity is positive
- Validate clean and accrued interest amounts are non-negative
- Validate trade and settlement dates
2. Business Logic Validation
- Verify fund exists and is accessible
- Check bond exists and is available for purchase
- Validate purchase amounts against fund limits
3. Transaction Recording
- Record bond transaction in portfolio
- Update bond holdings with purchased quantity
- Calculate and store accrued interest
4. Accounting Journal Entries
Creates the following journal entries for proper bond purchase accounting:
| Account | Type | Amount | Description |
|---|---|---|---|
| Investment in Bond Securities | Debit | Clean price amount | |
| Accrued Interest Receivable (Bond) | Debit | Accrued interest amount | |
| Cash | Credit | Total dirty price (clean + accrued) |
5. Result Generation
- Generate transaction confirmation with all purchase details
- Return complete purchase summary for audit trail
Usage Example
const purchaseBondUseCase = app.applicationServices.purchaseBond;
const request: PurchaseBondRequest = {
fundId: "fund-123",
bondId: "bond-456",
quantity: 100,
cleanAmountMinorUnits: 50000000, // 500,000 BDT in minor units
accruedInterestMinorUnits: 2500000, // 25,000 BDT in accrued interest
tradeDate: "2025-01-15T00:00:00.000Z",
settlementDate: "2025-01-18T00:00:00.000Z",
notes: "Regular bond purchase",
createdBy: "user-789",
};
try {
const result = await purchaseBondUseCase.execute(request);
console.log("Bond purchased successfully:", result.transactionId);
} catch (error) {
console.error("Bond purchase failed:", error.message);
}
Accounting Treatment
Bond Purchase Principles
- Clean Price: The price of the bond excluding accrued interest
- Accrued Interest: Interest that has accumulated since the last coupon date
- Dirty Price: Total price paid = Clean Price + Accrued Interest
Journal Entry Logic
// Example: Purchase 100 bonds at 500 BDT each with 25 BDT accrued interest
// Journal Entry Creation
const journalEntry = await accountingService.createJournalEntry({
fundId: request.fundId,
description: `Purchase ${request.quantity} bonds of bond ${request.bondId}`,
lines: [
{
accountId: investmentInBondSecuritiesAccount.id,
amount: 50000000, // Debit: 100 × 500 BDT = 50,000,000 minor units
description: "Bond purchase - clean price",
},
{
accountId: accruedInterestReceivableAccount.id,
amount: 2500000, // Debit: 100 × 25 BDT = 2,500,000 minor units
description: "Bond purchase - accrued interest",
},
{
accountId: cashAccount.id,
amount: -52500000, // Credit: Total dirty price = 52,500,000 minor units
description: "Bond purchase - cash payment",
},
],
createdBy: request.createdBy,
});
Error Handling
Current Exception-Based Errors
The use case currently throws exceptions for validation failures:
- Invalid Quantity:
Purchase quantity must be positive - Invalid Amount:
Clean amount must be non-negative - Fund Not Found: When fund doesn't exist or user lacks access
- Bond Not Found: When bond doesn't exist
- Insufficient Funds: When fund lacks cash for purchase
Migration Target (Phase 3)
When migrated to neverthrow, errors will be structured:
export type PurchaseBondError =
| ValidationError
| NotFoundError
| InsufficientFundsError
| BusinessRuleError;
export interface ValidationError {
type: "ValidationError";
message: string;
field?: string;
value?: any;
}
export interface InsufficientFundsError {
type: "InsufficientFundsError";
available: number;
required: number;
accountId: string;
}
Integration Points
With Bond Portfolio Service
// Record transaction in portfolio
const transaction = await bondPortfolioService.recordTransaction({
fundId: request.fundId,
bondId: request.bondId,
quantity: request.quantity,
cleanPriceMinorUnits: request.cleanAmountMinorUnits / request.quantity,
tradeDate: request.tradeDate,
settlementDate: request.settlementDate,
notes: request.notes,
});
With Accounting Service
// Get required accounts
const accounts = await accountingService.getAccountsByFundId(request.fundId);
const investmentAccount = accounts.find(
(a) => a.name === STANDARD_ACCOUNTS.BOND.securities,
);
const interestAccount = accounts.find(
(a) => a.name === STANDARD_ACCOUNTS.BOND.interest,
);
const cashAccount = accounts.find((a) => a.name === STANDARD_ACCOUNTS.CASH);
Best Practices
Transaction Validation
- Always validate quantity is positive (purchases only)
- Verify clean and accrued interest are non-negative
- Ensure trade date is before or equal to settlement date
- Check bond availability and market access
Accounting Accuracy
- Use clean price excluding accrued interest
- Properly handle accrued interest for bond purchases
- Ensure journal entries balance (debits = credits)
- Include descriptive journal entry descriptions
Error Handling (Post-Migration)
- Use specific error types for different failure scenarios
- Include relevant context in error objects
- Provide actionable error messages for users
- Log all failed attempts for audit purposes
Testing Considerations
Test Scenarios
- Valid Purchase: Successful bond purchase with proper accounting
- Invalid Quantity: Reject negative or zero quantity purchases
- Invalid Amount: Reject negative clean or accrued interest amounts
- Missing Fund: Handle fund not found or access denied
- Missing Bond: Handle bond not found or unavailable
- Insufficient Funds: Handle fund cash balance insufficient
Test Data Patterns
// Valid purchase request
const validRequest: PurchaseBondRequest = {
fundId: "fund-123",
bondId: "bond-456",
quantity: 100,
cleanAmountMinorUnits: 50000000,
accruedInterestMinorUnits: 2500000,
tradeDate: "2025-01-15T00:00:00.000Z",
settlementDate: "2025-01-18T00:00:00.000Z",
};
// Invalid quantity (should throw error)
const invalidQuantityRequest = {
...validRequest,
quantity: -10,
};
// Invalid amount (should throw error)
const invalidAmountRequest = {
...validRequest,
cleanAmountMinorUnits: -5000,
};
Migration Notes
Current State
- ✅ Implements complete bond purchase workflow
- ✅ Creates proper accounting journal entries
- ✅ Handles clean price and accrued interest correctly
- ❌ Uses exception-based error handling
- ❌ Not yet migrated to neverthrow patterns
Migration Plan
- Phase 3: Migrate to
Result<T, DomainError>patterns - Phase 3: Add custom PurchaseBondError types
- Phase 3: Update error handling to return Results
- Phase 3: Add comprehensive error context
- Phase 3: Update all calling code to handle Results
Breaking Changes
Once migrated:
- Method signature changes from
Promise<Result>toPromise<Result<T, DomainError>> - Exception throwing replaced with Result-based error returns
- Callers must handle
.isOk()/.isErr()pattern - Error objects will have structured data instead of just messages
Last Updated: 2025-01-11 - Documentation created for PurchaseBond use case Migration Status: 🔄 Not yet migrated to neverthrow (scheduled for Phase 3)