summaryrefslogtreecommitdiff
path: root/src/FundLab.Domain/Sip.fs
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-21 12:38:56 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-21 12:38:56 +0800
commitd2a547b6a60ea769092894fff1f2b5105954f226 (patch)
tree5dfd493d926891509258ef133c0a54b0fac2363f /src/FundLab.Domain/Sip.fs
parentf4ab0b08d7648914f6fd049fa1887c0422f69318 (diff)
downloadfund-lab-d2a547b6a60ea769092894fff1f2b5105954f226.tar.gz
Drive SIP plans through the shared order pipeline (3d-6)
Advance executes due periods as real subscription orders: each due trade date claims a sip_executions row (PRIMARY KEY plan_id+trade_date makes same-plan same-day runs atomic), places the order through CreateSubscriptionOrder with a deterministic idempotency key (sip:{plan}:{date}) and the period's trade date, then confirms through the shared ConfirmSubscriptionOrder pipeline with key sip-confirm:{plan}:{date} — identical freeze/fee/units math as manual orders, nothing bypasses confirmation. Outcomes: succeeded, pending_nav (cash stays frozen; later drives retry the confirmation once the NAV lands), insufficient_cash (no order placed, period fails visibly, reruns keep the marker), failed. Plans roll their next_trade_date past the processed window and repeats of the same endDate replay recorded executions instead of re-debiting. Domain adds SipPolicy.advancePlan (due-date enumeration; weekends-only trading-calendar approximation, noted in code). POST /funds/{id}/sip/advance {endDate?, limit?} drives execution on demand (no daemon). Plan responses/rows now carry last execution status/date; front-end shows 已执行/待净值/现金不足 per plan. Optional tradeDateOverride/anchorOverride keep manual flows untouched.
Diffstat (limited to 'src/FundLab.Domain/Sip.fs')
-rw-r--r--src/FundLab.Domain/Sip.fs19
1 files changed, 19 insertions, 0 deletions
diff --git a/src/FundLab.Domain/Sip.fs b/src/FundLab.Domain/Sip.fs
index 2db7fbd..28e0896 100644
--- a/src/FundLab.Domain/Sip.fs
+++ b/src/FundLab.Domain/Sip.fs
@@ -82,6 +82,25 @@ module SipPolicy =
let months = max 0 candidateMonths
rollToWeekday (addMonthsPreservingDay anchorDate months)
+ /// Due trade dates a plan must run through, from its current nextTradeDate up to and
+ /// including endDate. Calendar approximation: only weekends are skipped (Chinese fund
+ /// trading calendar approximated by weekdays; public holidays are not modeled here —
+ /// noted per the no-fabrication rule, holidays may be added to the calendar later).
+ let advancePlan
+ (frequency: SipFrequency)
+ (anchorDate: DateOnly)
+ (dueFrom: DateOnly)
+ (endDate: DateOnly)
+ : DateOnly list =
+ let rec loop (current: DateOnly) (acc: DateOnly list) =
+ if current > endDate then
+ List.rev acc
+ else
+ let following = nextTradeDate frequency anchorDate (current.AddDays 1)
+ loop following (current :: acc)
+
+ if dueFrom > endDate then [] else loop dueFrom []
+
/// Execution is out of scope for this slice. These pure helpers define the
/// due/settlement contract that the (future) executor will implement: when a
/// plan is due, the net amount debited equals the plan amount exactly, and no