summaryrefslogtreecommitdiff
path: root/src/FundLab.Api/App.fs
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-22 10:24:58 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-22 10:24:58 +0800
commit2677ea0053ba9d6b34fec278ad52657da390b30c (patch)
treeb754cb7ba256a527147789ee6fb177bf29d58bf5 /src/FundLab.Api/App.fs
parent1262eea047d4d427ab37a5bf2003c2ab82b0d11c (diff)
downloadfund-lab-2677ea0053ba9d6b34fec278ad52657da390b30c.tar.gz
Fix stock sip plan instrument FK (3d-31 B4a)
Diffstat (limited to 'src/FundLab.Api/App.fs')
-rw-r--r--src/FundLab.Api/App.fs68
1 files changed, 52 insertions, 16 deletions
diff --git a/src/FundLab.Api/App.fs b/src/FundLab.Api/App.fs
index f45f7ae..d5c3a50 100644
--- a/src/FundLab.Api/App.fs
+++ b/src/FundLab.Api/App.fs
@@ -356,6 +356,7 @@ type SipPlanResponse =
id: Guid
fundId: Guid
instrumentCode: string
+ assetClass: string
amount: string
frequency: string
status: string
@@ -850,6 +851,7 @@ module App =
id = plan.Id
fundId = plan.FundId
instrumentCode = plan.InstrumentCode
+ assetClass = plan.AssetClass
amount = cashText plan.Amount
frequency = SipPolicy.frequencyText plan.Frequency
status = plan.Status
@@ -1227,7 +1229,7 @@ module App =
with
| :? JsonException -> Error "request body must be valid JSON"
- let private parseSipPlanCommand (body: string) : Result<SipPlanCommand, string> =
+ let private parseSipPlanCommand (body: string) : Result<SipPlanCommand * string, string> =
try
use document = JsonDocument.Parse(body)
let root = document.RootElement
@@ -1235,18 +1237,28 @@ module App =
if root.ValueKind <> JsonValueKind.Object then
Error "request body must be a JSON object"
else
+ let assetClass =
+ match tryStringProperty root "assetClass" with
+ | Some value when not (String.IsNullOrWhiteSpace value) -> value.Trim().ToLowerInvariant()
+ | _ -> "fund"
+
match tryStringProperty root "instrumentCode", tryStringProperty root "amount", tryStringProperty root "frequency" with
| Some code, Some amountText, Some frequencyText ->
- match tryDecimal "amount" amountText, SipPolicy.parseFrequency frequencyText with
- | Ok amount, Some frequency ->
- Ok
- {
- InstrumentCode = code
- Amount = amount
- Frequency = frequency
- }
- | Error message, _ -> Error message
- | _, None -> Error "frequency must be one of weekly, biweekly or monthly"
+ if assetClass <> "fund" && assetClass <> "stock" then
+ Error "assetClass must be fund or stock"
+ else
+ match tryDecimal "amount" amountText, SipPolicy.parseFrequency frequencyText with
+ | Ok amount, Some frequency ->
+ Ok(
+ {
+ InstrumentCode = code
+ Amount = amount
+ Frequency = frequency
+ },
+ assetClass
+ )
+ | Error message, _ -> Error message
+ | _, None -> Error "frequency must be one of weekly, biweekly or monthly"
| _ ->
Error "instrumentCode, amount and frequency are required"
with
@@ -1765,9 +1777,9 @@ module App =
match parseSipPlanCommand body with
| Error message ->
return! invokeHandler (errorResponse 400 "INVALID_SIP_REQUEST" message) next ctx
- | Ok command ->
+ | Ok(command, assetClass) ->
try
- match repository.CreateSipPlan(idempotencyKey, fundId, command) with
+ match repository.CreateSipPlan(idempotencyKey, fundId, command, assetClass = assetClass) with
| SipPlanWriteResult.SipPlanCreated plan ->
return! invokeHandler (setStatusCode 201 >=> json (sipPlanResponse plan)) next ctx
| SipPlanWriteResult.SipPlanReplayed plan ->
@@ -1952,6 +1964,7 @@ module App =
{|
planId = result.PlanId
instrumentCode = result.InstrumentCode
+ assetClass = result.AssetClass
amount = cashText result.Amount
frequency = SipPolicy.frequencyText result.Frequency
nextTradeDate = dateText result.NextTradeDate
@@ -1966,7 +1979,7 @@ module App =
|})
|}
- let private advanceSipPlans (repository: FundRepository) (fundIdText: string) : HttpHandler =
+ let private advanceSipPlans (repository: FundRepository) (probes: MarketProbes option) (fundIdText: string) : HttpHandler =
fun next ctx ->
task {
match Guid.TryParse fundIdText with
@@ -1984,12 +1997,27 @@ module App =
endDateText
|> Option.defaultWith (fun () -> ConfirmationPolicy.tradeDateFor DateTimeOffset.UtcNow)
+ let stockQuote =
+ match probes with
+ | Some configured ->
+ Some(fun (code: string) ->
+ match configured.StockQuotes.GetQuote(code, ctx.RequestAborted) with
+ | Ok quote ->
+ Ok
+ {
+ Price = quote.Price
+ Name = quote.Name
+ Suspended = quote.Suspended
+ }
+ | Error failure -> Error(sprintf "%A" failure))
+ | None -> None
+
try
match repository.GetFund fundId with
| None ->
return! invokeHandler (errorResponse 404 "FUND_NOT_FOUND" "fund was not found") next ctx
| Some _ ->
- let advanced = repository.AdvanceSipPlans(fundId, endDate, limit)
+ let advanced = repository.AdvanceSipPlans(fundId, endDate, limit, ?stockQuote = stockQuote)
return!
json
@@ -2397,6 +2425,12 @@ module App =
match quoteResult with
| Error failure ->
return! invokeHandler (marketDataError failure) next ctx
+ | Ok quote when quote.Suspended = Some true ->
+ return!
+ invokeHandler
+ (errorResponse 400 "STOCK_SUSPENDED" "stock is suspended and cannot be traded")
+ next
+ ctx
| Ok quote ->
match quote.Price with
| None ->
@@ -2426,6 +2460,8 @@ module App =
return! invokeHandler (errorResponse 409 "IDEMPOTENCY_CONFLICT" "idempotency key was used with a different request") next ctx
| StockTradeWriteResult.StockTradeInvalid message ->
return! invokeHandler (errorResponse 400 "INVALID_STOCK_TRADE_REQUEST" message) next ctx
+ | StockTradeWriteResult.StockTradeInsufficientFunds message ->
+ return! invokeHandler (errorResponse 400 "INSUFFICIENT_FUNDS" message) next ctx
| StockTradeWriteResult.StockTradeFundNotFound ->
return! invokeHandler (errorResponse 404 "FUND_NOT_FOUND" "fund was not found") next ctx
with _ ->
@@ -3521,7 +3557,7 @@ module App =
GET >=> routef "/funds/%s/capital/deposits" (getCapitalDeposits repository)
POST >=> routef "/funds/%s/sip/plans" (createSipPlan repository)
GET >=> routef "/funds/%s/sip/plans" (getSipPlans repository)
- POST >=> routef "/funds/%s/sip/advance" (advanceSipPlans repository)
+ POST >=> routef "/funds/%s/sip/advance" (advanceSipPlans repository probes)
POST >=> routef "/funds/%s/sip/plans/%s/pause" (fun (fundId, planId) -> setSipPlanStatus repository "paused" fundId planId)
POST >=> routef "/funds/%s/sip/plans/%s/resume" (fun (fundId, planId) -> setSipPlanStatus repository "active" fundId planId)
POST >=> routef "/funds/%s/rebalance/plans" (createRebalancePlan repository)