Complete CarPass product flows
This commit is contained in:
314
tests/test_product_readiness.py
Normal file
314
tests/test_product_readiness.py
Normal file
@@ -0,0 +1,314 @@
|
||||
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"},
|
||||
)
|
||||
|
||||
assert updated.status_code == 200
|
||||
assert updated.json()["plate_number"] == "34 나 7890"
|
||||
|
||||
|
||||
@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"
|
||||
Reference in New Issue
Block a user