diff options
| author | Somhairle H. Marisol <[email protected]> | 2026-09-22 05:23:52 +0800 |
|---|---|---|
| committer | Somhairle H. Marisol <[email protected]> | 2026-09-22 05:23:52 +0800 |
| commit | 6abd4c206fbfe8369b3fde4c905619d08f13c5d1 (patch) | |
| tree | f53ce1746591b640f78cd11d63c8411160059d98 /tests/FundLab.Api.Tests/SipPlanStatusTests.fs | |
| parent | 545f2a507fd0138f7fd50ed7fb2631d0273db625 (diff) | |
| download | fund-lab-6abd4c206fbfe8369b3fde4c905619d08f13c5d1.tar.gz | |
Add SIP plan pause/resume and rebalance plan archive lifecycle (3d-24)
Diffstat (limited to 'tests/FundLab.Api.Tests/SipPlanStatusTests.fs')
| -rw-r--r-- | tests/FundLab.Api.Tests/SipPlanStatusTests.fs | 177 |
1 files changed, 177 insertions, 0 deletions
diff --git a/tests/FundLab.Api.Tests/SipPlanStatusTests.fs b/tests/FundLab.Api.Tests/SipPlanStatusTests.fs new file mode 100644 index 0000000..780d85f --- /dev/null +++ b/tests/FundLab.Api.Tests/SipPlanStatusTests.fs @@ -0,0 +1,177 @@ +namespace FundLab.Api.Tests + +open System +open Xunit +open FundLab.Api +open FundLab.Domain + +[<Collection("postgres")>] +type SipPlanStatusTests(fixture: PostgresFixture) = + let sharedRepository = + lazy + let value = FundRepository(fixture.ConnectionString) + value.EnsureSchema() + value + + let repository () = sharedRepository.Value + + let seedInstrument () = + let code = Random.Shared.Next(0, 1000000).ToString("D6") + + let payload = + { + Source = "akshare" + SourceRevision = "akshare-test/eastmoney" + CollectedAt = DateTimeOffset(2026, 9, 21, 8, 0, 0, TimeSpan.Zero) + Instruments = [ { Code = code; Name = "定投状态测试基金"; FundType = None } ] + } + + repository().UpsertInstruments(payload, "sip-status-test-hash") + code + + let createFund (initialCash: decimal) = + let command = + { + Name = "定投状态测试 FOF" + InitialCash = initialCash + InitialUnitNav = 1.00000000m + IsSynthetic = true + } + + let key = fixture.Key(sprintf "sip-status-fund-%s" (Guid.NewGuid().ToString("N"))) + + match repository().CreateFund(key, command) with + | FundWriteResult.Created fund -> fund.Id + | other -> failwithf "unexpected fund creation result: %A" other + + let app () = App.createApplication (repository ()) + + let truncateMicroseconds (moment: DateTimeOffset) = + let utc = moment.ToUniversalTime() + DateTimeOffset(utc.Ticks - (utc.Ticks % 10L), TimeSpan.Zero) + + let insertQuoteOnDate (code: string) (nav: decimal) (navDate: DateOnly) = + let revision = sprintf "akshare-test/%O" (Guid.NewGuid()) + + let payload: MarketDataNavPayload = + { + Source = "akshare" + SourceRevision = revision + CollectedAt = truncateMicroseconds (DateTimeOffset.Now.AddSeconds(-10.0)) + Code = code + Observations = + [ + { + NavDate = navDate + PublishedAt = None + Nav = nav + AccumulatedNav = Some nav + DailyReturn = Some 0.0m + } + ] + } + + repository().UpsertNavObservations(payload, sprintf "sip-status-hash/%s" revision) + + let createPlanWithAnchor (fundId: Guid) (code: string) (amount: decimal) (frequency: SipFrequency) = + let key = fixture.Key(sprintf "sip-status-plan-%s" (Guid.NewGuid().ToString("N"))) + + match + repository().CreateSipPlan( + key, + fundId, + { InstrumentCode = code; Amount = amount; Frequency = frequency }, + DateOnly(2026, 9, 7) + ) + with + | SipPlanWriteResult.SipPlanCreated plan -> plan.Id + | other -> failwithf "unexpected sip plan result: %A" other + + let postStatus (fundId: Guid) (planId: Guid) (action: string) = + PersistenceTestHelpers.invoke + (app ()) + "POST" + (sprintf "/api/funds/%O/sip/plans/%O/%s" fundId planId action) + [ "Authorization", "Bearer test-token" ] + "" + + let advance (fundId: Guid) (body: string) = + PersistenceTestHelpers.invoke + (app ()) + "POST" + (sprintf "/api/funds/%O/sip/advance" fundId) + [ "Authorization", "Bearer test-token" ] + body + + let listPlans (fundId: Guid) = + PersistenceTestHelpers.invoke + (app ()) + "GET" + (sprintf "/api/funds/%O/sip/plans" fundId) + [ "Authorization", "Bearer test-token" ] + "" + + [<Fact>] + member _.``pause skips advance, resume lets the next due period execute again``() = + let fundId = createFund 10000.00m + let code = seedInstrument () + let planId = createPlanWithAnchor fundId code 100.00m Weekly + + insertQuoteOnDate code 2.5m (DateOnly(2026, 9, 14)) + insertQuoteOnDate code 2.5m (DateOnly(2026, 9, 21)) + + let pauseStatus, pauseBody = postStatus fundId planId "pause" + Assert.Equal(200, pauseStatus) + Assert.Contains("\"status\":\"paused\"", pauseBody) + + let advanceStatus, advanceBody = advance fundId "{\"endDate\":\"2026-09-21\"}" + Assert.Equal(200, advanceStatus) + Assert.DoesNotContain("\"status\":\"succeeded\"", advanceBody) + Assert.DoesNotContain("\"status\":\"pending_nav\"", advanceBody) + + let resumeStatus, resumeBody = postStatus fundId planId "resume" + Assert.Equal(200, resumeStatus) + Assert.Contains("\"status\":\"active\"", resumeBody) + + let _, resumedBody = advance fundId "{\"endDate\":\"2026-09-21\"}" + Assert.Contains("\"status\":\"succeeded\"", resumedBody) + Assert.Contains("\"tradeDate\":\"2026-09-14\"", resumedBody) + + [<Fact>] + member _.``pausing an already paused plan is idempotent``() = + let fundId = createFund 1000.00m + let code = seedInstrument () + let planId = createPlanWithAnchor fundId code 100.00m Weekly + + let firstStatus, firstBody = postStatus fundId planId "pause" + Assert.Equal(200, firstStatus) + Assert.Contains("\"status\":\"paused\"", firstBody) + + let secondStatus, secondBody = postStatus fundId planId "pause" + Assert.Equal(200, secondStatus) + Assert.Contains("\"status\":\"paused\"", secondBody) + + [<Fact>] + member _.``paused plan status is reflected in the sip plan list``() = + let fundId = createFund 1000.00m + let code = seedInstrument () + let planId = createPlanWithAnchor fundId code 100.00m Weekly + + let _, _ = postStatus fundId planId "pause" + let _, listBody = listPlans fundId + Assert.Contains("\"status\":\"paused\"", listBody) + + let _, _ = postStatus fundId planId "resume" + let _, resumedListBody = listPlans fundId + Assert.Contains("\"status\":\"active\"", resumedListBody) + + [<Fact>] + member _.``pausing a plan that belongs to another fund answers 404``() = + let fundId = createFund 1000.00m + let otherFundId = createFund 1000.00m + let code = seedInstrument () + let planId = createPlanWithAnchor fundId code 100.00m Weekly + + let status, body = postStatus otherFundId planId "pause" + Assert.Equal(404, status) + Assert.Contains("PLAN_NOT_FOUND", body) |
