Skip to content

Generated file. Source: docs/DOR_logic.md Edit the source document and run npm run docs:sync to refresh this published copy.

DOR Logic

This file documents the current DOR calculation engine implementation.

Document Metadata

  • Document status: handoff / implementation specification
  • Scope: current DOR engine and the shared monthly-income strategy family it uses
  • Primary code references:
  • monthly-income.ts: src/app/domain/bond-engine/strategies/monthly-income.ts
  • dor.ts: src/app/domain/bond-engine/strategies/dor.ts
  • Related tests:
  • monthly-income.strategy.test.ts: tests/monthly-income.strategy.test.ts
  • dor.strategy.test.ts: tests/dor.strategy.test.ts
  • Related prompt source:
  • init_prompt.md: ai/init_prompt.md
  • Last reviewed against code: 2026-03-23

Purpose

This document is intended to let another programmer:

  • understand how DOR is modeled today
  • understand which logic is shared with ROR
  • modify the code without breaking monthly payout, reinvestment, or 24-month cycle behavior

Scope

Included in scope:

  • shared monthly-income engine behavior
  • DOR monthly rate rules
  • monthly interest payout and taxation
  • monthly reinvestment
  • reinvestment profitability guard near the end of the horizon
  • 24-month redemption cycle
  • final early redemption
  • monthly snapshots
  • yearly result derivation
  • purchase-event and reinvestment-decision history

Out of scope:

  • ROR specialization details
  • other bond families
  • UI rendering decisions

Product Definition

Current DOR engine assumptions:

  • single bond nominal value: 100 PLN
  • cycle length: 24 months
  • month 1 annualized rate: 4.40%
  • months 2-24 annualized rate: NBP + 0.15%
  • interest is calculated and paid every month
  • each monthly interest payment is taxed immediately at 19%
  • paid net interest goes to cash and can be reinvested immediately
  • reinvestment is allowed only when a newly bought bond is expected to be non-loss-making over the remaining horizon
  • natural redemption returns principal after 24 months
  • early redemption cost: 3 PLN per bond

Implemented rounding policy:

  • internal monetary values are normalized to 0.001 PLN
  • final summary outputs are rounded to 0.01 PLN

Architecture

DOR does not use the generic yearly calculator.

Instead it uses the shared monthly-income strategy family:

  • shared module:
  • monthly-income.ts: src/app/domain/bond-engine/strategies/monthly-income.ts
  • DOR specialization:
  • dor.ts: src/app/domain/bond-engine/strategies/dor.ts

Design intent:

  • common monthly payout mechanics live in one place
  • bond-specific rules stay small and explicit
  • DOR and ROR share one production family instead of duplicating logic
flowchart LR
    A[calculateBondResult] --> B{bondId}
    B -->|DOR| C[calculateDorBondResult]
    C --> D[simulateMonthlyIncomeBond]
    D --> E[Month snapshots]
    D --> F[Purchase events]
    D --> G[Reinvestment decisions]
    D --> H[Yearly results]
    D --> I[Final totals]

Shared Monthly-Income Contract

The shared monthly-income engine expects:

  • cycle length in months
  • monthly annual-rate resolver
  • standard bond definition
  • investment amount and horizon
  • reference rate and inflation inputs
  • tax rate and bond unit price

The shared engine provides:

  • monthly simulation
  • monthly interest posting
  • monthly tax posting
  • batch-based redemption
  • purchase-event ledger
  • reinvestment-decision ledger
  • yearly snapshots
  • final profitability summary

DOR Specialization

DOR configures the shared engine with:

  • strategyId = monthly-income-dor
  • cycleLengthMonths = 24
  • rate resolver:
  • cycleMonth === 1 -> 4.40%
  • otherwise -> referenceRate + 0.15%

So the DOR module contains only product-specific policy, while all monthly mechanics remain shared.

Monthly Flow

For each month m from 1 to totalMonths:

  1. For every active batch, calculate monthly interest.
  2. Use the batch age to determine the month inside the 24-month cycle.
  3. Apply the DOR rate rule:
  4. first month in cycle -> 4.40%
  5. later months -> referenceRate + 0.15%
  6. Add net interest to cash after tax.
  7. Redeem any batch that reached age 24.
  8. Reinvest available cash into full new DOR bonds if the profitability guard approves it.
  9. If this is the final month and active batches remain, early redeem them by returning principal minus 3 PLN per bond.
  10. Store end-of-month snapshot.
