""" VIN decoder screen for the NEXUS AUTOPARTS console application. Prompts for a 17-character Vehicle Identification Number, decodes it via the NHTSA vPIC API (with local caching), and displays the decoded vehicle information. The user can then navigate to the parts catalog filtered by the matched vehicle. """ import json from console.core.screens import Screen from console.core.keybindings import Key from console.config import APP_NAME, VERSION from console.utils.vin_api import decode_vin_nhtsa # Footer labels _FOOTER_INPUT = [ ("ENTER", "Decodificar"), ("ESC", "Atras"), ] _FOOTER_RESULT = [ ("1", "Ver partes"), ("2/F3", "Nuevo VIN"), ("ESC", "Atras"), ] _FOOTER_ERROR = [ ("F3", "Nuevo VIN"), ("ESC", "Atras"), ] class VinDecoderScreen(Screen): """VIN decoder with NHTSA API integration and local cache.""" def __init__(self): super().__init__(name="vin_decoder", title="Decodificador VIN") self._vin = None self._decoded = None self._error = None self._needs_input = True # ------------------------------------------------------------------ # Helpers # ------------------------------------------------------------------ def _decode(self, vin, db): """Decode a VIN using cache first, then NHTSA API.""" self._vin = vin.upper().strip() self._decoded = None self._error = None # Check cache cached = db.get_vin_cache(self._vin) if cached: self._decoded = { "make": cached.get("make", ""), "model": cached.get("model", ""), "year": cached.get("year", ""), "engine_info": cached.get("engine_info", ""), "body_class": cached.get("body_class", ""), "drive_type": cached.get("drive_type", ""), } return # Call NHTSA API result = decode_vin_nhtsa(self._vin) if "error" in result: self._error = result["error"] return # Extract fields make = result.get("make", "") model = result.get("model", "") year = result.get("year", "") body_class = result.get("body_class", "") drive_type = result.get("drive_type", "") # Build engine info string engine_info_dict = result.get("engine_info", {}) engine_parts = [] if engine_info_dict.get("displacement_l"): engine_parts.append(f"{engine_info_dict['displacement_l']}L") if engine_info_dict.get("cylinders"): engine_parts.append(f"{engine_info_dict['cylinders']}cil") if engine_info_dict.get("fuel_type"): engine_parts.append(engine_info_dict["fuel_type"]) if engine_info_dict.get("power_hp"): engine_parts.append(f"{engine_info_dict['power_hp']}hp") engine_info = " ".join(engine_parts) self._decoded = { "make": make, "model": model, "year": year, "engine_info": engine_info, "body_class": body_class, "drive_type": drive_type, } # Cache the result try: year_int = int(year) if year else 0 except (ValueError, TypeError): year_int = 0 try: db.save_vin_cache( vin=self._vin, data=json.dumps(result), make=make, model=model, year=year_int, engine_info=engine_info, body_class=body_class, drive_type=drive_type, ) except Exception: pass # Non-critical: caching failure should not break the flow def _try_match_vehicle(self, db): """Try to match the decoded VIN to a vehicle in the database. Returns a context dict for the catalogo screen if a match is found, or None if no match exists. """ if not self._decoded: return None make = self._decoded.get("make", "") model = self._decoded.get("model", "") year = self._decoded.get("year", "") if not make or not model: return None try: year_int = int(year) if year else None except (ValueError, TypeError): year_int = None # Try to find matching model_year_engine records if year_int: mye_records = db.get_model_year_engine(make, model, year_int) else: mye_records = [] ctx = { "level": "categories", "brand": make, "model": model, "year": year, "engine": self._decoded.get("engine_info", ""), } if mye_records: # Use the first match ctx["mye_id"] = mye_records[0]["id"] return ctx # ------------------------------------------------------------------ # Render # ------------------------------------------------------------------ def render(self, context, db, renderer): # Header renderer.draw_header( f" {APP_NAME} v{VERSION}", " DECODIFICADOR VIN ", ) if self._needs_input: renderer.draw_footer(_FOOTER_INPUT) return if self._error: renderer.draw_text(5, 4, f"Error: {self._error}", "error") renderer.draw_footer(_FOOTER_ERROR) return if self._decoded is None: renderer.draw_text(5, 4, "Presione F3 para ingresar un VIN", "info") renderer.draw_footer(_FOOTER_ERROR) return # Display decoded VIN info fields = [ ("VIN", self._vin or ""), ("Marca", self._decoded.get("make", "")), ("Modelo", self._decoded.get("model", "")), ("Ano", str(self._decoded.get("year", ""))), ("Motor", self._decoded.get("engine_info", "")), ("Carroceria", self._decoded.get("body_class", "")), ("Traccion", self._decoded.get("drive_type", "")), ] renderer.draw_detail(fields, title="INFORMACION DEL VEHICULO") # Action menu below detail h, _w = renderer.get_size() action_row = 5 + len(fields) + 3 if action_row < h - 4: renderer.draw_text(action_row, 4, "1. Ver partes compatibles", "normal") renderer.draw_text(action_row + 1, 4, "2. Nueva consulta VIN", "normal") renderer.draw_footer(_FOOTER_RESULT) # ------------------------------------------------------------------ # Key handling # ------------------------------------------------------------------ def on_key(self, key, context, db, renderer, nav): # If we need input, show the input dialog if self._needs_input: self._needs_input = False value = renderer.show_input("VIN (17 caracteres)", max_len=17) if value is None: # User pressed ESC in input dialog if self._decoded is not None: return None return "back" value = value.strip() if len(value) != 17: self._error = "El VIN debe tener exactamente 17 caracteres" self._decoded = None return None self._decode(value, db) return None # ESC: go back if key == Key.ESCAPE: return "back" # F3 or '2': new VIN input if key == Key.F3 or key == ord("2"): self._needs_input = True self._error = None return None # '1': view compatible parts if key == ord("1"): if self._decoded: cat_context = self._try_match_vehicle(db) if cat_context: return ("catalogo", cat_context, "Catalogo") else: renderer.show_message( "No se encontro el vehiculo en la base de datos.\n" "Se mostrara el catalogo completo.", "info", ) return ("catalogo", {"level": "categories"}, "Catalogo") return None return None