1use super::{
2 CeremonyDefinition, CeremonyEvidencePack, CeremonyEvidenceRequest, CeremonyEvidenceSourceId,
3 CeremonyGuardApproval, CeremonyGuardDeferral, CeremonyInstance, CeremonyIntervention,
4 CeremonyInterventionContent, CeremonyInterventionId, CeremonyInterventionKind,
5 CeremonyInterventionProvenance, CeremonyInterventionResponse, CeremonyInterventionTarget,
6 CeremonyReason, CeremonyReasonKind, CeremonyRecordRef, CeremonyTransitionRecord, DomainError,
7 MemoryConfidence, OffsetDateTime, ReasonAsserter, RoleAction, RoleId,
8};
9
10impl CeremonyInstance {
11 #[allow(clippy::too_many_arguments)]
12 pub fn request_intervention_as(
13 &mut self,
14 definition: &CeremonyDefinition,
15 intervention_id: CeremonyInterventionId,
16 role_id: RoleId,
17 kind: CeremonyInterventionKind,
18 target: CeremonyInterventionTarget,
19 content: CeremonyInterventionContent,
20 now: OffsetDateTime,
21 ) -> Result<(), DomainError> {
22 self.request_intervention_with_provenance_as(
23 definition,
24 intervention_id,
25 role_id,
26 kind,
27 target,
28 content,
29 None,
30 now,
31 )
32 }
33
34 #[allow(clippy::too_many_arguments)]
35 pub fn request_intervention_with_provenance_as(
36 &mut self,
37 definition: &CeremonyDefinition,
38 intervention_id: CeremonyInterventionId,
39 role_id: RoleId,
40 kind: CeremonyInterventionKind,
41 target: CeremonyInterventionTarget,
42 content: CeremonyInterventionContent,
43 provenance: Option<CeremonyInterventionProvenance>,
44 now: OffsetDateTime,
45 ) -> Result<(), DomainError> {
46 self.require_active(
47 definition,
48 "terminal ceremony instances cannot accept interventions",
49 )?;
50 self.require_role(definition, &role_id, &RoleAction::request_intervention())?;
51 Self::require_intervention_target(definition, &target)?;
52 if let Some(provenance) = provenance.as_ref() {
53 self.require_intervention_provenance(definition, &role_id, &target, provenance)?;
54 }
55 if self
56 .interventions
57 .iter()
58 .any(|intervention| intervention.id() == &intervention_id)
59 {
60 return Err(DomainError::AlreadyExists {
61 what: "ceremony_intervention",
62 });
63 }
64 let intervention = CeremonyIntervention::open_with_provenance(
65 intervention_id,
66 kind,
67 role_id,
68 target,
69 content,
70 provenance,
71 now,
72 );
73 self.interventions.push(intervention);
74 self.updated_at = now;
75 Ok(())
76 }
77
78 pub fn respond_to_intervention_as(
79 &mut self,
80 definition: &CeremonyDefinition,
81 intervention_id: &CeremonyInterventionId,
82 role_id: RoleId,
83 content: CeremonyInterventionContent,
84 now: OffsetDateTime,
85 ) -> Result<(), DomainError> {
86 self.require_active(
87 definition,
88 "terminal ceremony instances cannot receive intervention responses",
89 )?;
90 self.require_role(definition, &role_id, &RoleAction::respond_to_intervention())?;
91 self.interventions
92 .iter_mut()
93 .find(|intervention| intervention.id() == intervention_id)
94 .ok_or(DomainError::NotFound {
95 what: "ceremony_intervention",
96 })?
97 .respond(role_id, content, now)?;
98 self.record_that_it_answers(intervention_id, now);
99 self.updated_at = now;
100 Ok(())
101 }
102
103 pub fn prepare_evidence_request_as(
104 &self,
105 definition: &CeremonyDefinition,
106 intervention_id: CeremonyInterventionId,
107 role_id: RoleId,
108 source_id: CeremonyEvidenceSourceId,
109 query: CeremonyInterventionContent,
110 ) -> Result<CeremonyEvidenceRequest, DomainError> {
111 self.require_active(
112 definition,
113 "terminal ceremony instances cannot collect intervention evidence",
114 )?;
115 self.require_role(definition, &role_id, &RoleAction::respond_to_intervention())?;
116 self.intervention(&intervention_id)
117 .ok_or(DomainError::NotFound {
118 what: "ceremony_intervention",
119 })?
120 .ensure_can_respond(&role_id)?;
121 Ok(CeremonyEvidenceRequest::new(
122 self.id.clone(),
123 intervention_id,
124 role_id,
125 source_id,
126 query,
127 self.context.clone(),
128 ))
129 }
130
131 pub fn respond_to_intervention_with_evidence_as(
132 &mut self,
133 definition: &CeremonyDefinition,
134 intervention_id: &CeremonyInterventionId,
135 role_id: RoleId,
136 evidence_pack: CeremonyEvidencePack,
137 now: OffsetDateTime,
138 ) -> Result<(), DomainError> {
139 self.require_active(
140 definition,
141 "terminal ceremony instances cannot receive intervention evidence",
142 )?;
143 self.require_role(definition, &role_id, &RoleAction::respond_to_intervention())?;
144 self.interventions
145 .iter_mut()
146 .find(|intervention| intervention.id() == intervention_id)
147 .ok_or(DomainError::NotFound {
148 what: "ceremony_intervention",
149 })?
150 .respond_with_evidence(role_id, evidence_pack, now)?;
151 self.record_that_it_answers(intervention_id, now);
152 self.updated_at = now;
153 Ok(())
154 }
155
156 #[allow(clippy::too_many_arguments)]
171 pub fn assert_reason_as(
172 &mut self,
173 definition: &CeremonyDefinition,
174 role_id: RoleId,
175 from: CeremonyRecordRef,
176 to: CeremonyRecordRef,
177 kind: CeremonyReasonKind,
178 why: impl Into<String>,
179 confidence: MemoryConfidence,
180 now: OffsetDateTime,
181 ) -> Result<(), DomainError> {
182 self.require_declared_role(definition, &role_id)?;
183 self.require_record(&from)?;
184 self.require_record(&to)?;
185
186 match kind.asserter() {
187 ReasonAsserter::TheEngine => {
188 return Err(DomainError::InvariantViolated {
189 reason:
190 "this kind of reason states the shape of the session, not a judgement, \
191 and only the engine may assert it",
192 });
193 }
194 ReasonAsserter::ItsAuthor => {
195 if self.author_of(&from) != Some(&role_id) {
196 return Err(DomainError::InvariantViolated {
197 reason: "only whoever produced something may say why they decided it or \
198 how they did it",
199 });
200 }
201 }
202 ReasonAsserter::AnySeat => {}
203 }
204
205 self.reasons.push(CeremonyReason::new(
206 from,
207 to,
208 kind,
209 why,
210 confidence,
211 Some(role_id),
212 now,
213 )?);
214 self.updated_at = now;
215 Ok(())
216 }
217
218 fn author_of(&self, record: &CeremonyRecordRef) -> Option<&RoleId> {
225 match record {
226 CeremonyRecordRef::Step { .. } => None,
227 CeremonyRecordRef::AgendaItem { agenda_item } => self
228 .intervention(agenda_item)
229 .map(CeremonyIntervention::requested_by),
230 CeremonyRecordRef::Contribution {
231 agenda_item,
232 ordinal,
233 } => self
234 .intervention(agenda_item)
235 .and_then(|item| item.responses().get(*ordinal as usize))
236 .map(CeremonyInterventionResponse::role_id),
237 CeremonyRecordRef::GuardDecision { guard_name } => self
238 .guard_approvals
239 .iter()
240 .find(|approval| approval.guard_name() == guard_name)
241 .map(CeremonyGuardApproval::approved_by)
242 .or_else(|| {
243 self.guard_deferrals
244 .iter()
245 .find(|deferral| deferral.guard_name() == guard_name)
246 .map(CeremonyGuardDeferral::deferred_by)
247 }),
248 CeremonyRecordRef::Transition { ordinal } => self
249 .transitions
250 .get(ordinal.saturating_sub(1) as usize)
251 .and_then(CeremonyTransitionRecord::applied_by),
252 }
253 }
254
255 fn require_record(&self, record: &CeremonyRecordRef) -> Result<(), DomainError> {
262 let exists = match record {
263 CeremonyRecordRef::Step { step_id } => self.step_records.contains_key(step_id),
264 CeremonyRecordRef::AgendaItem { agenda_item } => {
265 self.intervention(agenda_item).is_some()
266 }
267 CeremonyRecordRef::Contribution {
268 agenda_item,
269 ordinal,
270 } => self
271 .intervention(agenda_item)
272 .is_some_and(|item| item.responses().len() > *ordinal as usize),
273 CeremonyRecordRef::GuardDecision { guard_name } => {
274 self.guard_approvals
275 .iter()
276 .any(|approval| approval.guard_name() == guard_name)
277 || self
278 .guard_deferrals
279 .iter()
280 .any(|deferral| deferral.guard_name() == guard_name)
281 }
282 CeremonyRecordRef::Transition { ordinal } => {
283 *ordinal >= 1 && (*ordinal as usize) <= self.transitions.len()
284 }
285 };
286 if exists {
287 Ok(())
288 } else {
289 Err(DomainError::NotFound {
290 what: "ceremony_record",
291 })
292 }
293 }
294
295 fn record_that_it_answers(
302 &mut self,
303 agenda_item: &CeremonyInterventionId,
304 now: OffsetDateTime,
305 ) {
306 let Some(ordinal) = self
307 .intervention(agenda_item)
308 .map(|item| item.responses().len())
309 .and_then(|count| u32::try_from(count.checked_sub(1)?).ok())
310 else {
311 return;
312 };
313 if let Ok(reason) = CeremonyReason::new(
314 CeremonyRecordRef::contribution(agenda_item.clone(), ordinal),
315 CeremonyRecordRef::agenda_item(agenda_item.clone()),
316 CeremonyReasonKind::Answers,
317 "a contribution made against this agenda item",
318 MemoryConfidence::High,
319 None,
320 now,
321 ) {
322 self.reasons.push(reason);
323 }
324 }
325
326 pub fn close_intervention_as(
327 &mut self,
328 definition: &CeremonyDefinition,
329 intervention_id: &CeremonyInterventionId,
330 role_id: &RoleId,
331 now: OffsetDateTime,
332 ) -> Result<(), DomainError> {
333 self.require_active(
334 definition,
335 "terminal ceremony instances cannot close interventions",
336 )?;
337 self.require_role(definition, role_id, &RoleAction::request_intervention())?;
338 self.interventions
339 .iter_mut()
340 .find(|intervention| intervention.id() == intervention_id)
341 .ok_or(DomainError::NotFound {
342 what: "ceremony_intervention",
343 })?
344 .close(role_id, now)?;
345 self.updated_at = now;
346 Ok(())
347 }
348}