diff options
Diffstat (limited to 'src/FundLab.Api/App.fs')
| -rw-r--r-- | src/FundLab.Api/App.fs | 68 |
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) |
