1use crate::context::ContextId;
7use crate::errors::SisterResult;
8use crate::types::{Metadata, SisterType, UniqueId};
9use chrono::{DateTime, Utc};
10use serde::{Deserialize, Serialize};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
14pub struct ReceiptId(pub UniqueId);
15
16impl ReceiptId {
17 pub fn new() -> Self {
18 Self(UniqueId::new())
19 }
20}
21
22impl Default for ReceiptId {
23 fn default() -> Self {
24 Self::new()
25 }
26}
27
28impl std::fmt::Display for ReceiptId {
29 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30 write!(f, "rcpt_{}", self.0)
31 }
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
36#[serde(tag = "status", rename_all = "snake_case")]
37pub enum ActionOutcome {
38 Success {
40 #[serde(skip_serializing_if = "Option::is_none")]
41 result: Option<serde_json::Value>,
42 },
43
44 Failure {
46 error_code: String,
47 error_message: String,
48 },
49
50 Partial {
52 #[serde(skip_serializing_if = "Option::is_none")]
53 result: Option<serde_json::Value>,
54 warnings: Vec<String>,
55 },
56}
57
58impl ActionOutcome {
59 pub fn success() -> Self {
60 Self::Success { result: None }
61 }
62
63 pub fn success_with(result: impl Serialize) -> Self {
64 Self::Success {
65 result: serde_json::to_value(result).ok(),
66 }
67 }
68
69 pub fn failure(code: impl Into<String>, message: impl Into<String>) -> Self {
70 Self::Failure {
71 error_code: code.into(),
72 error_message: message.into(),
73 }
74 }
75
76 pub fn partial(warnings: Vec<String>) -> Self {
77 Self::Partial {
78 result: None,
79 warnings,
80 }
81 }
82
83 pub fn is_success(&self) -> bool {
84 matches!(self, Self::Success { .. })
85 }
86
87 pub fn is_failure(&self) -> bool {
88 matches!(self, Self::Failure { .. })
89 }
90}
91
92#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct ActionRecord {
95 pub sister_type: SisterType,
97
98 pub action_type: String,
100
101 #[serde(default)]
103 pub parameters: Metadata,
104
105 pub outcome: ActionOutcome,
107
108 #[serde(default)]
110 pub evidence_ids: Vec<String>,
111
112 #[serde(skip_serializing_if = "Option::is_none")]
114 pub context_id: Option<ContextId>,
115
116 pub timestamp: DateTime<Utc>,
118}
119
120impl ActionRecord {
121 pub fn new(
123 sister_type: SisterType,
124 action_type: impl Into<String>,
125 outcome: ActionOutcome,
126 ) -> Self {
127 Self {
128 sister_type,
129 action_type: action_type.into(),
130 parameters: Metadata::new(),
131 outcome,
132 evidence_ids: vec![],
133 context_id: None,
134 timestamp: Utc::now(),
135 }
136 }
137
138 pub fn param(mut self, key: impl Into<String>, value: impl Serialize) -> Self {
140 if let Ok(v) = serde_json::to_value(value) {
141 self.parameters.insert(key.into(), v);
142 }
143 self
144 }
145
146 pub fn evidence(mut self, evidence_id: impl Into<String>) -> Self {
148 self.evidence_ids.push(evidence_id.into());
149 self
150 }
151
152 pub fn in_context(mut self, context_id: ContextId) -> Self {
154 self.context_id = Some(context_id);
155 self
156 }
157}
158
159#[derive(Debug, Clone, Serialize, Deserialize)]
161pub struct Receipt {
162 pub id: ReceiptId,
164
165 pub action: ActionRecord,
167
168 pub signature: String,
170
171 pub chain_position: u64,
173
174 pub previous_hash: String,
176
177 pub hash: String,
179
180 pub created_at: DateTime<Utc>,
182}
183
184impl Receipt {
185 pub fn verify_signature(&self, _public_key: &[u8]) -> bool {
188 !self.signature.is_empty()
191 }
192
193 pub fn action_type(&self) -> &str {
195 &self.action.action_type
196 }
197
198 pub fn was_successful(&self) -> bool {
200 self.action.outcome.is_success()
201 }
202}
203
204#[derive(Debug, Clone, Default, Serialize, Deserialize)]
206pub struct ReceiptFilter {
207 #[serde(skip_serializing_if = "Option::is_none")]
209 pub sister_type: Option<SisterType>,
210
211 #[serde(skip_serializing_if = "Option::is_none")]
213 pub action_type: Option<String>,
214
215 #[serde(skip_serializing_if = "Option::is_none")]
217 pub context_id: Option<ContextId>,
218
219 #[serde(skip_serializing_if = "Option::is_none")]
221 pub after: Option<DateTime<Utc>>,
222
223 #[serde(skip_serializing_if = "Option::is_none")]
225 pub before: Option<DateTime<Utc>>,
226
227 #[serde(skip_serializing_if = "Option::is_none")]
229 pub outcome: Option<String>, #[serde(skip_serializing_if = "Option::is_none")]
233 pub limit: Option<usize>,
234
235 #[serde(skip_serializing_if = "Option::is_none")]
237 pub offset: Option<usize>,
238}
239
240impl ReceiptFilter {
241 pub fn new() -> Self {
242 Self::default()
243 }
244
245 pub fn for_sister(mut self, sister_type: SisterType) -> Self {
246 self.sister_type = Some(sister_type);
247 self
248 }
249
250 pub fn action(mut self, action_type: impl Into<String>) -> Self {
251 self.action_type = Some(action_type.into());
252 self
253 }
254
255 pub fn in_context(mut self, context_id: ContextId) -> Self {
256 self.context_id = Some(context_id);
257 self
258 }
259
260 pub fn after(mut self, time: DateTime<Utc>) -> Self {
261 self.after = Some(time);
262 self
263 }
264
265 pub fn before(mut self, time: DateTime<Utc>) -> Self {
266 self.before = Some(time);
267 self
268 }
269
270 pub fn successful_only(mut self) -> Self {
271 self.outcome = Some("success".to_string());
272 self
273 }
274
275 pub fn limit(mut self, limit: usize) -> Self {
276 self.limit = Some(limit);
277 self
278 }
279}
280
281pub trait ReceiptIntegration {
286 fn create_receipt(&self, action: ActionRecord) -> SisterResult<ReceiptId>;
288
289 fn get_receipt(&self, id: ReceiptId) -> SisterResult<Receipt>;
291
292 fn list_receipts(&self, filter: ReceiptFilter) -> SisterResult<Vec<Receipt>>;
294
295 fn receipt_count(&self) -> SisterResult<u64> {
297 self.list_receipts(ReceiptFilter::new())
298 .map(|r| r.len() as u64)
299 }
300
301 fn receipts_for_action(&self, action_type: &str) -> SisterResult<Vec<Receipt>> {
303 self.list_receipts(ReceiptFilter::new().action(action_type))
304 }
305}
306
307pub struct ActionBuilder {
309 sister_type: SisterType,
310 action_type: String,
311}
312
313impl ActionBuilder {
314 pub fn new(sister_type: SisterType, action_type: impl Into<String>) -> Self {
315 Self {
316 sister_type,
317 action_type: action_type.into(),
318 }
319 }
320
321 pub fn success(self) -> ActionRecord {
322 ActionRecord::new(self.sister_type, self.action_type, ActionOutcome::success())
323 }
324
325 pub fn success_with(self, result: impl Serialize) -> ActionRecord {
326 ActionRecord::new(
327 self.sister_type,
328 self.action_type,
329 ActionOutcome::success_with(result),
330 )
331 }
332
333 pub fn failure(self, code: impl Into<String>, message: impl Into<String>) -> ActionRecord {
334 ActionRecord::new(
335 self.sister_type,
336 self.action_type,
337 ActionOutcome::failure(code, message),
338 )
339 }
340}
341
342#[cfg(test)]
343mod tests {
344 use super::*;
345
346 #[test]
347 fn test_action_record() {
348 let record = ActionRecord::new(SisterType::Memory, "memory_add", ActionOutcome::success())
349 .param("content", "test memory")
350 .evidence("ev_123");
351
352 assert_eq!(record.sister_type, SisterType::Memory);
353 assert_eq!(record.action_type, "memory_add");
354 assert!(record.outcome.is_success());
355 assert_eq!(record.evidence_ids, vec!["ev_123"]);
356 }
357
358 #[test]
359 fn test_action_builder() {
360 let record = ActionBuilder::new(SisterType::Vision, "vision_capture")
361 .success_with(serde_json::json!({"capture_id": "cap_123"}));
362
363 assert_eq!(record.action_type, "vision_capture");
364 assert!(record.outcome.is_success());
365 }
366
367 #[test]
368 fn test_receipt_filter() {
369 let filter = ReceiptFilter::new()
370 .for_sister(SisterType::Memory)
371 .action("memory_add")
372 .successful_only()
373 .limit(10);
374
375 assert_eq!(filter.sister_type, Some(SisterType::Memory));
376 assert_eq!(filter.action_type, Some("memory_add".to_string()));
377 assert_eq!(filter.outcome, Some("success".to_string()));
378 assert_eq!(filter.limit, Some(10));
379 }
380}