"""Warranty / RMA engine (Mejora #10). Registers warranties at sale time and manages the claim lifecycle. Tables: warranties — one row per warranted item sold warranty_claims — one row per claim filed """ from datetime import date, timedelta from services.audit import log_action def register_warranty(conn, sale_id, sale_item_id, inventory_id, customer_id, warranty_months, supplier_id=None, part_number=None, name=None, notes=None): """Register a warranty for a sold item. Args: warranty_months: int (e.g., 12, 24, 36) Returns: int: warranty_id """ if not warranty_months or warranty_months <= 0: raise ValueError("warranty_months must be a positive integer") start = date.today() end = start + timedelta(days=30 * warranty_months) cur = conn.cursor() cur.execute(""" INSERT INTO warranties (sale_id, sale_item_id, inventory_id, customer_id, supplier_id, part_number, name, warranty_months, start_date, end_date, notes) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) RETURNING id """, ( sale_id, sale_item_id, inventory_id, customer_id, supplier_id, part_number, name, warranty_months, start, end, notes )) w_id = cur.fetchone()[0] cur.close() log_action(conn, 'WARRANTY_REGISTER', 'warranty', w_id, new_value={'months': warranty_months, 'end_date': str(end)}) return w_id def create_claim(conn, warranty_id, reason, employee_id=None, notes=None): """File a warranty claim. Args: warranty_id: int reason: str (min 10 chars) Returns: int: claim_id """ if not reason or len(reason.strip()) < 10: raise ValueError("Claim reason must be at least 10 characters") cur = conn.cursor() # Verify warranty exists and is active cur.execute("SELECT status FROM warranties WHERE id = %s", (warranty_id,)) row = cur.fetchone() if not row: raise ValueError("Warranty not found") if row[0] != 'active': raise ValueError(f"Cannot claim a warranty with status '{row[0]}'") cur.execute(""" INSERT INTO warranty_claims (warranty_id, reason, employee_id, notes) VALUES (%s, %s, %s, %s) RETURNING id """, (warranty_id, reason, employee_id, notes)) claim_id = cur.fetchone()[0] cur.close() log_action(conn, 'WARRANTY_CLAIM', 'warranty_claim', claim_id, new_value={'warranty_id': warranty_id, 'reason': reason}) return claim_id def resolve_claim(conn, claim_id, resolution, diagnosis=None, replacement_inventory_id=None, refund_amount=None, labor_cost=None, supplier_rma_number=None, notes=None): """Resolve a warranty claim. Args: resolution: 'approved'|'rejected'|'repaired'|'replaced'|'refunded' """ if resolution not in ('approved', 'rejected', 'repaired', 'replaced', 'refunded'): raise ValueError(f"Invalid resolution: {resolution}") cur = conn.cursor() cur.execute(""" UPDATE warranty_claims SET resolution = %s, diagnosis = COALESCE(%s, diagnosis), replacement_inventory_id = COALESCE(%s, replacement_inventory_id), refund_amount = COALESCE(%s, refund_amount), labor_cost = COALESCE(%s, labor_cost), supplier_rma_number = COALESCE(%s, supplier_rma_number), notes = COALESCE(notes || ' | ', '') || %s, status = 'resolved', resolved_at = NOW() WHERE id = %s AND status != 'closed' """, ( resolution, diagnosis, replacement_inventory_id, refund_amount, labor_cost, supplier_rma_number, notes or 'Resuelto', claim_id )) updated = cur.rowcount > 0 # If replaced or refunded, mark warranty as claimed if updated and resolution in ('replaced', 'refunded', 'approved'): cur.execute(""" UPDATE warranties SET status = 'claimed' WHERE id = (SELECT warranty_id FROM warranty_claims WHERE id = %s) """, (claim_id,)) cur.close() return updated def close_claim(conn, claim_id): """Close a resolved claim (final status).""" cur = conn.cursor() cur.execute(""" UPDATE warranty_claims SET status = 'closed' WHERE id = %s AND status = 'resolved' """, (claim_id,)) updated = cur.rowcount > 0 cur.close() return updated def get_warranty(conn, warranty_id): """Get warranty detail.""" cur = conn.cursor() cur.execute(""" SELECT w.id, w.sale_id, w.sale_item_id, w.inventory_id, w.customer_id, w.supplier_id, w.part_number, w.name, w.warranty_months, w.start_date, w.end_date, w.status, w.notes, w.created_at, c.name as customer_name, s.name as supplier_name FROM warranties w LEFT JOIN customers c ON w.customer_id = c.id LEFT JOIN suppliers s ON w.supplier_id = s.id WHERE w.id = %s """, (warranty_id,)) row = cur.fetchone() cur.close() if not row: return None return { 'id': row[0], 'sale_id': row[1], 'sale_item_id': row[2], 'inventory_id': row[3], 'customer_id': row[4], 'supplier_id': row[5], 'part_number': row[6], 'name': row[7], 'warranty_months': row[8], 'start_date': str(row[9]), 'end_date': str(row[10]), 'status': row[11], 'notes': row[12], 'created_at': str(row[13]), 'customer_name': row[14], 'supplier_name': row[15], } def list_warranties(conn, customer_id=None, status=None, limit=50, offset=0): """List warranties.""" cur = conn.cursor() filters = [] params = [] if customer_id: filters.append("w.customer_id = %s") params.append(customer_id) if status: filters.append("w.status = %s") params.append(status) where = "WHERE " + " AND ".join(filters) if filters else "" cur.execute(f""" SELECT w.id, w.part_number, w.name, w.warranty_months, w.start_date, w.end_date, w.status, c.name as customer_name FROM warranties w LEFT JOIN customers c ON w.customer_id = c.id {where} ORDER BY w.end_date ASC LIMIT %s OFFSET %s """, params + [limit, offset]) rows = cur.fetchall() cur.close() return [{ 'id': r[0], 'part_number': r[1], 'name': r[2], 'warranty_months': r[3], 'start_date': str(r[4]), 'end_date': str(r[5]), 'status': r[6], 'customer_name': r[7], } for r in rows] def get_claim(conn, claim_id): """Get claim detail.""" cur = conn.cursor() cur.execute(""" SELECT wc.id, wc.warranty_id, wc.claim_date, wc.reason, wc.diagnosis, wc.resolution, wc.replacement_inventory_id, wc.refund_amount, wc.labor_cost, wc.status, wc.supplier_rma_number, wc.notes, wc.created_at, wc.resolved_at, w.part_number, w.name, w.customer_id, c.name as customer_name FROM warranty_claims wc JOIN warranties w ON wc.warranty_id = w.id LEFT JOIN customers c ON w.customer_id = c.id WHERE wc.id = %s """, (claim_id,)) row = cur.fetchone() cur.close() if not row: return None return { 'id': row[0], 'warranty_id': row[1], 'claim_date': str(row[2]), 'reason': row[3], 'diagnosis': row[4], 'resolution': row[5], 'replacement_inventory_id': row[6], 'refund_amount': float(row[7]) if row[7] else None, 'labor_cost': float(row[8]) if row[8] else None, 'status': row[9], 'supplier_rma_number': row[10], 'notes': row[11], 'created_at': str(row[12]), 'resolved_at': str(row[13]) if row[13] else None, 'part_number': row[14], 'name': row[15], 'customer_id': row[16], 'customer_name': row[17], } def list_claims(conn, status=None, warranty_id=None, limit=50, offset=0): """List warranty claims.""" cur = conn.cursor() filters = [] params = [] if status: filters.append("wc.status = %s") params.append(status) if warranty_id: filters.append("wc.warranty_id = %s") params.append(warranty_id) where = "WHERE " + " AND ".join(filters) if filters else "" cur.execute(f""" SELECT wc.id, wc.claim_date, wc.reason, wc.resolution, wc.status, w.part_number, w.name, c.name as customer_name FROM warranty_claims wc JOIN warranties w ON wc.warranty_id = w.id LEFT JOIN customers c ON w.customer_id = c.id {where} ORDER BY wc.created_at DESC LIMIT %s OFFSET %s """, params + [limit, offset]) rows = cur.fetchall() cur.close() return [{ 'id': r[0], 'claim_date': str(r[1]), 'reason': r[2], 'resolution': r[3], 'status': r[4], 'part_number': r[5], 'name': r[6], 'customer_name': r[7], } for r in rows] def expire_warranties(conn): """Batch-update warranties whose end_date has passed to 'expired'. Should be run periodically (e.g., daily via cron). Returns number of warranties expired. """ cur = conn.cursor() cur.execute(""" UPDATE warranties SET status = 'expired' WHERE status = 'active' AND end_date < CURRENT_DATE """) count = cur.rowcount cur.close() return count