summaryrefslogtreecommitdiff
path: root/tests/FundLab.Domain.Tests/InvestmentPlanTests.fs
blob: 9a5e2b9ae3a2b4795753dd23bcfc3c4b7f8cf217 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
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)))