summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-21 06:38:58 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-21 06:38:58 +0800
commit64a4ca5c0a523ffc51c861eac1291671b90a2f3a (patch)
tree12b846aa15ba8563dfbce29f53b966667c8d30a7 /tests
parent03f89aea966dfe9edcd08695a79788a6be703b26 (diff)
downloadfund-lab-64a4ca5c0a523ffc51c861eac1291671b90a2f3a.tar.gz
fix(api,web): 下单/读取请求身份隔离与零手续费支持(3d-1 评审修正)
- Web: orderSeq 拆分为 orderCreateSeq/orderReadSeq,并发刷新不再丢弃在途下单结果;换基金/换 token 双序列失效并携带 fundId 双重守卫 - 零手续费:DB CHECK fee_amount >= 0(幂等迁移),校验改为拒绝负数,UI 提示与占位符同步 - 测试:API 增加零手续费回放与 DB 负手续费约束回归(43/43),浏览器新增 H 系列延迟 POST 并发刷新/失败恢复/换基金隔离(78/78)
Diffstat (limited to 'tests')
-rw-r--r--tests/FundLab.Api.Tests/OrderTests.fs85
1 files changed, 80 insertions, 5 deletions
diff --git a/tests/FundLab.Api.Tests/OrderTests.fs b/tests/FundLab.Api.Tests/OrderTests.fs
index 5e6771c..d1f57df 100644
--- a/tests/FundLab.Api.Tests/OrderTests.fs
+++ b/tests/FundLab.Api.Tests/OrderTests.fs
@@ -6,6 +6,7 @@ open System.Net.Http
open System.Text.Json
open System.Threading.Tasks
open FundLab.Api
+open Npgsql
open Xunit
[<Collection("postgres")>]
@@ -167,7 +168,7 @@ type OrderTests(fixture: PostgresFixture) =
orderBody code "0.00" "1.00"
orderBody code "-5.00" "1.00"
orderBody code "10.005" "1.00"
- orderBody code "10.00" "0.00"
+ orderBody code "10.00" "-0.01"
orderBody code "10.00" "1.005"
"{\"amount\":\"10.00\",\"feeAmount\":\"1.00\"}"
"{\"fundCode\":123,\"amount\":\"10.00\",\"feeAmount\":\"1.00\"}"
@@ -189,14 +190,14 @@ type OrderTests(fixture: PostgresFixture) =
Assert.Equal(0, orderCount listBody)
let boundaryStatus, boundaryBody =
- postOrder api fundId key (orderBody code "0.01" "0.01")
+ postOrder api fundId key (orderBody code "0.01" "0.00")
Assert.Equal(201, boundaryStatus)
- Assert.Contains("\"reservedTotal\":\"0.02\"", boundaryBody)
+ Assert.Contains("\"reservedTotal\":\"0.01\"", boundaryBody)
let _, fundAfterBoundary = getFund api fundId
- Assert.Contains("\"availableCash\":\"999.98\"", fundAfterBoundary)
- Assert.Contains("\"reservedCash\":\"0.02\"", fundAfterBoundary)
+ Assert.Contains("\"availableCash\":\"999.99\"", fundAfterBoundary)
+ Assert.Contains("\"reservedCash\":\"0.01\"", fundAfterBoundary)
[<Fact>]
member _.``subscription order requires authentication and an idempotency key``() =
@@ -398,3 +399,77 @@ type OrderTests(fixture: PostgresFixture) =
Assert.Equal(200, replayStatus)
Assert.Equal(orderId, PersistenceTestHelpers.responseId replayResponse)
+
+ [<Fact>]
+ member _.``zero fee subscription order is accepted and replays``() =
+ let code = seedInstrument ()
+ let fundId = createFund 200.00m
+ let api = app ()
+ let key = fixture.Key("order-zero-fee")
+
+ let status, body =
+ postOrder api fundId key (orderBody code "50.00" "0.00")
+
+ Assert.Equal(201, status)
+ Assert.Contains("\"amount\":\"50.00\"", body)
+ Assert.Contains("\"feeAmount\":\"0.00\"", body)
+ Assert.Contains("\"reservedTotal\":\"50.00\"", body)
+ Assert.Contains("\"status\":\"submitted\"", body)
+ let orderId = PersistenceTestHelpers.responseId body
+
+ let replayStatus, replayBody = postOrder api fundId key (orderBody code "50.00" "0.00")
+ Assert.Equal(200, replayStatus)
+ Assert.Equal(orderId, PersistenceTestHelpers.responseId replayBody)
+
+ let _, fundBody = getFund api fundId
+ Assert.Contains("\"availableCash\":\"150.00\"", fundBody)
+ Assert.Contains("\"reservedCash\":\"50.00\"", fundBody)
+
+ let _, listBody = getOrderList api fundId
+ Assert.Equal(1, orderCount listBody)
+
+ [<Fact>]
+ member _.``database constraint rejects negative fee but allows exact zero fee``() =
+ let code = seedInstrument ()
+ let fundId = createFund 100.00m
+ repository () |> ignore
+
+ use connection = new NpgsqlConnection(fixture.ConnectionString)
+ connection.Open()
+
+ let insertOrder (fee: decimal) (reservedTotal: decimal) =
+ use command = connection.CreateCommand()
+ command.CommandText <-
+ """
+ INSERT INTO subscription_orders
+ (id, fund_id, fund_code, amount, fee_amount, reserved_total, status, is_synthetic)
+ VALUES
+ (@id, @fund_id, @fund_code, @amount, @fee_amount, @reserved_total, 'submitted', true)
+ """
+
+ command.Parameters.Add("id", NpgsqlTypes.NpgsqlDbType.Uuid).Value <- box (Guid.NewGuid())
+ command.Parameters.Add("fund_id", NpgsqlTypes.NpgsqlDbType.Uuid).Value <- box fundId
+ command.Parameters.Add("fund_code", NpgsqlTypes.NpgsqlDbType.Text).Value <- box code
+ command.Parameters.Add("amount", NpgsqlTypes.NpgsqlDbType.Numeric).Value <- box 10.00m
+ command.Parameters.Add("fee_amount", NpgsqlTypes.NpgsqlDbType.Numeric).Value <- box fee
+ command.Parameters.Add("reserved_total", NpgsqlTypes.NpgsqlDbType.Numeric).Value <- box reservedTotal
+ command.ExecuteNonQuery() |> ignore
+
+ Assert.Throws<Npgsql.PostgresException>(fun () -> insertOrder -1.00m 9.00m) |> ignore
+
+ insertOrder 0.00m 10.00m
+
+ let negativeCount =
+ PersistenceTestHelpers.queryCount
+ fixture.ConnectionString
+ "SELECT count(*) FROM subscription_orders WHERE fund_id = @fund_id AND fee_amount < 0"
+ [ "fund_id", box fundId, NpgsqlTypes.NpgsqlDbType.Uuid ]
+
+ let zeroCount =
+ PersistenceTestHelpers.queryCount
+ fixture.ConnectionString
+ "SELECT count(*) FROM subscription_orders WHERE fund_id = @fund_id AND fee_amount = 0"
+ [ "fund_id", box fundId, NpgsqlTypes.NpgsqlDbType.Uuid ]
+
+ Assert.Equal(0L, negativeCount)
+ Assert.Equal(1L, zeroCount)