summaryrefslogtreecommitdiff
path: root/tests/FundLab.Api.Tests/OrderTests.fs
diff options
context:
space:
mode:
Diffstat (limited to 'tests/FundLab.Api.Tests/OrderTests.fs')
-rw-r--r--tests/FundLab.Api.Tests/OrderTests.fs132
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)