backbone_catalog/infrastructure/persistence/
item_repository.rs1use rust_decimal::Decimal;
17use sqlx::{PgConnection, PgPool, Row};
18use uuid::Uuid;
19
20use crate::domain::entity::Item;
21
22pub const TABLE_NAME: &str = "catalog.items";
24
25pub struct ItemRepository(
30 backbone_orm::GenericCrudRepository<Item, backbone_orm::SoftDelete>,
31);
32
33impl std::ops::Deref for ItemRepository {
34 type Target = backbone_orm::GenericCrudRepository<Item, backbone_orm::SoftDelete>;
35 fn deref(&self) -> &Self::Target { &self.0 }
36}
37
38impl ItemRepository {
39 pub fn new(pool: PgPool) -> Self {
41 Self(backbone_orm::GenericCrudRepository::new(pool, TABLE_NAME))
42 }
43}
44
45#[derive(Debug, Clone, serde::Serialize, sqlx::FromRow)]
51pub struct ItemHit {
52 pub item_id: Uuid,
53 pub variant_id: Option<Uuid>,
54 pub item_code: String,
55 pub name: String,
56 pub barcode: Option<String>,
57 pub sku: Option<String>,
58}
59
60pub struct NewItemRow<'a> {
62 pub id: Uuid,
63 pub item_code: &'a str,
64 pub name: &'a str,
65 pub description: Option<&'a str>,
66 pub barcode: Option<&'a str>,
67 pub brand_id: Option<Uuid>,
68 pub item_group_id: Uuid,
69 pub default_uom_id: Uuid,
70 pub item_type: &'a str,
71 pub is_sales_item: bool,
72 pub is_purchase_item: bool,
73 pub is_stock_item: bool,
74 pub hsn_code: Option<&'a str>,
75 pub is_taxable: bool,
76 pub weight_per_unit: Option<Decimal>,
77 pub standard_cost: Option<Decimal>,
78 pub tags: &'a serde_json::Value,
79 pub data: &'a serde_json::Value,
80}
81
82impl ItemRepository {
84 pub async fn find_by_scan_code(
89 &self,
90 executor: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
91 code: &str,
92 ) -> Result<Option<ItemHit>, sqlx::Error> {
93 let hit = sqlx::query_as::<_, ItemHit>(
94 r#"SELECT id AS item_id, NULL::uuid AS variant_id, item_code, name, barcode, NULL::text AS sku
95 FROM catalog.items
96 WHERE (barcode = $1 OR item_code = $1)
97 AND (metadata->>'deleted_at') IS NULL
98 LIMIT 1"#,
99 )
100 .bind(code)
101 .fetch_optional(executor)
102 .await?;
103 Ok(hit)
104 }
105
106 pub async fn find_by_scan_code_scoped(
110 &self,
111 pool: &PgPool,
112 code: &str,
113 ) -> Result<Option<ItemHit>, sqlx::Error> {
114 let row = backbone_orm::org_scope::fetch_optional_row_scoped(
115 pool,
116 sqlx::query(
117 r#"SELECT id AS item_id, NULL::uuid AS variant_id, item_code, name, barcode, NULL::text AS sku
118 FROM catalog.items
119 WHERE (barcode = $1 OR item_code = $1)
120 AND (metadata->>'deleted_at') IS NULL
121 LIMIT 1"#,
122 )
123 .bind(code),
124 )
125 .await?;
126 Ok(row.map(|r| ItemHit {
127 item_id: r.get("item_id"),
128 variant_id: r.get("variant_id"),
129 item_code: r.get("item_code"),
130 name: r.get("name"),
131 barcode: r.get("barcode"),
132 sku: r.get("sku"),
133 }))
134 }
135
136 pub async fn exists_id(
140 &self,
141 executor: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
142 id: Uuid,
143 ) -> Result<bool, sqlx::Error> {
144 let found: Option<Uuid> = sqlx::query_scalar(
145 "SELECT id FROM catalog.items \
146 WHERE id = $1 AND (metadata->>'deleted_at') IS NULL",
147 )
148 .bind(id)
149 .fetch_optional(executor)
150 .await?;
151 Ok(found.is_some())
152 }
153
154 pub async fn insert_item(
157 &self,
158 executor: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
159 r: &NewItemRow<'_>,
160 ) -> Result<(), sqlx::Error> {
161 sqlx::query(
162 r#"INSERT INTO catalog.items
163 (id, item_code, name, description, barcode, brand_id, item_group_id,
164 default_uom_id, item_type, is_sales_item, is_purchase_item, is_stock_item,
165 hsn_code, is_taxable, weight_per_unit, standard_cost, tags, data, status)
166 VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9::item_type,$10,$11,$12,$13,$14,$15,$16,$17,$18,'active'::catalog_status)"#,
167 )
168 .bind(r.id)
169 .bind(r.item_code)
170 .bind(r.name)
171 .bind(r.description)
172 .bind(r.barcode)
173 .bind(r.brand_id)
174 .bind(r.item_group_id)
175 .bind(r.default_uom_id)
176 .bind(r.item_type)
177 .bind(r.is_sales_item)
178 .bind(r.is_purchase_item)
179 .bind(r.is_stock_item)
180 .bind(r.hsn_code)
181 .bind(r.is_taxable)
182 .bind(r.weight_per_unit)
183 .bind(r.standard_cost)
184 .bind(r.tags)
185 .bind(r.data)
186 .execute(executor)
187 .await?;
188 Ok(())
189 }
190
191 pub async fn set_has_variants_true(
194 &self,
195 conn: &mut PgConnection,
196 item_id: Uuid,
197 ) -> Result<(), sqlx::Error> {
198 sqlx::query("UPDATE catalog.items SET has_variants = TRUE WHERE id = $1")
199 .bind(item_id)
200 .execute(conn)
201 .await?;
202 Ok(())
203 }
204
205 pub async fn find_status(
208 &self,
209 conn: &mut PgConnection,
210 item_id: Uuid,
211 ) -> Result<Option<crate::domain::entity::CatalogStatus>, sqlx::Error> {
212 Ok(sqlx::query_scalar("SELECT status FROM catalog.items WHERE id = $1")
213 .bind(item_id)
214 .fetch_optional(conn)
215 .await?)
216 }
217
218 pub async fn set_status(
220 &self,
221 conn: &mut PgConnection,
222 item_id: Uuid,
223 status: crate::domain::entity::CatalogStatus,
224 ) -> Result<(), sqlx::Error> {
225 sqlx::query("UPDATE catalog.items SET status = $1 WHERE id = $2")
226 .bind(status)
227 .bind(item_id)
228 .execute(conn)
229 .await?;
230 Ok(())
231 }
232
233 pub async fn set_has_variants_false(
236 &self,
237 conn: &mut PgConnection,
238 item_id: Uuid,
239 ) -> Result<(), sqlx::Error> {
240 sqlx::query("UPDATE catalog.items SET has_variants = FALSE WHERE id=$1")
241 .bind(item_id)
242 .execute(conn)
243 .await?;
244 Ok(())
245 }
246}
247
248backbone_core::impl_crud_repository!(ItemRepository, Item, soft_delete);