pispas_modules/
mypos.rs

1#[cfg(target_os = "windows")]
2use std::process::{Child, Command};
3use async_trait::async_trait;
4use serde::{Deserialize, Serialize};
5use serde_json::{json, Value};
6use std::sync::{
7    atomic::{AtomicBool, Ordering},
8    Arc,
9};
10use tokio::sync::{Mutex, oneshot};
11use tokio::time::{Duration, timeout, sleep};
12use crate::service::{Service, WebSocketWrite};
13use easy_trace::prelude::{info, error, warn};
14use tokio_tungstenite::{connect_async, tungstenite::protocol::Message, MaybeTlsStream, WebSocketStream};
15use futures_util::{stream::SplitSink, SinkExt, StreamExt, stream::SplitStream};
16use tokio::net::TcpStream;
17
18// Imports específicos de Windows
19#[cfg(target_os = "windows")]
20use {sharing::MYPOS_SERVER_FILE, sharing::proc::kill_process_by_name};
21
22/// MyPOS service version
23pub const MYPOS_VERSION: &str = "1.1.0";
24
25/// Número máximo de reintentos para operaciones
26const MAX_RETRIES: u32 = 3;
27
28/// Tiempo de espera entre reintentos (en segundos)
29const RETRY_DELAY_SECS: u64 = 2;
30
31/// Intervalo de verificación del watchdog (en segundos)
32const WATCHDOG_CHECK_INTERVAL_SECS: u64 = 5;
33
34/// Tiempo de espera antes de reiniciar el servidor después de un fallo (en segundos)
35const RESTART_DELAY_SECS: u64 = 3;
36
37/// Tiempo máximo de espera para la conexión WebSocket (en segundos)
38const WEBSOCKET_CONNECT_TIMEOUT_SECS: u64 = 10;
39
40#[derive(Debug, Serialize, Deserialize)]
41struct ServerResponse {
42    status: String,
43    message: Option<String>,
44}
45
46type WsWriter = SplitSink<WebSocketStream<MaybeTlsStream<TcpStream>>, Message>;
47type WsReader = SplitStream<WebSocketStream<MaybeTlsStream<TcpStream>>>;
48
49/// MyPOS Service implementation with automatic retries and watchdog monitoring
50pub struct MyPOSService {
51    /// Handle for the `mypos_server.exe` process (solo Windows).
52    #[cfg(target_os = "windows")]
53    child_process: Arc<Mutex<Option<Child>>>,
54
55    /// State indicating if the service is running.
56    running: Arc<AtomicBool>,
57
58    /// State indicating if watchdog should be active
59    watchdog_enabled: Arc<AtomicBool>,
60
61    /// WebSocket writer for communicating with the Python server
62    ws_writer: Arc<Mutex<Option<WsWriter>>>,
63
64    /// Channel for sending responses (store the sender, not receiver)
65    response_sender: Arc<Mutex<Option<oneshot::Sender<ServerResponse>>>>,
66
67    /// Counter for restart attempts
68    restart_count: Arc<Mutex<u32>>,
69}
70
71impl MyPOSService {
72    /// Creates a new instance of the MyPOSService.
73    pub fn new() -> Self {
74        MyPOSService {
75            #[cfg(target_os = "windows")]
76            child_process: Arc::new(Mutex::new(None)),
77            running: Arc::new(AtomicBool::new(false)),
78            watchdog_enabled: Arc::new(AtomicBool::new(false)),
79            ws_writer: Arc::new(Mutex::new(None)),
80            response_sender: Arc::new(Mutex::new(None)),
81            restart_count: Arc::new(Mutex::new(0)),
82        }
83    }
84
85    // ========================================================================
86    // SECCIÓN: GESTIÓN DEL PROCESO Y CONEXIÓN
87    // ========================================================================
88
89    /// Launches the MyPOS server process (Windows only)
90    #[cfg(target_os = "windows")]
91    async fn launch_process(&self) -> Result<(), String> {
92        let exe_path = sharing::get_path_mypos_server();
93        info!("Launching MyPOS server at: {}", exe_path);
94
95        let mut guard = self.child_process.lock().await;
96
97        match Command::new(exe_path).spawn() {
98            Ok(child) => {
99                *guard = Some(child);
100                info!("MyPOS server process started successfully.");
101                Ok(())
102            }
103            Err(e) => {
104                error!("Failed to start MyPOS server: {:?}", e);
105                Err(format!("Failed to start process: {}", e))
106            }
107        }
108    }
109
110    /// Connects to the WebSocket server with timeout
111    async fn connect_websocket(&self) -> Result<(), String> {
112        let url = "ws://127.0.0.1:8765";
113        info!("Connecting to WebSocket at: {}", url);
114
115        let connect_future = connect_async(url);
116
117        let (ws_stream, _) = match timeout(
118            Duration::from_secs(WEBSOCKET_CONNECT_TIMEOUT_SECS),
119            connect_future
120        ).await {
121            Ok(Ok(result)) => result,
122            Ok(Err(e)) => return Err(format!("Failed to connect to WebSocket: {}", e)),
123            Err(_) => return Err("WebSocket connection timeout".to_string()),
124        };
125
126        let (write, read) = ws_stream.split();
127
128        // Store the writer
129        *self.ws_writer.lock().await = Some(write);
130
131        // Start reading responses
132        let response_sender = self.response_sender.clone();
133        tokio::spawn(async move {
134            Self::handle_websocket_responses(read, response_sender).await;
135        });
136
137        info!("Connected to WebSocket successfully");
138        Ok(())
139    }
140
141    /// Handles incoming WebSocket responses
142    async fn handle_websocket_responses(
143        mut read: WsReader,
144        response_sender: Arc<Mutex<Option<oneshot::Sender<ServerResponse>>>>
145    ) {
146        while let Some(msg_result) = read.next().await {
147            match msg_result {
148                Ok(Message::Text(response)) => {
149                    if let Ok(server_response) = serde_json::from_str::<ServerResponse>(&response) {
150                        info!("Received response: {:?}", server_response);
151
152                        // Send to the current waiting sender
153                        let mut sender_guard = response_sender.lock().await;
154                        if let Some(sender) = sender_guard.take() {
155                            if sender.send(server_response).is_err() {
156                                warn!("Failed to send response to waiting transaction");
157                            }
158                        }
159                    } else {
160                        warn!("Received unexpected response: {}", response);
161                    }
162                }
163                Ok(Message::Close(_)) => {
164                    warn!("WebSocket connection closed");
165                    break;
166                }
167                Err(e) => {
168                    error!("WebSocket error: {:?}", e);
169                    break;
170                }
171                _ => {}
172            }
173        }
174        info!("WebSocket response handler terminated");
175    }
176
177    // ========================================================================
178    // SECCIÓN: WATCHDOG Y MONITOREO
179    // ========================================================================
180
181    /// Launches and monitors the MyPOS server with watchdog (Windows)
182    #[cfg(target_os = "windows")]
183    async fn launch_and_monitor(&self) {
184        // Habilitar watchdog
185        self.watchdog_enabled.store(true, Ordering::Relaxed);
186
187        // Intentar lanzar el proceso
188        if let Err(e) = self.launch_process().await {
189            error!("Failed to launch process: {}", e);
190            return;
191        }
192
193        self.running.store(true, Ordering::Relaxed);
194
195        // Wait for the server to start
196        sleep(Duration::from_secs(RESTART_DELAY_SECS)).await;
197
198        // Connect to WebSocket
199        if let Err(e) = self.connect_websocket().await {
200            error!("Failed to connect to WebSocket: {:?}", e);
201            return;
202        }
203
204        // Iniciar watchdog
205        self.start_watchdog().await;
206    }
207
208    /// Launches connection monitoring for macOS (no process management)
209    #[cfg(target_os = "macos")]
210    async fn launch_and_monitor(&self) {
211        info!("macOS: Skipping process launch, connecting to existing WebSocket server...");
212
213        self.running.store(true, Ordering::Relaxed);
214        self.watchdog_enabled.store(true, Ordering::Relaxed);
215
216        // Intentar conectar al WebSocket
217        if let Err(e) = self.connect_websocket().await {
218            error!("Failed to connect to WebSocket: {:?}", e);
219            self.running.store(false, Ordering::Relaxed);
220            return;
221        }
222
223        info!("macOS: Connected to existing WebSocket server");
224
225        // Iniciar watchdog solo para monitorear la conexión
226        self.start_watchdog().await;
227    }
228
229    /// Starts the watchdog that monitors the process and connection health
230    async fn start_watchdog(&self) {
231        let running = self.running.clone();
232        let watchdog_enabled = self.watchdog_enabled.clone();
233        let ws_writer = self.ws_writer.clone();
234        let restart_count = self.restart_count.clone();
235        let response_sender = self.response_sender.clone();
236
237        #[cfg(target_os = "windows")]
238        let child_process = self.child_process.clone();
239
240        tokio::spawn(async move {
241            info!("Watchdog started");
242
243            while watchdog_enabled.load(Ordering::Relaxed) {
244                sleep(Duration::from_secs(WATCHDOG_CHECK_INTERVAL_SECS)).await;
245
246                if !watchdog_enabled.load(Ordering::Relaxed) {
247                    info!("Watchdog disabled, stopping...");
248                    break;
249                }
250
251                let mut needs_restart = false;
252                #[cfg(target_os = "windows")]
253                let mut process_died = false;
254
255                // Verificar si el proceso sigue corriendo (solo Windows)
256                #[cfg(target_os = "windows")]
257                {
258                    let mut child_guard = child_process.lock().await;
259
260                    if let Some(child) = child_guard.as_mut() {
261                        match child.try_wait() {
262                            Ok(Some(status)) => {
263                                // Proceso murió
264                                error!("MyPOS server process exited with status: {:?}", status);
265                                *child_guard = None;
266                                drop(child_guard);
267                                needs_restart = true;
268                                process_died = true;
269                            }
270                            Ok(None) => {
271                                // Proceso sigue corriendo
272                                drop(child_guard);
273                            }
274                            Err(e) => {
275                                // Error al verificar estado
276                                error!("Error checking process status: {:?}", e);
277                                *child_guard = None;
278                                drop(child_guard);
279                                needs_restart = true;
280                                process_died = true;
281                            }
282                        }
283                    } else {
284                        // No hay proceso hijo
285                        drop(child_guard);
286                        if running.load(Ordering::Relaxed) {
287                            warn!("Process handle lost, attempting restart");
288                            needs_restart = true;
289                            process_died = true;
290                        }
291                    }
292                }
293
294                // Verificar si la conexión WebSocket está activa
295                if ws_writer.lock().await.is_none() && running.load(Ordering::Relaxed) {
296                    warn!("WebSocket connection lost");
297                    needs_restart = true;
298                }
299
300                // Si necesita reiniciar
301                if needs_restart {
302                    let mut count = restart_count.lock().await;
303                    *count += 1;
304                    let attempt = *count;
305                    drop(count);
306
307                    warn!("🔄 Watchdog detected failure. Restart attempt #{}", attempt);
308
309                    // Limpiar estado
310                    *ws_writer.lock().await = None;
311                    running.store(false, Ordering::Relaxed);
312
313                    // Reintentar solo en Windows
314                    #[cfg(target_os = "windows")]
315                    {
316                        info!("Watchdog: Attempting to restart MyPOS server...");
317
318                        // Si el proceso murió, limpiar procesos residuales
319                        if process_died {
320                            info!("Watchdog: Cleaning up residual processes...");
321                            kill_process_by_name(MYPOS_SERVER_FILE);
322                            sleep(Duration::from_millis(500)).await;
323                        }
324
325                        // Esperar antes de reintentar
326                        sleep(Duration::from_secs(RESTART_DELAY_SECS)).await;
327
328                        // Lanzar proceso nuevamente
329                        let exe_path = sharing::get_path_mypos_server();
330                        info!("Watchdog: Spawning new process at: {}", exe_path);
331
332                        match Command::new(&exe_path).spawn() {
333                            Ok(child) => {
334                                *child_process.lock().await = Some(child);
335                                info!("Watchdog: ✓ Process restarted successfully");
336
337                                // Esperar a que el servidor inicie
338                                info!("Watchdog: Waiting {} seconds for server to start...", RESTART_DELAY_SECS);
339                                sleep(Duration::from_secs(RESTART_DELAY_SECS)).await;
340
341                                // Reconectar WebSocket
342                                info!("Watchdog: Attempting to reconnect WebSocket...");
343                                match Self::reconnect_websocket_static(ws_writer.clone(), response_sender.clone()).await {
344                                    Ok(_) => {
345                                        running.store(true, Ordering::Relaxed);
346                                        info!("Watchdog: ✓ WebSocket reconnected successfully");
347                                        info!("Watchdog: ✓ Full recovery completed on attempt #{}", attempt);
348                                    }
349                                    Err(e) => {
350                                        error!("Watchdog: ✗ Failed to reconnect WebSocket: {}", e);
351                                    }
352                                }
353                            }
354                            Err(e) => {
355                                error!("Watchdog: ✗ Failed to restart process: {:?}", e);
356                            }
357                        }
358                    }
359
360                    // En macOS solo intentar reconectar
361                    #[cfg(target_os = "macos")]
362                    {
363                        sleep(Duration::from_secs(RESTART_DELAY_SECS)).await;
364                        info!("Watchdog: Attempting to reconnect WebSocket...");
365                        match Self::reconnect_websocket_static(ws_writer.clone(), response_sender.clone()).await {
366                            Ok(_) => {
367                                running.store(true, Ordering::Relaxed);
368                                info!("Watchdog: WebSocket reconnected successfully");
369                            }
370                            Err(e) => {
371                                error!("Watchdog: Failed to reconnect: {}", e);
372                            }
373                        }
374                    }
375                }
376            }
377
378            info!("Watchdog stopped");
379        });
380    }
381
382    /// Helper estático para reconectar WebSocket desde el watchdog
383    async fn reconnect_websocket_static(
384        ws_writer: Arc<Mutex<Option<WsWriter>>>,
385        response_sender: Arc<Mutex<Option<oneshot::Sender<ServerResponse>>>>
386    ) -> Result<(), String> {
387        let url = "ws://127.0.0.1:8765";
388        info!("Reconnecting to WebSocket at: {}", url);
389
390        let connect_future = connect_async(url);
391        let (ws_stream, _) = match timeout(
392            Duration::from_secs(WEBSOCKET_CONNECT_TIMEOUT_SECS),
393            connect_future
394        ).await {
395            Ok(Ok(result)) => result,
396            Ok(Err(e)) => return Err(format!("Failed to reconnect: {}", e)),
397            Err(_) => return Err("Reconnection timeout".to_string()),
398        };
399
400        let (write, read) = ws_stream.split();
401        *ws_writer.lock().await = Some(write);
402
403        // Restart the WebSocket response handler
404        tokio::spawn(async move {
405            Self::handle_websocket_responses(read, response_sender).await;
406        });
407
408        info!("WebSocket reconnection completed");
409        Ok(())
410    }
411
412    // ========================================================================
413    // SECCIÓN: DETENCIÓN DEL SERVICIO
414    // ========================================================================
415
416    /// Stops the MyPOS server (Windows)
417    #[cfg(target_os = "windows")]
418    async fn stop(&self) {
419        info!("Stopping MyPOS service...");
420
421        // Deshabilitar watchdog primero
422        self.watchdog_enabled.store(false, Ordering::Relaxed);
423
424        // Esperar un momento para que el watchdog termine
425        sleep(Duration::from_millis(500)).await;
426
427        // Close WebSocket connection
428        *self.ws_writer.lock().await = None;
429
430        // Stop process
431        let mut guard = self.child_process.lock().await;
432        if let Some(mut child) = guard.take() {
433            if let Err(e) = child.kill() {
434                error!("Error killing MyPOS server: {:?}", e);
435            } else {
436                kill_process_by_name(MYPOS_SERVER_FILE);
437                info!("MyPOS server stopped.");
438            }
439        }
440
441        self.running.store(false, Ordering::Relaxed);
442        *self.restart_count.lock().await = 0;
443    }
444
445    /// Stops the service (macOS - only closes WebSocket)
446    #[cfg(target_os = "macos")]
447    async fn stop(&self) {
448        info!("macOS: Closing WebSocket connection...");
449
450        // Deshabilitar watchdog
451        self.watchdog_enabled.store(false, Ordering::Relaxed);
452
453        // Esperar un momento
454        sleep(Duration::from_millis(500)).await;
455
456        // Close WebSocket connection
457        *self.ws_writer.lock().await = None;
458
459        self.running.store(false, Ordering::Relaxed);
460        *self.restart_count.lock().await = 0;
461        info!("MyPOS WebSocket connection closed.");
462    }
463
464    // ========================================================================
465    // SECCIÓN: COMUNICACIÓN CON EL SERVIDOR
466    // ========================================================================
467
468    /// Sends a message to the WebSocket server and waits for response
469    async fn send_websocket_message(&self, message: Value) -> Result<ServerResponse, String> {
470        // Create a channel for the response
471        let (sender, receiver) = oneshot::channel();
472
473        // Store the sender for this transaction
474        {
475            let mut response_sender = self.response_sender.lock().await;
476            *response_sender = Some(sender);
477        }
478
479        // Send the message
480        {
481            let mut ws_writer = self.ws_writer.lock().await;
482            if let Some(writer) = ws_writer.as_mut() {
483                let msg = Message::Text(message.to_string());
484                if let Err(e) = writer.send(msg).await {
485                    return Err(format!("Failed to send WebSocket message: {}", e));
486                }
487            } else {
488                return Err("WebSocket not connected".to_string());
489            }
490        }
491
492        // Wait for response with timeout
493        match timeout(Duration::from_secs(60), receiver).await {
494            Ok(Ok(response)) => Ok(response),
495            Ok(Err(_)) => Err("Response channel closed".to_string()),
496            Err(_) => Err("Transaction timeout".to_string()),
497        }
498    }
499
500    // ========================================================================
501    // SECCIÓN: REINTENTOS AUTOMÁTICOS
502    // ========================================================================
503
504    /// Attempts a transaction with automatic retries
505    async fn attempt_transaction_with_retries(
506        &self,
507        action: &str,
508        amount: f64,
509    ) -> Result<ServerResponse, String> {
510        for attempt in 1..=MAX_RETRIES {
511            info!("Attempt {}/{} for {} of {:.2} EUR", attempt, MAX_RETRIES, action, amount);
512
513            let message = json!({
514                "action": action,
515                "amount": amount
516            });
517
518            match self.send_websocket_message(message).await {
519                Ok(response) => {
520                    let status_lower = response.status.to_lowercase();
521
522                    // Si es exitoso, retornar inmediatamente
523                    if status_lower == "success" {
524                        info!("Transaction successful on attempt {}", attempt);
525                        return Ok(response);
526                    }
527
528                    // Si fue cancelado por el usuario, no reintentar
529                    if status_lower == "usercancel" {
530                        warn!("Transaction cancelled by user, no retries");
531                        return Ok(response);
532                    }
533
534                    // Para otros errores, continuar con reintentos si quedan intentos
535                    if attempt < MAX_RETRIES {
536                        warn!("Attempt {} failed with status: {}. Retrying in {} seconds...",
537                              attempt, response.status, RETRY_DELAY_SECS);
538                        sleep(Duration::from_secs(RETRY_DELAY_SECS)).await;
539                    } else {
540                        error!("All {} attempts failed. Last status: {}", MAX_RETRIES, response.status);
541                        return Ok(response);
542                    }
543                }
544                Err(e) => {
545                    if attempt < MAX_RETRIES {
546                        warn!("Attempt {} failed with error: {}. Retrying in {} seconds...",
547                              attempt, e, RETRY_DELAY_SECS);
548                        sleep(Duration::from_secs(RETRY_DELAY_SECS)).await;
549                    } else {
550                        error!("All {} attempts failed. Last error: {}", MAX_RETRIES, e);
551                        return Err(e);
552                    }
553                }
554            }
555        }
556
557        Err("Maximum retries exceeded".to_string())
558    }
559
560    // ========================================================================
561    // SECCIÓN: MANEJO DE COMPRAS Y DEVOLUCIONES
562    // ========================================================================
563
564    /// Handles a purchase with automatic retries
565    async fn handle_purchase(&self, amount: f64, write: WebSocketWrite) -> (i32, String) {
566        info!("Handling purchase of: {:.2} EUR (with up to {} retries)", amount, MAX_RETRIES);
567
568        match self.attempt_transaction_with_retries("purchase", amount).await {
569            Ok(response) => {
570                let success = response.status.to_lowercase() == "success";
571                let message = response.message.unwrap_or_else(||
572                    if success {
573                        format!("Purchase of {:.2} EUR completed successfully", amount)
574                    } else {
575                        format!("Purchase of {:.2} EUR failed after {} attempts: {}",
576                                amount, MAX_RETRIES, response.status)
577                    }
578                );
579
580                // Send response to the main WebSocket client if available
581                if let Some(ws_lock) = &write {
582                    let response_msg = json!({
583                        "type": "purchase_result",
584                        "success": success,
585                        "amount": amount,
586                        "message": message,
587                        "status": response.status
588                    });
589
590                    let mut ws = ws_lock.write().await;
591                    if let Err(e) = ws.send(Message::Text(response_msg.to_string())).await {
592                        error!("Failed to send purchase result: {:?}", e);
593                    }
594                }
595
596                if success { (0, message) } else { (1, message) }
597            }
598            Err(e) => {
599                let error_msg = format!("Purchase failed after {} attempts: {}", MAX_RETRIES, e);
600                error!("{}", error_msg);
601                (1, error_msg)
602            }
603        }
604    }
605
606    /// Handles a refund with automatic retries
607    async fn handle_refund(&self, amount: f64, write: WebSocketWrite) -> (i32, String) {
608        info!("Handling refund of: {:.2} EUR (with up to {} retries)", amount, MAX_RETRIES);
609
610        match self.attempt_transaction_with_retries("refund", amount).await {
611            Ok(response) => {
612                let success = response.status.to_lowercase() == "success";
613                let message = response.message.unwrap_or_else(||
614                    if success {
615                        format!("Refund of {:.2} EUR completed successfully", amount)
616                    } else {
617                        format!("Refund of {:.2} EUR failed after {} attempts: {}",
618                                amount, MAX_RETRIES, response.status)
619                    }
620                );
621
622                // Send response to the main WebSocket client if available
623                if let Some(ws_lock) = &write {
624                    let response_msg = json!({
625                        "type": "refund_result",
626                        "success": success,
627                        "amount": amount,
628                        "message": message,
629                        "status": response.status
630                    });
631
632                    let mut ws = ws_lock.write().await;
633                    if let Err(e) = ws.send(Message::Text(response_msg.to_string())).await {
634                        error!("Failed to send refund result: {:?}", e);
635                    }
636                }
637
638                if success { (0, message) } else { (1, message) }
639            }
640            Err(e) => {
641                let error_msg = format!("Refund failed after {} attempts: {}", MAX_RETRIES, e);
642                error!("{}", error_msg);
643                (1, error_msg)
644            }
645        }
646    }
647}
648
649// ========================================================================
650// IMPLEMENTACIÓN DEL TRAIT SERVICE
651// ========================================================================
652
653#[async_trait]
654impl Service for MyPOSService {
655    /// Handles incoming service actions (START, STOP, PURCHASE, REFUND)
656    async fn run(&self, action: Value, write: WebSocketWrite) -> (i32, String) {
657        info!("MyPOSService: Running action: {:?}", action);
658
659        // Parse ACTION similar to ScaleService
660        let parsed_action = if let Some(action_str) = action.get("ACTION").and_then(|a| a.as_str()) {
661            // If ACTION is a string, parse it as JSON
662            serde_json::from_str::<Value>(action_str).unwrap_or_else(|_| {
663                // If parsing fails, treat the string as a simple command
664                json!({"command": action_str})
665            })
666        } else if let Some(action_obj) = action.get("ACTION") {
667            // If ACTION is already an object, use it directly
668            action_obj.clone()
669        } else {
670            // Fallback: use the action itself
671            action
672        };
673
674        let command = parsed_action
675            .get("command")
676            .and_then(|c| c.as_str())
677            .unwrap_or("UNKNOWN");
678
679        info!("MyPOS Command: {}", command);
680
681        match command {
682            "START" => {
683                self.launch_and_monitor().await;
684
685                #[cfg(target_os = "windows")]
686                let msg = "MyPOS server started with watchdog monitoring enabled.";
687
688                #[cfg(target_os = "macos")]
689                let msg = "MyPOS WebSocket connection established with watchdog monitoring (no process launched on macOS).";
690
691                (0, msg.to_string())
692            }
693            "STOP" => {
694                self.stop().await;
695                (0, "MyPOS stopped.".to_string())
696            }
697            "PURCHASE" => {
698                let amount = parsed_action.get("amount").and_then(|a| a.as_f64()).unwrap_or(0.0);
699                if amount > 0.0 {
700                    self.handle_purchase(amount, write).await
701                } else {
702                    (1, "Invalid purchase amount.".to_string())
703                }
704            }
705            "REFUND" => {
706                let amount = parsed_action.get("amount").and_then(|a| a.as_f64()).unwrap_or(0.0);
707                if amount > 0.0 {
708                    self.handle_refund(amount, write).await
709                } else {
710                    (1, "Invalid refund amount.".to_string())
711                }
712            }
713            _ => (1, format!("Unknown command: {}", command)),
714        }
715    }
716
717    /// Converts the service instance into a `dyn Any` reference
718    fn as_any(&self) -> &dyn std::any::Any {
719        self
720    }
721
722    /// Stops the service and cleans up any running processes
723    fn stop_service(&self) {
724        let running = self.running.clone();
725        let watchdog_enabled = self.watchdog_enabled.clone();
726
727        #[cfg(target_os = "windows")]
728        let child_process = self.child_process.clone();
729
730        let ws_writer = self.ws_writer.clone();
731
732        tokio::spawn(async move {
733            if running.load(Ordering::Relaxed) {
734                // Deshabilitar watchdog
735                watchdog_enabled.store(false, Ordering::Relaxed);
736                sleep(Duration::from_millis(500)).await;
737
738                // Close WebSocket
739                *ws_writer.lock().await = None;
740
741                // Stop process (solo Windows)
742                #[cfg(target_os = "windows")]
743                {
744                    if let Some(mut child) = child_process.lock().await.take() {
745                        if let Err(e) = child.kill() {
746                            error!("Error stopping MyPOS server: {:?}", e);
747                        } else {
748                            info!("MyPOS server stopped cleanly.");
749                        }
750                    }
751                }
752
753                #[cfg(target_os = "macos")]
754                info!("MyPOS WebSocket closed (no process to stop on macOS).");
755
756                running.store(false, Ordering::Relaxed);
757            }
758        });
759    }
760
761    /// Returns the version of the MyPOS service
762    fn get_version(&self) -> String {
763        MYPOS_VERSION.to_string()
764    }
765}