Generated file. Source:
docs/DOR_logic.mdEdit the source document and runnpm run docs:syncto refresh this published copy.
DOR Logic¶
This file documents the current DOR calculation engine implementation.
Document Metadata¶
- Document status: handoff / implementation specification
- Scope: current
DORengine and the shared monthly-income strategy family it uses - Primary code references:
monthly-income.ts: src/app/domain/bond-engine/strategies/monthly-income.tsdor.ts: src/app/domain/bond-engine/strategies/dor.ts- Related tests:
monthly-income.strategy.test.ts: tests/monthly-income.strategy.test.tsdor.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
DORis 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
DORmonthly 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:
RORspecialization 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 PLNper 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.tsDORspecialization: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
DORandRORshare 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-dorcycleLengthMonths = 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:
- For every active batch, calculate monthly interest.
- Use the batch age to determine the month inside the 24-month cycle.
- Apply the
DORrate rule: - first month in cycle ->
4.40% - later months ->
referenceRate + 0.15% - Add net interest to cash after tax.
- Redeem any batch that reached age
24. - Reinvest available cash into full new
DORbonds if the profitability guard approves it. - If this is the final month and active batches remain, early redeem them by returning principal minus
3 PLNper bond. - 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 PLNper 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-monthmaturity 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 PLNearly redemption cost, reinvestment is blocked
Why:
DORpays 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.purchaseEventssimulationDetails.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-dorsimulationDetails.purchaseEventssimulationDetails.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-incomenpm run test:dor
Remaining Assumptions¶
The DOR implementation is strongly tested, but these still depend on broader project conventions:
- the exact
approve/blockboundary is sensitive to the implemented0.001 PLNinternal rounding policy - yearly
netValuesemantics 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.tsdor.ts: src/app/domain/bond-engine/strategies/dor.tsmonthly-income.strategy.test.ts: tests/monthly-income.strategy.test.tsdor.strategy.test.ts: tests/dor.strategy.test.ts- DOR_logic.md
init_prompt.md: ai/init_prompt.mdif product rules change