flowchart TD
    A[Start month] --> B[Calculate monthly interest for each active batch]
    B --> C[Post tax and net interest to cash]
    C --> D[Redeem batches aged 24 months]
    D --> E[Add redeemed principal to cash]
    E --> F{Profitability guard allows reinvestment?}
    F -- Yes --> G[Buy new batch and store purchase event]
    F -- No --> H[Store blocked reinvestment decision]
    G --> I{final month and active positions remain?}
    H --> I
    I -- Yes --> J[Apply early redemption to remaining batches]
    I -- No --> K[Build month snapshot]
    J --> K

Interest Formula

For one batch in one month:

grossInterest = bonds * 100 * (annualRatePercent / 100 / 12)
taxPaid = grossInterest * 0.19
netInterest = grossInterest - taxPaid
cash += netInterest

annualRatePercent depends on the batch month inside its 24-month cycle.

Redemption Rules

Natural redemption:

  • happens when ageInMonths === 24
  • only principal is returned at that moment, because monthly interest has already been paid during the cycle

Final early redemption:

  • happens only in the terminal month
  • applies to every still-active batch
  • adds principal to cash
  • subtracts 3 PLN per bond
  • does not add extra monthly interest beyond what the engine already paid during simulated months

Reinvestment Profitability Guard

DOR does not automatically buy every late bond candidate.

Current project policy:

  • if a newly purchased bond can still reach its natural 24-month maturity before the simulation ends, reinvestment is allowed
  • otherwise the engine estimates the net monthly interest that one new bond would earn until the simulation end
  • if that expected net interest is lower than the 3 PLN early redemption cost, reinvestment is blocked

Why:

  • DOR pays interest monthly, but a late purchase can still be net unprofitable because the fixed early redemption cost is large relative to a short remaining holding period

This means DOR differs from ROR only by:

  • cycle length
  • first-month rate
  • later-month margin above NBP

The profitability policy itself stays shared.

This policy is treated as a confirmed project business rule for this codebase, not only as a test assumption.

Purchase and Decision History

The shared engine stores:

  • simulationDetails.purchaseEvents
  • simulationDetails.reinvestmentDecisions

This data is already suitable for future reporting tables.

For DOR, important interpretation points are:

  • reinvestment can happen from monthly interest cash alone
  • naturally redeemed principal at month 24 can still be left in cash if the remaining horizon is too short for profitable reinvestment

Yearly Result Semantics

Yearly rows are created at months divisible by 12.

Current rule:

  • intermediate yearly grossValue = carried value + already realized taxes/costs
  • intermediate yearly netValue = carried portfolio value
  • final yearly netValue = liquidationValue

Why:

  • intermediate years should preserve both:
  • carried value after realized taxes
  • gross value before realized taxes/costs
  • final year should show actual realizable outcome

Public Result Contract

calculateDorBondResult(...) returns a normal BondCalculationResult.

In addition, it exposes strategy details through:

  • simulationDetails.strategy = monthly-income-dor
  • simulationDetails.purchaseEvents
  • simulationDetails.reinvestmentDecisions

Tested Coverage

Current automated DOR tests cover:

  • first-month special rate
  • switch to NBP + 0.15%
  • natural 24-month redemption
  • 1-month final early redemption
  • reinvestment when enough horizon remains
  • blocked late reinvestment when unprofitable
  • guard flip between 12 and 13 remaining months after month-24 decision
  • low-rate vs high-rate late-horizon guard behavior
  • dense month-24 boundary matrix across many remaining horizons and reference rates
  • month-24 redeemed-principal case where cash stays uninvested
  • gross vs liquidation invariants across a broad scenario grid
  • final-total consistency across a longer scenario grid
  • large-principal decision-history consistency
  • large-principal and long-horizon stress consistency
  • initial purchase-event recording
  • public engine routing through calculateBondResult
  • shared profitability-guard behavior for the 24-month strategy family

Test entry points:

  • npm run test:monthly-income
  • npm run test:dor

Remaining Assumptions

The DOR implementation is strongly tested, but these still depend on broader project conventions:

  • the exact approve/block boundary is sensitive to the implemented 0.001 PLN internal rounding policy
  • yearly netValue semantics follow the same carried-vs-final pattern used in the other strategies

Change Management

If DOR logic changes, update together:

  • monthly-income.ts: src/app/domain/bond-engine/strategies/monthly-income.ts
  • dor.ts: src/app/domain/bond-engine/strategies/dor.ts
  • monthly-income.strategy.test.ts: tests/monthly-income.strategy.test.ts
  • dor.strategy.test.ts: tests/dor.strategy.test.ts
  • DOR_logic.md
  • init_prompt.md: ai/init_prompt.md if product rules change