import re import base64 from decimal import Decimal, ROUND_HALF_UP from typing import Tuple, Optional from fastapi import HTTPException, status def validate_lock_credentials(lock_type: str, lock_passcode: Optional[str]) -> Optional[str]: """ Validates lock_type and lock_passcode strictly according to security requirements. Returns cleaned passcode or None. """ valid_types = {"NONE", "PIN", "PASSWORD", "PATTERN"} if lock_type not in valid_types: raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=f"Invalid lock_type '{lock_type}'. Must be one of: NONE, PIN, PASSWORD, PATTERN." ) if lock_type == "NONE": return None if not lock_passcode or not lock_passcode.strip(): raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=f"Passcode is required when lock_type is '{lock_type}'." ) passcode = lock_passcode.strip() if lock_type == "PIN": if not re.match(r"^\d{4,8}$", passcode): raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="PIN must consist strictly of 4 to 8 numeric digits (0-9)." ) return passcode elif lock_type == "PASSWORD": if len(passcode) < 1 or len(passcode) > 64: raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="Password length must be between 1 and 64 characters." ) return passcode elif lock_type == "PATTERN": # Format e.g. "1-4-7-8-9" or "1-2-3-6-9" parts = passcode.split("-") if len(parts) < 4 or len(parts) > 9: raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="Pattern sequence must contain between 4 and 9 nodes." ) seen_nodes = set() for node in parts: if not node.isdigit(): raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=f"Invalid pattern node '{node}'. All nodes must be numbers between 1 and 9." ) n_int = int(node) if n_int < 1 or n_int > 9: raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=f"Pattern node '{n_int}' is out of bounds (must be 1-9)." ) if n_int in seen_nodes: raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=f"Duplicate node '{n_int}' in pattern sequence. A node cannot be visited twice." ) seen_nodes.add(n_int) return passcode return None def calculate_fulfillment_pricing(base_price_val: float, fulfillment_type: str) -> Tuple[Decimal, Decimal, Decimal, Decimal, Decimal]: """ Authoritative server calculation using Decimal arithmetic: returns (base_price, fulfillment_fee, total_price, advance_deposit, remaining_balance) """ base = Decimal(str(base_price_val)).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP) if fulfillment_type == "DOORSTEP_PICKUP": fee = Decimal("250.00") else: # WALK_IN or COURIER fee = Decimal("0.00") total = base + fee advance = (total * Decimal("0.20")).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP) balance = total - advance return base, fee, total, advance, balance def encrypt_credential(plain_text: str) -> Optional[str]: """Encrypts device credential at rest.""" if not plain_text: return None try: from cryptography.fernet import Fernet import os key = os.getenv("SECRET_KEY", "uO_v6N9kK7X6W_N7b5V8X3Z1Y9W5V3Z1Y9W5V3Z1Y9W=") key_bytes = base64.urlsafe_b64encode(key.encode()[:32].ljust(32, b"0")) f = Fernet(key_bytes) return f.encrypt(plain_text.encode()).decode() except Exception: encoded = base64.b64encode(plain_text.encode()).decode() return f"ENC_{encoded}" def decrypt_credential(cipher_text: str) -> Optional[str]: """Decrypts stored device credential for authorized technician viewing.""" if not cipher_text: return None try: if cipher_text.startswith("ENC_"): raw = cipher_text[4:] return base64.b64decode(raw.encode()).decode() from cryptography.fernet import Fernet import os key = os.getenv("SECRET_KEY", "uO_v6N9kK7X6W_N7b5V8X3Z1Y9W5V3Z1Y9W5V3Z1Y9W=") key_bytes = base64.urlsafe_b64encode(key.encode()[:32].ljust(32, b"0")) f = Fernet(key_bytes) return f.decrypt(cipher_text.encode()).decode() except Exception: return None