"""Rentman API client wrapper for equipment import and request submission.""" import httpx import logging import re from typing import Any from app.config import get_settings logger = logging.getLogger(__name__) settings = get_settings() RENTMAN_BASE_URL = "https://api.rentman.net" class RentmanService: """Wrapper around the Rentman REST API using httpx.""" def __init__(self, token: str | None = None) -> None: self._token = token or settings.rentman_api_token self._base_url = RENTMAN_BASE_URL def _headers(self) -> dict[str, str]: return { "Authorization": f"Bearer {self._token}", "Content-Type": "application/json", } async def get_equipment_page(self, limit: int = 100, offset: int = 0) -> dict[str, Any]: """Fetch a single page of equipment from Rentman. Returns the raw JSON response dict with 'data' and optional 'itemCount'. """ url = f"{self._base_url}/equipment" params = {"limit": limit, "offset": offset} async with httpx.AsyncClient(timeout=30.0) as client: resp = await client.get(url, headers=self._headers(), params=params) resp.raise_for_status() return resp.json() async def get_all_equipment(self, limit: int = 100) -> list[dict[str, Any]]: """Paginate through all equipment pages until data is empty.""" all_items: list[dict[str, Any]] = [] offset = 0 while True: page = await self.get_equipment_page(limit=limit, offset=offset) data = page.get("data", []) if not data: break all_items.extend(data) offset += limit return all_items async def get_file_url(self, file_id: str | int) -> str | None: """Fetch the S3 URL for a file from Rentman. GET /files/{file_id} → response contains 'url' field with S3 link. """ url = f"{self._base_url}/files/{file_id}" try: async with httpx.AsyncClient(timeout=30.0) as client: resp = await client.get(url, headers=self._headers()) resp.raise_for_status() data = resp.json() file_data = data.get("data", data) file_url = file_data.get("url") if file_url: return file_url logger.warning("No url field in file response for file_id=%s", file_id) return None except Exception as exc: logger.warning("Failed to fetch file URL for file_id=%s: %s", file_id, exc) return None async def transform_equipment(self, raw: dict[str, Any]) -> dict[str, Any]: """Map a raw Rentman equipment object to equipment_cache schema. Uses 'image' (singular) field which contains a relative path like '/files/3173'. Fetches the actual S3 URL via GET /files/{file_id}. """ image_path = raw.get("image") image_urls: list[str] = [] if image_path and isinstance(image_path, str): # Extract file_id from path like /files/3173 match = re.search(r"/files/(\d+)", image_path) if match: file_id = match.group(1) s3_url = await self.get_file_url(file_id) if s3_url: image_urls = [s3_url] else: logger.debug("Could not extract file_id from image path: %s", image_path) group = raw.get("equipment_group") or {} category = group.get("name", "") if isinstance(group, dict) else str(group or "") return { "rentman_id": str(raw.get("id", "")), "name": raw.get("name", ""), "number": raw.get("number") or raw.get("code", ""), "category": category, "subcategory": raw.get("subcategory", ""), "description": raw.get("description", ""), "specifications": raw.get("specifications", {}), "images": image_urls, "rental_price": raw.get("rental_price"), "brand": raw.get("brand", ""), "available": raw.get("available", True), "update_hash": raw.get("updateHash", ""), } async def create_project_request(self, payload: dict[str, Any]) -> dict[str, Any]: """POST /projectrequests to create a new project request in Rentman.""" url = f"{self._base_url}/projectrequests" async with httpx.AsyncClient(timeout=30.0) as client: resp = await client.post(url, headers=self._headers(), json=payload) resp.raise_for_status() return resp.json() async def add_equipment_to_request( self, request_id: str, equipment_payload: dict[str, Any] ) -> dict[str, Any]: """POST /projectrequests/{id}/projectrequestequipment for a single item.""" url = f"{self._base_url}/projectrequests/{request_id}/projectrequestequipment" async with httpx.AsyncClient(timeout=30.0) as client: resp = await client.post(url, headers=self._headers(), json=equipment_payload) resp.raise_for_status() return resp.json() @staticmethod def build_project_request_payload(data: dict[str, Any]) -> dict[str, Any]: """Map frontend rental request data to Rentman POST /projectrequests payload.""" date_start = data.get("date_start") date_end = data.get("date_end") contact_name = data.get("contact_name", "") parts = contact_name.strip().split(" ", 1) first_name = parts[0] if parts else "" last_name = parts[1] if len(parts) > 1 else "" street = data.get("contact_street", "") house_number = "" if street: match = re.search(r"(\d+[a-zA-Z]*)", street) if match: house_number = match.group(1) return { "name": data.get("event_name", ""), "planperiod_start": f"{date_start}T08:00:00+02:00" if date_start else None, "planperiod_end": f"{date_end}T02:00:00+02:00" if date_end else None, "usageperiod_start": f"{date_start}T18:00:00+02:00" if date_start else None, "usageperiod_end": f"{date_end}T23:59:00+02:00" if date_end else None, "contact_name": data.get("contact_company") or data.get("contact_name", ""), "contact_person_first_name": first_name, "contact_person_lastname": last_name, "contact_person_email": data.get("contact_email", ""), "contact_person_phone": data.get("contact_phone", ""), "location_name": data.get("location", ""), "location_mailing_street": street, "location_mailing_number": house_number, "location_mailing_postalcode": data.get("contact_postalcode", ""), "location_mailing_city": data.get("contact_city") or data.get("location", ""), "remark": data.get("message", ""), } @staticmethod def build_equipment_payload(item: dict[str, Any]) -> dict[str, Any]: """Map a single rental request item to Rentman projectrequestequipment payload.""" return { "name": item.get("equipment_name", ""), "quantity": item.get("quantity", 1), "quantity_total": item.get("quantity", 1), "unit_price": item.get("unit_price", 0), "linked_equipment": f"/equipment/{item.get('rentman_equipment_id', '')}" if item.get("rentman_equipment_id") else None, }