339 lines
11 KiB
Python
339 lines
11 KiB
Python
import pytest
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_license_plate_can_be_saved_and_edited(client, auth_headers) -> None:
|
|
car = (
|
|
await client.post(
|
|
"/api/cars",
|
|
headers=auth_headers,
|
|
json={"name": "Plate car", "plate_number": "12 가 3456"},
|
|
)
|
|
).json()
|
|
|
|
updated = await client.patch(
|
|
f"/api/cars/{car['id']}",
|
|
headers=auth_headers,
|
|
json={"plate_number": "34 나 7890", "body_type": "SUV"},
|
|
)
|
|
|
|
assert updated.status_code == 200
|
|
assert updated.json()["plate_number"] == "34 나 7890"
|
|
assert updated.json()["body_type"] == "SUV"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_insurance_six_months_allocates_proportionally(client, auth_headers) -> None:
|
|
car = (await client.post("/api/cars", headers=auth_headers, json={"name": "Insurance 6"})).json()
|
|
await client.post(
|
|
"/api/expenses",
|
|
headers=auth_headers,
|
|
json={
|
|
"car_id": car["id"],
|
|
"entry_date": "2026-01-01",
|
|
"category": "insurance",
|
|
"title": "Insurance",
|
|
"total_cost": 600,
|
|
"period_start": "2026-01-01",
|
|
"period_months": 6,
|
|
"payment_period_months": 6,
|
|
"is_recurring": True,
|
|
},
|
|
)
|
|
|
|
stats = await client.get(
|
|
f"/api/cars/{car['id']}/stats?date_from=2026-02-01&date_to=2026-02-28",
|
|
headers=auth_headers,
|
|
)
|
|
|
|
assert stats.status_code == 200
|
|
assert stats.json()["cost_by_category"]["insurance"] == "100.00"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_custom_insurance_period_allocates_by_overlap(client, auth_headers) -> None:
|
|
car = (await client.post("/api/cars", headers=auth_headers, json={"name": "Insurance custom"})).json()
|
|
await client.post(
|
|
"/api/expenses",
|
|
headers=auth_headers,
|
|
json={
|
|
"car_id": car["id"],
|
|
"entry_date": "2026-01-15",
|
|
"category": "insurance",
|
|
"title": "Short insurance",
|
|
"total_cost": 310,
|
|
"period_start": "2026-01-15",
|
|
"period_end": "2026-02-14",
|
|
"is_recurring": True,
|
|
},
|
|
)
|
|
|
|
stats = await client.get(
|
|
f"/api/cars/{car['id']}/stats?date_from=2026-02-01&date_to=2026-02-28",
|
|
headers=auth_headers,
|
|
)
|
|
|
|
assert stats.status_code == 200
|
|
assert stats.json()["cost_by_category"]["insurance"] == "140.00"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_loan_calculator_regular_and_zero_rate(client, auth_headers) -> None:
|
|
regular = await client.post(
|
|
"/api/loans/calculate",
|
|
headers=auth_headers,
|
|
json={"principal": 6000000, "term_months": 36, "annual_interest_rate": 5.5},
|
|
)
|
|
zero = await client.post(
|
|
"/api/loans/calculate",
|
|
headers=auth_headers,
|
|
json={"principal": 1200, "term_months": 12, "annual_interest_rate": 0},
|
|
)
|
|
|
|
assert regular.status_code == 200
|
|
assert float(regular.json()["monthly_payment"]) > 0
|
|
assert zero.json()["monthly_payment"] == "100.00"
|
|
assert zero.json()["total_interest"] == "0.00"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_ownership_cost_includes_credit_and_fixed_variable_split(client, auth_headers) -> None:
|
|
car = (
|
|
await client.post(
|
|
"/api/cars",
|
|
headers=auth_headers,
|
|
json={
|
|
"name": "Credit car",
|
|
"purchase_type": "credit",
|
|
"loan_principal": 1200,
|
|
"loan_term_months": 12,
|
|
"loan_annual_interest_rate": 0,
|
|
"loan_first_payment_date": "2026-01-01",
|
|
},
|
|
)
|
|
).json()
|
|
await client.post(
|
|
"/api/fuel",
|
|
headers=auth_headers,
|
|
json={
|
|
"car_id": car["id"],
|
|
"entry_date": "2026-01-10",
|
|
"odometer": 1000,
|
|
"liters": 20,
|
|
"price_per_liter": 2,
|
|
},
|
|
)
|
|
await client.post(
|
|
"/api/expenses",
|
|
headers=auth_headers,
|
|
json={
|
|
"car_id": car["id"],
|
|
"entry_date": "2026-01-12",
|
|
"category": "fine",
|
|
"title": "Fine",
|
|
"total_cost": 30,
|
|
},
|
|
)
|
|
|
|
stats = await client.get(
|
|
f"/api/cars/{car['id']}/stats?date_from=2026-01-01&date_to=2026-01-31",
|
|
headers=auth_headers,
|
|
)
|
|
|
|
payload = stats.json()
|
|
assert payload["loan_principal_cost"] == "100.00"
|
|
assert payload["loan_interest_cost"] == "0.00"
|
|
assert payload["fixed_costs"] == "100.00"
|
|
assert payload["variable_costs"] == "70.00"
|
|
assert payload["total_cost_with_credit"] == "170.00"
|
|
assert payload["total_cost_without_credit"] == "70.00"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_odometer_lower_fuel_requires_confirmation_and_delete_recalculates(client, auth_headers) -> None:
|
|
car = (await client.post("/api/cars", headers=auth_headers, json={"name": "Odo car"})).json()
|
|
first = (
|
|
await client.post(
|
|
"/api/fuel",
|
|
headers=auth_headers,
|
|
json={
|
|
"car_id": car["id"],
|
|
"entry_date": "2026-01-01",
|
|
"odometer": 1000,
|
|
"liters": 20,
|
|
"price_per_liter": 2,
|
|
},
|
|
)
|
|
).json()
|
|
second = (
|
|
await client.post(
|
|
"/api/fuel",
|
|
headers=auth_headers,
|
|
json={
|
|
"car_id": car["id"],
|
|
"entry_date": "2026-01-02",
|
|
"odometer": 2000,
|
|
"liters": 20,
|
|
"price_per_liter": 2,
|
|
},
|
|
)
|
|
).json()
|
|
blocked = await client.post(
|
|
"/api/fuel",
|
|
headers=auth_headers,
|
|
json={
|
|
"car_id": car["id"],
|
|
"entry_date": "2026-01-03",
|
|
"odometer": 900,
|
|
"liters": 20,
|
|
"price_per_liter": 2,
|
|
},
|
|
)
|
|
await client.delete(f"/api/fuel/{second['id']}", headers=auth_headers)
|
|
refreshed = await client.get(f"/api/cars/{car['id']}", headers=auth_headers)
|
|
history = await client.get(f"/api/cars/{car['id']}/odometer-history", headers=auth_headers)
|
|
|
|
assert first["odometer"] == 1000
|
|
assert blocked.status_code == 409
|
|
assert refreshed.json()["current_odometer"] == 1000
|
|
assert len(history.json()) >= 2
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_service_lower_odometer_can_be_confirmed(client, auth_headers) -> None:
|
|
car = (
|
|
await client.post(
|
|
"/api/cars",
|
|
headers=auth_headers,
|
|
json={"name": "Service odo", "current_odometer": 1000},
|
|
)
|
|
).json()
|
|
blocked = await client.post(
|
|
"/api/service",
|
|
headers=auth_headers,
|
|
json={
|
|
"car_id": car["id"],
|
|
"entry_date": "2026-01-02",
|
|
"odometer": 900,
|
|
"service_type": "maintenance",
|
|
"title": "Correction",
|
|
"total_cost": 0,
|
|
},
|
|
)
|
|
confirmed = await client.post(
|
|
"/api/service",
|
|
headers=auth_headers,
|
|
json={
|
|
"car_id": car["id"],
|
|
"entry_date": "2026-01-02",
|
|
"odometer": 900,
|
|
"service_type": "maintenance",
|
|
"title": "Correction",
|
|
"total_cost": 0,
|
|
"confirm_lower_odometer": True,
|
|
},
|
|
)
|
|
|
|
assert blocked.status_code == 409
|
|
assert confirmed.status_code == 201
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_full_tank_analysis_uses_full_tank_intervals_and_warns(client, auth_headers) -> None:
|
|
car = (await client.post("/api/cars", headers=auth_headers, json={"name": "Tank car"})).json()
|
|
for odometer, liters, full in [
|
|
(1000, 40, True),
|
|
(1200, 10, False),
|
|
(1500, 35, True),
|
|
(1600, 40, True),
|
|
]:
|
|
await client.post(
|
|
"/api/fuel",
|
|
headers=auth_headers,
|
|
json={
|
|
"car_id": car["id"],
|
|
"entry_date": "2026-01-01",
|
|
"odometer": odometer,
|
|
"liters": liters,
|
|
"price_per_liter": 2,
|
|
"is_full_tank": full,
|
|
},
|
|
)
|
|
|
|
analytics = await client.get(f"/api/cars/{car['id']}/analytics", headers=auth_headers)
|
|
|
|
payload = analytics.json()
|
|
assert payload["average_full_tank_distance"] == 300.0
|
|
assert payload["last_full_tank_distance"] == 100
|
|
assert payload["full_tank_warning"] is not None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_parser_recognizes_full_tank_and_credit_purchase(client, auth_headers) -> None:
|
|
fuel = await client.post(
|
|
"/api/parse/record",
|
|
headers=auth_headers,
|
|
json={"text": "заправил полный бак 43 литра на 72000, пробег 184230"},
|
|
)
|
|
purchase = await client.post(
|
|
"/api/parse/record",
|
|
headers=auth_headers,
|
|
json={"text": "купил машину за 8500000 вон, кредит 6000000 на 36 месяцев под 5.5%"},
|
|
)
|
|
|
|
assert fuel.json()["event_type"] == "fuel"
|
|
assert fuel.json()["data"]["is_full_tank"] is True
|
|
assert purchase.json()["event_type"] == "vehicle_purchase"
|
|
assert purchase.json()["data"]["purchase_type"] == "credit"
|
|
assert purchase.json()["data"]["loan_term_months"] == 36
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_admin_request_changes_keeps_application_visible_to_moderation(
|
|
client, auth_headers, admin_auth_headers, internal_headers
|
|
) -> None:
|
|
center = (
|
|
await client.post(
|
|
"/api/service-centers",
|
|
headers=auth_headers,
|
|
json={"display_name": "Needs Work Service", "country": "KR"},
|
|
)
|
|
).json()
|
|
await client.post(
|
|
"/api/users",
|
|
headers=internal_headers,
|
|
json={"telegram_id": 9001, "platform_role": "admin"},
|
|
)
|
|
|
|
response = await client.post(
|
|
f"/api/admin/service-centers/{center['id']}/request-changes",
|
|
headers=admin_auth_headers,
|
|
json={"reason": "Добавьте документы", "comment": "Нужны фото регистрации"},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert response.json()["verification_status"] == "needs_changes"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_service_center_application_is_visible_to_moderators(
|
|
client, auth_headers, admin_auth_headers, internal_headers
|
|
) -> None:
|
|
center = (
|
|
await client.post(
|
|
"/api/service-centers",
|
|
headers=auth_headers,
|
|
json={"display_name": "Pending Review Service", "country": "KR"},
|
|
)
|
|
).json()
|
|
await client.post(
|
|
"/api/users",
|
|
headers=internal_headers,
|
|
json={"telegram_id": 9001, "platform_role": "moderator"},
|
|
)
|
|
|
|
pending = await client.get("/api/admin/service-centers/pending", headers=admin_auth_headers)
|
|
|
|
assert pending.status_code == 200
|
|
assert center["id"] in [item["id"] for item in pending.json()]
|