backbone_inventory/infrastructure/persistence/
stock_adjustment_repository.rs1use rust_decimal::Decimal;
18use sqlx::{PgConnection, Row};
19use uuid::Uuid;
20
21pub struct QuantCountRow {
23 pub id: Uuid,
24 pub item_id: Uuid,
25 pub location_id: Uuid,
26 pub quantity: Decimal,
27 pub reserved_quantity: Decimal,
28 pub inventory_quantity: Option<Decimal>,
29 pub inventory_diff_quantity: Option<Decimal>,
30 pub inventory_quantity_set: bool,
31 pub inventory_date: Option<chrono::NaiveDate>,
32 pub location_usage: String,
34 pub location_warehouse_id: Option<Uuid>,
36}
37
38#[derive(Debug, Clone, Copy)]
41pub enum QuantSelector {
42 ById(Uuid),
43 ItemLocation { item_id: Uuid, location_id: Uuid },
44}
45
46impl QuantSelector {
47 fn where_clause(&self) -> (&'static str, Vec<Uuid>) {
50 match self {
51 QuantSelector::ById(id) => ("q.id = $1", vec![*id]),
52 QuantSelector::ItemLocation { item_id, location_id } => (
53 "q.item_id = $1 AND q.location_id = $2 AND q.lot_id IS NULL AND q.package_id IS NULL AND q.owner_id IS NULL",
54 vec![*item_id, *location_id],
55 ),
56 }
57 }
58}
59
60#[derive(Default)]
62pub struct StockAdjustmentRepository;
63
64impl StockAdjustmentRepository {
65 pub fn new() -> Self {
66 Self
67 }
68
69 pub async fn lock_quant(
73 &self,
74 conn: &mut PgConnection,
75 selector: QuantSelector,
76 ) -> Result<Option<QuantCountRow>, sqlx::Error> {
77 let (clause, binds) = selector.where_clause();
78 let sql = format!(
79 r#"SELECT q.id, q.item_id, q.location_id, q.quantity, q.reserved_quantity,
80 q.inventory_quantity, q.inventory_diff_quantity, q.inventory_quantity_set,
81 q.inventory_date,
82 l.usage::text AS location_usage,
83 l.warehouse_id AS location_warehouse_id
84 FROM inventory.stock_quants q
85 JOIN inventory.locations l ON l.id = q.location_id
86 WHERE {clause} AND (q.metadata->>'deleted_at') IS NULL
87 FOR UPDATE OF q"#
88 );
89 let mut q = sqlx::query(&sql);
90 for b in binds {
91 q = q.bind(b);
92 }
93 let row = q.fetch_optional(conn).await?;
94 Ok(row.map(|r| QuantCountRow {
95 id: r.get("id"),
96 item_id: r.get("item_id"),
97 location_id: r.get("location_id"),
98 quantity: r.get("quantity"),
99 reserved_quantity: r.get("reserved_quantity"),
100 inventory_quantity: r.get("inventory_quantity"),
101 inventory_diff_quantity: r.get("inventory_diff_quantity"),
102 inventory_quantity_set: r.get("inventory_quantity_set"),
103 inventory_date: r.get("inventory_date"),
104 location_usage: r.get("location_usage"),
105 location_warehouse_id: r.get("location_warehouse_id"),
106 }))
107 }
108
109 pub async fn init_quant(
114 &self,
115 conn: &mut PgConnection,
116 item_id: Uuid,
117 location_id: Uuid,
118 ) -> Result<Uuid, sqlx::Error> {
119 let dims = super::quant_repository::QuantDims { item_id, location_id, lot_id: None, package_id: None, owner_id: None };
120 Ok(super::quant_repository::QuantRepository::lock_or_init_on(conn, dims).await?.id)
121 }
122
123 pub async fn stage_count(
128 &self,
129 conn: &mut PgConnection,
130 quant_id: Uuid,
131 counted: Decimal,
132 diff: Decimal,
133 ) -> Result<(), sqlx::Error> {
134 sqlx::query(
135 r#"UPDATE inventory.stock_quants SET
136 inventory_quantity = $2,
137 inventory_diff_quantity = $3,
138 inventory_quantity_set = TRUE,
139 inventory_date = CURRENT_DATE
140 WHERE id = $1"#,
141 )
142 .bind(quant_id)
143 .bind(counted)
144 .bind(diff)
145 .execute(conn)
146 .await?;
147 Ok(())
148 }
149
150 pub async fn consume_staged_count(
158 &self,
159 conn: &mut PgConnection,
160 quant_id: Uuid,
161 ) -> Result<(), sqlx::Error> {
162 sqlx::query(
163 r#"UPDATE inventory.stock_quants SET
164 inventory_quantity = NULL,
165 inventory_diff_quantity = NULL,
166 inventory_quantity_set = FALSE,
167 inventory_date = NULL
168 WHERE id = $1"#,
169 )
170 .bind(quant_id)
171 .execute(conn)
172 .await?;
173 Ok(())
174 }
175
176 pub async fn staged_counts_at_location(
179 &self,
180 conn: &mut PgConnection,
181 location_id: Uuid,
182 ) -> Result<Vec<StagedCountRow>, sqlx::Error> {
183 let rows = sqlx::query(
184 r#"SELECT id, item_id, quantity, inventory_quantity, inventory_diff_quantity
185 FROM inventory.stock_quants
186 WHERE location_id = $1 AND inventory_quantity_set
187 AND (metadata->>'deleted_at') IS NULL
188 ORDER BY id"#,
189 )
190 .bind(location_id)
191 .fetch_all(conn)
192 .await?;
193 Ok(rows
194 .iter()
195 .map(|r| StagedCountRow {
196 quant_id: r.get("id"),
197 item_id: r.get("item_id"),
198 on_hand_qty: r.get("quantity"),
199 counted_qty: r.get("inventory_quantity"),
200 diff_qty: r.get("inventory_diff_quantity"),
201 })
202 .collect())
203 }
204}
205
206pub struct StagedCountRow {
208 pub quant_id: Uuid,
209 pub item_id: Uuid,
210 pub on_hand_qty: Decimal,
211 pub counted_qty: Option<Decimal>,
212 pub diff_qty: Option<Decimal>,
213}