Skip to main content

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 permissions
  • BondPortfolioService – Records bond transaction and portfolio holdings
  • AccountingService – 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:

AccountTypeAmountDescription
Investment in Bond SecuritiesDebitClean price amount
Accrued Interest Receivable (Bond)DebitAccrued interest amount
CashCreditTotal 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

  1. Clean Price: The price of the bond excluding accrued interest
  2. Accrued Interest: Interest that has accumulated since the last coupon date
  3. 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

  1. Valid Purchase: Successful bond purchase with proper accounting
  2. Invalid Quantity: Reject negative or zero quantity purchases
  3. Invalid Amount: Reject negative clean or accrued interest amounts
  4. Missing Fund: Handle fund not found or access denied
  5. Missing Bond: Handle bond not found or unavailable
  6. 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

  1. Phase 3: Migrate to Result<T, DomainError> patterns
  2. Phase 3: Add custom PurchaseBondError types
  3. Phase 3: Update error handling to return Results
  4. Phase 3: Add comprehensive error context
  5. Phase 3: Update all calling code to handle Results

Breaking Changes

Once migrated:

  • Method signature changes from Promise<Result> to Promise<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)