1use alloc::collections::BTreeMap;
15use alloc::sync::Arc;
16use core::time::Duration;
17use std::sync::Mutex;
18
19use serde_json::{Value, json};
20use sha2::{Digest as _, Sha256};
21
22use super::api::{
23 Cart, CartOp, Fee, GrabBackend, GrabClient, GrabError, GrabResult, Health, Line, LoginDriver,
24 Menu, MenuItem, Merchant, Placed, Quote, SessionSlot, Step, Tracked,
25};
26use crate::Secret;
27
28pub const FAKE_CODE: &str = "123456";
30
31#[derive(Default)]
34pub struct Fake {
35 carts: Arc<Mutex<BTreeMap<String, Cart>>>,
36 pub orders: Arc<Mutex<Vec<String>>>,
38 pub next_quotes: Arc<Mutex<Vec<String>>>,
41}
42
43fn merchants() -> Vec<Merchant> {
44 vec![
45 Merchant {
46 id: "fake-chicken".into(),
47 name: "Chicken House (Fake)".into(),
48 cuisine: "Fast food".into(),
49 eta_min: Some(25),
50 distance_km: Some(1.2),
51 rating: Some(4.7),
52 promo: "₱50 off".into(),
53 open: true,
54 },
55 Merchant {
56 id: "fake-ramen".into(),
57 name: "Ramen Bar (Fake)".into(),
58 cuisine: "Japanese".into(),
59 eta_min: Some(35),
60 distance_km: Some(2.8),
61 rating: Some(4.8),
62 promo: String::new(),
63 open: true,
64 },
65 ]
66}
67
68fn items(merchant: &str) -> Vec<MenuItem> {
69 let it = |id: &str, name: &str, price: i64| MenuItem {
70 id: format!("{merchant}:{id}"),
71 name: name.into(),
72 category: "Mains".into(),
73 price,
74 description: String::new(),
75 available: true,
76 };
77 if merchant == "fake-ramen" {
78 vec![
79 it("og", "Tonkotsu Ramen", 46_000),
80 it("black", "Black Garlic Ramen", 48_000),
81 ]
82 } else {
83 vec![
84 it("c1", "Fried Chicken 1pc", 9_900),
85 it("spag", "Sweet Spaghetti", 7_500),
86 ]
87 }
88}
89
90struct FakeClient {
91 fake: Arc<FakeShared>,
92 slot: Arc<dyn SessionSlot>,
93}
94
95struct FakeShared {
96 carts: Arc<Mutex<BTreeMap<String, Cart>>>,
97 orders: Arc<Mutex<Vec<String>>>,
98 next_quotes: Arc<Mutex<Vec<String>>>,
99}
100
101impl FakeClient {
102 async fn account(&self) -> GrabResult<(String, Value)> {
104 let s = self
105 .slot
106 .load()
107 .await?
108 .ok_or_else(|| GrabError::NeedsRelogin("not connected".into()))?;
109 if s["dead"].as_bool() == Some(true) {
110 return Err(GrabError::NeedsRelogin("the SSO login was refused".into()));
111 }
112 Ok((s["account"].as_str().unwrap_or("fake").to_owned(), s))
113 }
114
115 fn cart_of(&self, account: &str) -> Cart {
116 crate::util::lock(&self.fake.carts)
117 .get(account)
118 .cloned()
119 .unwrap_or_else(|| Cart {
120 currency: "PHP".into(),
121 ..Cart::default()
122 })
123 }
124
125 fn quote_of(&self, cart: &Cart) -> Quote {
126 let mut h = Sha256::new();
127 h.update(serde_json::to_vec(cart).unwrap_or_default());
128 let mut id = format!("fq-{}", &crate::util::hex(&h.finalize())[..12]);
129 if let Some(next) = crate::util::lock(&self.fake.next_quotes).pop() {
130 id = next;
131 }
132 let fees = vec![
133 Fee {
134 label: "Delivery fee".into(),
135 amount: 4_900,
136 },
137 Fee {
138 label: "Platform fee".into(),
139 amount: 500,
140 },
141 ];
142 let discount = if cart.subtotal >= 20_000 { 5_000 } else { 0 };
143 let total = cart.subtotal + fees.iter().map(|f| f.amount).sum::<i64>() - discount;
144 Quote {
145 quote_id: id,
146 merchant_name: cart.merchant_name.clone(),
147 currency: "PHP".into(),
148 lines: cart.lines.clone(),
149 subtotal: cart.subtotal,
150 fees,
151 discount,
152 voucher: if discount > 0 {
153 "FAKE50".into()
154 } else {
155 String::new()
156 },
157 total,
158 eta_min: Some(30),
159 payment_method: "GrabPay (fake)".into(),
160 address_label: "Home".into(),
161 }
162 }
163}
164
165#[async_trait::async_trait]
166impl GrabClient for FakeClient {
167 async fn search(&self, query: &str, limit: usize) -> GrabResult<Vec<Merchant>> {
168 self.account().await?;
169 let q = query.to_lowercase();
170 Ok(merchants()
171 .into_iter()
172 .filter(|m| {
173 q.is_empty()
174 || m.name.to_lowercase().contains(&q)
175 || m.cuisine.to_lowercase().contains(&q)
176 })
177 .take(limit)
178 .collect())
179 }
180
181 async fn menu(&self, merchant_id: &str) -> GrabResult<Menu> {
182 self.account().await?;
183 let merchant = merchants()
184 .into_iter()
185 .find(|m| m.id == merchant_id)
186 .ok_or_else(|| GrabError::Failed(format!("no merchant {merchant_id}")))?;
187 Ok(Menu {
188 items: items(&merchant.id),
189 merchant,
190 currency: "PHP".into(),
191 })
192 }
193
194 async fn cart(&self, op: &CartOp) -> GrabResult<Cart> {
195 let (account, _) = self.account().await?;
196 let mut cart = self.cart_of(&account);
197 match op {
198 CartOp::Show => {}
199 CartOp::Clear => {
200 cart = Cart {
201 currency: "PHP".into(),
202 ..Cart::default()
203 };
204 }
205 CartOp::Add {
206 merchant_id,
207 item_id,
208 quantity,
209 note,
210 } => {
211 let m = merchants()
212 .into_iter()
213 .find(|m| m.id == *merchant_id)
214 .ok_or_else(|| GrabError::Failed(format!("no merchant {merchant_id}")))?;
215 if !cart.merchant_id.is_empty() && cart.merchant_id != m.id {
216 return Err(GrabError::Failed(
217 "the cart has another merchant's items: clear it first".into(),
218 ));
219 }
220 let it = items(&m.id)
221 .into_iter()
222 .find(|i| i.id == *item_id)
223 .ok_or_else(|| GrabError::Failed(format!("no item {item_id}")))?;
224 cart.merchant_id = m.id;
225 cart.merchant_name = m.name;
226 cart.lines.push(Line {
227 item_id: it.id,
228 name: it.name,
229 quantity: *quantity,
230 amount: it.price * i64::from(*quantity),
231 note: note.clone().unwrap_or_default(),
232 });
233 }
234 CartOp::Remove { item_id } => {
235 let before = cart.lines.len();
236 cart.lines.retain(|l| l.item_id != *item_id);
237 if cart.lines.len() == before {
238 return Err(GrabError::Failed(format!("{item_id} isn't in the cart")));
239 }
240 if cart.lines.is_empty() {
241 cart.merchant_id.clear();
242 cart.merchant_name.clear();
243 }
244 }
245 }
246 cart.subtotal = cart.lines.iter().map(|l| l.amount).sum();
247 crate::util::lock(&self.fake.carts).insert(account, cart.clone());
248 Ok(cart)
249 }
250
251 async fn quote(&self) -> GrabResult<Quote> {
252 let (account, _) = self.account().await?;
253 let cart = self.cart_of(&account);
254 if cart.lines.is_empty() {
255 return Err(GrabError::Failed("the cart is empty".into()));
256 }
257 Ok(self.quote_of(&cart))
258 }
259
260 async fn place_order(&self, quote_id: &str) -> GrabResult<Placed> {
261 let (account, _) = self.account().await?;
262 let mut orders = crate::util::lock(&self.fake.orders);
263 orders.push(quote_id.to_owned());
264 crate::util::lock(&self.fake.carts).remove(&account);
265 Ok(Placed {
266 order_id: format!("fake-order-{}", orders.len()),
267 eta_min: Some(30),
268 })
269 }
270
271 async fn track(&self, order_id: Option<&str>) -> GrabResult<Vec<Tracked>> {
272 self.account().await?;
273 let n = crate::util::lock(&self.fake.orders).len();
274 Ok((1..=n)
275 .map(|i| Tracked {
276 order_id: format!("fake-order-{i}"),
277 merchant_name: "Chicken House (Fake)".into(),
278 status: "Driver on the way".into(),
279 eta_min: Some(12),
280 driver: "Sample Driver (fake)".into(),
281 })
282 .filter(|t| order_id.is_none_or(|o| o == t.order_id))
283 .collect())
284 }
285
286 async fn refresh(&self, liveness: bool) -> GrabResult<Health> {
287 let (account, mut s) = self.account().await?;
288 s["n"] = json!(s["n"].as_i64().unwrap_or(0) + 1);
289 self.slot.save(&s).await?;
290 Ok(Health {
291 display_name: liveness.then_some(account),
292 sso_expires_at: s["sso_expires_at"].as_i64(),
293 })
294 }
295}
296
297impl GrabBackend for Fake {
298 fn client(&self, session: Arc<dyn SessionSlot>) -> Arc<dyn GrabClient> {
299 Arc::new(FakeClient {
300 fake: Arc::new(FakeShared {
301 carts: Arc::clone(&self.carts),
302 orders: Arc::clone(&self.orders),
303 next_quotes: Arc::clone(&self.next_quotes),
304 }),
305 slot: session,
306 })
307 }
308
309 fn login(
310 &self,
311 _cdp_url: &str,
312 _previous: Option<Value>,
313 ) -> Result<Box<dyn LoginDriver>, String> {
314 Ok(Box::new(FakeLogin::default()))
315 }
316}
317
318#[derive(Default)]
320pub struct FakeLogin {
321 waits: u32,
322}
323
324fn done() -> Step {
325 Step::Done {
326 session: Fake::session("Fake Grab"),
327 display_name: "Fake Grab".into(),
328 sso_expires_at: Some(crate::util::now_s() + 60 * 86400),
329 }
330}
331
332impl Fake {
333 pub fn session(account: &str) -> Value {
336 json!({
337 "fake": true,
338 "account": account,
339 "n": 0,
340 "sso_expires_at": crate::util::now_s() + 60 * 86400,
341 })
342 }
343
344 pub fn dead(mut session: Value) -> Value {
346 session["dead"] = json!(true);
347 session
348 }
349}
350
351impl LoginDriver for FakeLogin {
352 fn start(&mut self, phone: &Secret) -> Step {
353 let p = phone.expose();
354 if p.ends_with('9') {
355 return Step::Failed("Grab asked for a reCAPTCHA check (a step-up): try later".into());
356 }
357 if p.ends_with('1') {
358 return Step::AwaitingApproval;
359 }
360 Step::AwaitingCode {
361 channel: "sms".into(),
362 }
363 }
364
365 fn submit_code(&mut self, code: &Secret) -> Step {
366 if code.expose() == FAKE_CODE {
367 done()
368 } else {
369 Step::Failed("Grab refused the code".into())
370 }
371 }
372
373 fn wait(&mut self, timeout: Duration) -> Step {
374 self.waits += 1;
375 if self.waits >= 2 {
376 return done();
377 }
378 std::thread::sleep(timeout.min(Duration::from_millis(50)));
379 Step::AwaitingApproval
380 }
381
382 fn cancel(&mut self) {
383 self.waits = 0;
384 }
385}