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.fs104
1 files changed, 104 insertions, 0 deletions
diff --git a/src/FundLab.Api/App.fs b/src/FundLab.Api/App.fs
index 55a29f4..9f88468 100644
--- a/src/FundLab.Api/App.fs
+++ b/src/FundLab.Api/App.fs
@@ -4,6 +4,7 @@ open System
open System.Globalization
open System.IO
open System.Text.Json
+open FundLab.Domain
open Giraffe
open Microsoft.AspNetCore.Http
open Microsoft.Extensions.DependencyInjection
@@ -131,6 +132,20 @@ type FundPositionsResponse =
positions: FundPositionResponse list
}
+type SipPlanResponse =
+ {
+ id: Guid
+ fundId: Guid
+ instrumentCode: string
+ amount: string
+ frequency: string
+ status: string
+ anchorDate: string
+ nextTradeDate: string
+ isSynthetic: bool
+ createdAt: string
+ }
+
type CapitalDepositResponse =
{
id: Guid
@@ -281,6 +296,20 @@ module App =
createdAt = timestampText deposit.CreatedAt
}
+ let private sipPlanResponse (plan: SipPlanRecord) : SipPlanResponse =
+ {
+ id = plan.Id
+ fundId = plan.FundId
+ instrumentCode = plan.InstrumentCode
+ amount = cashText plan.Amount
+ frequency = SipPolicy.frequencyText plan.Frequency
+ status = plan.Status
+ anchorDate = dateText plan.AnchorDate
+ nextTradeDate = dateText plan.NextTradeDate
+ isSynthetic = plan.IsSynthetic
+ createdAt = timestampText plan.CreatedAt
+ }
+
let private errorResponse status error message : HttpHandler =
setStatusCode status
>=> json ({
@@ -411,8 +440,34 @@ module App =
with
| :? JsonException -> Error "request body must be valid JSON"
+ let private parseSipPlanCommand (body: string) =
+ try
+ use document = JsonDocument.Parse(body)
+ let root = document.RootElement
+
+ if root.ValueKind <> JsonValueKind.Object then
+ Error "request body must be a JSON object"
+ else
+ 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"
+ | _ ->
+ Error "instrumentCode, amount and frequency are required"
+ with
+ | :? JsonException -> Error "request body must be valid JSON"
+
let private invokeHandler handler next ctx = handler next ctx
+
let private unauthorized : HttpHandler =
setStatusCode 401
>=> setHttpHeader "WWW-Authenticate" "Bearer"
@@ -708,6 +763,53 @@ module App =
json (deposits |> List.map capitalDepositResponse) next ctx
with _ ->
errorResponse 500 "PERSISTENCE_ERROR" "capital deposit persistence failed" next ctx
+
+ let private createSipPlan (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()
+ let idempotencyKey = ctx.Request.Headers["Idempotency-Key"].ToString()
+
+ match parseSipPlanCommand body with
+ | Error message ->
+ return! invokeHandler (errorResponse 400 "INVALID_SIP_REQUEST" message) next ctx
+ | Ok command ->
+ try
+ match repository.CreateSipPlan(idempotencyKey, fundId, command) with
+ | SipPlanWriteResult.SipPlanCreated plan ->
+ return! invokeHandler (setStatusCode 201 >=> json (sipPlanResponse plan)) next ctx
+ | SipPlanWriteResult.SipPlanReplayed plan ->
+ return! invokeHandler (json (sipPlanResponse plan)) next ctx
+ | SipPlanWriteResult.SipPlanIdempotencyConflict ->
+ return! invokeHandler (errorResponse 409 "IDEMPOTENCY_CONFLICT" "idempotency key was used with a different request") next ctx
+ | SipPlanWriteResult.SipPlanInvalid message ->
+ return! invokeHandler (errorResponse 400 "INVALID_SIP_REQUEST" message) next ctx
+ | SipPlanWriteResult.SipPlanFundNotFound ->
+ return! invokeHandler (errorResponse 404 "FUND_NOT_FOUND" "fund was not found") next ctx
+ | SipPlanWriteResult.SipPlanInstrumentNotFound ->
+ 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" "sip plan persistence failed") next ctx
+ }
+
+ let private getSipPlans (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.GetSipPlans fundId
+ json (plans |> List.map sipPlanResponse) next ctx
+ with _ ->
+ errorResponse 500 "PERSISTENCE_ERROR" "sip plan persistence failed" next ctx
let private marketDataError (failure: MarketDataFailure) : HttpHandler =
let status, error, message =
match failure with
@@ -812,6 +914,8 @@ module App =
GET >=> routef "/funds/%s/positions" (getPositions repository)
POST >=> routef "/funds/%s/capital/deposit" (createCapitalDeposit repository)
GET >=> routef "/funds/%s/capital/deposits" (getCapitalDeposits repository)
+ POST >=> routef "/funds/%s/sip/plans" (createSipPlan repository)
+ GET >=> routef "/funds/%s/sip/plans" (getSipPlans repository)
GET >=> routef "/funds/%s" (getFund repository)
]
@ (marketData |> Option.map marketDataRoutes |> Option.defaultValue [])