diff options
| author | Somhairle H. Marisol <[email protected]> | 2026-09-21 06:38:58 +0800 |
|---|---|---|
| committer | Somhairle H. Marisol <[email protected]> | 2026-09-21 06:38:58 +0800 |
| commit | 64a4ca5c0a523ffc51c861eac1291671b90a2f3a (patch) | |
| tree | 12b846aa15ba8563dfbce29f53b966667c8d30a7 /tests | |
| parent | 03f89aea966dfe9edcd08695a79788a6be703b26 (diff) | |
| download | fund-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.fs | 85 |
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) |
