backbone_catalog/infrastructure/persistence/
uom_repository.rs1use rust_decimal::Decimal;
17use sqlx::{PgConnection, PgPool};
18use uuid::Uuid;
19
20use crate::domain::entity::Uom;
21use crate::domain::services::uom_tree::UomChainNode;
22
23pub const TABLE_NAME: &str = "catalog.uoms";
25
26pub struct UomRepository(
31 backbone_orm::GenericCrudRepository<Uom, backbone_orm::SoftDelete>,
32);
33
34impl std::ops::Deref for UomRepository {
35 type Target = backbone_orm::GenericCrudRepository<Uom, backbone_orm::SoftDelete>;
36 fn deref(&self) -> &Self::Target { &self.0 }
37}
38
39impl UomRepository {
40 pub fn new(pool: PgPool) -> Self {
42 Self(backbone_orm::GenericCrudRepository::new(pool, TABLE_NAME))
43 }
44}
45
46pub struct NewUomRow<'a> {
48 pub id: Uuid,
49 pub code: &'a str,
50 pub name: &'a str,
51 pub uom_type: &'a str,
52 pub decimal_places: i32,
53 pub relative_uom_id: Option<Uuid>,
55 pub relative_factor: Option<Decimal>,
57 pub factor: Decimal,
60}
61
62impl UomRepository {
64 pub async fn exists_id(
68 &self,
69 executor: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
70 id: Uuid,
71 ) -> Result<bool, sqlx::Error> {
72 let found: Option<Uuid> = sqlx::query_scalar(
73 "SELECT id FROM catalog.uoms \
74 WHERE id = $1 AND (metadata->>'deleted_at') IS NULL",
75 )
76 .bind(id)
77 .fetch_optional(executor)
78 .await?;
79 Ok(found.is_some())
80 }
81
82 pub async fn find_factor(
85 &self,
86 executor: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
87 id: Uuid,
88 ) -> Result<Option<Decimal>, sqlx::Error> {
89 let factor: Option<Decimal> = sqlx::query_scalar(
90 "SELECT factor FROM catalog.uoms \
91 WHERE id = $1 AND (metadata->>'deleted_at') IS NULL",
92 )
93 .bind(id)
94 .fetch_optional(executor)
95 .await?;
96 Ok(factor)
97 }
98
99 pub async fn insert_uom(
102 &self,
103 executor: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
104 r: &NewUomRow<'_>,
105 ) -> Result<(), sqlx::Error> {
106 sqlx::query(
107 r#"INSERT INTO catalog.uoms
108 (id, code, name, uom_type, decimal_places,
109 relative_uom_id, relative_factor, factor, status)
110 VALUES ($1,$2,$3,$4::uom_type,$5,$6,$7,$8,'active'::catalog_status)"#,
111 )
112 .bind(r.id)
113 .bind(r.code)
114 .bind(r.name)
115 .bind(r.uom_type)
116 .bind(r.decimal_places)
117 .bind(r.relative_uom_id)
118 .bind(r.relative_factor)
119 .bind(r.factor)
120 .execute(executor)
121 .await?;
122 Ok(())
123 }
124
125 pub async fn load_tree_chain(
134 &self,
135 executor: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
136 uom: Uuid,
137 ) -> Result<Option<Vec<UomChainNode>>, sqlx::Error> {
138 let rows: Vec<UomChainNode> = sqlx::query_as(
139 r#"WITH RECURSIVE chain AS (
140 SELECT id, code, relative_uom_id, relative_factor, factor
141 FROM catalog.uoms
142 WHERE id = $1 AND (metadata->>'deleted_at') IS NULL
143 UNION
144 SELECT p.id, p.code, p.relative_uom_id, p.relative_factor, p.factor
145 FROM catalog.uoms p
146 JOIN chain ON p.id = chain.relative_uom_id
147 )
148 SELECT id, code, relative_uom_id, relative_factor, factor FROM chain"#,
149 )
150 .bind(uom)
151 .fetch_all(executor)
152 .await?;
153 Ok(if rows.is_empty() { None } else { Some(rows) })
154 }
155
156 pub async fn is_self_or_descendant(
160 &self,
161 conn: &mut PgConnection,
162 uom: Uuid,
163 candidate: Uuid,
164 ) -> Result<bool, sqlx::Error> {
165 let hit: bool = sqlx::query_scalar(
166 r#"WITH RECURSIVE subtree AS (
167 SELECT id FROM catalog.uoms WHERE id = $1
168 UNION
169 SELECT c.id FROM catalog.uoms c JOIN subtree s ON c.relative_uom_id = s.id
170 )
171 SELECT EXISTS (SELECT 1 FROM subtree WHERE id = $2)"#,
172 )
173 .bind(uom)
174 .bind(candidate)
175 .fetch_one(conn)
176 .await?;
177 Ok(hit)
178 }
179
180 pub async fn set_relative(
184 &self,
185 conn: &mut PgConnection,
186 uom: Uuid,
187 relative_uom_id: Option<Uuid>,
188 relative_factor: Option<Decimal>,
189 ) -> Result<(), sqlx::Error> {
190 sqlx::query(
191 "UPDATE catalog.uoms SET relative_uom_id = $2, relative_factor = $3 \
192 WHERE id = $1 AND (metadata->>'deleted_at') IS NULL",
193 )
194 .bind(uom)
195 .bind(relative_uom_id)
196 .bind(relative_factor)
197 .execute(conn)
198 .await?;
199 Ok(())
200 }
201
202 pub async fn recompute_factors(
207 &self,
208 executor: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
209 ) -> Result<(), sqlx::Error> {
210 sqlx::query("SELECT catalog.uom_recompute_factors()")
211 .execute(executor)
212 .await?;
213 Ok(())
214 }
215
216 pub async fn find_protection(
221 &self,
222 conn: &mut PgConnection,
223 id: Uuid,
224 ) -> Result<Option<bool>, sqlx::Error> {
225 let protected: Option<bool> = sqlx::query_scalar(
226 "SELECT is_protected FROM catalog.uoms \
227 WHERE id = $1 AND (metadata->>'deleted_at') IS NULL",
228 )
229 .bind(id)
230 .fetch_optional(conn)
231 .await?;
232 Ok(protected)
233 }
234
235 pub async fn count_live_children(
239 &self,
240 conn: &mut PgConnection,
241 uom: Uuid,
242 ) -> Result<i64, sqlx::Error> {
243 let n: i64 = sqlx::query_scalar(
244 "SELECT count(*) FROM catalog.uoms \
245 WHERE relative_uom_id = $1 AND (metadata->>'deleted_at') IS NULL",
246 )
247 .bind(uom)
248 .fetch_one(conn)
249 .await?;
250 Ok(n)
251 }
252
253 pub async fn exists_live_item_using(
257 &self,
258 conn: &mut PgConnection,
259 uom: Uuid,
260 ) -> Result<bool, sqlx::Error> {
261 let hit: bool = sqlx::query_scalar(
262 "SELECT EXISTS ( \
263 SELECT 1 FROM catalog.items \
264 WHERE default_uom_id = $1 \
265 AND (metadata->>'deleted_at') IS NULL )",
266 )
267 .bind(uom)
268 .fetch_one(conn)
269 .await?;
270 Ok(hit)
271 }
272
273 pub async fn soft_delete_uom(
277 &self,
278 conn: &mut PgConnection,
279 uom: Uuid,
280 ) -> Result<(), sqlx::Error> {
281 sqlx::query(
282 "UPDATE catalog.uoms \
283 SET metadata = jsonb_set(metadata, '{deleted_at}', to_jsonb(now())) \
284 WHERE id = $1",
285 )
286 .bind(uom)
287 .execute(conn)
288 .await?;
289 Ok(())
290 }
291}
292
293backbone_core::impl_crud_repository!(UomRepository, Uom, soft_delete);