summaryrefslogtreecommitdiff
path: root/src/FundLab.Api/App.fs
diff options
context:
space:
mode:
Diffstat (limited to 'src/FundLab.Api/App.fs')
-rw-r--r--src/FundLab.Api/App.fs92
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 [])