stateset-core 0.8.1

Core domain models and business logic for StateSet iCommerce
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
//! Cost Accounting domain models
//!
//! Models for inventory costing, cost variance tracking, and cost layer management.

use chrono::{DateTime, Utc};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use stateset_primitives::CurrencyCode;
use strum::{Display, EnumString};
use uuid::Uuid;

// ============================================================================
// Core Cost Types
// ============================================================================

/// Cost record for an inventory item (standard cost master).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ItemCost {
    pub id: Uuid,
    pub sku: String,
    pub cost_method: CostMethod,
    pub standard_cost: Decimal,
    pub average_cost: Decimal,
    pub last_cost: Decimal,
    pub material_cost: Decimal,
    pub labor_cost: Decimal,
    pub overhead_cost: Decimal,
    pub currency: CurrencyCode,
    pub effective_date: DateTime<Utc>,
    pub created_at: DateTime<Utc>,
    pub updated_at: DateTime<Utc>,
}

/// A cost layer for FIFO/LIFO costing.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CostLayer {
    pub id: Uuid,
    pub sku: String,
    pub layer_date: DateTime<Utc>,
    pub quantity: Decimal,
    pub remaining_quantity: Decimal,
    pub unit_cost: Decimal,
    pub total_cost: Decimal,
    pub source_type: CostLayerSource,
    pub source_id: Option<Uuid>,
    pub lot_id: Option<Uuid>,
    pub location_id: Option<i32>,
    pub created_at: DateTime<Utc>,
}

/// A cost transaction (records cost movements).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CostTransaction {
    pub id: Uuid,
    pub sku: String,
    pub transaction_type: CostTransactionType,
    pub quantity: Decimal,
    pub unit_cost: Decimal,
    pub total_cost: Decimal,
    pub layer_id: Option<Uuid>,
    pub reference_type: Option<String>,
    pub reference_id: Option<Uuid>,
    pub notes: Option<String>,
    pub created_at: DateTime<Utc>,
}

/// Cost variance record.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CostVariance {
    pub id: Uuid,
    pub sku: String,
    pub variance_type: VarianceType,
    pub variance_date: DateTime<Utc>,
    pub standard_cost: Decimal,
    pub actual_cost: Decimal,
    pub variance_amount: Decimal,
    pub variance_percent: Decimal,
    pub quantity: Decimal,
    pub total_variance: Decimal,
    pub reference_type: Option<String>,
    pub reference_id: Option<Uuid>,
    pub notes: Option<String>,
    pub created_at: DateTime<Utc>,
}

/// Cost adjustment record.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CostAdjustment {
    pub id: Uuid,
    pub adjustment_number: String,
    pub sku: String,
    pub adjustment_type: CostAdjustmentType,
    pub previous_cost: Decimal,
    pub new_cost: Decimal,
    pub adjustment_amount: Decimal,
    pub reason: String,
    pub approved_by: Option<String>,
    pub approved_at: Option<DateTime<Utc>>,
    pub status: CostAdjustmentStatus,
    pub created_by: Option<String>,
    pub created_at: DateTime<Utc>,
}

/// Standard cost roll-up for manufactured items.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CostRollup {
    pub id: Uuid,
    pub sku: String,
    pub bom_id: Option<Uuid>,
    pub rollup_date: DateTime<Utc>,
    pub material_cost: Decimal,
    pub labor_cost: Decimal,
    pub overhead_cost: Decimal,
    pub total_cost: Decimal,
    pub previous_cost: Decimal,
    pub cost_change: Decimal,
    pub created_at: DateTime<Utc>,
}

// ============================================================================
// Enums
// ============================================================================

/// Inventory costing method.
#[derive(
    Debug, Clone, Copy, PartialEq, Eq, Display, EnumString, Serialize, Deserialize, Default,
)]
#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum CostMethod {
    /// Weighted average cost recalculated on each receipt.
    #[default]
    #[strum(serialize = "average", serialize = "avg")]
    Average,
    /// First-in, first-out: oldest cost layers are consumed first.
    Fifo,
    /// Last-in, first-out: newest cost layers are consumed first.
    Lifo,
    /// Pre-determined standard cost used for all transactions.
    #[strum(serialize = "standard", serialize = "std")]
    Standard,
    /// Each unit is tracked with its own specific cost.
    Specific,
}

