diff options
Diffstat (limited to 'src/FundLab.Api/App.fs')
| -rw-r--r-- | src/FundLab.Api/App.fs | 92 |
1 files changed, 92 insertions, 0 deletions
diff --git a/src/FundLab.Api/App.fs b/src/FundLab.Api/App.fs index 9f88468..9badcfa 100644 --- a/src/FundLab.Api/App.fs +++ b/src/FundLab.Api/App.fs @@ -142,6 +142,8 @@ type SipPlanResponse = status: string anchorDate: string nextTradeDate: string + lastExecutionStatus: string option + lastExecutionDate: string option isSynthetic: bool createdAt: string } @@ -306,6 +308,8 @@ module App = status = plan.Status anchorDate = dateText plan.AnchorDate nextTradeDate = dateText plan.NextTradeDate + lastExecutionStatus = plan.LastExecutionStatus + lastExecutionDate = plan.LastExecutionDate |> Option.map dateText isSynthetic = plan.IsSynthetic createdAt = timestampText plan.CreatedAt } @@ -810,6 +814,93 @@ module App = json (plans |> List.map sipPlanResponse) next ctx with _ -> errorResponse 500 "PERSISTENCE_ERROR" "sip plan persistence failed" next ctx + + let private parseSipAdvanceCommand (body: string) = + try + if String.IsNullOrWhiteSpace body then + Ok(None, 24) + else + use document = JsonDocument.Parse(body) + let root = document.RootElement + + if root.ValueKind <> JsonValueKind.Object then + Error "request body must be a JSON object" + else + let endDate = + match tryStringProperty root "endDate" with + | None -> None + | Some text -> + match DateOnly.TryParseExact(text, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None) with + | true, date -> Some date + | _ -> failwith "endDate must be yyyy-MM-dd" + + let limit = + match tryStringProperty root "limit" with + | None -> 24 + | Some text -> + match Int32.TryParse(text, CultureInfo.InvariantCulture) with + | true, value when value > 0 -> value + | _ -> failwith "limit must be a positive integer" + + Ok(endDate, limit) + with + | :? JsonException -> Error "request body must be valid JSON" + + let private sipPlanAdvanceResponse (result: SipPlanAdvanceResult) = + {| + planId = result.PlanId + instrumentCode = result.InstrumentCode + amount = cashText result.Amount + frequency = SipPolicy.frequencyText result.Frequency + nextTradeDate = dateText result.NextTradeDate + executions = + result.Executions + |> List.map (fun outcome -> + {| + tradeDate = dateText outcome.TradeDate + status = outcome.Status + orderId = outcome.OrderId |> Option.map (fun id -> id.ToString("D")) + pendingReason = outcome.PendingReason + |}) + |} + + let private advanceSipPlans (repository: FundRepository) (fundIdText: string) : HttpHandler = + fun next ctx -> + task { + match Guid.TryParse fundIdText with + | false, _ -> + return! invokeHandler (errorResponse 400 "INVALID_SIP_REQUEST" "fund id must be a UUID") next ctx + | true, fundId -> + use reader = new StreamReader(ctx.Request.Body) + let! body = reader.ReadToEndAsync() + + match parseSipAdvanceCommand body with + | Error message -> + return! invokeHandler (errorResponse 400 "INVALID_SIP_REQUEST" message) next ctx + | Ok(endDateText, limit) -> + let endDate = + endDateText + |> Option.defaultWith (fun () -> ConfirmationPolicy.tradeDateFor DateTimeOffset.UtcNow) + + 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) + + return! + json + {| + fundId = fundId + endDate = dateText endDate + plans = advanced.Plans |> List.map sipPlanAdvanceResponse + |} + next + ctx + with _ -> + return! invokeHandler (errorResponse 500 "PERSISTENCE_ERROR" "sip advance failed") next ctx + } let private marketDataError (failure: MarketDataFailure) : HttpHandler = let status, error, message = match failure with @@ -916,6 +1007,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) GET >=> routef "/funds/%s" (getFund repository) ] @ (marketData |> Option.map marketDataRoutes |> Option.defaultValue []) |
