diff options
| author | Somhairle H. Marisol <[email protected]> | 2026-09-21 23:21:54 +0800 |
|---|---|---|
| committer | Somhairle H. Marisol <[email protected]> | 2026-09-21 23:21:54 +0800 |
| commit | d4b539a2791a0cd4d098fecfece0039073a9f7e2 (patch) | |
| tree | dfd314b37fa24379ef13dcd66a1de76e2a923bf2 /tests/FundLab.Domain.Tests | |
| parent | 12c4d3625458c830a2746a1f277fb684e9c498cb (diff) | |
| download | fund-lab-d4b539a2791a0cd4d098fecfece0039073a9f7e2.tar.gz | |
Add scheduled investment plans (3d-10)
Diffstat (limited to 'tests/FundLab.Domain.Tests')
| -rw-r--r-- | tests/FundLab.Domain.Tests/FundLab.Domain.Tests.fsproj | 1 | ||||
| -rw-r--r-- | tests/FundLab.Domain.Tests/InvestmentPlanTests.fs | 81 |
2 files changed, 82 insertions, 0 deletions
diff --git a/tests/FundLab.Domain.Tests/FundLab.Domain.Tests.fsproj b/tests/FundLab.Domain.Tests/FundLab.Domain.Tests.fsproj index 2ba20b5..a9c6fc0 100644 --- a/tests/FundLab.Domain.Tests/FundLab.Domain.Tests.fsproj +++ b/tests/FundLab.Domain.Tests/FundLab.Domain.Tests.fsproj @@ -20,6 +20,7 @@ </ItemGroup> <ItemGroup> <Compile Include="DomainTests.fs" /> + <Compile Include="InvestmentPlanTests.fs" /> <Compile Include="Program.fs" /> </ItemGroup> </Project> 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))) |
