reports/
reports.rs

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()]; // Amazon US Marketplace ID
185
186    // create report specification
187    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    // save report content to a file
200    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    // // load report content from the file
205    // let report_file_path = "/tmp/restock_inventory_report.txt";
206    // let report_content =
207    //     std::fs::read_to_string(report_file_path).expect("Unable to read report content from file");
208
209    let restocks = parse_restock_report(&report_content);
210
211    println!("Restock inventory report generated successfully.");
212    println!("{:#?}", restocks);
213
214    Ok(())
215}