use alloc::sync::Arc;
use core::time::Duration;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::Secret;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CartOp {
Show,
Add {
merchant_id: String,
item_id: String,
quantity: u32,
note: Option<String>,
},
Remove {
item_id: String,
},
Clear,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Op {
Search {
query: String,
limit: usize,
},
Menu {
merchant_id: String,
query: Option<String>,
},
Cart(CartOp),
Quote,
Track {
order_id: Option<String>,
},
PlaceOrder {
quote_id: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum GrabError {
#[error("Grab needs re-login: {0}")]
NeedsRelogin(String),
#[error("{0}")]
Failed(String),
}
pub type GrabResult<T> = Result<T, GrabError>;
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct Merchant {
pub id: String,
pub name: String,
#[serde(default)]
pub cuisine: String,
pub eta_min: Option<u32>,
pub distance_km: Option<f64>,
pub rating: Option<f64>,
#[serde(default)]
pub promo: String,
#[serde(default)]
pub open: bool,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct MenuItem {
pub id: String,
pub name: String,
#[serde(default)]
pub category: String,
pub price: i64,
#[serde(default)]
pub description: String,
#[serde(default)]
pub available: bool,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct Menu {
pub merchant: Merchant,
pub currency: String,
pub items: Vec<MenuItem>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Line {
pub item_id: String,
pub name: String,
pub quantity: u32,
pub amount: i64,
#[serde(default)]
pub note: String,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Cart {
pub merchant_id: String,
pub merchant_name: String,
pub currency: String,
pub lines: Vec<Line>,
pub subtotal: i64,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Fee {
pub label: String,
pub amount: i64,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Quote {
pub quote_id: String,
pub merchant_name: String,
pub currency: String,
pub lines: Vec<Line>,
pub subtotal: i64,
pub fees: Vec<Fee>,
pub discount: i64,
#[serde(default)]
pub voucher: String,
pub total: i64,
pub eta_min: Option<u32>,
pub payment_method: String,
pub address_label: String,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Placed {
pub order_id: String,
pub eta_min: Option<u32>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Tracked {
pub order_id: String,
pub merchant_name: String,
pub status: String,
pub eta_min: Option<u32>,
#[serde(default)]
pub driver: String,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Health {
pub display_name: Option<String>,
pub sso_expires_at: Option<i64>,
}
#[async_trait::async_trait]
pub trait SessionSlot: Send + Sync {
fn id(&self) -> &str;
async fn load(&self) -> GrabResult<Option<Value>>;
async fn save(&self, session: &Value) -> GrabResult<()>;
async fn load_cart(&self) -> GrabResult<Option<Value>>;
async fn save_cart(&self, cart: Option<&Value>) -> GrabResult<()>;
}
#[async_trait::async_trait]
pub trait GrabClient: Send + Sync {
async fn search(&self, query: &str, limit: usize) -> GrabResult<Vec<Merchant>>;
async fn menu(&self, merchant_id: &str) -> GrabResult<Menu>;
async fn cart(&self, op: &CartOp) -> GrabResult<Cart>;
async fn quote(&self) -> GrabResult<Quote>;
async fn place_order(&self, quote_id: &str) -> GrabResult<Placed>;
async fn track(&self, order_id: Option<&str>) -> GrabResult<Vec<Tracked>>;
async fn refresh(&self, liveness: bool) -> GrabResult<Health>;
}
#[derive(Clone, PartialEq, Eq)]
pub enum Step {
AwaitingCode {
channel: String,
},
AwaitingApproval,
Done {
session: Value,
display_name: String,
sso_expires_at: Option<i64>,
},
Failed(String),
}
impl core::fmt::Debug for Step {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::AwaitingCode { channel } => f
.debug_struct("AwaitingCode")
.field("channel", channel)
.finish(),
Self::AwaitingApproval => f.write_str("AwaitingApproval"),
Self::Done {
display_name,
sso_expires_at,
..
} => f
.debug_struct("Done")
.field("session", &crate::Redacted)
.field("display_name", display_name)
.field("sso_expires_at", sso_expires_at)
.finish(),
Self::Failed(m) => f.debug_tuple("Failed").field(m).finish(),
}
}
}
pub trait LoginDriver: Send {
fn start(&mut self, phone: &Secret) -> Step;
fn submit_code(&mut self, code: &Secret) -> Step;
fn wait(&mut self, timeout: Duration) -> Step;
fn cancel(&mut self);
}
pub trait GrabBackend: Send + Sync {
fn client(&self, session: Arc<dyn SessionSlot>) -> Arc<dyn GrabClient>;
fn login(&self, cdp_url: &str, previous: Option<Value>)
-> Result<Box<dyn LoginDriver>, String>;
}
pub fn money(minor: i64, currency: &str) -> String {
let sign = if minor < 0 { "-" } else { "" };
let m = minor.unsigned_abs();
let whole = (m / 100).to_string();
let mut grouped = String::new();
for (i, c) in whole.chars().enumerate() {
if i > 0 && (whole.len() - i).is_multiple_of(3) {
grouped.push(',');
}
grouped.push(c);
}
let cents = m % 100;
let unit = match currency {
"" | "PHP" => "₱".to_owned(),
c => format!("{c} "),
};
format!("{sign}{unit}{grouped}.{cents:02}")
}