Add UPS support
This commit is contained in:
parent
f005a9d106
commit
fc6e940d39
19 changed files with 642 additions and 1 deletions
143
Cargo.lock
generated
143
Cargo.lock
generated
|
@ -110,6 +110,34 @@ dependencies = [
|
||||||
"windows-targets 0.52.4",
|
"windows-targets 0.52.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cookie"
|
||||||
|
version = "0.17.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7efb37c3e1ccb1ff97164ad95ac1606e8ccd35b3fa0a7d99a304c7f4a428cc24"
|
||||||
|
dependencies = [
|
||||||
|
"percent-encoding",
|
||||||
|
"time",
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cookie_store"
|
||||||
|
version = "0.20.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "387461abbc748185c3a6e1673d826918b450b87ff22639429c694619a83b6cf6"
|
||||||
|
dependencies = [
|
||||||
|
"cookie",
|
||||||
|
"idna 0.3.0",
|
||||||
|
"log",
|
||||||
|
"publicsuffix",
|
||||||
|
"serde",
|
||||||
|
"serde_derive",
|
||||||
|
"serde_json",
|
||||||
|
"time",
|
||||||
|
"url",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "core-foundation"
|
name = "core-foundation"
|
||||||
version = "0.9.4"
|
version = "0.9.4"
|
||||||
|
@ -126,6 +154,15 @@ version = "0.8.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
|
checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "deranged"
|
||||||
|
version = "0.3.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
|
||||||
|
dependencies = [
|
||||||
|
"powerfmt",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "encoding_rs"
|
name = "encoding_rs"
|
||||||
version = "0.8.33"
|
version = "0.8.33"
|
||||||
|
@ -351,6 +388,16 @@ dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "idna"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-bidi",
|
||||||
|
"unicode-normalization",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "idna"
|
name = "idna"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
|
@ -476,6 +523,12 @@ dependencies = [
|
||||||
"tempfile",
|
"tempfile",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-conv"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-traits"
|
name = "num-traits"
|
||||||
version = "0.2.18"
|
version = "0.2.18"
|
||||||
|
@ -500,6 +553,7 @@ version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"odeli-canadapost",
|
"odeli-canadapost",
|
||||||
"odeli-core",
|
"odeli-core",
|
||||||
|
"odeli-ups",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -525,6 +579,20 @@ dependencies = [
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "odeli-ups"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
|
"isocountry",
|
||||||
|
"odeli-core",
|
||||||
|
"reqwest",
|
||||||
|
"reqwest_cookie_store",
|
||||||
|
"serde",
|
||||||
|
"serde_derive",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.19.0"
|
version = "1.19.0"
|
||||||
|
@ -599,6 +667,12 @@ version = "0.3.30"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
|
checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "powerfmt"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.78"
|
version = "1.0.78"
|
||||||
|
@ -608,6 +682,22 @@ dependencies = [
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "psl-types"
|
||||||
|
version = "2.0.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "publicsuffix"
|
||||||
|
version = "2.2.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "96a8c1bda5ae1af7f99a2962e49df150414a43d62404644d98dd5c3a93d07457"
|
||||||
|
dependencies = [
|
||||||
|
"idna 0.3.0",
|
||||||
|
"psl-types",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.35"
|
version = "1.0.35"
|
||||||
|
@ -625,6 +715,8 @@ checksum = "c6920094eb85afde5e4a138be3f2de8bbdf28000f0029e72c45025a56b042251"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
"cookie",
|
||||||
|
"cookie_store",
|
||||||
"encoding_rs",
|
"encoding_rs",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
|
@ -657,6 +749,18 @@ dependencies = [
|
||||||
"winreg",
|
"winreg",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "reqwest_cookie_store"
|
||||||
|
version = "0.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ba529055ea150e42e4eb9c11dcd380a41025ad4d594b0cb4904ef28b037e1061"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"cookie_store",
|
||||||
|
"reqwest",
|
||||||
|
"url",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc-demangle"
|
name = "rustc-demangle"
|
||||||
version = "0.1.23"
|
version = "0.1.23"
|
||||||
|
@ -855,6 +959,37 @@ dependencies = [
|
||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "time"
|
||||||
|
version = "0.3.34"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749"
|
||||||
|
dependencies = [
|
||||||
|
"deranged",
|
||||||
|
"itoa",
|
||||||
|
"num-conv",
|
||||||
|
"powerfmt",
|
||||||
|
"serde",
|
||||||
|
"time-core",
|
||||||
|
"time-macros",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "time-core"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "time-macros"
|
||||||
|
version = "0.2.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774"
|
||||||
|
dependencies = [
|
||||||
|
"num-conv",
|
||||||
|
"time-core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tinyvec"
|
name = "tinyvec"
|
||||||
version = "1.6.0"
|
version = "1.6.0"
|
||||||
|
@ -980,7 +1115,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633"
|
checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"form_urlencoded",
|
"form_urlencoded",
|
||||||
"idna",
|
"idna 0.5.0",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -990,6 +1125,12 @@ version = "0.2.15"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "version_check"
|
||||||
|
version = "0.9.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "want"
|
name = "want"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
|
|
|
@ -12,9 +12,11 @@ readme = "README.md"
|
||||||
members = [
|
members = [
|
||||||
"odeli-core",
|
"odeli-core",
|
||||||
"shipper/odeli-canadapost",
|
"shipper/odeli-canadapost",
|
||||||
|
"shipper/odeli-ups",
|
||||||
]
|
]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
odeli-core = { version = "0.1", path = "./odeli-core" }
|
odeli-core = { version = "0.1", path = "./odeli-core" }
|
||||||
|
|
||||||
odeli-canadapost = { version = "0.1", path = "./shipper/odeli-canadapost" }
|
odeli-canadapost = { version = "0.1", path = "./shipper/odeli-canadapost" }
|
||||||
|
odeli-ups = { version = "0.1", path = "./shipper/odeli-ups" }
|
||||||
|
|
16
odeli-core/src/debug/methods.rs
Normal file
16
odeli-core/src/debug/methods.rs
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub enum Method {
|
||||||
|
WebApi,
|
||||||
|
OfficialApi,
|
||||||
|
Custom(&'static str),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl core::fmt::Display for Method {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::WebApi => write!(f, "Web API"),
|
||||||
|
Self::OfficialApi => write!(f, "Official API"),
|
||||||
|
Self::Custom(s) => write!(f, "{}", s),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
4
odeli-core/src/debug/mod.rs
Normal file
4
odeli-core/src/debug/mod.rs
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
mod methods;
|
||||||
|
pub use methods::Method;
|
||||||
|
mod shippers;
|
||||||
|
pub use shippers::Shipper;
|
16
odeli-core/src/debug/shippers.rs
Normal file
16
odeli-core/src/debug/shippers.rs
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub enum Shipper {
|
||||||
|
CanadaPost,
|
||||||
|
Ups,
|
||||||
|
Custom(&'static str),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl core::fmt::Display for Shipper {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::CanadaPost => write!(f, "Canada Post"),
|
||||||
|
Self::Ups => write!(f, "UPS"),
|
||||||
|
Self::Custom(s) => write!(f, "{}", s),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,3 +1,7 @@
|
||||||
pub trait DebugInfo: super::Adapter + core::fmt::Debug {
|
pub trait DebugInfo: super::Adapter + core::fmt::Debug {
|
||||||
fn shipper_name(&self) -> String;
|
fn shipper_name(&self) -> String;
|
||||||
|
|
||||||
|
fn shipper(&self) -> super::debug::Shipper;
|
||||||
|
|
||||||
|
fn api(&self) -> super::debug::Method;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
mod adapter;
|
mod adapter;
|
||||||
pub mod data;
|
pub mod data;
|
||||||
pub use adapter::{Adapter, Confidence};
|
pub use adapter::{Adapter, Confidence};
|
||||||
|
pub mod debug;
|
||||||
mod debug_info;
|
mod debug_info;
|
||||||
pub use debug_info::DebugInfo;
|
pub use debug_info::DebugInfo;
|
||||||
mod errors;
|
mod errors;
|
||||||
|
|
|
@ -128,4 +128,12 @@ impl DebugInfo for CanadaPost {
|
||||||
fn shipper_name(&self) -> String {
|
fn shipper_name(&self) -> String {
|
||||||
crate::consts::SHIPPER_NAME.to_owned()
|
crate::consts::SHIPPER_NAME.to_owned()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn shipper(&self) -> odeli_core::debug::Shipper {
|
||||||
|
odeli_core::debug::Shipper::CanadaPost
|
||||||
|
}
|
||||||
|
|
||||||
|
fn api(&self) -> odeli_core::debug::Method {
|
||||||
|
odeli_core::debug::Method::WebApi
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
21
shipper/odeli-ups/Cargo.toml
Normal file
21
shipper/odeli-ups/Cargo.toml
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
[package]
|
||||||
|
name = "odeli-ups"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
odeli-core = { version = "0.1", path = "../../odeli-core" }
|
||||||
|
|
||||||
|
chrono = { version = "0.4" }
|
||||||
|
isocountry = { version = "0.3" }
|
||||||
|
|
||||||
|
reqwest = { version = "0.11", features = [ "json", "cookies" ] }
|
||||||
|
reqwest_cookie_store = "0.6"
|
||||||
|
|
||||||
|
serde = { version = "1.0" }
|
||||||
|
serde_derive = { version = "1.0" }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tokio = { version = "1", features = [ "rt", "macros" ] }
|
141
shipper/odeli-ups/src/adapter.rs
Normal file
141
shipper/odeli-ups/src/adapter.rs
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
use odeli_core::{Adapter, Confidence, DebugInfo};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Ups {
|
||||||
|
client: Arc<reqwest::Client>,
|
||||||
|
//cookies: Arc<reqwest_cookie_store::CookieStoreMutex>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ups {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
//let cookie_store = Arc::new(reqwest_cookie_store::CookieStoreMutex::new(reqwest_cookie_store::CookieStore::new(None)));
|
||||||
|
Self {
|
||||||
|
client: Arc::new(
|
||||||
|
reqwest::Client::builder()
|
||||||
|
.timeout(std::time::Duration::from_secs(10))
|
||||||
|
.cookie_store(true)
|
||||||
|
//.cookie_provider(cookie_store.clone())
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
//cookies: cookie_store,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_client(client: Arc<reqwest::Client>) -> Self {
|
||||||
|
Self { client }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn progress_activity_to_core_event(
|
||||||
|
activity: crate::data::ProgressActivity,
|
||||||
|
) -> Result<odeli_core::data::TrackingEvent, odeli_core::AdapterError> {
|
||||||
|
let type_ = match &activity.act_code as &str {
|
||||||
|
"RQ" => odeli_core::data::EventType::Delivered,
|
||||||
|
"RB" => odeli_core::data::EventType::OutForDelivery,
|
||||||
|
"Q2" => odeli_core::data::EventType::Info,
|
||||||
|
"Q0" => odeli_core::data::EventType::Creation,
|
||||||
|
_ => odeli_core::data::EventType::Unknown,
|
||||||
|
};
|
||||||
|
Ok(odeli_core::data::TrackingEvent {
|
||||||
|
time: activity
|
||||||
|
.datetime()
|
||||||
|
.map_err(|e| odeli_core::AdapterError::Custom(Box::new(e)))?,
|
||||||
|
location: activity
|
||||||
|
.core_location()
|
||||||
|
.map_err(|e| odeli_core::AdapterError::Custom(e.to_owned().into()))?,
|
||||||
|
type_,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Adapter for Ups {
|
||||||
|
async fn track(
|
||||||
|
&self,
|
||||||
|
id: odeli_core::data::TrackingNumber,
|
||||||
|
) -> Result<odeli_core::data::TrackingInfo, odeli_core::AdapterError> {
|
||||||
|
let payload = crate::data::StatusRequest {
|
||||||
|
tracking_numbers: vec![id.as_str().to_owned()],
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let main_page = self
|
||||||
|
.client
|
||||||
|
.get(crate::urls::html_page_url(&id))
|
||||||
|
.header("Accept-Encoding", "")
|
||||||
|
.header("Accept", "text/html")
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|e| odeli_core::AdapterError::Network(Box::new(e)))?;
|
||||||
|
let mut status_req = self
|
||||||
|
.client
|
||||||
|
.post(crate::urls::SHIPMENT_STATUS_URL)
|
||||||
|
.header("Accept-Encoding", "")
|
||||||
|
.header("Origin", "https://www.ups.com")
|
||||||
|
//.version(reqwest::Version::HTTP_11)
|
||||||
|
.json(&payload);
|
||||||
|
for cookie in main_page.cookies() {
|
||||||
|
println!("Cookie {}={}", cookie.name(), cookie.value());
|
||||||
|
if cookie.name().to_uppercase() == "X-XSRF-TOKEN-ST" {
|
||||||
|
println!("setting xsrf token to {}", cookie.value());
|
||||||
|
status_req = status_req.header("X-XSRF-TOKEN", cookie.value());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut ship_status: crate::data::StatusResponse = status_req
|
||||||
|
//let text = status_req
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|e| odeli_core::AdapterError::Network(Box::new(e)))?
|
||||||
|
.json()
|
||||||
|
.await
|
||||||
|
.map_err(|e| odeli_core::AdapterError::Network(Box::new(e)))?;
|
||||||
|
|
||||||
|
let mut ship_details: Option<crate::data::Details> = None;
|
||||||
|
while let Some(deets) = ship_status.track_details.pop() {
|
||||||
|
if deets.requested_tracking_number == id.as_str()
|
||||||
|
&& deets.tracking_number == id.as_str()
|
||||||
|
{
|
||||||
|
ship_details = Some(deets);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ship_details.is_none() {
|
||||||
|
return Err(odeli_core::AdapterError::UnsupportedTrackingNumber(id));
|
||||||
|
}
|
||||||
|
let ship_details = ship_details.unwrap();
|
||||||
|
|
||||||
|
let mut events = Vec::with_capacity(ship_details.shipment_progress_activities.len());
|
||||||
|
for activity in ship_details.shipment_progress_activities.into_iter() {
|
||||||
|
events.push(Self::progress_activity_to_core_event(activity)?);
|
||||||
|
}
|
||||||
|
let is_delivered = ship_details.package_status_type == "Delivered";
|
||||||
|
Ok(odeli_core::data::TrackingInfo {
|
||||||
|
id,
|
||||||
|
is_delivered,
|
||||||
|
is_finished: is_delivered,
|
||||||
|
events,
|
||||||
|
destination: ship_details.ship_to_address.to_core_location_opt().ok(),
|
||||||
|
origin: ship_details
|
||||||
|
.ship_from_address
|
||||||
|
.and_then(|addr| addr.to_core_location_opt().ok()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_supported_id(&self, _id: odeli_core::data::TrackingNumber) -> Confidence {
|
||||||
|
Confidence::new(0.5)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DebugInfo for Ups {
|
||||||
|
fn shipper_name(&self) -> String {
|
||||||
|
crate::consts::SHIPPER_NAME.to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn shipper(&self) -> odeli_core::debug::Shipper {
|
||||||
|
odeli_core::debug::Shipper::Ups
|
||||||
|
}
|
||||||
|
|
||||||
|
fn api(&self) -> odeli_core::debug::Method {
|
||||||
|
odeli_core::debug::Method::WebApi
|
||||||
|
}
|
||||||
|
}
|
1
shipper/odeli-ups/src/consts.rs
Normal file
1
shipper/odeli-ups/src/consts.rs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
pub const SHIPPER_NAME: &'static str = "UPS";
|
66
shipper/odeli-ups/src/data/address.rs
Normal file
66
shipper/odeli-ups/src/data/address.rs
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct Address {
|
||||||
|
#[serde(rename = "streetAddress1")]
|
||||||
|
pub address_1: String,
|
||||||
|
#[serde(rename = "streetAddress2")]
|
||||||
|
pub address_2: String,
|
||||||
|
#[serde(rename = "streetAddress3")]
|
||||||
|
pub address_3: String,
|
||||||
|
pub city: String,
|
||||||
|
pub state: String,
|
||||||
|
pub country: String,
|
||||||
|
/// Zip or postal code
|
||||||
|
#[serde(rename = "zipCode")]
|
||||||
|
pub zip_code: String,
|
||||||
|
#[serde(rename = "companyName")]
|
||||||
|
pub company_name: String,
|
||||||
|
#[serde(rename = "attentionName")]
|
||||||
|
pub attention_name: String,
|
||||||
|
#[serde(rename = "isAddressCorrected")]
|
||||||
|
pub is_corrected: bool,
|
||||||
|
#[serde(rename = "isReturnAddress")]
|
||||||
|
pub is_return: bool,
|
||||||
|
#[serde(rename = "isHoldAddress")]
|
||||||
|
pub is_hold: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Address {
|
||||||
|
pub fn to_core_location_opt(
|
||||||
|
self,
|
||||||
|
) -> Result<odeli_core::data::Location, isocountry::CountryCodeParseErr> {
|
||||||
|
self.parse_country_code().map(|cc| {
|
||||||
|
let region = odeli_core::data::Region {
|
||||||
|
municipality: empty_to_none(self.city),
|
||||||
|
administrative_division: empty_to_none(self.state),
|
||||||
|
country: cc.name().to_owned(),
|
||||||
|
country_code: cc,
|
||||||
|
};
|
||||||
|
if self.address_1.is_empty() {
|
||||||
|
region.into()
|
||||||
|
} else {
|
||||||
|
odeli_core::data::Address {
|
||||||
|
address_1: self.address_1,
|
||||||
|
address_2: empty_to_none(self.address_2),
|
||||||
|
region,
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_country_code(
|
||||||
|
&self,
|
||||||
|
) -> Result<isocountry::CountryCode, isocountry::CountryCodeParseErr> {
|
||||||
|
isocountry::CountryCode::for_alpha2_caseless(&self.country)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn empty_to_none(s: String) -> Option<String> {
|
||||||
|
if s.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(s)
|
||||||
|
}
|
||||||
|
}
|
23
shipper/odeli-ups/src/data/details.rs
Normal file
23
shipper/odeli-ups/src/data/details.rs
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct Details {
|
||||||
|
#[serde(rename = "requestedTrackingNumber")]
|
||||||
|
pub requested_tracking_number: String,
|
||||||
|
#[serde(rename = "trackingNumber")]
|
||||||
|
pub tracking_number: String,
|
||||||
|
#[serde(rename = "packageStatusType")]
|
||||||
|
pub package_status_type: String,
|
||||||
|
#[serde(rename = "packageStatusCode")]
|
||||||
|
pub package_status_code: String,
|
||||||
|
#[serde(rename = "progressBarType")]
|
||||||
|
pub progress_bar_type: String,
|
||||||
|
#[serde(rename = "shipToAddress")]
|
||||||
|
pub ship_to_address: super::Address,
|
||||||
|
#[serde(rename = "shipFromAddress")]
|
||||||
|
pub ship_from_address: Option<super::Address>,
|
||||||
|
#[serde(rename = "proofOfDeliveryUrl")]
|
||||||
|
pub proof_of_delivery_url: Option<String>,
|
||||||
|
#[serde(rename = "shipmentProgressActivities")]
|
||||||
|
pub shipment_progress_activities: Vec<super::ProgressActivity>,
|
||||||
|
}
|
10
shipper/odeli-ups/src/data/mod.rs
Normal file
10
shipper/odeli-ups/src/data/mod.rs
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
mod address;
|
||||||
|
pub use address::Address;
|
||||||
|
mod details;
|
||||||
|
pub use details::Details;
|
||||||
|
mod progress_activity;
|
||||||
|
pub use progress_activity::ProgressActivity;
|
||||||
|
mod status_request;
|
||||||
|
pub use status_request::StatusRequest;
|
||||||
|
mod status_response;
|
||||||
|
pub use status_response::StatusResponse;
|
72
shipper/odeli-ups/src/data/progress_activity.rs
Normal file
72
shipper/odeli-ups/src/data/progress_activity.rs
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct ProgressActivity {
|
||||||
|
pub date: String,
|
||||||
|
/// Local time?
|
||||||
|
pub time: String,
|
||||||
|
/// Country?
|
||||||
|
pub location: String,
|
||||||
|
/// Event type
|
||||||
|
#[serde(rename = "activityScan")]
|
||||||
|
pub activity_scan: String,
|
||||||
|
/// Event code
|
||||||
|
#[serde(rename = "actCode")]
|
||||||
|
pub act_code: String,
|
||||||
|
#[serde(rename = "gmtDate")]
|
||||||
|
pub gmt_date: String,
|
||||||
|
#[serde(rename = "gmtOffset")]
|
||||||
|
pub gmt_offset: String,
|
||||||
|
#[serde(rename = "gmtTime")]
|
||||||
|
pub gmt_time: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProgressActivity {
|
||||||
|
pub fn datetime(&self) -> chrono::format::ParseResult<chrono::DateTime<chrono::offset::Utc>> {
|
||||||
|
let date = chrono::naive::NaiveDate::parse_from_str(&self.date, "%m/%d/%Y")?;
|
||||||
|
let time =
|
||||||
|
chrono::naive::NaiveTime::parse_from_str(&self.time.replace('.', ""), "%I:%M %p")?;
|
||||||
|
let datetime = date.and_time(time);
|
||||||
|
// FIXME
|
||||||
|
// The timezone cannot be guessed, so everything is assumed to be the UTC timezone.
|
||||||
|
// UPS seems to use local timezone for this but doesn't provide enough information
|
||||||
|
// to figure out what the local timezone actually is...
|
||||||
|
Ok(datetime.and_utc())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn core_location(&self) -> Result<Option<odeli_core::data::Location>, &'static str> {
|
||||||
|
if self.location.is_empty() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
let mut split_location: std::collections::VecDeque<_> = self.location.split(", ").collect();
|
||||||
|
if let Some(country_name) = split_location.pop_back() {
|
||||||
|
let cc = Self::find_country_code(country_name)?;
|
||||||
|
let administrative_division = split_location.pop_back().map(|s| s.to_owned());
|
||||||
|
let municipality = split_location.pop_back().map(|s| s.to_owned());
|
||||||
|
Ok(Some(
|
||||||
|
odeli_core::data::Region {
|
||||||
|
municipality,
|
||||||
|
administrative_division,
|
||||||
|
country: country_name.to_owned(),
|
||||||
|
country_code: cc,
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
Err("Unrecognized location string")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_country_code(country: &str) -> Result<isocountry::CountryCode, &'static str> {
|
||||||
|
let country_lower = country.to_lowercase();
|
||||||
|
for cc in isocountry::CountryCode::iter() {
|
||||||
|
let cc_lower = cc.name().to_lowercase();
|
||||||
|
if cc_lower == country_lower
|
||||||
|
|| (cc_lower.contains(' ') && cc_lower.contains(&country_lower))
|
||||||
|
{
|
||||||
|
return Ok(*cc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err("Unrecognized country name")
|
||||||
|
}
|
||||||
|
}
|
28
shipper/odeli-ups/src/data/status_request.rs
Normal file
28
shipper/odeli-ups/src/data/status_request.rs
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct StatusRequest {
|
||||||
|
/// Browser language
|
||||||
|
#[serde(rename = "Locale")]
|
||||||
|
pub locale: String,
|
||||||
|
/// Tracking numbers for which to retrieve status info
|
||||||
|
#[serde(rename = "TrackingNumber")]
|
||||||
|
pub tracking_numbers: Vec<String>,
|
||||||
|
/// ???
|
||||||
|
#[serde(rename = "Requester")]
|
||||||
|
pub requester: String,
|
||||||
|
/// ???
|
||||||
|
#[serde(rename = "returnToValue")]
|
||||||
|
pub return_to_value: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for StatusRequest {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
locale: "en_US".to_owned(),
|
||||||
|
requester: "st/trackdetails".to_owned(),
|
||||||
|
return_to_value: "".to_owned(),
|
||||||
|
tracking_numbers: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
15
shipper/odeli-ups/src/data/status_response.rs
Normal file
15
shipper/odeli-ups/src/data/status_response.rs
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct StatusResponse {
|
||||||
|
#[serde(rename = "statusCode")]
|
||||||
|
pub status_code: String,
|
||||||
|
#[serde(rename = "statusText")]
|
||||||
|
pub status_text: String,
|
||||||
|
#[serde(rename = "isLoggedInUser")]
|
||||||
|
pub is_logged_in: bool,
|
||||||
|
#[serde(rename = "trackedDateTime")]
|
||||||
|
pub tracked_datetime: String,
|
||||||
|
#[serde(rename = "trackDetails")]
|
||||||
|
pub track_details: Vec<super::Details>,
|
||||||
|
}
|
58
shipper/odeli-ups/src/lib.rs
Normal file
58
shipper/odeli-ups/src/lib.rs
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
mod adapter;
|
||||||
|
pub use adapter::Ups;
|
||||||
|
pub(crate) mod consts;
|
||||||
|
pub(crate) mod data;
|
||||||
|
pub(crate) mod urls;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use odeli_core::Adapter;
|
||||||
|
|
||||||
|
const TEST_VALID_TRACKING_NUMBER: &str = "801189003898800967";
|
||||||
|
const TEST_INVALID_TRACKING_NUMBER: &str = "NOTATRACKINGNUMBER";
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_works() {
|
||||||
|
println!("Ups::new() -> {:?}", Ups::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn track_valid() {
|
||||||
|
let adapter = Ups::new();
|
||||||
|
println!(
|
||||||
|
"Trying to get tracking info for {}",
|
||||||
|
TEST_VALID_TRACKING_NUMBER
|
||||||
|
);
|
||||||
|
let info_result = adapter
|
||||||
|
.track(odeli_core::data::TrackingNumber::new(
|
||||||
|
TEST_VALID_TRACKING_NUMBER,
|
||||||
|
))
|
||||||
|
.await;
|
||||||
|
dbg!(&info_result);
|
||||||
|
assert!(info_result.is_ok());
|
||||||
|
let info = info_result.unwrap();
|
||||||
|
println!(
|
||||||
|
"Got tracking info for {}: {:?}",
|
||||||
|
TEST_VALID_TRACKING_NUMBER, info
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn track_invalid() {
|
||||||
|
let adapter = Ups::new();
|
||||||
|
println!(
|
||||||
|
"Trying to get tracking info for (bad) {}",
|
||||||
|
TEST_INVALID_TRACKING_NUMBER
|
||||||
|
);
|
||||||
|
let info_result = adapter
|
||||||
|
.track(odeli_core::data::TrackingNumber::new(
|
||||||
|
TEST_INVALID_TRACKING_NUMBER,
|
||||||
|
))
|
||||||
|
.await;
|
||||||
|
assert!(
|
||||||
|
info_result.is_err(),
|
||||||
|
"Tracking info retrieval should return Result::Err(...) for invalid tracking number"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
14
shipper/odeli-ups/src/urls.rs
Normal file
14
shipper/odeli-ups/src/urls.rs
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
pub const SHIPMENT_STATUS_URL: &'static str =
|
||||||
|
"https://webapis.ups.com/track/api/Track/GetStatus?loc=en_US";
|
||||||
|
|
||||||
|
const HTML_PAGE_URL_START: &'static str = "https://www.ups.com/track?track=yes&trackNums=";
|
||||||
|
const HTML_PAGE_URL_END: &'static str = "&loc=en_US&requester=ST/trackdetails";
|
||||||
|
|
||||||
|
pub fn html_page_url(id: &odeli_core::data::TrackingNumber) -> String {
|
||||||
|
format!(
|
||||||
|
"{}{}{}",
|
||||||
|
HTML_PAGE_URL_START,
|
||||||
|
id.as_str(),
|
||||||
|
HTML_PAGE_URL_END
|
||||||
|
)
|
||||||
|
}
|
Loading…
Reference in a new issue