backbone_inventory/domain/entity/
stock_item.rs1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use sqlx::FromRow;
4use uuid::Uuid;
5use rust_decimal::Decimal;
6
7use super::ValuationMethod;
8use super::ServiceTrackingType;
9use super::AuditMetadata;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
13#[serde(transparent)]
14pub struct StockItemId(pub Uuid);
15
16impl StockItemId {
17 pub fn new(id: Uuid) -> Self { Self(id) }
18 pub fn generate() -> Self { Self(Uuid::new_v4()) }
19 pub fn into_inner(self) -> Uuid { self.0 }
20}
21
22impl std::fmt::Display for StockItemId {
23 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24 write!(f, "{}", self.0)
25 }
26}
27
28impl std::str::FromStr for StockItemId {
29 type Err = uuid::Error;
30 fn from_str(s: &str) -> Result<Self, Self::Err> {
31 Ok(Self(Uuid::parse_str(s)?))
32 }
33}
34
35impl From<Uuid> for StockItemId {
36 fn from(id: Uuid) -> Self { Self(id) }
37}
38
39impl From<StockItemId> for Uuid {
40 fn from(id: StockItemId) -> Self { id.0 }
41}
42
43impl AsRef<Uuid> for StockItemId {
44 fn as_ref(&self) -> &Uuid { &self.0 }
45}
46
47impl std::ops::Deref for StockItemId {
48 type Target = Uuid;
49 fn deref(&self) -> &Self::Target { &self.0 }
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
53pub struct StockItem {
54 pub id: Uuid,
55 pub item_id: Uuid,
56 pub stock_uom: String,
57 pub is_stock_item: bool,
58 pub has_batch: bool,
59 pub valuation_method: ValuationMethod,
60 pub reorder_level: Decimal,
61 pub weight_per_unit: Decimal,
62 pub service_tracking: ServiceTrackingType,
63 pub service_project_id: Option<Uuid>,
64 pub service_project_template_id: Option<Uuid>,
65 #[serde(default)]
66 #[sqlx(json)]
67 pub metadata: AuditMetadata,
68}
69
70impl StockItem {
71 pub fn builder() -> StockItemBuilder {
73 <StockItemBuilder as Default>::default()
74 }
75
76 pub fn new(item_id: Uuid, stock_uom: String, is_stock_item: bool, has_batch: bool, valuation_method: ValuationMethod, reorder_level: Decimal, weight_per_unit: Decimal, service_tracking: ServiceTrackingType) -> Self {
78 Self {
79 id: Uuid::new_v4(),
80 item_id,
81 stock_uom,
82 is_stock_item,
83 has_batch,
84 valuation_method,
85 reorder_level,
86 weight_per_unit,
87 service_tracking,
88 service_project_id: None,
89 service_project_template_id: None,
90 metadata: AuditMetadata::default(),
91 }
92 }
93
94 pub fn id(&self) -> &Uuid {
96 &self.id
97 }
98
99 pub fn typed_id(&self) -> StockItemId {
101 StockItemId(self.id)
102 }
103
104 pub fn created_at(&self) -> Option<&DateTime<Utc>> {
106 self.metadata.created_at.as_ref()
107 }
108
109 pub fn updated_at(&self) -> Option<&DateTime<Utc>> {
111 self.metadata.updated_at.as_ref()
112 }
113
114 pub fn is_deleted(&self) -> bool {
116 self.metadata.deleted_at.is_some()
117 }
118
119 pub fn is_active(&self) -> bool {
121 self.metadata.deleted_at.is_none()
122 }
123
124 pub fn deleted_at(&self) -> Option<&DateTime<Utc>> {
126 self.metadata.deleted_at.as_ref()
127 }
128
129 pub fn created_by(&self) -> Option<&Uuid> {
131 self.metadata.created_by.as_ref()
132 }
133
134 pub fn updated_by(&self) -> Option<&Uuid> {
136 self.metadata.updated_by.as_ref()
137 }
138
139 pub fn deleted_by(&self) -> Option<&Uuid> {
141 self.metadata.deleted_by.as_ref()
142 }
143
144
145 pub fn with_service_project_id(mut self, value: Uuid) -> Self {
151 self.service_project_id = Some(value);
152 self
153 }
154
155 pub fn with_service_project_template_id(mut self, value: Uuid) -> Self {
157 self.service_project_template_id = Some(value);
158 self
159 }
160
161 pub fn apply_patch(&mut self, fields: std::collections::HashMap<String, serde_json::Value>) {
167 for (key, value) in fields {
168 match key.as_str() {
169 "item_id" => {
170 if let Ok(v) = serde_json::from_value(value) { self.item_id = v; }
171 }
172 "stock_uom" => {
173 if let Ok(v) = serde_json::from_value(value) { self.stock_uom = v; }
174 }
175 "is_stock_item" => {
176 if let Ok(v) = serde_json::from_value(value) { self.is_stock_item = v; }
177 }
178 "has_batch" => {
179 if let Ok(v) = serde_json::from_value(value) { self.has_batch = v; }
180 }
181 "valuation_method" => {
182 if let Ok(v) = serde_json::from_value(value) { self.valuation_method = v; }
183 }
184 "reorder_level" => {
185 if let Ok(v) = serde_json::from_value(value) { self.reorder_level = v; }
186 }
187 "weight_per_unit" => {
188 if let Ok(v) = serde_json::from_value(value) { self.weight_per_unit = v; }
189 }
190 "service_tracking" => {
191 if let Ok(v) = serde_json::from_value(value) { self.service_tracking = v; }
192 }
193 "service_project_id" => {
194 if let Ok(v) = serde_json::from_value(value) { self.service_project_id = v; }
195 }
196 "service_project_template_id" => {
197 if let Ok(v) = serde_json::from_value(value) { self.service_project_template_id = v; }
198 }
199 _ => {} }
201 }
202 }
203
204}
205
206impl super::Entity for StockItem {
207 type Id = Uuid;
208
209 fn entity_id(&self) -> &Self::Id {
210 &self.id
211 }
212
213 fn entity_type() -> &'static str {
214 "StockItem"
215 }
216}
217
218impl backbone_core::PersistentEntity for StockItem {
219 fn entity_id(&self) -> String {
220 self.id.to_string()
221 }
222 fn set_entity_id(&mut self, id: String) {
223 if let Ok(uuid) = uuid::Uuid::parse_str(&id) {
224 self.id = uuid;
225 }
226 }
227 fn created_at(&self) -> Option<chrono::DateTime<chrono::Utc>> {
228 self.metadata.created_at
229 }
230 fn set_created_at(&mut self, ts: chrono::DateTime<chrono::Utc>) {
231 self.metadata.created_at = Some(ts);
232 }
233 fn updated_at(&self) -> Option<chrono::DateTime<chrono::Utc>> {
234 self.metadata.updated_at
235 }
236 fn set_updated_at(&mut self, ts: chrono::DateTime<chrono::Utc>) {
237 self.metadata.updated_at = Some(ts);
238 }
239 fn deleted_at(&self) -> Option<chrono::DateTime<chrono::Utc>> {
240 self.metadata.deleted_at
241 }
242 fn set_deleted_at(&mut self, ts: Option<chrono::DateTime<chrono::Utc>>) {
243 self.metadata.deleted_at = ts;
244 }
245}
246
247impl backbone_orm::EntityRepoMeta for StockItem {
248 fn column_types() -> std::collections::HashMap<String, String> {
249 let mut m = std::collections::HashMap::new();
250 m.insert("id".to_string(), "uuid".to_string());
251 m.insert("item_id".to_string(), "uuid".to_string());
252 m.insert("service_project_id".to_string(), "uuid".to_string());
253 m.insert("service_project_template_id".to_string(), "uuid".to_string());
254 m.insert("valuation_method".to_string(), "valuation_method".to_string());
255 m.insert("service_tracking".to_string(), "service_tracking_type".to_string());
256 m
257 }
258 fn search_fields() -> &'static [&'static str] {
259 &["stock_uom"]
260 }
261}
262
263#[derive(Debug, Clone, Default)]
268pub struct StockItemBuilder {
269 item_id: Option<Uuid>,
270 stock_uom: Option<String>,
271 is_stock_item: Option<bool>,
272 has_batch: Option<bool>,
273 valuation_method: Option<ValuationMethod>,
274 reorder_level: Option<Decimal>,
275 weight_per_unit: Option<Decimal>,
276 service_tracking: Option<ServiceTrackingType>,
277 service_project_id: Option<Uuid>,
278 service_project_template_id: Option<Uuid>,
279}
280
281impl StockItemBuilder {
282 pub fn item_id(mut self, value: Uuid) -> Self {
284 self.item_id = Some(value);
285 self
286 }
287
288 pub fn stock_uom(mut self, value: String) -> Self {
290 self.stock_uom = Some(value);
291 self
292 }
293
294 pub fn is_stock_item(mut self, value: bool) -> Self {
296 self.is_stock_item = Some(value);
297 self
298 }
299
300 pub fn has_batch(mut self, value: bool) -> Self {
302 self.has_batch = Some(value);
303 self
304 }
305
306 pub fn valuation_method(mut self, value: ValuationMethod) -> Self {
308 self.valuation_method = Some(value);
309 self
310 }
311
312 pub fn reorder_level(mut self, value: Decimal) -> Self {
314 self.reorder_level = Some(value);
315 self
316 }
317
318 pub fn weight_per_unit(mut self, value: Decimal) -> Self {
320 self.weight_per_unit = Some(value);
321 self
322 }
323
324 pub fn service_tracking(mut self, value: ServiceTrackingType) -> Self {
326 self.service_tracking = Some(value);
327 self
328 }
329
330 pub fn service_project_id(mut self, value: Uuid) -> Self {
332 self.service_project_id = Some(value);
333 self
334 }
335
336 pub fn service_project_template_id(mut self, value: Uuid) -> Self {
338 self.service_project_template_id = Some(value);
339 self
340 }
341
342 pub fn build(self) -> Result<StockItem, String> {
346 let item_id = self.item_id.ok_or_else(|| "item_id is required".to_string())?;
347 let stock_uom = self.stock_uom.ok_or_else(|| "stock_uom is required".to_string())?;
348
349 Ok(StockItem {
350 id: Uuid::new_v4(),
351 item_id,
352 stock_uom,
353 is_stock_item: self.is_stock_item.unwrap_or(true),
354 has_batch: self.has_batch.unwrap_or(false),
355 valuation_method: self.valuation_method.unwrap_or_default(),
356 reorder_level: self.reorder_level.unwrap_or(Decimal::from(0)),
357 weight_per_unit: self.weight_per_unit.unwrap_or(Decimal::from(0)),
358 service_tracking: self.service_tracking.unwrap_or_default(),
359 service_project_id: self.service_project_id,
360 service_project_template_id: self.service_project_template_id,
361 metadata: AuditMetadata::default(),
362 })
363 }
364}