made_core/entities/
proposal.rs1use serde::{Deserialize, Serialize};
9use time::OffsetDateTime;
10
11use crate::error::DomainError;
12use crate::value_objects::{AgentId, Attributes, ProposalContent, ProposalId, Specialty};
13
14#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
16pub struct Proposal {
17 id: ProposalId,
18 author: AgentId,
19 specialty: Specialty,
20 content: ProposalContent,
21 attributes: Attributes,
22 #[serde(with = "time::serde::rfc3339")]
23 created_at: OffsetDateTime,
24 #[serde(with = "time::serde::rfc3339")]
25 updated_at: OffsetDateTime,
26 revision_count: u32,
27}
28
29impl Proposal {
30 pub fn new(
34 id: ProposalId,
35 author: AgentId,
36 specialty: Specialty,
37 content: impl Into<ProposalContent>,
38 attributes: Attributes,
39 now: OffsetDateTime,
40 ) -> Result<Self, DomainError> {
41 let content = content.into();
42 if content.as_str().trim().is_empty() {
43 return Err(DomainError::EmptyField {
44 field: "proposal.content",
45 });
46 }
47 Ok(Self {
48 id,
49 author,
50 specialty,
51 content,
52 attributes,
53 created_at: now,
54 updated_at: now,
55 revision_count: 0,
56 })
57 }
58
59 pub fn revise(
62 &mut self,
63 new_content: impl Into<ProposalContent>,
64 now: OffsetDateTime,
65 ) -> Result<(), DomainError> {
66 let content = new_content.into();
67 if content.as_str().trim().is_empty() {
68 return Err(DomainError::EmptyField {
69 field: "proposal.content",
70 });
71 }
72 self.content = content;
73 self.updated_at = now;
74 self.revision_count = self.revision_count.saturating_add(1);
75 Ok(())
76 }
77
78 #[must_use]
79 pub fn id(&self) -> &ProposalId {
80 &self.id
81 }
82 #[must_use]
83 pub fn author(&self) -> &AgentId {
84 &self.author
85 }
86 #[must_use]
87 pub fn specialty(&self) -> &Specialty {
88 &self.specialty
89 }
90 #[must_use]
91 pub const fn content(&self) -> &ProposalContent {
92 &self.content
93 }
94 #[must_use]
95 pub fn attributes(&self) -> &Attributes {
96 &self.attributes
97 }
98 #[must_use]
99 pub fn created_at(&self) -> OffsetDateTime {
100 self.created_at
101 }
102 #[must_use]
103 pub fn updated_at(&self) -> OffsetDateTime {
104 self.updated_at
105 }
106 #[must_use]
107 pub fn revision_count(&self) -> u32 {
108 self.revision_count
109 }
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115 use time::macros::datetime;
116
117 fn at() -> OffsetDateTime {
118 datetime!(2026-04-15 12:00:00 UTC)
119 }
120
121 fn make(content: &str) -> Result<Proposal, DomainError> {
122 Proposal::new(
123 ProposalId::new("p1").unwrap(),
124 AgentId::new("a1").unwrap(),
125 Specialty::new("triage").unwrap(),
126 content,
127 Attributes::empty(),
128 at(),
129 )
130 }
131
132 #[test]
133 fn new_requires_non_empty_content() {
134 assert!(matches!(
135 make("").unwrap_err(),
136 DomainError::EmptyField {
137 field: "proposal.content"
138 }
139 ));
140 assert!(matches!(
141 make(" \n").unwrap_err(),
142 DomainError::EmptyField { .. }
143 ));
144 }
145
146 #[test]
147 fn new_seeds_timestamps_and_zero_revisions() {
148 let p = make("plan A").unwrap();
149 assert_eq!(p.created_at(), at());
150 assert_eq!(p.updated_at(), at());
151 assert_eq!(p.revision_count(), 0);
152 assert_eq!(p.content(), "plan A");
153 }
154
155 #[test]
156 fn revise_updates_content_and_bumps_revision() {
157 let mut p = make("plan A").unwrap();
158 let later = datetime!(2026-04-15 12:00:05 UTC);
159 p.revise("plan A prime", later).unwrap();
160 assert_eq!(p.content(), "plan A prime");
161 assert_eq!(p.revision_count(), 1);
162 assert_eq!(p.updated_at(), later);
163 assert_eq!(p.created_at(), at()); }
165
166 #[test]
167 fn revise_preserves_identity() {
168 let mut p = make("x").unwrap();
169 let id_before = p.id().clone();
170 p.revise("y", at()).unwrap();
171 assert_eq!(p.id(), &id_before);
172 }
173
174 #[test]
175 fn revise_rejects_empty_content() {
176 let mut p = make("x").unwrap();
177 assert!(p.revise(" ", at()).is_err());
178 assert_eq!(p.content(), "x");
179 assert_eq!(p.revision_count(), 0);
180 }
181
182 #[test]
183 fn revision_count_saturates() {
184 let mut p = make("x").unwrap();
185 for _ in 0..3 {
186 p.revise("y", at()).unwrap();
187 }
188 assert_eq!(p.revision_count(), 3);
189 }
190}