diff options
Diffstat (limited to 'src/FundLab.Api')
| -rw-r--r-- | src/FundLab.Api/App.fs | 129 |
1 files changed, 99 insertions, 30 deletions
diff --git a/src/FundLab.Api/App.fs b/src/FundLab.Api/App.fs index 3de514e..ac4c174 100644 --- a/src/FundLab.Api/App.fs +++ b/src/FundLab.Api/App.fs @@ -525,37 +525,33 @@ module App = if root.ValueKind <> JsonValueKind.Object then Error "request body must be a JSON object" else - let mutable parsed = None - let mutable failure = None - use targetsElement = document.RootElement.Clone() - - if not (root.TryGetProperty("targets", &targetsElement)) then - failure <- Some "targets is required" - elif targetsElement.ValueKind <> JsonValueKind.Array then - failure <- Some "targets must be an array" + let mutable mutableTargets = Unchecked.defaultof<JsonElement> + + if not (root.TryGetProperty("targets", &mutableTargets)) then + Error "targets is required" else - let rows = ResizeArray<RebalanceTarget>() - let mutable rowError = None - - for item in targetsElement.EnumerateArray() do - if rowError.IsNone then - match tryStringProperty item "instrumentCode", tryStringProperty item "targetPercent" with - | Some code, Some percentText -> - match tryDecimal "targetPercent" percentText with - | Ok percent -> rows.Add({ InstrumentCode = code; TargetPercent = percent }) - | Error message -> rowError <- Some message - | _ -> rowError <- Some "each target needs instrumentCode and targetPercent" - - failure <- rowError - - if failure.IsNone then parsed <- Some { Targets = rows |> Seq.toList } - - match failure with - | Some message -> Error message - | None -> - match parsed with - | Some command -> Ok command - | None -> Error "targets is required" + + let targetsElement = mutableTargets + if targetsElement.ValueKind <> JsonValueKind.Array then + Error "targets must be an array" + else + let mutable failure = None + let rows = ResizeArray<RebalanceTarget>() + + for item in targetsElement.EnumerateArray() do + if failure.IsNone then + match tryStringProperty item "instrumentCode", tryStringProperty item "targetPercent" with + | Some code, Some percentText -> + match tryDecimal "targetPercent" percentText with + | Ok percent -> + let targetRow: RebalancePolicy.TargetAllocation = { InstrumentCode = code; TargetPercent = percent } + rows.Add(targetRow) + | Error message -> failure <- Some message + | _ -> failure <- Some "each target needs instrumentCode and targetPercent" + + match failure with + | Some message -> Error message + | None -> Ok { Targets = rows |> Seq.toList } with | :? JsonException -> Error "request body must be valid JSON" @@ -992,6 +988,76 @@ module App = with _ -> return! invokeHandler (errorResponse 500 "PERSISTENCE_ERROR" "sip advance failed") next ctx } + + let private createRebalancePlan (repository: FundRepository) (fundIdText: string) : HttpHandler = + fun next ctx -> + task { + match Guid.TryParse fundIdText with + | false, _ -> + return! invokeHandler (errorResponse 400 "INVALID_REBALANCE_REQUEST" "fund id must be a UUID") next ctx + | true, fundId -> + use reader = new StreamReader(ctx.Request.Body) + let! body = reader.ReadToEndAsync() + let idempotencyKey = ctx.Request.Headers["Idempotency-Key"].ToString() + + match parseRebalancePlanCommand body with + | Error message -> + return! invokeHandler (errorResponse 400 "INVALID_REBALANCE_REQUEST" message) next ctx + | Ok command -> + try + match repository.CreateRebalancePlan(idempotencyKey, fundId, command) with + | RebalanceWriteResult.RebalancePlanCreated plan -> + return! invokeHandler (setStatusCode 201 >=> json (rebalancePlanResponse plan)) next ctx + | RebalanceWriteResult.RebalancePlanReplayed plan -> + return! invokeHandler (json (rebalancePlanResponse plan)) next ctx + | RebalanceWriteResult.RebalanceIdempotencyConflict -> + return! invokeHandler (errorResponse 409 "IDEMPOTENCY_CONFLICT" "idempotency key was used with a different request") next ctx + | RebalanceWriteResult.RebalanceInvalid message -> + return! invokeHandler (errorResponse 400 "INVALID_REBALANCE_REQUEST" message) next ctx + | RebalanceWriteResult.RebalanceFundNotFound -> + return! invokeHandler (errorResponse 404 "FUND_NOT_FOUND" "fund was not found") next ctx + | RebalanceWriteResult.RebalanceInstrumentNotFound -> + return! invokeHandler (errorResponse 404 "INSTRUMENT_NOT_FOUND" "instrument code was not found in the instrument catalog") next ctx + with _ -> + return! invokeHandler (errorResponse 500 "PERSISTENCE_ERROR" "rebalance plan persistence failed") next ctx + } + + let private getRebalancePlans (repository: FundRepository) (fundIdText: string) : HttpHandler = + fun next ctx -> + match Guid.TryParse fundIdText with + | false, _ -> errorResponse 400 "INVALID_FUND_ID" "fund id must be a UUID" next ctx + | true, fundId -> + try + match repository.GetFund fundId with + | None -> errorResponse 404 "FUND_NOT_FOUND" "fund was not found" next ctx + | Some _ -> + let plans = repository.GetRebalancePlans fundId + json (plans |> List.map rebalancePlanResponse) next ctx + with _ -> + errorResponse 500 "PERSISTENCE_ERROR" "rebalance plan persistence failed" next ctx + + let private executeRebalancePlan (repository: FundRepository) (fundIdText: string) (planIdText: string) : HttpHandler = + fun next ctx -> + match Guid.TryParse fundIdText, Guid.TryParse planIdText with + | (false, _), _ + | _, (false, _) -> + errorResponse 400 "INVALID_REBALANCE_REQUEST" "fund id and plan id must be UUIDs" next ctx + | (true, fundId), (true, planId) -> + try + match repository.GetFund fundId with + | None -> + errorResponse 404 "FUND_NOT_FOUND" "fund was not found" next ctx + | Some _ -> + match repository.ExecuteRebalancePlan planId with + | Error message -> + errorResponse (if message.Contains("was not found") then 404 else 400) "INVALID_REBALANCE_REQUEST" message next ctx + | Ok result -> + if result.PlanId <> Guid.Empty && repository.GetRebalancePlans fundId |> List.exists (fun plan -> plan.Id = result.PlanId) |> not then + errorResponse 404 "PLAN_NOT_FOUND" "rebalance plan does not belong to this fund" next ctx + else + json (rebalanceExecutionResponse result) next ctx + with _ -> + errorResponse 500 "PERSISTENCE_ERROR" "rebalance execution failed" next ctx let private marketDataError (failure: MarketDataFailure) : HttpHandler = let status, error, message = match failure with @@ -1099,6 +1165,9 @@ module App = 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/rebalance/plans" (createRebalancePlan repository) + GET >=> routef "/funds/%s/rebalance/plans" (getRebalancePlans repository) + POST >=> routef "/funds/%s/rebalance/plans/%s/execute" (fun (fundId, planId) -> executeRebalancePlan repository fundId planId) GET >=> routef "/funds/%s" (getFund repository) ] @ (marketData |> Option.map marketDataRoutes |> Option.defaultValue []) |
