diff options
Diffstat (limited to 'tests/FundLab.Api.Tests')
| -rw-r--r-- | tests/FundLab.Api.Tests/OrderTests.fs | 132 |
1 files changed, 132 insertions, 0 deletions
diff --git a/tests/FundLab.Api.Tests/OrderTests.fs b/tests/FundLab.Api.Tests/OrderTests.fs index 1ab8fb8..89154cd 100644 --- a/tests/FundLab.Api.Tests/OrderTests.fs +++ b/tests/FundLab.Api.Tests/OrderTests.fs @@ -2010,3 +2010,135 @@ type CapitalDepositTests(fixture: PostgresFixture) = Assert.Contains("\"amount\":\"111.00\"", response) Assert.Contains("\"amount\":\"222.00\"", response) Assert.Contains("第一笔", response) + +[<Collection("postgres")>] +type SipPlanTests(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, 10, 0, 0, TimeSpan.Zero) + Instruments = [ { Code = code; Name = "定投测试基金"; FundType = None } ] + } + + repository().UpsertInstruments(payload, "sip-test-hash") + code + + let createFund (initialCash: decimal) = + let command = + { + Name = "定投测试 FOF" + InitialCash = initialCash + InitialUnitNav = 1.00000000m + IsSynthetic = true + } + + let key = fixture.Key(sprintf "sip-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 availableCash fundId = + use connection = new NpgsqlConnection(fixture.ConnectionString) + connection.Open() + use command = connection.CreateCommand() + command.CommandText <- "SELECT available_cash FROM funds WHERE id = @fund_id" + let parameter = command.Parameters.Add("fund_id", NpgsqlTypes.NpgsqlDbType.Uuid) + parameter.Value <- box fundId + command.ExecuteScalar() :?> decimal + + let postPlan (fundId: Guid) (idempotencyKey: string) (code: string) (amount: string) (frequency: string) = + let body = + sprintf + "{\"instrumentCode\":\"%s\",\"amount\":\"%s\",\"frequency\":\"%s\"}" + code + amount + frequency + + PersistenceTestHelpers.invoke + (app ()) + "POST" + (sprintf "/api/funds/%O/sip/plans" fundId) + [ + "Authorization", "Bearer test-token" + "Idempotency-Key", idempotencyKey + ] + body + + let getPlans (fundId: Guid) = + PersistenceTestHelpers.invoke + (app ()) + "GET" + (sprintf "/api/funds/%O/sip/plans" fundId) + [ "Authorization", "Bearer test-token" ] + "" + + [<Fact>] + member _.``sip plan creation persists the plan without touching cash``() = + let fundId = createFund 10000.00m + let code = seedInstrument () + let key = fixture.Key(sprintf "sip-%s" (Guid.NewGuid().ToString("N"))) + + let status, response = postPlan fundId key code "200.00" "weekly" + + Assert.Equal(201, status) + Assert.Contains("\"amount\":\"200.00\"", response) + Assert.Contains("\"frequency\":\"weekly\"", response) + Assert.Contains("\"status\":\"active\"", response) + Assert.Contains("\"nextTradeDate\":\"2026-09-28\"", response) + Assert.Equal(10000.00m, availableCash fundId) + + [<Fact>] + member _.``sip plan replay and conflict follow the shared idempotency contract``() = + let fundId = createFund 10000.00m + let code = seedInstrument () + let key = fixture.Key(sprintf "sip-%s" (Guid.NewGuid().ToString("N"))) + + let firstStatus, firstResponse = postPlan fundId key code "150.00" "monthly" + let secondStatus, secondResponse = postPlan fundId key code "150.00" "monthly" + + Assert.Equal(201, firstStatus) + Assert.Equal(200, secondStatus) + Assert.Equal(firstResponse, secondResponse) + + let conflictingStatus, conflictingResponse = postPlan fundId key code "250.00" "weekly" + + Assert.Equal(409, conflictingStatus) + Assert.Contains("IDEMPOTENCY_CONFLICT", conflictingResponse) + + [<Fact>] + member _.``invalid sip amounts and frequencies are rejected``() = + let fundId = createFund 10000.00m + let code = seedInstrument () + + for amount, frequency in [ "0.00", "weekly"; "-1.00", "weekly"; "1.234", "weekly"; "100.00", "daily" ] do + let key = fixture.Key(sprintf "sip-%s" (Guid.NewGuid().ToString("N"))) + let status, response = postPlan fundId key code amount frequency + Assert.Equal(400, status) + Assert.Contains("INVALID_SIP_REQUEST", response) + + [<Fact>] + member _.``sip plan list exposes the stored plans``() = + let fundId = createFund 10000.00m + let code = seedInstrument () + let _, _ = postPlan fundId (fixture.Key(sprintf "sip-%s" (Guid.NewGuid().ToString("N")))) code "111.00" "biweekly" + + let status, response = getPlans fundId + + Assert.Equal(200, status) + Assert.Contains("\"instrumentCode\":\"" + code + "\"", response) + Assert.Contains("\"frequency\":\"biweekly\"", response) + Assert.Contains("\"amount\":\"111.00\"", response) |
