summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/FundLab.Api.Tests/OrderTests.fs130
-rw-r--r--tests/FundLab.Domain.Tests/DomainTests.fs21
-rw-r--r--tests/FundLab.Web.Tests/BoundaryTests.fs72
3 files changed, 223 insertions, 0 deletions
diff --git a/tests/FundLab.Api.Tests/OrderTests.fs b/tests/FundLab.Api.Tests/OrderTests.fs
index 4fc1da0..1ab8fb8 100644
--- a/tests/FundLab.Api.Tests/OrderTests.fs
+++ b/tests/FundLab.Api.Tests/OrderTests.fs
@@ -1880,3 +1880,133 @@ type RedemptionTests(fixture: PostgresFixture) =
Assert.Contains("\"instrumentCode\":\"" + code + "\"", listResponse)
Assert.Contains("\"status\":\"confirmed\"", listResponse)
Assert.Contains("\"confirmedProceeds\":\"99.60\"", listResponse)
+
+[<Collection("postgres")>]
+type CapitalDepositTests(fixture: PostgresFixture) =
+ let sharedRepository =
+ lazy
+ let value = FundRepository(fixture.ConnectionString)
+ value.EnsureSchema()
+ value
+
+ let repository () = sharedRepository.Value
+
+ let createFund (initialCash: decimal) =
+ let command =
+ {
+ Name = "追加资金测试 FOF"
+ InitialCash = initialCash
+ InitialUnitNav = 1.00000000m
+ IsSynthetic = true
+ }
+
+ let key = fixture.Key(sprintf "capital-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 postDeposit (fundId: Guid) (idempotencyKey: string) (amount: string) (note: string) =
+ let body = sprintf "{\"amount\":\"%s\",\"note\":\"%s\"}" amount note
+
+ PersistenceTestHelpers.invoke
+ (app ())
+ "POST"
+ (sprintf "/api/funds/%O/capital/deposit" fundId)
+ [
+ "Authorization", "Bearer test-token"
+ "Idempotency-Key", idempotencyKey
+ ]
+ body
+
+ 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 depositCount fundId =
+ use connection = new NpgsqlConnection(fixture.ConnectionString)
+ connection.Open()
+ use command = connection.CreateCommand()
+ command.CommandText <- "SELECT count(*) FROM fund_capital_deposits WHERE fund_id = @fund_id"
+ let parameter = command.Parameters.Add("fund_id", NpgsqlTypes.NpgsqlDbType.Uuid)
+ parameter.Value <- box fundId
+ command.ExecuteScalar() :?> int64
+
+ [<Fact>]
+ member _.``capital deposit credits available cash and records the deposit``() =
+ let fundId = createFund 10000.00m
+ let key = fixture.Key(sprintf "cap-%s" (Guid.NewGuid().ToString("N")))
+
+ let status, response = postDeposit fundId key "500.00" "首次追加"
+
+ Assert.Equal(201, status)
+ Assert.Contains("\"amount\":\"500.00\"", response)
+ Assert.Equal(10500.00m, availableCash fundId)
+ Assert.Equal(1L, depositCount fundId)
+
+ [<Fact>]
+ member _.``capital deposit replay with the same key credits cash exactly once``() =
+ let fundId = createFund 10000.00m
+ let key = fixture.Key(sprintf "cap-%s" (Guid.NewGuid().ToString("N")))
+
+ let firstStatus, firstResponse = postDeposit fundId key "300.00" "重复追加"
+ let secondStatus, secondResponse = postDeposit fundId key "300.00" "重复追加"
+
+ Assert.Equal(201, firstStatus)
+ Assert.Equal(200, secondStatus)
+ Assert.Equal(firstResponse, secondResponse)
+ Assert.Equal(10300.00m, availableCash fundId)
+ Assert.Equal(1L, depositCount fundId)
+
+ let conflictingStatus, conflictingResponse = postDeposit fundId key "100.00" "不同金额"
+
+ Assert.Equal(409, conflictingStatus)
+ Assert.Contains("IDEMPOTENCY_CONFLICT", conflictingResponse)
+ Assert.Equal(10300.00m, availableCash fundId)
+
+ [<Fact>]
+ member _.``invalid deposit amounts are rejected without any cash movement``() =
+ let fundId = createFund 10000.00m
+
+ for amount in [ "0.00"; "-5.00"; "1.234" ] do
+ let key = fixture.Key(sprintf "cap-%s" (Guid.NewGuid().ToString("N")))
+ let status, response = postDeposit fundId key amount "非法"
+ Assert.Equal(400, status)
+ Assert.Contains("INVALID_CAPITAL_REQUEST", response)
+
+ Assert.Equal(10000.00m, availableCash fundId)
+ Assert.Equal(0L, depositCount fundId)
+
+ [<Fact>]
+ member _.``deposit for an unknown fund returns 404``() =
+ let key = fixture.Key(sprintf "cap-%s" (Guid.NewGuid().ToString("N")))
+ let status, response = postDeposit (Guid.NewGuid()) key "100.00" "无基金"
+
+ Assert.Equal(404, status)
+ Assert.Contains("FUND_NOT_FOUND", response)
+
+ [<Fact>]
+ member _.``deposit history is exposed via GET``() =
+ let fundId = createFund 10000.00m
+ let _, _ = postDeposit fundId (fixture.Key(sprintf "cap-%s" (Guid.NewGuid().ToString("N")))) "111.00" "第一笔"
+ let _, _ = postDeposit fundId (fixture.Key(sprintf "cap-%s" (Guid.NewGuid().ToString("N")))) "222.00" "第二笔"
+
+ let status, response =
+ PersistenceTestHelpers.invoke
+ (app ())
+ "GET"
+ (sprintf "/api/funds/%O/capital/deposits" fundId)
+ [ "Authorization", "Bearer test-token" ]
+ ""
+
+ Assert.Equal(200, status)
+ Assert.Contains("\"amount\":\"111.00\"", response)
+ Assert.Contains("\"amount\":\"222.00\"", response)
+ Assert.Contains("第一笔", response)
diff --git a/tests/FundLab.Domain.Tests/DomainTests.fs b/tests/FundLab.Domain.Tests/DomainTests.fs
index 6a9fc66..46516d5 100644
--- a/tests/FundLab.Domain.Tests/DomainTests.fs
+++ b/tests/FundLab.Domain.Tests/DomainTests.fs
@@ -539,3 +539,24 @@ module RedemptionPolicyTests =
Assert.Equal(Error "redemption fee exceeds redemption value", RedemptionPolicy.compute 1.00000000m 2.5m 3.00m 100.00m 40.00000000m)
Assert.Equal(Error "redemption proceeds rounds to zero", RedemptionPolicy.compute 0.00000001m 1500000m 0.01m 100.00m 40.00000000m)
Assert.Equal(Error "redemption cash rounds to zero", RedemptionPolicy.compute 0.00000001m 2m 0.00m 100.00m 40.00000000m)
+
+module CapitalPolicyTests =
+
+ open Xunit
+ open FundLab.Domain
+
+ [<Fact>]
+ let ``deposit validation rejects non positive or imprecise amounts`` () =
+ Assert.Equal(Error "deposit amount must be positive", CapitalPolicy.validateDeposit 0m)
+ Assert.Equal(Error "deposit amount must be positive", CapitalPolicy.validateDeposit -100.00m)
+ Assert.Equal(Error "deposit amount exceeds cash precision", CapitalPolicy.validateDeposit 100.005m)
+ Assert.Equal(Ok(), CapitalPolicy.validateDeposit 500.00m)
+
+ [<Fact>]
+ let ``settlement adds to available cash without touching units or nav`` () =
+ Assert.Equal(Ok 5398.51m, CapitalPolicy.settle 4898.51m 500.00m)
+ Assert.Equal(Ok 10000.00m, CapitalPolicy.settle 9000.00m 1000.00m)
+
+ let overflow = CapitalPolicy.settle CapitalPolicy.cashMaximum 0.01m
+
+ Assert.Equal(Error "available cash exceeds database precision", overflow)
diff --git a/tests/FundLab.Web.Tests/BoundaryTests.fs b/tests/FundLab.Web.Tests/BoundaryTests.fs
index 015ed45..5226b65 100644
--- a/tests/FundLab.Web.Tests/BoundaryTests.fs
+++ b/tests/FundLab.Web.Tests/BoundaryTests.fs
@@ -635,3 +635,75 @@ module RedemptionBoundaryTests =
Assert.Equal("0.00000000", decoded.positions[0].reservedUnits)
Assert.Equal("5.00000000", decoded.positions[1].reservedUnits)
+
+module CapitalBoundaryTests =
+
+ let sampleFund () : FundSummary =
+ {
+ id = "3fa85f64-5717-4562-b3fc-2c963f66afa6"
+ name = "追加资金测试"
+ currency = "CNY"
+ initialCash = "10000.00"
+ initialUnitNav = "1.00000000"
+ isSynthetic = false
+ availableCash = "10000.00"
+ reservedCash = "0.00"
+ status = "active"
+ }
+
+ [<Fact>]
+ let ``capital deposit request requires fund token and positive amount`` () =
+ let noFund, _ = update CapitalDepositRequested { init () with token = "qa-token" }
+
+ Assert.Equal(0, noFund.capitalSeq)
+ Assert.False(noFund.capitalInFlight)
+
+ let noToken, _ =
+ update CapitalDepositRequested { init () with createdFund = Some(sampleFund ()) }
+
+ Assert.Equal(Some "请输入 API token", noToken.error)
+ Assert.Equal(0, noToken.capitalSeq)
+
+ let invalidAmount, _ =
+ update CapitalDepositRequested
+ { init () with token = "qa-token"; createdFund = Some(sampleFund ()); capitalAmount = "0.00" }
+
+ Assert.Equal(Some "追加金额必须是大于零的两位小数金额,例如 1000.00", invalidAmount.error)
+ Assert.Equal(0, invalidAmount.capitalSeq)
+
+ [<Fact>]
+ let ``valid deposit starts one attempt and completion refreshes the fund`` () =
+ let started, _ =
+ update CapitalDepositRequested
+ { init () with token = "qa-token"; createdFund = Some(sampleFund ()); capitalAmount = "500.00" }
+
+ Assert.Equal(1, started.capitalSeq)
+ Assert.True(started.capitalInFlight)
+ Assert.Equal("500.00", started.lastCapitalAttempt.Value.amount)
+ Assert.Equal(32, started.lastCapitalAttempt.Value.idempotencyKey.Length)
+
+ let completed, _ = update (CapitalDepositCompleted(1, "500.00")) started
+
+ Assert.False(completed.capitalInFlight)
+ Assert.Equal(Some "追加成功 + 500.00(现金余额已刷新)", completed.capitalNote)
+
+ [<Fact>]
+ let ``stale capital completion is ignored and token change clears the entry`` () =
+ let model =
+ { init () with
+ token = "qa-token"
+ createdFund = Some(sampleFund ())
+ capitalSeq = 4
+ capitalInFlight = true
+ lastCapitalAttempt = Some { idempotencyKey = "k"; amount = "500.00" } }
+
+ let stale, _ = update (CapitalDepositCompleted(3, "500.00")) model
+
+ Assert.True(stale.capitalInFlight)
+
+ let cleared, _ = update (TokenChanged "new-token") model
+
+ Assert.Equal("", cleared.capitalAmount)
+ Assert.Equal(None, cleared.createdFund)
+ Assert.False(cleared.capitalInFlight)
+ Assert.Equal(None, cleared.lastCapitalAttempt)