backbone_inventory/domain/entity/
landed_cost.rs1use chrono::{DateTime, Utc, NaiveDate};
2use serde::{Deserialize, Serialize};
3use sqlx::FromRow;
4use uuid::Uuid;
5use rust_decimal::Decimal;
6
7use super::LandedCostState;
8use super::GlPostingState;
9use super::AuditMetadata;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
13#[serde(transparent)]
14pub struct LandedCostId(pub Uuid);
15
16impl LandedCostId {
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 LandedCostId {
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 LandedCostId {
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 LandedCostId {
36 fn from(id: Uuid) -> Self { Self(id) }
37}
38
39impl From<LandedCostId> for Uuid {
40 fn from(id: LandedCostId) -> Self { id.0 }
41}
42
43impl AsRef<Uuid> for LandedCostId {
44 fn as_ref(&self) -> &Uuid { &self.0 }
45}
46
47impl std::ops::Deref for LandedCostId {
48 type Target = Uuid;
49 fn deref(&self) -> &Self::Target { &self.0 }
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
53pub struct LandedCost {
54 pub id: Uuid,
55 pub lc_number: String,
56 pub branch_id: Option<Uuid>,
57 pub target_receipt_id: Uuid,
58 pub transfer_id: Option<Uuid>,
59 pub currency: String,
60 pub posting_date: NaiveDate,
61 pub state: LandedCostState,
62 pub amount_total: Decimal,
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 pub notes: Option<String>,
68 #[serde(default)]
69 #[sqlx(json)]
70 pub metadata: AuditMetadata,
71}
72
73impl LandedCost {
74 pub fn builder() -> LandedCostBuilder {
76 <LandedCostBuilder as Default>::default()
77 }
78
79 pub fn new(lc_number: String, target_receipt_id: Uuid, currency: String, posting_date: NaiveDate, state: LandedCostState, amount_total: Decimal, posting_state: GlPostingState) -> Self {
81 Self {
82 id: Uuid::new_v4(),
83 lc_number,
84 branch_id: None,
85 target_receipt_id,
86 transfer_id: None,
87 currency,
88 posting_date,
89 state,
90 amount_total,
91 posting_state,
92 journal_id: None,
93 accounting_post_id: None,
94 posted_at: None,
95 notes: None,
96 metadata: AuditMetadata::default(),
97 }
98 }
99
100 pub fn id(&self) -> &Uuid {
102 &self.id
103 }
104
105 pub fn typed_id(&self) -> LandedCostId {
107 LandedCostId(self.id)
108 }
109
110 pub fn created_at(&self) -> Option<&DateTime<Utc>> {
112 self.metadata.created_at.as_ref()
113 }
114
115 pub fn updated_at(&self) -> Option<&DateTime<Utc>> {
117 self.metadata.updated_at.as_ref()
118 }
119
120 pub fn is_deleted(&self) -> bool {
122 self.metadata.deleted_at.is_some()
123 }
124
125 pub fn is_active(&self) -> bool {
127 self.metadata.deleted_at.is_none()
128 }
129
130 pub fn deleted_at(&self) -> Option<&DateTime<Utc>> {
132 self.metadata.deleted_at.as_ref()
133 }
134
135 pub fn created_by(&self) -> Option<&Uuid> {
137 self.metadata.created_by.as_ref()
138 }
139
140 pub fn updated_by(&self) -> Option<&Uuid> {
142 self.metadata.updated_by.as_ref()
143 }
144
145 pub fn deleted_by(&self) -> Option<&Uuid> {
147 self.metadata.deleted_by.as_ref()
148 }
149
150
151 pub fn with_branch_id(mut self, value: Uuid) -> Self {
157 self.branch_id = Some(value);
158 self
159 }
160
161 pub fn with_transfer_id(mut self, value: Uuid) -> Self {
163 self.transfer_id = Some(value);
164 self
165 }
166
167 pub fn with_journal_id(mut self, value: Uuid) -> Self {
169 self.journal_id = Some(value);
170 self
171 }
172
173 pub fn with_accounting_post_id(mut self, value: Uuid) -> Self {
175 self.accounting_post_id = Some(value);
176 self
177 }
178
179 pub fn with_posted_at(mut self, value: DateTime<Utc>) -> Self {
181 self.posted_at = Some(value);
182 self
183 }
184
185 pub fn with_notes(mut self, value: String) -> Self {
187 self.notes = Some(value);
188 self
189 }
190
191 pub fn apply_patch(&mut self, fields: std::collections::HashMap<String, serde_json::Value>) {
197 for (key, value) in fields {
198 match key.as_str() {
199 "lc_number" => {
200 if let Ok(v) = serde_json::from_value(value) { self.lc_number = v; }
201 }
202 "branch_id" => {
203 if let Ok(v) = serde_json::from_value(value) { self.branch_id = v; }
204 }
205 "target_receipt_id" => {
206 if let Ok(v) = serde_json::from_value(value) { self.target_receipt_id = v; }
207 }
208 "transfer_id" => {
209 if let Ok(v) = serde_json::from_value(value) { self.transfer_id = v; }
210 }
211 "currency" => {
212 if let Ok(v) = serde_json::from_value(value) { self.currency = v; }
213 }
214 "posting_date" => {
215 if let Ok(v) = serde_json::from_value(value) { self.posting_date = v; }
216 }
217 "state" => {
218 if let Ok(v) = serde_json::from_value(value) { self.state = v; }
219 }
220 "amount_total" => {
221 if let Ok(v) = serde_json::from_value(value) { self.amount_total = v; }
222 }
223 "posting_state" => {
224 if let Ok(v) = serde_json::from_value(value) { self.posting_state = v; }
225 }
226 "journal_id" => {
227 if let Ok(v) = serde_json::from_value(value) { self.journal_id = v; }
228 }
229 "accounting_post_id" => {
230 if let Ok(v) = serde_json::from_value(value) { self.accounting_post_id = v; }
231 }
232 "posted_at" => {
233 if let Ok(v) = serde_json::from_value(value) { self.posted_at = v; }
234 }
235 "notes" => {
236 if let Ok(v) = serde_json::from_value(value) { self.notes = v; }
237 }
238 _ => {} }
240 }
241 }
242
243}
244
245impl super::Entity for LandedCost {
246 type Id = Uuid;
247
248 fn entity_id(&self) -> &Self::Id {
249 &self.id
250 }
251
252 fn entity_type() -> &'static str {
253 "LandedCost"
254 }
255}
256
257impl backbone_core::PersistentEntity for LandedCost {
258 fn write_protected_fields() -> &'static [&'static str] {
259 &["state"]
260 }
261 fn entity_id(&self) -> String {
262 self.id.to_string()
263 }
264 fn set_entity_id(&mut self, id: String) {
265 if let Ok(uuid) = uuid::Uuid::parse_str(&id) {
266 self.id = uuid;
267 }
268 }
269 fn created_at(&self) -> Option<chrono::DateTime<chrono::Utc>> {
270 self.metadata.created_at
271 }
272 fn set_created_at(&mut self, ts: chrono::DateTime<chrono::Utc>) {
273 self.metadata.created_at = Some(ts);
274 }
275 fn updated_at(&self) -> Option<chrono::DateTime<chrono::Utc>> {
276 self.metadata.updated_at
277 }
278 fn set_updated_at(&mut self, ts: chrono::DateTime<chrono::Utc>) {
279 self.metadata.updated_at = Some(ts);
280 }
281 fn deleted_at(&self) -> Option<chrono::DateTime<chrono::Utc>> {
282 self.metadata.deleted_at
283 }
284 fn set_deleted_at(&mut self, ts: Option<chrono::DateTime<chrono::Utc>>) {
285 self.metadata.deleted_at = ts;
286 }
287}
288
289impl backbone_orm::EntityRepoMeta for LandedCost {
290 fn column_types() -> std::collections::HashMap<String, String> {
291 let mut m = std::collections::HashMap::new();
292 m.insert("id".to_string(), "uuid".to_string());
293 m.insert("branch_id".to_string(), "uuid".to_string());
294 m.insert("target_receipt_id".to_string(), "uuid".to_string());
295 m.insert("transfer_id".to_string(), "uuid".to_string());
296 m.insert("journal_id".to_string(), "uuid".to_string());
297 m.insert("accounting_post_id".to_string(), "uuid".to_string());
298 m.insert("state".to_string(), "landed_cost_state".to_string());
299 m.insert("posting_state".to_string(), "gl_posting_state".to_string());
300 m.insert("posting_date".to_string(), "date".to_string());
301 m.insert("posted_at".to_string(), "timestamptz".to_string());
302 m
303 }
304 fn search_fields() -> &'static [&'static str] {
305 &["lc_number", "currency"]
306 }
307 fn relations() -> &'static [(&'static str, &'static str, &'static str)] {
308 &[("transfer", "transfers", "transferId")]
309 }
310}
311
312#[derive(Debug, Clone, Default)]
317pub struct LandedCostBuilder {
318 lc_number: Option<String>,
319 branch_id: Option<Uuid>,
320 target_receipt_id: Option<Uuid>,
321 transfer_id: Option<Uuid>,
322 currency: Option<String>,
323 posting_date: Option<NaiveDate>,
324 state: Option<LandedCostState>,
325 amount_total: Option<Decimal>,
326 posting_state: Option<GlPostingState>,
327 journal_id: Option<Uuid>,
328 accounting_post_id: Option<Uuid>,
329 posted_at: Option<DateTime<Utc>>,
330 notes: Option<String>,
331}
332
333impl LandedCostBuilder {
334 pub fn lc_number(mut self, value: String) -> Self {
336 self.lc_number = Some(value);
337 self
338 }
339
340 pub fn branch_id(mut self, value: Uuid) -> Self {
342 self.branch_id = Some(value);
343 self
344 }
345
346 pub fn target_receipt_id(mut self, value: Uuid) -> Self {
348 self.target_receipt_id = Some(value);
349 self
350 }
351
352 pub fn transfer_id(mut self, value: Uuid) -> Self {
354 self.transfer_id = Some(value);
355 self
356 }
357
358 pub fn currency(mut self, value: String) -> Self {
360 self.currency = Some(value);
361 self
362 }
363
364 pub fn posting_date(mut self, value: NaiveDate) -> Self {
366 self.posting_date = Some(value);
367 self
368 }
369
370 pub fn state(mut self, value: LandedCostState) -> Self {
372 self.state = Some(value);
373 self
374 }
375
376 pub fn amount_total(mut self, value: Decimal) -> Self {
378 self.amount_total = Some(value);
379 self
380 }
381
382 pub fn posting_state(mut self, value: GlPostingState) -> Self {
384 self.posting_state = Some(value);
385 self
386 }
387
388 pub fn journal_id(mut self, value: Uuid) -> Self {
390 self.journal_id = Some(value);
391 self
392 }
393
394 pub fn accounting_post_id(mut self, value: Uuid) -> Self {
396 self.accounting_post_id = Some(value);
397 self
398 }
399
400 pub fn posted_at(mut self, value: DateTime<Utc>) -> Self {
402 self.posted_at = Some(value);
403 self
404 }
405
406 pub fn notes(mut self, value: String) -> Self {
408 self.notes = Some(value);
409 self
410 }
411
412 pub fn build(self) -> Result<LandedCost, String> {
416 let lc_number = self.lc_number.ok_or_else(|| "lc_number is required".to_string())?;
417 let target_receipt_id = self.target_receipt_id.ok_or_else(|| "target_receipt_id is required".to_string())?;
418 let posting_date = self.posting_date.ok_or_else(|| "posting_date is required".to_string())?;
419
420 Ok(LandedCost {
421 id: Uuid::new_v4(),
422 lc_number,
423 branch_id: self.branch_id,
424 target_receipt_id,
425 transfer_id: self.transfer_id,
426 currency: self.currency.unwrap_or("IDR".to_string()),
427 posting_date,
428 state: self.state.unwrap_or_default(),
429 amount_total: self.amount_total.unwrap_or(Decimal::from(0)),
430 posting_state: self.posting_state.unwrap_or_default(),
431 journal_id: self.journal_id,
432 accounting_post_id: self.accounting_post_id,
433 posted_at: self.posted_at,
434 notes: self.notes,
435 metadata: AuditMetadata::default(),
436 })
437 }
438}