1use time::{Duration, OffsetDateTime};
2
3use crate::entities::ceremony_events::{
4 CeremonyInstanceStarted, MemoryRecalled, SuccessionCarried,
5};
6use crate::entities::{
7 CeremonyDefinition, CeremonyEvent, CeremonyInstance, PublishedCeremonyDefinition,
8};
9use crate::error::DomainError;
10use crate::value_objects::{
11 BudgetAccountId, CeremonyContext, CeremonyDeadline, CeremonyDefinitionDigest, CeremonyId,
12 CeremonyLineage, CeremonySuccession, SessionRecollection, StateDeadline, StateVisit,
13 SuccessionPlan,
14};
15
16impl CeremonyInstance {
17 pub fn decide_start(
29 id: CeremonyId,
30 definition: &CeremonyDefinition,
31 context: CeremonyContext,
32 recollection: Option<SessionRecollection>,
33 now: OffsetDateTime,
34 ) -> Result<Vec<CeremonyEvent>, DomainError> {
35 Ok(Self::opening_batch(
36 Self::opening(id, definition, context, now, None, None, None)?,
37 recollection,
38 now,
39 ))
40 }
41
42 pub fn decide_start_bound(
45 id: CeremonyId,
46 published: &PublishedCeremonyDefinition,
47 context: CeremonyContext,
48 recollection: Option<SessionRecollection>,
49 now: OffsetDateTime,
50 ) -> Result<Vec<CeremonyEvent>, DomainError> {
51 Ok(Self::opening_batch(
52 Self::opening(
53 id,
54 published.definition(),
55 context,
56 now,
57 Some(published.digest()),
58 None,
59 None,
60 )?,
61 recollection,
62 now,
63 ))
64 }
65
66 pub fn decide_start_bound_child(
68 id: CeremonyId,
69 published: &PublishedCeremonyDefinition,
70 context: CeremonyContext,
71 lineage: CeremonyLineage,
72 recollection: Option<SessionRecollection>,
73 now: OffsetDateTime,
74 ) -> Result<Vec<CeremonyEvent>, DomainError> {
75 Ok(Self::opening_batch(
76 Self::opening(
77 id,
78 published.definition(),
79 context,
80 now,
81 Some(published.digest()),
82 Some(lineage),
83 None,
84 )?,
85 recollection,
86 now,
87 ))
88 }
89
90 pub fn decide_start_bound_budgeted(
92 id: CeremonyId,
93 published: &PublishedCeremonyDefinition,
94 context: CeremonyContext,
95 budget_account_id: BudgetAccountId,
96 recollection: Option<SessionRecollection>,
97 now: OffsetDateTime,
98 ) -> Result<Vec<CeremonyEvent>, DomainError> {
99 Ok(Self::opening_batch(
100 Self::opening(
101 id,
102 published.definition(),
103 context,
104 now,
105 Some(published.digest()),
106 None,
107 Some(budget_account_id),
108 )?,
109 recollection,
110 now,
111 ))
112 }
113
114 pub fn decide_start_bound_budgeted_child(
116 id: CeremonyId,
117 published: &PublishedCeremonyDefinition,
118 context: CeremonyContext,
119 lineage: CeremonyLineage,
120 budget_account_id: BudgetAccountId,
121 recollection: Option<SessionRecollection>,
122 now: OffsetDateTime,
123 ) -> Result<Vec<CeremonyEvent>, DomainError> {
124 Ok(Self::opening_batch(
125 Self::opening(
126 id,
127 published.definition(),
128 context,
129 now,
130 Some(published.digest()),
131 Some(lineage),
132 Some(budget_account_id),
133 )?,
134 recollection,
135 now,
136 ))
137 }
138
139 pub fn decide_start_successor(
153 id: CeremonyId,
154 published: &PublishedCeremonyDefinition,
155 context: CeremonyContext,
156 succession: CeremonySuccession,
157 plan: &SuccessionPlan,
158 now: OffsetDateTime,
159 ) -> Result<Vec<CeremonyEvent>, DomainError> {
160 let mut opening = Self::opening(
161 id,
162 published.definition(),
163 context,
164 now,
165 Some(published.digest()),
166 None,
167 None,
168 )?;
169 opening.succession = Some(Box::new(succession));
170 let mut events = vec![CeremonyEvent::CeremonyInstanceStarted(opening)];
171 if !plan.carried().is_empty() {
172 events.push(CeremonyEvent::SuccessionCarried(SuccessionCarried {
173 carried: plan.carried().to_vec(),
174 carried_at: now,
175 }));
176 }
177 Ok(events)
178 }
179
180 fn opening_batch(
189 opening: CeremonyInstanceStarted,
190 recollection: Option<SessionRecollection>,
191 now: OffsetDateTime,
192 ) -> Vec<CeremonyEvent> {
193 let mut events = vec![CeremonyEvent::CeremonyInstanceStarted(opening)];
194 if let Some(recollection) = recollection.filter(|recalled| !recalled.is_empty()) {
195 events.push(CeremonyEvent::MemoryRecalled(MemoryRecalled {
196 recollection,
197 recalled_at: now,
198 }));
199 }
200 events
201 }
202
203 pub(in crate::entities::ceremony_instance) fn opening(
206 id: CeremonyId,
207 definition: &CeremonyDefinition,
208 context: CeremonyContext,
209 now: OffsetDateTime,
210 bound_definition: Option<CeremonyDefinitionDigest>,
211 lineage: Option<CeremonyLineage>,
212 budget_account_id: Option<BudgetAccountId>,
213 ) -> Result<CeremonyInstanceStarted, DomainError> {
214 let missing = definition
215 .inputs()
216 .values()
217 .filter(|input| input.requirement().is_required())
218 .filter(|input| context.attributes().get(input.name().as_str()).is_none())
219 .map(|input| input.name().as_str())
220 .collect::<Vec<_>>();
221 if !missing.is_empty() {
222 return Err(DomainError::InvalidDocument {
223 reason: format!("missing required ceremony inputs: {}", missing.join(", ")),
224 });
225 }
226 let ceremony_deadline = definition
227 .ceremony_timeout()
228 .map(|timeout| checked_deadline(now, timeout.duration(), "ceremony_deadline"))
229 .transpose()?
230 .map(CeremonyDeadline::new);
231 let state_deadline = definition
232 .state_timeout()
233 .map(|timeout| checked_deadline(now, timeout.duration(), "state_deadline"))
234 .transpose()?
235 .map(|at| {
236 StateDeadline::new(definition.initial_state_id().clone(), StateVisit::FIRST, at)
237 });
238 Ok(CeremonyInstanceStarted {
239 ceremony_id: id,
240 definition_name: definition.name().clone(),
241 definition_version: definition.version().clone(),
242 initial_state: definition.initial_state_id().clone(),
243 step_ids: definition.steps().keys().cloned().collect(),
244 context,
245 bound_definition,
246 lineage,
247 succession: None,
248 budget_account_id,
249 ceremony_deadline,
250 state_deadline,
251 created_at: now,
252 })
253 }
254}
255
256pub(crate) fn checked_deadline(
257 now: OffsetDateTime,
258 duration: crate::value_objects::DurationMs,
259 field: &'static str,
260) -> Result<OffsetDateTime, DomainError> {
261 let millis = i64::try_from(duration.get()).map_err(|_| DomainError::OutOfRange {
262 field,
263 value: duration.get() as f64,
264 min: 0.0,
265 max: i64::MAX as f64,
266 })?;
267 now.checked_add(Duration::milliseconds(millis))
268 .ok_or(DomainError::OutOfRange {
269 field,
270 value: duration.get() as f64,
271 min: 0.0,
272 max: i64::MAX as f64,
273 })
274}