summaryrefslogtreecommitdiff
path: root/tests/FundLab.Domain.Tests/InvestmentPlanTests.fs
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-21 23:21:54 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-21 23:21:54 +0800
commitd4b539a2791a0cd4d098fecfece0039073a9f7e2 (patch)
treedfd314b37fa24379ef13dcd66a1de76e2a923bf2 /tests/FundLab.Domain.Tests/InvestmentPlanTests.fs
parent12c4d3625458c830a2746a1f277fb684e9c498cb (diff)
downloadfund-lab-d4b539a2791a0cd4d098fecfece0039073a9f7e2.tar.gz
Add scheduled investment plans (3d-10)
Diffstat (limited to 'tests/FundLab.Domain.Tests/InvestmentPlanTests.fs')
-rw-r--r--tests/FundLab.Domain.Tests/InvestmentPlanTests.fs81
1 files changed, 81 insertions, 0 deletions
diff --git a/tests/FundLab.Domain.Tests/InvestmentPlanTests.fs b/tests/FundLab.Domain.Tests/InvestmentPlanTests.fs
new file mode 100644
index 0000000..9a5e2b9
--- /dev/null
+++ b/tests/FundLab.Domain.Tests/InvestmentPlanTests.fs
@@ -0,0 +1,81 @@
+namespace FundLab.Domain.Tests
+
+module InvestmentPlanTests =
+
+ open System
+ open Xunit
+ open FundLab.Domain
+
+ let private anchor = DateOnly(2026, 9, 21)
+
+ [<Fact>]
+ let ``frequency parsing and text roundtrip`` () =
+ Assert.Equal(Some InvestmentFrequency.Daily, InvestmentPlanPolicy.parseFrequency "daily")
+ Assert.Equal(Some InvestmentFrequency.Weekly, InvestmentPlanPolicy.parseFrequency "weekly")
+ Assert.Equal(Some InvestmentFrequency.Monthly, InvestmentPlanPolicy.parseFrequency "monthly")
+ Assert.Equal(None, InvestmentPlanPolicy.parseFrequency "biweekly")
+ Assert.Equal(None, InvestmentPlanPolicy.parseFrequency null)
+ Assert.Equal("daily", InvestmentPlanPolicy.frequencyText InvestmentFrequency.Daily)
+ Assert.Equal("weekly", InvestmentPlanPolicy.frequencyText InvestmentFrequency.Weekly)
+ Assert.Equal("monthly", InvestmentPlanPolicy.frequencyText InvestmentFrequency.Monthly)
+
+ [<Fact>]
+ let ``amount validation requires positive two-decimal cash`` () =
+ Assert.Equal(Ok(), InvestmentPlanPolicy.validateAmount 200.00m)
+ Assert.Equal(Error "investment plan amount must be positive", InvestmentPlanPolicy.validateAmount 0m)
+ Assert.Equal(Error "investment plan amount must be positive", InvestmentPlanPolicy.validateAmount -1.00m)
+ Assert.Equal(Error "investment plan amount exceeds cash precision", InvestmentPlanPolicy.validateAmount 1.005m)
+
+ [<Fact>]
+ let ``daily schedule runs every weekday and skips weekends`` () =
+ Assert.Equal(DateOnly(2026, 9, 21), InvestmentPlanPolicy.nextRunDate InvestmentFrequency.Daily anchor anchor)
+ Assert.Equal(DateOnly(2026, 9, 22), InvestmentPlanPolicy.nextRunDate InvestmentFrequency.Daily anchor (DateOnly(2026, 9, 22)))
+ // Friday -> next scheduled run is Monday
+ Assert.Equal(DateOnly(2026, 9, 28), InvestmentPlanPolicy.nextRunDate InvestmentFrequency.Daily anchor (DateOnly(2026, 9, 26)))
+
+ let due =
+ InvestmentPlanPolicy.dueDates InvestmentFrequency.Daily (DateOnly(2026, 9, 25)) (DateOnly(2026, 9, 25)) (DateOnly(2026, 9, 28))
+
+ Assert.Equal<DateOnly list>([ DateOnly(2026, 9, 25); DateOnly(2026, 9, 28) ], due)
+
+ [<Fact>]
+ let ``weekly schedule keeps the seven day cadence`` () =
+ Assert.Equal(DateOnly(2026, 9, 21), InvestmentPlanPolicy.nextRunDate InvestmentFrequency.Weekly anchor anchor)
+ Assert.Equal(DateOnly(2026, 9, 28), InvestmentPlanPolicy.nextRunDate InvestmentFrequency.Weekly anchor (DateOnly(2026, 9, 22)))
+
+ let due =
+ InvestmentPlanPolicy.dueDates InvestmentFrequency.Weekly anchor anchor (DateOnly(2026, 10, 5))
+
+ Assert.Equal<DateOnly list>(
+ [ DateOnly(2026, 9, 21); DateOnly(2026, 9, 28); DateOnly(2026, 10, 5) ],
+ due
+ )
+
+ [<Fact>]
+ let ``monthly schedule clamps to month end and rolls off weekends`` () =
+ // 2026-02-15 is a Sunday, so the February run rolls to Monday 2026-02-16
+ Assert.Equal(DateOnly(2026, 2, 16), InvestmentPlanPolicy.nextRunDate InvestmentFrequency.Monthly (DateOnly(2026, 1, 15)) (DateOnly(2026, 2, 1)))
+
+ // 2026-02 has 28 days, so a 31st anchor clamps to the 28th then rolls to Monday 2026-03-02
+ Assert.Equal(DateOnly(2026, 3, 2), InvestmentPlanPolicy.nextRunDate InvestmentFrequency.Monthly (DateOnly(2026, 1, 31)) (DateOnly(2026, 2, 1)))
+
+ let due =
+ InvestmentPlanPolicy.dueDates InvestmentFrequency.Monthly anchor anchor (DateOnly(2026, 11, 30))
+
+ // 2026-11-21 is a Saturday, so the November run rolls to Monday 2026-11-23
+ Assert.Equal<DateOnly list>(
+ [ DateOnly(2026, 9, 21); DateOnly(2026, 10, 21); DateOnly(2026, 11, 23) ],
+ due
+ )
+
+ [<Fact>]
+ let ``due dates are truncated at the processing window`` () =
+ Assert.Equal<DateOnly list>([], InvestmentPlanPolicy.dueDates InvestmentFrequency.Daily anchor anchor (DateOnly(2026, 9, 20)))
+
+ let truncated =
+ InvestmentPlanPolicy.dueDates InvestmentFrequency.Daily anchor anchor (DateOnly(2026, 9, 22))
+
+ Assert.Equal<DateOnly list>([ DateOnly(2026, 9, 21); DateOnly(2026, 9, 22) ], truncated)
+
+ // a repeat drive over the same window yields the same dates (idempotent schedule)
+ Assert.Equal<DateOnly list>(truncated, InvestmentPlanPolicy.dueDates InvestmentFrequency.Daily anchor anchor (DateOnly(2026, 9, 22)))