1use std::collections::HashMap;
4
5use serde_json::Value;
6
7use crate::error::Result;
8use crate::sql_builder::SqlBuilder;
9
10#[derive(Debug, Clone, Default)]
16pub struct PriceFilter {
17 pub provider: Option<String>,
18 pub finish: Option<String>,
19 pub price_type: Option<String>,
20}
21
22pub struct PriceQuery<'a> {
28 conn: &'a crate::connection::Connection,
29}
30
31impl<'a> PriceQuery<'a> {
32 pub fn new(conn: &'a crate::connection::Connection) -> Self {
34 Self { conn }
35 }
36
37 pub fn get(&self, uuid: &str) -> Result<Value> {
41 self.conn.ensure_views(&["all_prices_today"])?;
42
43 let (sql, params) = SqlBuilder::new("all_prices_today")
44 .where_eq("uuid", uuid)
45 .order_by(&["date DESC"])
46 .build();
47
48 let rows = self.conn.execute(&sql, ¶ms)?;
49
50 let mut result: HashMap<String, HashMap<String, HashMap<String, HashMap<String, HashMap<String, HashMap<String, f64>>>>>> =
52 HashMap::new();
53
54 for row in &rows {
55 let source = row.get("source").and_then(|v| v.as_str()).unwrap_or("");
56 let provider = row.get("provider").and_then(|v| v.as_str()).unwrap_or("");
57 let currency = row.get("currency").and_then(|v| v.as_str()).unwrap_or("");
58 let price_type = row.get("price_type").and_then(|v| v.as_str()).unwrap_or("");
59 let finish = row.get("finish").and_then(|v| v.as_str()).unwrap_or("");
60 let date = row.get("date").and_then(|v| v.as_str()).unwrap_or("");
61 let price = row
62 .get("price")
63 .and_then(|v| v.as_f64())
64 .unwrap_or(0.0);
65
66 result
67 .entry(source.to_string())
68 .or_default()
69 .entry(provider.to_string())
70 .or_default()
71 .entry(currency.to_string())
72 .or_default()
73 .entry(price_type.to_string())
74 .or_default()
75 .entry(finish.to_string())
76 .or_default()
77 .insert(date.to_string(), price);
78 }
79
80 Ok(serde_json::to_value(result).unwrap_or(Value::Null))
81 }
82
83 pub fn today(&self, uuid: &str, filter: &PriceFilter) -> Result<Vec<Value>> {
87 self.conn.ensure_views(&["all_prices_today"])?;
88
89 let mut parts = vec![
90 "SELECT * FROM all_prices_today".to_string(),
91 "WHERE uuid = ?".to_string(),
92 "AND date = (SELECT MAX(date) FROM all_prices_today WHERE uuid = ?)".to_string(),
93 ];
94 let mut params = vec![uuid.to_string(), uuid.to_string()];
95
96 append_filter(&mut parts, &mut params, filter);
97
98 let sql = parts.join(" ");
99 let rows = self.conn.execute(&sql, ¶ms)?;
100 Ok(rows_to_values(rows))
101 }
102
103 pub fn history(
105 &self,
106 uuid: &str,
107 date_from: Option<&str>,
108 date_to: Option<&str>,
109 filter: &PriceFilter,
110 ) -> Result<Vec<Value>> {
111 self.conn.ensure_views(&["all_prices"])?;
112
113 let mut qb = SqlBuilder::new("all_prices");
114 qb.where_eq("uuid", uuid);
115 qb.order_by(&["date ASC"]);
116
117 if let Some(df) = date_from {
118 qb.where_gte("date", df);
119 }
120
121 if let Some(dt) = date_to {
122 qb.where_lte("date", dt);
123 }
124
125 if let Some(ref provider) = filter.provider {
126 qb.where_eq("provider", provider);
127 }
128 if let Some(ref finish) = filter.finish {
129 qb.where_eq("finish", finish);
130 }
131 if let Some(ref pt) = filter.price_type {
132 qb.where_eq("price_type", pt);
133 }
134
135 let (sql, params) = qb.build();
136 let rows = self.conn.execute(&sql, ¶ms)?;
137 Ok(rows_to_values(rows))
138 }
139
140 pub fn price_trend(&self, uuid: &str, filter: &PriceFilter) -> Result<Value> {
144 self.conn.ensure_views(&["all_prices"])?;
145
146 let price_type = filter
147 .price_type
148 .as_deref()
149 .unwrap_or("retail");
150
151 let mut parts = vec![
152 "SELECT".to_string(),
153 " MIN(price) AS min_price,".to_string(),
154 " MAX(price) AS max_price,".to_string(),
155 " AVG(price) AS avg_price,".to_string(),
156 " MIN(date) AS first_date,".to_string(),
157 " MAX(date) AS last_date,".to_string(),
158 " COUNT(*) AS data_points".to_string(),
159 "FROM all_prices_today".to_string(),
160 "WHERE uuid = ? AND price_type = ?".to_string(),
161 ];
162 let mut params = vec![uuid.to_string(), price_type.to_string()];
163
164 if let Some(ref provider) = filter.provider {
165 parts.push("AND provider = ?".to_string());
166 params.push(provider.clone());
167 }
168 if let Some(ref finish) = filter.finish {
169 parts.push("AND finish = ?".to_string());
170 params.push(finish.clone());
171 }
172
173 let sql = parts.join(" ");
174 let rows = self.conn.execute(&sql, ¶ms)?;
175 Ok(rows
176 .into_iter()
177 .next()
178 .map(|r| serde_json::to_value(r).unwrap_or(Value::Null))
179 .unwrap_or(Value::Null))
180 }
181
182 pub fn cheapest_printing(&self, name: &str, filter: &PriceFilter) -> Result<Option<Value>> {
186 self.conn.ensure_views(&["cards", "all_prices_today"])?;
187
188 let provider = filter.provider.as_deref().unwrap_or("tcgplayer");
189 let finish = filter.finish.as_deref().unwrap_or("normal");
190 let price_type = filter.price_type.as_deref().unwrap_or("retail");
191
192 let sql = r#"
193 SELECT c.uuid, c.setCode, c.number, p.price, p.date
194 FROM cards c
195 JOIN all_prices_today p ON c.uuid = p.uuid
196 WHERE c.name = ? AND p.provider = ?
197 AND p.finish = ? AND p.price_type = ?
198 AND p.date = (SELECT MAX(p2.date) FROM all_prices_today p2
199 WHERE p2.uuid = c.uuid AND p2.provider = ?
200 AND p2.finish = ? AND p2.price_type = ?)
201 ORDER BY p.price ASC
202 LIMIT 1
203 "#;
204
205 let rows = self.conn.execute(
206 sql,
207 &[
208 name.to_string(),
209 provider.to_string(),
210 finish.to_string(),
211 price_type.to_string(),
212 provider.to_string(),
213 finish.to_string(),
214 price_type.to_string(),
215 ],
216 )?;
217 Ok(rows
218 .into_iter()
219 .next()
220 .map(|r| serde_json::to_value(r).unwrap_or(Value::Null)))
221 }
222
223 pub fn cheapest_printings(
228 &self,
229 filter: &PriceFilter,
230 limit: Option<usize>,
231 offset: Option<usize>,
232 ) -> Result<Vec<Value>> {
233 self.conn.ensure_views(&["cards", "all_prices_today"])?;
234
235 let provider = filter.provider.as_deref().unwrap_or("tcgplayer");
236 let finish = filter.finish.as_deref().unwrap_or("normal");
237 let price_type = filter.price_type.as_deref().unwrap_or("retail");
238 let limit = limit.unwrap_or(100);
239 let offset = offset.unwrap_or(0);
240
241 let sql = format!(
242 r#"
243 SELECT c.name,
244 arg_min(c.setCode, p.price) AS cheapest_set,
245 arg_min(c.number, p.price) AS cheapest_number,
246 arg_min(c.uuid, p.price) AS cheapest_uuid,
247 MIN(p.price) AS min_price
248 FROM cards c
249 JOIN all_prices_today p ON c.uuid = p.uuid
250 WHERE p.provider = ? AND p.finish = ? AND p.price_type = ?
251 AND p.date = (SELECT MAX(date) FROM all_prices_today)
252 GROUP BY c.name
253 ORDER BY min_price ASC
254 LIMIT {} OFFSET {}
255 "#,
256 limit, offset
257 );
258
259 let rows = self.conn.execute(
260 &sql,
261 &[
262 provider.to_string(),
263 finish.to_string(),
264 price_type.to_string(),
265 ],
266 )?;
267 Ok(rows_to_values(rows))
268 }
269
270 pub fn most_expensive_printings(
275 &self,
276 filter: &PriceFilter,
277 limit: Option<usize>,
278 offset: Option<usize>,
279 ) -> Result<Vec<Value>> {
280 self.conn.ensure_views(&["cards", "all_prices_today"])?;
281
282 let provider = filter.provider.as_deref().unwrap_or("tcgplayer");
283 let finish = filter.finish.as_deref().unwrap_or("normal");
284 let price_type = filter.price_type.as_deref().unwrap_or("retail");
285 let limit = limit.unwrap_or(100);
286 let offset = offset.unwrap_or(0);
287
288 let sql = format!(
289 r#"
290 SELECT c.name,
291 arg_max(c.setCode, p.price) AS priciest_set,
292 arg_max(c.number, p.price) AS priciest_number,
293 arg_max(c.uuid, p.price) AS priciest_uuid,
294 MAX(p.price) AS max_price
295 FROM cards c
296 JOIN all_prices_today p ON c.uuid = p.uuid
297 WHERE p.provider = ? AND p.finish = ? AND p.price_type = ?
298 AND p.date = (SELECT MAX(date) FROM all_prices_today)
299 GROUP BY c.name
300 ORDER BY max_price DESC
301 LIMIT {} OFFSET {}
302 "#,
303 limit, offset
304 );
305
306 let rows = self.conn.execute(
307 &sql,
308 &[
309 provider.to_string(),
310 finish.to_string(),
311 price_type.to_string(),
312 ],
313 )?;
314 Ok(rows_to_values(rows))
315 }
316}
317
318fn append_filter(parts: &mut Vec<String>, params: &mut Vec<String>, filter: &PriceFilter) {
323 if let Some(ref provider) = filter.provider {
324 parts.push("AND provider = ?".to_string());
325 params.push(provider.clone());
326 }
327 if let Some(ref finish) = filter.finish {
328 parts.push("AND finish = ?".to_string());
329 params.push(finish.clone());
330 }
331 if let Some(ref pt) = filter.price_type {
332 parts.push("AND price_type = ?".to_string());
333 params.push(pt.clone());
334 }
335}
336
337fn rows_to_values(rows: Vec<HashMap<String, Value>>) -> Vec<Value> {
338 rows.into_iter()
339 .map(|r| serde_json::to_value(r).unwrap_or(Value::Null))
340 .collect()
341}