diff options
Diffstat (limited to 'tests/FundLab.Api.Tests/BondTradeTests.fs')
| -rw-r--r-- | tests/FundLab.Api.Tests/BondTradeTests.fs | 139 |
1 files changed, 134 insertions, 5 deletions
diff --git a/tests/FundLab.Api.Tests/BondTradeTests.fs b/tests/FundLab.Api.Tests/BondTradeTests.fs index 76827f5..b176aa8 100644 --- a/tests/FundLab.Api.Tests/BondTradeTests.fs +++ b/tests/FundLab.Api.Tests/BondTradeTests.fs @@ -24,6 +24,17 @@ module BondTradeSupport = AccruedInterest = None Date = None MaturityDate = Some(DateOnly(2026, 10, 15)) + ParValue = Some 100m + IssuePrice = Some 100m + ValueDate = Some(DateOnly(2020, 10, 15)) + ListingDate = Some(DateOnly(2020, 11, 3)) + PublishDate = None + PayInterestDay = Some "10-15" + CouponRate = Some 0.02m + CouponRateExplain = None + BondExpireYears = Some "6" + Rating = Some "AAA" + DataStatus = Some "complete" } let appWithQuote (probe: IBondQuoteProbe) = @@ -78,25 +89,143 @@ type BondTradeTests(fixture: PostgresFixture) = [ "Authorization", "Bearer test-token" ] "" + let recordCashflow (probe: IBondQuoteProbe) fundId body idempotencyKey = + PersistenceTestHelpers.invoke + (app probe) + "POST" + (sprintf "/api/funds/%O/bond-cashflows" fundId) + [ "Authorization", "Bearer test-token"; "Idempotency-Key", idempotencyKey ] + body + + let readCashflows (probe: IBondQuoteProbe) fundId = + PersistenceTestHelpers.invoke + (app probe) + "GET" + (sprintf "/api/funds/%O/bond-cashflows" fundId) + [ "Authorization", "Bearer test-token" ] + "" + [<Fact>] - member _.``buying a bond with a live quote records a trade and a position``() = + member _.``buying a bond records clean price, accrued interest, dirty price and settlement``() = let fundId = createFund () let probe = BondTradeSupport.stubQuoteProbe BondTradeSupport.quote110075 let status, body = - buy probe fundId """{"instrumentCode":"110075","quantity":"10"}""" (fixture.Key "bond-trade-buy-ok") + buy + probe + fundId + """{"instrumentCode":"110075","quantity":"10","tradeDate":"2026-09-21"}""" + (fixture.Key "bond-trade-buy-ok") Assert.Equal(201, status) Assert.Contains("\"instrumentCode\":\"110075\"", body) Assert.Contains("南航转债", body) - Assert.Contains("106.368", body) - Assert.Contains("1063.68", body) + Assert.Contains("\"cleanPrice\":\"106.36800000\"", body) + Assert.Contains("\"accruedInterest\":\"1.86849300\"", body) + Assert.Contains("\"price\":\"108.23649300\"", body) + Assert.Contains("\"parValue\":\"100.00000000\"", body) + Assert.Contains("\"couponRate\":\"0.02000000\"", body) + Assert.Contains("\"valueDate\":\"2020-10-15\"", body) + Assert.Contains("\"maturityDate\":\"2026-10-15\"", body) + Assert.Contains("\"settlementDate\":\"2026-09-21\"", body) + Assert.Contains("\"tradeDate\":\"2026-09-21\"", body) + Assert.Contains("1082.36", body) let status, positionsBody = readPositions probe fundId Assert.Equal(200, status) Assert.Contains("\"instrumentCode\":\"110075\"", positionsBody) Assert.Contains("\"quantity\":\"10.00000000\"", positionsBody) - Assert.Contains("1063.68", positionsBody) + Assert.Contains("1082.36", positionsBody) + + [<Fact>] + member _.``buying a quantity below the minimum unit is rejected``() = + let fundId = createFund () + let probe = BondTradeSupport.stubQuoteProbe BondTradeSupport.quote110075 + + let status, body = + buy + probe + fundId + """{"instrumentCode":"110075","quantity":"5","tradeDate":"2026-09-21"}""" + (fixture.Key "bond-trade-min-unit") + + Assert.Equal(400, status) + Assert.Contains("INVALID_BOND_TRADE_REQUEST", body) + Assert.Contains("multiple", body) + + [<Fact>] + member _.``a coupon event credits cash and is recorded idempotently``() = + let fundId = createFund () + let probe = BondTradeSupport.stubQuoteProbe BondTradeSupport.quote110075 + + let buyStatus, _ = + buy + probe + fundId + """{"instrumentCode":"110075","quantity":"10","tradeDate":"2026-09-21"}""" + (fixture.Key "bond-coupon-buy") + + Assert.Equal(201, buyStatus) + + let key = fixture.Key "bond-coupon-event" + + let body = + """{"instrumentCode":"110075","eventType":"coupon","eventDate":"2026-10-15","quantity":"10","amount":"20.00"}""" + + let status, response = recordCashflow probe fundId body key + Assert.Equal(201, status) + Assert.Contains("\"eventType\":\"coupon\"", response) + Assert.Contains("\"amount\":\"20.00\"", response) + + let replayStatus, _ = recordCashflow probe fundId body key + Assert.Equal(200, replayStatus) + + let status, events = readCashflows probe fundId + Assert.Equal(200, status) + Assert.Contains("\"eventType\":\"coupon\"", events) + Assert.DoesNotContain("\"eventType\":\"maturity\"", events) + + [<Fact>] + member _.``a maturity event settles the position and records the cashflow``() = + let fundId = createFund () + let probe = BondTradeSupport.stubQuoteProbe BondTradeSupport.quote110075 + + let buyStatus, _ = + buy + probe + fundId + """{"instrumentCode":"110075","quantity":"10","tradeDate":"2026-09-21"}""" + (fixture.Key "bond-maturity-buy") + + Assert.Equal(201, buyStatus) + + let status, response = + recordCashflow + probe + fundId + """{"instrumentCode":"110075","eventType":"maturity","eventDate":"2026-10-15","quantity":"10","amount":"1000.00"}""" + (fixture.Key "bond-maturity-event") + + Assert.Equal(201, status) + Assert.Contains("\"eventType\":\"maturity\"", response) + + let _, positionsBody = readPositions probe fundId + Assert.Contains("\"positions\":[]", positionsBody) + + [<Fact>] + member _.``a cashflow for a bond the fund does not hold answers 404``() = + let fundId = createFund () + let probe = BondTradeSupport.stubQuoteProbe BondTradeSupport.quote110075 + + let status, body = + recordCashflow + probe + fundId + """{"instrumentCode":"110075","eventType":"coupon","eventDate":"2026-10-15","quantity":"10","amount":"20.00"}""" + (fixture.Key "bond-coupon-no-position") + + Assert.Equal(404, status) + Assert.Contains("BOND_POSITION_NOT_FOUND", body) [<Fact>] member _.``buying an unknown code with an unavailable quote does not create a position``() = |
