fix: repair Rentman equipment sync with real API field mapping
- Map real Rentman fields: price -> rental_price, folder -> category/subcategory (resolved via folder hierarchy to top-level categories), code -> number, shop_description_long (fallback short/external_remark) -> description (HTML stripped) - Sync only in_shop items; exclude archived/temporary from public catalog - Extract known brands from equipment names (d&b, Shure, Pioneer, ...) - Build specifications from physical fields (power, weight, dimensions) - Persist equipment images in docker volume (survive redeploys) - Router: filter available items, add price_asc/price_desc sorting - Frontend: show prices in catalog card, detail page and cart with per-day subtotal; price sorting options - Tests: use real Rentman field names in fixtures, mock folder_map
This commit is contained in:
@@ -10,31 +10,63 @@ from app.services.rentman_service import RentmanService
|
||||
|
||||
|
||||
def make_raw_equipment(rid: str, name: str, category: str = "Lautsprecher") -> dict:
|
||||
"""Raw equipment item using REAL Rentman API field names."""
|
||||
return {
|
||||
"id": rid,
|
||||
"name": name,
|
||||
"number": f"{name[:3].upper()}-001",
|
||||
"displayname": name,
|
||||
"code": f"{name[:3].upper()}-001",
|
||||
"equipment_group": {"name": category},
|
||||
"description": f"Description for {name}",
|
||||
"specifications": {"weight": 50, "power": 750},
|
||||
"images": [{"url": f"https://example.com/{rid}.jpg"}],
|
||||
"folder": f"/folders/{int(rid) % 5 + 1}",
|
||||
"shop_description_long": f"<p>Description for {name}</p>",
|
||||
"external_remark": "",
|
||||
"power": 750,
|
||||
"weight": 50,
|
||||
"empty_weight": 50,
|
||||
"image": f"/files/{rid}",
|
||||
"price": 150.00,
|
||||
"in_shop": True,
|
||||
"in_archive": False,
|
||||
"temporary": False,
|
||||
"updateHash": f"hash-{rid}",
|
||||
}
|
||||
|
||||
|
||||
def make_transformed(raw: dict, folder_map=None) -> dict:
|
||||
"""Transformed equipment dict as produced by the real Rentman mapping (no network)."""
|
||||
return {
|
||||
"rentman_id": str(raw["id"]),
|
||||
"name": raw["name"],
|
||||
"number": raw.get("code", ""),
|
||||
"category": "Tontechnik",
|
||||
"subcategory": "Mikrofone",
|
||||
"description": f"Description for {raw['name']}",
|
||||
"specifications": None,
|
||||
"images": [],
|
||||
"rental_price": 150.00,
|
||||
"brand": "L-Acoustics",
|
||||
"brand": "",
|
||||
"available": True,
|
||||
"update_hash": raw.get("updateHash", ""),
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_transform_equipment():
|
||||
raw = make_raw_equipment("42", "K2 Line Array", "Lautsprecher")
|
||||
result = RentmanService.transform_equipment(raw)
|
||||
raw = make_raw_equipment("42", "Shure SM58 Mikrofon dynamisch")
|
||||
service = RentmanService.__new__(RentmanService) # no network on init path
|
||||
folder_map = {
|
||||
f"/folders/{42 % 5 + 1}": {"category": "Tontechnik", "subcategory": "Mikrofone"},
|
||||
}
|
||||
with patch.object(RentmanService, "get_file_url", new=AsyncMock(return_value="https://example.com/42.jpg")):
|
||||
result = await service.transform_equipment(raw, folder_map=folder_map)
|
||||
assert result["rentman_id"] == "42"
|
||||
assert result["name"] == "K2 Line Array"
|
||||
assert result["category"] == "Lautsprecher"
|
||||
assert result["images"] == ["https://example.com/42.jpg"]
|
||||
assert result["brand"] == "L-Acoustics"
|
||||
assert result["name"] == "Shure SM58 Mikrofon dynamisch"
|
||||
assert result["category"] == "Tontechnik"
|
||||
assert result["subcategory"] == "Mikrofone"
|
||||
assert result["rental_price"] == 150.00
|
||||
assert result["brand"] == "Shure"
|
||||
assert result["available"] is True
|
||||
assert result["description"] == "Description for Shure SM58 Mikrofon dynamisch"
|
||||
assert result["images"] == ["https://example.com/42.jpg"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -49,6 +81,8 @@ async def test_paginated_import(test_db):
|
||||
mock_rentman.get_all_equipment = AsyncMock(return_value=[
|
||||
*[make_raw_equipment(str(i), f"Item {i}") for i in range(250)]
|
||||
])
|
||||
mock_rentman.get_folder_map = AsyncMock(return_value={})
|
||||
mock_rentman.transform_equipment = AsyncMock(side_effect=make_transformed)
|
||||
|
||||
with patch("app.services.sync_service.cache") as mock_cache:
|
||||
mock_cache.delete_pattern = AsyncMock(return_value=0)
|
||||
@@ -83,6 +117,8 @@ async def test_sync_upsert_existing(test_db):
|
||||
mock_rentman.get_all_equipment = AsyncMock(return_value=[
|
||||
make_raw_equipment("100", "New Name", "Lautsprecher")
|
||||
])
|
||||
mock_rentman.get_folder_map = AsyncMock(return_value={})
|
||||
mock_rentman.transform_equipment = AsyncMock(side_effect=make_transformed)
|
||||
|
||||
with patch("app.services.sync_service.cache") as mock_cache:
|
||||
mock_cache.delete_pattern = AsyncMock(return_value=0)
|
||||
|
||||
Reference in New Issue
Block a user