1use amazon_spapi::client::{SpapiClient, SpapiConfig};
2use anyhow::{anyhow, Result};
3use csv::ReaderBuilder;
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct RestockRecommendation {
8 #[serde(rename = "Country")]
9 pub country: String,
10
11 #[serde(rename = "Product Name")]
12 pub product_name: String,
13
14 #[serde(rename = "FNSKU")]
15 pub fnsku: String,
16
17 #[serde(rename = "Merchant SKU")]
18 pub merchant_sku: String,
19
20 #[serde(rename = "ASIN")]
21 pub asin: String,
22
23 #[serde(rename = "Condition")]
24 pub condition: String,
25
26 #[serde(rename = "Supplier")]
27 pub supplier: String,
28
29 #[serde(rename = "Supplier part no.")]
30 pub supplier_part_no: String,
31
32 #[serde(rename = "Currency code")]
33 pub currency_code: String,
34
35 #[serde(rename = "Price", deserialize_with = "deserialize_f64")]
36 pub price: f64,
37
38 #[serde(rename = "Sales last 30 days", deserialize_with = "deserialize_f64")]
39 pub sales_last_30_days: f64,
40
41 #[serde(
42 rename = "Units Sold Last 30 Days",
43 deserialize_with = "deserialize_u32"
44 )]
45 pub units_sold_last_30_days: u32,
46
47 #[serde(rename = "Total Units", deserialize_with = "deserialize_u32")]
48 pub total_units: u32,
49
50 #[serde(rename = "Inbound", deserialize_with = "deserialize_u32")]
51 pub inbound: u32,
52
53 #[serde(rename = "Available", deserialize_with = "deserialize_u32")]
54 pub available: u32,
55
56 #[serde(rename = "FC transfer", deserialize_with = "deserialize_u32")]
57 pub fc_transfer: u32,
58
59 #[serde(rename = "FC Processing", deserialize_with = "deserialize_u32")]
60 pub fc_processing: u32,
61
62 #[serde(rename = "Customer Order", deserialize_with = "deserialize_u32")]
63 pub customer_order: u32,
64
65 #[serde(rename = "Unfulfillable", deserialize_with = "deserialize_u32")]
66 pub unfulfillable: u32,
67
68 #[serde(rename = "Working", deserialize_with = "deserialize_u32")]
69 pub working: u32,
70
71 #[serde(rename = "Shipped", deserialize_with = "deserialize_u32")]
72 pub shipped: u32,
73
74 #[serde(rename = "Receiving", deserialize_with = "deserialize_u32")]
75 pub receiving: u32,
76
77 #[serde(rename = "Fulfilled by")]
78 pub fulfilled_by: String,
79
80 #[serde(
81 rename = "Total Days of Supply (including units from open shipments)",
82 deserialize_with = "deserialize_u32"
83 )]
84 pub total_days_of_supply: u32,
85
86 #[serde(
87 rename = "Days of Supply at Amazon Fulfillment Network",
88 deserialize_with = "deserialize_u32"
89 )]
90 pub days_of_supply_at_afn: u32,
91
92 #[serde(rename = "Alert")]
93 pub alert: String,
94
95 #[serde(
96 rename = "Recommended replenishment qty",
97 deserialize_with = "deserialize_u32"
98 )]
99 pub recommended_replenishment_qty: u32,
100
101 #[serde(rename = "Recommended ship date")]
102 pub recommended_ship_date: String,
103
104 #[serde(rename = "Recommended action")]
105 pub recommended_action: String,
106
107 #[serde(rename = "Unit storage size")]
108 pub unit_storage_size: String,
109}
110
111fn deserialize_f64<'de, D>(deserializer: D) -> Result<f64, D::Error>
112where
113 D: serde::Deserializer<'de>,
114{
115 let s: String = Deserialize::deserialize(deserializer)?;
116 let cleaned = s.trim();
117 if cleaned.is_empty() {
118 return Ok(0.0);
119 }
120
121 let cleaned = cleaned.trim_end_matches('+');
122 cleaned
123 .parse::<f64>()
124 .map_err(|_| anyhow!("Failed to parse number: '{}'", s))
125 .map_err(serde::de::Error::custom)
126}
127
128fn deserialize_u32<'de, D>(deserializer: D) -> Result<u32, D::Error>
129where
130 D: serde::Deserializer<'de>,
131{
132 let s: String = Deserialize::deserialize(deserializer)?;
133 let cleaned = s.trim();
134 if cleaned.is_empty() {
135 return Ok(0);
136 }
137
138 let cleaned = if cleaned.ends_with('+') {
139 cleaned.trim_end_matches('+')
140 } else {
141 cleaned
142 };
143
144 let result = cleaned.parse::<u32>().unwrap_or_else(|_| {
145 eprintln!("Warning: Could not parse integer '{}', using 0", s);
146 0
147 });
148
149 Ok(result)
150}
151
152pub fn parse_restock_report(csv_content: &str) -> Result<Vec<RestockRecommendation>> {
153 let mut reader = ReaderBuilder::new()
154 .delimiter(b'\t')
155 .has_headers(true)
156 .from_reader(csv_content.as_bytes());
157
158 let mut recommendations = Vec::new();
159
160 for (line_num, result) in reader.deserialize().enumerate() {
161 match result {
162 Ok(record) => {
163 recommendations.push(record);
164 }
165 Err(e) => {
166 return Err(anyhow!("Failed to parse CSV at line {}: {}", line_num + 2, e));
167 }
168 }
169 }
170
171 println!(
172 "Successfully parsed {} recommendations",
173 recommendations.len()
174 );
175 Ok(recommendations)
176}
177
178#[tokio::main]
179async fn main() -> Result<()> {
180 env_logger::init();
181
182 let spapi_config = SpapiConfig::from_env()?;
183 let client = SpapiClient::new(spapi_config.clone())?;
184 let marketplace_ids = vec!["ATVPDKIKX0DER".to_string()]; let report_type = "GET_RESTOCK_INVENTORY_RECOMMENDATIONS_REPORT";
188 let report_content = client
189 .fetch_report(
190 report_type,
191 marketplace_ids.clone(),
192 None,
193 Some(|attempt, status| {
194 println!("Attempt get report {}: {:?}", attempt, status);
195 }),
196 )
197 .await?;
198
199 let report_file_path = "/tmp/restock_inventory_report.txt";
201 std::fs::write(report_file_path, &report_content)
202 .expect("Unable to write report content to file");
203
204 let restocks = parse_restock_report(&report_content);
210
211 println!("Restock inventory report generated successfully.");
212 println!("{:#?}", restocks);
213
214 Ok(())
215}