use crate::app::*; use crate::stream::model::{ IntoStreamMessage, StreamDataChunk, StreamDataEnd, StreamDataExtra, StreamDataStart, }; use crate::websocket::{core::*, helper::*, model::*}; use log::{debug, error, info, warn}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::{fs::File, io::Read, sync::Arc}; use tokio::sync::{Mutex, mpsc::Sender}; use crate::websocket::core::WebsocketMessageResult; #[allow(non_snake_case)] #[derive(Debug, Serialize, Deserialize, Clone)] struct MenuPrice { ProductCode: String, NewPrice: serde_json::Value, #[serde(skip_serializing_if = "Option::is_none")] StringParam: Option, #[serde(skip_serializing_if = "Option::is_none")] Discount: Option, #[serde(skip_serializing_if = "Option::is_none")] Percent: Option, #[serde(skip_serializing_if = "Option::is_none")] roundup: Option, } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct RecipePrice { #[serde(flatten)] metadata: serde_json::Value, content: Vec, } impl RecipePrice { pub const HIDE_PARAM: &str = "hide=true"; pub fn import(path: String) -> RecipePrice { debug!("try import {path}"); let mut file = File::open(path).expect("file not found"); let mut data = String::new(); file.read_to_string(&mut data).unwrap(); let res: Result = serde_json::from_str(&data); match res { Ok(rp) => rp, Err(e) => { error!("error while deserialize price: {e}"); RecipePrice { content: Vec::new(), metadata: serde_json::Value::Null, } } } } pub fn import_from_raw_string(raw: String) -> RecipePrice { let res: Result = serde_json::from_str(&raw); match res { Ok(rp) => rp, Err(e) => { error!("error while deserialize price: {e}"); RecipePrice { content: Vec::new(), metadata: serde_json::Value::Null, } } } } // only for getting data without modify pub fn find_by_pd(&self, pd: &str) -> Option { self.content .iter() .find(|x| x.ProductCode.eq(pd)) .map(|x| x.clone()) } pub fn modify_price_by_pd(&mut self, pd: &str, price: serde_json::Value) { if let Some(mp) = self.content.iter_mut().find(|x| x.ProductCode.eq(pd)) { mp.NewPrice = price; } } fn modify_string_param_by_pd(&mut self, pd: &str, param: &str) { if let Some(mp) = self.content.iter_mut().find(|x| x.ProductCode.eq(pd)) { mp.StringParam = Some(param.to_string()); } } pub fn set_visibility_by_pd(&mut self, pd: &str, show: bool) { if !show { self.modify_string_param_by_pd(pd, RecipePrice::HIDE_PARAM); } else { self.modify_string_param_by_pd(pd, ""); } } // NOTE: disable write to file // pub fn export_to_json_file(self, outpath: Option) { // let json = serde_json::to_string(&self).unwrap(); // let json2: serde_json::Value = serde_json::from_str(&json).unwrap(); // if let Some(outpath) = outpath { // let writer = File::create(outpath).unwrap(); // let _ = serde_json::to_writer_pretty(writer, &json2); // } else { // println!("Default save to (execute)/price.json"); // let writer = File::create("price.json").unwrap(); // let _ = serde_json::to_writer_pretty(writer, &json2); // } // } } /// Get list of price // pub async fn handle_price_list_request( // config: DevConfig, // redis: redis::Client, // tx: Sender, // req: WebsocketMessageRequest, // uid_clone: Arc>, // ) -> WebsocketMessageResult { // } /// Get main price profile of country pub async fn handle_price_request( config: DevConfig, redis: redis::Client, tx: Sender, req: WebsocketMessageRequest, uid_clone: Arc>, ) -> WebsocketMessageResult { let p = req.payload.unwrap(); let price_param: PriceRequestPayload = serde_json::from_value(p)?; let mut price_file_format = format!( "{}/profile_{}_master.json", price_param.country, price_param.country.to_uppercase() ); if let Some(override_file) = price_param.override_file { price_file_format = override_file; } let price_action = price_param.action; let price_content = match invoke_checkout_request(config.clone(), price_file_format.clone()).await { Ok(pc) => pc, Err(e) => return Err(format!("Cannot find price of expected country: {e:?}").into()), }; info!("price content len: {}", price_content.len()); let mut rpp = RecipePrice::import_from_raw_string(price_content); let _uid = uid_clone.clone(); let uidd = _uid.try_lock().unwrap(); info!("price action: {price_action:?}"); let user_info = price_param.user_info.clone(); match price_action { PriceRequestAction::View(view_opt) => { let viewing_options: HashMap = get_extra_parameters(view_opt); // sa=all // sa=get,pd=... // sa=query,list=1|2|3 // sa=query,where=contain,kw=... let sub_action = viewing_options.get("sa"); let pd = viewing_options.get("pd"); if let Some(sa) = sub_action { let mut result = Vec::new(); let action_done = match sa.as_str() { "all" => { result = rpp.content; true } "get" if let Some(pd) = pd && let Some(mp) = rpp.find_by_pd(pd) => { result.push(mp); true } _ => false, }; if action_done { let _ = tx .send(TxControlMessage::Payload(serde_json::json!({ "type": "price", "payload": { "req_action": sa, "status": "ok", "content": result, "to": uidd.to_string() } }))) .await; } else { let _ = tx .send(TxControlMessage::Payload(serde_json::json!({ "type": "price", "payload": { "req_action": sa, "status": "fail", "to": uidd.to_string() } }))) .await; } } } PriceRequestAction::Edit(edit_opt) => { let editing_options: HashMap = get_extra_parameters(edit_opt); // sa=change,pd=...,to=... // sa=hide,pd=... // sa=disable,pd=... disable = hide // sa=show,pd=... // sa=toggle,pd=...,state=show|hide let sub_action = editing_options.get("sa"); let pd = editing_options.get("pd"); let to = editing_options.get("to"); let state = editing_options.get("state"); if let Some(sa) = sub_action { let mut action_message = String::new(); let action_done = match sa.as_str() { "change" if let Some(pd) = pd && let Some(to) = to && let Some(mp) = rpp.find_by_pd(pd.as_str()) => { info!( "[CHANGE] price of {pd} from {} to {to}", mp.NewPrice.as_i64().unwrap() ); action_message = format!( "[CHANGE] price of {pd} from {} to {to}", mp.NewPrice.as_i64().unwrap() ); let price_int = to.parse::()?; rpp.modify_price_by_pd(pd, serde_json::json!(price_int)); true } "toggle" if let Some(pd) = pd && let Some(state) = state => { info!("[TOGGLE] {pd} to {state}"); action_message = format!("[TOGGLE] {pd} to {state}"); match state.as_str() { "show" => rpp.set_visibility_by_pd(pd.as_str(), true), "hide" | "disable" => rpp.set_visibility_by_pd(pd.as_str(), false), _ => { warn!("unknown state toggle"); } } true } _ => false, }; if action_done { let _ = tx .send(TxControlMessage::Payload(serde_json::json!({ "type": "price", "payload": { "req_action": sa, "status": "ok", "to": uidd.to_string() } }))) .await; // send save // let all_prices_str = serde_json::to_string_pretty(&rpp)?; let commit_payload = CommitPayload { file_bytes: all_prices_str.as_bytes().to_vec(), path: price_file_format.clone(), signature_username: user_info .get("displayName") .unwrap_or_default() .as_str() .unwrap_or(&"unknown".to_string()) .to_string(), signature_email: user_info .get("email") .unwrap_or_default() .as_str() .unwrap_or(&"unknown".to_string()) .to_string(), message: action_message, }; if invoke_pull_sync_request(config.clone()).await.is_err() { // backup let _ = commit_payload.dump_backup(); return Err("Fail to sync repo, backing up ...".into()); } let _ = invoke_commit_request(config.clone(), commit_payload.clone()).await; if invoke_push_request(config.clone()).await.is_err() { let _ = commit_payload.dump_backup(); return Err("Fail to push repo, backing up ...".into()); } // push to git } else { let _ = tx .send(TxControlMessage::Payload(serde_json::json!({ "type": "price", "payload": { "req_action": sa, "status": "fail", "to": uidd.to_string() } }))) .await; } } } } Ok(()) }