feat(api): customer filters user_id / add_time_since / last_chat_since
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,9 +1,10 @@
|
|||||||
"""GET /api/customers — paginated, filterable customer list.
|
"""GET /api/customers — paginated, filterable customer list.
|
||||||
|
|
||||||
Read-only query over the mirrored ``customer`` table. Supports filtering by
|
Read-only query over the mirrored ``customer`` table. Supports filtering by
|
||||||
``channel``, ``owner_account_id`` (direct columns) and ``department_id``
|
``channel``, ``owner_account_id``, ``user_id``, ``add_time_since`` and
|
||||||
(resolved through ``customer.user_id`` -> ``staff.dept_id``). Results are
|
``last_chat_since`` (direct columns) and ``department_id`` (resolved through
|
||||||
ordered by ``last_chat_time desc`` (nulls last) then ``friend_id`` so the
|
``customer.user_id`` -> ``staff.dept_id``). Results are ordered by
|
||||||
|
``last_chat_time desc`` (nulls last) then ``friend_id`` so the
|
||||||
most-recently-active contacts surface first.
|
most-recently-active contacts surface first.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -34,6 +35,9 @@ async def list_customers(
|
|||||||
channel: Annotated[str | None, Query()] = None,
|
channel: Annotated[str | None, Query()] = None,
|
||||||
department_id: Annotated[str | None, Query()] = None,
|
department_id: Annotated[str | None, Query()] = None,
|
||||||
owner_account_id: Annotated[str | None, Query()] = None,
|
owner_account_id: Annotated[str | None, Query()] = None,
|
||||||
|
user_id: Annotated[str | None, Query()] = None,
|
||||||
|
add_time_since: Annotated[int | None, Query(ge=0)] = None,
|
||||||
|
last_chat_since: Annotated[int | None, Query(ge=0)] = None,
|
||||||
) -> Page[CustomerOut]:
|
) -> Page[CustomerOut]:
|
||||||
"""List customers with optional filters and pagination."""
|
"""List customers with optional filters and pagination."""
|
||||||
conditions = []
|
conditions = []
|
||||||
@@ -47,6 +51,12 @@ async def list_customers(
|
|||||||
Staff.dept_id == department_id
|
Staff.dept_id == department_id
|
||||||
)
|
)
|
||||||
conditions.append(Customer.user_id.in_(dept_user_ids))
|
conditions.append(Customer.user_id.in_(dept_user_ids))
|
||||||
|
if user_id is not None:
|
||||||
|
conditions.append(Customer.user_id == user_id)
|
||||||
|
if add_time_since is not None:
|
||||||
|
conditions.append(Customer.add_time >= add_time_since)
|
||||||
|
if last_chat_since is not None:
|
||||||
|
conditions.append(Customer.last_chat_time >= last_chat_since)
|
||||||
|
|
||||||
total_stmt = sa.select(sa.func.count()).select_from(Customer)
|
total_stmt = sa.select(sa.func.count()).select_from(Customer)
|
||||||
if conditions:
|
if conditions:
|
||||||
|
|||||||
@@ -255,3 +255,90 @@ async def test_filters_compose_channel_and_owner(client: AsyncClient):
|
|||||||
body = r.json()
|
body = r.json()
|
||||||
assert body["total"] == 2
|
assert body["total"] == 2
|
||||||
assert {it["friend_id"] for it in body["items"]} == {"wx_a", "wx_b"}
|
assert {it["friend_id"] for it in body["items"]} == {"wx_a", "wx_b"}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
async def filter_client():
|
||||||
|
"""Independent in-memory client seeded for the new add_time/last_chat filters.
|
||||||
|
|
||||||
|
Deliberately separate from the shared ``client`` fixture: that one seeds 3
|
||||||
|
rows and four existing tests assert ``total == 3`` against it, so adding
|
||||||
|
rows there would break them. This fixture mirrors the engine + override
|
||||||
|
pattern of ``client`` but seeds rows that specifically exercise the
|
||||||
|
``user_id`` / ``add_time_since`` / ``last_chat_since`` filters.
|
||||||
|
"""
|
||||||
|
engine = create_async_engine(
|
||||||
|
"sqlite+aiosqlite:///:memory:",
|
||||||
|
poolclass=StaticPool,
|
||||||
|
connect_args={"check_same_thread": False},
|
||||||
|
)
|
||||||
|
async with engine.begin() as conn:
|
||||||
|
await conn.run_sync(Base.metadata.create_all)
|
||||||
|
|
||||||
|
SessionLocal = async_sessionmaker(engine, expire_on_commit=False)
|
||||||
|
async with SessionLocal() as session:
|
||||||
|
session.add_all(
|
||||||
|
[
|
||||||
|
Customer(
|
||||||
|
friend_id="u1_a",
|
||||||
|
channel="wechat",
|
||||||
|
user_id="U1",
|
||||||
|
add_time=1000,
|
||||||
|
last_chat_time=500,
|
||||||
|
version="v1",
|
||||||
|
),
|
||||||
|
Customer(
|
||||||
|
friend_id="u1_b",
|
||||||
|
channel="wechat",
|
||||||
|
user_id="U1",
|
||||||
|
add_time=2000,
|
||||||
|
last_chat_time=2000,
|
||||||
|
version="v1",
|
||||||
|
),
|
||||||
|
Customer(
|
||||||
|
friend_id="u2_a",
|
||||||
|
channel="wechat",
|
||||||
|
user_id="U2",
|
||||||
|
add_time=3000,
|
||||||
|
last_chat_time=3000,
|
||||||
|
version="v1",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
async def _override_session():
|
||||||
|
async with SessionLocal() as s:
|
||||||
|
yield s
|
||||||
|
|
||||||
|
app.dependency_overrides[deps.get_session] = _override_session
|
||||||
|
try:
|
||||||
|
async with AsyncClient(
|
||||||
|
transport=ASGITransport(app=app), base_url="http://test"
|
||||||
|
) as ac:
|
||||||
|
yield ac
|
||||||
|
finally:
|
||||||
|
app.dependency_overrides.clear()
|
||||||
|
await engine.dispose()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_filter_by_user_id(filter_client: AsyncClient):
|
||||||
|
"""``?user_id=U1`` -> only customers owned by U1."""
|
||||||
|
r = await filter_client.get("/api/customers", params={"user_id": "U1"})
|
||||||
|
assert r.status_code == 200
|
||||||
|
assert {c["friend_id"] for c in r.json()["items"]} == {"u1_a", "u1_b"}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_filter_by_add_time_since(filter_client: AsyncClient):
|
||||||
|
"""``?add_time_since=1500`` -> rows with add_time >= 1500 (u1_b, u2_a)."""
|
||||||
|
r = await filter_client.get("/api/customers", params={"add_time_since": 1500})
|
||||||
|
assert r.status_code == 200
|
||||||
|
ids = [c["friend_id"] for c in r.json()["items"]]
|
||||||
|
assert "u1_a" not in ids and "u1_b" in ids and "u2_a" in ids
|
||||||
|
|
||||||
|
|
||||||
|
async def test_filter_by_last_chat_since(filter_client: AsyncClient):
|
||||||
|
"""``?last_chat_since=1500`` -> rows with last_chat_time >= 1500 (u1_a out)."""
|
||||||
|
r = await filter_client.get("/api/customers", params={"last_chat_since": 1500})
|
||||||
|
assert r.status_code == 200
|
||||||
|
assert "u1_a" not in [c["friend_id"] for c in r.json()["items"]]
|
||||||
|
|||||||
Reference in New Issue
Block a user