diff --git a/.gitignore b/.gitignore index 8adb609..ccee31f 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,7 @@ Cargo.lock .env* samples .DS_Store -.vscode \ No newline at end of file +.vscode +.idea +/schemas +/tmp \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index a7912f5..f7b9f4c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,6 @@ [workspace] members = [ "cli", - "walmart-partner-api" + "walmart-partner-api", + "openapi" ] \ No newline at end of file diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 0daf519..4004ded 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -2,12 +2,15 @@ name = "cli" version = "0.1.0" authors = ["Flux Xu "] -edition = "2018" +edition = "2021" [dependencies] -clap = "2.26.0" -dotenv = "0.10.1" +clap = { version = "4.0.26", features = ["derive"] } +dotenv = "0.15.0" chrono = { version = "0.4", features = ["serde"] } walmart_partner_api = { path = "../walmart-partner-api" } serde_json = "1.0.2" -env_logger = "0.6.2" \ No newline at end of file +anyhow = "1.0.66" +tokio = { version = "1.0", features = ["rt"] } +tracing-subscriber = "0.3.16" +tracing = "0.1.37" \ No newline at end of file diff --git a/cli/src/feed.rs b/cli/src/feed.rs index 355b91f..e595c9e 100644 --- a/cli/src/feed.rs +++ b/cli/src/feed.rs @@ -1,20 +1,112 @@ -use std::fs::File; -use walmart_partner_api::Client; +use anyhow::Result; +use clap::{Parser, Subcommand}; -pub fn upload(client: &Client, feed_type: &str, path: &str) { - let f = File::open(path).unwrap(); - let ack = client.bulk_upload_xml(feed_type, f).unwrap(); - println!("{:#?}", ack); +#[derive(Subcommand)] +pub enum CaFeedCommand { + ListStatuses(FeedStatus), + GetStatusItem(FeedStatusItem), + UploadXml(CaUploadXml), } -pub fn status(client: &Client) { - let status = client.get_all_feed_statuses(&Default::default()).unwrap(); - println!("{:#?}", status); +#[derive(Subcommand)] +pub enum UsFeedCommand { + ListStatuses(FeedStatus), + GetStatusItem(FeedStatusItem), } -pub fn inspect(client: &Client, id: &str) { - let status = client - .get_feed_and_item_status(id, &Default::default()) - .unwrap(); - println!("{:#?}", status); +#[derive(Parser)] +pub struct FeedStatus { + #[clap(long)] + pub feed_id: Option, + #[clap(long)] + pub limit: Option, + #[clap(long)] + pub offset: Option, +} + +#[derive(Parser)] +pub struct FeedStatusItem { + #[clap(long)] + pub id: String, + #[clap(long)] + pub include_details: Option, + #[clap(long)] + pub limit: Option, + #[clap(long)] + pub offset: Option, +} + +#[derive(Parser)] +pub struct CaUploadXml { + #[clap(long)] + pub feed_type: String, + #[clap(long)] + pub path: String, +} + +impl CaFeedCommand { + pub async fn run(self, client: walmart_partner_api::ca::Client) -> Result<()> { + match self { + CaFeedCommand::ListStatuses(cmd) => { + let r = client + .get_all_feed_statuses(walmart_partner_api::ca::GetAllFeedStatusesQuery { + feed_id: cmd.feed_id, + limit: cmd.limit, + offset: cmd.offset, + }) + .await?; + println!("{:#?}", r) + } + CaFeedCommand::GetStatusItem(cmd) => { + let r = client + .get_feed_and_item_status( + &cmd.id, + walmart_partner_api::ca::GetFeedAndItemStatusQuery { + include_details: cmd.include_details, + limit: cmd.limit, + offset: cmd.offset, + }, + ) + .await?; + println!("{:#?}", r) + } + CaFeedCommand::UploadXml(cmd) => { + let f = std::fs::File::open(cmd.path).unwrap(); + let r = client.bulk_upload_xml(&cmd.feed_type, f).await?; + println!("{:#?}", r) + } + } + Ok(()) + } +} + +impl UsFeedCommand { + pub async fn run(self, client: walmart_partner_api::us::Client) -> Result<()> { + match self { + UsFeedCommand::ListStatuses(cmd) => { + let r = client + .get_all_feed_statuses(walmart_partner_api::us::GetAllFeedStatusesQuery { + feed_id: cmd.feed_id, + limit: cmd.limit, + offset: cmd.offset, + }) + .await?; + println!("{:#?}", r) + } + UsFeedCommand::GetStatusItem(cmd) => { + let r = client + .get_feed_and_item_status( + &cmd.id, + walmart_partner_api::us::GetFeedAndItemStatusQuery { + include_details: cmd.include_details, + limit: cmd.limit, + offset: cmd.offset, + }, + ) + .await?; + println!("{:#?}", r) + } + } + Ok(()) + } } diff --git a/cli/src/inventory.rs b/cli/src/inventory.rs index cca044d..d30f7a4 100644 --- a/cli/src/inventory.rs +++ b/cli/src/inventory.rs @@ -1,15 +1,97 @@ -use walmart_partner_api::inventory::*; -use walmart_partner_api::Client; - -pub fn set_inventory(client: &Client, sku: &str, quantity: i32, lagtime: i32) { - let inventory = Inventory { - quantity: Quantity { - unit: "EACH".to_string(), - amount: quantity, - }, - sku: sku.to_string(), - fulfillmentLagTime: lagtime, - }; - let res = client.update_item_inventory(&inventory).unwrap(); - println!("{}", serde_json::to_string_pretty(&res).unwrap()); +use anyhow::Result; +use clap::{Parser, Subcommand}; + +#[derive(Subcommand)] +pub enum CaInventoryCommand { + Get(Get), + Update(CaUpdate), +} + +#[derive(Subcommand)] +pub enum UsInventoryCommand { + Get(Get), + Update(UsUpdate), +} + +#[derive(Parser)] +pub struct Get { + #[clap(long)] + pub sku: String, +} + +#[derive(Parser)] +pub struct CaUpdate { + #[clap(long)] + pub sku: String, + #[clap(long)] + pub unit: String, + #[clap(long)] + pub amount: i32, + #[clap(long)] + pub fulfillment_lag_time: i32, + #[clap(long)] + pub partner_id: Option, + #[clap(long)] + pub offer_id: Option, +} + +#[derive(Parser)] +pub struct UsUpdate { + #[clap(long)] + pub sku: String, + #[clap(long)] + pub unit: String, + #[clap(long)] + pub amount: i32, +} + +impl CaInventoryCommand { + pub async fn run(self, client: walmart_partner_api::ca::Client) -> Result<()> { + match self { + CaInventoryCommand::Get(cmd) => { + let r = client.get_item_inventory(cmd.sku).await?; + println!("{:#?}", r) + } + CaInventoryCommand::Update(cmd) => { + let r = client + .update_item_inventory(walmart_partner_api::ca::Inventory { + sku: cmd.sku, + fulfillment_lag_time: cmd.fulfillment_lag_time, + partner_id: cmd.partner_id, + offer_id: cmd.offer_id, + quantity: walmart_partner_api::ca::InventoryQuantity { + unit: cmd.unit, + amount: cmd.amount, + }, + }) + .await?; + println!("{:#?}", r) + } + } + Ok(()) + } +} + +impl UsInventoryCommand { + pub async fn run(self, client: walmart_partner_api::us::Client) -> Result<()> { + match self { + UsInventoryCommand::Get(cmd) => { + let r = client.get_item_inventory(cmd.sku).await?; + println!("{:#?}", r) + } + UsInventoryCommand::Update(cmd) => { + let r = client + .update_item_inventory(walmart_partner_api::us::Inventory { + sku: cmd.sku, + quantity: walmart_partner_api::us::InventoryQuantity { + unit: cmd.unit, + amount: cmd.amount, + }, + }) + .await?; + println!("{:#?}", r) + } + } + Ok(()) + } } diff --git a/cli/src/item.rs b/cli/src/item.rs index bc9e9c2..acf0085 100644 --- a/cli/src/item.rs +++ b/cli/src/item.rs @@ -1,31 +1,97 @@ -use serde_json; -use walmart_partner_api::item::*; -use walmart_partner_api::Client; +use anyhow::Result; +use clap::{Parser, Subcommand}; -pub fn dump(client: &Client) { - let mut params: GetAllItemsQueryParams = Default::default(); - params.limit = Some(20); - let mut items = vec![]; - loop { - println!("loading params = {:#?}", params); +#[derive(Subcommand)] +pub enum CaItemCommand { + Get(Get), + List(List), + Retire(Retire), +} - let (res, next_params) = client.get_all_items(¶ms).unwrap(); - let mut res = res.into_inner(); +#[derive(Subcommand)] +pub enum UsItemCommand { + Get(Get), + List(List), + Retire(Retire), +} - if res.items.is_empty() { - break; - } +#[derive(Parser)] +pub struct Get { + #[clap(long)] + pub sku: String, +} + +#[derive(Parser)] +pub struct List { + #[clap(long)] + pub next_cursor: Option, + #[clap(long)] + pub sku: Option, + #[clap(long)] + pub limit: Option, + #[clap(long)] + pub offset: Option, +} - if let Some(next_params) = next_params { - params = next_params; - println!("page items = {}", res.items.len()); - items.append(&mut res.items); - } else { - break; +#[derive(Parser)] +pub struct Retire { + #[clap(long)] + pub sku: String, +} + +impl CaItemCommand { + pub async fn run(self, client: walmart_partner_api::ca::Client) -> Result<()> { + match self { + CaItemCommand::Get(cmd) => { + let r = client.get_item(&cmd.sku).await?; + println!("{:#?}", r) + } + CaItemCommand::List(cmd) => { + let r = client + .get_all_items(walmart_partner_api::ca::GetAllItemsQuery { + next_cursor: cmd.next_cursor.map(Into::into).unwrap_or_default(), + sku: cmd.sku, + limit: cmd.limit, + offset: cmd.offset, + }) + .await?; + println!("{:#?}", r); + println!("response count: {}", r.item_response.len()) + } + CaItemCommand::Retire(cmd) => { + let r = client.retire_item(&cmd.sku).await?; + println!("{:#?}", r) + } } + Ok(()) } +} - println!("totalItems = {}", items.len()); - - println!("{}", serde_json::to_string_pretty(&items).unwrap()); +impl UsItemCommand { + pub async fn run(self, client: walmart_partner_api::us::Client) -> Result<()> { + match self { + UsItemCommand::Get(cmd) => { + let r = client.get_item(&cmd.sku).await?; + println!("{:#?}", r) + } + UsItemCommand::List(cmd) => { + let r = client + .get_all_items(walmart_partner_api::us::GetAllItemsQuery { + next_cursor: cmd.next_cursor.map(Into::into).unwrap_or_default(), + sku: cmd.sku, + limit: cmd.limit, + offset: cmd.offset, + ..Default::default() + }) + .await?; + println!("{:#?}", r); + println!("response count: {}", r.item_response.len()) + } + UsItemCommand::Retire(cmd) => { + let r = client.retire_item(&cmd.sku).await?; + println!("{:#?}", r) + } + } + Ok(()) + } } diff --git a/cli/src/main.rs b/cli/src/main.rs index a9a1ed6..10a8408 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,12 +1,16 @@ extern crate chrono; extern crate dotenv; -extern crate walmart_partner_api; -#[macro_use] -extern crate clap; extern crate serde_json; +extern crate walmart_partner_api; use std::env; -use walmart_partner_api::{Client, WalmartCredential, WalmartMarketplace}; + +use anyhow::Result; +use clap::{command, Parser, Subcommand}; +use tracing::Level; +use tracing_subscriber::FmtSubscriber; + +use walmart_partner_api::WalmartCredential; mod feed; mod inventory; @@ -14,193 +18,111 @@ mod item; mod order; mod report; -fn main() { - let matches = clap_app!(cli => - (version: "0.1") - (about: "Walmart CLI") - (@arg ENV: -e --env +takes_value "Sets a custom env file") - (@subcommand feed => - (about: "Feed API") - (@subcommand upload => - (about: "upload feed") - (@arg feed_type: -t --type +required +takes_value "Sets the feed type") - (@arg INPUT: +required "Sets the feed file to upload") - ) - (@subcommand status => - (about: "list all feed statues") - ) - (@subcommand inspect => - (about: "inspect uploaded feed") - (@arg FEED_ID: +required "Sets the feed id to inspect") - ) - ) - (@subcommand order => - (about: "Order API") - (@subcommand list => - (about: "Get orders created in last 24 hours") - (@arg STATUS: "Sets the order status (default: Created)") - ) - (@subcommand list_released => - ) - (@subcommand list_status => - (about: "Get orders with status") - (@arg STATUS: "Sets the order status") - ) - (@subcommand dump => - (about: "dump top 200 orders in last 365 days") - ) - (@subcommand get => - (about: "get order") - (@arg ORDER_ID: +required "Sets the order id") - ) - (@subcommand ship => - (about: "ship order") - (@arg ORDER_ID: +required "Sets the order id") - (@arg line_number: -n --line_number +takes_value "Sets the line number, default 1") - (@arg method: -m --method_code +takes_value "Sets the method code, default 'Standard'") - (@arg other_carrier: -o --other_carrier +takes_value "Sets the otherCarrier") - (@arg unit_of_measurement: -u --unit_of_measurement +takes_value "Sets the unitOfMeasurement") - (@arg amount: -a --amount +takes_value "Sets the amount") - (@arg carrier_name: -c --carrier_name +takes_value "Sets the carrier name") - (@arg tracking_number: -t --tracking_number +takes_value "Sets the tracking number") - (@arg tracking_url: -r --tracking_url +takes_value "Sets the tracking url") - ) - (@subcommand ack => - (about: "ack order") - (@arg PO_ID: +required "Sets the po id") - ) - ) - (@subcommand report => - (about: "Report API") - (@subcommand get => - (about: "get report") - (@arg report_type: -t --type +required +takes_value "Sets the report type") - ) - (@subcommand get_raw => - (about: "get report to file") - (@arg report_type: -t --type +required +takes_value "Sets the report type") - (@arg out: -o --out +required +takes_value "Sets the output path") - ) - ) - (@subcommand item => - (about: "Item API") - (@subcommand dump => - (about: "dump items") - ) - ) - (@subcommand inventory => - (about: "Inventory API") - (@subcommand set => - (about: "set sku inventory") - (@arg sku: -s --sku +required +takes_value "SKU") - (@arg quantity: -q --quantity +required +takes_value "Quantity") - (@arg lagtime: -l --lagtime +required +takes_value "Fulfillment Lag Time") - ) - ) - ) - .get_matches(); +#[derive(Parser)] +#[command(author, version = "0.1", about = "Walmart CLI", long_about = None)] +#[command(propagate_version = true)] +struct Cli { + #[command(subcommand)] + command: Commands, + + #[arg(short, long)] + env: Option, +} - match matches.value_of("ENV") { - Some(path) => { - dotenv::from_path(::std::path::Path::new(path)).unwrap(); +#[derive(Parser)] +enum Commands { + #[command(subcommand)] + Ca(CaCommands), + #[command(subcommand)] + Us(UsCommands), +} + +#[derive(Subcommand)] +enum CaCommands { + #[command(subcommand)] + Feed(feed::CaFeedCommand), + #[command(subcommand)] + Item(item::CaItemCommand), + #[command(subcommand)] + Report(report::CaReportCommand), + #[command(subcommand)] + Order(order::CaOrderCommand), + #[command(subcommand)] + Inventory(inventory::CaInventoryCommand), +} + +#[derive(Subcommand)] +enum UsCommands { + #[command(subcommand)] + Feed(feed::UsFeedCommand), + #[command(subcommand)] + Report(report::UsReportCommand), + #[command(subcommand)] + Order(order::UsOrderCommand), + #[command(subcommand)] + Inventory(inventory::UsInventoryCommand), + #[command(subcommand)] + Item(item::UsItemCommand), +} + +impl CaCommands { + pub async fn run(self, client: walmart_partner_api::ca::Client) -> Result<()> { + match self { + CaCommands::Feed(cmd) => cmd.run(client).await, + CaCommands::Item(cmd) => cmd.run(client).await, + CaCommands::Report(cmd) => cmd.run(client).await, + CaCommands::Order(cmd) => cmd.run(client).await, + CaCommands::Inventory(cmd) => cmd.run(client).await, } - None => { - dotenv::dotenv().unwrap(); + } +} + +impl UsCommands { + pub async fn run(self, client: walmart_partner_api::us::Client) -> Result<()> { + match self { + UsCommands::Feed(cmd) => cmd.run(client).await, + UsCommands::Report(cmd) => cmd.run(client).await, + UsCommands::Order(cmd) => cmd.run(client).await, + UsCommands::Inventory(cmd) => cmd.run(client).await, + UsCommands::Item(cmd) => cmd.run(client).await, } } +} - env_logger::init(); +#[tokio::main] +async fn main() { + let subscriber = FmtSubscriber::builder() + .with_max_level(Level::TRACE) + .finish(); + tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed"); - let client = Client::new( - match env::var("WALMART_MARKETPLACE").unwrap().as_ref() { - "USA" => WalmartMarketplace::USA, - "Canada" => WalmartMarketplace::Canada, - _ => unreachable!(), - }, - if env::var("WALMART_CLIENT_ID").ok().is_some() { - WalmartCredential::TokenApi { - client_id: env::var("WALMART_CLIENT_ID").unwrap(), - client_secret: env::var("WALMART_CLIENT_SECRET").unwrap(), - } - } else { - WalmartCredential::Signature { - channel_type: env::var("WALMART_CHANNEL_TYPE").unwrap(), - consumer_id: env::var("WALMART_CONSUMER_ID").unwrap(), - private_key: env::var("WALMART_PRIVATE_KEY").unwrap(), - } - }, - ) - .unwrap(); + let cli = Cli::parse(); + if let Some(path) = cli.env.as_deref() { + dotenv::from_path(::std::path::Path::new(path)).unwrap(); + } else { + dotenv::dotenv().unwrap(); + } - match matches.subcommand() { - ("feed", Some(matches)) => match matches.subcommand() { - ("upload", Some(matches)) => { - feed::upload( - &client, - matches.value_of("feed_type").unwrap(), - matches.value_of("INPUT").unwrap(), - ); - } - ("status", _) => { - feed::status(&client); - } - ("inspect", Some(matches)) => { - feed::inspect(&client, matches.value_of("FEED_ID").unwrap()); - } - _ => {} - }, - ("order", Some(matches)) => match matches.subcommand() { - ("list", m) => { - order::list(&client, m.and_then(|m| m.value_of("STATUS"))); - } - ("list_released", _) => { - order::list_released(&client); - } - ("list_status", Some(m)) => { - order::list_status(&client, m.value_of("STATUS").unwrap()); - } - ("get", Some(m)) => { - order::get(&client, m.value_of("ORDER_ID").unwrap()); - } - ("dump", _) => { - order::dump(&client); - } - ("ship", Some(m)) => { - order::ship(&client, m); - } - ("ack", Some(m)) => { - order::ack(&client, m.value_of("PO_ID").unwrap()); - } - _ => {} - }, - ("report", Some(matches)) => match matches.subcommand() { - ("get", Some(matches)) => { - report::get(&client, matches.value_of("report_type").unwrap()); - } - ("get_raw", Some(matches)) => { - report::get_raw( - &client, - matches.value_of("report_type").unwrap(), - matches.value_of("out").unwrap(), - ); - } - _ => {} - }, - ("item", Some(matches)) => match matches.subcommand() { - ("dump", _) => { - item::dump(&client); - } - _ => {} - }, - ("inventory", Some(matches)) => match matches.subcommand() { - ("set", Some(m)) => { - let sku = m.value_of("sku").unwrap(); - let quantity = m.value_of("quantity").unwrap().parse().unwrap(); - let lagtime = m.value_of("lagtime").unwrap().parse().unwrap(); - inventory::set_inventory(&client, &sku, quantity, lagtime); - } - _ => {} - }, - _ => {} + let credentials = if env::var("WALMART_CLIENT_ID").ok().is_some() { + WalmartCredential::TokenApi { + client_id: env::var("WALMART_CLIENT_ID").unwrap(), + client_secret: env::var("WALMART_CLIENT_SECRET").unwrap(), + } + } else { + WalmartCredential::Signature { + channel_type: env::var("WALMART_CHANNEL_TYPE").unwrap(), + consumer_id: env::var("WALMART_CONSUMER_ID").unwrap(), + private_key: env::var("WALMART_PRIVATE_KEY").unwrap(), + } + }; + match cli.command { + Commands::Ca(cmd) => { + let client = walmart_partner_api::ca::Client::new(credentials).unwrap(); + cmd.run(client).await.unwrap(); + } + Commands::Us(cmd) => { + let client = walmart_partner_api::us::Client::new(credentials).unwrap(); + cmd.run(client).await.unwrap(); + } } } diff --git a/cli/src/order.rs b/cli/src/order.rs index 797895b..bd4f193 100644 --- a/cli/src/order.rs +++ b/cli/src/order.rs @@ -1,96 +1,283 @@ -use chrono::{Duration, Utc}; -use clap::ArgMatches; -use serde_json; -use walmart_partner_api::order::*; -use walmart_partner_api::Client; +use anyhow::Result; +use chrono::{DateTime, Utc}; +use clap::{Parser, Subcommand}; -pub fn list(client: &Client, status: Option<&str>) { - let mut query: QueryParams = Default::default(); - let start_date = (Utc::now() - Duration::days(7)).date().and_hms(0, 0, 0); - query.createdStartDate = Some(start_date); - query.createdEndDate = Some(Utc::now()); - query.status = status.or(Some("Created")).map(|s| s.to_owned()); - let res = client.get_all_orders(&query).unwrap(); - println!("{:#?}", res); +#[derive(Subcommand)] +pub enum CaOrderCommand { + List(List), + ListByCursor(ListByCursor), + ListReleased(ListReleased), + Get(Get), + Ack(Get), + Ship(CaShip), } -pub fn list_released(client: &Client) { - let query: ReleasedQueryParams = Default::default(); - let res = client.get_all_released_orders(&query).unwrap(); - println!("{:#?}", res); +#[derive(Subcommand)] +pub enum UsOrderCommand { + List(List), + ListByCursor(ListByCursor), + ListReleased(ListReleased), + Get(Get), + Ack(Get), + Ship(UsShip), } -pub fn list_status(client: &Client, status: &str) { - let mut query: QueryParams = Default::default(); - let start_date = (Utc::now() - Duration::days(30)).date().and_hms(0, 0, 0); - query.createdStartDate = Some(start_date); - query.status = Some(status.to_string()); - let res = client.get_all_orders(&query).unwrap(); - println!("{:#?}", res); +#[derive(Parser)] +pub struct Get { + #[clap(long)] + pub purchase_order_id: String, } -pub fn dump(client: &Client) { - let mut query: QueryParams = Default::default(); - query.limit = Some(200); - let start_date = (Utc::now() - Duration::days(100)).date().and_hms(0, 0, 0); - query.createdStartDate = Some(start_date); +#[derive(Parser)] +pub struct List { + #[clap(long)] + pub sku: Option, + #[clap(long)] + pub customer_order_id: Option, + #[clap(long)] + pub purchase_order_id: Option, + #[clap(long)] + pub status: Option, + #[clap(long)] + pub created_start_date: Option>, + #[clap(long)] + pub created_end_date: Option>, + #[clap(long)] + pub from_expected_ship_date: Option>, + #[clap(long)] + pub to_expected_ship_date: Option>, + #[clap(long)] + pub limit: Option, + #[clap(long)] + pub product_info: Option, + #[clap(long)] + pub ship_node_type: Option, +} - let res = client.get_all_orders(&query).unwrap(); +#[derive(Parser)] +pub struct ListByCursor { + #[clap(long)] + pub cursor: String, +} - let mut next_cursor = res.get_next_cursor().map(str::to_string); - let mut elements = res.elements; - while let Some(cursor) = next_cursor { - let mut res = client.get_all_orders_by_next_cursor(&cursor).unwrap(); - next_cursor = res.get_next_cursor().map(str::to_string); - // println!( - // "n = {:?}, next_cursur = {:?}", - // res.get_total_count(), - // next_cursor - // ); - elements.append(&mut res.elements); - ::std::thread::sleep(::std::time::Duration::from_secs(1)); - } +#[derive(Parser)] +pub struct ListReleased { + #[clap(long)] + pub limit: Option, + #[clap(long)] + pub created_start_date: Option>, + #[clap(long)] + pub created_end_date: Option>, + #[clap(long)] + pub product_info: Option, +} - println!("{}", serde_json::to_string_pretty(&elements).unwrap()); +#[derive(Parser)] +pub struct CaShip { + #[clap(long)] + pub purchase_order_id: String, + #[clap(long)] + pub line_number: String, + #[clap(long)] + pub ship_from_country: String, + #[clap(long)] + pub ship_date_time: DateTime, + #[clap(long)] + pub other_carrier: Option, + #[clap(long)] + pub carrier: Option, + #[clap(long)] + pub method_code: String, + #[clap(long)] + pub tracking_number: String, + #[clap(long)] + pub tracking_url: Option, } -pub fn get(client: &Client, id: &str) { - let res = client.get_order(id).unwrap(); - println!("{}", serde_json::to_string_pretty(&res).unwrap()); +#[derive(Parser)] +pub struct UsShip { + #[clap(long)] + pub purchase_order_id: String, + #[clap(long)] + pub line_number: String, + #[clap(long)] + pub seller_order_id: String, + #[clap(long)] + pub ship_date_time: DateTime, + #[clap(long)] + pub other_carrier: Option, + #[clap(long)] + pub carrier: Option, + #[clap(long)] + pub method_code: String, + #[clap(long)] + pub tracking_number: String, + #[clap(long)] + pub tracking_url: Option, } -pub fn ship(client: &Client, m: &ArgMatches) { - let params = ShipParams { - lineNumber: m - .value_of("line_number") - .map(ToString::to_string) - .unwrap_or_else(|| "1".to_string()), - shipDateTime: Utc::now(), - carrierName: m.value_of("carrier_name").map(ToString::to_string), - methodCode: m - .value_of("method") - .map(ToString::to_string) - .unwrap_or_else(|| "Standard".to_string()), - trackingNumber: m - .value_of("tracking_number") - .map(ToString::to_string) - .unwrap(), - trackingURL: m - .value_of("tracking_url") - .map(ToString::to_string) - .unwrap_or_default(), - otherCarrier: m.value_of("other_carrier").map(ToString::to_string), - unitOfMeasurement: m.value_of("unit_of_measurement").map(ToString::to_string), - amount: m.value_of("amount").map(ToString::to_string), - shipFromCountry: m.value_of("shipFromCountry").map(ToString::to_string).unwrap(), - }; - let res = client - .ship_order_line(m.value_of("ORDER_ID").unwrap(), ¶ms) - .unwrap(); - println!("{}", serde_json::to_string_pretty(&res).unwrap()); +impl CaOrderCommand { + pub async fn run(self, client: walmart_partner_api::ca::Client) -> Result<()> { + match self { + CaOrderCommand::List(cmd) => { + let r = client + .get_all_orders(walmart_partner_api::ca::GetAllOrdersQuery { + sku: cmd.sku, + customer_order_id: cmd.customer_order_id, + purchase_order_id: cmd.purchase_order_id, + status: cmd.status, + created_start_date: cmd.created_start_date.unwrap_or_default(), + created_end_date: cmd.created_end_date, + from_expected_ship_date: cmd.from_expected_ship_date, + to_expected_ship_date: cmd.to_expected_ship_date, + limit: cmd.limit, + product_info: cmd.product_info, + ship_node_type: None, + }) + .await?; + println!("{:#?}", r) + } + CaOrderCommand::ListByCursor(cmd) => { + let r = client.get_all_orders_by_next_cursor(cmd.cursor).await?; + println!("{:#?}", r) + } + CaOrderCommand::ListReleased(cmd) => { + let r = client + .get_all_released_orders(walmart_partner_api::ca::GetAllReleasedOrdersQuery { + limit: cmd.limit, + created_start_date: cmd.created_start_date.unwrap_or_default(), + created_end_date: cmd.created_end_date, + product_info: cmd.product_info, + }) + .await?; + println!("{:#?}", r) + } + CaOrderCommand::Get(cmd) => { + let r = client + .get_order(&cmd.purchase_order_id, Default::default()) + .await?; + println!("{:#?}", r) + } + CaOrderCommand::Ack(cmd) => { + let r = client.ack_order(&cmd.purchase_order_id).await?; + println!("{:#?}", r) + } + CaOrderCommand::Ship(cmd) => { + let r = client + .ship_order_lines( + cmd.purchase_order_id, + walmart_partner_api::ca::ShipOrderLines { + order_lines: vec![walmart_partner_api::ca::ShipOrderLine { + line_number: cmd.line_number, + ship_from_country: cmd.ship_from_country, + status_quantity: None, + asn: None, + tracking_info: walmart_partner_api::ca::OrderLineTrackingInfo { + ship_date_time: Default::default(), + carrier_name: walmart_partner_api::ca::OrderLineTrackingCarrier { + other_carrier: cmd.other_carrier, + carrier: cmd.carrier, + }, + method_code: cmd.method_code, + tracking_number: cmd.tracking_number, + tracking_url: cmd.tracking_url, + }, + }], + }, + ) + .await?; + println!("{:#?}", r) + } + } + Ok(()) + } } -pub fn ack(client: &Client, po_id: &str) { - let res = client.ack_order(po_id).unwrap(); - println!("{}", serde_json::to_string_pretty(&res).unwrap()); +impl UsOrderCommand { + pub async fn run(self, client: walmart_partner_api::us::Client) -> Result<()> { + match self { + UsOrderCommand::List(cmd) => { + let r = client + .get_all_orders(walmart_partner_api::us::GetAllOrdersQuery { + sku: cmd.sku, + customer_order_id: cmd.customer_order_id, + purchase_order_id: cmd.purchase_order_id, + status: cmd.status, + created_start_date: cmd.created_start_date, + created_end_date: cmd.created_end_date, + from_expected_ship_date: cmd.from_expected_ship_date, + to_expected_ship_date: cmd.to_expected_ship_date, + limit: cmd.limit, + product_info: cmd.product_info, + ship_node_type: cmd.ship_node_type, + replacement_into: None, + order_type: None, + }) + .await?; + println!("{:#?}", r) + } + UsOrderCommand::ListByCursor(cmd) => { + let r = client.get_all_orders_by_next_cursor(cmd.cursor).await?; + println!("{:#?}", r) + } + UsOrderCommand::ListReleased(cmd) => { + let r = client + .get_all_released_orders(walmart_partner_api::us::GetAllReleasedOrdersQuery { + limit: cmd.limit, + created_start_date: cmd.created_start_date, + created_end_date: cmd.created_end_date, + product_info: cmd.product_info, + sku: None, + customer_order_id: None, + purchase_order_id: None, + from_expected_ship_date: None, + to_expected_ship_date: None, + replacement_into: None, + order_type: None, + ship_node_type: None, + }) + .await?; + println!("{:#?}", r) + } + UsOrderCommand::Get(cmd) => { + let r = client + .get_order(&cmd.purchase_order_id, Default::default()) + .await?; + println!("{:#?}", r) + } + UsOrderCommand::Ack(cmd) => { + let r = client.ack_order(&cmd.purchase_order_id).await?; + println!("{:#?}", r) + } + UsOrderCommand::Ship(cmd) => { + let r = client + .ship_order_lines( + cmd.purchase_order_id, + walmart_partner_api::us::ShipOrderLines { + order_lines: vec![walmart_partner_api::us::ShipOrderLine { + line_number: cmd.line_number, + seller_order_id: cmd.seller_order_id, + intent_to_cancel_override: None, + status_quantity: None, + asn: None, + tracking_info: walmart_partner_api::us::OrderLineTrackingInfo { + ship_date_time: cmd.ship_date_time, + carrier_name: walmart_partner_api::us::OrderLineTrackingCarrier { + other_carrier: cmd.other_carrier, + carrier: cmd.carrier, + }, + method_code: cmd.method_code, + tracking_number: cmd.tracking_number, + tracking_url: cmd.tracking_url, + }, + }], + process_mode: None, + }, + ) + .await?; + println!("{:#?}", r) + } + } + Ok(()) + } } diff --git a/cli/src/report.rs b/cli/src/report.rs index 0b01a41..37c3211 100644 --- a/cli/src/report.rs +++ b/cli/src/report.rs @@ -1,16 +1,134 @@ -use walmart_partner_api::report::ItemReportType; -use walmart_partner_api::Client; +use anyhow::Result; +use chrono::{DateTime, Utc}; +use clap::{Parser, Subcommand}; -pub fn get(client: &Client, report_type: &str) { - let report = match report_type { - "item" => client.get_report::(), - _ => unimplemented!(), - }; - println!("{:#?}", report); +#[derive(Subcommand)] +pub enum CaReportCommand { + Get(CaGet), + GetRaw(CaGetRaw), } -pub fn get_raw(client: &Client, report_type: &str, path: &str) { - use std::fs::File; - let out = File::create(path).unwrap(); - client.get_report_raw(report_type, out).unwrap(); +#[derive(Parser)] +pub struct CaGet { + #[clap(long)] + pub report_type: String, +} + +#[derive(Parser)] +pub struct CaGetRaw { + #[clap(long)] + pub report_type: String, + #[clap(long)] + pub path: String, +} + +#[derive(Subcommand)] +pub enum UsReportCommand { + List(UsList), + Get(UsGet), + Create(UsCreate), + GetDownload(UsGet), +} + +#[derive(Parser)] +pub struct UsList { + #[clap(long)] + pub report_type: String, + #[clap(long)] + pub request_status: Option, + #[clap(long)] + pub report_version: Option, + #[clap(long)] + pub request_submission_start_date: Option>, + #[clap(long)] + pub request_submission_end_date: Option>, +} + +#[derive(Parser)] +pub struct UsGet { + #[clap(long)] + pub report_request_id: String, +} + +#[derive(Parser)] +pub struct UsCreate { + #[clap(long)] + pub report_type: String, + #[clap(long)] + pub report_version: String, +} + +impl CaReportCommand { + pub async fn run(self, client: walmart_partner_api::ca::Client) -> Result<()> { + match self { + CaReportCommand::Get(cmd) => { + let r = match &*cmd.report_type { + "item" => { + client + .get_report::() + .await? + } + _ => unimplemented!(), + }; + println!("{:#?}", r) + } + CaReportCommand::GetRaw(cmd) => { + use std::fs::File; + let out = File::create(cmd.path).unwrap(); + client + .get_report_raw( + walmart_partner_api::ca::GetReportQuery { + type_: &cmd.report_type, + }, + out, + ) + .await?; + } + } + Ok(()) + } +} + +impl UsReportCommand { + pub async fn run(self, client: walmart_partner_api::us::Client) -> Result<()> { + match self { + UsReportCommand::List(cmd) => { + let r = client + .get_all_report_requests(walmart_partner_api::us::GetAllReportRequestsQuery { + report_type: cmd.report_type, + request_status: cmd + .request_status + .map(|s| serde_json::from_str(&s).unwrap()), + report_version: cmd.report_version, + request_submission_start_date: cmd.request_submission_start_date, + request_submission_end_date: cmd.request_submission_end_date, + }) + .await?; + println!("{:#?}", r) + } + UsReportCommand::Get(cmd) => { + let r = client.get_report_request(cmd.report_request_id).await?; + println!("{:#?}", r) + } + UsReportCommand::Create(cmd) => { + let r = client + .create_report_request( + walmart_partner_api::us::CreateReportRequestQuery { + report_type: cmd.report_type, + report_version: cmd.report_version, + }, + walmart_partner_api::us::CreateReportRequestInput { + ..Default::default() + }, + ) + .await?; + println!("{:#?}", r) + } + UsReportCommand::GetDownload(cmd) => { + let r = client.get_report_download(cmd.report_request_id).await?; + println!("{:#?}", r) + } + } + Ok(()) + } } diff --git a/openapi/Cargo.toml b/openapi/Cargo.toml new file mode 100644 index 0000000..a3f0184 --- /dev/null +++ b/openapi/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "openapi" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +clap = {version = "4.0.26", features = ["derive"]} +reqwest = {version = "0.11", features = ["blocking", "json"]} +serde = {version = "1.0", features = ["derive"]} +serde_json = "1.0" +anyhow = "1.0" \ No newline at end of file diff --git a/openapi/src/main.rs b/openapi/src/main.rs new file mode 100644 index 0000000..2ccff64 --- /dev/null +++ b/openapi/src/main.rs @@ -0,0 +1,187 @@ +use std::fs; +use std::io; +use std::path::PathBuf; + +use anyhow::{Context, Result}; +use clap::{Parser, Subcommand, ValueEnum}; +use serde::Serialize; + +#[derive(Parser)] +#[command(author, version, about, long_about = None)] +#[command(propagate_version = true)] +struct Cli { + #[command(subcommand)] + command: Commands, +} + +#[derive(Subcommand)] +enum Commands { + Download { country: Country, api_name: ApiName }, + Generate { country: Country, api_name: ApiName }, +} + +#[derive(Debug, Serialize, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] +#[serde(rename_all = "lowercase")] +enum Country { + Us, + Ca, +} + +impl ToString for Country { + fn to_string(&self) -> String { + serde_json::to_value(self) + .unwrap() + .as_str() + .unwrap() + .to_string() + } +} + +#[derive(Debug, Serialize, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] +#[serde(rename_all = "lowercase")] +enum ApiName { + Auth, + Feeds, + Items, + Price, + Promotion, + Orders, + Returns, + Inventory, + Settings, + Rules, + Reports, + Fulfillment, + Notifications, + Utilities, + Insights, + OnRequestReports, +} + +impl ToString for ApiName { + fn to_string(&self) -> String { + serde_json::to_value(self) + .unwrap() + .as_str() + .unwrap() + .to_string() + } +} + +fn download_schema_file(country: Country, api_name: ApiName) -> Result { + #[derive(Debug, Serialize)] + struct Body { + params: Params, + } + #[derive(Debug, Serialize)] + #[serde(rename_all = "camelCase")] + struct Params { + country: Country, + api_name: ApiName, + category: &'static str, + } + + let body = Body { + params: Params { + country, + api_name, + category: "mp", + }, + }; + + let client = reqwest::blocking::Client::new(); + let mut response = client + .post("https://developer.walmart.com/api/detail") + .json(&body) + .send() + .unwrap() + .error_for_status() + .unwrap(); + + let resolver = PathResolver::new(country, api_name); + resolver.create_schema_dir()?; + let (path, mut file) = resolver.create_schema_file()?; + + io::copy(&mut response, &mut file).context("Failed to copy reqwest body to schema file")?; + Ok(path) +} + +#[derive(Clone)] +struct PathResolver { + country: Country, + api_name: ApiName, +} + +impl PathResolver { + fn new(country: Country, api_name: ApiName) -> Self { + Self { country, api_name } + } + + fn get_current_dir() -> PathBuf { + let path = std::env::current_dir().unwrap(); + if !path.ends_with("walmart-partner-api") { + panic!( + "Make sure you run this command in the root directory of the project (walmart-partner-api)" + ); + } + path + } + + fn create_schema_file(&self) -> Result<(PathBuf, fs::File)> { + let path = self.get_schema_file_path(); + let file = fs::File::create(path.clone()) + .context(format!("Failed to create schema file for {:?}", path))?; + Ok((path, file)) + } + + fn create_schema_dir(&self) -> Result { + let path = self.get_country_schema_dir(); + fs::create_dir_all(&path).context(format!("Failed to create schema directory: {:?}", path))?; + Ok(path) + } + + fn get_schema_file_path(&self) -> PathBuf { + let path = self + .get_country_schema_dir() + .join(format!("{}.json", self.api_name.to_string())); + path + } + + fn get_country_schema_dir(&self) -> PathBuf { + let path = Self::get_current_dir() + .join("schemas") + .join(self.country.to_string()); + path + } +} + +fn generate(_country: Country, _api_name: ApiName) -> Result<()> { + unimplemented!(); + + // Command::new("openapi-generator") + // .arg("generate") + // .arg("-i") + // .arg(path.to_str().unwrap_or_default()) + // .arg("--skip-validate-spec") + // .arg("--global-property") + // .arg("models,modelDocs=false") + // .arg("-g") + // .arg("rust") + // .arg("-o") + // .arg("tmp") + // .status() + // .context("Failed to execute openapi-generator command")?; +} + +fn main() { + let cli = Cli::parse(); + + match &cli.command { + Commands::Download { country, api_name } => { + download_schema_file(*country, *api_name).unwrap(); + } + Commands::Generate { country, api_name } => { + generate(*country, *api_name).unwrap(); + } + } +} diff --git a/walmart-partner-api/Cargo.toml b/walmart-partner-api/Cargo.toml index e2c339b..5f86e57 100644 --- a/walmart-partner-api/Cargo.toml +++ b/walmart-partner-api/Cargo.toml @@ -1,29 +1,34 @@ [package] name = "walmart_partner_api" -version = "0.6.0" +version = "0.7.0" authors = ["Flux Xu "] description = "Rust client for Walmart Partner APIs" repository = "https://github.com/Ventmere/walmart-partner-api" license = "MIT" exclude = ["target"] -edition = "2018" +edition = "2021" [dependencies] -url = "1.6.0" -reqwest = "0.9.0" -base64 = "0.6.0" +url = "2.3" +reqwest = { version = "0.11", features = ["json", "stream", "multipart"] } +base64 = "0.13" chrono = { version = "0.4", features = ["serde"] } -openssl = "0.10.0" -serde = "1.0.11" -serde_json = "1.0.2" -serde_derive = "1.0.11" -rand = "0.3.16" -serde_urlencoded = "0.5.1" -zip = "0.3.1" -csv = "1.0.0-beta.5" -tempfile = "2" -bigdecimal = ">=0.0.10,<0.2.0" -xmltree = "0.8.0" -failure = "0.1.2" -failure_derive = "0.1.2" -log = "0.4" \ No newline at end of file +openssl = "0.10" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +serde_derive = "1.0" +rand = "0.8" +serde_urlencoded = "0.7" +bigdecimal = "0.3" +thiserror = "1.0" +tracing = "0.1" +uuid = { version = "1.2", features = ["v4"] } +serde-xml-rs = "0.6" +xml-builder = "0.5" +zip = "0.6" +csv = "1.1" +tempfile = "3.3" + +[dev-dependencies] +mockito = "0.31" +tokio = { version = "1.13", features = ["rt", "macros"] } \ No newline at end of file diff --git a/walmart-partner-api/src/api/ca/client.rs b/walmart-partner-api/src/api/ca/client.rs new file mode 100644 index 0000000..c9fd9e2 --- /dev/null +++ b/walmart-partner-api/src/api/ca/client.rs @@ -0,0 +1,27 @@ +use crate::result::WalmartResult; +use crate::WalmartCredential; + +pub struct Client { + inner: crate::client::Client, +} + +impl Client { + pub fn new(credential: WalmartCredential) -> WalmartResult { + let inner = crate::client::Client::new(crate::WalmartMarketplace::Canada, credential)?; + Ok(Self { inner }) + } +} + +impl std::ops::Deref for Client { + type Target = crate::client::Client; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl std::ops::DerefMut for Client { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } +} diff --git a/walmart-partner-api/src/api/ca/feed/mod.rs b/walmart-partner-api/src/api/ca/feed/mod.rs new file mode 100644 index 0000000..c4c45c3 --- /dev/null +++ b/walmart-partner-api/src/api/ca/feed/mod.rs @@ -0,0 +1,197 @@ +use std::io::Read; + +pub use types::*; + +use crate::api::ca::Client; +use crate::client::Method; +use crate::result::WalmartResult; + +mod types; + +impl Client { + pub async fn get_all_feed_statuses( + &self, + query: GetAllFeedStatusesQuery, + ) -> WalmartResult { + let qs = serde_urlencoded::to_string(&query)?; + let req = self.req_xml(Method::GET, "/v3/ca/feeds", qs)?; + self.send(req).await?.res_xml().await + } + + pub async fn get_feed_and_item_status( + &self, + feed_id: impl AsRef, + query: GetFeedAndItemStatusQuery, + ) -> WalmartResult { + let qs = serde_urlencoded::to_string(&query)?; + let path = format!("/v3/ca/feeds/{}", feed_id.as_ref()); + let req = self.req_xml(Method::GET, &path, qs)?; + self.send(req).await?.res_xml().await + } + + pub async fn bulk_upload_xml( + &self, + feed_type: impl AsRef, + mut feed: R, + ) -> WalmartResult { + let mut buffer = Vec::new(); + feed.read_to_end(&mut buffer)?; + let part = reqwest::multipart::Part::bytes(buffer); + let form = reqwest::multipart::Form::new().part("file", part); + let req = self + .req_xml(Method::POST, "/v3/ca/feeds", vec![("feedType", feed_type)])? + .form(form); + self.send(req).await?.res_xml().await + } +} + +#[cfg(test)] +mod tests { + use mockito::Matcher; + + use super::*; + + #[tokio::test] + async fn get_all_feed_statuses() { + let client = crate::test_util::get_client_ca(); + let body = r#" + + + 1 + 0 + 50 + + + 12234EGGT564YTEGFA@AQMBAQA + MARKETPLACE_PARTNER + item + 1413254255 + 1 + 1 + 0 + 0 + PROCESSED + 2018-07-20T21:56:12.605Z + HP_REQUEST_BATCH + 2018-07-20T21:56:17.948Z + ItemFeed99_ParadiseCounty_paperback.xml + 0 + 0 + 0 + WM_TEST + + +"#; + + let feed_id = "12234EGGT564YTEGFA"; + let limit = 10; + let _m = mockito::mock("GET", "/v3/ca/feeds") + .match_query(Matcher::AllOf(vec![ + Matcher::UrlEncoded("feedId".into(), feed_id.into()), + Matcher::UrlEncoded("limit".into(), limit.to_string()), + ])) + .with_status(200) + .with_header("content-type", "application/xml") + .with_body(body) + .create(); + + let q = GetAllFeedStatusesQuery { + feed_id: Some(feed_id.to_string()), + limit: Some(limit), + offset: None, + }; + let got = client.get_all_feed_statuses(q).await.unwrap(); + assert_eq!(got.total_results.unwrap(), 1); + assert_eq!(got.offset.unwrap(), 0); + assert_eq!(got.limit.unwrap(), 50); + assert_eq!( + got.results.feed[0].feed_id.as_ref().unwrap(), + &"12234EGGT564YTEGFA@AQMBAQA".to_string() + ); + } + + #[tokio::test] + async fn get_feed_and_item_status() { + let client = crate::test_util::get_client_ca(); + let body = r#" + + + 640787F441ASSFF1C4FB7BB749E20C0A3 + PROCESSED + 2018-07-20T21:56:12.605Z + 1 + 1 + 0 + 0 + 0 + 50 + + + 0 + 234325346-8fbf-4fa0-a70c-2424rfwefq + 7K69FC732QRRE5KTFS + 0 + 24234 + + + GTIN + 086756453 + + + ISBN + 13432543634 + + + SUCCESS + + + + ""#; + + let feed_id = "640787F441ASSFF1C4FB7BB749E20C0A3"; + let _m = mockito::mock("GET", format!("/v3/ca/feeds/{}", feed_id).as_str()) + .with_status(200) + .with_header("content-type", "application/xml") + .with_body(body) + .create(); + + let got = client + .get_feed_and_item_status(feed_id, Default::default()) + .await + .unwrap(); + assert_eq!(got.feed_id.unwrap(), feed_id); + assert_eq!(got.feed_status.unwrap(), "PROCESSED".to_string()); + assert_eq!(got.item_details.item_ingestion_status.len(), 1); + assert_eq!( + got.item_details.item_ingestion_status[0].mart_id.unwrap(), + 0 + ); + } + + #[tokio::test] + async fn test_bulk_upload_xml() { + let client = crate::test_util::get_client_ca(); + let body = r#" + + + E9C04D1FFD99479FBC1341D56DD5F930@AQMB_wA + + "#; + + let _m = mockito::mock("POST", "/v3/ca/feeds") + .match_query(Matcher::UrlEncoded("feedType".into(), "test".into())) + .with_status(200) + .with_header("content-type", "application/xml") + .with_body(body) + .create(); + + let got = client + .bulk_upload_xml("test", "somefeed".as_bytes()) + .await + .unwrap(); + assert_eq!( + got.feed_id.unwrap(), + "E9C04D1FFD99479FBC1341D56DD5F930@AQMB_wA" + ); + } +} diff --git a/walmart-partner-api/src/api/ca/feed/types.rs b/walmart-partner-api/src/api/ca/feed/types.rs new file mode 100644 index 0000000..1540ca5 --- /dev/null +++ b/walmart-partner-api/src/api/ca/feed/types.rs @@ -0,0 +1,94 @@ +pub use crate::shared::feed::*; +use crate::xml::get_element_with_text; +use crate::{WalmartResult, XmlSer}; +use xml_builder::XMLElement; + +#[derive(Debug, PartialEq)] +pub struct InventoryFeed { + pub items: Vec, +} + +impl InventoryFeed { + pub fn emit_xml(&self) -> WalmartResult { + self.to_string() + } +} + +#[derive(Debug, PartialEq)] +pub struct InventoryFeedItem { + pub sku: String, + pub quantity: i32, + pub fulfillment_lag_time: i32, +} + +impl XmlSer for InventoryFeed { + fn to_xml(&self) -> WalmartResult { + let mut root = XMLElement::new("InventoryFeed"); + root.add_attribute("xmlns", "http://walmart.com/"); + + let mut header = XMLElement::new("InventoryHeader"); + header.add_child(get_element_with_text("version", "1.4")?)?; + root.add_child(header)?; + + for item in &self.items { + let mut inventory = XMLElement::new("inventory"); + inventory.add_child(get_element_with_text("sku", &item.sku)?)?; + + let mut quantity = XMLElement::new("quantity"); + quantity.add_child(get_element_with_text("unit", "EACH")?)?; + quantity.add_child(get_element_with_text("amount", item.quantity)?)?; + + inventory.add_child(quantity)?; + inventory.add_child(get_element_with_text( + "fulfillmentLagTime", + item.fulfillment_lag_time, + )?)?; + + root.add_child(inventory)?; + } + + Ok(root) + } +} + +#[test] +fn test_emit_inventory_feed() { + let data = InventoryFeed { + items: vec![ + InventoryFeedItem { + sku: "1068155".to_string(), + quantity: 10, + fulfillment_lag_time: 2, + }, + InventoryFeedItem { + sku: "10210321".to_string(), + quantity: 20, + fulfillment_lag_time: 2, + }, + ], + }; + let xml = data.emit_xml().unwrap(); + let want = r#" + + + 1.4 + + + 1068155 + + EACH + 10 + + 2 + + + 10210321 + + EACH + 20 + + 2 + +"#; + crate::test_util::assert_xml_str_eq(&xml, want, "not equal"); +} diff --git a/walmart-partner-api/src/api/ca/inventory/mod.rs b/walmart-partner-api/src/api/ca/inventory/mod.rs new file mode 100644 index 0000000..2a6d292 --- /dev/null +++ b/walmart-partner-api/src/api/ca/inventory/mod.rs @@ -0,0 +1,96 @@ +pub use types::*; + +use crate::api::ca::Client; +use crate::client::Method; +use crate::result::WalmartResult; + +mod types; + +impl Client { + pub async fn get_item_inventory(&self, sku: impl AsRef) -> WalmartResult { + let req = self.req_xml(Method::GET, "/v3/ca/inventory", vec![("sku", sku)])?; + self.send(req).await?.res_xml().await + } + + pub async fn update_item_inventory(&self, inventory: Inventory) -> WalmartResult { + let req = self + .req_xml( + Method::PUT, + "/v3/ca/inventory", + vec![("sku", &inventory.sku)], + )? + .body_xml(inventory)?; + self.send(req).await?.res_xml().await + } +} + +#[cfg(test)] +mod tests { + use mockito::mock; + + use crate::test_util::get_client_ca; + + use super::*; + + #[tokio::test] + async fn test_get_item_inventory() { + let client = get_client_ca(); + let _m = mock("GET", "/v3/ca/inventory?sku=1068155") + .with_status(200) + .with_header("content-type", "application/xml") + .with_body( + r##" + + + 1068155 + + EACH + 23 + + 1 + +"##, + ) + .create(); + + let inventory = client.get_item_inventory("1068155").await.unwrap(); + assert_eq!(inventory.sku, "1068155"); + assert_eq!(inventory.quantity.amount, 23); + } + + #[tokio::test] + async fn test_update_item_inventory() { + let client = get_client_ca(); + let _m = mock("PUT", "/v3/ca/inventory?sku=1068155") + .with_status(200) + .with_header("content-type", "application/xml") + .with_body( + r##" + + + 1068155 + + EACH + 23 + + 1 +"##, + ) + .create(); + + let body = Inventory { + sku: "1068155".to_string(), + quantity: InventoryQuantity { + unit: "EACH".to_string(), + amount: 23, + }, + fulfillment_lag_time: 1, + partner_id: None, + offer_id: None, + }; + + let inventory = client.update_item_inventory(body).await.unwrap(); + assert_eq!(inventory.sku, "1068155"); + assert_eq!(inventory.quantity.amount, 23); + } +} diff --git a/walmart-partner-api/src/api/ca/inventory/types.rs b/walmart-partner-api/src/api/ca/inventory/types.rs new file mode 100644 index 0000000..79edc05 --- /dev/null +++ b/walmart-partner-api/src/api/ca/inventory/types.rs @@ -0,0 +1,49 @@ +use xml_builder::XMLElement; + +pub use crate::shared::inventory::*; +use crate::WalmartResult; + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct Inventory { + /// An arbitrary alphanumeric unique ID, seller-specified, identifying each item. + /// Please note that get inventory returns nothing for sku field -_- + #[serde(rename = "sku", default)] + pub sku: String, + #[serde(rename = "quantity")] + pub quantity: InventoryQuantity, + /// The number of days between when the item is ordered and when it is shipped + #[serde(rename = "fulfillmentLagTime")] + pub fulfillment_lag_time: i32, + #[serde(rename = "partnerId", skip_serializing_if = "Option::is_none")] + pub partner_id: Option, + #[serde(rename = "offerId", skip_serializing_if = "Option::is_none")] + pub offer_id: Option, +} + +impl crate::XmlSer for Inventory { + fn to_xml(&self) -> WalmartResult { + let mut inventory = XMLElement::new("inventory"); + inventory.add_attribute("xmlns", "http://walmart.com/"); + let mut sku = XMLElement::new("sku"); + sku.add_text(self.sku.clone())?; + inventory.add_child(sku)?; + + inventory.add_child(self.quantity.to_xml()?)?; + + let mut fulfillment_lag_time = XMLElement::new("fulfillmentLagTime"); + fulfillment_lag_time.add_text(self.fulfillment_lag_time.to_string())?; + inventory.add_child(fulfillment_lag_time)?; + + if let Some(partner_id_v) = &self.partner_id { + let mut partner_id = XMLElement::new("partnerId"); + partner_id.add_text(partner_id_v.clone())?; + inventory.add_child(partner_id)?; + } + if let Some(offer_id_v) = &self.offer_id { + let mut offer_id = XMLElement::new("offerId"); + offer_id.add_text(offer_id_v.clone())?; + inventory.add_child(offer_id)?; + } + Ok(inventory) + } +} diff --git a/walmart-partner-api/src/api/ca/item/mod.rs b/walmart-partner-api/src/api/ca/item/mod.rs new file mode 100644 index 0000000..a863f83 --- /dev/null +++ b/walmart-partner-api/src/api/ca/item/mod.rs @@ -0,0 +1,215 @@ +use serde::Serialize; + +pub use types::*; + +use crate::api::ca::Client; +use crate::client::Method; +use crate::result::WalmartResult; + +mod types; + +#[derive(Debug, Clone, Default, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GetAllItemsQuery { + pub next_cursor: GetAllItemsCursor, + pub sku: Option, + pub limit: Option, + pub offset: Option, +} + +#[derive(Debug, Clone, Serialize)] +pub struct GetAllItemsCursor(String); + +impl Default for GetAllItemsCursor { + fn default() -> Self { + GetAllItemsCursor("*".to_string()) + } +} + +impl From for GetAllItemsCursor { + fn from(s: T) -> Self { + GetAllItemsCursor(s.to_string()) + } +} + +impl Into for GetAllItemsCursor { + fn into(self) -> String { + self.0 + } +} + +impl Client { + pub async fn get_all_items(&self, query: GetAllItemsQuery) -> WalmartResult { + let qs = serde_urlencoded::to_string(&query)?; + let req = self.req_xml(Method::GET, "/v3/ca/items", qs)?; + self.send(req).await?.res_xml().await + } + + pub async fn get_item(&self, sku: impl AsRef) -> WalmartResult { + let req = self.req_xml(Method::GET, &format!("/v3/ca/items/{}", sku.as_ref()), "")?; + self.send(req).await?.res_xml().await + } + + pub async fn retire_item(&self, sku: impl AsRef) -> WalmartResult { + let req = self.req_xml( + Method::DELETE, + &format!("/v3/ca/items/{}", sku.as_ref()), + "", + )?; + self.send(req).await?.res_xml().await + } +} + +#[cfg(test)] +mod tests { + use mockito::{mock, Matcher}; + + use super::*; + + #[tokio::test] + async fn test_get_all_items() { + let client = crate::test_util::get_client_ca(); + + let q = GetAllItemsQuery { + ..Default::default() + }; + + let body = r#" + + + + WALMART_CA + 379third908 + 00313159099947 + Carex Soft Grip Folding Cane - Black Walking Cane + Walking Canes + + CAD + 13.27 + + IN_PROGRESS + + + WALMART_US + prodtest1571 + 889296686590 + 00889296686590 + REFURBISHED: HP 250 G3 15.6" Notebook, Intel 4th Gen i3, 4GB RAM, 500GB HDD, Win 8.1, M5G69UT#ABA + RAM Memory + + CAD + 329.99 + + PUBLISHED + +"#; + + let _m = mock("GET", "/v3/ca/items") + .match_query(Matcher::AllOf(vec![Matcher::UrlEncoded( + "nextCursor".into(), + q.next_cursor.clone().into(), + )])) + .with_status(200) + .with_header("content-type", "application/xml") + .with_body(body) + .create(); + + let items = client.get_all_items(q).await.unwrap(); + let got = items.item_response[0].clone(); + let want = Item { + mart: Some("WALMART_CA".to_string()), + sku: "379third908".to_string(), + wpid: None, + upc: None, + gtin: Some("00313159099947".to_string()), + product_name: Some("Carex Soft Grip Folding Cane - Black Walking Cane".to_string()), + shelf: None, + product_type: Some("Walking Canes".to_string()), + price: Some(Price { + currency: "CAD".to_string(), + amount: "13.27".to_string(), + }), + published_status: Some("IN_PROGRESS".to_string()), + }; + assert_eq!(got, want); + } + + #[tokio::test] + async fn test_get_item() { + let client = crate::test_util::get_client_ca(); + + let sku = "foo"; + let body = r#" + + + + WALMART_CA + 4W1AF6YU04F5 + foo + 735732770692 + 00735732770692 + Victoria Classics Vista Paisley 8-Piece Bedding Comforter Set + Bedding Sets + + CAD + 13.27 + + IN_PROGRESS + + + "#; + + let _m = mock("GET", format!("/v3/ca/items/{}", sku).as_str()) + .with_status(200) + .with_header("content-type", "application/xml") + .with_body(body) + .create(); + + let got = client.get_item(sku).await.unwrap(); + let want = GetItem { + item_response: Item { + mart: Some("WALMART_CA".to_string()), + sku: "foo".to_string(), + wpid: Some("4W1AF6YU04F5".into()), + upc: Some("735732770692".into()), + gtin: Some("00735732770692".into()), + product_name: Some("Victoria Classics Vista Paisley 8-Piece Bedding Comforter Set".into()), + shelf: None, + product_type: Some("Bedding Sets".into()), + price: Some(Price { + currency: "CAD".to_string(), + amount: "13.27".into(), + }), + published_status: Some("IN_PROGRESS".into()), + }, + }; + assert_eq!(got, want); + } + + #[tokio::test] + async fn test_retire_item() { + let client = crate::test_util::get_client_ca(); + + let sku = "foo"; + let body = r#" + + + 34931712 + Thank you. Your item has been submitted for retirement from Walmart Catalog. Please note that it can take up to 48 hours for items to be retired from our catalog. + + "#; + + let _m = mock("DELETE", format!("/v3/ca/items/{}", sku).as_str()) + .with_status(200) + .with_header("content-type", "application/xml") + .with_body(body) + .create(); + + let got = client.retire_item(sku).await.unwrap(); + let want = RetireItem { + sku: "34931712".to_string(), + message: Some("Thank you. Your item has been submitted for retirement from Walmart Catalog. Please note that it can take up to 48 hours for items to be retired from our catalog.".to_string()) + }; + assert_eq!(got, want); + } +} diff --git a/walmart-partner-api/src/api/ca/item/types.rs b/walmart-partner-api/src/api/ca/item/types.rs new file mode 100644 index 0000000..b8b5e23 --- /dev/null +++ b/walmart-partner-api/src/api/ca/item/types.rs @@ -0,0 +1 @@ +pub use crate::shared::item::*; diff --git a/walmart-partner-api/src/api/ca/mod.rs b/walmart-partner-api/src/api/ca/mod.rs new file mode 100644 index 0000000..aa581c3 --- /dev/null +++ b/walmart-partner-api/src/api/ca/mod.rs @@ -0,0 +1,15 @@ +pub use client::Client; +pub use feed::*; +pub use inventory::*; +pub use item::*; +pub use order::*; +pub use report::*; + +pub use crate::shared::*; + +mod client; +mod feed; +mod inventory; +mod item; +mod order; +mod report; diff --git a/walmart-partner-api/src/api/ca/order/mod.rs b/walmart-partner-api/src/api/ca/order/mod.rs new file mode 100644 index 0000000..59583f2 --- /dev/null +++ b/walmart-partner-api/src/api/ca/order/mod.rs @@ -0,0 +1,725 @@ +use chrono::{DateTime, Utc}; +use serde::Serialize; +use xml_builder::XMLElement; + +pub use types::*; + +use crate::api::ca::Client; +use crate::client::Method; +use crate::{WalmartResult, XmlSer}; + +mod types; + +#[derive(Debug, Clone, Default, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GetAllReleasedOrdersQuery { + pub limit: Option, + pub created_start_date: DateTime, + pub created_end_date: Option>, + pub product_info: Option, +} + +#[derive(Debug, Clone, Default, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GetAllOrdersQuery { + pub created_start_date: DateTime, + pub sku: Option, + pub customer_order_id: Option, + pub purchase_order_id: Option, + pub status: Option, + pub created_end_date: Option>, + pub from_expected_ship_date: Option>, + pub to_expected_ship_date: Option>, + pub limit: Option, + pub product_info: Option, + pub ship_node_type: Option, +} + +#[derive(Debug, Clone, Default, Serialize)] +pub struct GetOrderQuery { + pub product_info: Option, +} + +#[derive(Debug, Clone, Serialize)] +pub struct ShipOrderLines { + pub order_lines: Vec, +} + +#[derive(Debug, Clone, Serialize)] +pub struct ShipOrderLine { + pub line_number: String, + pub ship_from_country: String, + /// If not provided the default values are used + pub status_quantity: Option, + pub asn: Option, + pub tracking_info: OrderLineTrackingInfo, +} + +impl Client { + pub async fn get_all_orders(&self, query: GetAllOrdersQuery) -> WalmartResult { + let qs = serde_urlencoded::to_string(query)?; + let req = self.req_xml(Method::GET, "/v3/ca/orders", qs)?; + self.send(req).await?.res_xml().await + } + + pub async fn get_all_orders_by_next_cursor( + &self, + next_cursor: impl AsRef, + ) -> WalmartResult { + use url::form_urlencoded; + let req = self.req_xml( + Method::GET, + "/v3/ca/orders", + form_urlencoded::parse((&next_cursor.as_ref()[1..]).as_bytes()) + .into_owned() + .collect::>(), + )?; + self.send(req).await?.res_xml().await + } + + pub async fn get_all_released_orders( + &self, + query: GetAllReleasedOrdersQuery, + ) -> WalmartResult { + let qs = serde_urlencoded::to_string(query)?; + let req = self.req_xml(Method::GET, "/v3/ca/orders/released", qs)?; + self.send(req).await?.res_xml().await + } + + pub async fn get_order( + &self, + purchase_order_id: impl AsRef, + query: GetOrderQuery, + ) -> WalmartResult { + let qs = serde_urlencoded::to_string(query)?; + let req = self.req_xml( + Method::GET, + &format!("/v3/ca/orders/{}", purchase_order_id.as_ref()), + qs, + )?; + self.send(req).await?.res_xml().await + } + + pub async fn ack_order(&self, purchase_order_id: impl AsRef) -> WalmartResult { + let req = self.req_xml( + Method::POST, + &format!("/v3/ca/orders/{}/acknowledge", purchase_order_id.as_ref()), + (), + )?; + self.send(req).await?.res_xml().await + } + + pub async fn ship_order_lines( + &self, + purchase_order_id: impl AsRef, + input: ShipOrderLines, + ) -> WalmartResult { + let req = self + .req_xml( + Method::POST, + &format!("/v3/ca/orders/{}/shipping", purchase_order_id.as_ref()), + (), + )? + .body_xml(input)?; + + self.send(req).await?.res_xml().await + } +} + +impl XmlSer for ShipOrderLines { + fn to_xml(&self) -> WalmartResult { + let mut order_shipment = XMLElement::new("orderShipment"); + order_shipment.add_attribute("xmlns:ns2", "http://walmart.com/mp/v3/orders"); + order_shipment.add_attribute("xmlns:ns3", "http://walmart.com/"); + + let mut order_lines = XMLElement::new("orderLines"); + + for line in &self.order_lines { + order_lines.add_child(line.to_xml()?)?; + } + + order_shipment.add_child(order_lines)?; + Ok(order_shipment) + } +} + +impl XmlSer for ShipOrderLine { + fn to_xml(&self) -> WalmartResult { + let mut order_line = XMLElement::new("orderLine"); + let mut line_number = XMLElement::new("lineNumber"); + line_number.add_text(self.line_number.clone())?; + order_line.add_child(line_number)?; + + let mut ship_from_country = XMLElement::new("shipFromCountry"); + ship_from_country.add_text(self.ship_from_country.clone())?; + order_line.add_child(ship_from_country)?; + + let mut order_line_statuses = XMLElement::new("orderLineStatuses"); + let mut order_line_status = XMLElement::new("orderLineStatus"); + { + let mut status = XMLElement::new("status"); + status.add_text("Shipped".to_string())?; + order_line_status.add_child(status)?; + + if let Some(asn) = self.asn.clone() { + order_line_status.add_child(asn.to_xml()?)?; + } + + order_line_status.add_child(self.status_quantity.to_xml()?)?; + order_line_status.add_child(self.tracking_info.to_xml()?)?; + } + order_line_statuses.add_child(order_line_status)?; + order_line.add_child(order_line_statuses)?; + + Ok(order_line) + } +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use mockito::{mock, Matcher}; + + use super::*; + + #[tokio::test] + async fn test_get_all_orders() { + let client = crate::test_util::get_client_ca(); + + let _m = mock("GET", "/v3/ca/orders") + .match_query(Matcher::AllOf(vec![Matcher::UrlEncoded( + "limit".into(), + "10".into(), + )])) + .with_status(200) + .with_header("content-type", "application/xml") + .with_body(include_str!("./test_get_all_orders_body.xml")) + .create(); + + let got = client + .get_all_orders(GetAllOrdersQuery { + limit: Some(10), + ..Default::default() + }) + .await + .unwrap(); + + assert_eq!(got.elements.order.len(), 2); + let order = got.elements.order[0].clone(); + assert_eq!(order.purchase_order_id, "2575263094491"); + let date = DateTime::::from_str("2016-05-18T16:53:14.000Z").unwrap(); + assert_eq!(order.order_date, date); + assert_eq!(order.shipping_info.method_code, "Standard"); + assert_eq!(order.order_lines.order_line.len(), 1); + let line = order.order_lines.order_line[0].clone(); + assert_eq!(line.line_number, 4.to_string()); + assert_eq!(line.charges.charge.len(), 1); + } + + #[tokio::test] + async fn test_get_all_released_orders() { + let client = crate::test_util::get_client_ca(); + let body = r#" + + + + 8 + 1 + ?limit=1&hasMoreElements=true&soIndex=8&poIndex=1577014034085&sellerId=801&status=Created&createdStartDate=2016-11-01&createdEndDate=2016-12-12T22:50:46.014Z + + + + 1577014034085 + 5851600737546 + mgr@walmartlabs.com + 2016-11-09T00:31:31.000Z + + 6502248603 + 2016-11-22T07:00:00.000Z + 2016-11-09T07:00:00.000Z + Value + + Madhukara + PGOMS + 860 W Cal Ave + Seat # 860C.2.176 + Sunnyvale + CA + 94086 + USA + RESIDENTIAL + + + + + 1 + + Ozark + Trail 4-Person Dome Tent + NJ_WITHOUT_RCA_003 + + + + PRODUCT + ItemPrice + + USD + 21.99 + + + Tax1 + + USD + 1.92 + + + + + + EACH + 1 + + 2016-11-09T00:32:44.000Z + + + Created + + EACH + 1 + + + + + + 2 + + Lacoste + Sienna Stainless Steel Womens Fashion Watch White Strap 2000855 + 2000855-custom-1a + + + + PRODUCT + ItemPrice + + USD + 75.00 + + + Tax1 + + USD + 6.57 + + + + + + EACH + 1 + + 2016-11-09T00:32:44.000Z + + + Created + + EACH + 1 + + + + + + + + + "#; + + let _m = mock("GET", "/v3/ca/orders/released") + .match_query(Matcher::AllOf(vec![Matcher::UrlEncoded( + "limit".into(), + "10".into(), + )])) + .with_status(200) + .with_header("content-type", "application/xml") + .with_body(body) + .create(); + + let got = client + .get_all_released_orders(GetAllReleasedOrdersQuery { + limit: Some(10), + ..Default::default() + }) + .await + .unwrap(); + assert_eq!(got.meta.total_count, Some(8)); + assert_eq!(got.elements.order.len(), 1); + } + + #[tokio::test] + async fn test_get_order() { + let client = crate::test_util::get_client_ca(); + let body = r#" + + + 1796330120075 + 1568964842632 + 3944889D125D4AA599DCEB84056C2183@relay.walmart.com + 2019-09-19T13:03:55.000+05:30 + + 5106793150 + 2019-10-04T00:30:00.000+05:30 + 2019-10-02T10:00:00.000+05:30 + OneDay + + Shai Kodela + 640 West California Avenue + asdasd + CA + Sunnyvale + 94043 + USA + RESIDENTIAL + + + + + 1 + + TEST-02 + Item 1 + + + + PRODUCT + ItemPrice + + USD + 0.00 + + + + SHIPPING + Shipping + + USD + 0.00 + + + + + EACH + 1 + + 2019-10-01T11:07:00.727+05:30 + + + Created + + EACH + 1 + + + + 67 + + S2H + RUSH + 2019-09-30T13:03:55.000+05:30 + Raj Accept Kumar + TWO_DAY + + + + + + 3PLFulfilled + + + "#; + + let _m = mock("GET", "/v3/ca/orders/1796330120075") + .with_status(200) + .with_header("content-type", "application/xml") + .with_body(body) + .create(); + + let got = client + .get_order("1796330120075", Default::default()) + .await + .unwrap(); + assert_eq!(got.purchase_order_id, "1796330120075"); + assert_eq!(got.customer_order_id, "1568964842632"); + assert_eq!(got.order_lines.order_line.len(), 1); + } + + #[tokio::test] + pub async fn test_ack_order() { + let client = crate::test_util::get_client_ca(); + let body = r#" + + + 2575193093772 + 4021603841173 + mgr@walmartlabs.com + 2016-05-11T22:55:28.000Z + + 6502248603 + 2016-05-20T17:00:00.000Z + 2016-05-16T17:00:00.000Z + Standard + + Joe Doe PGOMS + 860 W Cal Ave + Seat # 860C.2.176 + Sunnyvale + CA + 94086 + USA + RESIDENTIAL + + + + + 2 + + Garmin Refurbished nuvi 2595LMT 5 GPS w Lifetime Maps and Traffic + GRMN100201 + + + + PRODUCT + ItemPrice + + USD + 124.98 + + + Tax1 + + USD + 10.93 + + + + + + EACH + 1 + + 2016-06-03T23:32:51.000Z + + + Acknowledged + + EACH + 1 + + + + + + + "#; + + let _m = mock("POST", "/v3/ca/orders/2575193093772/acknowledge") + .with_status(200) + .with_header("content-type", "application/xml") + .with_body(body) + .create(); + + let got = client.ack_order("2575193093772").await.unwrap(); + assert_eq!(got.purchase_order_id, "2575193093772"); + } + + #[tokio::test] + pub async fn test_ship_order_serialize() { + struct TestCase { + name: &'static str, + input: ShipOrderLines, + want: &'static str, + } + + let test_cases = vec![TestCase { + name: "full_input", + input: ShipOrderLines { + order_lines: vec![ShipOrderLine { + line_number: "2".to_string(), + ship_from_country: "CA".to_string(), + status_quantity: Some(ShipOrderLineStatusQuantity { + unit_of_measurement: Some("EACH".to_string()), + amount: Some(1.to_string()), + }), + asn: Some(ShipOrderLineAsn { + package_asn: "package_asn".to_string(), + pallet_asn: Some("pallet_asn".to_string()), + }), + tracking_info: OrderLineTrackingInfo { + ship_date_time: DateTime::::from_str("2016-06-27T05:30:15.000Z").unwrap(), + carrier_name: OrderLineTrackingCarrier { + other_carrier: None, + carrier: Some("FedEx".to_string()), + }, + method_code: "Standard".to_string(), + tracking_number: "12333634122".to_string(), + tracking_url: Some("http://www.fedex.com".to_string()), + }, + }], + }, + want: r#" + + + + 2 + CA + + + Shipped + + package_asn + pallet_asn + + + EACH + 1 + + + 2016-06-27T05:30:15+00:00 + + FedEx + + Standard + 12333634122 + http://www.fedex.com + + + + + +"#, + }]; + + for tc in test_cases { + crate::test_util::assert_xml_eq(tc.input, tc.want, format!("test case: {}", tc.name)); + } + } + + #[tokio::test] + pub async fn test_ship_order_lines() { + let client = crate::test_util::get_client_ca(); + let body = r#" + + + 2575193093772 + 4021603841173 + mgr@walmartlabs.com + 2016-05-11T22:55:28.000Z + + 6502248603 + 2016-05-20T17:00:00.000Z + 2016-05-16T17:00:00.000Z + Standard + + Joe Doe PGOMS + 860 W Cal Ave + Seat # 860C.2.176 + Sunnyvale + CA + 94086 + USA + RESIDENTIAL + + + + + 2 + CA + + Garmin Refurbished nuvi 2595LMT 5 GPS w Lifetime Maps and Traffic + GRMN100201 + + + + PRODUCT + ItemPrice + + USD + 124.98 + + + Tax1 + + USD + 10.93 + + + + + + EACH + 1 + + 2016-06-03T23:44:41.000Z + + + Shipped + + EACH + 1 + + + 2016-06-27T05:30:15.000Z + + FedEx + + Standard + 12333634122 + http://www.fedex.com + + + + + + + "#; + + let _m = mock("POST", "/v3/ca/orders/2575193093772/shipping") + .with_status(200) + .with_header("content-type", "application/xml") + .match_header("content-type", "application/xml") + .with_body(body) + .create(); + + let input = ShipOrderLines { + order_lines: vec![ShipOrderLine { + line_number: "2".to_string(), + ship_from_country: "CA".to_string(), + status_quantity: None, + asn: None, + tracking_info: OrderLineTrackingInfo { + ship_date_time: DateTime::::from_str("2016-06-27T05:30:15+00:00").unwrap(), + carrier_name: OrderLineTrackingCarrier { + other_carrier: None, + carrier: Some("FedEx".to_string()), + }, + method_code: "Standard".to_string(), + tracking_number: "12333634122".to_string(), + tracking_url: Some("http://www.fedex.com".to_string()), + }, + }], + }; + + let got = client + .ship_order_lines("2575193093772", input) + .await + .unwrap(); + assert_eq!(got.purchase_order_id, "2575193093772"); + } +} diff --git a/walmart-partner-api/src/api/ca/order/test_get_all_orders_body.xml b/walmart-partner-api/src/api/ca/order/test_get_all_orders_body.xml new file mode 100644 index 0000000..b0a2d9b --- /dev/null +++ b/walmart-partner-api/src/api/ca/order/test_get_all_orders_body.xml @@ -0,0 +1,138 @@ + + + + 367 + 10 + ?limit=10&hasMoreElements=true&soIndex=2&poIndex=2&sellerId=10&status=Created&createdStartDate=2016-01-01&createdEndDate=2016-06-03T22:45:16.377Z + + + + 2575263094491 + 4091603648841 + gsingh@walmartlabs.com + 2016-05-18T16:53:14.000Z + + 2342423234 + 2016-06-22T06:00:00.000Z + 2016-06-15T06:00:00.000Z + Standard + + PGOMS Walmart + 850 Cherry Avenue + Floor 5 + San Bruno + CA + 94066 + USA + RESIDENTIAL + + + + + 4 + + Kenmore CF1 or 2086883 Canister Secondary Filter Generic 2 Pack + RCA-OF-444gku444 + + + + PRODUCT + ItemPrice + + USD + 25.00 + + + Tax1 + + USD + 1.87 + + + + + + EACH + 1 + + 2016-05-18T17:01:26.000Z + + + Created + + EACH + 1 + + + + + + + + 2575263094492 + 4091603648841 + gsingh@walmartlabs.com + 2016-05-18T16:53:14.000Z + + 2342423234 + 2016-06-22T06:00:00.000Z + 2016-06-15T06:00:00.000Z + Standard + + PGOMS Walmart + 850 Cherry Avenue + Floor 5 + San Bruno + CA + 94066 + USA + RESIDENTIAL + + + + + 1 + + Kenmore CF1 or 2086883 Canister Secondary Filter Generic 2 Pack + RCA-OF-444gku444 + + + + PRODUCT + ItemPrice + + USD + 25.00 + + + Tax1 + + USD + 1.89 + + + + + + EACH + 1 + + 2016-05-18T17:01:26.000Z + + + Created + + EACH + 1 + + + + 91 + + + + + \ No newline at end of file diff --git a/walmart-partner-api/src/api/ca/order/types.rs b/walmart-partner-api/src/api/ca/order/types.rs new file mode 100644 index 0000000..3edd070 --- /dev/null +++ b/walmart-partner-api/src/api/ca/order/types.rs @@ -0,0 +1,86 @@ +use chrono::{DateTime, Utc}; + +pub use crate::shared::order::*; + +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] +pub struct OrderList { + #[serde(rename = "meta")] + pub meta: OrderListMeta, + #[serde(rename = "elements")] + pub elements: Orders, +} + +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] +pub struct Orders { + /// Purchase Order List + #[serde(rename = "order")] + #[serde(default)] + pub order: Vec, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct Order { + /// A unique ID associated with the seller's purchase order + #[serde(rename = "purchaseOrderId")] + pub purchase_order_id: String, + /// A unique ID associated with the sales order for specified customer + #[serde(rename = "customerOrderId")] + pub customer_order_id: String, + /// The email address of the customer for the sales order + #[serde(rename = "customerEmailId")] + pub customer_email_id: String, + /// The date the customer submitted the sales order + #[serde(rename = "orderDate")] + pub order_date: DateTime, + #[serde(rename = "shippingInfo")] + pub shipping_info: ShippingInfo, + #[serde(rename = "orderLines")] + pub order_lines: OrderLines, + #[serde(rename = "shipNode")] + #[serde(default)] + pub ship_node: ShipNode, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct OrderLines { + /// A list of order lines in the order + #[serde(rename = "orderLine")] + #[serde(default)] + pub order_line: Vec, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct OrderLine { + /// The line number associated with the details for each individual item in the purchase order + #[serde(rename = "lineNumber")] + pub line_number: String, + /// The ship from country is associated with the details for each individual item in the purchase order + #[serde(rename = "shipFromCountry", skip_serializing_if = "Option::is_none")] + pub ship_from_country: Option, + #[serde(rename = "item")] + pub item: OrderLineItem, + #[serde(rename = "charges")] + pub charges: OrderLineCharges, + #[serde(rename = "orderLineQuantity")] + pub order_line_quantity: OrderLineStatusQuantity, + /// The date shown on the recent order status + #[serde(rename = "statusDate")] + pub status_date: DateTime, + #[serde(rename = "orderLineStatuses")] + pub order_line_statuses: OrderLineStatuses, + #[serde(rename = "refund", skip_serializing_if = "Option::is_none")] + pub refund: Option, + #[serde( + rename = "originalCarrierMethod", + skip_serializing_if = "Option::is_none" + )] + pub original_carrier_method: Option, + #[serde(rename = "referenceLineId", skip_serializing_if = "Option::is_none")] + pub reference_line_id: Option, + #[serde(rename = "fulfillment", skip_serializing_if = "Option::is_none")] + pub fulfillment: Option, + #[serde(rename = "intentToCancel", skip_serializing_if = "Option::is_none")] + pub intent_to_cancel: Option, + #[serde(rename = "configId", skip_serializing_if = "Option::is_none")] + pub config_id: Option, +} diff --git a/walmart-partner-api/src/api/ca/report/mod.rs b/walmart-partner-api/src/api/ca/report/mod.rs new file mode 100644 index 0000000..6baf118 --- /dev/null +++ b/walmart-partner-api/src/api/ca/report/mod.rs @@ -0,0 +1,39 @@ +use std::io::Write; + +use serde_urlencoded; + +pub use types::*; + +use crate::client::{Client, Method}; +use crate::result::*; + +mod types; + +#[derive(Debug, Default, Serialize)] +#[allow(non_snake_case)] +pub struct GetReportQuery<'a> { + #[serde(rename = "type")] + pub type_: &'a str, +} + +impl Client { + pub async fn get_report(&self) -> WalmartResult { + let qs = serde_urlencoded::to_string(&GetReportQuery { + type_: R::report_type(), + })?; + let req = self.req_xml(Method::GET, "/v3/getReport", qs)?; + let res = self.send(req).await?.res_bytes().await?; + R::deserialize(&*res) + } + + pub async fn get_report_raw( + &self, + query: GetReportQuery<'_>, + mut w: W, + ) -> WalmartResult { + let qs = serde_urlencoded::to_string(&query)?; + let req = self.req_xml(Method::GET, "/v3/getReport", qs)?; + let res = self.send(req).await?.res_bytes().await?; + std::io::copy(&mut &*res, &mut w).map_err(Into::into) + } +} diff --git a/walmart-partner-api/src/report/item.rs b/walmart-partner-api/src/api/ca/report/types.rs similarity index 75% rename from walmart-partner-api/src/report/item.rs rename to walmart-partner-api/src/api/ca/report/types.rs index cdb168a..3ae310c 100644 --- a/walmart-partner-api/src/report/item.rs +++ b/walmart-partner-api/src/api/ca/report/types.rs @@ -1,12 +1,20 @@ -use super::ReportType; -use crate::result::*; -use bigdecimal::BigDecimal; -use csv; use std::io::prelude::*; use std::io::BufReader; + +use bigdecimal::BigDecimal; +use csv; use tempfile; use zip::read::ZipArchive; +use crate::result::*; + +pub trait ReportType { + type Data; + + fn report_type() -> &'static str; + fn deserialize(r: R) -> WalmartResult; +} + pub struct ItemReportType; #[derive(Debug)] @@ -38,19 +46,13 @@ macro_rules! impl_from_csv { match headers.get(i).map(AsRef::as_ref) { $( Some($header) => { - $field = v.parse().map_err(|err| -> WalmartError { - format!("Parse field '{}' error: {}", $header, err).into() + $field = v.parse().map_err(|err| { + WalmartError::Csv(format!("Parse field '{}' error: {}", $header, err)) }).ok() } )*, - Some(unknown_header) => { - return Err( - format!("Unknown header '{}'.", unknown_header).into() - ) - } - None => return Err( - format!("Column index {} is out of bound.", i).into() - ), + Some(unknown_header) => return Err(WalmartError::Csv(format!("Unknown header '{}'.", unknown_header))), + None => return Err(WalmartError::Csv(format!("Column index {} is out of bound.", i))), } } @@ -64,9 +66,7 @@ macro_rules! impl_from_csv { }; (GET_VALUE $field:ident, $header:expr, ) => { - $field.ok_or_else(|| -> WalmartError { - format!("Field '{}' was not found.", $header).into() - })? + $field.ok_or_else(|| WalmartError::Csv(format!("Field '{}' was not found.", $header)))? }; (GET_VALUE $field:ident, $header:expr, optional) => { @@ -84,10 +84,16 @@ impl_from_csv! { pub product_name: String, #[csv(header = "PRODUCT CATEGORY")] pub product_category: String, - #[csv(header = "PRICE")] - pub price: BigDecimal, - #[csv(header = "CURRENCY")] - pub currency: String, + #[csv(header = "PRICE", optional)] + pub price: Option, + #[csv(header = "CURRENCY", optional)] + pub currency: Option, + #[csv(header = "BUY BOX ITEM PRICE", optional)] + pub buy_box_item_price: Option, + #[csv(header = "BUY BOX SHIPPING PRICE", optional)] + pub buy_box_shipping_price: Option, + #[csv(header = "WON BUY BOX?", optional)] + pub won_buy_box: Option, #[csv(header = "PUBLISH STATUS")] pub publish_status: String, #[csv(header = "STATUS CHANGE REASON")] @@ -142,21 +148,23 @@ impl ReportType for ItemReportType { let mut tmp_file = tempfile::tempfile()?; copy(&mut r, &mut tmp_file)?; + tmp_file.seek(SeekFrom::Start(0))?; let mut br = BufReader::new(&mut tmp_file); let mut zip = ZipArchive::new(&mut br)?; + let csv_file = zip.by_index(0)?; let mut csv_reader = csv::Reader::from_reader(csv_file); let headers: Vec = csv_reader .headers() - .map_err(|err| -> WalmartError { format!("Parse csv header error: {}", err).into() })? + .map_err(|err| WalmartError::Csv(format!("Parse csv header error: {}", err)))? .iter() .map(|s| s.to_string()) .collect(); let mut rows = vec![]; for (i, result) in csv_reader.records().enumerate() { - let record = result.map_err(|err| -> WalmartError { - format!("Line {}: Parse csv error: {}", i + 1, err).into() + let record = result.map_err(|err| { + WalmartError::Csv(format!("Line {}: Parse csv error: {}", i + 1, err).into()) })?; let fields: Vec<&str> = record.into_iter().collect(); rows.push(ItemReportRow::from_csv(&headers, &fields)?) @@ -165,13 +173,3 @@ impl ReportType for ItemReportType { Ok(ItemReport { rows }) } } - -// #[test] -// fn test_item_report_deserialize() { -// use std::io::prelude::*; -// use std::fs::File; - -// let mut f = File::open("../target/report.zip").unwrap(); -// let report = ItemReportType::deserialize(f).unwrap(); -// println!("{:#?}", report.rows) -// } diff --git a/walmart-partner-api/src/api/mod.rs b/walmart-partner-api/src/api/mod.rs new file mode 100644 index 0000000..53b18f2 --- /dev/null +++ b/walmart-partner-api/src/api/mod.rs @@ -0,0 +1,2 @@ +pub mod ca; +pub mod us; diff --git a/walmart-partner-api/src/api/us/client.rs b/walmart-partner-api/src/api/us/client.rs new file mode 100644 index 0000000..f525c40 --- /dev/null +++ b/walmart-partner-api/src/api/us/client.rs @@ -0,0 +1,27 @@ +use crate::result::WalmartResult; +use crate::WalmartCredential; + +pub struct Client { + inner: crate::client::Client, +} + +impl Client { + pub fn new(credential: WalmartCredential) -> WalmartResult { + let inner = crate::client::Client::new(crate::WalmartMarketplace::USA, credential)?; + Ok(Self { inner }) + } +} + +impl std::ops::Deref for Client { + type Target = crate::client::Client; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl std::ops::DerefMut for Client { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } +} diff --git a/walmart-partner-api/src/api/us/feed/mod.rs b/walmart-partner-api/src/api/us/feed/mod.rs new file mode 100644 index 0000000..c2622bb --- /dev/null +++ b/walmart-partner-api/src/api/us/feed/mod.rs @@ -0,0 +1,219 @@ +use std::io::Read; +pub use types::*; + +use crate::api::us::Client; +use crate::client::Method; +use crate::result::WalmartResult; + +mod types; + +impl Client { + pub async fn get_all_feed_statuses( + &self, + query: GetAllFeedStatusesQuery, + ) -> WalmartResult { + let qs = serde_urlencoded::to_string(&query)?; + let req = self.req_xml(Method::GET, "/v3/feeds", qs)?; + self.send(req).await?.res_xml().await + } + + pub async fn get_feed_and_item_status( + &self, + feed_id: impl AsRef, + query: GetFeedAndItemStatusQuery, + ) -> WalmartResult { + let qs = serde_urlencoded::to_string(&query)?; + let path = format!("/v3/feeds/{}", feed_id.as_ref()); + let req = self.req_xml(Method::GET, &path, qs)?; + self.send(req).await?.res_xml().await + } + + pub async fn bulk_upload_xml( + &self, + feed_type: impl AsRef, + mut feed: R, + ) -> WalmartResult { + let mut buffer = Vec::new(); + feed.read_to_end(&mut buffer)?; + let part = reqwest::multipart::Part::bytes(buffer); + let form = reqwest::multipart::Form::new().part("file", part); + let req = self + .req_xml(Method::POST, "/v3/feeds", vec![("feedType", feed_type)])? + .form(form); + self.send(req).await?.res_xml().await + } +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use chrono::{DateTime, Utc}; + use mockito::Matcher; + + use super::*; + + #[tokio::test] + async fn get_all_feed_statuses() { + let client = crate::test_util::get_client_us(); + + let feed_id = "12234EGGT564YTEGFA"; + let limit = 50; + let _m = mockito::mock("GET", "/v3/feeds") + .match_query(Matcher::AllOf(vec![ + Matcher::UrlEncoded("feedId".into(), feed_id.into()), + Matcher::UrlEncoded("limit".into(), limit.to_string()), + ])) + .with_status(200) + .with_header("content-type", "application/xml") + .with_body(include_str!("./test_get_all_feed_statuses.xml")) + .create(); + + let q = GetAllFeedStatusesQuery { + feed_id: Some(feed_id.to_string()), + limit: Some(limit), + offset: None, + }; + let got = client.get_all_feed_statuses(q).await.unwrap(); + assert_eq!(got.total_results.unwrap(), 210); + assert_eq!(got.results.feed.len(), 50); + let feed = got.results.feed[49].clone(); + assert_eq!( + feed.feed_id.unwrap(), + "1F6CF24319FF42FCACFB3D672E7A6F60@AU8BAgA".to_string() + ); + assert_eq!(feed.feed_type.unwrap(), "item".to_string()); + assert_eq!(feed.partner_id.unwrap(), "100009".to_string()); + assert_eq!(feed.items_received.unwrap(), 1); + assert_eq!(feed.items_succeeded.unwrap(), 0); + assert_eq!(feed.items_failed.unwrap(), 1); + assert_eq!(feed.items_processing.unwrap(), 0); + assert_eq!(feed.feed_status.unwrap(), "INPROGRESS".to_string()); + assert_eq!( + feed.feed_date.unwrap(), + DateTime::::from_str("2019-09-12T18:11:34.607Z").unwrap() + ); + } + + #[tokio::test] + async fn get_feed_and_item_status() { + let client = crate::test_util::get_client_us(); + let body = r#" + + + F129C19240844B97A3C6AD8F1A2C4997@AU8BAQA + PROCESSED + 2019-09-12T17:53:23.059Z + 1 + 1 + 0 + 0 + 0 + 20 + + + 0 + 0960B3B82687490FA5E51CB0801478A4@AU8BAgA + 71ZLHHMKNS6G + 0 + 51681142 + + + GTIN + 00363824587165 + + + UPC + 363824587165 + + + SUCCESS + + + + ""#; + + let feed_id = "F129C19240844B97A3C6AD8F1A2C4997@AU8BAQA"; + let _m = mockito::mock("GET", format!("/v3/feeds/{}", feed_id).as_str()) + .with_status(200) + .with_header("content-type", "application/xml") + .with_body(body) + .create(); + + let got = client + .get_feed_and_item_status(feed_id, Default::default()) + .await + .unwrap(); + assert_eq!(got.feed_id.unwrap(), feed_id); + assert_eq!(got.feed_status.unwrap(), "PROCESSED".to_string()); + assert_eq!( + got.item_details.item_ingestion_status[0].mart_id.unwrap(), + 0, + ); + } + + #[tokio::test] + async fn test_bulk_upload_xml() { + let client = crate::test_util::get_client_us(); + let body = r#" + + + 884C20C71B7E42FAA41FABFA52596A62@AUoBAQA + + "#; + + let _m = mockito::mock("POST", "/v3/feeds") + .match_query(Matcher::UrlEncoded("feedType".into(), "test".into())) + .with_status(200) + .with_header("content-type", "application/xml") + .with_body(body) + .create(); + + let got = client + .bulk_upload_xml("test", "somefeed".as_bytes()) + .await + .unwrap(); + assert_eq!( + got.feed_id.unwrap(), + "884C20C71B7E42FAA41FABFA52596A62@AUoBAQA" + ); + } +} + +#[test] +fn test_emit_inventory_feed() { + let data = InventoryFeed { + items: vec![ + InventoryFeedItem { + sku: "1068155".to_string(), + quantity: 10, + }, + InventoryFeedItem { + sku: "10210321".to_string(), + quantity: 20, + }, + ], + }; + let xml = data.emit_xml().unwrap(); + let want = r#" + + + 1.4 + + + 1068155 + + EACH + 10 + + + + 10210321 + + EACH + 20 + + +"#; + crate::test_util::assert_xml_str_eq(&xml, want, "not equal"); +} diff --git a/walmart-partner-api/src/api/us/feed/test_get_all_feed_statuses.xml b/walmart-partner-api/src/api/us/feed/test_get_all_feed_statuses.xml new file mode 100644 index 0000000..556fc8a --- /dev/null +++ b/walmart-partner-api/src/api/us/feed/test_get_all_feed_statuses.xml @@ -0,0 +1,911 @@ + + + 210 + 0 + 50 + + + 1401AE7FA2B045A79294C90C43671720@AVUBAQA + PROMO_PRICE + 100009 + 1 + 0 + 1 + 0 + PROCESSED + 2019-09-13T22:43:38.467Z + 2019-09-13T23:43:38.526Z + 1401AE7FA2B045A79294C90C43671720@AVUBAQA.json + 1 + 0 + 0 + f5e3d40a-1267-46b9-a2f3-7811e4c0b2de + + + 61CDBC483DB64EC097E7D0961DA54621@AU8BAgA + item + 100009 + 1 + 0 + 1 + 0 + INPROGRESS + 2019-09-13T20:46:33.195Z + 2019-09-13T20:46:33.383Z + 61CDBC483DB64EC097E7D0961DA54621@AU8BAgA.json + 1 + 0 + 0 + 0f3e4dd4-0514-4346-b39d-af0e00ea066d + f5e3d40a-1267-46b9-a2f3-7811e4c0b2de + + + 4873EBF14F2048FC963B3E25535D0D13@AU8BAgA + item + 100009 + 1 + 0 + 1 + 0 + INPROGRESS + 2019-09-13T20:45:37.504Z + 2019-09-13T20:45:38.754Z + 4873EBF14F2048FC963B3E25535D0D13@AU8BAgA.json + 1 + 0 + 0 + 0f3e4dd4-0514-4346-b39d-af0e00ea066d + f5e3d40a-1267-46b9-a2f3-7811e4c0b2de + + + B50CF594659842E5A0D2F8FBC38E0E0D@AU8BAQA + item + 100009 + 1 + 0 + 1 + 0 + INPROGRESS + 2019-09-13T20:44:59.352Z + 2019-09-13T20:44:59.460Z + B50CF594659842E5A0D2F8FBC38E0E0D@AU8BAQA.json + 1 + 0 + 0 + 0f3e4dd4-0514-4346-b39d-af0e00ea066d + f5e3d40a-1267-46b9-a2f3-7811e4c0b2de + + + 4028C1C2C3264C25A42E4A01DEDF432D@AU8BAQA + item + 100009 + 1 + 0 + 1 + 0 + INPROGRESS + 2019-09-13T20:44:08.545Z + 2019-09-13T20:44:08.663Z + 4028C1C2C3264C25A42E4A01DEDF432D@AU8BAQA.json + 1 + 0 + 0 + 0f3e4dd4-0514-4346-b39d-af0e00ea066d + f5e3d40a-1267-46b9-a2f3-7811e4c0b2de + + + 5467D0563CD14AC68972B7ECF122C5E3@AU8BAgA + item + 100009 + 1 + 0 + 1 + 0 + INPROGRESS + 2019-09-13T20:35:40.013Z + 2019-09-13T20:35:40.151Z + 5467D0563CD14AC68972B7ECF122C5E3@AU8BAgA.json + 1 + 0 + 0 + 0f3e4dd4-0514-4346-b39d-af0e00ea066d + f5e3d40a-1267-46b9-a2f3-7811e4c0b2de + + + E7235D494D6F474AA47BC2AA817AD938@AU8BAgA + item + 100009 + 1 + 0 + 1 + 0 + INPROGRESS + 2019-09-13T20:34:12.765Z + 2019-09-13T20:34:12.936Z + E7235D494D6F474AA47BC2AA817AD938@AU8BAgA.json + 1 + 0 + 0 + 0f3e4dd4-0514-4346-b39d-af0e00ea066d + f5e3d40a-1267-46b9-a2f3-7811e4c0b2de + + + 492C7888FAEE4779A8A48E31EAAC65CE@AU8BAgA + item + 100009 + 1 + 0 + 1 + 0 + PROCESSED + 2019-09-13T20:32:54.404Z + 2019-09-13T22:32:54.869Z + 492C7888FAEE4779A8A48E31EAAC65CE@AU8BAgA.json + 0 + 0 + 1 + 0f3e4dd4-0514-4346-b39d-af0e00ea066d + f5e3d40a-1267-46b9-a2f3-7811e4c0b2de + + + EDD3F56A2E824959AF60550DE88F8086@AU8BAgA + item + 100009 + 0 + 0 + 0 + 0 + ERROR + 2019-09-13T20:31:25.086Z + 2019-09-13T20:31:25.273Z + EDD3F56A2E824959AF60550DE88F8086@AU8BAgA.json + 0 + 0 + 0 + 0f3e4dd4-0514-4346-b39d-af0e00ea066d + f5e3d40a-1267-46b9-a2f3-7811e4c0b2de + + + 8AE380EB33B74728A91FC6470E9F3251@AU8BAgA + item + 100009 + 1 + 0 + 1 + 0 + INPROGRESS + 2019-09-13T20:28:55.200Z + 2019-09-13T20:28:55.302Z + 8AE380EB33B74728A91FC6470E9F3251@AU8BAgA.json + 1 + 0 + 0 + 0f3e4dd4-0514-4346-b39d-af0e00ea066d + f5e3d40a-1267-46b9-a2f3-7811e4c0b2de + + + 248E08F0AE2D4878BDC5A929805CDE35@AU8BAQA + item + 100009 + 0 + 0 + 0 + 0 + ERROR + 2019-09-13T20:09:11.024Z + 2019-09-13T20:09:11.198Z + 248E08F0AE2D4878BDC5A929805CDE35@AU8BAQA.json + 0 + 0 + 0 + 0f3e4dd4-0514-4346-b39d-af0e00ea066d + f5e3d40a-1267-46b9-a2f3-7811e4c0b2de + + + 595884EDE2AF436AA417727EDCAD5E29@AU8BAQA + item + 100009 + 1 + 0 + 1 + 0 + PROCESSED + 2019-09-13T20:07:35.045Z + 2019-09-13T22:07:35.882Z + 595884EDE2AF436AA417727EDCAD5E29@AU8BAQA.json + 0 + 0 + 1 + 0f3e4dd4-0514-4346-b39d-af0e00ea066d + f5e3d40a-1267-46b9-a2f3-7811e4c0b2de + + + E18D54C2E3F3443DB565AF4CC343162A@AU8BAgA + item + 100009 + 1 + 0 + 1 + 0 + PROCESSED + 2019-09-13T19:41:08.430Z + 2019-09-13T21:41:09.015Z + E18D54C2E3F3443DB565AF4CC343162A@AU8BAgA.json + 0 + 0 + 1 + 0f3e4dd4-0514-4346-b39d-af0e00ea066d + f5e3d40a-1267-46b9-a2f3-7811e4c0b2de + + + FFE538D9B25A4000AD2A067ACEDCF4C4@AUoBAQA + SELLER + inventory + 100009 + 2 + 2 + 0 + 0 + PROCESSED + 2019-09-13T19:37:27.788Z + 2019-09-13T19:37:48.622Z + 0 + 0 + 0 + 0f3e4dd4-0514-4346-b39d-af0e00ea066d + f5e3d40a-1267-46b9-a2f3-7811e4c0b2de + + + F3B56955D77B45FCB00EFE1F256F8050@AUoBAQA + SELLER + inventory + 100009 + 2 + 2 + 0 + 0 + PROCESSED + 2019-09-13T19:35:01.262Z + 2019-09-13T19:35:31.392Z + 0 + 0 + 0 + 0f3e4dd4-0514-4346-b39d-af0e00ea066d + f5e3d40a-1267-46b9-a2f3-7811e4c0b2de + + + EA1334C394ED43BA8743D67747332539@AUoBAQA + SELLER + inventory + 100009 + 2 + 2 + 0 + 0 + PROCESSED + 2019-09-13T19:33:24.128Z + 2019-09-13T19:33:44.904Z + 0 + 0 + 0 + 0f3e4dd4-0514-4346-b39d-af0e00ea066d + f5e3d40a-1267-46b9-a2f3-7811e4c0b2de + + + D5584F239FFD4D7C90738986DDC2326E@AVUBAQA + PROMO_PRICE + 100009 + 1 + 0 + 1 + 0 + PROCESSED + 2019-09-13T19:25:27.795Z + 2019-09-13T20:25:27.808Z + D5584F239FFD4D7C90738986DDC2326E@AVUBAQA.json + 1 + 0 + 0 + f5e3d40a-1267-46b9-a2f3-7811e4c0b2de + + + 6966CA408F7E4FCAA24E03BDC097E9D2@AVMBAQA + MP_ITEM_PRICE_UPDATE + 100009 + 2 + 2 + 0 + 0 + PROCESSED + 2019-09-13T19:13:04.816Z + 2019-09-13T20:13:04.865Z + 6966CA408F7E4FCAA24E03BDC097E9D2@AVMBAQA.json + 0 + 0 + 0 + 0f3e4dd4-0514-4346-b39d-af0e00ea066d + f5e3d40a-1267-46b9-a2f3-7811e4c0b2de + + + 67F29CF7F11A4D79A8617F4B22A17DA0@AU8BAgA + item + 100009 + 1 + 0 + 1 + 0 + INPROGRESS + 2019-09-13T18:08:46.860Z + 2019-09-13T18:08:46.973Z + 67F29CF7F11A4D79A8617F4B22A17DA0@AU8BAgA.json + 1 + 0 + 0 + 0f3e4dd4-0514-4346-b39d-af0e00ea066d + f5e3d40a-1267-46b9-a2f3-7811e4c0b2de + + + 152E7873BD4A4517A430BA5711CE71FD@AU8BAgA + item + 100009 + 1 + 0 + 1 + 0 + INPROGRESS + 2019-09-13T18:07:43.567Z + 2019-09-13T18:07:43.764Z + 152E7873BD4A4517A430BA5711CE71FD@AU8BAgA.json + 1 + 0 + 0 + 0f3e4dd4-0514-4346-b39d-af0e00ea066d + f5e3d40a-1267-46b9-a2f3-7811e4c0b2de + + + 0217880566B7457BA08083B2D1731626@AU8BAQA + item + 100009 + 1 + 0 + 1 + 0 + PROCESSED + 2019-09-13T18:06:23.932Z + 2019-09-13T18:06:25.067Z + 0217880566B7457BA08083B2D1731626@AU8BAQA.json + 1 + 0 + 0 + 0f3e4dd4-0514-4346-b39d-af0e00ea066d + f5e3d40a-1267-46b9-a2f3-7811e4c0b2de + + + 1BE8F13936D34BB3A5E48EEB3F76F53B@AU8BAgA + MARKETPLACE_PARTNER + item + 100009 + 1 + 1 + 0 + 0 + PROCESSED + 2019-09-13T18:05:17.747Z + 2019-09-13T18:05:26.662Z + 1BE8F13936D34BB3A5E48EEB3F76F53B@AU8BAgA.json + 0 + 0 + 0 + 0f3e4dd4-0514-4346-b39d-af0e00ea066d + f5e3d40a-1267-46b9-a2f3-7811e4c0b2de + + + 7CE6FA7AE1734C2EB96848BA59906B85@AU8BAgA + item + 100009 + 1 + 0 + 1 + 0 + INPROGRESS + 2019-09-13T18:04:03.558Z + 2019-09-13T18:04:03.663Z + 7CE6FA7AE1734C2EB96848BA59906B85@AU8BAgA.json + 1 + 0 + 0 + 0f3e4dd4-0514-4346-b39d-af0e00ea066d + f5e3d40a-1267-46b9-a2f3-7811e4c0b2de + + + BB518A98B013423D902144C653982154@AU8BAgA + item + 100009 + 1 + 0 + 1 + 0 + INPROGRESS + 2019-09-13T18:02:28.628Z + 2019-09-13T18:02:28.866Z + BB518A98B013423D902144C653982154@AU8BAgA.json + 1 + 0 + 0 + 0f3e4dd4-0514-4346-b39d-af0e00ea066d + f5e3d40a-1267-46b9-a2f3-7811e4c0b2de + + + 271557ABB4934F8998646579998E4A7B@AVUBAQA + PROMO_PRICE + 100009 + 1 + 0 + 1 + 0 + PROCESSED + 2019-09-13T18:00:59.568Z + 2019-09-13T19:00:59.661Z + 271557ABB4934F8998646579998E4A7B@AVUBAQA.json + 1 + 0 + 0 + f5e3d40a-1267-46b9-a2f3-7811e4c0b2de + + + 0027E5A8ED86488583C3542F5D1AAC7F@AU8BAgA + item + 100009 + 1 + 0 + 1 + 0 + INPROGRESS + 2019-09-13T18:00:39.619Z + 2019-09-13T18:00:40.237Z + 0027E5A8ED86488583C3542F5D1AAC7F@AU8BAgA.json + 1 + 0 + 0 + 0f3e4dd4-0514-4346-b39d-af0e00ea066d + f5e3d40a-1267-46b9-a2f3-7811e4c0b2de + + + FD5CF5592F544FFF89A3542C353CD13E@AU8BAQA + MARKETPLACE_PARTNER + item + 100009 + 1 + 1 + 0 + 0 + PROCESSED + 2019-09-13T17:58:35.603Z + 2019-09-13T17:59:36.782Z + FD5CF5592F544FFF89A3542C353CD13E@AU8BAQA.json + 0 + 0 + 0 + 0f3e4dd4-0514-4346-b39d-af0e00ea066d + f5e3d40a-1267-46b9-a2f3-7811e4c0b2de + + + 0435024C72DC4912A13AC2485E5830F6@AU8BAQA + MARKETPLACE_PARTNER + item + 100009 + 1 + 1 + 0 + 0 + PROCESSED + 2019-09-13T17:55:38.958Z + 2019-09-13T18:07:52.349Z + 0435024C72DC4912A13AC2485E5830F6@AU8BAQA.json + 0 + 0 + 0 + 0f3e4dd4-0514-4346-b39d-af0e00ea066d + f5e3d40a-1267-46b9-a2f3-7811e4c0b2de + + + 1F0AD5E8DA004A05BDBCD650E4919510@AU8BAQA + MARKETPLACE_PARTNER + item + 100009 + 1 + 1 + 0 + 0 + PROCESSED + 2019-09-13T17:49:43.465Z + 2019-09-13T17:50:12.404Z + 1F0AD5E8DA004A05BDBCD650E4919510@AU8BAQA.json + 0 + 0 + 0 + 0f3e4dd4-0514-4346-b39d-af0e00ea066d + f5e3d40a-1267-46b9-a2f3-7811e4c0b2de + + + 0F0DF90C72A44EB0A712D500BC50C1AE@AU8BAQA + MARKETPLACE_PARTNER + item + 100009 + 1 + 1 + 0 + 0 + PROCESSED + 2019-09-13T17:49:31.546Z + 2019-09-13T17:50:01.381Z + 0F0DF90C72A44EB0A712D500BC50C1AE@AU8BAQA.json + 0 + 0 + 0 + 0f3e4dd4-0514-4346-b39d-af0e00ea066d + f5e3d40a-1267-46b9-a2f3-7811e4c0b2de + + + 0828C7D2099841408C5D93584921E05A@AU8BAQA + MARKETPLACE_PARTNER + item + 100009 + 1 + 1 + 0 + 0 + PROCESSED + 2019-09-13T17:48:26.837Z + 2019-09-13T17:48:29.117Z + 0828C7D2099841408C5D93584921E05A@AU8BAQA.json + 0 + 0 + 0 + 0f3e4dd4-0514-4346-b39d-af0e00ea066d + f5e3d40a-1267-46b9-a2f3-7811e4c0b2de + + + 66AA78FB0EFA448CB60A9ACB7E4E5062@AU8BAQA + MARKETPLACE_PARTNER + item + 100009 + 1 + 1 + 0 + 0 + PROCESSED + 2019-09-13T17:48:04.988Z + 2019-09-13T17:48:11.512Z + 66AA78FB0EFA448CB60A9ACB7E4E5062@AU8BAQA.json + 0 + 0 + 0 + 0f3e4dd4-0514-4346-b39d-af0e00ea066d + f5e3d40a-1267-46b9-a2f3-7811e4c0b2de + + + DA648820C9EB4794B0FCEAE260A247B5@AVMBAQA + MP_ITEM_PRICE_UPDATE + 100009 + 2 + 1 + 1 + 0 + PROCESSED + 2019-09-13T17:43:16.271Z + 2019-09-13T18:43:16.324Z + DA648820C9EB4794B0FCEAE260A247B5@AVMBAQA.json + 1 + 0 + 0 + 0f3e4dd4-0514-4346-b39d-af0e00ea066d + f5e3d40a-1267-46b9-a2f3-7811e4c0b2de + + + 487BEFCB4BD640BE997C0B6168D353B8@AVMBAgA + MP_ITEM_PRICE_UPDATE + 100009 + 2 + 0 + 2 + 0 + PROCESSED + 2019-09-12T22:47:22.670Z + 2019-09-12T23:47:22.772Z + 487BEFCB4BD640BE997C0B6168D353B8@AVMBAgA.json + 2 + 0 + 0 + 0f3e4dd4-0514-4346-b39d-af0e00ea066d + f5e3d40a-1267-46b9-a2f3-7811e4c0b2de + + + 3D86663718DE4876B8A9848CF5EA6470@AVMBAgA + MP_ITEM_PRICE_UPDATE + 100009 + 2 + 1 + 1 + 0 + PROCESSED + 2019-09-12T22:46:57.755Z + 2019-09-12T23:46:57.811Z + 3D86663718DE4876B8A9848CF5EA6470@AVMBAgA.json + 1 + 0 + 0 + 0f3e4dd4-0514-4346-b39d-af0e00ea066d + f5e3d40a-1267-46b9-a2f3-7811e4c0b2de + + + F8ECD28A5E6D484F92CC5B263F61014C@AVMBAQA + MP_ITEM_PRICE_UPDATE + 100009 + 2 + 1 + 1 + 0 + PROCESSED + 2019-09-12T22:44:55.967Z + 2019-09-12T23:44:55.981Z + F8ECD28A5E6D484F92CC5B263F61014C@AVMBAQA.json + 1 + 0 + 0 + 0f3e4dd4-0514-4346-b39d-af0e00ea066d + f5e3d40a-1267-46b9-a2f3-7811e4c0b2de + + + E25727D5DE444786A9AD3B843C89D71E@AVUBAQA + PROMO_PRICE + 100009 + 1 + 0 + 1 + 0 + PROCESSED + 2019-09-12T21:16:10.432Z + 2019-09-12T22:16:10.491Z + E25727D5DE444786A9AD3B843C89D71E@AVUBAQA.json + 1 + 0 + 0 + f5e3d40a-1267-46b9-a2f3-7811e4c0b2de + + + 8944073F3C624077BBB41A2B480F1F55@AVMBAQA + MP_ITEM_PRICE_UPDATE + 100009 + 2 + 1 + 1 + 0 + PROCESSED + 2019-09-12T21:15:15.967Z + 2019-09-12T22:15:15.987Z + 8944073F3C624077BBB41A2B480F1F55@AVMBAQA.json + 1 + 0 + 0 + 0f3e4dd4-0514-4346-b39d-af0e00ea066d + f5e3d40a-1267-46b9-a2f3-7811e4c0b2de + + + E7BB7DBAF3FF461AB63A2169F3BEAADE@AVQBAgA + SELLER + LAGTIME + 100009 + 1 + 1 + 0 + 0 + PROCESSED + 2019-09-12T20:49:18.481Z + 2019-09-12T20:49:25.485Z + 0 + 0 + 0 + 0f3e4dd4-0514-4346-b39d-af0e00ea066d + f5e3d40a-1267-46b9-a2f3-7811e4c0b2de + + + C45D0A2AB40F45C78A17B00AADA4CBF9@AVQBAgA + SELLER + LAGTIME + 100009 + 1 + 1 + 0 + 0 + PROCESSED + 2019-09-12T20:48:48.747Z + 2019-09-12T20:48:49.769Z + 0 + 0 + 0 + 0f3e4dd4-0514-4346-b39d-af0e00ea066d + f5e3d40a-1267-46b9-a2f3-7811e4c0b2de + + + 609B1E4C0B3E48989690263DEAC85C63@AVUBAQA + PROMO_PRICE + 100009 + 1 + 0 + 1 + 0 + PROCESSED + 2019-09-12T20:44:42.488Z + 2019-09-12T21:44:42.553Z + 609B1E4C0B3E48989690263DEAC85C63@AVUBAQA.json + 1 + 0 + 0 + f5e3d40a-1267-46b9-a2f3-7811e4c0b2de + + + 14066B6642344B76A8B77AC094F8C63B@AVMBAgA + MP_ITEM_PRICE_UPDATE + 100009 + 2 + 1 + 1 + 0 + PROCESSED + 2019-09-12T20:34:46.039Z + 2019-09-12T21:34:46.128Z + 14066B6642344B76A8B77AC094F8C63B@AVMBAgA.json + 1 + 0 + 0 + 0f3e4dd4-0514-4346-b39d-af0e00ea066d + f5e3d40a-1267-46b9-a2f3-7811e4c0b2de + + + DF87BFD042884E8E9FFACE03EC83CDEB@AU8BAgA + MARKETPLACE_PARTNER + item + 100009 + 1 + 0 + 1 + 0 + PROCESSED + 2019-09-12T20:04:28.376Z + 2019-09-12T20:59:00.118Z + DF87BFD042884E8E9FFACE03EC83CDEB@AU8BAgA.json + 1 + 0 + 0 + 0f3e4dd4-0514-4346-b39d-af0e00ea066d + f5e3d40a-1267-46b9-a2f3-7811e4c0b2de + + + 9D0727D3D46A4D7FB6DF98FA54759B6C@AU8BAQA + item + 100009 + 0 + 0 + 0 + 0 + ERROR + 2019-09-12T20:04:25.336Z + 2019-09-13T01:04:25.401Z + 9D0727D3D46A4D7FB6DF98FA54759B6C@AU8BAQA.json + 0 + 0 + 0 + 0f3e4dd4-0514-4346-b39d-af0e00ea066d + f5e3d40a-1267-46b9-a2f3-7811e4c0b2de + + + E19D348B57094877A8381BD0757F9918@AU8BAQA + item + 100009 + 0 + 0 + 0 + 0 + ERROR + 2019-09-12T20:01:18.116Z + 2019-09-13T01:01:18.178Z + E19D348B57094877A8381BD0757F9918@AU8BAQA.json + 0 + 0 + 0 + 0f3e4dd4-0514-4346-b39d-af0e00ea066d + f5e3d40a-1267-46b9-a2f3-7811e4c0b2de + + + FE99DEBBDF294BC3BBD99BDAC67637A4@AU8BAgA + item + 100009 + 1 + 0 + 1 + 0 + INPROGRESS + 2019-09-12T20:01:13.475Z + 2019-09-12T20:01:13.617Z + FE99DEBBDF294BC3BBD99BDAC67637A4@AU8BAgA.json + 1 + 0 + 0 + 0f3e4dd4-0514-4346-b39d-af0e00ea066d + f5e3d40a-1267-46b9-a2f3-7811e4c0b2de + + + CEAB9DE0784B40348A19ED645F2FAC5A@AU8BAgA + item + 100009 + 1 + 0 + 1 + 0 + INPROGRESS + 2019-09-12T20:00:24.985Z + 2019-09-12T20:00:25.546Z + CEAB9DE0784B40348A19ED645F2FAC5A@AU8BAgA.json + 1 + 0 + 0 + 0f3e4dd4-0514-4346-b39d-af0e00ea066d + f5e3d40a-1267-46b9-a2f3-7811e4c0b2de + + + DE6985E23D78411190EC91EED01BB838@AU8BAQA + item + 100009 + 1 + 0 + 1 + 0 + INPROGRESS + 2019-09-12T18:15:16.202Z + 2019-09-12T18:15:16.531Z + DE6985E23D78411190EC91EED01BB838@AU8BAQA.json + 1 + 0 + 0 + 0f3e4dd4-0514-4346-b39d-af0e00ea066d + f5e3d40a-1267-46b9-a2f3-7811e4c0b2de + + + 3B111C7AF7F642D2B25599C8EBB7793F@AU8BAgA + item + 100009 + 1 + 0 + 1 + 0 + INPROGRESS + 2019-09-12T18:13:31.850Z + 2019-09-12T18:13:31.978Z + 3B111C7AF7F642D2B25599C8EBB7793F@AU8BAgA.json + 1 + 0 + 0 + 0f3e4dd4-0514-4346-b39d-af0e00ea066d + f5e3d40a-1267-46b9-a2f3-7811e4c0b2de + + + 1F6CF24319FF42FCACFB3D672E7A6F60@AU8BAgA + item + 100009 + 1 + 0 + 1 + 0 + INPROGRESS + 2019-09-12T18:11:34.607Z + 2019-09-12T18:11:34.731Z + 1F6CF24319FF42FCACFB3D672E7A6F60@AU8BAgA.json + 1 + 0 + 0 + 0f3e4dd4-0514-4346-b39d-af0e00ea066d + f5e3d40a-1267-46b9-a2f3-7811e4c0b2de + + + \ No newline at end of file diff --git a/walmart-partner-api/src/api/us/feed/types.rs b/walmart-partner-api/src/api/us/feed/types.rs new file mode 100644 index 0000000..3cd1778 --- /dev/null +++ b/walmart-partner-api/src/api/us/feed/types.rs @@ -0,0 +1,47 @@ +pub use crate::shared::feed::*; +use crate::xml::get_element_with_text; +use crate::{WalmartResult, XmlSer}; +use xml_builder::XMLElement; + +#[derive(Debug, PartialEq)] +pub struct InventoryFeed { + pub items: Vec, +} + +impl InventoryFeed { + pub fn emit_xml(&self) -> WalmartResult { + self.to_string() + } +} + +#[derive(Debug, PartialEq)] +pub struct InventoryFeedItem { + pub sku: String, + pub quantity: i32, +} + +impl XmlSer for InventoryFeed { + fn to_xml(&self) -> WalmartResult { + let mut root = XMLElement::new("InventoryFeed"); + root.add_attribute("xmlns", "http://walmart.com/"); + + let mut header = XMLElement::new("InventoryHeader"); + header.add_child(get_element_with_text("version", "1.4")?)?; + root.add_child(header)?; + + for item in &self.items { + let mut inventory = XMLElement::new("inventory"); + inventory.add_child(get_element_with_text("sku", &item.sku)?)?; + + let mut quantity = XMLElement::new("quantity"); + quantity.add_child(get_element_with_text("unit", "EACH")?)?; + quantity.add_child(get_element_with_text("amount", item.quantity)?)?; + + inventory.add_child(quantity)?; + + root.add_child(inventory)?; + } + + Ok(root) + } +} diff --git a/walmart-partner-api/src/api/us/inventory/mod.rs b/walmart-partner-api/src/api/us/inventory/mod.rs new file mode 100644 index 0000000..dbe6ac8 --- /dev/null +++ b/walmart-partner-api/src/api/us/inventory/mod.rs @@ -0,0 +1,87 @@ +pub use types::*; + +use crate::api::us::Client; +use crate::client::Method; +use crate::result::WalmartResult; + +mod types; + +impl Client { + pub async fn get_item_inventory(&self, sku: impl AsRef) -> WalmartResult { + let req = self.req_xml(Method::GET, "/v3/inventory", vec![("sku", sku)])?; + self.send(req).await?.res_xml().await + } + + pub async fn update_item_inventory(&self, inventory: Inventory) -> WalmartResult { + let req = self + .req_xml(Method::PUT, "/v3/inventory", vec![("sku", &inventory.sku)])? + .body_xml(inventory)?; + self.send(req).await?.res_xml().await + } +} + +#[cfg(test)] +mod tests { + use mockito::mock; + + use crate::test_util::get_client_us; + + use super::*; + + #[tokio::test] + async fn test_get_item_inventory() { + let client = get_client_us(); + let _m = mock("GET", "/v3/inventory?sku=97964_KFTest") + .with_status(200) + .with_header("content-type", "application/xml") + .with_body( + r##" + + + 97964_KFTest + + EACH + 10 + + +"##, + ) + .create(); + + let inventory = client.get_item_inventory("97964_KFTest").await.unwrap(); + assert_eq!(inventory.sku, "97964_KFTest"); + assert_eq!(inventory.quantity.amount, 10); + } + + #[tokio::test] + async fn test_update_item_inventory() { + let client = get_client_us(); + let _m = mock("PUT", "/v3/inventory?sku=97964_KFTest") + .with_status(200) + .with_header("content-type", "application/xml") + .with_body( + r##" + + + 97964_KFTest + + EACH + 3 + +"##, + ) + .create(); + + let body = Inventory { + sku: "97964_KFTest".to_string(), + quantity: InventoryQuantity { + unit: "EACH".to_string(), + amount: 3, + }, + }; + + let inventory = client.update_item_inventory(body).await.unwrap(); + assert_eq!(inventory.sku, "97964_KFTest"); + assert_eq!(inventory.quantity.amount, 3); + } +} diff --git a/walmart-partner-api/src/api/us/inventory/types.rs b/walmart-partner-api/src/api/us/inventory/types.rs new file mode 100644 index 0000000..2f1374b --- /dev/null +++ b/walmart-partner-api/src/api/us/inventory/types.rs @@ -0,0 +1,28 @@ +use xml_builder::XMLElement; + +pub use crate::shared::inventory::*; +use crate::WalmartResult; + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename = "inventory")] +pub struct Inventory { + /// A seller-provided Product ID. Response will have decoded value. + #[serde(rename = "sku")] + pub sku: String, + #[serde(rename = "quantity")] + pub quantity: InventoryQuantity, +} + +impl crate::XmlSer for Inventory { + fn to_xml(&self) -> WalmartResult { + let mut inventory = XMLElement::new("inventory"); + inventory.add_attribute("xmlns", "http://walmart.com/"); + + let mut sku = XMLElement::new("sku"); + sku.add_text(self.sku.clone())?; + inventory.add_child(sku)?; + inventory.add_child(self.quantity.to_xml()?)?; + + Ok(inventory) + } +} diff --git a/walmart-partner-api/src/api/us/item/mod.rs b/walmart-partner-api/src/api/us/item/mod.rs new file mode 100644 index 0000000..bbd997a --- /dev/null +++ b/walmart-partner-api/src/api/us/item/mod.rs @@ -0,0 +1,295 @@ +use serde::Serialize; + +pub use types::*; + +use crate::api::us::Client; +use crate::client::Method; +use crate::result::WalmartResult; + +mod types; + +#[derive(Debug, Clone, Default, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GetAllItemsQuery { + pub next_cursor: GetAllItemsCursor, + pub sku: Option, + pub limit: Option, + pub offset: Option, + pub lifecycle_status: Option, + pub published_status: Option, + pub variant_group_id: Option, +} + +#[derive(Debug, Clone, Serialize)] +pub struct GetAllItemsCursor(String); + +impl Default for GetAllItemsCursor { + fn default() -> Self { + GetAllItemsCursor("*".to_string()) + } +} + +impl From for GetAllItemsCursor { + fn from(s: T) -> Self { + GetAllItemsCursor(s.to_string()) + } +} + +impl Into for GetAllItemsCursor { + fn into(self) -> String { + self.0 + } +} + +impl Client { + pub async fn get_all_items(&self, query: GetAllItemsQuery) -> WalmartResult { + let qs = serde_urlencoded::to_string(&query)?; + let req = self.req_xml(Method::GET, "/v3/items", qs)?; + self.send(req).await?.res_xml().await + } + + pub async fn get_item(&self, sku: impl AsRef) -> WalmartResult { + let req = self.req_xml(Method::GET, &format!("/v3/items/{}", sku.as_ref()), "")?; + self.send(req).await?.res_xml().await + } + + pub async fn retire_item(&self, sku: impl AsRef) -> WalmartResult { + let req = self.req_xml(Method::DELETE, &format!("/v3/items/{}", sku.as_ref()), "")?; + self.send(req).await?.res_xml().await + } +} + +#[cfg(test)] +mod tests { + use mockito::{mock, Matcher}; + + use super::*; + + #[tokio::test] + async fn test_get_all_items() { + let client = crate::test_util::get_client_us(); + + let q = GetAllItemsQuery { + ..Default::default() + }; + + let body = r#" + + + + WALMART_US + 2792005 + 0RE1TWYTKBKH + 886859944807 + 00886859944807 + Carhartt Men's Rugged Vest + ["Home Page","Clothing","Mens Clothing","Mens Jackets & Outerwear","Mens Jackets & Outerwear"] + Outerwear Coats, Jackets & Vests + + USD + 59.99 + + UNPUBLISHED + + No shipping information was set up. Please re - ingest this item with a shipping price if you are submitting an over - ride. + You did not assign a tax code to your listing during item setup. Please re-ingest your item with a valid tax code. + + ACTIVE + + + WALMART_US + 4534200 + 0RFL011ILHS3 + 889192592087 + 00889192592087 + Carhartt Women's Slim-Fit Layton Skinny Leg Jean + ["UNNAV"] + Jeans + + USD + 49.99 + + UNPUBLISHED + + No shipping information was set up. Please re - ingest this item with a shipping price if you are submitting an over - ride. + You did not assign a tax code to your listing during item setup. Please re-ingest your item with a valid tax code. + + ACTIVE + + + WALMART_US + 4419464 + 0RFUYQBZLPP5 + 889169324734 + 00889169324734 + Marmot Women's Minimalist Jacket + ["Home Page","Clothing","Women","Womens Coats & Jackets Shop All"] + Outerwear Coats, Jackets & Vests + + USD + 189.0 + + UNPUBLISHED + + No shipping information was set up. Please re - ingest this item with a shipping price if you are submitting an over - ride. + You did not assign a tax code to your listing during item setup. Please re-ingest your item with a valid tax code. + + ACTIVE + + + WALMART_US + 2928108 + 0RH410PGF6E5 + 883956217001 + 00883956217001 + Olukai Women's Upena Sandal + ["Home Page","Clothing","Shoes","Womens Shoes","Womens Sandals & Flip-flops","Womens Sandals"] + Sandals + + USD + 89.95 + + UNPUBLISHED + + No shipping information was set up. Please re - ingest this item with a shipping price if you are submitting an over - ride. + You did not assign a tax code to your listing during item setup. Please re-ingest your item with a valid tax code. + + ACTIVE + + + WALMART_US + 4364805 + 0RXZMHYOF3YN + 686487307056 + 00686487307056 + Arcteryx Women's RHO LT Zip Neck Hooded + ["UNNAV"] + Sweatshirts & Hoodies + + USD + 139.0 + + SYSTEM_PROBLEM + + This item is prohibited because it violates one of our legal or compliance policies. For more details, create a case for Partner Support and include the code COMP. + + ACTIVE + + 11440 + AoE/GjBSWFpNSFlPRjNZTjBTRUxMRVJfT0ZGRVI1QUFDOTZFNzcyRjc0NkE1OTU5QjUxQTdGMUJFQTY5OQ== + + "#; + + let _m = mock("GET", "/v3/items") + .match_query(Matcher::AllOf(vec![Matcher::UrlEncoded( + "nextCursor".into(), + q.next_cursor.clone().into(), + )])) + .with_status(200) + .with_header("content-type", "application/xml") + .with_body(body) + .create(); + + let got = client.get_all_items(q).await.unwrap(); + assert_eq!(got.item_response.len(), 5); + let got = got.item_response[4].clone(); + let want = Item { + mart: Some("WALMART_US".to_string()), + sku: "4364805".to_string(), + wpid: Some("0RXZMHYOF3YN".to_string()), + upc: Some("686487307056".to_string()), + gtin: Some("00686487307056".to_string()), + product_name: Some("Arcteryx Women's RHO LT Zip Neck Hooded".to_string()), + shelf: Some("[\"UNNAV\"]".to_string()), + product_type: Some("Sweatshirts & Hoodies".to_string()), + price: Some(Price { + currency: "USD".to_string(), + amount: "139.0".to_string(), + }), + published_status: Some("SYSTEM_PROBLEM".to_string()), + }; + assert_eq!(got, want); + } + + #[tokio::test] + async fn test_get_item() { + let client = crate::test_util::get_client_us(); + + let sku = "foo"; + let body = r#" + + + + WALMART_US + setup_by_ref + 2VYRD2YCHYX1 + 05518319011365 + WL_Sim_verizon + Music + + USD + 12.00 + + UNPUBLISHED + + Your item is unpublished because the end date has passed. To republish your item, re-ingest the item with a new start and end date. + + RETIRED + + + "#; + + let _m = mock("GET", format!("/v3/items/{}", sku).as_str()) + .with_status(200) + .with_header("content-type", "application/xml") + .with_body(body) + .create(); + + let got = client.get_item(sku).await.unwrap(); + let want = GetItem { + item_response: Item { + mart: Some("WALMART_US".to_string()), + sku: "setup_by_ref".to_string(), + wpid: Some("2VYRD2YCHYX1".into()), + upc: None, + gtin: Some("05518319011365".into()), + product_name: Some("WL_Sim_verizon".into()), + shelf: None, + product_type: Some("Music".into()), + price: Some(Price { + currency: "USD".to_string(), + amount: "12.00".into(), + }), + published_status: Some("UNPUBLISHED".into()), + }, + }; + assert_eq!(got, want); + } + + #[tokio::test] + async fn test_retire_item() { + let client = crate::test_util::get_client_us(); + + let sku = "foo"; + let body = r#" + + + 34931712 + Thank you. Your item has been submitted for retirement from Walmart Catalog. Please note that it can take up to 48 hours for items to be retired from our catalog. + + "#; + + let _m = mock("DELETE", format!("/v3/items/{}", sku).as_str()) + .with_status(200) + .with_header("content-type", "application/xml") + .with_body(body) + .create(); + + let got = client.retire_item(sku).await.unwrap(); + let want = RetireItem { + sku: "34931712".to_string(), + message: Some("Thank you. Your item has been submitted for retirement from Walmart Catalog. Please note that it can take up to 48 hours for items to be retired from our catalog.".to_string()) + }; + assert_eq!(got, want); + } +} diff --git a/walmart-partner-api/src/api/us/item/types.rs b/walmart-partner-api/src/api/us/item/types.rs new file mode 100644 index 0000000..b8b5e23 --- /dev/null +++ b/walmart-partner-api/src/api/us/item/types.rs @@ -0,0 +1 @@ +pub use crate::shared::item::*; diff --git a/walmart-partner-api/src/api/us/mod.rs b/walmart-partner-api/src/api/us/mod.rs new file mode 100644 index 0000000..91eb36a --- /dev/null +++ b/walmart-partner-api/src/api/us/mod.rs @@ -0,0 +1,13 @@ +pub use client::Client; +pub use feed::*; +pub use inventory::*; +pub use item::*; +pub use order::*; +pub use report::*; + +mod client; +mod feed; +mod inventory; +mod item; +mod order; +mod report; diff --git a/walmart-partner-api/src/api/us/order/mod.rs b/walmart-partner-api/src/api/us/order/mod.rs new file mode 100644 index 0000000..1814f21 --- /dev/null +++ b/walmart-partner-api/src/api/us/order/mod.rs @@ -0,0 +1,655 @@ +use chrono::{DateTime, Utc}; +use serde::Serialize; +use xml_builder::XMLElement; + +pub use types::*; + +use crate::api::us::Client; +use crate::client::Method; +use crate::{WalmartResult, XmlSer}; + +mod types; + +#[derive(Debug, Clone, Default, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GetAllReleasedOrdersQuery { + pub limit: Option, + pub created_start_date: Option>, + pub created_end_date: Option>, + pub product_info: Option, + pub sku: Option, + pub customer_order_id: Option, + pub purchase_order_id: Option, + pub from_expected_ship_date: Option>, + pub to_expected_ship_date: Option>, + pub replacement_into: Option, + pub order_type: Option, + pub ship_node_type: Option, +} + +#[derive(Debug, Clone, Default, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GetAllOrdersQuery { + pub sku: Option, + pub customer_order_id: Option, + pub purchase_order_id: Option, + pub status: Option, + pub created_start_date: Option>, + pub created_end_date: Option>, + pub from_expected_ship_date: Option>, + pub to_expected_ship_date: Option>, + pub limit: Option, + pub product_info: Option, + pub ship_node_type: Option, + pub replacement_into: Option, + pub order_type: Option, +} + +#[derive(Debug, Clone, Default, Serialize)] +pub struct GetOrderQuery { + pub product_info: Option, + pub replacement_into: Option, +} + +#[derive(Debug, Clone, Serialize)] +pub struct ShipOrderLines { + pub order_lines: Vec, + pub process_mode: Option, +} + +#[derive(Debug, Clone, Serialize)] +pub struct ShipOrderLine { + pub line_number: String, + pub seller_order_id: String, + pub intent_to_cancel_override: Option, + /// If not provided the default values are used + pub status_quantity: Option, + pub asn: Option, + pub tracking_info: OrderLineTrackingInfo, +} + +impl Client { + pub async fn get_all_orders(&self, query: GetAllOrdersQuery) -> WalmartResult { + let qs = serde_urlencoded::to_string(query)?; + let req = self.req_xml(Method::GET, "/v3/orders", qs)?; + self.send(req).await?.res_xml().await + } + + pub async fn get_all_orders_by_next_cursor( + &self, + next_cursor: impl AsRef, + ) -> WalmartResult { + use url::form_urlencoded; + let req = self.req_xml( + Method::GET, + "/v3/orders", + form_urlencoded::parse((&next_cursor.as_ref()[1..]).as_bytes()) + .into_owned() + .collect::>(), + )?; + self.send(req).await?.res_xml().await + } + + pub async fn get_all_released_orders( + &self, + query: GetAllReleasedOrdersQuery, + ) -> WalmartResult { + let qs = serde_urlencoded::to_string(query)?; + let req = self.req_xml(Method::GET, "/v3/orders/released", qs)?; + self.send(req).await?.res_xml().await + } + + pub async fn get_order( + &self, + purchase_order_id: impl AsRef, + query: GetOrderQuery, + ) -> WalmartResult { + let qs = serde_urlencoded::to_string(query)?; + let req = self.req_xml( + Method::GET, + &format!("/v3/orders/{}", purchase_order_id.as_ref()), + qs, + )?; + self.send(req).await?.res_xml().await + } + + pub async fn ack_order(&self, purchase_order_id: impl AsRef) -> WalmartResult { + let req = self.req_xml( + Method::POST, + &format!("/v3/orders/{}/acknowledge", purchase_order_id.as_ref()), + (), + )?; + self.send(req).await?.res_xml().await + } + + pub async fn ship_order_lines( + &self, + purchase_order_id: impl AsRef, + input: ShipOrderLines, + ) -> WalmartResult { + let req = self + .req_xml( + Method::POST, + &format!("/v3/orders/{}/shipping", purchase_order_id.as_ref()), + (), + )? + .body_xml(input)?; + + self.send(req).await?.res_xml().await + } +} + +impl XmlSer for ShipOrderLines { + fn to_xml(&self) -> WalmartResult { + let mut order_shipment = XMLElement::new("orderShipment"); + order_shipment.add_attribute("xmlns", "http://walmart.com/mp/v3/orders"); + let mut order_lines = XMLElement::new("orderLines"); + + if let Some(process_mode_v) = self.process_mode.clone() { + let mut process_mode = XMLElement::new("processMode"); + process_mode.add_text(process_mode_v)?; + order_shipment.add_child(process_mode)?; + } + + for line in &self.order_lines { + order_lines.add_child(line.to_xml()?)?; + } + + order_shipment.add_child(order_lines)?; + Ok(order_shipment) + } +} + +impl XmlSer for ShipOrderLine { + fn to_xml(&self) -> WalmartResult { + let mut order_line = XMLElement::new("orderLine"); + let mut line_number = XMLElement::new("lineNumber"); + line_number.add_text(self.line_number.clone())?; + order_line.add_child(line_number)?; + + let mut seller_order_id = XMLElement::new("sellerOrderId"); + seller_order_id.add_text(self.seller_order_id.clone())?; + order_line.add_child(seller_order_id)?; + + if let Some(intent_to_cancel_override_v) = self.intent_to_cancel_override.clone() { + let mut intent_to_cancel_override = XMLElement::new("intentToCancelOverride"); + intent_to_cancel_override.add_text(intent_to_cancel_override_v.to_string())?; + order_line.add_child(intent_to_cancel_override)?; + } + + let mut order_line_statuses = XMLElement::new("orderLineStatuses"); + let mut order_line_status = XMLElement::new("orderLineStatus"); + { + let mut status = XMLElement::new("status"); + status.add_text("Shipped".to_string())?; + order_line_status.add_child(status)?; + + if let Some(asn) = self.asn.clone() { + order_line_status.add_child(asn.to_xml()?)?; + } + + order_line_status.add_child(self.status_quantity.to_xml()?)?; + order_line_status.add_child(self.tracking_info.to_xml()?)?; + } + order_line_statuses.add_child(order_line_status)?; + order_line.add_child(order_line_statuses)?; + + Ok(order_line) + } +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use mockito::{mock, Matcher}; + + use super::*; + + #[tokio::test] + async fn test_get_all_orders() { + let client = crate::test_util::get_client_us(); + + let _m = mock("GET", "/v3/orders") + .match_query(Matcher::AllOf(vec![Matcher::UrlEncoded( + "limit".into(), + "10".into(), + )])) + .with_status(200) + .with_header("content-type", "application/xml") + .with_body(include_str!("./test_get_all_orders_body.xml")) + .create(); + + let got = client + .get_all_orders(GetAllOrdersQuery { + limit: Some(10), + ..Default::default() + }) + .await + .unwrap(); + + assert_eq!(got.elements.order.len(), 10); + let order = got.elements.order[0].clone(); + assert_eq!(order.purchase_order_id, "1796277083022"); + let date = DateTime::::from_str("2019-09-14T13:09:31.000Z").unwrap(); + assert_eq!(order.order_date, date); + assert_eq!(order.shipping_info.method_code, "Value"); + assert_eq!( + order.shipping_info.postal_address.address1, + "3258BWarners rd".to_string() + ); + assert_eq!(order.order_lines.order_line.len(), 1); + let line = order.order_lines.order_line[0].clone(); + assert_eq!(line.line_number, 4.to_string()); + assert_eq!(line.charges.charge.len(), 1); + } + + #[tokio::test] + async fn test_get_all_released_orders() { + let client = crate::test_util::get_client_us(); + + let _m = mock("GET", "/v3/orders/released") + .match_query(Matcher::AllOf(vec![Matcher::UrlEncoded( + "limit".into(), + "10".into(), + )])) + .with_status(200) + .with_header("content-type", "application/xml") + .with_body(include_str!("./test_get_all_released_order_body.xml")) + .create(); + + let got = client + .get_all_released_orders(GetAllReleasedOrdersQuery { + limit: Some(10), + ..Default::default() + }) + .await + .unwrap(); + assert_eq!(got.meta.total_count, Some(78449)); + assert_eq!(got.elements.order.len(), 10); + } + + #[tokio::test] + async fn test_get_order() { + let client = crate::test_util::get_client_us(); + let body = r#" + + + 1815367951431 + 4372135469400 + 7D9F02968955480499C84C5871AD1401@relay.walmart.com + 2021-06-15T22:33:27.000Z + + 3143450432 + 2021-06-25T19:00:00.000Z + 2021-06-17T05:00:00.000Z + Value + + Donald Ashley + 6600 gazebo drive + Cedar hill + MO + 63016 + USA + RESIDENTIAL + + + + + 1 + + Mac Sports Heavy Duty Steel Double Decker Collapsible Yard Cart Wagon, Purple + YHY-FN25200B2 + + + + PRODUCT + ItemPrice + + USD + 5.00 + + + Tax1 + + USD + 0.34 + + + + + + EACH + 1 + + 2021-06-16T09:30:55.623Z + + + Shipped + + EACH + 1 + + + 2021-06-16T09:30:55.000Z + + USPS + + Value + 435678956435467 + https://www.walmart.com/tracking?tracking_id=435678956435467&order_id=1815367951431 + + + + + S2H + VALUE + 2021-06-25T19:00:00.000Z + + + + + SellerFulfilled + + + "#; + + let _m = mock("GET", "/v3/orders/1796330120075") + .with_status(200) + .with_header("content-type", "application/xml") + .with_body(body) + .create(); + + let got = client + .get_order("1796330120075", Default::default()) + .await + .unwrap(); + assert_eq!(got.purchase_order_id, "1815367951431"); + assert_eq!(got.customer_order_id, "4372135469400"); + assert_eq!(got.order_lines.order_line.len(), 1); + } + + #[tokio::test] + pub async fn test_ack_order() { + let client = crate::test_util::get_client_us(); + let body = r#" + + + 1796277083022 + 5281956426648 + 3A31739D8B0A45A1B23F7F8C81C8747F@relay.walmart.com + 2019-09-14T13:09:31.000Z + + 3155598681 + 2019-09-25T19:00:00.000Z + 2019-09-17T06:00:00.000Z + Value + + Kathryn Cole + 3258BWarners rd + Garage + Warners + NY + 13164 + USA + RESIDENTIAL + + + + + 4 + + Beba Bean Pee-pee Teepee Airplane - Blue - Laundry Bag + test1 + + + + PRODUCT + ItemPrice + + USD + 10.00 + + + Tax1 + + USD + 0.80 + + + + + + EACH + 1 + + 2019-09-17T20:45:56.000Z + + + Acknowledged + + EACH + 1 + + + + + S2H + VALUE + 2019-09-19T19:00:00.000Z + + + + + "#; + + let _m = mock("POST", "/v3/orders/2575193093772/acknowledge") + .with_status(200) + .with_header("content-type", "application/xml") + .with_body(body) + .create(); + + let got = client.ack_order("2575193093772").await.unwrap(); + assert_eq!(got.shipping_info.phone, "3155598681".to_string()); + assert_eq!(got.order_lines.order_line.len(), 1); + } + + #[tokio::test] + pub async fn test_ship_order_serialize() { + struct TestCase { + name: &'static str, + input: ShipOrderLines, + want: &'static str, + } + + let test_cases = vec![TestCase { + name: "full_input", + input: ShipOrderLines { + process_mode: Some("PARTIAL_UPDATE".to_string()), + order_lines: vec![ShipOrderLine { + line_number: "2".to_string(), + seller_order_id: "seller_id".to_string(), + intent_to_cancel_override: Some(true), + status_quantity: Some(ShipOrderLineStatusQuantity { + unit_of_measurement: Some("EACH".to_string()), + amount: Some(1.to_string()), + }), + asn: Some(ShipOrderLineAsn { + package_asn: "package_asn".to_string(), + pallet_asn: Some("pallet_asn".to_string()), + }), + tracking_info: OrderLineTrackingInfo { + ship_date_time: DateTime::::from_str("2016-06-27T05:30:15.000Z").unwrap(), + carrier_name: OrderLineTrackingCarrier { + other_carrier: None, + carrier: Some("FedEx".to_string()), + }, + method_code: "Standard".to_string(), + tracking_number: "12333634122".to_string(), + tracking_url: Some("http://www.fedex.com".to_string()), + }, + }], + }, + want: r#" + + PARTIAL_UPDATE + + + 2 + seller_id + true + + + Shipped + + package_asn + pallet_asn + + + EACH + 1 + + + 2016-06-27T05:30:15+00:00 + + FedEx + + Standard + 12333634122 + http://www.fedex.com + + + + + +"#, + }]; + + for tc in test_cases { + crate::test_util::assert_xml_eq(tc.input, tc.want, format!("test case: {}", tc.name)); + } + } + + #[tokio::test] + pub async fn test_ship_order_lines() { + let client = crate::test_util::get_client_us(); + let body = r#" + + + 2575193093772 + 4021603841173 + mgr@walmartlabs.com + 2016-05-11T22:55:28.000Z + + 6502248603 + 2016-05-20T17:00:00.000Z + 2016-05-16T17:00:00.000Z + Standard + + Joe Doe PGOMS + 860 W Cal Ave + Seat # 860C.2.176 + Sunnyvale + CA + 94086 + USA + RESIDENTIAL + + + + + 2 + + Garmin Refurbished nuvi 2595LMT 5 GPS w Lifetime Maps and Traffic + GRMN100201 + + + + PRODUCT + ItemPrice + + USD + 124.98 + + + Tax1 + + USD + 10.93 + + + + + + EACH + 1 + + 2016-06-03T23:44:41.000Z + + + Shipped + + EACH + 1 + + + 2016-06-27T05:30:15.000Z + + FedEx + + Standard + 12333634122 + http://www.fedex.com + + + + + + + "#; + + let _m = mock("POST", "/v3/orders/2575193093772/shipping") + .with_status(200) + .with_header("content-type", "application/xml") + .match_header("content-type", "application/xml") + .with_body(body) + .create(); + + let input = ShipOrderLines { + order_lines: vec![ShipOrderLine { + line_number: "2".to_string(), + seller_order_id: "seller_order_id".to_string(), + intent_to_cancel_override: Some(true), + status_quantity: Some(ShipOrderLineStatusQuantity { + unit_of_measurement: Some("EA".to_string()), + amount: Some("2".to_string()), + }), + asn: Some(ShipOrderLineAsn { + package_asn: "package_asn".to_string(), + pallet_asn: Some("pallet_asn".to_string()), + }), + tracking_info: OrderLineTrackingInfo { + ship_date_time: DateTime::::from_str("2016-06-27T05:30:15+00:00").unwrap(), + carrier_name: OrderLineTrackingCarrier { + other_carrier: None, + carrier: Some("FedEx".to_string()), + }, + method_code: "Standard".to_string(), + tracking_number: "12333634122".to_string(), + tracking_url: Some("http://www.fedex.com".to_string()), + }, + }], + process_mode: Some("PARTIAL_UPDATE".to_string()), + }; + + let got = client + .ship_order_lines("2575193093772", input) + .await + .unwrap(); + assert_eq!(got.purchase_order_id, "2575193093772"); + } +} diff --git a/walmart-partner-api/src/api/us/order/test_get_all_orders_body.xml b/walmart-partner-api/src/api/us/order/test_get_all_orders_body.xml new file mode 100644 index 0000000..7cb5854 --- /dev/null +++ b/walmart-partner-api/src/api/us/order/test_get_all_orders_body.xml @@ -0,0 +1,701 @@ + + + + 31 + 10 + ?limit=10&hasMoreElements=true&soIndex=31&poIndex=10&partnerId=100009&sellerId=8&createdStartDate=2013-08-16&createdEndDate=2019-09-17T18:47:03.703Z + + + + 1796277083022 + 5281956426648 + 3A31739D8B0A45A1B23F7F8C81C8747F@relay.walmart.com + REPLACEMENT + 1234567891234 + 2019-09-14T13:09:31.000Z + + 3155598681 + 2019-09-25T19:00:00.000Z + 2019-09-17T06:00:00.000Z + Value + + Kathryn Cole + 3258BWarners rd + Garage + Warners + NY + 13164 + USA + RESIDENTIAL + + + + + 4 + + Beba Bean Pee-pee Teepee Airplane - Blue - Laundry Bag + test1 + + + + PRODUCT + ItemPrice + + USD + 10.00 + + + Tax1 + + USD + 0.80 + + + + + + EACH + 1 + + 2019-09-14T13:10:47.000Z + + + Created + + EACH + 1 + + + + + S2H + VALUE + 2019-09-19T19:00:00.000Z + + + + + + 3796235970012 + 5241952426446 + 630083BD274E4FB2968D3166C8336151@relay.walmart.com + REGULAR + 2019-09-10T19:59:53.000Z + + 4088592715 + 2019-09-19T19:00:00.000Z + 2019-09-11T06:00:00.000Z + Value + + Steffen Matt + 1830 Mason St + San Francisco + CA + 94133 + USA + RESIDENTIAL + + + + + 1 + + Qwik Time QT5 Quartz Metronome + TEST-02 + + + + PRODUCT + ItemPrice + + USD + 0.00 + + + Tax1 + + USD + 0.00 + + + + + + EACH + 1 + + 2019-09-15T06:00:47.000Z + + + Cancelled + + EACH + 1 + + + + + S2H + VALUE + 2019-09-13T19:00:00.000Z + + + + + + 1796235744850 + 5231951859044 + 04BB1243498A43E9ADC8F1C9EAB622DC@relay.walmart.com + REPLACEMENT + 1234567891235 + 2019-09-10T00:34:33.000Z + + 7732831392 + 2019-09-19T19:00:00.000Z + 2019-09-11T06:00:00.000Z + Value + + Dorothy Kelly + 5831 N Kilpatrick Ave + Chicago + IL + 60646 + USA + RESIDENTIAL + + + + + 1 + + Qwik Time QT5 Quartz Metronome + TEST-02 + + + + PRODUCT + ItemPrice + + USD + 0.00 + + + Tax1 + + USD + 0.00 + + + + + + EACH + 1 + + 2019-09-15T06:01:21.000Z + + + Cancelled + + EACH + 1 + + + + + S2H + VALUE + 2019-09-13T19:00:00.000Z + + + + + + 2792525175246 + 5221950975717 + 9CE34D060D4E4A8F9504B70A564E32EB@relay.walmart.com + REPLACEMENT + 1234567891236 + 2019-09-08T23:20:18.000Z + + 2564575354 + 2019-09-18T19:00:00.000Z + 2019-09-10T06:00:00.000Z + Value + + Donna Becker + 6555 meandering way + Lakewood ranch + FL + 34202 + USA + RESIDENTIAL + + + + + 1 + + Qwik Time QT5 Quartz Metronome + TEST-02 + + + + PRODUCT + ItemPrice + + USD + 0.00 + + + + + EACH + 1 + + 2019-09-14T06:00:30.000Z + + + Cancelled + + EACH + 1 + + + + + S2H + VALUE + 2019-09-12T19:00:00.000Z + + + + + + 4792514745205 + 5211999883559 + FD63F38FD4ED41C6ACAC5A5988888E2B@relay.walmart.com + REPLACEMENT + 1234567891237 + 2019-09-07T13:30:35.000Z + + 3053037923 + 2019-09-18T19:00:00.000Z + 2019-09-10T06:00:00.000Z + Value + + Vivian Katzaroff + 945 Bay Dr apt 5 + Miami Beach + FL + 33141 + USA + RESIDENTIAL + + + + + 2 + + Beba Bean Pee-pee Teepee Airplane - Blue - Laundry Bag + test1 + + + + PRODUCT + ItemPrice + + USD + 0.00 + + + + + EACH + 1 + + 2019-09-14T06:06:04.000Z + + + Cancelled + + EACH + 1 + + + + + S2H + VALUE + 2019-09-12T19:00:00.000Z + + + + + + 3796091528503 + 5101992777190 + 630083BD274E4FB2968D3166C8336151@relay.walmart.com + REPLACEMENT + 1234567891238 + 2019-08-27T20:18:31.000Z + + 4088592715 + 2019-09-06T19:00:00.000Z + 2019-08-28T06:00:00.000Z + Value + + Steffen Matt + 1830 Mason St + San Francisco + CA + 94133 + USA + RESIDENTIAL + + + + + 1 + + Qwik Time QT5 Quartz Metronome + TEST-02 + + + + PRODUCT + ItemPrice + + USD + 0.00 + + + Tax1 + + USD + 0.00 + + + + + + EACH + 1 + + 2019-09-01T06:01:05.000Z + + + Cancelled + + EACH + 1 + + + + + S2H + VALUE + 2019-08-30T19:00:00.000Z + + + + + + 3796050123784 + 5061989889122 + 630083BD274E4FB2968D3166C8336151@relay.walmart.com + REPLACEMENT + 1234567891239 + 2019-08-23T22:54:06.000Z + + 4088592715 + 2019-09-05T19:00:00.000Z + 2019-08-27T06:00:00.000Z + Value + + Steffen Matt + 1830 Mason St + San Francisco + CA + 94133 + USA + RESIDENTIAL + + + + + 1 + + Qwik Time QT5 Quartz Metronome + TEST-02 + + + + PRODUCT + ItemPrice + + USD + 9.99 + + + Tax1 + + USD + 0.85 + + + + + + EACH + 1 + + 2019-08-27T19:24:49.000Z + + + Shipped + + EACH + 1 + + + 2019-08-27T19:12:49.000Z + + USPS Marketplace + + Value + 9400100000000000000000 + http://walmart.narvar.com/walmart/tracking/usps?&type=MP&seller_id=8&promise_date=09/05/2019&dzip=94133&tracking_numbers=9400100000000000000000 + + + + + S2H + VALUE + 2019-08-29T19:00:00.000Z + + + + + + 2792360062965 + 5061989884505 + 630083BD274E4FB2968D3166C8336151@relay.walmart.com + REGULAR + 2019-08-23T22:44:59.000Z + + 4088592715 + 2019-09-05T19:00:00.000Z + 2019-08-27T06:00:00.000Z + Value + + Steffen Matt + 1830 Mason St + San Francisco + CA + 94133 + USA + RESIDENTIAL + + + + + 1 + + Qwik Time QT5 Quartz Metronome + TEST-02 + + + + PRODUCT + ItemPrice + + USD + 9.99 + + + Tax1 + + USD + 0.85 + + + + + + EACH + 1 + + 2019-08-23T23:01:16.000Z + + + Shipped + + EACH + 1 + + + 2019-08-23T22:51:40.000Z + + USPS + + Value + 9400100000000000000000 + http://walmart.narvar.com/walmart/tracking/usps?&type=MP&seller_id=8&promise_date=09/05/2019&dzip=94133&tracking_numbers=9400100000000000000000 + + + + + S2H + VALUE + 2019-08-29T19:00:00.000Z + + + + + + 2792338900105 + 5021986553201 + 6941EFC46C6541D0B8FE5914873EF869@relay.walmart.com + REGULAR + 2019-08-20T00:45:56.000Z + + 4089130712 + 2019-08-29T19:00:00.000Z + 2019-08-21T06:00:00.000Z + Value + + Sunitha Murthy + 9400 w parmer ln + Apt # 1027 + Austin + TX + 78717 + USA + RESIDENTIAL + + + + + 1 + + Qwik Time QT5 Quartz Metronome + TEST-02 + + + + PRODUCT + ItemPrice + + USD + 9.99 + + + Tax1 + + USD + 0.62 + + + + + + EACH + 1 + + 2019-08-20T20:13:13.000Z + + + Shipped + + EACH + 1 + + + 2019-08-20T20:10:24.000Z + + UPS + + Value + 123456789 + http://walmart.narvar.com/walmart/tracking/ups?&type=MP&seller_id=8&promise_date=08/29/2019&dzip=78717&tracking_numbers=123456789 + + + + + S2H + VALUE + 2019-08-23T19:00:00.000Z + + + + + + 2792163543930 + 4851974348317 + 8244CABDE158461D9BF57F92BDD147D6@relay.walmart.com + REGULAR + 2019-08-03T01:15:37.000Z + + 5019411012 + 2019-08-15T19:00:00.000Z + 2019-08-07T04:00:00.000Z + Value + + Debbie Chandler + 1012 Campground Road + Cabot + AR + 72023 + USA + RESIDENTIAL + + + + + 3 + + Motorola TurboPower 15 USB-C / Type C car charger - Turbo Power for Moto Z, Z2, Z3, Z4, X4, G7, G6, G6 Plus [Not for G6 Play] (Retail Box) + AC004 + + + + PRODUCT + ItemPrice + + USD + 0.00 + + + Tax1 + + USD + 0.00 + + + + + + EACH + 1 + + 2019-08-11T04:00:44.000Z + + + Cancelled + + EACH + 1 + + + + + S2H + VALUE + 2019-08-09T19:00:00.000Z + + + + + + \ No newline at end of file diff --git a/walmart-partner-api/src/api/us/order/test_get_all_released_order_body.xml b/walmart-partner-api/src/api/us/order/test_get_all_released_order_body.xml new file mode 100644 index 0000000..6763b95 --- /dev/null +++ b/walmart-partner-api/src/api/us/order/test_get_all_released_order_body.xml @@ -0,0 +1,778 @@ + + + + 78449 + 10 + ?limit=10&hasMoreElements=true&soIndex=78449&poIndex=10&partnerId=100009&sellerId=8&status=Created&createdStartDate=2013-08-16&createdEndDate=2019-10-24T17:41:15.701Z + + + + 4792982839409 + 5681962097195 + 9336DEB95193429EA032F07770C3E48A@relay.walmart.com + REPLACEMENT + 1234567891234 + 2019-10-24T07:52:30.000Z + + 7992381678 + 2019-10-29T19:00:00.000Z + 2019-10-26T06:00:00.000Z + Express + + SFirst1340778269137n SLast1340778269137n + 13227 City Square Dr + Jacksonville + CA + 12901 + USA + RESIDENTIAL + + + + + 3 + + Dummystresstest_MP_Home_29 + StressTestHome_29 + + + + PRODUCT + ItemPrice + + USD + 99.00 + + + Tax1 + + USD + 7.92 + + + + + SHIPPING + Shipping + + USD + 60.00 + + + + + EACH + 1 + + 2019-10-24T07:55:48.000Z + + + Created + + EACH + 1 + + + + + S2H + EXPEDITED + 2019-10-28T19:00:00.000Z + + + + + + + 2792982839545 + 5681963507621 + 608603150B4049338B773566484C0591@relay.walmart.com + REGULAR + 2019-10-24T07:52:19.000Z + + 7998673792 + 2019-10-29T19:00:00.000Z + 2019-10-26T06:00:00.000Z + Express + + SFirst1340778269137n SLast1340778269137n + 2200 Wheatsheaf Ln + Philadelphia + CA + 28078 + USA + RESIDENTIAL + + + + + 11 + + Dummystresstest_MP_Home_55 + StressTestHome_55 + + + + PRODUCT + ItemPrice + + USD + 99.00 + + + + SHIPPING + Shipping + + USD + 60.00 + + + + + EACH + 1 + + 2019-10-24T07:56:03.000Z + + + Created + + EACH + 1 + + + + + S2H + EXPEDITED + 2019-10-28T19:00:00.000Z + + + + + + + 2792982839414 + 5681962895313 + BC058103AF6C400E99FDEC95795F16AF@relay.walmart.com + REPLACEMENT + 1234567891235 + 2019-10-24T07:52:18.000Z + + 7998211844 + 2019-10-29T19:00:00.000Z + 2019-10-26T06:00:00.000Z + Express + + SFirst1340778912575r SLast1340778912575r + 444 Castro St + Mountain View + CA + 94041 + USA + RESIDENTIAL + + + + + 4 + + Dummystresstest_MP_Home_13 + StressTestHome_13 + + + + PRODUCT + ItemPrice + + USD + 99.00 + + + Tax1 + + USD + 8.89 + + + + + SHIPPING + Shipping + + USD + 60.00 + + + + + EACH + 1 + + 2019-10-24T07:55:47.000Z + + + Created + + EACH + 1 + + + + + S2H + EXPEDITED + 2019-10-28T19:00:00.000Z + + + + + + + 4792982839305 + 5681962393943 + B100013EF5B6415E97F13A1E2F76D33E@relay.walmart.com + REPLACEMENT + 1234567891236 + 2019-10-24T07:52:17.000Z + + 7997281822 + 2019-10-29T19:00:00.000Z + 2019-10-26T06:00:00.000Z + Express + + SFirst1340778914882k SLast1340778914882k + 444 Castro St + Mountain View + CA + 94041 + USA + RESIDENTIAL + + + + + 4 + + Dummystresstest_MP_Home_31 + StressTestHome_31 + + + + PRODUCT + ItemPrice + + USD + 99.00 + + + Tax1 + + USD + 8.90 + + + + + SHIPPING + Shipping + + USD + 60.00 + + + + + EACH + 1 + + 2019-10-24T07:55:46.000Z + + + Created + + EACH + 1 + + + + + S2H + EXPEDITED + 2019-10-28T19:00:00.000Z + + + + + + + 4792982839157 + 5681962094947 + F26C0523EB674390ABD87D8148A9CF9A@relay.walmart.com + REPLACEMENT + 1234567891237 + 2019-10-24T07:52:16.000Z + + 7994363541 + 2019-10-29T19:00:00.000Z + 2019-10-26T06:00:00.000Z + Express + + SFirst1340778269137n SLast1340778269137nt + 14507 Plank Rd + Baker + CA + 70714 + USA + RESIDENTIAL + + + + + 3 + + Dummystresstest_MP_Home_55 + StressTestHome_55 + + + + PRODUCT + ItemPrice + + USD + 99.00 + + + + SHIPPING + Shipping + + USD + 60.00 + + + + + EACH + 1 + + 2019-10-24T07:55:32.000Z + + + Created + + EACH + 1 + + + + + S2H + EXPEDITED + 2019-10-28T19:00:00.000Z + + + + + + + 4792982839565 + 5681963200599 + B442C0D25766479DA1D98D0BEE18AA4E@relay.walmart.com + REPLACEMENT + 1234567891238 + 2019-10-24T07:52:16.000Z + + 7997678284 + 2019-10-29T19:00:00.000Z + 2019-10-26T06:00:00.000Z + Express + + SFirst1340778269137n SLast1340778269137nt + 925 Keyser Ave + Natchitoches + CA + 43311 + USA + RESIDENTIAL + + + + + 3 + + Dummystresstest_MP_Home_55 + StressTestHome_55 + + + + PRODUCT + ItemPrice + + USD + 99.00 + + + Tax1 + + USD + 7.18 + + + + + SHIPPING + Shipping + + USD + 60.00 + + + + + EACH + 1 + + 2019-10-24T07:56:05.000Z + + + Created + + EACH + 1 + + + + + S2H + EXPEDITED + 2019-10-28T19:00:00.000Z + + + + + + + 1796673089007 + 5681962798220 + 7DF73CDD9E134A8C9E5096111698FE5B@relay.walmart.com + REPLACEMENT + 1234567891239 + 2019-10-24T07:52:15.000Z + + 7993811707 + 2019-10-29T19:00:00.000Z + 2019-10-26T06:00:00.000Z + Express + + SFirst1340778269137n SLast1340778269137n + 2765 10Th Ave N + Palm Springs + CA + 27612 + USA + RESIDENTIAL + + + + + 2 + + Dummystresstest_MP_Home_29 + StressTestHome_29 + + + + PRODUCT + ItemPrice + + USD + 99.00 + + + + SHIPPING + Shipping + + USD + 55.00 + + + + + EACH + 1 + + 2019-10-24T07:56:20.000Z + + + Created + + EACH + 1 + + + + + S2H + EXPEDITED + 2019-10-28T19:00:00.000Z + + + + 13 + + Dummystresstest_MP_Home_29 + StressTestHome_29 + + + + PRODUCT + ItemPrice + + USD + 99.00 + + + + SHIPPING + Shipping + + USD + 55.00 + + + + + EACH + 1 + + 2019-10-24T07:56:20.000Z + + + Created + + EACH + 1 + + + + + S2H + EXPEDITED + 2019-10-28T19:00:00.000Z + + + + + + + 3796673088300 + 5681962299170 + A78E13850B234D4599FD6B42DB0EFB19@relay.walmart.com + REGULAR + 2019-10-24T07:52:15.000Z + + 7999539847 + 2019-10-29T19:00:00.000Z + 2019-10-26T06:00:00.000Z + Express + + SFirst1340778269137n SLast1340778269137nt + 4542 Kenowa Ave Sw + Grandville + CA + 33155 + USA + RESIDENTIAL + + + + + 3 + + Dummystresstest_MP_Home_55 + StressTestHome_55 + + + + PRODUCT + ItemPrice + + USD + 99.00 + + + + SHIPPING + Shipping + + USD + 60.00 + + + + + EACH + 1 + + 2019-10-24T07:55:16.000Z + + + Created + + EACH + 1 + + + + + S2H + EXPEDITED + 2019-10-28T19:00:00.000Z + + + + + + + 1796673088779 + 5681963402652 + B7F3DF65F7DB47CD9F4108B5C2BA89BF@relay.walmart.com + REGULAR + 2019-10-24T07:52:15.000Z + + 7995572470 + 2019-10-29T19:00:00.000Z + 2019-10-26T06:00:00.000Z + Express + + SFirst1340778269137n SLast1340778269137n + 1450 Johns Lake Rd + Clermont + CA + 30529 + USA + RESIDENTIAL + + + + + 3 + + Dummystresstest_MP_Home_13 + StressTestHome_13 + + + + PRODUCT + ItemPrice + + USD + 99.00 + + + + SHIPPING + Shipping + + USD + 60.00 + + + + + EACH + 1 + + 2019-10-24T07:55:48.000Z + + + Created + + EACH + 1 + + + + + S2H + EXPEDITED + 2019-10-28T19:00:00.000Z + + + + + + + 4792982839536 + 5681963403079 + C087D231D7F846C3BBE04B1AC869F3FB@relay.walmart.com + REGULAR + 2019-10-24T07:52:15.000Z + + 7998487320 + 2019-10-29T19:00:00.000Z + 2019-10-26T06:00:00.000Z + Express + + SFirst1340778269137n SLast1340778269137n + 100 Mcginnis Dr + Wayne + CA + 33323 + USA + RESIDENTIAL + + + + + 4 + + Dummystresstest_MP_Home_32 + StressTestHome_32 + + + + PRODUCT + ItemPrice + + USD + 99.00 + + + + SHIPPING + Shipping + + USD + 60.00 + + + + + EACH + 1 + + 2019-10-24T07:56:02.000Z + + + Created + + EACH + 1 + + + + + S2H + EXPEDITED + 2019-10-28T19:00:00.000Z + + + + + + + \ No newline at end of file diff --git a/walmart-partner-api/src/api/us/order/types.rs b/walmart-partner-api/src/api/us/order/types.rs new file mode 100644 index 0000000..f336418 --- /dev/null +++ b/walmart-partner-api/src/api/us/order/types.rs @@ -0,0 +1,101 @@ +use chrono::{DateTime, Utc}; + +pub use crate::shared::order::*; + +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] +pub struct OrderList { + #[serde(rename = "meta")] + pub meta: OrderListMeta, + #[serde(rename = "elements")] + pub elements: Orders, +} + +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] +pub struct Orders { + /// Purchase Order List + #[serde(rename = "order")] + #[serde(default)] + pub order: Vec, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct Order { + /// A unique ID associated with the seller's purchase order + #[serde(rename = "purchaseOrderId")] + pub purchase_order_id: String, + /// A unique ID associated with the sales order for specified customer + #[serde(rename = "customerOrderId")] + pub customer_order_id: String, + /// The email address of the customer for the sales order + #[serde(rename = "customerEmailId")] + pub customer_email_id: String, + /// Specifies if the order is a regular order or replacement order. Possible values are REGULAR or REPLACEMENT. Provided in response only if query parameter replacementInfo=true. + #[serde(rename = "orderType", skip_serializing_if = "Option::is_none")] + pub order_type: Option, + /// customer order ID of the original customer order on which the replacement is created. + #[serde( + rename = "originalCustomerOrderID", + skip_serializing_if = "Option::is_none" + )] + pub original_customer_order_id: Option, + /// The date the customer submitted the sales order + #[serde(rename = "orderDate")] + pub order_date: DateTime, + /// Unique ID associated with the specified buyer + #[serde(rename = "buyerId", skip_serializing_if = "Option::is_none")] + pub buyer_id: Option, + /// Mart information + #[serde(rename = "mart", skip_serializing_if = "Option::is_none")] + pub mart: Option, + /// Indicates a guest customer + #[serde(rename = "isGuest", skip_serializing_if = "Option::is_none")] + pub is_guest: Option, + #[serde(rename = "shippingInfo")] + pub shipping_info: ShippingInfo, + #[serde(rename = "orderLines")] + pub order_lines: OrderLines, + #[serde(rename = "shipNode")] + #[serde(default)] + pub ship_node: ShipNode, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct OrderLines { + /// A list of order lines in the order + #[serde(rename = "orderLine")] + #[serde(default)] + pub order_line: Vec, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct OrderLine { + /// The line number associated with the details for each individual item in the purchase order + #[serde(rename = "lineNumber")] + pub line_number: String, + #[serde(rename = "item")] + pub item: OrderLineItem, + #[serde(rename = "charges")] + pub charges: OrderLineCharges, + #[serde(rename = "orderLineQuantity")] + pub order_line_quantity: OrderLineStatusQuantity, + /// The date shown on the recent order status + #[serde(rename = "statusDate")] + pub status_date: DateTime, + #[serde(rename = "orderLineStatuses")] + pub order_line_statuses: OrderLineStatuses, + #[serde(rename = "refund", skip_serializing_if = "Option::is_none")] + pub refund: Option, + #[serde( + rename = "originalCarrierMethod", + skip_serializing_if = "Option::is_none" + )] + pub original_carrier_method: Option, + #[serde(rename = "referenceLineId", skip_serializing_if = "Option::is_none")] + pub reference_line_id: Option, + #[serde(rename = "fulfillment", skip_serializing_if = "Option::is_none")] + pub fulfillment: Option, + #[serde(rename = "intentToCancel", skip_serializing_if = "Option::is_none")] + pub intent_to_cancel: Option, + #[serde(rename = "configId", skip_serializing_if = "Option::is_none")] + pub config_id: Option, +} diff --git a/walmart-partner-api/src/api/us/report/mod.rs b/walmart-partner-api/src/api/us/report/mod.rs new file mode 100644 index 0000000..158d11a --- /dev/null +++ b/walmart-partner-api/src/api/us/report/mod.rs @@ -0,0 +1,74 @@ +use chrono::{DateTime, Utc}; +use serde::Serialize; + +pub use types::*; + +use crate::api::us::Client; +use crate::client::Method; +use crate::result::WalmartResult; + +mod types; + +#[derive(Debug, Clone, Default, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GetAllReportRequestsQuery { + pub report_type: String, + pub report_version: Option, + pub request_status: Option, + pub request_submission_start_date: Option>, + pub request_submission_end_date: Option>, +} + +#[derive(Debug, Clone, Default, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CreateReportRequestQuery { + pub report_type: String, + pub report_version: String, +} + +impl Client { + pub async fn get_all_report_requests( + &self, + query: GetAllReportRequestsQuery, + ) -> WalmartResult { + let qs = serde_urlencoded::to_string(&query)?; + let req = self.req_json(Method::GET, "/v3/reports/reportRequests", qs)?; + self.send(req).await?.res_json().await + } + + pub async fn get_report_request( + &self, + report_request_id: impl AsRef, + ) -> WalmartResult { + let req = self.req_json( + Method::GET, + &format!("/v3/reports/reportRequests/{}", report_request_id.as_ref()), + (), + )?; + self.send(req).await?.res_json().await + } + + pub async fn create_report_request( + &self, + query: CreateReportRequestQuery, + input: CreateReportRequestInput, + ) -> WalmartResult { + let qs = serde_urlencoded::to_string(&query)?; + let req = self + .req_json(Method::POST, "/v3/reports/reportRequests", qs)? + .body_json(&input)?; + self.send(req).await?.res_json().await + } + + pub async fn get_report_download( + &self, + report_request_id: impl AsRef, + ) -> WalmartResult { + let req = self.req_json( + Method::GET, + "/v3/reports/downloadReport", + vec![("requestId", report_request_id.as_ref())], + )?; + self.send(req).await?.res_json().await + } +} diff --git a/walmart-partner-api/src/api/us/report/types.rs b/walmart-partner-api/src/api/us/report/types.rs new file mode 100644 index 0000000..73845f6 --- /dev/null +++ b/walmart-partner-api/src/api/us/report/types.rs @@ -0,0 +1,99 @@ +use chrono::{DateTime, Utc}; + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct ReportRequestList { + /// Current page + #[serde(rename = "page", skip_serializing_if = "Option::is_none")] + pub page: Option, + /// Number of records fetched. + #[serde(rename = "totalCount", skip_serializing_if = "Option::is_none")] + pub total_count: Option, + /// Number of records to be returned. Default is 10. + #[serde(rename = "limit", skip_serializing_if = "Option::is_none")] + pub limit: Option, + /// Used for pagination when more than specified limit (or default 10) records are found. Use this param for next API call. Just have to use this value as query param. Need to pass only the cursor value and not the initial API call query params. For e.g. if ['nextCursor'='reportType=ITEM&requestStatus=ERROR&requestSubmissionStartDate=2021-08-20T10:52:59Z&requestSubmissionEndDate=2021-09-14T10:52:59Z&page=2&limit=1'] then subsequent call to will be [marketplace.walmartapis.com/v3/reports/reportRequests?reportType=ITEM&requestStatus=ERROR&requestSubmissionStartDate=2021-08-20T10:52:59Z&requestSubmissionEndDate=2021-09-14T10:52:59Z&page=2&limit=1]. Just have to use nextCursor value instead of query params + #[serde(rename = "nextCursor", skip_serializing_if = "Option::is_none")] + pub next_cursor: Option, + /// List of requests + #[serde(rename = "requests", skip_serializing_if = "Option::is_none")] + pub requests: Option>, +} + +#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] +pub struct CreateReportRequestInput { + /// Columns to exclude from report + #[serde(rename = "excludeColumns", skip_serializing_if = "Option::is_none")] + pub exclude_columns: Option>, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct ReportRequest { + /// Automated ID generated by system that uniquely identifies the report request. + #[serde(rename = "requestId")] + pub request_id: String, + /// Status of report request. Possible values are RECEIVED, INPROGRESS, READY, ERROR. + #[serde(rename = "requestStatus")] + pub request_status: ReportRequestStatus, + /// Date and time on which the report request is submitted. + #[serde( + rename = "requestSubmissionDate", + skip_serializing_if = "Option::is_none" + )] + pub request_submission_date: Option>, + /// Type of report for which the request is created. Example, ITEM for Item Report. + #[serde(rename = "reportType")] + pub report_type: String, + /// Version of report for which the request is created. Example, v1. + #[serde(rename = "reportVersion", skip_serializing_if = "Option::is_none")] + pub report_version: Option, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct ReportDownload { + /// URL to be used to download the report. + #[serde(rename = "downloadURL", skip_serializing_if = "Option::is_none")] + pub download_url: Option, + /// Time till when the URL to download the report will be valid. + #[serde( + rename = "downloadURLExpirationTime", + skip_serializing_if = "Option::is_none" + )] + pub download_url_expiration_time: Option, + /// Automated ID generated by system that uniquely identifies the report request. + #[serde(rename = "requestId")] + pub request_id: String, + /// Status of report request. Possible values are RECEIVED, INPROGRESS, READY, ERROR. + #[serde(rename = "requestStatus")] + pub request_status: ReportRequestStatus, + /// Date and time on which the report request is submitted. + #[serde( + rename = "requestSubmissionDate", + skip_serializing_if = "Option::is_none" + )] + pub request_submission_date: Option>, + /// Type of report for which the request is created. Example, ITEM for Item Report. + #[serde(rename = "reportType")] + pub report_type: String, + /// Version of report for which the request is created. Example, v1. + #[serde(rename = "reportVersion", skip_serializing_if = "Option::is_none")] + pub report_version: Option, + /// Date and time on which the report is generated. Attribute is available only if report is generated. + #[serde( + rename = "reportGenerationDate", + skip_serializing_if = "Option::is_none" + )] + pub report_generation_date: Option>, +} + +/// Status of report request. Possible values are RECEIVED, INPROGRESS, READY, ERROR. +#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)] +pub enum ReportRequestStatus { + #[serde(rename = "RECEIVED")] + Received, + #[serde(rename = "INPROGRESS")] + Inprogress, + #[serde(rename = "READY")] + Ready, + #[serde(rename = "ERROR")] + Error, +} diff --git a/walmart-partner-api/src/client.rs b/walmart-partner-api/src/client.rs index b23d940..2c2fa52 100644 --- a/walmart-partner-api/src/client.rs +++ b/walmart-partner-api/src/client.rs @@ -1,77 +1,18 @@ -use crate::result::*; -use crate::sign::Signature; -use chrono::Utc; -use rand::{thread_rng, Rng}; -use reqwest; -use reqwest::header::HeaderMap; -pub use reqwest::{Method, Request, RequestBuilder, Response, StatusCode, Url}; +use std::collections::HashMap; use std::sync::RwLock; use std::time::{Duration, Instant}; -const BASE_URL: &'static str = "https://marketplace.walmartapis.com"; - -#[derive(Debug, Clone, Copy)] -pub enum WalmartMarketplace { - USA, - Canada, -} - -pub enum WalmartCredential { - TokenApi { - client_id: String, - client_secret: String, - }, - Signature { - channel_type: String, - consumer_id: String, - private_key: String, - }, -} - -enum AuthState { - TokenApi { - client_id: String, - client_secret: String, - bearer_token: RwLock>, - }, - Signature { - channel_type: String, - signature: Signature, - }, -} - -struct BearerToken { - access_token: String, - expires_at: Instant, -} - -pub trait ExtendUrlParams { - fn extend_url_params(self, url: &mut Url); -} - -impl ExtendUrlParams for () { - fn extend_url_params(self, _: &mut Url) {} -} - -impl<'a> ExtendUrlParams for &'a str { - fn extend_url_params(self, url: &mut Url) { - url.set_query(Some(self)); - } -} +use chrono::Utc; +use reqwest; +use reqwest::header::{HeaderMap, HeaderName, HeaderValue}; +pub use reqwest::{Method, Request, RequestBuilder, Response, StatusCode, Url}; +use serde::de::DeserializeOwned; +use serde::{Deserialize, Serialize}; -impl<'a> ExtendUrlParams for String { - fn extend_url_params(self, url: &mut Url) { - if !self.is_empty() { - url.set_query(Some(self.as_ref())); - } - } -} +use crate::result::*; +use crate::sign::Signature; -impl, T2: AsRef> ExtendUrlParams for Vec<(T1, T2)> { - fn extend_url_params(self, url: &mut Url) { - url.query_pairs_mut().extend_pairs(self); - } -} +const BASE_URL: &'static str = "https://marketplace.walmartapis.com"; pub struct Client { marketplace: WalmartMarketplace, @@ -118,41 +59,51 @@ impl Client { http, }) } +} - fn request

(&self, method: Method, path: &str, params: P) -> WalmartResult +impl Client { + #[cfg(test)] + pub fn set_base_url(&mut self, base_url: &str) { + self.base_url = Url::parse(base_url).unwrap(); + } + + pub fn get_marketplace(&self) -> WalmartMarketplace { + self.marketplace + } + + pub fn req_json

(&self, method: Method, path: &str, params: P) -> WalmartResult where P: ExtendUrlParams, { - let mut url = match self.marketplace { - WalmartMarketplace::USA => self.base_url.join(path)?, - WalmartMarketplace::Canada => { - // add `ca` to url - let path = path - .split('/') - .enumerate() - .map(|(i, seg)| { - if i == 1 { - format!("{}/ca", seg) - } else { - seg.to_string() - } - }) - .collect::>() - .join("/"); - self.base_url.join(&path)? - } - }; - params.extend_url_params(&mut url); + use reqwest::header::ACCEPT; + + self + .request(method, path, params) + .map(|req| req.header(ACCEPT, HeaderValue::from_static("application/json"))) + } - debug!("request: method = {}, url = {}", method, url); + pub fn req_xml

(&self, method: Method, path: &str, params: P) -> WalmartResult + where + P: ExtendUrlParams, + { + use reqwest::header::ACCEPT; + self + .request(method, path, params) + .map(|req| req.header(ACCEPT, HeaderValue::from_static("application/xml"))) + } + + pub async fn send(&self, req: WalmartReq) -> WalmartResult { + let WalmartReq { + url, + method, + rb: mut req, + } = req; let timestamp = Utc::now(); let timestamp = timestamp.timestamp() * 1000 + timestamp.timestamp_subsec_millis() as i64; - let mut req = self.http.request(method.clone(), url.as_str()); - let mut headers = HeaderMap::new(); - let rid: String = thread_rng().gen_ascii_chars().take(10).collect(); + let rid = uuid::Uuid::new_v4().hyphenated().to_string(); headers.insert("WM_SVC.NAME", "Walmart Marketplace".parse()?); headers.insert("WM_QOS.CORRELATION_ID", rid.parse()?); headers.insert("WM_SEC.TIMESTAMP", timestamp.to_string().parse()?); @@ -163,7 +114,7 @@ impl Client { ref signature, } => { let sign = signature.sign(url.as_str(), method.clone(), timestamp)?; - debug!("auth: Signature: sign = {}", sign); + tracing::debug!("auth: Signature: sign = {}", sign); headers.insert("WM_CONSUMER.CHANNEL.TYPE", channel_type.parse()?); headers.insert("WM_CONSUMER.ID", signature.consumer_id().parse()?); headers.insert("WM_SEC.AUTH_SIGNATURE", sign.parse()?); @@ -173,14 +124,40 @@ impl Client { ref client_secret, .. } => { - let access_token = self.get_access_token(false)?; - debug!("auth: TokenApi: access_token = {}", access_token); + let access_token = self.get_or_refresh_access_token(false).await?; + tracing::debug!("auth: TokenApi: access_token = {}", access_token); headers.insert("WM_SEC.ACCESS_TOKEN", access_token.parse()?); req = req.basic_auth(client_id, Some(client_secret)); } } let req = req.headers(headers); - Ok(req) + + match req.send().await { + Ok(res) => { + if res.status() == StatusCode::UNAUTHORIZED { + self.clear_access_token(); + } + Ok(WalmartRes::new(res)) + } + Err(err) => Err(err.into()), + } + } + + fn request

(&self, method: Method, path: &str, params: P) -> WalmartResult + where + P: ExtendUrlParams, + { + let mut url = match self.marketplace { + WalmartMarketplace::USA => self.base_url.join(path)?, + WalmartMarketplace::Canada => self.base_url.join(&path)?, + }; + params.extend_url_params(&mut url); + + tracing::debug!("request: method = {}, url = {}", method, url); + + let req = self.http.request(method.clone(), url.as_str()); + + Ok(WalmartReq::new(url, method, req)) } fn clear_access_token(&self) { @@ -194,8 +171,7 @@ impl Client { } } - fn get_access_token(&self, force_renew: bool) -> WalmartResult { - use std::collections::HashMap; + async fn get_or_refresh_access_token(&self, force_renew: bool) -> WalmartResult { #[derive(Debug, Deserialize)] struct WalmartBearerToken { access_token: String, @@ -212,7 +188,7 @@ impl Client { if !force_renew { let lock = bearer_token.read().unwrap(); if let Some(ref token) = lock.as_ref() { - if token.expires_at.saturating_duration_since(Instant::now()) > Duration::from_secs(120) + if token.expires_at.saturating_duration_since(Instant::now()) > Duration::from_secs(60) { return Ok(token.access_token.clone()); } @@ -224,30 +200,32 @@ impl Client { form.insert("grant_type", "client_credentials"); let mut headers = HeaderMap::new(); - let rid: String = thread_rng().gen_ascii_chars().take(10).collect(); + let rid = uuid::Uuid::new_v4().hyphenated().to_string(); headers.insert("WM_SVC.NAME", "Walmart Marketplace".parse()?); headers.insert("WM_QOS.CORRELATION_ID", rid.parse()?); headers.insert("Accept", "application/json".parse()?); - let mut res = self + let res = self .http .request(Method::POST, &format!("{}/v3/token", BASE_URL)) .headers(headers) .form(&form) .basic_auth(client_id, Some(client_secret)) - .send()?; + .send() + .await?; - let token: WalmartBearerToken = res.json()?; + let token = res.json::().await?; let access_token = token.access_token.clone(); if token.token_type != "Bearer" { - return Err(WalmartError::Msg(format!( + return Err(WalmartError::Auth(format!( "unsupported token type: {}", token.token_type ))); } - debug!("token: {:#?}", token); + tracing::debug!("token: {:#?}", token); + tracing::debug!("token expires in {} seconds", token.expires_in); let mut lock = bearer_token.write().unwrap(); lock.replace(BearerToken { @@ -258,84 +236,204 @@ impl Client { Ok(access_token) } _ => { - return Err(WalmartError::Msg( + return Err(WalmartError::Auth( "cannot get bearer with Signature Authentication".to_string(), )) } } } +} - pub fn request_json

( - &self, - method: Method, - path: &str, - params: P, - ) -> WalmartResult - where - P: ExtendUrlParams, - { - use reqwest::header::{HeaderValue, ACCEPT}; +pub struct WalmartReq { + url: Url, + method: Method, + rb: RequestBuilder, +} - self - .request(method, path, params) - .map(|req| req.header(ACCEPT, HeaderValue::from_static("application/json"))) +impl WalmartReq { + pub fn new(url: Url, method: Method, rb: RequestBuilder) -> Self { + Self { url, method, rb } } - pub fn request_xml

( - &self, - method: Method, - path: &str, - params: P, - ) -> WalmartResult - where - P: ExtendUrlParams, - { - use reqwest::header::{HeaderValue, ACCEPT}; + pub fn header(self, key: HeaderName, value: HeaderValue) -> Self { + Self { + rb: self.rb.header(key, value), + ..self + } + } - self - .request(method, path, params) - .map(|req| req.header(ACCEPT, HeaderValue::from_static("application/xml"))) + pub fn form(self, form: reqwest::multipart::Form) -> Self { + Self { + rb: self.rb.multipart(form), + ..self + } } - pub fn send(&self, req: RequestBuilder) -> WalmartResult { - match req.send() { - Ok(res) => { - if res.status() == StatusCode::UNAUTHORIZED { - self.clear_access_token(); - } - Ok(res) - } - Err(err) => Err(err.into()), + // Can be optimized by using streams + pub fn body_raw( + self, + mut body: R, + content_type: &'static str, + ) -> WalmartResult { + let mut buffer = Vec::new(); + body.read_to_end(&mut buffer)?; + + Ok(Self { + rb: self.rb.body(buffer).header( + reqwest::header::CONTENT_TYPE, + HeaderValue::from_static(content_type), + ), + ..self + }) + } + + pub fn body_json(self, body: &T) -> WalmartResult { + Ok(Self { + rb: self.rb.json(body), + ..self + }) + } + + /// Should switch to serde_xml_rs once they fix serialization issues + /// some fields can't be serialized atm e.g. https://github.com/RReverser/serde-xml-rs/issues/186 + pub fn body_xml(self, body: T) -> WalmartResult { + use xml_builder::{XMLBuilder, XMLVersion}; + let mut xml = XMLBuilder::new() + .version(XMLVersion::XML1_0) + .encoding("UTF-8".into()) + .build(); + xml.set_root_element(body.to_xml()?); + let mut writer = Vec::::new(); + xml.generate(&mut writer)?; + + Ok(Self { + rb: self + .rb + .header(reqwest::header::CONTENT_TYPE, "application/xml") + .body(writer), + ..self + }) + } +} + +impl Into for WalmartReq { + fn into(self) -> RequestBuilder { + self.rb + } +} + +#[derive(Debug, Clone, Copy)] +pub enum WalmartMarketplace { + USA, + Canada, +} + +pub enum WalmartCredential { + TokenApi { + client_id: String, + client_secret: String, + }, + Signature { + channel_type: String, + consumer_id: String, + private_key: String, + }, +} + +enum AuthState { + TokenApi { + client_id: String, + client_secret: String, + bearer_token: RwLock>, + }, + Signature { + channel_type: String, + signature: Signature, + }, +} + +struct BearerToken { + access_token: String, + expires_at: Instant, +} + +pub trait ExtendUrlParams { + fn extend_url_params(self, url: &mut Url); +} + +impl ExtendUrlParams for () { + fn extend_url_params(self, _: &mut Url) {} +} + +impl<'a> ExtendUrlParams for &'a str { + fn extend_url_params(self, url: &mut Url) { + if !self.is_empty() { + url.set_query(Some(self)); + } + } +} + +impl<'a> ExtendUrlParams for String { + fn extend_url_params(self, url: &mut Url) { + if !self.is_empty() { + url.set_query(Some(self.as_ref())); } } +} - pub fn get_marketplace(&self) -> WalmartMarketplace { - self.marketplace +impl, T2: AsRef> ExtendUrlParams for Vec<(T1, T2)> { + fn extend_url_params(self, url: &mut Url) { + url.query_pairs_mut().extend_pairs(self); } } -// #[cfg(test)] -// mod tests { -// use super::*; -// use dotenv::dotenv; -// use std::env; - -// #[test] -// fn client() { -// use std::io::Read; -// dotenv().ok(); - -// let client = Client::new(&env::var("WALMART_CONSUMER_ID").unwrap(), &env::var("WALMART_PRIVATE_KEY").unwrap()).unwrap(); -// let mut res = client.request_json(Method::Get, "/v3/feeds/117E39F0B7654B08A059457FB6E803FF@AQYBAAA", ()).unwrap().send().unwrap(); -// println!("status: {}", res.status()); -// let mut json = String::new(); -// res.read_to_string(&mut json).unwrap(); -// println!("body: {}", json); -// { -// use std::fs::File; -// use std::io::Write; -// let mut f = File::create("samples/get_feed_aand_item_status.json").unwrap(); -// write!(&mut f, "{}", json).unwrap() -// } -// } -// } +pub struct WalmartRes(Response); + +impl WalmartRes { + fn new(res: Response) -> Self { + WalmartRes(res) + } +} + +impl WalmartRes { + pub async fn res_bytes(self) -> WalmartResult> { + let res = self.error_for_status().await?; + let bytes = res.bytes().await?; + Ok(bytes.to_vec()) + } + + pub async fn res_json(self) -> WalmartResult { + let res = self.error_for_status().await?; + let json = res.json::().await?; + Ok(json) + } + + pub async fn res_xml(self) -> WalmartResult { + let res = self.error_for_status().await?; + + let text = res.text().await?; + serde_xml_rs::from_str(&text).map_err(|e| e.into()) + } + + async fn error_for_status(self) -> WalmartResult { + let res = self.into_inner(); + if !res.status().is_success() { + let status = res.status(); + let path = res.url().path().to_string(); + let body = res.text().await.unwrap_or_default(); + tracing::debug!( + "walmart response error: status: '{}', path: '{}', response body: '{}'", + status, + path, + body + ); + Err(ApiResponseError { status, path, body }.into()) + } else { + Ok(res) + } + } + + pub fn into_inner(self) -> Response { + self.0 + } +} diff --git a/walmart-partner-api/src/feed/mod.rs b/walmart-partner-api/src/feed/mod.rs deleted file mode 100644 index f94981b..0000000 --- a/walmart-partner-api/src/feed/mod.rs +++ /dev/null @@ -1,67 +0,0 @@ -use crate::response::JsonMaybe; -use crate::result::*; -use std::io::Read; -mod types; -use serde_urlencoded; - -pub use self::types::*; -use crate::client::{Client, Method}; -use crate::xml::Xml; - -#[derive(Debug, Serialize, Default)] -#[allow(non_snake_case)] -pub struct GetAllFeedStatusesQuery<'a> { - pub feedId: Option<&'a str>, - pub limit: Option, - pub offset: Option, -} - -#[derive(Debug, Serialize, Default)] -#[allow(non_snake_case)] -pub struct GetFeedAndItemStatusQuery { - pub includeDetails: Option, - pub limit: Option, - pub offset: Option, -} - -impl Client { - pub fn get_all_feed_statuses( - &self, - query: &GetAllFeedStatusesQuery, - ) -> WalmartResult { - let qs = serde_urlencoded::to_string(query)?; - self - .send(self.request_json(Method::GET, "/v3/feeds", qs)?)? - .json_maybe::() - .map_err(Into::into) - } - - pub fn get_feed_and_item_status( - &self, - feed_id: &str, - query: &GetFeedAndItemStatusQuery, - ) -> WalmartResult { - let path = format!("/v3/feeds/{}", feed_id); - self - .send(self.request_json(Method::GET, &path, serde_urlencoded::to_string(query)?)?)? - .json_maybe::() - .map_err(Into::into) - } - - pub fn bulk_upload_xml( - &self, - feed_type: &str, - feed: R, - ) -> WalmartResult { - use reqwest::header::{HeaderValue, CONTENT_TYPE}; - use reqwest::Body; - let mut res = self.send( - self - .request_xml(Method::POST, "/v3/feeds", vec![("feedType", feed_type)])? - .body(Body::new(feed)) - .header(CONTENT_TYPE, HeaderValue::from_static("application/xml")), - )?; - let xml = Xml::::from_res(&mut res)?; - Ok(xml.into_inner()) - } -} diff --git a/walmart-partner-api/src/feed/types.rs b/walmart-partner-api/src/feed/types.rs deleted file mode 100644 index 610814c..0000000 --- a/walmart-partner-api/src/feed/types.rs +++ /dev/null @@ -1,126 +0,0 @@ -use crate::result::*; -use crate::utils::deserialize_timestamp; -use crate::xml::{Element, FromXmlElement}; -use chrono::{DateTime, Utc}; -use serde_json::Value; - -#[derive(Debug, Serialize, Deserialize)] -#[allow(non_snake_case)] -pub struct FeedAck { - pub feedId: String, -} - -impl FromXmlElement for FeedAck { - fn from_xml_element(elem: Element) -> WalmartResult { - Ok(FeedAck { - feedId: elem - .get_child("feedId") - .ok_or_else(|| WalmartError::UnexpectedXml(format!("missing `feedId` element")))? - .text - .clone() - .ok_or_else(|| WalmartError::UnexpectedXml(format!("empty `feedId` element")))?, - }) - } -} - -#[derive(Debug, Serialize, Deserialize)] -#[allow(non_snake_case)] -pub struct FeedStatus { - pub feedId: String, - pub feedType: String, - pub partnerId: String, - pub itemsReceived: i32, - pub itemsSucceeded: i32, - pub itemsFailed: i32, - pub itemsProcessing: i32, - pub feedStatus: String, - #[serde(deserialize_with = "deserialize_timestamp")] - pub feedDate: DateTime, - #[serde(deserialize_with = "deserialize_timestamp")] - pub modifiedDtm: DateTime, - pub fileName: Option, - pub itemDataErrorCount: i32, - pub itemSystemErrorCount: i32, - pub itemTimeoutErrorCount: i32, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct FeedStatusesResults { - pub feed: Vec, -} - -#[derive(Debug, Serialize, Deserialize)] -#[allow(non_snake_case)] -pub struct FeedStatuses { - pub totalResults: i32, - pub offset: i32, - pub limit: i32, - pub results: FeedStatusesResults, -} - -#[derive(Debug, Serialize, Deserialize)] -#[allow(non_snake_case)] -pub struct ItemIngestionStatus { - pub martId: i32, - pub sku: String, - pub wpid: String, - pub ingestionStatus: String, - pub ingestionErrors: IngestionErrors, -} - -#[derive(Debug, Serialize, Deserialize)] -#[allow(non_snake_case)] -pub struct IngestionErrors { - pub ingestionError: Value, -} - -#[derive(Debug, Serialize, Deserialize)] -#[allow(non_snake_case)] -pub struct ItemDetails { - pub itemIngestionStatus: Vec, -} - -#[derive(Debug, Serialize, Deserialize)] -#[allow(non_snake_case)] -pub struct PartnerFeedResponse { - pub feedId: String, - pub feedStatus: String, - pub ingestionErrors: Option, - pub itemsReceived: i32, - pub itemsSucceeded: i32, - pub itemsFailed: i32, - pub itemsProcessing: i32, - pub offset: i32, - pub limit: i32, - pub itemDetails: ItemDetails, -} - -#[cfg(test)] -mod tests { - use super::*; - use serde_json::from_str; - - #[test] - fn deserialize_partner_feed_response() { - from_str::( - r##"{ - "feedId": "117E39F0B7654B08A059457FB6E803FF@AQYBAAA", - "feedStatus": "PROCESSED", - "shipNode": null, - "ingestionErrors": { - "ingestionError": null - }, - "itemsReceived": 1, - "itemsSucceeded": 0, - "itemsFailed": 1, - "itemsProcessing": 0, - "offset": 0, - "limit": 50, - "itemDetails": { - "itemIngestionStatus": [] - } - }"##, - ) - .unwrap(); - } -} diff --git a/walmart-partner-api/src/inventory/mod.rs b/walmart-partner-api/src/inventory/mod.rs deleted file mode 100644 index 5c663f2..0000000 --- a/walmart-partner-api/src/inventory/mod.rs +++ /dev/null @@ -1,34 +0,0 @@ -use crate::response::JsonMaybe; -use crate::result::*; -mod types; - -pub use self::types::*; -use crate::client::{Client, Method, WalmartMarketplace}; - -impl Client { - pub fn get_item_inventory(&self, sku: &str) -> WalmartResult { - let path = match self.get_marketplace() { - WalmartMarketplace::USA => "/v2/inventory", - WalmartMarketplace::Canada => "/v3/inventory", - }; - self - .send(self.request_json(Method::GET, path, vec![("sku", sku)])?)? - .json_maybe::() - .map_err(Into::into) - } - - pub fn update_item_inventory(&self, inventory: &Inventory) -> WalmartResult { - let path = match self.get_marketplace() { - WalmartMarketplace::USA => "/v2/inventory", - WalmartMarketplace::Canada => "/v3/inventory", - }; - self - .send( - self - .request_json(Method::PUT, path, vec![("sku", &inventory.sku)])? - .json(inventory), - )? - .json_maybe::() - .map_err(Into::into) - } -} diff --git a/walmart-partner-api/src/inventory/types.rs b/walmart-partner-api/src/inventory/types.rs deleted file mode 100644 index 9ec6ac0..0000000 --- a/walmart-partner-api/src/inventory/types.rs +++ /dev/null @@ -1,26 +0,0 @@ -#[derive(Debug, Serialize, Deserialize)] -pub struct Quantity { - pub unit: String, - pub amount: i32, -} - -#[derive(Debug, Serialize, Deserialize)] -#[allow(non_snake_case)] -pub struct Inventory { - pub sku: String, - pub quantity: Quantity, - pub fulfillmentLagTime: i32, -} - -impl Inventory { - pub fn new(sku: &str, quantity: i32, fulfillment_lag_time: i32) -> Inventory { - Inventory { - sku: sku.to_owned(), - quantity: Quantity { - unit: "EACH".to_owned(), - amount: quantity, - }, - fulfillmentLagTime: fulfillment_lag_time, - } - } -} \ No newline at end of file diff --git a/walmart-partner-api/src/item/mod.rs b/walmart-partner-api/src/item/mod.rs deleted file mode 100644 index f7821ce..0000000 --- a/walmart-partner-api/src/item/mod.rs +++ /dev/null @@ -1,41 +0,0 @@ -use crate::result::*; -mod types; -use crate::client::{Client, Method}; -use crate::xml::Xml; - -pub use self::types::*; - -/// Query parameters for `get_all_items` -#[derive(Debug, Serialize, Clone)] -#[allow(non_snake_case)] -pub struct GetAllItemsQueryParams { - pub nextCursor: String, - pub sku: Option, - pub limit: Option, - pub offset: Option, -} - -impl Default for GetAllItemsQueryParams { - fn default() -> Self { - GetAllItemsQueryParams { - nextCursor: "*".to_string(), - sku: None, - limit: None, - offset: None, - } - } -} - -impl Client { - pub fn get_all_items( - &self, - params: &GetAllItemsQueryParams, - ) -> WalmartResult<(Xml, Option)> { - let qs = serde_urlencoded::to_string(params)?; - let mut res = self.send(self.request_xml(Method::GET, "/v3/items", qs)?)?; - - let xml = Xml::::from_res(&mut res)?; - let next_params = xml.get_next_query_params(params, self.get_marketplace()); - Ok((xml, next_params)) - } -} diff --git a/walmart-partner-api/src/item/types.rs b/walmart-partner-api/src/item/types.rs deleted file mode 100644 index 8364875..0000000 --- a/walmart-partner-api/src/item/types.rs +++ /dev/null @@ -1,117 +0,0 @@ -use super::GetAllItemsQueryParams; -use crate::client::WalmartMarketplace; -use crate::result::*; -use crate::xml::*; -use xmltree::Element; - -/// Response of `get_all_items` -#[derive(Debug, Serialize)] -#[allow(non_snake_case)] -pub struct GetAllItems { - pub items: Vec, - - /// US only - pub totalItems: i64, - /// US only - pub nextCursor: Option, -} - -impl GetAllItems { - /// As of 2018/10/30, - /// `offset` works differently in USA - /// `totalItems` is always 0 in Canada response - /// `nextCursor` element never show up in Canada response - pub(crate) fn get_next_query_params( - &self, - current_params: &GetAllItemsQueryParams, - marketplace: WalmartMarketplace, - ) -> Option { - match marketplace { - // use nextCursor - WalmartMarketplace::USA => { - self - .nextCursor - .as_ref() - .map(|next_cursor| GetAllItemsQueryParams { - nextCursor: next_cursor.to_string(), - ..current_params.clone() - }) - } - // increase offset until the response has no items - WalmartMarketplace::Canada => { - if self.items.is_empty() { - None - } else { - let limit = current_params.limit.clone().unwrap_or(20); - Some(GetAllItemsQueryParams { - offset: current_params.offset.or(Some(0)).map(|v| v + limit), - ..current_params.clone() - }) - } - } - } - } -} - -#[derive(Debug, Serialize, Default)] -pub struct Price { - pub currency: String, - pub amount: String, -} - -#[derive(Debug, Serialize)] -#[allow(non_snake_case)] -pub struct Item { - pub mart: String, - pub sku: String, - pub wpid: String, - pub upc: String, - pub gtin: String, - pub productName: String, - pub shelf: String, - pub productType: String, - pub price: Price, - pub publishedStatus: String, -} - -impl FromXmlElement for GetAllItems { - fn from_xml_element(elem: Element) -> WalmartResult { - let items = elem - .children - .iter() - .filter_map(|c| { - if c.name == "ItemResponse" { - Some(Item { - mart: c.get_child_text_or_default("mart"), - sku: c.get_child_text_or_default("sku"), - wpid: c.get_child_text_or_default("wpid"), - upc: c.get_child_text_or_default("upc"), - gtin: c.get_child_text_or_default("gtin"), - productName: c.get_child_text_or_default("productName"), - shelf: c.get_child_text_or_default("shelf"), - productType: c.get_child_text_or_default("productType"), - price: c - .get_child("price") - .map(|c| Price { - currency: c.get_child_text_or_default("currency"), - amount: c.get_child_text_or_default("amount"), - }) - .unwrap_or_default(), - publishedStatus: c.get_child_text_or_default("publishedStatus"), - }) - } else { - None - } - }) - .collect(); - Ok(GetAllItems { - items, - totalItems: elem - .get_child_text_or_default("totalItems") - .parse() - .ok() - .unwrap_or_default(), - nextCursor: elem.get_child_text("nextCursor"), - }) - } -} diff --git a/walmart-partner-api/src/lib.rs b/walmart-partner-api/src/lib.rs index 5eaed30..2767f38 100644 --- a/walmart-partner-api/src/lib.rs +++ b/walmart-partner-api/src/lib.rs @@ -1,27 +1,18 @@ -extern crate base64; -extern crate bigdecimal; -extern crate chrono; -extern crate csv; -extern crate failure; -#[macro_use] -extern crate failure_derive; #[macro_use] extern crate serde_derive; -#[macro_use] -extern crate serde_json; -#[macro_use] -extern crate log; +pub use api::*; +pub use client::{WalmartCredential, WalmartMarketplace}; +pub use result::{WalmartError, WalmartResult}; + +mod api; mod client; -pub mod feed; -pub mod inventory; -pub mod item; -pub mod order; -pub mod report; -pub mod response; pub mod result; +mod shared; mod sign; +#[cfg(test)] +mod test_util; mod utils; mod xml; -pub use self::client::{Client, WalmartCredential, WalmartMarketplace}; +pub(crate) use xml::*; diff --git a/walmart-partner-api/src/order/mod.rs b/walmart-partner-api/src/order/mod.rs deleted file mode 100644 index ed0e940..0000000 --- a/walmart-partner-api/src/order/mod.rs +++ /dev/null @@ -1,163 +0,0 @@ -use crate::result::*; -use chrono::{DateTime, Utc}; -use serde_json::Value; -use serde_urlencoded; - -mod types; - -pub use self::types::*; -use crate::client::{Client, Method}; -use crate::response::{parse_list_elements_json, parse_object_json, ListResponse}; - -/// Query parameters for `get_all_released_orders` - -#[derive(Debug, Serialize, Default)] -#[allow(non_snake_case)] -pub struct ReleasedQueryParams { - pub limit: Option, - pub createdStartDate: Option>, - pub nextCursor: Option, -} - -/// Query parameters for `get_all_orders` -#[derive(Debug, Serialize, Default)] -#[allow(non_snake_case)] -pub struct QueryParams { - pub sku: Option, - pub customerOrderId: Option, - pub purchaseOrderId: Option, - pub status: Option, - pub createdStartDate: Option>, - pub createdEndDate: Option>, - pub fromExpectedShipDate: Option>, - pub toExpectedShipDate: Option>, - pub limit: Option, - pub nextCursor: Option, - pub shipNodeType: Option, -} - -#[derive(Debug, Clone)] -#[allow(non_snake_case)] -pub struct ShipParams { - pub lineNumber: String, - pub shipDateTime: DateTime, - pub carrierName: Option, - pub methodCode: String, - pub trackingNumber: String, - pub trackingURL: String, - pub otherCarrier: Option, - pub unitOfMeasurement: Option, - pub amount: Option, - pub shipFromCountry: String, -} - -impl ShipParams { - pub fn to_value(&self) -> Value { - let timestamp: i64 = - self.shipDateTime.timestamp() * 1000 + self.shipDateTime.timestamp_subsec_millis() as i64; - json!({ - "lineNumber": self.lineNumber, - "shipFromCountry": self.shipFromCountry, - "orderLineStatuses": { - "orderLineStatus": [ - { - "status": "Shipped", - "statusQuantity": { - "unitOfMeasurement": self.unitOfMeasurement.clone().unwrap_or_else(|| "EACH".to_owned()), - "amount": self.amount.clone().unwrap_or_else(|| "1".to_owned()), - }, - "trackingInfo": { - "shipDateTime": timestamp, - "carrierName": { - "otherCarrier": self.otherCarrier, - "carrier": self.carrierName, - }, - "methodCode": self.methodCode, - "trackingNumber": self.trackingNumber, - "trackingURL": self.trackingURL - } - } - ] - } - }) - } -} - -pub type OrderList = ListResponse; - -impl Client { - pub fn get_all_released_orders(&self, params: &ReleasedQueryParams) -> WalmartResult { - let qs = serde_urlencoded::to_string(params)?; - let mut res = self.send(self.request_json(Method::GET, "/v3/orders/released", qs)?)?; - parse_list_elements_json(res.status(), &mut res, "order").map_err(Into::into) - } - - pub fn get_all_orders(&self, params: &QueryParams) -> WalmartResult { - let mut res = self.send(self.request_json( - Method::GET, - "/v3/orders", - serde_urlencoded::to_string(params)?, - )?)?; - parse_list_elements_json(res.status(), &mut res, "order").map_err(Into::into) - } - - pub fn get_all_orders_by_next_cursor(&self, next_cursor: &str) -> WalmartResult { - use url::form_urlencoded; - let mut res = self.send( - self.request_json( - Method::GET, - "/v3/orders", - form_urlencoded::parse((&next_cursor[1..]).as_bytes()) - .into_owned() - .collect::>(), - )?, - )?; - parse_list_elements_json(res.status(), &mut res, "order").map_err(Into::into) - } - - pub fn get_order(&self, purchase_order_id: &str) -> WalmartResult { - let path = format!("/v3/orders/{}", purchase_order_id); - let mut res = self.send(self.request_json(Method::GET, &path, ())?)?; - parse_object_json(res.status(), &mut res, "order").map_err(Into::into) - } - - pub fn ack_order(&self, purchase_order_id: &str) -> WalmartResult { - let path = format!("/v3/orders/{}/acknowledge", purchase_order_id); - let mut res = self.send( - self - .request_json(Method::POST, &path, ())? - .json(&Vec::::new()), - )?; - parse_object_json(res.status(), &mut res, "order").map_err(Into::into) - } - - pub fn ship_order_line( - &self, - purchase_order_id: &str, - line: &ShipParams, - ) -> WalmartResult { - self.ship_order(purchase_order_id, &[line.clone()]) - } - - pub fn ship_order(&self, purchase_order_id: &str, lines: &[ShipParams]) -> WalmartResult { - let line_values: Vec<_> = lines.into_iter().map(ShipParams::to_value).collect(); - let body = json!({ - "orderShipment": { - "orderLines": { - "orderLine": line_values, - } - } - }); - let path = format!("/v3/orders/{}/shipping", purchase_order_id); - let mut res = self.send( - self - .request_json( - Method::POST, - &path, - vec![("purchaseOrderId", purchase_order_id)], - )? - .json(&body), - )?; - parse_object_json(res.status(), &mut res, "order").map_err(Into::into) - } -} diff --git a/walmart-partner-api/src/order/test_order.json b/walmart-partner-api/src/order/test_order.json deleted file mode 100644 index 351b0e7..0000000 --- a/walmart-partner-api/src/order/test_order.json +++ /dev/null @@ -1,101 +0,0 @@ -{ - "order": { - "purchaseOrderId": "1577684050862", - "customerOrderId": "2861700797280", - "customerEmailId": "jsanthanam@walmartlabs.com", - "orderDate": 1484458949000, - "shippingInfo": { - "phone": "4151234567", - "estimatedDeliveryDate": 1485586800000, - "estimatedShipDate": 1484636400000, - "methodCode": "Value", - "postalAddress": { - "name": "Asha Chakre", - "address1": "860 W California ave", - "address2": null, - "city": "Sunnyvale", - "state": "CA", - "postalCode": "94086", - "country": "USA", - "addressType": "RESIDENTIAL" - } - }, - "orderLines": { - "orderLine": [ - { - "lineNumber": "1", - "item": { - "productName": "Kellogg's Rice Krispies Cereal, 24 oz", - "sku": "MGR_07_21_00100123" - }, - "charges": { - "charge": [ - { - "chargeType": "PRODUCT", - "chargeName": "ItemPrice", - "chargeAmount": { - "currency": "USD", - "amount": 19.99 - }, - "tax": { - "taxName": "Tax1", - "taxAmount": { - "currency": "USD", - "amount": 1.7 - } - } - } - ] - }, - "orderLineQuantity": { - "unitOfMeasurement": "EACH", - "amount": "1" - }, - "statusDate": 1487888747000, - "orderLineStatuses": { - "orderLineStatus": [ - { - "status": "Shipped", - "statusQuantity": { - "unitOfMeasurement": "EACH", - "amount": "1" - }, - "cancellationReason": null, - "trackingInfo": { - "shipDateTime": 1485549015000, - "carrierName": { - "otherCarrier": null, - "carrier": "FedEx" - }, - "methodCode": "Value", - "trackingNumber": "3445435443441221", - "trackingURL": "http://walmart.narvar.com/walmart/tracking/Fedex?&type=MP&seller_id=801&promise_date=01/28/2017&dzip=94086&tracking_numbers=3445435443441221" - } - } - ] - }, - "refund": { - "refundId": null, - "refundComments": null, - "refundCharges": { - "refundCharge": [ - { - "refundReason": "ItemNotReceivedByCustomer", - "charge": { - "chargeType": "PRODUCT", - "chargeName": "Lost in Transit", - "chargeAmount": { - "currency": "USD", - "amount": -0.01 - }, - "tax": null - } - } - ] - } - } - } - ] - } - } -} diff --git a/walmart-partner-api/src/order/test_order_list_res.json b/walmart-partner-api/src/order/test_order_list_res.json deleted file mode 100644 index 0bd12de..0000000 --- a/walmart-partner-api/src/order/test_order_list_res.json +++ /dev/null @@ -1,152 +0,0 @@ -{ - "list": { - "meta": { - "totalCount": 66, - "limit": 10, - "nextCursor": "?limit=10&hasMoreElements=true&soIndex=66&poIndex=10&partnerId=10000000754&sellerId=747&status=Shipped&createdStartDate=2016-08-16T10:30:30.155Z&createdEndDate=2017-08-08T18:52:57.162Z" - }, - "elements": { - "order": [ - { - "purchaseOrderId": "11", - "customerOrderId": "12", - "customerEmailId": "1@relay.walmart.com", - "orderDate": 1501903867000, - "shippingInfo": { - "phone": "9176070000", - "estimatedDeliveryDate": 1502863200000, - "estimatedShipDate": 1502258400000, - "methodCode": "Standard", - "postalAddress": { - "name": "Foo Bar", - "address1": "7777 Madisoner Ct", - "address2": null, - "city": "Fake", - "state": "VA", - "postalCode": "78787", - "country": "USA", - "addressType": "RESIDENTIAL" - } - }, - "orderLines": { - "orderLine": [ - { - "lineNumber": "1", - "item": { - "productName": "Edifier H850 Over-the-ear Pro Headphones", - "sku": "edifier-h850" - }, - "charges": { - "charge": [ - { - "chargeType": "PRODUCT", - "chargeName": "ItemPrice", - "chargeAmount": { - "currency": "USD", - "amount": 39.99 - }, - "tax": null - } - ] - }, - "orderLineQuantity": { - "unitOfMeasurement": "EACH", - "amount": "1" - }, - "statusDate": 1502114839000, - "orderLineStatuses": { - "orderLineStatus": [ - { - "status": "Shipped", - "statusQuantity": { - "unitOfMeasurement": "EACH", - "amount": "1" - }, - "cancellationReason": null, - "trackingInfo": { - "shipDateTime": 1502089301000, - "carrierName": { - "otherCarrier": "OtherCarrier", - "carrier": null - }, - "methodCode": "Standard", - "trackingNumber": "TBA387619000000", - "trackingURL": "https://edifier-usa.myshopify.com/admin/orders/5994717066" - } - } - ] - }, - "refund": null - } - ] - } - }, - { - "purchaseOrderId": "21", - "customerOrderId": "22", - "customerEmailId": "2@relay.walmart.com", - "orderDate": 1491380201000, - "shippingInfo": { - "phone": "6026900000", - "estimatedDeliveryDate": 1492236000000, - "estimatedShipDate": 1491631200000, - "methodCode": "Standard", - "postalAddress": { - "name": "Joe Da", - "address1": "1234 Vard St. Apt 4444", - "address2": null, - "city": "Fake", - "state": "AZ", - "postalCode": "77777", - "country": "USA", - "addressType": "RESIDENTIAL" - } - }, - "orderLines": { - "orderLine": [ - { - "lineNumber": "1", - "item": { - "productName": "Edifier R1280T Powered Bookshelf Speakers Studio Monitors", - "sku": "EDIFIERr1280t" - }, - "charges": { - "charge": [ - { - "chargeType": "PRODUCT", - "chargeName": "ItemPrice", - "chargeAmount": { - "currency": "USD", - "amount": 0.0 - }, - "tax": null - } - ] - }, - "orderLineQuantity": { - "unitOfMeasurement": "EACH", - "amount": "1" - }, - "statusDate": 1491839507000, - "orderLineStatuses": { - "orderLineStatus": [ - { - "status": "Cancelled", - "statusQuantity": { - "unitOfMeasurement": "EACH", - "amount": "1" - }, - "cancellationReason": null, - "trackingInfo": null - } - ] - }, - "refund": null - } - ] - } - } - ] - } - } -} diff --git a/walmart-partner-api/src/order/types.rs b/walmart-partner-api/src/order/types.rs deleted file mode 100644 index a403a52..0000000 --- a/walmart-partner-api/src/order/types.rs +++ /dev/null @@ -1,284 +0,0 @@ -#[derive(Debug, Serialize, Deserialize)] -#[allow(non_snake_case)] -pub struct CurrencyAmount { - pub currency: String, - pub amount: f64, -} - -#[derive(Debug, Serialize, Deserialize)] -#[allow(non_snake_case)] -pub struct PostalAddress { - pub name: String, - pub address1: String, - pub address2: Option, - pub city: String, - pub state: String, - pub postalCode: String, - pub country: String, - pub addressType: String, -} - -#[derive(Debug, Serialize, Deserialize)] -#[allow(non_snake_case)] -pub struct ShippingInformation { - pub phone: String, - pub estimatedDeliveryDate: i64, - pub estimatedShipDate: i64, - pub methodCode: String, - pub postalAddress: PostalAddress, -} - -#[derive(Debug, Serialize, Deserialize)] -#[allow(non_snake_case)] -pub struct OrderLineItem { - pub productName: String, - pub sku: String, -} - -#[derive(Debug, Serialize, Deserialize)] -#[allow(non_snake_case)] -pub struct Tax { - pub taxName: String, - pub taxAmount: CurrencyAmount, -} - -#[derive(Debug, Serialize, Deserialize)] -#[allow(non_snake_case)] -pub struct OrderLineChargeItem { - pub chargeType: String, - pub chargeName: String, - pub chargeAmount: CurrencyAmount, - pub tax: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -#[allow(non_snake_case)] -pub struct Quantity { - pub unitOfMeasurement: String, - pub amount: String, -} - -#[derive(Debug, Serialize, Deserialize)] -#[allow(non_snake_case)] -pub struct OrderLineCharges { - pub charge: Vec, -} - -#[derive(Debug, Serialize, Deserialize)] -#[allow(non_snake_case)] -pub struct OrderLineTrackingInfoCarrier { - pub otherCarrier: Option, - pub carrier: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -#[allow(non_snake_case)] -pub struct OrderLineTrackingInfo { - pub shipDateTime: i64, - pub carrierName: OrderLineTrackingInfoCarrier, - pub methodCode: String, - pub trackingNumber: String, - pub trackingURL: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -#[allow(non_snake_case)] -pub struct OrderLineStatus { - pub status: String, - pub statusQuantity: Quantity, - // pub cancellationReason: Option, - pub trackingInfo: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -#[allow(non_snake_case)] -pub struct OrderLineStatuss { - pub orderLineStatus: Vec, -} - -#[derive(Debug, Serialize, Deserialize)] -#[allow(non_snake_case)] -pub struct OrderLine { - pub lineNumber: String, - pub item: OrderLineItem, - pub charges: OrderLineCharges, - pub orderLineQuantity: Quantity, - pub statusDate: i64, - pub orderLineStatuses: OrderLineStatuss, - // pub refund: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -#[allow(non_snake_case)] -pub struct OrderLines { - pub orderLine: Vec, -} - -#[derive(Debug, Serialize, Deserialize)] -#[allow(non_snake_case)] -pub struct Order { - pub purchaseOrderId: String, - pub customerOrderId: String, - pub customerEmailId: Option, - pub orderDate: i64, - pub shippingInfo: ShippingInformation, - pub orderLines: OrderLines, -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn deserialize() { - use serde_json; - - let json = r##" - [ - { - "purchaseOrderId": "11", - "customerOrderId": "12", - "customerEmailId": "1@relay.walmart.com", - "orderDate": 1501903867000, - "shippingInfo": { - "phone": "9176070000", - "estimatedDeliveryDate": 1502863200000, - "estimatedShipDate": 1502258400000, - "methodCode": "Standard", - "postalAddress": { - "name": "Foo Bar", - "address1": "7777 Madisoner Ct", - "address2": null, - "city": "Fake", - "state": "VA", - "postalCode": "78787", - "country": "USA", - "addressType": "RESIDENTIAL" - } - }, - "orderLines": { - "orderLine": [ - { - "lineNumber": "1", - "item": { - "productName": "Edifier H850 Over-the-ear Pro Headphones", - "sku": "edifier-h850" - }, - "charges": { - "charge": [ - { - "chargeType": "PRODUCT", - "chargeName": "ItemPrice", - "chargeAmount": { - "currency": "USD", - "amount": 39.99 - }, - "tax": null - } - ] - }, - "orderLineQuantity": { - "unitOfMeasurement": "EACH", - "amount": "1" - }, - "statusDate": 1502114839000, - "orderLineStatuses": { - "orderLineStatus": [ - { - "status": "Shipped", - "statusQuantity": { - "unitOfMeasurement": "EACH", - "amount": "1" - }, - "cancellationReason": null, - "trackingInfo": { - "shipDateTime": 1502089301000, - "carrierName": { - "otherCarrier": "OtherCarrier", - "carrier": null - }, - "methodCode": "Standard", - "trackingNumber": "TBA387619000000", - "trackingURL": "https://edifier-usa.myshopify.com/admin/orders/5994717066" - } - } - ] - }, - "refund": null - } - ] - } - }, - { - "purchaseOrderId": "21", - "customerOrderId": "22", - "customerEmailId": "2@relay.walmart.com", - "orderDate": 1491380201000, - "shippingInfo": { - "phone": "6026900000", - "estimatedDeliveryDate": 1492236000000, - "estimatedShipDate": 1491631200000, - "methodCode": "Standard", - "postalAddress": { - "name": "Joe Da", - "address1": "1234 Vard St. Apt 4444", - "address2": null, - "city": "Fake", - "state": "AZ", - "postalCode": "77777", - "country": "USA", - "addressType": "RESIDENTIAL" - } - }, - "orderLines": { - "orderLine": [ - { - "lineNumber": "1", - "item": { - "productName": "Edifier R1280T Powered Bookshelf Speakers Studio Monitors", - "sku": "EDIFIERr1280t" - }, - "charges": { - "charge": [ - { - "chargeType": "PRODUCT", - "chargeName": "ItemPrice", - "chargeAmount": { - "currency": "USD", - "amount": 0.0 - }, - "tax": null - } - ] - }, - "orderLineQuantity": { - "unitOfMeasurement": "EACH", - "amount": "1" - }, - "statusDate": 1491839507000, - "orderLineStatuses": { - "orderLineStatus": [ - { - "status": "Cancelled", - "statusQuantity": { - "unitOfMeasurement": "EACH", - "amount": "1" - }, - "cancellationReason": null, - "trackingInfo": null - } - ] - }, - "refund": null - } - ] - } - } - ] - "##; - - let orders: Vec = serde_json::from_str(&json).unwrap(); - assert_eq!(orders[0].orderLines.orderLine[0].lineNumber, "1"); - assert_eq!(orders[0].orderLines.orderLine[0].item.sku, "edifier-h850"); - } -} diff --git a/walmart-partner-api/src/report/mod.rs b/walmart-partner-api/src/report/mod.rs deleted file mode 100644 index 5b0ed7d..0000000 --- a/walmart-partner-api/src/report/mod.rs +++ /dev/null @@ -1,38 +0,0 @@ -use crate::result::*; -use serde_urlencoded; -use std::io::{Read, Write}; -mod item; - -pub use self::item::{ItemReport, ItemReportRow, ItemReportType}; -use crate::client::{Client, Method}; - -pub trait ReportType { - type Data; - - fn report_type() -> &'static str; - fn deserialize(r: R) -> WalmartResult; -} - -#[derive(Debug, Serialize, Default)] -#[allow(non_snake_case)] -pub struct GetReportQuery<'a> { - #[serde(rename = "type")] - pub type_: &'a str, -} - -impl Client { - pub fn get_report(&self) -> WalmartResult { - let qs = serde_urlencoded::to_string(&GetReportQuery { - type_: R::report_type(), - })?; - let res = self.send(self.request_json(Method::GET, "/v2/getReport", qs)?)?; - R::deserialize(res) - } - pub fn get_report_raw(&self, type_: &str, mut w: W) -> WalmartResult { - let qs = serde_urlencoded::to_string(&GetReportQuery { type_ })?; - let mut res = self - .send(self.request_json(Method::GET, "/v2/getReport", qs)?)? - .error_for_status()?; - res.copy_to(&mut w).map_err(Into::into) - } -} diff --git a/walmart-partner-api/src/response.rs b/walmart-partner-api/src/response.rs deleted file mode 100644 index 8c61f24..0000000 --- a/walmart-partner-api/src/response.rs +++ /dev/null @@ -1,242 +0,0 @@ -use crate::result::ApiResponseError; -use reqwest::{Response, StatusCode}; -use serde::de::DeserializeOwned; -use serde::Serialize; -use serde_json::{self, Value}; -use std::io::Read; - -#[derive(Debug, Serialize, Deserialize)] -#[allow(non_snake_case)] -pub struct ListMeta { - pub totalCount: i32, - pub limit: i32, - pub nextCursor: Option, -} - -#[derive(Debug, Serialize)] -pub struct ListResponse { - pub meta: Option, - pub elements: Vec, -} - -#[allow(non_snake_case)] -impl ListResponse { - pub fn get_next_cursor(&self) -> Option<&str> { - match *self { - ListResponse { - meta: Some(ListMeta { - nextCursor: Some(ref cursor), - .. - }), - .. - } => Some(&cursor), - _ => None, - } - } - - pub fn get_total_count(&self) -> Option { - match *self { - ListResponse { - meta: Some(ListMeta { totalCount, .. }), - .. - } => Some(totalCount), - _ => None, - } - } -} - -pub type Result = ::std::result::Result; - -pub trait JsonMaybe { - fn json_maybe(&mut self) -> Result; -} - -impl JsonMaybe for Response { - fn json_maybe(&mut self) -> Result { - let status = self.status(); - - let mut body = String::new(); - match self.read_to_string(&mut body) { - Err(err) => { - return Err(ApiResponseError { - status: status.clone(), - message: format!("read response: {}", err), - body: "".to_owned(), - }); - } - _ => {} - } - - if !status.is_success() { - return Err(ApiResponseError { - message: format!("status not ok: {}", status), - status: status.clone(), - body: body, - }); - } - - match serde_json::from_str::(&body) { - Ok(v) => { - return Ok(v); - } - Err(err) => { - return Err(ApiResponseError { - message: format!("deserialize body: {}", err), - status: status.clone(), - body: body, - }); - } - } - } -} - -/// Get `meta` and `elements` from a JSON API response -pub fn parse_list_elements_json( - status: StatusCode, - reader: &mut R, - key: &str, -) -> Result> -where - T: Serialize + DeserializeOwned, - R: Read, -{ - use std::collections::BTreeMap; - - #[derive(Debug, Deserialize)] - pub struct Inner { - pub meta: ListMeta, - pub elements: BTreeMap, - } - - #[derive(Debug, Deserialize)] - pub struct Response { - pub list: Inner, - } - - if status == StatusCode::NOT_FOUND { - return Ok(ListResponse { - meta: None, - elements: vec![], - }); - } - - let mut body = String::new(); - match reader.read_to_string(&mut body) { - _ => {} - } - - if !status.is_success() { - return Err(ApiResponseError { - message: status.to_string(), - status: status.clone(), - body: body, - }); - } - - match serde_json::from_str::(&body) { - Ok(mut res) => { - let value = match res.list.elements.remove(key) { - Some(value) => value, - None => { - return Err(ApiResponseError { - message: format!("key '{}' was not found in resposne", key), - status: status.clone(), - body: body, - }); - } - }; - - match serde_json::from_value::>(value) { - Ok(elements) => { - return Ok(ListResponse { - meta: res.list.meta.into(), - elements: elements, - }); - } - Err(err) => { - return Err(ApiResponseError { - message: format!("deserialize json response elements: {}", err.to_string()), - status: status.clone(), - body: body, - }); - } - } - } - Err(err) => { - return Err(ApiResponseError { - message: format!("deserialize json response: {}", err.to_string()), - status: status.clone(), - body: body, - }); - } - } -} - -/// Get single object from a JSON API response -pub fn parse_object_json(status: StatusCode, reader: &mut R, key: &str) -> Result -where - T: Serialize + DeserializeOwned, - R: Read, -{ - use std::collections::BTreeMap; - - let mut body = String::new(); - match reader.read_to_string(&mut body) { - _ => {} - } - - if !status.is_success() { - return Err(ApiResponseError { - message: status.to_string(), - status: status.clone(), - body: body, - }); - } - - match serde_json::from_str::>(&body) { - Ok(mut obj) => match obj.remove(key) { - Some(value) => { - return Ok(value); - } - None => { - return Err(ApiResponseError { - message: format!("key '{}' was not found in resposne", key), - status: status.clone(), - body: body, - }); - } - }, - Err(err) => { - return Err(ApiResponseError { - message: format!("deserialize json response: {}", err.to_string()), - status: status.clone(), - body: body, - }); - } - } -} - -#[cfg(test)] -mod test { - use super::*; - use std::io::Cursor; - - #[test] - fn test_parse_list_elements_json() { - use order::Order; - let mut r = Cursor::new(include_str!("./order/test_order_list_res.json").to_string()); - let res = parse_list_elements_json::(StatusCode::Ok, &mut r, "order").unwrap(); - let meta = res.meta.unwrap(); - assert_eq!(meta.totalCount, 66); - assert_eq!(meta.limit, 10); - assert_eq!(res.elements.len(), 2); - } - - #[test] - fn test_parse_object_json() { - use order::Order; - let mut r = Cursor::new(include_str!("./order/test_order.json").to_string()); - let res = parse_object_json::(StatusCode::Ok, &mut r, "order").unwrap(); - assert_eq!(res.shippingInfo.estimatedDeliveryDate, 1485586800000); - } -} diff --git a/walmart-partner-api/src/result.rs b/walmart-partner-api/src/result.rs index ce21f20..e9e56a8 100644 --- a/walmart-partner-api/src/result.rs +++ b/walmart-partner-api/src/result.rs @@ -1,47 +1,58 @@ -use reqwest::StatusCode; use std::error; use std::fmt; -#[derive(Fail, Debug)] +use reqwest::StatusCode; +use thiserror::Error; + +#[derive(Error, Debug)] pub enum WalmartError { - #[fail(display = "{}", _0)] - Msg(String), + #[error("{0}")] + Auth(String), + + #[error("{0}")] + Csv(String), - #[fail(display = "api response error: {}", _0)] - Api(ApiResponseError), + #[error("api response error: {0}")] + Api(#[from] ApiResponseError), - #[fail(display = "http error: {}", _0)] - Reqwest(::reqwest::Error), + #[error("http error: {0}")] + Reqwest(#[from] reqwest::Error), - #[fail(display = "url error: {}", _0)] - UrlError(::reqwest::UrlError), + #[error("url parse error: {0}")] + UrlParse(#[from] url::ParseError), - #[fail(display = "base64 error: {}", _0)] - Base64(::base64::DecodeError), + #[error("base64 error: {0}")] + Base64(#[from] base64::DecodeError), - #[fail(display = "openssl error: {}", _0)] - OpenSSL(::openssl::error::ErrorStack), + #[error("openssl error: {0}")] + OpenSSL(#[from] openssl::error::ErrorStack), - #[fail(display = "url query serialize error: {}", _0)] - UrlEncoded(::serde_urlencoded::ser::Error), + #[error("url query serialize error: {0}")] + UrlEncoded(#[from] serde_urlencoded::ser::Error), - #[fail(display = "io error: {}", _0)] - Io(::std::io::Error), + #[error("json error: {0}")] + JsonSerde(#[from] serde_json::Error), - #[fail(display = "zip error: {}", _0)] - Zip(::zip::result::ZipError), + #[error("xml error: {0}")] + XmlSerde(#[from] serde_xml_rs::Error), - #[fail(display = "xml parse error: {}", _0)] - XmlParse(::xmltree::ParseError), + #[error("xml serialization error: {0}")] + XmlSer(String), - #[fail(display = "csv error: {}", _0)] - Csv(::csv::Error), + #[error("io error: {0}")] + Io(#[from] std::io::Error), - #[fail(display = "invalid header value: {}", _0)] - InvalidHeaderValue(::reqwest::header::InvalidHeaderValue), + #[error("{0}")] + Zip(#[from] zip::result::ZipError), - #[fail(display = "unexpected xml: {}", _0)] - UnexpectedXml(String), + #[error("invalid header value: {0}")] + InvalidHeaderValue(#[from] reqwest::header::InvalidHeaderValue), +} + +impl From for WalmartError { + fn from(e: xml_builder::XMLError) -> Self { + Self::XmlSer(format!("{:?}", e)) + } } impl WalmartError { @@ -56,37 +67,18 @@ impl WalmartError { false } } + WalmartError::Api(ref err) => { + let code = err.status.as_u16(); + code == 429 || code == 500 || code == 503 + } _ => false, } } } -macro_rules! impl_from { - ($v:ident($t:ty)) => { - impl From<$t> for WalmartError { - fn from(e: $t) -> Self { - WalmartError::$v(e) - } - } - }; -} - -impl_from!(Msg(String)); -impl_from!(Api(ApiResponseError)); -impl_from!(Reqwest(::reqwest::Error)); -impl_from!(UrlError(::reqwest::UrlError)); -impl_from!(Base64(::base64::DecodeError)); -impl_from!(OpenSSL(::openssl::error::ErrorStack)); -impl_from!(UrlEncoded(::serde_urlencoded::ser::Error)); -impl_from!(Io(::std::io::Error)); -impl_from!(Zip(::zip::result::ZipError)); -impl_from!(XmlParse(::xmltree::ParseError)); -impl_from!(Csv(::csv::Error)); -impl_from!(InvalidHeaderValue(::reqwest::header::InvalidHeaderValue)); - #[derive(Debug)] pub struct ApiResponseError { - pub message: String, + pub path: String, pub status: StatusCode, pub body: String, } @@ -95,8 +87,8 @@ impl fmt::Display for ApiResponseError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!( f, - "API Error: status = '{}', message = '{}'", - self.status, self.message + "Walmart API error: status = '{}', path = '{}", + self.status, self.path, ) } } diff --git a/walmart-partner-api/src/shared/error.rs b/walmart-partner-api/src/shared/error.rs new file mode 100644 index 0000000..3dae9b6 --- /dev/null +++ b/walmart-partner-api/src/shared/error.rs @@ -0,0 +1,42 @@ +#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] +pub struct ResponseError { + #[serde(rename = "code")] + pub code: String, + #[serde(rename = "field", skip_serializing_if = "Option::is_none")] + pub field: Option, + #[serde(rename = "description", skip_serializing_if = "Option::is_none")] + pub description: Option, + #[serde(rename = "info", skip_serializing_if = "Option::is_none")] + pub info: Option, + #[serde(rename = "severity", skip_serializing_if = "Option::is_none")] + pub severity: Option, + #[serde(rename = "category", skip_serializing_if = "Option::is_none")] + pub category: Option, + #[serde(rename = "causes", skip_serializing_if = "Option::is_none")] + pub causes: Option>, + #[serde(rename = "errorIdentifiers", skip_serializing_if = "Option::is_none")] + pub error_identifiers: Option>, + #[serde(rename = "component", skip_serializing_if = "Option::is_none")] + pub component: Option, + #[serde(rename = "type", skip_serializing_if = "Option::is_none")] + pub r#type: Option, + #[serde(rename = "serviceName", skip_serializing_if = "Option::is_none")] + pub service_name: Option, + #[serde( + rename = "gatewayErrorCategory", + skip_serializing_if = "Option::is_none" + )] + pub gateway_error_category: Option, +} + +#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] +pub struct ResponseErrorCause { + #[serde(rename = "code", skip_serializing_if = "Option::is_none")] + pub code: Option, + #[serde(rename = "field", skip_serializing_if = "Option::is_none")] + pub field: Option, + #[serde(rename = "type", skip_serializing_if = "Option::is_none")] + pub r#type: Option, + #[serde(rename = "description", skip_serializing_if = "Option::is_none")] + pub description: Option, +} diff --git a/walmart-partner-api/src/shared/feed.rs b/walmart-partner-api/src/shared/feed.rs new file mode 100644 index 0000000..b844176 --- /dev/null +++ b/walmart-partner-api/src/shared/feed.rs @@ -0,0 +1,173 @@ +use chrono::{DateTime, Utc}; + +use crate::shared::error::ResponseError; + +#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] +pub struct FeedAck { + /// A unique ID, returned from the Bulk Upload API, used for tracking the feed file + #[serde(rename = "feedId", skip_serializing_if = "Option::is_none")] + pub feed_id: Option, +} + +#[derive(Debug, Clone, Serialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct GetAllFeedStatusesQuery { + pub feed_id: Option, + pub limit: Option, + pub offset: Option, +} + +#[derive(Debug, Clone, Serialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct GetFeedAndItemStatusQuery { + pub include_details: Option, + pub limit: Option, + pub offset: Option, +} + +#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] +pub struct GetFeedStatuses { + #[serde(rename = "errors", skip_serializing_if = "Option::is_none")] + pub errors: Option>, + /// Total number of feeds returned + #[serde(rename = "totalResults", skip_serializing_if = "Option::is_none")] + pub total_results: Option, + /// The object response to the starting number, where 0 is the first available + #[serde(rename = "offset", skip_serializing_if = "Option::is_none")] + pub offset: Option, + /// The number of items to be returned + #[serde(rename = "limit", skip_serializing_if = "Option::is_none")] + pub limit: Option, + #[serde(rename = "results")] + #[serde(default)] + pub results: FeedResults, +} + +#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] +pub struct FeedResults { + /// The feed status results + #[serde(rename = "feed")] + #[serde(default)] + pub feed: Vec, +} + +#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] +pub struct FeedResult { + /// A unique ID used for tracking the Feed File + #[serde(rename = "feedId", skip_serializing_if = "Option::is_none")] + pub feed_id: Option, + /// The source of the feed + #[serde(rename = "feedSource", skip_serializing_if = "Option::is_none")] + pub feed_source: Option, + /// The feed type + #[serde(rename = "feedType", skip_serializing_if = "Option::is_none")] + pub feed_type: Option, + /// The seller ID + #[serde(rename = "partnerId", skip_serializing_if = "Option::is_none")] + pub partner_id: Option, + /// The number of items received + #[serde(rename = "itemsReceived", skip_serializing_if = "Option::is_none")] + pub items_received: Option, + /// The number of items in the feed that have successfully processed + #[serde(rename = "itemsSucceeded", skip_serializing_if = "Option::is_none")] + pub items_succeeded: Option, + /// The number of items in the feed that failed due to a data or system error + #[serde(rename = "itemsFailed", skip_serializing_if = "Option::is_none")] + pub items_failed: Option, + /// The number of items in the feed that are still in progress + #[serde(rename = "itemsProcessing", skip_serializing_if = "Option::is_none")] + pub items_processing: Option, + /// Can be one of the following: RECEIVED, INPROGRESS, PROCESSED, or ERROR. For details, see the definitions listed under 'Feed Statuses' at the beginning of this section. + #[serde(rename = "feedStatus", skip_serializing_if = "Option::is_none")] + pub feed_status: Option, + /// The date and time the feed was submitted. Format: yyyymmddThh:mm:ss.xxxz + #[serde(rename = "feedDate", skip_serializing_if = "Option::is_none")] + pub feed_date: Option>, + /// The batch ID for the feed, if provided + #[serde(rename = "batchId", skip_serializing_if = "Option::is_none")] + pub batch_id: Option, + /// The most recent time the feed was modified. Format: yyyymmddThh:mm:ss.xxxz + #[serde(rename = "modifiedDtm", skip_serializing_if = "Option::is_none")] + pub modified_dtm: Option>, +} + +#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] +pub struct GetFeedItemStatus { + #[serde(rename = "errors", skip_serializing_if = "Option::is_none")] + pub errors: Option>, + /// A unique ID used for tracking the Feed File + #[serde(rename = "feedId", skip_serializing_if = "Option::is_none")] + pub feed_id: Option, + /// Can be one of the following: RECEIVED, INPROGRESS, PROCESSED, or ERROR + #[serde(rename = "feedStatus", skip_serializing_if = "Option::is_none")] + pub feed_status: Option, + /// The number of items received in the feed + #[serde(rename = "itemsReceived", skip_serializing_if = "Option::is_none")] + pub items_received: Option, + /// The number of items in the feed that processed successfully + #[serde(rename = "itemsSucceeded", skip_serializing_if = "Option::is_none")] + pub items_succeeded: Option, + /// The number of items in the feed that failed due to a data or system error + #[serde(rename = "itemsFailed", skip_serializing_if = "Option::is_none")] + pub items_failed: Option, + /// The number of items in the feed that are still processing + #[serde(rename = "itemsProcessing", skip_serializing_if = "Option::is_none")] + pub items_processing: Option, + /// The object response to the starting number, where 0 is the first entity available for request + #[serde(rename = "offset", skip_serializing_if = "Option::is_none")] + pub offset: Option, + /// The number of items returned. Cannot be greater than 1000. + #[serde(rename = "limit", skip_serializing_if = "Option::is_none")] + pub limit: Option, + #[serde(rename = "itemDetails")] + #[serde(default)] + pub item_details: FeedItemDetails, +} + +#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] +pub struct FeedItemDetails { + /// The ingestion status of an individual item + #[serde(rename = "itemIngestionStatus")] + #[serde(default)] + pub item_ingestion_status: Vec, +} + +#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] +pub struct FeedItemIngestionStatus { + /// Mart ID that a user or seller uses for a marketplace + #[serde(rename = "martId", skip_serializing_if = "Option::is_none")] + pub mart_id: Option, + /// An arbitrary alphanumeric unique ID, seller-specified, identifying each item. + #[serde(rename = "sku", skip_serializing_if = "Option::is_none")] + pub sku: Option, + /// An alphanumeric product ID, generated by Walmart + #[serde(rename = "wpid", skip_serializing_if = "Option::is_none")] + pub wpid: Option, + /// index of items in the feed + #[serde(rename = "index", skip_serializing_if = "Option::is_none")] + pub index: Option, + /// Can be one of the following: DATA_ERROR, SYSTEM_ERROR, TIMEOUT_ERROR, or INPROGRESS + #[serde(rename = "ingestionStatus")] + pub ingestion_status: String, + #[serde(rename = "ingestionErrors", skip_serializing_if = "Option::is_none")] + pub ingestion_errors: Option, +} + +#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] +pub struct FeedItemIngestionErrors { + #[serde(rename = "ingestionError", skip_serializing_if = "Option::is_none")] + pub ingestion_error: Option>, +} + +#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] +pub struct FeedItemIngestionError { + /// Error Type + #[serde(rename = "type")] + pub r#type: String, + /// Error code + #[serde(rename = "code")] + pub code: String, + /// Error description + #[serde(rename = "description", skip_serializing_if = "Option::is_none")] + pub description: Option, +} diff --git a/walmart-partner-api/src/shared/inventory.rs b/walmart-partner-api/src/shared/inventory.rs new file mode 100644 index 0000000..7c641a1 --- /dev/null +++ b/walmart-partner-api/src/shared/inventory.rs @@ -0,0 +1,29 @@ +use xml_builder::XMLElement; + +use crate::WalmartResult; + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct InventoryQuantity { + /// The unit of measurement. Example: 'EACH' + #[serde(rename = "unit")] + pub unit: String, + /// The number available in the inventory + #[serde(rename = "amount")] + pub amount: i32, +} + +impl crate::XmlSer for InventoryQuantity { + fn to_xml(&self) -> WalmartResult { + let mut root = XMLElement::new("quantity"); + + let mut unit = XMLElement::new("unit"); + unit.add_text(self.unit.clone())?; + root.add_child(unit)?; + + let mut amount = XMLElement::new("amount"); + amount.add_text(self.amount.to_string())?; + root.add_child(amount)?; + + Ok(root) + } +} diff --git a/walmart-partner-api/src/shared/item.rs b/walmart-partner-api/src/shared/item.rs new file mode 100644 index 0000000..7041740 --- /dev/null +++ b/walmart-partner-api/src/shared/item.rs @@ -0,0 +1,70 @@ +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct GetAllItems { + /// Items included in the response list + #[serde(rename = "ItemResponse")] + #[serde(alias = "itemResponse")] + #[serde(default)] + pub item_response: Vec, + /// Total items for the query + #[serde(rename = "totalItems", skip_serializing_if = "Option::is_none")] + pub total_items: Option, + /// Used for pagination to fetch the next set of items + #[serde(rename = "nextCursor", skip_serializing_if = "Option::is_none")] + pub next_cursor: Option, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct RetireItem { + /// An arbitrary alphanumeric unique ID, specified by the seller, which identifies each item. + #[serde(rename = "sku")] + pub sku: String, + /// Message confirming the deletion or retirement of an item from the Walmart Catalog + #[serde(rename = "message", skip_serializing_if = "Option::is_none")] + pub message: Option, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct Item { + /// The marketplace name. Example: Walmart_US + #[serde(rename = "mart", skip_serializing_if = "Option::is_none")] + pub mart: Option, + /// An arbitrary alphanumeric unique ID, specified by the seller, which identifies each item. + #[serde(rename = "sku")] + pub sku: String, + /// The Walmart Product ID assigned by Walmart to the item when listed on Walmart.com + #[serde(rename = "wpid", skip_serializing_if = "Option::is_none")] + pub wpid: Option, + /// The 12-digit bar code used extensively for retail packaging in the United States + #[serde(rename = "upc", skip_serializing_if = "Option::is_none")] + pub upc: Option, + /// The GTIN-compatible Product ID (i.e. UPC or EAN). UPCs must be 12 or 14 digitis in length. EANs must be 13 digits in length. + #[serde(rename = "gtin", skip_serializing_if = "Option::is_none")] + pub gtin: Option, + /// A seller-specified, alphanumeric string uniquely identifying the product name. Example: 'Sterling Silver Blue Diamond Heart Pendant with 18in Chain' + #[serde(rename = "productName", skip_serializing_if = "Option::is_none")] + pub product_name: Option, + /// Walmart assigned an item shelf name + #[serde(rename = "shelf", skip_serializing_if = "Option::is_none")] + pub shelf: Option, + /// A seller-specified, alphanumeric string uniquely identifying the Product Type. Example: 'Diamond' + #[serde(rename = "productType", skip_serializing_if = "Option::is_none")] + pub product_type: Option, + #[serde(rename = "price", skip_serializing_if = "Option::is_none")] + pub price: Option, + /// The status of an item when the item is in the submission process. The status can be one of the following: PUBLISHED, READY_TO_PUBLISH, IN_PROGRESS, UNPUBLISHED, STAGE, or SYSTEM_PROBLEM. + #[serde(rename = "publishedStatus", skip_serializing_if = "Option::is_none")] + pub published_status: Option, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct GetItem { + #[serde(rename = "ItemResponse")] + #[serde(alias = "itemResponse")] + pub item_response: Item, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct Price { + pub currency: String, + pub amount: String, +} diff --git a/walmart-partner-api/src/shared/mod.rs b/walmart-partner-api/src/shared/mod.rs new file mode 100644 index 0000000..ae8e8d0 --- /dev/null +++ b/walmart-partner-api/src/shared/mod.rs @@ -0,0 +1,7 @@ +pub use error::*; + +pub mod error; +pub mod feed; +pub mod inventory; +pub mod item; +pub mod order; diff --git a/walmart-partner-api/src/shared/order.rs b/walmart-partner-api/src/shared/order.rs new file mode 100644 index 0000000..0c9a92e --- /dev/null +++ b/walmart-partner-api/src/shared/order.rs @@ -0,0 +1,296 @@ +use chrono::{DateTime, Utc}; +use xml_builder::XMLElement; + +use crate::{WalmartResult, XmlSer}; + +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] +pub struct OrderListMeta { + /// Total no of purchase orders. + #[serde(rename = "totalCount", skip_serializing_if = "Option::is_none")] + pub total_count: Option, + /// Number of purchase orders in the current page. + #[serde(rename = "limit", skip_serializing_if = "Option::is_none")] + pub limit: Option, + /// String to be used as query parameter for getting next set of purchase orders, when more than 200 orders are retrieved. + #[serde(rename = "nextCursor", skip_serializing_if = "Option::is_none")] + pub next_cursor: Option, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct ShippingInfo { + /// The customer's phone number + #[serde(rename = "phone")] + pub phone: String, + /// The estimated time and date for the delivery of the item. Format: yyyy-MM-ddThh:MM:ssZ Example: '2020-06-15T06:00:00Z' + #[serde(rename = "estimatedDeliveryDate")] + pub estimated_delivery_date: DateTime, + /// The estimated time and date when the item will be shipped. Format: yyyy-MM-ddThh:MM:ssZ Example: '2020-06-15T06:00:00Z' + #[serde(rename = "estimatedShipDate")] + pub estimated_ship_date: DateTime, + /// The shipping method. Can be one of the following: Standard, Express, OneDay, WhiteGlove, Value or Freight + #[serde(rename = "methodCode")] + pub method_code: String, + #[serde(rename = "postalAddress")] + pub postal_address: PostalAddress, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct PostalAddress { + /// The name for the person/place of shipping address + #[serde(rename = "name")] + pub name: String, + /// The first line of the shipping address + #[serde(rename = "address1")] + pub address1: String, + /// The second line of the shipping address + #[serde(rename = "address2", skip_serializing_if = "Option::is_none")] + pub address2: Option, + /// The city of the shipping address + #[serde(rename = "city")] + pub city: String, + /// The state of the shipping address + #[serde(rename = "state")] + pub state: String, + /// The zip code of the shipping address + #[serde(rename = "postalCode")] + pub postal_code: String, + /// The country of the shipping address + #[serde(rename = "country")] + pub country: String, + /// The address type, example: 'RESIDENTIAL' + #[serde(rename = "addressType", skip_serializing_if = "Option::is_none")] + pub address_type: Option, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct OrderLineItem { + /// The name of the product associated with the line item. Example: 'Kenmore CF1' or '2086883 Canister Secondary Filter Generic 2 Pack' + #[serde(rename = "productName")] + pub product_name: String, + /// An arbitrary alphanumeric unique ID, assigned to each item in the XSD file. + #[serde(rename = "sku")] + pub sku: String, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct OrderLineCharges { + /// List of elements that make up a charge + #[serde(rename = "charge")] + #[serde(default)] + pub charge: Vec, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct OrderLineCharge { + /// The charge type for line items can be one of the following: PRODUCT or SHIPPING For details, refer to 'Charge Types' + #[serde(rename = "chargeType")] + pub charge_type: String, + /// If chargeType is PRODUCT, chargeName is Item Price. If chargeType is SHIPPING, chargeName is Shipping + #[serde(rename = "chargeName")] + pub charge_name: String, + #[serde(rename = "chargeAmount")] + pub charge_amount: CurrencyAmount, + #[serde(rename = "tax", skip_serializing_if = "Option::is_none")] + pub tax: Option, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct CurrencyAmount { + /// The type of currency for the charge. Example: USD for US Dollars + #[serde(rename = "currency")] + pub currency: String, + /// The numerical amount for that charge. Example: 9.99 + #[serde(rename = "amount")] + pub amount: f64, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct Tax { + /// The name associated with the tax. Example: 'Sales Tax' + #[serde(rename = "taxName")] + pub tax_name: String, + #[serde(rename = "taxAmount")] + pub tax_amount: CurrencyAmount, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct OrderLineStatusQuantity { + /// Always use 'EACH' + #[serde(rename = "unitOfMeasurement")] + pub unit_of_measurement: String, + /// Always use '1' + #[serde(rename = "amount")] + pub amount: String, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct OrderLineStatuses { + /// Details about the Order Line status + #[serde(rename = "orderLineStatus")] + #[serde(default)] + pub order_line_status: Vec, +} + +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] +pub struct ShipNode { + /// Specifies the type of shipNode. Allowed values are SellerFulfilled, WFSFulfilled and 3PLFulfilled + #[serde(rename = "type", skip_serializing_if = "Option::is_none")] + pub type_: Option, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct OrderLineStatus { + /// Should be 'Created' + #[serde(rename = "status")] + pub status: String, + #[serde(rename = "statusQuantity")] + pub status_quantity: OrderLineStatusQuantity, + /// If order is cancelled, cancellationReason will explain the reason + #[serde(rename = "cancellationReason", skip_serializing_if = "Option::is_none")] + pub cancellation_reason: Option, + #[serde(rename = "trackingInfo", skip_serializing_if = "Option::is_none")] + pub tracking_info: Option, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct OrderLineTrackingInfo { + /// The date the package was shipped + #[serde(rename = "shipDateTime")] + pub ship_date_time: DateTime, + #[serde(rename = "carrierName")] + pub carrier_name: OrderLineTrackingCarrier, + /// The shipping method. Can be one of the following: Standard, Express, Oneday, or Freight + #[serde(rename = "methodCode")] + pub method_code: String, + /// The shipment tracking number + #[serde(rename = "trackingNumber")] + pub tracking_number: String, + /// The URL for tracking the shipment + #[serde(rename = "trackingURL", skip_serializing_if = "Option::is_none")] + pub tracking_url: Option, +} + +impl XmlSer for OrderLineTrackingInfo { + fn to_xml(&self) -> WalmartResult { + let mut tracking_info = XMLElement::new("trackingInfo"); + let mut ship_date_time = XMLElement::new("shipDateTime"); + ship_date_time.add_text(self.ship_date_time.to_rfc3339())?; + tracking_info.add_child(ship_date_time)?; + + tracking_info.add_child(self.carrier_name.to_xml()?)?; + + let mut method_code = XMLElement::new("methodCode"); + method_code.add_text(self.method_code.clone())?; + tracking_info.add_child(method_code)?; + + let mut tracking_number = XMLElement::new("trackingNumber"); + tracking_number.add_text(self.tracking_number.clone())?; + tracking_info.add_child(tracking_number)?; + + if let Some(tracking_url_v) = self.tracking_url.clone() { + let mut tracking_url = XMLElement::new("trackingURL"); + tracking_url.add_text(tracking_url_v)?; + tracking_info.add_child(tracking_url)?; + } + + Ok(tracking_info) + } +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct OrderLineTrackingCarrier { + /// Other carrier name + #[serde(rename = "otherCarrier", skip_serializing_if = "Option::is_none")] + pub other_carrier: Option, + /// The package shipment carrier + #[serde(rename = "carrier", skip_serializing_if = "Option::is_none")] + pub carrier: Option, +} + +impl XmlSer for OrderLineTrackingCarrier { + fn to_xml(&self) -> WalmartResult { + let mut carrier_name = XMLElement::new("carrierName"); + if let Some(other_carrier_v) = self.other_carrier.clone() { + let mut other_carrier = XMLElement::new("otherCarrier"); + other_carrier.add_text(other_carrier_v)?; + carrier_name.add_child(other_carrier)?; + } + + if let Some(carrier_v) = self.carrier.clone() { + let mut carrier = XMLElement::new("carrier"); + carrier.add_text(carrier_v)?; + carrier_name.add_child(carrier)?; + } + Ok(carrier_name) + } +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct OrderRefund { + #[serde(rename = "refundId", skip_serializing_if = "Option::is_none")] + pub refund_id: Option, + #[serde(rename = "refundComments", skip_serializing_if = "Option::is_none")] + pub refund_comments: Option, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct OrderFulfillment { + #[serde(rename = "fulfillmentOption", skip_serializing_if = "Option::is_none")] + pub fulfillment_option: Option, + #[serde(rename = "shipMethod", skip_serializing_if = "Option::is_none")] + pub ship_method: Option, + #[serde(rename = "storeId", skip_serializing_if = "Option::is_none")] + pub store_id: Option, +} + +#[derive(Debug, Clone, Serialize)] +pub struct ShipOrderLineAsn { + pub package_asn: String, + pub pallet_asn: Option, +} + +impl XmlSer for ShipOrderLineAsn { + fn to_xml(&self) -> WalmartResult { + let mut asn = XMLElement::new("asn"); + + let mut package_asn = XMLElement::new("packageASN"); + package_asn.add_text(self.package_asn.clone())?; + asn.add_child(package_asn)?; + + if let Some(pallet_asn_v) = self.pallet_asn.clone() { + let mut pallet_asn = XMLElement::new("palletASN"); + pallet_asn.add_text(pallet_asn_v)?; + asn.add_child(pallet_asn)?; + } + Ok(asn) + } +} + +#[derive(Debug, Clone, Default, Serialize)] +pub struct ShipOrderLineStatusQuantity { + pub unit_of_measurement: Option, + pub amount: Option, +} + +impl XmlSer for Option { + fn to_xml(&self) -> WalmartResult { + let mut status_quantity = XMLElement::new("statusQuantity"); + let mut unit_of_measurement = XMLElement::new("unitOfMeasurement"); + let unit_of_measurement_v = self + .as_ref() + .and_then(|v| v.unit_of_measurement.clone()) + .unwrap_or("EACH".to_string()); + unit_of_measurement.add_text(unit_of_measurement_v)?; + status_quantity.add_child(unit_of_measurement)?; + + let mut amount = XMLElement::new("amount"); + let amount_v = self + .as_ref() + .and_then(|v| v.amount.clone()) + .unwrap_or("1".to_string()); + amount.add_text(amount_v)?; + status_quantity.add_child(amount)?; + + Ok(status_quantity) + } +} diff --git a/walmart-partner-api/src/sign.rs b/walmart-partner-api/src/sign.rs index 753a16a..6cfecc5 100644 --- a/walmart-partner-api/src/sign.rs +++ b/walmart-partner-api/src/sign.rs @@ -1,7 +1,5 @@ -//! Implement Walmart's authentication signature -//! -//! [Walmart Documentation](https://developer.walmart.com/#/apicenter/contentProvider#authentication) - +/// Implement Walmart's authentication signature +/// use base64::encode; use openssl::hash::MessageDigest; use openssl::pkey::{PKey, Private}; @@ -17,7 +15,7 @@ pub struct Signature { } impl Signature { - /// Cosntruct a new `Signature` + /// Construct a new `Signature` pub fn new(consumer_id: &str, private_key: &str) -> WalmartResult { let pem_key = format!( "-----BEGIN PRIVATE KEY-----\n{}\n-----END PRIVATE KEY-----", @@ -62,7 +60,7 @@ mod tests { // Test data from https://github.com/fillup/walmart-auth-signature-php/blob/develop/tests/SignatureTest.php let fake_key = "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAKzXEfCYdnBNkKAwVbCpg/tR40WixoZtiuEviSEi4+LdnYAAPy57Qw6+9eqJGTh9iCB2wP/I8lWh5TZ49Hq/chjTCPeJiOqi6bvX1xzyBlSq2ElSY3iEVKeVoQG/5f9MYQLEj5/vfTWSNASsMwnNeBbbHcV1S1aY9tOsXCzRuxapAgMBAAECgYBjkM1j1OA9l2Ed9loWl8BQ8X5D6h4E6Gudhx2uugOe9904FGxRIW6iuvy869dchGv7j41ki+SV0dpRw+HKKCjYE6STKpe0YwIm/tml54aNDQ0vQvF8JWILca1a7v3Go6chf3Ib6JPs6KVsUuNo+Yd+jKR9GAKgnDeXS6NZlTBUAQJBANex815VAySumJ/n8xR+h/dZ2V5qGj6wu3Gsdw6eNYKQn3I8AGQw8N4yzDUoFnrQxqDmP3LOyr3/zgOMNTdszIECQQDNIxiZOVl3/Sjyxy9WHMk5qNfSf5iODynv1OlTG+eWao0Wj/NdfLb4pwxRsf4XZFZ1SQNkbNne7+tEO8FTG1YpAkAwNMY2g/ty3E6iFl3ea7UJlBwfnMkGz8rkye3F55f/+UCZcE2KFuIOVv4Kt03m3vg1h6AQkaUAN8acRl6yZ2+BAkEAke2eiRmYANiR8asqjGqr5x2qcm8ceiplXdwrI1kddQ5VUbCTonSewOIszEz/gWp6arLG/ADHOGWaCo8rptAyiQJACXd1ddXUAKs6x3l752tSH8dOde8nDBgF86NGvgUnBiAPPTmJHuhWrmOZmNaB68PsltEiiFwWByGFV+ld9VKmKg=="; let signature = Signature::new("f3aead96-d681-41c9-9b81-bb4facacd8f0", fake_key).unwrap(); - let signed = signature.sign("https://developer.walmart.com/proxy/item-api-doc-app/rest/v3/feeds?includeDetails=false&offset=0&limit=50", Method::Get, 1502165720641).unwrap(); + let signed = signature.sign("https://developer.walmart.com/proxy/item-api-doc-app/rest/v3/feeds?includeDetails=false&offset=0&limit=50", Method::GET, 1502165720641).unwrap(); assert_eq!( signed, "joVK3ddX6Fso7adAjuT1FIX5D5So8ue1Am4MwY8ncsP7zLBtnwMYiveyfQeqGm2+GQbtfOy5LvCkzUeEchLznJFZzF7vJaTHhENrDsRIzjPsgJYpRO8FgdfgSLUhO7v0skjHezMxuJr9ROWia900LOZ6QU+u/LvoChbxxZye9GE=" diff --git a/walmart-partner-api/src/test_util/mod.rs b/walmart-partner-api/src/test_util/mod.rs new file mode 100644 index 0000000..519a74d --- /dev/null +++ b/walmart-partner-api/src/test_util/mod.rs @@ -0,0 +1,42 @@ +use crate::{WalmartCredential, XmlSer}; + +pub fn get_client_ca() -> crate::ca::Client { + let mock_url = mockito::server_url(); + let mut client = crate::ca::Client::new(WalmartCredential::Signature { + channel_type: "".to_string(), + consumer_id: "".to_string(), + private_key: "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAG7HmWVk9bwH1vefuW/DrCp41Rf8YpQHeJY3oVZZdFGVJ2q82QjacaKIoTmUm/5xrnTs//E+yP9OGae2dU+vCI33wVaVjyHsY8oCo/PMb4L6Q4+mq+RmbXREef+mNx0CLlCoFq4ytkKKzvvHk7XKckW7oNwMf9Az7Q7gUIv4odxqEOGn/c5zk7hwYNsYlxzwCycPt3YO/XKIUwOJ0qDY65Ahv9i93GA74MoHc+NqWLd6xwPtv9OP2JhAiFXfARxcEWd6dAarPbbtua25Fq3IOBw4HkcvyT9ijHNObv7VCHauHMkF7nN1nzoyzs/tE8KD6h37B6HNSjDFaTEHfva9ZZ8CAwEAAQKCAQBTGpFMqyxdXlQ5dy0ZVuT1B6h0UfVxrxkbN6hkqr7D5Oyo+fqm1ZihoXWxSHatroJ9XL20MLGANQqx8gKXQGtedRoo5hF2FWvWw5xS7G5LB4tfXF1e/ifmLOiIjByUOmqcPzykeY6Y5KDZ6KI6oiCPh23pJcdMXWfc3RIPrvld64aZRWY2/DdX/8WOlCqACBVGwWhUjyt3fLER5lHL9Pz+VlZZDG33bfYilx33XMdcUS+P6BDykC3KEsnfh0Ml1PsWzt6gMCQaqyifVN51uUY9ur6mJ4Hn3EapyJmAFClxk2x8K7LNEDeySB3UW7Y1GfOB0OSUoeeMRxR+JvXaepMBAoGBALK7vt0Bl7MAN6NsIPHJGkecZ+nUnPmn9j+P2G8YQ+XvZqIUVmkPdFvPeQ+f1JrJhWrlnGOW34rC6WM0Wmlh4rRlVnbNaLVWaGsfCX7fJ6Z/i5+W28IMK60M0uOOS3WuXCLP54OYdImI4VIRzm4lwWvBwBYMEoFA/W+fhUWonjd/AoGBAJ6re3MB3/1bemHsOC/a6eCKZD29Jkh9ZrAWcrKF8Xk/vLzAnqaDGc2/8GK7Cp2pLkNjvYRWrS2PBW+kcDrEKJYphD/2WFv04lmwANsBoxGArO4oKanEkDZzoQoNDZMrT/dZ0suem4Bcpwc/fywomYrGbnCBGMLSlkulZwr/GOHhAoGBAIS7EjGUBjEDP05YdVq5So/VogGvR+fLCP8I9uUBsyKll6VTzxv0QygPOksVGdDdSPwqieoXV+j3eFSYw2+xJqdq/jv5rQHFqoOqp+WVGR/3Zhvc71P6r9CyTkZ5HKbHFlsv5DEA3cJpaVMGMDPyS+KXHuwAiRl9xvfHEjS51M1HAoGAPrrhJYjaO1pNOiWf2RudV06fbuE3H3WkgX1+fyIBY8RVI/KrRn2SWAvIR+BWxBo81hu6s3VpJhfjOE40qKcgvK1RQdBtAn4AdyDkVbGB/Mt4kveB8UJrGXwBcO3ULhjzloEGm8XrCIaY6n6qEpVCjuEAjK4dUfjbvrB32pscBUECgYEAiqNdh+a0dE41dW0wK3muW3dj897QqQDq+9+MYydEqua8SwdeHwCP1k+JMPEErB4mCCD0Ggn9uzeO4Q0jHlwnvpqIFix2HD7ggYXv0h9mbveTZkL17HL2br3KnsazOZ1lUm6IWVGgkUExCbqgMfX61Q9b0z/voCHwamORbHp0Bys=".to_string(), + }) + .unwrap(); + client.set_base_url(&mock_url); + client +} + +pub fn get_client_us() -> crate::us::Client { + let mock_url = mockito::server_url(); + let mut client = crate::us::Client::new(WalmartCredential::Signature { + channel_type: "".to_string(), + consumer_id: "".to_string(), + private_key: "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAG7HmWVk9bwH1vefuW/DrCp41Rf8YpQHeJY3oVZZdFGVJ2q82QjacaKIoTmUm/5xrnTs//E+yP9OGae2dU+vCI33wVaVjyHsY8oCo/PMb4L6Q4+mq+RmbXREef+mNx0CLlCoFq4ytkKKzvvHk7XKckW7oNwMf9Az7Q7gUIv4odxqEOGn/c5zk7hwYNsYlxzwCycPt3YO/XKIUwOJ0qDY65Ahv9i93GA74MoHc+NqWLd6xwPtv9OP2JhAiFXfARxcEWd6dAarPbbtua25Fq3IOBw4HkcvyT9ijHNObv7VCHauHMkF7nN1nzoyzs/tE8KD6h37B6HNSjDFaTEHfva9ZZ8CAwEAAQKCAQBTGpFMqyxdXlQ5dy0ZVuT1B6h0UfVxrxkbN6hkqr7D5Oyo+fqm1ZihoXWxSHatroJ9XL20MLGANQqx8gKXQGtedRoo5hF2FWvWw5xS7G5LB4tfXF1e/ifmLOiIjByUOmqcPzykeY6Y5KDZ6KI6oiCPh23pJcdMXWfc3RIPrvld64aZRWY2/DdX/8WOlCqACBVGwWhUjyt3fLER5lHL9Pz+VlZZDG33bfYilx33XMdcUS+P6BDykC3KEsnfh0Ml1PsWzt6gMCQaqyifVN51uUY9ur6mJ4Hn3EapyJmAFClxk2x8K7LNEDeySB3UW7Y1GfOB0OSUoeeMRxR+JvXaepMBAoGBALK7vt0Bl7MAN6NsIPHJGkecZ+nUnPmn9j+P2G8YQ+XvZqIUVmkPdFvPeQ+f1JrJhWrlnGOW34rC6WM0Wmlh4rRlVnbNaLVWaGsfCX7fJ6Z/i5+W28IMK60M0uOOS3WuXCLP54OYdImI4VIRzm4lwWvBwBYMEoFA/W+fhUWonjd/AoGBAJ6re3MB3/1bemHsOC/a6eCKZD29Jkh9ZrAWcrKF8Xk/vLzAnqaDGc2/8GK7Cp2pLkNjvYRWrS2PBW+kcDrEKJYphD/2WFv04lmwANsBoxGArO4oKanEkDZzoQoNDZMrT/dZ0suem4Bcpwc/fywomYrGbnCBGMLSlkulZwr/GOHhAoGBAIS7EjGUBjEDP05YdVq5So/VogGvR+fLCP8I9uUBsyKll6VTzxv0QygPOksVGdDdSPwqieoXV+j3eFSYw2+xJqdq/jv5rQHFqoOqp+WVGR/3Zhvc71P6r9CyTkZ5HKbHFlsv5DEA3cJpaVMGMDPyS+KXHuwAiRl9xvfHEjS51M1HAoGAPrrhJYjaO1pNOiWf2RudV06fbuE3H3WkgX1+fyIBY8RVI/KrRn2SWAvIR+BWxBo81hu6s3VpJhfjOE40qKcgvK1RQdBtAn4AdyDkVbGB/Mt4kveB8UJrGXwBcO3ULhjzloEGm8XrCIaY6n6qEpVCjuEAjK4dUfjbvrB32pscBUECgYEAiqNdh+a0dE41dW0wK3muW3dj897QqQDq+9+MYydEqua8SwdeHwCP1k+JMPEErB4mCCD0Ggn9uzeO4Q0jHlwnvpqIFix2HD7ggYXv0h9mbveTZkL17HL2br3KnsazOZ1lUm6IWVGgkUExCbqgMfX61Q9b0z/voCHwamORbHp0Bys=" + .to_string(), + }) + .unwrap(); + client.set_base_url(&mock_url); + client +} + +pub fn assert_xml_str_eq(got: &str, want: &str, msg: impl ToString) { + let got: String = got.split_whitespace().collect(); + let want: String = want.split_whitespace().collect(); + assert_eq!(got, want, "{}", msg.to_string()); +} + +pub fn assert_xml_eq(got: impl XmlSer, want: &str, msg: impl ToString) { + let mut buf = Vec::new(); + got + .to_xml() + .unwrap() + .render(&mut buf, false, false) + .unwrap(); + assert_xml_str_eq(&String::from_utf8(buf).unwrap(), want, msg); +} diff --git a/walmart-partner-api/src/utils.rs b/walmart-partner-api/src/utils.rs index 787c7af..873205d 100644 --- a/walmart-partner-api/src/utils.rs +++ b/walmart-partner-api/src/utils.rs @@ -1,12 +1,12 @@ +use std::fmt; + use chrono::{DateTime, TimeZone, Utc}; use serde::de; -use serde::Deserializer; -use std::fmt; /// Walmart serialize Date to a milliseconds since January 1, 1970 0:00:00 UTC, /// It's not a standard unix timestamp, so we need to impl custom unserialize -struct TimestampVistor; -impl<'de> de::Visitor<'de> for TimestampVistor { +struct TimestampVisitor; +impl<'de> de::Visitor<'de> for TimestampVisitor { type Value = DateTime; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { @@ -23,32 +23,3 @@ impl<'de> de::Visitor<'de> for TimestampVistor { .ok_or_else(|| E::custom(format!("invalid timestamp value `{}`", v))) } } - -pub fn deserialize_timestamp<'de, D>(d: D) -> Result, D::Error> -where - D: Deserializer<'de>, -{ - d.deserialize_any(TimestampVistor) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_deserialize_timestamp() { - use serde_json; - - #[derive(Deserialize)] - struct T { - #[serde(deserialize_with = "deserialize_timestamp")] - date: DateTime, - } - - let t: T = serde_json::from_str(r##"{"date":1502506180690}"##).unwrap(); - assert_eq!( - t.date.format("%Y-%m-%d").to_string(), - "2017-08-12".to_string() - ); - } -} diff --git a/walmart-partner-api/src/xml.rs b/walmart-partner-api/src/xml.rs index 5ffa151..49a6308 100644 --- a/walmart-partner-api/src/xml.rs +++ b/walmart-partner-api/src/xml.rs @@ -1,67 +1,36 @@ -use crate::result::*; -use reqwest::Response; -pub use xmltree::Element; - -pub trait FromXmlElement: Sized { - fn from_xml_element(elem: Element) -> WalmartResult; -} - -pub struct Xml { - inner: T, - text: String, -} - -impl ::std::ops::Deref for Xml { - type Target = T; - - fn deref(&self) -> &Self::Target { - &self.inner +use crate::WalmartResult; + +pub trait XmlSer { + fn to_xml(&self) -> WalmartResult; + + fn get_element_with_text( + &self, + name: &str, + text: T, + ) -> WalmartResult { + let mut elem = xml_builder::XMLElement::new(name); + elem.add_text(text.to_string())?; + Ok(elem) } -} - -impl Xml -where - T: FromXmlElement, -{ - pub fn from_res(res: &mut Response) -> WalmartResult { - use std::io::Cursor; - - let status = res.status(); - let text = res.text().map_err(|err| ApiResponseError { - message: format!("get response text: {}", err.to_string()), - status: status.clone(), - body: "".to_string(), - })?; - - let elem = Element::parse(Cursor::new(text.as_bytes())).map_err(|err| ApiResponseError { - message: format!("parse response xml: {}", err.to_string()), - status: status.clone(), - body: text.clone(), - })?; - let inner = T::from_xml_element(elem)?; - - Ok(Xml { inner, text }) - } - - pub fn text(&self) -> &str { - self.text.as_ref() - } - - pub fn into_inner(self) -> T { - self.inner + fn to_string(&self) -> WalmartResult { + use xml_builder::{XMLBuilder, XMLVersion}; + let mut xml = XMLBuilder::new() + .version(XMLVersion::XML1_0) + .encoding("UTF-8".into()) + .build(); + xml.set_root_element(self.to_xml()?); + let mut writer = Vec::::new(); + xml.generate(&mut writer)?; + Ok(String::from_utf8_lossy(&writer).to_string()) } } -pub trait GetChildText { - fn get_child_text(&self, name: &str) -> Option; - fn get_child_text_or_default(&self, name: &str) -> String { - self.get_child_text(name).unwrap_or_default() - } -} - -impl GetChildText for Element { - fn get_child_text(&self, name: &str) -> Option { - self.get_child(name).and_then(|c| c.text.clone()) - } +pub fn get_element_with_text( + name: &str, + text: T, +) -> WalmartResult { + let mut elem = xml_builder::XMLElement::new(name); + elem.add_text(text.to_string())?; + Ok(elem) }