1use serde_json::json;
7use sqlx::PgPool;
8use uuid::Uuid;
9
10use crate::{
11 auth::Actor,
12 domain::{self, ItemType},
13 error::{AppError, AppResult},
14 models::Revision,
15 services::{audit, items, revisions},
16};
17
18pub struct SaveAs {
19 pub number: Option<String>,
21 pub name: String,
23 pub project_id: Option<Uuid>,
25 pub with_files: bool,
26 pub with_bom: bool,
27}
28
29pub async fn save_as(
46 pool: &PgPool,
47 actor: &Actor,
48 source_revision_id: Uuid,
49 input: SaveAs,
50) -> AppResult<Revision> {
51 let src = revisions::get(pool, actor, source_revision_id).await?;
52 let item_type = ItemType::parse(&src.item_type)
53 .ok_or_else(|| AppError::Other(anyhow::anyhow!("unknown item type {}", src.item_type)))?;
54 let name = match input.name.trim() {
55 "" => src.name.clone(),
56 name => name.to_string(),
57 };
58 let mut tx = pool.begin().await?;
59 let target = input.project_id.or(src.project_id);
62 if let Some(source) = src.project_id {
63 if target != Some(source) {
64 items::ensure_may_take_out(&mut tx, actor, source, "Copying").await?;
65 }
66 }
67 let created = items::create_tx(
69 &mut tx,
70 actor,
71 items::NewItem {
72 number: input.number.filter(|n| !n.trim().is_empty()),
73 item_type,
74 unit: src.unit.clone(),
75 project_id: input.project_id.or(src.project_id),
76 name,
77 description: src.description.clone(),
78 material: src.material.clone(),
79 mass_kg: src.mass_kg,
80 unit_cost: src.unit_cost,
81 classification: domain::Classification::parse(&src.classification).unwrap_or_default(),
82 attributes: Vec::new(),
83 },
84 )
85 .await?;
86 let new_rev = created.revision_id;
87 sqlx::query("UPDATE revisions SET attributes = $2 WHERE id = $1")
90 .bind(new_rev)
91 .bind(&src.attributes)
92 .execute(&mut *tx)
93 .await?;
94 let label = format!("{}/{}", src.item_number, src.code);
95 let source_restricted =
99 match src.project_id {
100 Some(source) if target != Some(source) => sqlx::query_scalar::<_, bool>(
101 "SELECT EXISTS(SELECT 1 FROM projects WHERE id = $1 AND visibility = 'restricted')",
102 )
103 .bind(source)
104 .fetch_one(&mut *tx)
105 .await?,
106 _ => false,
107 };
108 let copied_from = if source_restricted {
109 "a restricted item".to_string()
110 } else {
111 label.clone()
112 };
113
114 let mut files = 0;
115 if input.with_files {
116 files = sqlx::query(
117 "INSERT INTO files (id, revision_id, filename, content_type, size_bytes, sha256,
118 uploaded_by, uploaded_at, format, folder, retention)
119 SELECT gen_random_uuid(), $2, filename, content_type, size_bytes, sha256, $3, now(), format,
120 folder, retention
121 FROM files WHERE revision_id = $1",
122 )
123 .bind(src.id)
124 .bind(new_rev)
125 .bind(actor.id)
126 .execute(&mut *tx)
127 .await?
128 .rows_affected();
129 sqlx::query(
132 "INSERT INTO file_versions (id, file_id, version, sha256, size_bytes, content_type, comment,
133 uploaded_by, uploaded_at, content_text, references_scanned,
134 export_name, export_name_scanned, solver_summary)
135 SELECT gen_random_uuid(), f.id, 1, f.sha256, f.size_bytes, f.content_type,
136 'Copied from ' || $3, $4, now(),
137 COALESCE(ov.content_text, ''), COALESCE(ov.references_scanned, FALSE),
138 ov.export_name, COALESCE(ov.export_name_scanned, FALSE), ov.solver_summary
139 FROM files f
140 LEFT JOIN files ofl ON ofl.revision_id = $2 AND ofl.filename = f.filename
141 LEFT JOIN file_versions ov ON ov.file_id = ofl.id AND ov.version = ofl.current_version
142 WHERE f.revision_id = $1",
143 )
144 .bind(new_rev)
145 .bind(src.id)
146 .bind(&copied_from)
147 .bind(actor.id)
148 .execute(&mut *tx)
149 .await?;
150 sqlx::query(
151 "INSERT INTO file_references (file_version_id, name, instances)
152 SELECT nv.id, fr.name, fr.instances
153 FROM files nf
154 JOIN file_versions nv ON nv.file_id = nf.id
155 JOIN files ofl ON ofl.revision_id = $2 AND ofl.filename = nf.filename
156 JOIN file_versions ov ON ov.file_id = ofl.id AND ov.version = ofl.current_version
157 JOIN file_references fr ON fr.file_version_id = ov.id
158 WHERE nf.revision_id = $1",
159 )
160 .bind(new_rev)
161 .bind(src.id)
162 .execute(&mut *tx)
163 .await?;
164 }
165 let mut lines = 0;
166 if input.with_bom {
167 lines = sqlx::query(
168 "INSERT INTO bom_lines (id, parent_revision_id, child_item_id, find_number, quantity, notes)
169 SELECT gen_random_uuid(), $2, child_item_id, find_number, quantity, notes
170 FROM bom_lines WHERE parent_revision_id = $1",
171 )
172 .bind(src.id)
173 .bind(new_rev)
174 .execute(&mut *tx)
175 .await?
176 .rows_affected();
177 }
178 audit::record_related(
179 &mut tx,
180 Some(actor),
181 "item.save_as",
182 "item",
183 Some(created.item_id),
184 Some(src.item_id),
185 format!("Saved {label} as a new item"),
186 json!({ "from_revision": src.id, "files": files, "bom_lines": lines }),
187 )
188 .await?;
189 tx.commit().await?;
190 revisions::get(pool, actor, new_rev).await
191}