backbone_catalog/infrastructure/persistence/
item_variant_repository.rs1use rust_decimal::Decimal;
17use sqlx::{PgConnection, PgPool, Row};
18use uuid::Uuid;
19
20use crate::domain::entity::ItemVariant;
21use crate::infrastructure::persistence::item_repository::ItemHit;
22
23pub const TABLE_NAME: &str = "catalog.item_variants";
25
26pub struct ItemVariantRepository(
31 backbone_orm::GenericCrudRepository<ItemVariant, backbone_orm::SoftDelete>,
32);
33
34impl std::ops::Deref for ItemVariantRepository {
35 type Target = backbone_orm::GenericCrudRepository<ItemVariant, backbone_orm::SoftDelete>;
36 fn deref(&self) -> &Self::Target { &self.0 }
37}
38
39impl ItemVariantRepository {
40 pub fn new(pool: PgPool) -> Self {
42 Self(backbone_orm::GenericCrudRepository::new(pool, TABLE_NAME))
43 }
44}
45
46pub struct NewItemVariantRow<'a> {
49 pub id: Uuid,
50 pub item_id: Uuid,
51 pub sku: &'a str,
52 pub variant_label: &'a str,
53 pub options: &'a serde_json::Value,
54 pub barcode: Option<&'a str>,
55 pub is_default: bool,
56 pub weight_per_unit: Option<Decimal>,
57}
58
59impl ItemVariantRepository {
61 pub async fn find_variant_by_scan_code(
66 &self,
67 executor: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
68 code: &str,
69 ) -> Result<Option<ItemHit>, sqlx::Error> {
70 let hit = sqlx::query_as::<_, ItemHit>(
71 r#"SELECT v.item_id, v.id AS variant_id, i.item_code, i.name, v.barcode, v.sku
72 FROM catalog.item_variants v JOIN catalog.items i ON i.id = v.item_id
73 WHERE (v.barcode = $1 OR v.sku = $1)
74 AND (v.metadata->>'deleted_at') IS NULL
75 LIMIT 1"#,
76 )
77 .bind(code)
78 .fetch_optional(executor)
79 .await?;
80 Ok(hit)
81 }
82
83 pub async fn find_variant_by_scan_code_scoped(
87 &self,
88 pool: &PgPool,
89 code: &str,
90 ) -> Result<Option<ItemHit>, sqlx::Error> {
91 let row = backbone_orm::org_scope::fetch_optional_row_scoped(
92 pool,
93 sqlx::query(
94 r#"SELECT v.item_id, v.id AS variant_id, i.item_code, i.name, v.barcode, v.sku
95 FROM catalog.item_variants v JOIN catalog.items i ON i.id = v.item_id
96 WHERE (v.barcode = $1 OR v.sku = $1)
97 AND (v.metadata->>'deleted_at') IS NULL
98 LIMIT 1"#,
99 )
100 .bind(code),
101 )
102 .await?;
103 Ok(row.map(|r| ItemHit {
104 item_id: r.get("item_id"),
105 variant_id: r.get("variant_id"),
106 item_code: r.get("item_code"),
107 name: r.get("name"),
108 barcode: r.get("barcode"),
109 sku: r.get("sku"),
110 }))
111 }
112
113 pub async fn find_item_id_for_live(
116 &self,
117 executor: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
118 variant_id: Uuid,
119 ) -> Result<Option<Uuid>, sqlx::Error> {
120 let item_id: Option<Uuid> = sqlx::query_scalar(
121 "SELECT item_id FROM catalog.item_variants \
122 WHERE id=$1 AND (metadata->>'deleted_at') IS NULL",
123 )
124 .bind(variant_id)
125 .fetch_optional(executor)
126 .await?;
127 Ok(item_id)
128 }
129
130 pub async fn insert_variant(
133 &self,
134 conn: &mut PgConnection,
135 r: &NewItemVariantRow<'_>,
136 ) -> Result<(), sqlx::Error> {
137 sqlx::query(
138 r#"INSERT INTO catalog.item_variants
139 (id, item_id, sku, variant_label, options, barcode, is_default, weight_per_unit, status)
140 VALUES ($1,$2,$3,$4,$5,$6,$7,$8,'active'::catalog_status)"#,
141 )
142 .bind(r.id)
143 .bind(r.item_id)
144 .bind(r.sku)
145 .bind(r.variant_label)
146 .bind(r.options)
147 .bind(r.barcode)
148 .bind(r.is_default)
149 .bind(r.weight_per_unit)
150 .execute(conn)
151 .await?;
152 Ok(())
153 }
154
155 pub async fn soft_delete_variant(
157 &self,
158 conn: &mut PgConnection,
159 variant_id: Uuid,
160 ) -> Result<(), sqlx::Error> {
161 sqlx::query(
162 "UPDATE catalog.item_variants \
163 SET metadata = jsonb_set(metadata, '{deleted_at}', to_jsonb(now())) \
164 WHERE id=$1",
165 )
166 .bind(variant_id)
167 .execute(conn)
168 .await?;
169 Ok(())
170 }
171
172 pub async fn count_live_variants(
175 &self,
176 conn: &mut PgConnection,
177 item_id: Uuid,
178 ) -> Result<i64, sqlx::Error> {
179 let remaining: i64 = sqlx::query_scalar(
180 "SELECT COUNT(*) FROM catalog.item_variants \
181 WHERE item_id=$1 AND (metadata->>'deleted_at') IS NULL",
182 )
183 .bind(item_id)
184 .fetch_one(conn)
185 .await?;
186 Ok(remaining)
187 }
188}
189
190backbone_core::impl_crud_repository!(ItemVariantRepository, ItemVariant, soft_delete);