backbone_inventory/domain/entity/
reordering_rule.rs1use chrono::{DateTime, Utc, NaiveDate};
2use serde::{Deserialize, Serialize};
3use sqlx::FromRow;
4use uuid::Uuid;
5use rust_decimal::Decimal;
6
7use super::OrderpointTrigger;
8use super::AuditMetadata;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
12#[serde(transparent)]
13pub struct ReorderingRuleId(pub Uuid);
14
15impl ReorderingRuleId {
16 pub fn new(id: Uuid) -> Self { Self(id) }
17 pub fn generate() -> Self { Self(Uuid::new_v4()) }
18 pub fn into_inner(self) -> Uuid { self.0 }
19}
20
21impl std::fmt::Display for ReorderingRuleId {
22 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23 write!(f, "{}", self.0)
24 }
25}
26
27impl std::str::FromStr for ReorderingRuleId {
28 type Err = uuid::Error;
29 fn from_str(s: &str) -> Result<Self, Self::Err> {
30 Ok(Self(Uuid::parse_str(s)?))
31 }
32}
33
34impl From<Uuid> for ReorderingRuleId {
35 fn from(id: Uuid) -> Self { Self(id) }
36}
37
38impl From<ReorderingRuleId> for Uuid {
39 fn from(id: ReorderingRuleId) -> Self { id.0 }
40}
41
42impl AsRef<Uuid> for ReorderingRuleId {
43 fn as_ref(&self) -> &Uuid { &self.0 }
44}
45
46impl std::ops::Deref for ReorderingRuleId {
47 type Target = Uuid;
48 fn deref(&self) -> &Self::Target { &self.0 }
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
52pub struct ReorderingRule {
53 pub id: Uuid,
54 pub name: String,
55 pub trigger: OrderpointTrigger,
56 pub active: bool,
57 pub snoozed_until: Option<NaiveDate>,
58 pub item_id: Uuid,
59 pub location_id: Uuid,
60 pub warehouse_id: Uuid,
61 pub item_min_qty: Decimal,
62 pub item_max_qty: Decimal,
63 pub route_id: Option<Uuid>,
64 pub qty_on_hand: Decimal,
65 pub qty_forecast: Decimal,
66 pub qty_to_order: Decimal,
67 pub qty_to_order_manual: Decimal,
68 pub lead_days: Decimal,
69 pub deadline_date: Option<NaiveDate>,
70 #[serde(default)]
71 #[sqlx(json)]
72 pub metadata: AuditMetadata,
73}
74
75impl ReorderingRule {
76 pub fn builder() -> ReorderingRuleBuilder {
78 <ReorderingRuleBuilder as Default>::default()
79 }
80
81 pub fn new(name: String, trigger: OrderpointTrigger, active: bool, item_id: Uuid, location_id: Uuid, warehouse_id: Uuid, item_min_qty: Decimal, item_max_qty: Decimal, qty_on_hand: Decimal, qty_forecast: Decimal, qty_to_order: Decimal, qty_to_order_manual: Decimal, lead_days: Decimal) -> Self {
83 Self {
84 id: Uuid::new_v4(),
85 name,
86 trigger,
87 active,
88 snoozed_until: None,
89 item_id,
90 location_id,
91 warehouse_id,
92 item_min_qty,
93 item_max_qty,
94 route_id: None,
95 qty_on_hand,
96 qty_forecast,
97 qty_to_order,
98 qty_to_order_manual,
99 lead_days,
100 deadline_date: None,
101 metadata: AuditMetadata::default(),
102 }
103 }
104
105 pub fn id(&self) -> &Uuid {
107 &self.id
108 }
109
110 pub fn typed_id(&self) -> ReorderingRuleId {
112 ReorderingRuleId(self.id)
113 }
114
115 pub fn created_at(&self) -> Option<&DateTime<Utc>> {
117 self.metadata.created_at.as_ref()
118 }
119
120 pub fn updated_at(&self) -> Option<&DateTime<Utc>> {
122 self.metadata.updated_at.as_ref()
123 }
124
125 pub fn is_deleted(&self) -> bool {
127 self.metadata.deleted_at.is_some()
128 }
129
130 pub fn is_active(&self) -> bool {
132 self.metadata.deleted_at.is_none()
133 }
134
135 pub fn deleted_at(&self) -> Option<&DateTime<Utc>> {
137 self.metadata.deleted_at.as_ref()
138 }
139
140 pub fn created_by(&self) -> Option<&Uuid> {
142 self.metadata.created_by.as_ref()
143 }
144
145 pub fn updated_by(&self) -> Option<&Uuid> {
147 self.metadata.updated_by.as_ref()
148 }
149
150 pub fn deleted_by(&self) -> Option<&Uuid> {
152 self.metadata.deleted_by.as_ref()
153 }
154
155
156 pub fn with_snoozed_until(mut self, value: NaiveDate) -> Self {
162 self.snoozed_until = Some(value);
163 self
164 }
165
166 pub fn with_route_id(mut self, value: Uuid) -> Self {
168 self.route_id = Some(value);
169 self
170 }
171
172 pub fn with_deadline_date(mut self, value: NaiveDate) -> Self {
174 self.deadline_date = Some(value);
175 self
176 }
177
178 pub fn apply_patch(&mut self, fields: std::collections::HashMap<String, serde_json::Value>) {
184 for (key, value) in fields {
185 match key.as_str() {
186 "name" => {
187 if let Ok(v) = serde_json::from_value(value) { self.name = v; }
188 }
189 "trigger" => {
190 if let Ok(v) = serde_json::from_value(value) { self.trigger = v; }
191 }
192 "active" => {
193 if let Ok(v) = serde_json::from_value(value) { self.active = v; }
194 }
195 "snoozed_until" => {
196 if let Ok(v) = serde_json::from_value(value) { self.snoozed_until = v; }
197 }
198 "item_id" => {
199 if let Ok(v) = serde_json::from_value(value) { self.item_id = v; }
200 }
201 "location_id" => {
202 if let Ok(v) = serde_json::from_value(value) { self.location_id = v; }
203 }
204 "warehouse_id" => {
205 if let Ok(v) = serde_json::from_value(value) { self.warehouse_id = v; }
206 }
207 "item_min_qty" => {
208 if let Ok(v) = serde_json::from_value(value) { self.item_min_qty = v; }
209 }
210 "item_max_qty" => {
211 if let Ok(v) = serde_json::from_value(value) { self.item_max_qty = v; }
212 }
213 "route_id" => {
214 if let Ok(v) = serde_json::from_value(value) { self.route_id = v; }
215 }
216 "qty_on_hand" => {
217 if let Ok(v) = serde_json::from_value(value) { self.qty_on_hand = v; }
218 }
219 "qty_forecast" => {
220 if let Ok(v) = serde_json::from_value(value) { self.qty_forecast = v; }
221 }
222 "qty_to_order" => {
223 if let Ok(v) = serde_json::from_value(value) { self.qty_to_order = v; }
224 }
225 "qty_to_order_manual" => {
226 if let Ok(v) = serde_json::from_value(value) { self.qty_to_order_manual = v; }
227 }
228 "lead_days" => {
229 if let Ok(v) = serde_json::from_value(value) { self.lead_days = v; }
230 }
231 "deadline_date" => {
232 if let Ok(v) = serde_json::from_value(value) { self.deadline_date = v; }
233 }
234 _ => {} }
236 }
237 }
238
239}
240
241impl super::Entity for ReorderingRule {
242 type Id = Uuid;
243
244 fn entity_id(&self) -> &Self::Id {
245 &self.id
246 }
247
248 fn entity_type() -> &'static str {
249 "ReorderingRule"
250 }
251}
252
253impl backbone_core::PersistentEntity for ReorderingRule {
254 fn entity_id(&self) -> String {
255 self.id.to_string()
256 }
257 fn set_entity_id(&mut self, id: String) {
258 if let Ok(uuid) = uuid::Uuid::parse_str(&id) {
259 self.id = uuid;
260 }
261 }
262 fn created_at(&self) -> Option<chrono::DateTime<chrono::Utc>> {
263 self.metadata.created_at
264 }
265 fn set_created_at(&mut self, ts: chrono::DateTime<chrono::Utc>) {
266 self.metadata.created_at = Some(ts);
267 }
268 fn updated_at(&self) -> Option<chrono::DateTime<chrono::Utc>> {
269 self.metadata.updated_at
270 }
271 fn set_updated_at(&mut self, ts: chrono::DateTime<chrono::Utc>) {
272 self.metadata.updated_at = Some(ts);
273 }
274 fn deleted_at(&self) -> Option<chrono::DateTime<chrono::Utc>> {
275 self.metadata.deleted_at
276 }
277 fn set_deleted_at(&mut self, ts: Option<chrono::DateTime<chrono::Utc>>) {
278 self.metadata.deleted_at = ts;
279 }
280}
281
282impl backbone_orm::EntityRepoMeta for ReorderingRule {
283 fn column_types() -> std::collections::HashMap<String, String> {
284 let mut m = std::collections::HashMap::new();
285 m.insert("id".to_string(), "uuid".to_string());
286 m.insert("item_id".to_string(), "uuid".to_string());
287 m.insert("location_id".to_string(), "uuid".to_string());
288 m.insert("warehouse_id".to_string(), "uuid".to_string());
289 m.insert("route_id".to_string(), "uuid".to_string());
290 m.insert("trigger".to_string(), "orderpoint_trigger".to_string());
291 m.insert("snoozed_until".to_string(), "date".to_string());
292 m.insert("deadline_date".to_string(), "date".to_string());
293 m
294 }
295 fn search_fields() -> &'static [&'static str] {
296 &["name"]
297 }
298 fn relations() -> &'static [(&'static str, &'static str, &'static str)] {
299 &[("location", "locations", "locationId"), ("warehouse", "warehouses", "warehouseId"), ("route", "routes", "routeId")]
300 }
301}
302
303#[derive(Debug, Clone, Default)]
308pub struct ReorderingRuleBuilder {
309 name: Option<String>,
310 trigger: Option<OrderpointTrigger>,
311 active: Option<bool>,
312 snoozed_until: Option<NaiveDate>,
313 item_id: Option<Uuid>,
314 location_id: Option<Uuid>,
315 warehouse_id: Option<Uuid>,
316 item_min_qty: Option<Decimal>,
317 item_max_qty: Option<Decimal>,
318 route_id: Option<Uuid>,
319 qty_on_hand: Option<Decimal>,
320 qty_forecast: Option<Decimal>,
321 qty_to_order: Option<Decimal>,
322 qty_to_order_manual: Option<Decimal>,
323 lead_days: Option<Decimal>,
324 deadline_date: Option<NaiveDate>,
325}
326
327impl ReorderingRuleBuilder {
328 pub fn name(mut self, value: String) -> Self {
330 self.name = Some(value);
331 self
332 }
333
334 pub fn trigger(mut self, value: OrderpointTrigger) -> Self {
336 self.trigger = Some(value);
337 self
338 }
339
340 pub fn active(mut self, value: bool) -> Self {
342 self.active = Some(value);
343 self
344 }
345
346 pub fn snoozed_until(mut self, value: NaiveDate) -> Self {
348 self.snoozed_until = Some(value);
349 self
350 }
351
352 pub fn item_id(mut self, value: Uuid) -> Self {
354 self.item_id = Some(value);
355 self
356 }
357
358 pub fn location_id(mut self, value: Uuid) -> Self {
360 self.location_id = Some(value);
361 self
362 }
363
364 pub fn warehouse_id(mut self, value: Uuid) -> Self {
366 self.warehouse_id = Some(value);
367 self
368 }
369
370 pub fn item_min_qty(mut self, value: Decimal) -> Self {
372 self.item_min_qty = Some(value);
373 self
374 }
375
376 pub fn item_max_qty(mut self, value: Decimal) -> Self {
378 self.item_max_qty = Some(value);
379 self
380 }
381
382 pub fn route_id(mut self, value: Uuid) -> Self {
384 self.route_id = Some(value);
385 self
386 }
387
388 pub fn qty_on_hand(mut self, value: Decimal) -> Self {
390 self.qty_on_hand = Some(value);
391 self
392 }
393
394 pub fn qty_forecast(mut self, value: Decimal) -> Self {
396 self.qty_forecast = Some(value);
397 self
398 }
399
400 pub fn qty_to_order(mut self, value: Decimal) -> Self {
402 self.qty_to_order = Some(value);
403 self
404 }
405
406 pub fn qty_to_order_manual(mut self, value: Decimal) -> Self {
408 self.qty_to_order_manual = Some(value);
409 self
410 }
411
412 pub fn lead_days(mut self, value: Decimal) -> Self {
414 self.lead_days = Some(value);
415 self
416 }
417
418 pub fn deadline_date(mut self, value: NaiveDate) -> Self {
420 self.deadline_date = Some(value);
421 self
422 }
423
424 pub fn build(self) -> Result<ReorderingRule, String> {
428 let name = self.name.ok_or_else(|| "name is required".to_string())?;
429 let item_id = self.item_id.ok_or_else(|| "item_id is required".to_string())?;
430 let location_id = self.location_id.ok_or_else(|| "location_id is required".to_string())?;
431 let warehouse_id = self.warehouse_id.ok_or_else(|| "warehouse_id is required".to_string())?;
432
433 Ok(ReorderingRule {
434 id: Uuid::new_v4(),
435 name,
436 trigger: self.trigger.unwrap_or_default(),
437 active: self.active.unwrap_or(true),
438 snoozed_until: self.snoozed_until,
439 item_id,
440 location_id,
441 warehouse_id,
442 item_min_qty: self.item_min_qty.unwrap_or(Decimal::from(0)),
443 item_max_qty: self.item_max_qty.unwrap_or(Decimal::from(0)),
444 route_id: self.route_id,
445 qty_on_hand: self.qty_on_hand.unwrap_or(Decimal::from(0)),
446 qty_forecast: self.qty_forecast.unwrap_or(Decimal::from(0)),
447 qty_to_order: self.qty_to_order.unwrap_or(Decimal::from(0)),
448 qty_to_order_manual: self.qty_to_order_manual.unwrap_or(Decimal::from(0)),
449 lead_days: self.lead_days.unwrap_or(Decimal::from(0)),
450 deadline_date: self.deadline_date,
451 metadata: AuditMetadata::default(),
452 })
453 }
454}