Skip to main content

stateset_embedded/
fixed_assets.rs

1//! Fixed asset register operations
2//!
3//! # Example
4//!
5//! ```ignore
6//! use stateset_embedded::{Commerce, CreateFixedAsset};
7//!
8//! let commerce = Commerce::new("./store.db")?;
9//! let asset = commerce.fixed_assets().create(input)?;
10//! let asset = commerce.fixed_assets().place_in_service(asset.id, date)?;
11//! let schedule = commerce.fixed_assets().generate_schedule(asset.id)?;
12//! let asset = commerce.fixed_assets().post_depreciation(asset.id, 1)?;
13//! # Ok::<(), stateset_embedded::CommerceError>(())
14//! ```
15
16use chrono::NaiveDate;
17use rust_decimal::Decimal;
18use stateset_core::{
19    CreateFixedAsset, DepreciationSchedule, FixedAsset, FixedAssetFilter, Result, UpdateFixedAsset,
20};
21use stateset_db::{Database, DatabaseCapability};
22use std::sync::Arc;
23use uuid::Uuid;
24
25#[cfg(feature = "events")]
26use crate::events::EventSystem;
27#[cfg(feature = "events")]
28use chrono::Utc;
29#[cfg(feature = "events")]
30use stateset_core::CommerceEvent;
31
32/// Fixed asset register operations.
33pub struct FixedAssets {
34    db: Arc<dyn Database>,
35    #[cfg(feature = "events")]
36    event_system: Arc<EventSystem>,
37}
38
39impl std::fmt::Debug for FixedAssets {
40    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41        f.debug_struct("FixedAssets").finish_non_exhaustive()
42    }
43}
44
45impl FixedAssets {
46    #[cfg(feature = "events")]
47    pub(crate) fn new(db: Arc<dyn Database>, event_system: Arc<EventSystem>) -> Self {
48        Self { db, event_system }
49    }
50
51    #[cfg(not(feature = "events"))]
52    pub(crate) fn new(db: Arc<dyn Database>) -> Self {
53        Self { db }
54    }
55
56    #[cfg(feature = "events")]
57    fn emit(&self, event: CommerceEvent) {
58        self.event_system.emit(event);
59    }
60
61    /// Whether fixed assets are supported by the active backend.
62    #[must_use]
63    pub fn is_supported(&self) -> bool {
64        self.db.supports_capability(DatabaseCapability::FixedAssets)
65    }
66
67    fn ensure(&self) -> Result<()> {
68        self.db.ensure_capability(DatabaseCapability::FixedAssets)
69    }
70
71    /// Create a new fixed asset.
72    pub fn create(&self, input: CreateFixedAsset) -> Result<FixedAsset> {
73        self.ensure()?;
74        self.db.fixed_assets().create(input)
75    }
76
77    /// Get a fixed asset by ID.
78    pub fn get(&self, id: Uuid) -> Result<Option<FixedAsset>> {
79        self.ensure()?;
80        self.db.fixed_assets().get(id)
81    }
82
83    /// List fixed assets with optional filtering.
84    pub fn list(&self, filter: FixedAssetFilter) -> Result<Vec<FixedAsset>> {
85        self.ensure()?;
86        self.db.fixed_assets().list(filter)
87    }
88
89    /// Update a fixed asset (partial).
90    pub fn update(&self, id: Uuid, input: UpdateFixedAsset) -> Result<FixedAsset> {
91        self.ensure()?;
92        self.db.fixed_assets().update(id, input)
93    }
94
95    /// Place a draft asset in service.
96    pub fn place_in_service(&self, id: Uuid, date: NaiveDate) -> Result<FixedAsset> {
97        self.ensure()?;
98        let asset = self.db.fixed_assets().place_in_service(id, date)?;
99        #[cfg(feature = "events")]
100        self.emit(CommerceEvent::FixedAssetPlacedInService {
101            asset_id: asset.id,
102            asset_number: asset.asset_number.clone(),
103            in_service_date: asset.in_service_date.unwrap_or(date),
104            acquisition_cost: asset.acquisition_cost,
105            timestamp: Utc::now(),
106        });
107        Ok(asset)
108    }
109
110    /// Dispose of an asset for the given proceeds, recording gain/loss.
111    pub fn dispose(
112        &self,
113        id: Uuid,
114        date: NaiveDate,
115        proceeds: Decimal,
116        notes: Option<String>,
117    ) -> Result<FixedAsset> {
118        self.ensure()?;
119        let asset = self.db.fixed_assets().dispose(id, date, proceeds, notes)?;
120        #[cfg(feature = "events")]
121        self.emit(CommerceEvent::FixedAssetDisposed {
122            asset_id: asset.id,
123            asset_number: asset.asset_number.clone(),
124            disposal_date: asset.disposal.as_ref().map_or(date, |d| d.disposal_date),
125            proceeds,
126            gain_loss: asset.disposal.as_ref().map_or(rust_decimal::Decimal::ZERO, |d| d.gain_loss),
127            timestamp: Utc::now(),
128        });
129        Ok(asset)
130    }
131
132    /// Write off an asset (disposal with zero proceeds).
133    pub fn write_off(
134        &self,
135        id: Uuid,
136        date: NaiveDate,
137        notes: Option<String>,
138    ) -> Result<FixedAsset> {
139        self.ensure()?;
140        let asset = self.db.fixed_assets().write_off(id, date, notes)?;
141        #[cfg(feature = "events")]
142        self.emit(CommerceEvent::FixedAssetWrittenOff {
143            asset_id: asset.id,
144            asset_number: asset.asset_number.clone(),
145            write_off_date: asset.disposal.as_ref().map_or(date, |d| d.disposal_date),
146            loss: asset
147                .disposal
148                .as_ref()
149                .map_or(rust_decimal::Decimal::ZERO, |d| d.gain_loss.abs()),
150            timestamp: Utc::now(),
151        });
152        Ok(asset)
153    }
154
155    /// Generate and persist the depreciation schedule for an asset.
156    pub fn generate_schedule(&self, id: Uuid) -> Result<DepreciationSchedule> {
157        self.ensure()?;
158        self.db.fixed_assets().generate_schedule(id)
159    }
160
161    /// Get the persisted depreciation schedule for an asset, if generated.
162    pub fn get_schedule(&self, id: Uuid) -> Result<Option<DepreciationSchedule>> {
163        self.ensure()?;
164        self.db.fixed_assets().get_schedule(id)
165    }
166
167    /// Post the next `periods` scheduled depreciation entries.
168    ///
169    /// If the active GL auto-posting configuration has `auto_post_depreciation`
170    /// enabled, a balanced journal entry is also created and posted (debit
171    /// depreciation expense, credit accumulated depreciation), using the
172    /// asset's configured GL accounts or, as a fallback, the first active
173    /// posting accounts with the matching account sub-types.
174    pub fn post_depreciation(&self, id: Uuid, periods: u32) -> Result<FixedAsset> {
175        self.ensure()?;
176        #[cfg(feature = "events")]
177        let previous_accumulated = self
178            .db
179            .fixed_assets()
180            .get(id)?
181            .map(|a| a.accumulated_depreciation)
182            .unwrap_or(rust_decimal::Decimal::ZERO);
183        let asset = self.db.fixed_assets().post_depreciation(id, periods)?;
184        #[cfg(feature = "events")]
185        self.emit(CommerceEvent::DepreciationPosted {
186            asset_id: asset.id,
187            asset_number: asset.asset_number.clone(),
188            periods,
189            amount: asset.accumulated_depreciation - previous_accumulated,
190            accumulated_depreciation: asset.accumulated_depreciation,
191            timestamp: Utc::now(),
192        });
193        Ok(asset)
194    }
195}