/// Source of a cost layer.
#[derive(
    Debug, Clone, Copy, PartialEq, Eq, Display, EnumString, Serialize, Deserialize, Default,
)]
#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum CostLayerSource {
    /// Layer created from a supplier purchase receipt.
    #[default]
    Purchase,
    /// Layer created from a completed manufacturing work order.
    Production,
    /// Layer created by transferring inventory between locations.
    Transfer,
    /// Layer created by a manual cost adjustment.
    Adjustment,
    /// Layer representing inventory on hand at system go-live.
    #[strum(serialize = "opening", serialize = "opening_balance")]
    Opening,
}

/// Cost transaction type.
#[derive(
    Debug, Clone, Copy, PartialEq, Eq, Display, EnumString, Serialize, Deserialize, Default,
)]
#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum CostTransactionType {
    /// Goods received into inventory; cost layer is added.
    #[default]
    Receipt,
    /// Goods consumed or sold; cost is relieved from a layer.
    Issue,
    /// Manual change to cost without a physical movement.
    Adjustment,
    /// Physical movement between locations; cost moves with inventory.
    Transfer,
    /// Cost is updated to reflect a new standard or market value.
    Revaluation,
}

/// Variance type.
#[derive(
    Debug, Clone, Copy, PartialEq, Eq, Display, EnumString, Serialize, Deserialize, Default,
)]
#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum VarianceType {
    /// Difference between purchase price and standard cost.
    #[default]
    Purchase,
    /// Difference in raw material usage versus the standard bill of materials.
    Material,
    /// Difference in direct labor hours or rates versus standard.
    Labor,
    /// Difference in applied overhead versus actual overhead incurred.
    Overhead,
    /// Difference due to operating at a different efficiency than standard.
    Efficiency,
    /// Difference due to producing a different volume than the planned level.
    Volume,
}

/// Cost adjustment type.
#[derive(
    Debug, Clone, Copy, PartialEq, Eq, Display, EnumString, Serialize, Deserialize, Default,
)]
#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum CostAdjustmentType {
    /// Periodic update to the standard cost for a SKU.
    #[default]
    #[strum(serialize = "standard_cost_update", serialize = "standardcostupdate")]
    StandardCostUpdate,
    /// Restate inventory value to reflect current market or replacement cost.
    Revaluation,
    /// Remove obsolete or damaged inventory value from the books.
    #[strum(serialize = "write_off", serialize = "writeoff")]
    WriteOff,
    /// Fix a data entry or calculation error in recorded cost.
    Correction,
}

/// Cost adjustment status.
#[derive(
    Debug, Clone, Copy, PartialEq, Eq, Display, EnumString, Serialize, Deserialize, Default,
)]
#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum CostAdjustmentStatus {
    /// Adjustment has been submitted and is awaiting review.
    #[default]
    Pending,
    /// Adjustment has been reviewed and approved; ready to apply.
    Approved,
    /// Adjustment has been applied to inventory cost records.
    Applied,
    /// Adjustment was reviewed and denied.
    Rejected,
}

// ============================================================================
// Input Types
// ============================================================================

/// Input for setting item cost.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct SetItemCost {
    pub sku: String,
    pub cost_method: Option<CostMethod>,
    pub standard_cost: Option<Decimal>,
    pub material_cost: Option<Decimal>,
    pub labor_cost: Option<Decimal>,
    pub overhead_cost: Option<Decimal>,
    pub currency: Option<CurrencyCode>,
}

/// Input for creating a cost layer.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateCostLayer {
    pub sku: String,
    pub quantity: Decimal,
    pub unit_cost: Decimal,
    pub source_type: CostLayerSource,
    pub source_id: Option<Uuid>,
    pub lot_id: Option<Uuid>,
    pub location_id: Option<i32>,
}

/// Input for issuing from cost layers (FIFO/LIFO).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IssueCostLayers {
    pub sku: String,
    pub quantity: Decimal,
    pub reference_type: Option<String>,
    pub reference_id: Option<Uuid>,
    pub notes: Option<String>,
}

/// Input for creating a cost adjustment.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateCostAdjustment {
    pub sku: String,
    pub adjustment_type: CostAdjustmentType,
    pub new_cost: Decimal,
    pub reason: String,
    pub created_by: Option<String>,
}

/// Input for recording a cost variance.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RecordCostVariance {
    pub sku: String,
    pub variance_type: VarianceType,
    pub standard_cost: Decimal,
    pub actual_cost: Decimal,
    pub quantity: Decimal,
    pub reference_type: Option<String>,
    pub reference_id: Option<Uuid>,
    pub notes: Option<String>,
}

// ============================================================================
// Filter Types
// ============================================================================

/// Filter for listing item costs.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ItemCostFilter {
    pub sku: Option<String>,
    pub cost_method: Option<CostMethod>,
    pub limit: Option<u32>,
    pub offset: Option<u32>,
}

