summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-22 05:23:52 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-22 05:23:52 +0800
commit6abd4c206fbfe8369b3fde4c905619d08f13c5d1 (patch)
treef53ce1746591b640f78cd11d63c8411160059d98 /src
parent545f2a507fd0138f7fd50ed7fb2631d0273db625 (diff)
downloadfund-lab-6abd4c206fbfe8369b3fde4c905619d08f13c5d1.tar.gz
Add SIP plan pause/resume and rebalance plan archive lifecycle (3d-24)
Diffstat (limited to 'src')
-rw-r--r--src/FundLab.Api/App.fs55
-rw-r--r--src/FundLab.Api/Persistence.fs105
-rw-r--r--src/FundLab.Web/App.fs146
-rw-r--r--src/FundLab.Web/src/api.js20
4 files changed, 321 insertions, 5 deletions
diff --git a/src/FundLab.Api/App.fs b/src/FundLab.Api/App.fs
index 138230f..5ff7fdd 100644
--- a/src/FundLab.Api/App.fs
+++ b/src/FundLab.Api/App.fs
@@ -1505,6 +1505,29 @@ module App =
with _ ->
errorResponse 500 "PERSISTENCE_ERROR" "sip plan persistence failed" next ctx
+ let private setSipPlanStatus
+ (repository: FundRepository)
+ (targetStatus: string)
+ (fundIdText: string)
+ (planIdText: string)
+ : HttpHandler =
+ fun next ctx ->
+ match Guid.TryParse fundIdText, Guid.TryParse planIdText with
+ | (false, _), _
+ | _, (false, _) ->
+ errorResponse 400 "INVALID_SIP_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.SetSipPlanStatus(fundId, planId, targetStatus) with
+ | SipPlanStatusResult.SipPlanStatusChanged plan -> json (sipPlanResponse plan) next ctx
+ | SipPlanStatusResult.SipPlanStatusNotFound ->
+ errorResponse 404 "PLAN_NOT_FOUND" "sip plan does not belong to this fund" next ctx
+ with _ ->
+ errorResponse 500 "PERSISTENCE_ERROR" "sip plan status update failed" next ctx
+
let private createInvestmentPlan (repository: FundRepository) (fundIdText: string) : HttpHandler =
fun next ctx ->
task {
@@ -1735,6 +1758,24 @@ module App =
with _ ->
errorResponse 500 "PERSISTENCE_ERROR" "rebalance plan persistence failed" next ctx
+ let private archiveRebalancePlan (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.ArchiveRebalancePlan(fundId, planId) with
+ | RebalanceArchiveResult.RebalancePlanArchived plan -> json (rebalancePlanResponse plan) next ctx
+ | RebalanceArchiveResult.RebalanceArchiveNotFound ->
+ errorResponse 404 "PLAN_NOT_FOUND" "rebalance plan does not belong to this fund" next ctx
+ with _ ->
+ errorResponse 500 "PERSISTENCE_ERROR" "rebalance plan archive failed" next ctx
+
let private executeRebalancePlan (repository: FundRepository) (fundIdText: string) (planIdText: string) : HttpHandler =
fun next ctx ->
match Guid.TryParse fundIdText, Guid.TryParse planIdText with
@@ -1749,7 +1790,10 @@ module App =
| 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
+ if message.Contains("is archived") then
+ errorResponse 409 "PLAN_ARCHIVED" "rebalance plan is archived" next ctx
+ else
+ 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
@@ -1787,7 +1831,11 @@ module App =
errorResponse 404 "PLAN_NOT_FOUND" "rebalance plan does not belong to this fund" next ctx
else
match repository.PreviewRebalancePlan planId with
- | Error message -> errorResponse 400 "INVALID_REBALANCE_REQUEST" message next ctx
+ | Error message ->
+ if message.Contains("is archived") then
+ errorResponse 409 "PLAN_ARCHIVED" "rebalance plan is archived" next ctx
+ else
+ errorResponse 400 "INVALID_REBALANCE_REQUEST" message next ctx
| Ok preview -> json (rebalancePreviewResponse preview) next ctx
with _ ->
errorResponse 500 "PERSISTENCE_ERROR" "rebalance preview failed" next ctx
@@ -2275,8 +2323,11 @@ 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/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)
GET >=> routef "/funds/%s/rebalance/plans" (getRebalancePlans repository)
+ POST >=> routef "/funds/%s/rebalance/plans/%s/archive" (fun (fundId, planId) -> archiveRebalancePlan repository fundId planId)
POST >=> routef "/funds/%s/rebalance/plans/%s/execute" (fun (fundId, planId) -> executeRebalancePlan repository fundId planId)
GET >=> routef "/funds/%s/rebalance/plans/%s/preview" (fun (fundId, planId) -> previewRebalancePlan repository fundId planId)
GET >=> routef "/funds/%s/rebalance/executions" (getRebalanceExecutions repository)
diff --git a/src/FundLab.Api/Persistence.fs b/src/FundLab.Api/Persistence.fs
index 5000161..379c10d 100644
--- a/src/FundLab.Api/Persistence.fs
+++ b/src/FundLab.Api/Persistence.fs
@@ -389,6 +389,10 @@ type SipPlanWriteResult =
| SipPlanFundNotFound
| SipPlanInstrumentNotFound
+type SipPlanStatusResult =
+ | SipPlanStatusChanged of SipPlanRecord
+ | SipPlanStatusNotFound
+
type SipExecutionOutcome =
{
TradeDate: DateOnly
@@ -494,6 +498,10 @@ type RebalanceWriteResult =
| RebalanceFundNotFound
| RebalanceInstrumentNotFound
+type RebalanceArchiveResult =
+ | RebalancePlanArchived of RebalancePlanRecord
+ | RebalanceArchiveNotFound
+
type RebalanceOrderOutcome =
{
InstrumentCode: string
@@ -3347,6 +3355,54 @@ type FundRepository(connectionString: string) =
records |> Seq.toList
+ member _.SetSipPlanStatus(fundId: Guid, planId: Guid, status: string) : SipPlanStatusResult =
+ use connection = new NpgsqlConnection(connectionString)
+ connection.Open()
+ use transaction = connection.BeginTransaction(IsolationLevel.ReadCommitted)
+
+ try
+ use lockCommand =
+ commandWithTransaction
+ connection
+ (Some transaction)
+ "SELECT pg_advisory_xact_lock(hashtext(@lock_key))"
+
+ addParameter lockCommand "lock_key" NpgsqlDbType.Text (box (sprintf "sip-plan-status:%O" planId)) |> ignore
+ lockCommand.ExecuteNonQuery() |> ignore
+
+ match findSipPlan connection (Some transaction) planId with
+ | Some plan when plan.FundId = fundId ->
+ if plan.Status = status then
+ transaction.Commit()
+ SipPlanStatusResult.SipPlanStatusChanged plan
+ else
+ use updateCommand =
+ commandWithTransaction
+ connection
+ (Some transaction)
+ "UPDATE sip_plans SET status = @status WHERE id = @plan_id"
+
+ addParameter updateCommand "status" NpgsqlDbType.Text (box status) |> ignore
+ addParameter updateCommand "plan_id" NpgsqlDbType.Uuid (box planId) |> ignore
+ updateCommand.ExecuteNonQuery() |> ignore
+
+ let updated = findSipPlan connection (Some transaction) planId
+ transaction.Commit()
+
+ match updated with
+ | Some updatedPlan -> SipPlanStatusResult.SipPlanStatusChanged updatedPlan
+ | None -> SipPlanStatusResult.SipPlanStatusNotFound
+ | _ ->
+ transaction.Rollback()
+ SipPlanStatusResult.SipPlanStatusNotFound
+ with error ->
+ try
+ transaction.Rollback()
+ with _ ->
+ ()
+
+ raise error
+
member _.CreateInvestmentPlan(idempotencyKey: string, fundId: Guid, command: InvestmentPlanCommand, ?anchorOverride: DateOnly) : InvestmentPlanWriteResult =
if String.IsNullOrWhiteSpace idempotencyKey then
InvestmentPlanWriteResult.InvestmentPlanInvalid "idempotency key cannot be empty"
@@ -3838,6 +3894,53 @@ type FundRepository(connectionString: string) =
reader.Close()
plans |> Seq.toList |> List.map (rebalancePlanWithTargets connection None)
+ member _.ArchiveRebalancePlan(fundId: Guid, planId: Guid) : RebalanceArchiveResult =
+ use connection = new NpgsqlConnection(connectionString)
+ connection.Open()
+ use transaction = connection.BeginTransaction(IsolationLevel.ReadCommitted)
+
+ try
+ use lockCommand =
+ commandWithTransaction
+ connection
+ (Some transaction)
+ "SELECT pg_advisory_xact_lock(hashtext(@lock_key))"
+
+ addParameter lockCommand "lock_key" NpgsqlDbType.Text (box (sprintf "rebalance-archive:%O" planId)) |> ignore
+ lockCommand.ExecuteNonQuery() |> ignore
+
+ match findRebalancePlan connection (Some transaction) planId with
+ | Some plan when plan.FundId = fundId ->
+ if plan.Status = "archived" then
+ transaction.Commit()
+ RebalanceArchiveResult.RebalancePlanArchived plan
+ else
+ use updateCommand =
+ commandWithTransaction
+ connection
+ (Some transaction)
+ "UPDATE rebalance_plans SET status = 'archived' WHERE id = @plan_id"
+
+ addParameter updateCommand "plan_id" NpgsqlDbType.Uuid (box planId) |> ignore
+ updateCommand.ExecuteNonQuery() |> ignore
+
+ let updated = findRebalancePlan connection (Some transaction) planId
+ transaction.Commit()
+
+ match updated with
+ | Some updatedPlan -> RebalanceArchiveResult.RebalancePlanArchived updatedPlan
+ | None -> RebalanceArchiveResult.RebalanceArchiveNotFound
+ | _ ->
+ transaction.Rollback()
+ RebalanceArchiveResult.RebalanceArchiveNotFound
+ with error ->
+ try
+ transaction.Rollback()
+ with _ ->
+ ()
+
+ raise error
+
member this.ExecuteRebalancePlan(planId: Guid) : Result<RebalanceExecutionResult, string> =
use connection = new NpgsqlConnection(connectionString)
connection.Open()
@@ -3868,6 +3971,7 @@ type FundRepository(connectionString: string) =
match plan with
| None -> Error "rebalance plan was not found"
+ | Some plan when plan.Status = "archived" -> Error "rebalance plan is archived"
| Some plan ->
let fundId = plan.FundId
let runDate = ConfirmationPolicy.tradeDateFor DateTimeOffset.UtcNow
@@ -4033,6 +4137,7 @@ type FundRepository(connectionString: string) =
match findRebalancePlan connection None planId with
| None -> Error "rebalance plan was not found"
+ | Some plan when plan.Status = "archived" -> Error "rebalance plan is archived"
| Some plan ->
match this.GetFund plan.FundId with
| None -> Error "fund was not found"
diff --git a/src/FundLab.Web/App.fs b/src/FundLab.Web/App.fs
index 62bb20e..7dea8bb 100644
--- a/src/FundLab.Web/App.fs
+++ b/src/FundLab.Web/App.fs
@@ -1005,6 +1005,9 @@ module Api =
[<Import("getRebalancePlans", "./src/api.js")>]
let getRebalancePlans (token: string) (fundId: string) : JS.Promise<RawRebalancePlan array> = jsNative
+ [<Import("archiveRebalancePlan", "./src/api.js")>]
+ let archiveRebalancePlan (token: string) (fundId: string) (planId: string) : JS.Promise<RawRebalancePlan> = jsNative
+
[<Import("executeRebalancePlan", "./src/api.js")>]
let executeRebalancePlan (token: string) (fundId: string) (planId: string) : JS.Promise<RawRebalanceExecution> = jsNative
@@ -1020,6 +1023,12 @@ module Api =
[<Import("getSipPlans", "./src/api.js")>]
let getSipPlans (token: string) (fundId: string) : JS.Promise<RawSipPlan array> = jsNative
+ [<Import("pauseSipPlan", "./src/api.js")>]
+ let pauseSipPlan (token: string) (fundId: string) (planId: string) : JS.Promise<RawSipPlan> = jsNative
+
+ [<Import("resumeSipPlan", "./src/api.js")>]
+ let resumeSipPlan (token: string) (fundId: string) (planId: string) : JS.Promise<RawSipPlan> = jsNative
+
[<Import("getInvestmentPlans", "./src/api.js")>]
let getInvestmentPlans (token: string) (fundId: string) : JS.Promise<RawInvestmentPlan array> = jsNative
@@ -1411,11 +1420,15 @@ type Model =
sipCreateSeq: int
sipReadSeq: int
sipInFlight: bool
+ sipStatusSeq: int
+ sipStatusInFlight: bool
lastSipAttempt: SipAttempt option
sipPlans: SipPlan list
rebalTargets: RebalanceTargetDraft list
rebalCreateSeq: int
rebalReadSeq: int
+ rebalArchiveSeq: int
+ rebalArchiveInFlight: bool
rebalInFlight: bool
rebalExecuteSeq: int
rebalExecuteInFlight: bool
@@ -1548,6 +1561,10 @@ type Msg =
| SipPlansReadRequested
| SipPlansReadCompleted of requestId: int * plans: RawSipPlan array
| SipPlansReadFailed of requestId: int * message: string
+ | SipPlanPauseRequested of planId: string
+ | SipPlanResumeRequested of planId: string
+ | SipPlanStatusChanged of requestId: int * plan: RawSipPlan
+ | SipPlanStatusChangeFailed of requestId: int * message: string
| RebalTargetCodeChanged of index: int * value: string
| RebalTargetPercentChanged of index: int * value: string
| RebalTargetAdded
@@ -1558,6 +1575,9 @@ type Msg =
| RebalancePlansReadRequested
| RebalancePlansReadCompleted of requestId: int * plans: RawRebalancePlan array
| RebalancePlansReadFailed of requestId: int * message: string
+ | RebalancePlanArchiveRequested of planId: string
+ | RebalancePlanArchived of requestId: int * plan: RawRebalancePlan
+ | RebalancePlanArchiveFailed of requestId: int * message: string
| RebalancePreviewRequested of planId: string
| RebalancePreviewCompleted of requestId: int * preview: RawRebalancePreview
| RebalancePreviewFailed of requestId: int * message: string
@@ -1705,11 +1725,15 @@ let init () =
sipCreateSeq = 0
sipReadSeq = 0
sipInFlight = false
+ sipStatusSeq = 0
+ sipStatusInFlight = false
lastSipAttempt = None
sipPlans = []
rebalTargets = [ { code = ""; percent = "" }; { code = ""; percent = "" } ]
rebalCreateSeq = 0
rebalReadSeq = 0
+ rebalArchiveSeq = 0
+ rebalArchiveInFlight = false
rebalInFlight = false
rebalExecuteSeq = 0
rebalExecuteInFlight = false
@@ -1894,6 +1918,20 @@ let private readSipPlansCommand token fundId requestId =
(fun plans -> SipPlansReadCompleted(requestId, plans))
(fun error -> SipPlansReadFailed(requestId, errorText error))
+let private pauseSipPlanCommand token fundId planId requestId =
+ Cmd.OfPromise.either
+ (fun () -> Api.pauseSipPlan token fundId planId)
+ ()
+ (fun plan -> SipPlanStatusChanged(requestId, plan))
+ (fun error -> SipPlanStatusChangeFailed(requestId, errorText error))
+
+let private resumeSipPlanCommand token fundId planId requestId =
+ Cmd.OfPromise.either
+ (fun () -> Api.resumeSipPlan token fundId planId)
+ ()
+ (fun plan -> SipPlanStatusChanged(requestId, plan))
+ (fun error -> SipPlanStatusChangeFailed(requestId, errorText error))
+
let private createRebalancePlanCommand token fundId payload requestId =
Cmd.OfPromise.either
(fun () -> Api.createRebalancePlan token fundId payload)
@@ -1908,6 +1946,13 @@ let private readRebalancePlansCommand token fundId requestId =
(fun plans -> RebalancePlansReadCompleted(requestId, plans))
(fun error -> RebalancePlansReadFailed(requestId, errorText error))
+let private archiveRebalancePlanCommand token fundId planId requestId =
+ Cmd.OfPromise.either
+ (fun () -> Api.archiveRebalancePlan token fundId planId)
+ ()
+ (fun plan -> RebalancePlanArchived(requestId, plan))
+ (fun error -> RebalancePlanArchiveFailed(requestId, errorText error))
+
let private executeRebalanceCommand token fundId planId requestId =
Cmd.OfPromise.either
(fun () -> Api.executeRebalancePlan token fundId planId)
@@ -2027,6 +2072,18 @@ let private runInvestmentPlansCommand token fundId requestId =
(fun run -> InvestmentPlansRunCompleted(requestId, fundId, run))
(fun error -> InvestmentPlansRunFailed(requestId, fundId, errorText error))
+let private dispatchSipStatusChange model planId pause =
+ match model.createdFund with
+ | Some fund when not (String.IsNullOrWhiteSpace model.token) && not model.sipStatusInFlight ->
+ let requestId = model.sipStatusSeq + 1
+
+ { model with sipStatusSeq = requestId; sipStatusInFlight = true; error = None },
+ (if pause then
+ pauseSipPlanCommand model.token fund.id planId requestId
+ else
+ resumeSipPlanCommand model.token fund.id planId requestId)
+ | _ -> model, Cmd.none
+
let update message model =
match message with
| TokenChanged token ->
@@ -2834,6 +2891,28 @@ let update message model =
{ model with error = Some message }, Cmd.none
else
model, Cmd.none
+ | SipPlanPauseRequested planId -> dispatchSipStatusChange model planId true
+ | SipPlanResumeRequested planId -> dispatchSipStatusChange model planId false
+ | SipPlanStatusChanged (requestId, plan) ->
+ if requestId = model.sipStatusSeq then
+ let decoded = Api.decodeSipPlan plan
+
+ {
+ model with
+ sipStatusInFlight = false
+ sipPlans =
+ model.sipPlans
+ |> List.map (fun existing -> if existing.id = decoded.id then decoded else existing)
+ error = None
+ },
+ Cmd.none
+ else
+ model, Cmd.none
+ | SipPlanStatusChangeFailed (requestId, message) ->
+ if requestId = model.sipStatusSeq then
+ { model with sipStatusInFlight = false; error = Some message }, Cmd.none
+ else
+ model, Cmd.none
| RebalTargetCodeChanged (index, value) ->
{
model with
@@ -2958,6 +3037,33 @@ let update message model =
{ model with error = Some message }, Cmd.none
else
model, Cmd.none
+ | RebalancePlanArchiveRequested planId ->
+ match model.createdFund with
+ | Some fund when not (String.IsNullOrWhiteSpace model.token) && not model.rebalArchiveInFlight ->
+ let requestId = model.rebalArchiveSeq + 1
+ { model with rebalArchiveSeq = requestId; rebalArchiveInFlight = true; error = None },
+ archiveRebalancePlanCommand model.token fund.id planId requestId
+ | _ -> model, Cmd.none
+ | RebalancePlanArchived (requestId, plan) ->
+ if requestId = model.rebalArchiveSeq then
+ let decoded = Api.decodeRebalancePlan plan
+
+ {
+ model with
+ rebalArchiveInFlight = false
+ rebalPlans =
+ model.rebalPlans
+ |> List.map (fun existing -> if existing.id = decoded.id then decoded else existing)
+ error = None
+ },
+ Cmd.none
+ else
+ model, Cmd.none
+ | RebalancePlanArchiveFailed (requestId, message) ->
+ if requestId = model.rebalArchiveSeq then
+ { model with rebalArchiveInFlight = false; error = Some message }, Cmd.none
+ else
+ model, Cmd.none
| RebalancePreviewRequested planId ->
match model.createdFund with
| Some fund when not (String.IsNullOrWhiteSpace model.token) && not model.rebalPreviewInFlight ->
@@ -4394,7 +4500,7 @@ let private sipFrequencyText (frequency: string) =
elif frequency = "monthly" then "每月"
else frequency
-let private sipPlanRow (plan: SipPlan) =
+let private sipPlanRow dispatch (plan: SipPlan) =
Html.div [
prop.className "order-row"
prop.children [
@@ -4418,7 +4524,31 @@ let private sipPlanRow (plan: SipPlan) =
| None -> "最近执行 暂无"
Html.span [ prop.className "order-cell"; prop.text lastExecution ]
- Html.span [ prop.className "order-status"; prop.text (if plan.status = "active" then "进行中" else plan.status) ]
+ Html.span [
+ prop.className "order-status"
+ prop.text (
+ if plan.status = "active" then "进行中"
+ elif plan.status = "paused" then "已暂停"
+ else plan.status
+ )
+ ]
+ Html.span [
+ prop.className "order-cell sip-plan-actions"
+ prop.children [
+ if plan.status = "paused" then
+ Html.button [
+ prop.className "secondary-action sip-resume-action"
+ prop.onClick (fun _ -> dispatch (SipPlanResumeRequested plan.id))
+ prop.text "恢复"
+ ]
+ else
+ Html.button [
+ prop.className "secondary-action sip-pause-action"
+ prop.onClick (fun _ -> dispatch (SipPlanPauseRequested plan.id))
+ prop.text "暂停"
+ ]
+ ]
+ ]
]
]
@@ -4497,7 +4627,7 @@ let private sipPanel model dispatch =
if List.isEmpty model.sipPlans then
Html.p [ prop.className "hint"; prop.text "暂无定投计划" ]
else
- yield! (model.sipPlans |> List.map sipPlanRow)
+ yield! (model.sipPlans |> List.map (sipPlanRow dispatch))
]
]
Html.div [
@@ -4691,6 +4821,10 @@ let private rebalancePanel model dispatch =
|> List.map (fun target -> sprintf "%s %s%%" target.instrumentCode target.targetPercent)
|> String.concat " / ")
]
+ Html.span [
+ prop.className "order-status"
+ prop.text (if plan.status = "archived" then "已归档" else "进行中")
+ ]
(if plan.status = "active" then
Html.span [
prop.className "order-actions"
@@ -4707,6 +4841,12 @@ let private rebalancePanel model dispatch =
prop.onClick (fun _ -> dispatch (RebalanceExecuteRequested plan.id))
prop.text ((if model.rebalExecuteInFlight then "执行中..." else "执行再平衡"): string)
]
+ Html.button [
+ prop.className "secondary-action rebalance-archive-action"
+ prop.disabled model.rebalArchiveInFlight
+ prop.onClick (fun _ -> dispatch (RebalancePlanArchiveRequested plan.id))
+ prop.text "归档"
+ ]
]
]
else
diff --git a/src/FundLab.Web/src/api.js b/src/FundLab.Web/src/api.js
index 031ff74..a03b97f 100644
--- a/src/FundLab.Web/src/api.js
+++ b/src/FundLab.Web/src/api.js
@@ -199,6 +199,18 @@ export function getSipPlans(token, fundId) {
return requestJson(`/api/funds/${encodeURIComponent(fundId)}/sip/plans`, token);
}
+export function pauseSipPlan(token, fundId, planId) {
+ return requestJson(`/api/funds/${encodeURIComponent(fundId)}/sip/plans/${encodeURIComponent(planId)}/pause`, token, {
+ method: "POST"
+ });
+}
+
+export function resumeSipPlan(token, fundId, planId) {
+ return requestJson(`/api/funds/${encodeURIComponent(fundId)}/sip/plans/${encodeURIComponent(planId)}/resume`, token, {
+ method: "POST"
+ });
+}
+
export function getInvestmentPlans(token, fundId) {
return requestJson(`/api/funds/${encodeURIComponent(fundId)}/investment-plans`, token);
}
@@ -227,6 +239,14 @@ export function getRebalancePlans(token, fundId) {
return requestJson(`/api/funds/${encodeURIComponent(fundId)}/rebalance/plans`, token);
}
+export function archiveRebalancePlan(token, fundId, planId) {
+ return requestJson(`/api/funds/${encodeURIComponent(fundId)}/rebalance/plans/${encodeURIComponent(planId)}/archive`, token, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: "{}"
+ });
+}
+
export function executeRebalancePlan(token, fundId, planId) {
return requestJson(
`/api/funds/${encodeURIComponent(fundId)}/rebalance/plans/${encodeURIComponent(planId)}/execute`,