From 6abd4c206fbfe8369b3fde4c905619d08f13c5d1 Mon Sep 17 00:00:00 2001 From: "Somhairle H. Marisol" Date: Tue, 22 Sep 2026 05:23:52 +0800 Subject: Add SIP plan pause/resume and rebalance plan archive lifecycle (3d-24) --- src/FundLab.Api/Persistence.fs | 105 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) (limited to 'src/FundLab.Api/Persistence.fs') 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 = 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" -- cgit v1.2.3