/// Filter for listing cost layers.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct CostLayerFilter {
    pub sku: Option<String>,
    pub source_type: Option<CostLayerSource>,
    pub has_remaining: Option<bool>,
    pub from_date: Option<DateTime<Utc>>,
    pub to_date: Option<DateTime<Utc>>,
    pub limit: Option<u32>,
    pub offset: Option<u32>,
}

/// Filter for listing cost transactions.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct CostTransactionFilter {
    pub sku: Option<String>,
    pub transaction_type: Option<CostTransactionType>,
    pub from_date: Option<DateTime<Utc>>,
    pub to_date: Option<DateTime<Utc>>,
    pub limit: Option<u32>,
    pub offset: Option<u32>,
}

/// Filter for listing cost variances.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct CostVarianceFilter {
    pub sku: Option<String>,
    pub variance_type: Option<VarianceType>,
    pub from_date: Option<DateTime<Utc>>,
    pub to_date: Option<DateTime<Utc>>,
    pub limit: Option<u32>,
    pub offset: Option<u32>,
}

/// Filter for listing cost adjustments.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct CostAdjustmentFilter {
    pub sku: Option<String>,
    pub status: Option<CostAdjustmentStatus>,
    pub adjustment_type: Option<CostAdjustmentType>,
    pub from_date: Option<DateTime<Utc>>,
    pub to_date: Option<DateTime<Utc>>,
    pub limit: Option<u32>,
    pub offset: Option<u32>,
}

// ============================================================================
// Summary Types
// ============================================================================

/// Inventory valuation summary.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InventoryValuation {
    pub total_quantity: Decimal,
    pub total_value: Decimal,
    pub average_unit_cost: Decimal,
    pub valuation_method: CostMethod,
    pub as_of_date: DateTime<Utc>,
}

/// Cost summary by SKU.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SkuCostSummary {
    pub sku: String,
    pub quantity_on_hand: Decimal,
    pub standard_cost: Decimal,
    pub average_cost: Decimal,
    pub total_value: Decimal,
    pub variance_ytd: Decimal,
}

// ============================================================================
// Helper Functions
// ============================================================================

/// Generate a cost adjustment number.
pub fn generate_cost_adjustment_number() -> String {
    let timestamp = chrono::Utc::now().format("%Y%m%d%H%M").to_string();
    let random = &uuid::Uuid::new_v4().to_string()[..4].to_uppercase();
    format!("CADJ-{}-{}", timestamp, random)
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::str::FromStr;

    #[test]
    fn test_cost_method_from_str() {
        assert_eq!(CostMethod::from_str("avg").unwrap(), CostMethod::Average);
        assert_eq!(CostMethod::from_str("standard").unwrap(), CostMethod::Standard);
        assert!(CostMethod::from_str("nope").is_err());
    }

    #[test]
    fn test_cost_layer_source_from_str() {
        assert_eq!(CostLayerSource::from_str("opening_balance").unwrap(), CostLayerSource::Opening);
        assert_eq!(CostLayerSource::from_str("transfer").unwrap(), CostLayerSource::Transfer);
        assert!(CostLayerSource::from_str("nope").is_err());
    }

    #[test]
    fn test_cost_transaction_type_from_str() {
        assert_eq!(CostTransactionType::from_str("receipt").unwrap(), CostTransactionType::Receipt);
        assert_eq!(
            CostTransactionType::from_str("revaluation").unwrap(),
            CostTransactionType::Revaluation
        );
        assert!(CostTransactionType::from_str("nope").is_err());
    }

    #[test]
    fn test_variance_type_from_str() {
        assert_eq!(VarianceType::from_str("material").unwrap(), VarianceType::Material);
        assert_eq!(VarianceType::from_str("volume").unwrap(), VarianceType::Volume);
        assert!(VarianceType::from_str("nope").is_err());
    }

    #[test]
    fn test_cost_adjustment_type_from_str() {
        assert_eq!(
            CostAdjustmentType::from_str("standardcostupdate").unwrap(),
            CostAdjustmentType::StandardCostUpdate
        );
        assert_eq!(CostAdjustmentType::from_str("writeoff").unwrap(), CostAdjustmentType::WriteOff);
        assert!(CostAdjustmentType::from_str("nope").is_err());
    }

    #[test]
    fn test_cost_adjustment_status_from_str() {
        assert_eq!(
            CostAdjustmentStatus::from_str("approved").unwrap(),
            CostAdjustmentStatus::Approved
        );
        assert!(CostAdjustmentStatus::from_str("nope").is_err());
    }
}