backbone_inventory/domain/entity/
stock_reconciliation.rs1use chrono::{DateTime, Utc, NaiveDate};
2use serde::{Deserialize, Serialize};
3use sqlx::FromRow;
4use uuid::Uuid;
5use rust_decimal::Decimal;
6
7use super::DocStatus;
8use super::GlPostingState;
9use super::AuditMetadata;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
13#[serde(transparent)]
14pub struct StockReconciliationId(pub Uuid);
15
16impl StockReconciliationId {
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 StockReconciliationId {
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 StockReconciliationId {
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 StockReconciliationId {
36 fn from(id: Uuid) -> Self { Self(id) }
37}
38
39impl From<StockReconciliationId> for Uuid {
40 fn from(id: StockReconciliationId) -> Self { id.0 }
41}
42
43impl AsRef<Uuid> for StockReconciliationId {
44 fn as_ref(&self) -> &Uuid { &self.0 }
45}
46
47impl std::ops::Deref for StockReconciliationId {
48 type Target = Uuid;
49 fn deref(&self) -> &Self::Target { &self.0 }
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
53pub struct StockReconciliation {
54 pub id: Uuid,
55 pub recon_number: String,
56 pub warehouse_id: Uuid,
57 pub posting_date: NaiveDate,
58 pub net_difference: Decimal,
59 pub inventory_account_id: Uuid,
60 pub adjustment_account_id: Uuid,
61 pub transfer_id: Option<Uuid>,
62 pub status: DocStatus,
63 pub posting_state: GlPostingState,
64 pub journal_id: Option<Uuid>,
65 pub accounting_post_id: Option<Uuid>,
66 pub posted_at: Option<DateTime<Utc>>,
67 #[serde(default)]
68 #[sqlx(json)]
69 pub metadata: AuditMetadata,
70}
71
72impl StockReconciliation {
73 pub fn builder() -> StockReconciliationBuilder {
75 <StockReconciliationBuilder as Default>::default()
76 }
77
78 pub fn new(recon_number: String, warehouse_id: Uuid, posting_date: NaiveDate, net_difference: Decimal, inventory_account_id: Uuid, adjustment_account_id: Uuid, status: DocStatus, posting_state: GlPostingState) -> Self {
80 Self {
81 id: Uuid::new_v4(),
82 recon_number,
83 warehouse_id,
84 posting_date,
85 net_difference,
86 inventory_account_id,
87 adjustment_account_id,
88 transfer_id: None,
89 status,
90 posting_state,
91 journal_id: None,
92 accounting_post_id: None,
93 posted_at: None,
94 metadata: AuditMetadata::default(),
95 }
96 }
97
98 pub fn id(&self) -> &Uuid {
100 &self.id
101 }
102
103 pub fn typed_id(&self) -> StockReconciliationId {
105 StockReconciliationId(self.id)
106 }
107
108 pub fn created_at(&self) -> Option<&DateTime<Utc>> {
110 self.metadata.created_at.as_ref()
111 }
112
113 pub fn updated_at(&self) -> Option<&DateTime<Utc>> {
115 self.metadata.updated_at.as_ref()
116 }
117
118 pub fn is_deleted(&self) -> bool {
120 self.metadata.deleted_at.is_some()
121 }
122
123 pub fn is_active(&self) -> bool {
125 self.metadata.deleted_at.is_none()
126 }
127
128 pub fn deleted_at(&self) -> Option<&DateTime<Utc>> {
130 self.metadata.deleted_at.as_ref()
131 }
132
133 pub fn created_by(&self) -> Option<&Uuid> {
135 self.metadata.created_by.as_ref()
136 }
137
138 pub fn updated_by(&self) -> Option<&Uuid> {
140 self.metadata.updated_by.as_ref()
141 }
142
143 pub fn deleted_by(&self) -> Option<&Uuid> {
145 self.metadata.deleted_by.as_ref()
146 }
147
148 pub fn status(&self) -> &DocStatus {
150 &self.status
151 }
152
153
154 pub fn with_transfer_id(mut self, value: Uuid) -> Self {
160 self.transfer_id = Some(value);
161 self
162 }
163
164 pub fn with_journal_id(mut self, value: Uuid) -> Self {
166 self.journal_id = Some(value);
167 self
168 }
169
170 pub fn with_accounting_post_id(mut self, value: Uuid) -> Self {
172 self.accounting_post_id = Some(value);
173 self
174 }
175
176 pub fn with_posted_at(mut self, value: DateTime<Utc>) -> Self {
178 self.posted_at = Some(value);
179 self
180 }
181
182 pub fn apply_patch(&mut self, fields: std::collections::HashMap<String, serde_json::Value>) {
188 for (key, value) in fields {
189 match key.as_str() {
190 "recon_number" => {
191 if let Ok(v) = serde_json::from_value(value) { self.recon_number = v; }
192 }
193 "warehouse_id" => {
194 if let Ok(v) = serde_json::from_value(value) { self.warehouse_id = v; }
195 }
196 "posting_date" => {
197 if let Ok(v) = serde_json::from_value(value) { self.posting_date = v; }
198 }
199 "net_difference" => {
200 if let Ok(v) = serde_json::from_value(value) { self.net_difference = v; }
201 }
202 "inventory_account_id" => {
203 if let Ok(v) = serde_json::from_value(value) { self.inventory_account_id = v; }
204 }
205 "adjustment_account_id" => {
206 if let Ok(v) = serde_json::from_value(value) { self.adjustment_account_id = v; }
207 }
208 "transfer_id" => {
209 if let Ok(v) = serde_json::from_value(value) { self.transfer_id = v; }
210 }
211 "status" => {
212 if let Ok(v) = serde_json::from_value(value) { self.status = v; }
213 }
214 "posting_state" => {
215 if let Ok(v) = serde_json::from_value(value) { self.posting_state = v; }
216 }
217 "journal_id" => {
218 if let Ok(v) = serde_json::from_value(value) { self.journal_id = v; }
219 }
220 "accounting_post_id" => {
221 if let Ok(v) = serde_json::from_value(value) { self.accounting_post_id = v; }
222 }
223 "posted_at" => {
224 if let Ok(v) = serde_json::from_value(value) { self.posted_at = v; }
225 }
226 _ => {} }
228 }
229 }
230
231}
232
233impl super::Entity for StockReconciliation {
234 type Id = Uuid;
235
236 fn entity_id(&self) -> &Self::Id {
237 &self.id
238 }
239
240 fn entity_type() -> &'static str {
241 "StockReconciliation"
242 }
243}
244
245impl backbone_core::PersistentEntity for StockReconciliation {
246 fn write_protected_fields() -> &'static [&'static str] {
247 &["status"]
248 }
249 fn entity_id(&self) -> String {
250 self.id.to_string()
251 }
252 fn set_entity_id(&mut self, id: String) {
253 if let Ok(uuid) = uuid::Uuid::parse_str(&id) {
254 self.id = uuid;
255 }
256 }
257 fn created_at(&self) -> Option<chrono::DateTime<chrono::Utc>> {
258 self.metadata.created_at
259 }
260 fn set_created_at(&mut self, ts: chrono::DateTime<chrono::Utc>) {
261 self.metadata.created_at = Some(ts);
262 }
263 fn updated_at(&self) -> Option<chrono::DateTime<chrono::Utc>> {
264 self.metadata.updated_at
265 }
266 fn set_updated_at(&mut self, ts: chrono::DateTime<chrono::Utc>) {
267 self.metadata.updated_at = Some(ts);
268 }
269 fn deleted_at(&self) -> Option<chrono::DateTime<chrono::Utc>> {
270 self.metadata.deleted_at
271 }
272 fn set_deleted_at(&mut self, ts: Option<chrono::DateTime<chrono::Utc>>) {
273 self.metadata.deleted_at = ts;
274 }
275}
276
277impl backbone_orm::EntityRepoMeta for StockReconciliation {
278 fn column_types() -> std::collections::HashMap<String, String> {
279 let mut m = std::collections::HashMap::new();
280 m.insert("id".to_string(), "uuid".to_string());
281 m.insert("warehouse_id".to_string(), "uuid".to_string());
282 m.insert("inventory_account_id".to_string(), "uuid".to_string());
283 m.insert("adjustment_account_id".to_string(), "uuid".to_string());
284 m.insert("transfer_id".to_string(), "uuid".to_string());
285 m.insert("journal_id".to_string(), "uuid".to_string());
286 m.insert("accounting_post_id".to_string(), "uuid".to_string());
287 m.insert("status".to_string(), "doc_status".to_string());
288 m.insert("posting_state".to_string(), "gl_posting_state".to_string());
289 m.insert("posting_date".to_string(), "date".to_string());
290 m.insert("posted_at".to_string(), "timestamptz".to_string());
291 m
292 }
293 fn search_fields() -> &'static [&'static str] {
294 &["recon_number"]
295 }
296 fn relations() -> &'static [(&'static str, &'static str, &'static str)] {
297 &[("warehouse", "warehouses", "warehouseId"), ("transfer", "transfers", "transferId")]
298 }
299}
300
301#[derive(Debug, Clone, Default)]
306pub struct StockReconciliationBuilder {
307 recon_number: Option<String>,
308 warehouse_id: Option<Uuid>,
309 posting_date: Option<NaiveDate>,
310 net_difference: Option<Decimal>,
311 inventory_account_id: Option<Uuid>,
312 adjustment_account_id: Option<Uuid>,
313 transfer_id: Option<Uuid>,
314 status: Option<DocStatus>,
315 posting_state: Option<GlPostingState>,
316 journal_id: Option<Uuid>,
317 accounting_post_id: Option<Uuid>,
318 posted_at: Option<DateTime<Utc>>,
319}
320
321impl StockReconciliationBuilder {
322 pub fn recon_number(mut self, value: String) -> Self {
324 self.recon_number = Some(value);
325 self
326 }
327
328 pub fn warehouse_id(mut self, value: Uuid) -> Self {
330 self.warehouse_id = Some(value);
331 self
332 }
333
334 pub fn posting_date(mut self, value: NaiveDate) -> Self {
336 self.posting_date = Some(value);
337 self
338 }
339
340 pub fn net_difference(mut self, value: Decimal) -> Self {
342 self.net_difference = Some(value);
343 self
344 }
345
346 pub fn inventory_account_id(mut self, value: Uuid) -> Self {
348 self.inventory_account_id = Some(value);
349 self
350 }
351
352 pub fn adjustment_account_id(mut self, value: Uuid) -> Self {
354 self.adjustment_account_id = Some(value);
355 self
356 }
357
358 pub fn transfer_id(mut self, value: Uuid) -> Self {
360 self.transfer_id = Some(value);
361 self
362 }
363
364 pub fn status(mut self, value: DocStatus) -> Self {
366 self.status = Some(value);
367 self
368 }
369
370 pub fn posting_state(mut self, value: GlPostingState) -> Self {
372 self.posting_state = Some(value);
373 self
374 }
375
376 pub fn journal_id(mut self, value: Uuid) -> Self {
378 self.journal_id = Some(value);
379 self
380 }
381
382 pub fn accounting_post_id(mut self, value: Uuid) -> Self {
384 self.accounting_post_id = Some(value);
385 self
386 }
387
388 pub fn posted_at(mut self, value: DateTime<Utc>) -> Self {
390 self.posted_at = Some(value);
391 self
392 }
393
394 pub fn build(self) -> Result<StockReconciliation, String> {
398 let recon_number = self.recon_number.ok_or_else(|| "recon_number is required".to_string())?;
399 let warehouse_id = self.warehouse_id.ok_or_else(|| "warehouse_id is required".to_string())?;
400 let posting_date = self.posting_date.ok_or_else(|| "posting_date is required".to_string())?;
401 let inventory_account_id = self.inventory_account_id.ok_or_else(|| "inventory_account_id is required".to_string())?;
402 let adjustment_account_id = self.adjustment_account_id.ok_or_else(|| "adjustment_account_id is required".to_string())?;
403
404 Ok(StockReconciliation {
405 id: Uuid::new_v4(),
406 recon_number,
407 warehouse_id,
408 posting_date,
409 net_difference: self.net_difference.unwrap_or(Decimal::from(0)),
410 inventory_account_id,
411 adjustment_account_id,
412 transfer_id: self.transfer_id,
413 status: self.status.unwrap_or_default(),
414 posting_state: self.posting_state.unwrap_or_default(),
415 journal_id: self.journal_id,
416 accounting_post_id: self.accounting_post_id,
417 posted_at: self.posted_at,
418 metadata: AuditMetadata::default(),
419 })
420 }
421}