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.
|
||||
|
||||
Read-only query over the mirrored ``customer`` table. Supports filtering by
|
||||
``channel``, ``owner_account_id`` (direct columns) and ``department_id``
|
||||
(resolved through ``customer.user_id`` -> ``staff.dept_id``). Results are
|
||||
ordered by ``last_chat_time desc`` (nulls last) then ``friend_id`` so the
|
||||
``channel``, ``owner_account_id``, ``user_id``, ``add_time_since`` and
|
||||
``last_chat_since`` (direct columns) and ``department_id`` (resolved through
|
||||
``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.
|
||||
"""
|
||||
|
||||
@@ -34,6 +35,9 @@ async def list_customers(
|
||||
channel: Annotated[str | None, Query()] = None,
|
||||
department_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]:
|
||||
"""List customers with optional filters and pagination."""
|
||||
conditions = []
|
||||
@@ -47,6 +51,12 @@ async def list_customers(
|
||||
Staff.dept_id == department_id
|
||||
)
|
||||
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)
|
||||
if conditions:
|
||||
|
||||
@@ -255,3 +255,90 @@ async def test_filters_compose_channel_and_owner(client: AsyncClient):
|
||||
body = r.json()
|
||||
assert body["total"] == 2
|
||||
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