1use chrono::{DateTime, Utc};
6use serde::{Deserialize, Serialize};
7use uuid::Uuid;
8
9use crate::bundling::{Bundle, ProductRelationship};
10use crate::eligibility::EligibilityRule;
11use crate::pricing::PricingRule;
12use crate::rules::CatalogRule;
13
14#[derive(Debug, Clone, Serialize, Deserialize, Default)]
16pub struct CatalogSnapshot {
17 pub pricing_rules: Vec<PricingRule>,
18 pub eligibility_rules: Vec<EligibilityRule>,
19 pub bundles: Vec<Bundle>,
20 pub catalog_rules: Vec<CatalogRule>,
21 pub relationships: Vec<ProductRelationship>,
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct CatalogVersion {
27 pub id: Uuid,
28 pub catalog_id: Uuid,
29 pub version: String,
30 pub description: Option<String>,
31 pub created_at: DateTime<Utc>,
32 pub created_by: Option<Uuid>,
33 pub is_active: bool,
34 pub is_published: bool,
35 pub published_at: Option<DateTime<Utc>>,
36 pub metadata: Option<serde_json::Value>,
37 #[serde(default)]
39 pub snapshot: CatalogSnapshot,
40}
41
42#[derive(Default)]
44pub struct VersionManager {
45 versions: Vec<CatalogVersion>,
46}
47
48impl VersionManager {
49 pub fn new() -> Self {
51 Self {
52 versions: Vec::new(),
53 }
54 }
55
56 pub fn create_version(
58 &mut self,
59 catalog_id: Uuid,
60 version: String,
61 description: Option<String>,
62 created_by: Option<Uuid>,
63 snapshot: CatalogSnapshot,
64 ) -> CatalogVersion {
65 let catalog_version = CatalogVersion {
66 id: Uuid::new_v4(),
67 catalog_id,
68 version,
69 description,
70 created_at: Utc::now(),
71 created_by,
72 is_active: false,
73 is_published: false,
74 published_at: None,
75 metadata: None,
76 snapshot,
77 };
78 self.versions.push(catalog_version.clone());
79 catalog_version
80 }
81
82 pub fn publish_version(&mut self, version_id: Uuid) -> Result<&CatalogVersion, String> {
84 let catalog_id = {
85 let version = self
86 .versions
87 .iter()
88 .find(|v| v.id == version_id)
89 .ok_or_else(|| "Version not found".to_string())?;
90 version.catalog_id
91 };
92
93 for v in self.versions.iter_mut() {
94 if v.catalog_id == catalog_id && v.id != version_id {
95 v.is_active = false;
96 }
97 }
98
99 let version = self
100 .versions
101 .iter_mut()
102 .find(|v| v.id == version_id)
103 .ok_or_else(|| "Version not found".to_string())?;
104 version.is_active = true;
105 version.is_published = true;
106 version.published_at = Some(Utc::now());
107 Ok(version)
108 }
109
110 pub fn rollback_to_version(&mut self, version_id: Uuid) -> Result<&CatalogVersion, String> {
112 self.publish_version(version_id)
113 }
114
115 pub fn get_active_version(&self, catalog_id: Uuid) -> Option<&CatalogVersion> {
117 self.versions
118 .iter()
119 .find(|v| v.catalog_id == catalog_id && v.is_active)
120 }
121
122 pub fn get_active_snapshot(&self, catalog_id: Uuid) -> Option<&CatalogSnapshot> {
124 self.get_active_version(catalog_id).map(|v| &v.snapshot)
125 }
126
127 pub fn get_versions(&self, catalog_id: Uuid) -> Vec<&CatalogVersion> {
129 self.versions
130 .iter()
131 .filter(|v| v.catalog_id == catalog_id)
132 .collect()
133 }
134
135 pub fn compare_versions(
137 &self,
138 version_id_1: Uuid,
139 version_id_2: Uuid,
140 ) -> Result<VersionDiff, String> {
141 let v1 = self
142 .versions
143 .iter()
144 .find(|v| v.id == version_id_1)
145 .ok_or_else(|| "Version 1 not found".to_string())?;
146 let v2 = self
147 .versions
148 .iter()
149 .find(|v| v.id == version_id_2)
150 .ok_or_else(|| "Version 2 not found".to_string())?;
151
152 Ok(VersionDiff {
153 version_1: v1.clone(),
154 version_2: v2.clone(),
155 differences: diff_snapshots(&v1.snapshot, &v2.snapshot),
156 })
157 }
158}
159
160fn diff_snapshots(a: &CatalogSnapshot, b: &CatalogSnapshot) -> Vec<String> {
161 let mut diffs = Vec::new();
162 if a.pricing_rules.len() != b.pricing_rules.len() {
163 diffs.push(format!(
164 "pricing_rules: {} → {}",
165 a.pricing_rules.len(),
166 b.pricing_rules.len()
167 ));
168 }
169 if a.eligibility_rules.len() != b.eligibility_rules.len() {
170 diffs.push(format!(
171 "eligibility_rules: {} → {}",
172 a.eligibility_rules.len(),
173 b.eligibility_rules.len()
174 ));
175 }
176 if a.bundles.len() != b.bundles.len() {
177 diffs.push(format!("bundles: {} → {}", a.bundles.len(), b.bundles.len()));
178 }
179 if a.catalog_rules.len() != b.catalog_rules.len() {
180 diffs.push(format!(
181 "catalog_rules: {} → {}",
182 a.catalog_rules.len(),
183 b.catalog_rules.len()
184 ));
185 }
186 if a.relationships.len() != b.relationships.len() {
187 diffs.push(format!(
188 "relationships: {} → {}",
189 a.relationships.len(),
190 b.relationships.len()
191 ));
192 }
193
194 let a_price_ids: std::collections::HashSet<_> =
195 a.pricing_rules.iter().map(|r| r.id).collect();
196 let b_price_ids: std::collections::HashSet<_> =
197 b.pricing_rules.iter().map(|r| r.id).collect();
198 for id in a_price_ids.difference(&b_price_ids) {
199 diffs.push(format!("pricing_rule removed: {id}"));
200 }
201 for id in b_price_ids.difference(&a_price_ids) {
202 diffs.push(format!("pricing_rule added: {id}"));
203 }
204
205 diffs
206}
207
208#[derive(Debug, Clone, Serialize, Deserialize)]
210pub struct VersionDiff {
211 pub version_1: CatalogVersion,
212 pub version_2: CatalogVersion,
213 pub differences: Vec<String>,
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219 use crate::pricing::{Money, PriceType, PricingRule};
220
221 #[test]
222 fn publish_and_diff_snapshots() {
223 let catalog_id = Uuid::new_v4();
224 let mut vm = VersionManager::new();
225 let empty = CatalogSnapshot::default();
226 let v1 = vm.create_version(catalog_id, "1.0.0".into(), None, None, empty);
227
228 let mut snap2 = CatalogSnapshot::default();
229 snap2.pricing_rules.push(PricingRule {
230 id: Uuid::new_v4(),
231 product_offering_id: Uuid::new_v4(),
232 price_type: PriceType::OneTime,
233 base_price: Money {
234 value: 10.0,
235 unit: "USD".into(),
236 },
237 priority: 1,
238 discount_rules: None,
239 valid_for: None,
240 });
241 let v2 = vm.create_version(
242 catalog_id,
243 "1.1.0".into(),
244 Some("add price".into()),
245 None,
246 snap2,
247 );
248
249 vm.publish_version(v2.id).unwrap();
250 assert_eq!(
251 vm.get_active_version(catalog_id).unwrap().version,
252 "1.1.0"
253 );
254
255 let diff = vm.compare_versions(v1.id, v2.id).unwrap();
256 assert!(!diff.differences.is_empty());
257 }
258}