# /home/Autopartes/pos/services/plate_lookup.py """Mexican license plate lookup service. Validates Mexican plate formats and searches the local plate_vehicles table. Since there is no free REPUVE API, this uses a tenant-local lookup table populated when customers register their vehicles. Mexican plate formats: - Standard: ABC-1234 (3 letters + hyphen + 3-4 digits) - Alternate: AB-123-C (2 letters + 3 digits + 1 letter) - Also accepted without hyphens: ABC1234, AB123C """ import re # Patterns for Mexican plates (with or without hyphens) _PATTERNS = [ re.compile(r'^[A-Z]{3}-?\d{3,4}$'), # ABC-1234 or ABC1234 re.compile(r'^[A-Z]{2}-?\d{3}-?[A-Z]$'), # AB-123-C or AB123C ] def normalize_plate(plate): """Normalize a plate string: uppercase, strip spaces/hyphens.""" if not plate: return '' return re.sub(r'[\s\-]+', '', plate.strip().upper()) def is_valid_mexican_plate(plate): """Check if a string looks like a valid Mexican license plate.""" norm = normalize_plate(plate) return any(p.match(norm) for p in _PATTERNS) def search_plate(tenant_conn, plate): """Search plate_vehicles table for a matching plate. Returns dict with vehicle info or None if not found. """ norm = normalize_plate(plate) if not norm: return None cur = tenant_conn.cursor() try: cur.execute(""" SELECT id, plate, make, model, year, vin, customer_id, created_at FROM plate_vehicles WHERE REPLACE(REPLACE(UPPER(plate), '-', ''), ' ', '') = %s LIMIT 1 """, (norm,)) row = cur.fetchone() if not row: return None return { 'id': row[0], 'plate': row[1], 'make': row[2], 'model': row[3], 'year': row[4], 'vin': row[5], 'customer_id': row[6], 'created_at': row[7].isoformat() if row[7] else None, } finally: cur.close() def register_plate(tenant_conn, plate, make=None, model=None, year=None, vin=None, customer_id=None): """Register or update a plate-to-vehicle mapping. Returns the plate_vehicles record id. """ norm_display = normalize_plate(plate) if not norm_display: raise ValueError('Plate is required') # Format nicely: ABC-1234 or AB-123-C if re.match(r'^[A-Z]{3}\d{3,4}$', norm_display): display = norm_display[:3] + '-' + norm_display[3:] elif re.match(r'^[A-Z]{2}\d{3}[A-Z]$', norm_display): display = norm_display[:2] + '-' + norm_display[2:5] + '-' + norm_display[5:] else: display = norm_display cur = tenant_conn.cursor() try: cur.execute(""" INSERT INTO plate_vehicles (plate, make, model, year, vin, customer_id) VALUES (%s, %s, %s, %s, %s, %s) ON CONFLICT (plate) DO UPDATE SET make = COALESCE(EXCLUDED.make, plate_vehicles.make), model = COALESCE(EXCLUDED.model, plate_vehicles.model), year = COALESCE(EXCLUDED.year, plate_vehicles.year), vin = COALESCE(EXCLUDED.vin, plate_vehicles.vin), customer_id = COALESCE(EXCLUDED.customer_id, plate_vehicles.customer_id) RETURNING id """, (display, make, model, year, vin, customer_id)) rec_id = cur.fetchone()[0] tenant_conn.commit() return rec_id except Exception: tenant_conn.rollback() raise finally: cur.close()