backbone_inventory/infrastructure/persistence/
bin_repository.rs1use anyhow::Result;
11use backbone_orm::org_scope;
12use rust_decimal::Decimal;
13use sqlx::{PgPool, Row};
14use uuid::Uuid;
15
16use crate::domain::entity::Bin;
17
18
19pub const TABLE_NAME: &str = "inventory.bins";
21
22pub struct BinRepository(
27 backbone_orm::GenericCrudRepository<Bin, backbone_orm::SoftDelete>,
28);
29
30impl std::ops::Deref for BinRepository {
31 type Target = backbone_orm::GenericCrudRepository<Bin, backbone_orm::SoftDelete>;
32 fn deref(&self) -> &Self::Target { &self.0 }
33}
34
35impl BinRepository {
36 pub fn new(pool: PgPool) -> Self {
38 Self(backbone_orm::GenericCrudRepository::new(pool, TABLE_NAME))
39 }
40}
41
42pub struct BinBalanceRow {
44 pub actual_qty: Decimal,
45 pub valuation_rate: Decimal,
46 pub stock_value: Decimal,
47}
48
49impl BinRepository {
52 pub async fn lock_or_init(
64 &self,
65 conn: &mut sqlx::PgConnection,
66 item_id: Uuid,
67 warehouse_id: Uuid,
68 ) -> Result<BinBalanceRow, sqlx::Error> {
69 let row = sqlx::query(
70 r#"SELECT actual_qty, valuation_rate, stock_value FROM inventory.bins
71 WHERE item_id=$1 AND warehouse_id=$2 AND (metadata->>'deleted_at') IS NULL
72 FOR UPDATE"#,
73 )
74 .bind(item_id).bind(warehouse_id)
75 .fetch_optional(&mut *conn)
76 .await?;
77 if let Some(r) = row {
78 return Ok(BinBalanceRow {
79 actual_qty: r.get("actual_qty"),
80 valuation_rate: r.get("valuation_rate"),
81 stock_value: r.get("stock_value"),
82 });
83 }
84 sqlx::query(
85 r#"INSERT INTO inventory.bins (id, item_id, warehouse_id, actual_qty, reserved_qty, valuation_rate, stock_value)
86 VALUES ($1,$2,$3,0,0,0,0)"#,
87 )
88 .bind(Uuid::new_v4()).bind(item_id).bind(warehouse_id)
89 .execute(&mut *conn)
90 .await?;
91 Ok(BinBalanceRow {
92 actual_qty: Decimal::ZERO, valuation_rate: Decimal::ZERO, stock_value: Decimal::ZERO,
93 })
94 }
95
96 pub async fn update_balance(
99 &self,
100 conn: &mut sqlx::PgConnection,
101 item_id: Uuid,
102 warehouse_id: Uuid,
103 actual_qty: Decimal,
104 valuation_rate: Decimal,
105 stock_value: Decimal,
106 ) -> Result<(), sqlx::Error> {
107 sqlx::query(
108 r#"UPDATE inventory.bins SET actual_qty=$3, valuation_rate=$4, stock_value=$5
109 WHERE item_id=$1 AND warehouse_id=$2 AND (metadata->>'deleted_at') IS NULL"#,
110 )
111 .bind(item_id).bind(warehouse_id)
112 .bind(actual_qty).bind(valuation_rate).bind(stock_value)
113 .execute(conn)
114 .await?;
115 Ok(())
116 }
117}
118
119pub struct BinAvailabilityRow {
121 pub actual_qty: Decimal,
122 pub reserved_qty: Decimal,
123}
124
125pub struct BinWarehouseAvailabilityRow {
127 pub warehouse_id: Uuid,
128 pub actual_qty: Decimal,
129 pub reserved_qty: Decimal,
130}
131
132impl BinRepository {
139 pub async fn fetch_availability(
142 &self,
143 pool: &PgPool,
144 item_id: Uuid,
145 warehouse_id: Uuid,
146 ) -> Result<Option<BinAvailabilityRow>, sqlx::Error> {
147 let row = org_scope::fetch_optional_row_scoped(
148 pool,
149 sqlx::query(
150 r#"SELECT actual_qty, reserved_qty FROM inventory.bins
151 WHERE item_id=$1 AND warehouse_id=$2 AND (metadata->>'deleted_at') IS NULL"#,
152 )
153 .bind(item_id).bind(warehouse_id),
154 )
155 .await?;
156 Ok(row.map(|r| BinAvailabilityRow {
157 actual_qty: r.get("actual_qty"),
158 reserved_qty: r.get("reserved_qty"),
159 }))
160 }
161
162 pub async fn fetch_availability_across_warehouses(
164 &self,
165 pool: &PgPool,
166 item_id: Uuid,
167 ) -> Result<Vec<BinWarehouseAvailabilityRow>, sqlx::Error> {
168 let rows = org_scope::fetch_all_rows_scoped(
169 pool,
170 sqlx::query(
171 r#"SELECT warehouse_id, actual_qty, reserved_qty FROM inventory.bins
172 WHERE item_id=$1 AND (metadata->>'deleted_at') IS NULL
173 ORDER BY warehouse_id"#,
174 )
175 .bind(item_id),
176 )
177 .await?;
178 Ok(rows
179 .into_iter()
180 .map(|r| BinWarehouseAvailabilityRow {
181 warehouse_id: r.get("warehouse_id"),
182 actual_qty: r.get("actual_qty"),
183 reserved_qty: r.get("reserved_qty"),
184 })
185 .collect())
186 }
187
188 pub async fn fetch_balance(
190 &self,
191 pool: &PgPool,
192 item_id: Uuid,
193 warehouse_id: Uuid,
194 ) -> Result<Option<BinBalanceRow>, sqlx::Error> {
195 let row = org_scope::fetch_optional_row_scoped(
196 pool,
197 sqlx::query(
198 r#"SELECT actual_qty, valuation_rate, stock_value FROM inventory.bins
199 WHERE item_id=$1 AND warehouse_id=$2 AND (metadata->>'deleted_at') IS NULL"#,
200 )
201 .bind(item_id).bind(warehouse_id),
202 )
203 .await?;
204 Ok(row.map(|r| BinBalanceRow {
205 actual_qty: r.get("actual_qty"),
206 valuation_rate: r.get("valuation_rate"),
207 stock_value: r.get("stock_value"),
208 }))
209 }
210}
211
212backbone_core::impl_crud_repository!(BinRepository, Bin, soft_delete);