"""Fixture payloads for ``POST /pc/callLog/list`` (M4 phone-call scraper). These records mirror the real field names confirmed by Phase-0 + the data dictionary §4 (电话记录 phone_call): ``id, startCallTime(YYYY-MM-DD HH:MM:SS), callDuration("M'SS\"")/callSeconds(int秒), callStatus(0未接/2接通), incomingCall(0呼出/1呼入), userId/userIdName(员工), departmentId, callNumber, simPhone, simNumber, imei, recordFile(录音OSS URL, may be null), isUpload, downloadCount, aiAsr, qcStatus, planCustomerName, version``. Live ``totalCount`` is 6047 across ``pageCount=303`` pages. The envelope differs from the wechat ``data.data`` / ``data.total`` shape: the row list sits under ``data.data`` (same) but the declared row count is ``data.totalCount`` (NOT ``data.total``) and the page count is ``data.pageCount``:: {"message":"调用成功","code":"10000","success":true,"traceId":"...", "data":{"data":[...], "page":.., "pageCount":.., "totalCount":.., "phoneHidden":false, "hideRecordPlay":false}} The cassette ships four records across two pages so a test can drive the page-number paginator across the empty-page stop signal: * page 1 -> 2 of the 4 fixture records, ``totalCount=4`` * page 2 -> the other 2 records, ``totalCount=4`` * (page 3 -> empty ``data`` array, served inline by the test) Records deliberately exercise the documented mapping edges: * ``pc_2001`` — outgoing connected call (``incomingCall=0``; ``callStatus=2``; ``callSeconds=151``; non-null ``recordFile``; full SIM/IMEI metadata). * ``pc_2002`` — incoming no-answer call with NO recording (``incomingCall=1``; ``callStatus=0``; ``recordFile=null``). * ``pc_2003`` — incoming connected call with a recording + AI ASR + QC data. * ``pc_2004`` — outgoing connected call (exercises the other polarity pair). NOTE on ``incomingCall`` polarity: this API's ``incomingCall`` is ``1=incoming / 0=outgoing`` — the SAME polarity as ``PhoneCall.incoming_call`` (no inversion needed, unlike the voice-call channel's ``isSend``). """ # The module name mirrors the upstream endpoint name (``callLog``); the M4 spec # uses this naming family. # ruff: noqa: N999 from __future__ import annotations from typing import Any # --------------------------------------------------------------------------- # Capture-shaped phone-call records (4 total across 2 pages). # --------------------------------------------------------------------------- CALL_RECORDS: list[dict[str, Any]] = [ { # Outgoing connected call with a recording. "id": "pc_2001", "startCallTime": "2026-01-15 10:30:00", # -> ms epoch UTC "callSeconds": 151, # preferred over callDuration string "callDuration": "2'31\"", # legacy "M'SS\"" string; scraper prefers callSeconds "callStatus": 2, # 2=接通 (connected) "incomingCall": 0, # 0=呼出 (outgoing) -> incoming_call=0 "userId": "staff_user_001", "userIdName": "Alice Wang", "departmentId": "dept_sales_east", "callNumber": "8613800000001", "simPhone": "8613800000111", "simNumber": "8986000112345678901", "imei": "356938035643801", "recordFile": ( "https://yunke-pcfile.oss-cn-beijing.aliyuncs.com/phone-call/" "8m0atd/imei001/1700000000000_1700000151000.mp3" ), "isUpload": 1, "downloadCount": 3, "aiAsr": "您好,请问您方便了解一下我们的产品吗?", "qcStatus": 1, "planCustomerName": "VIP Customer A", "version": "v1.20260731.pc2001", }, { # Incoming no-answer call, no recording. "id": "pc_2002", "startCallTime": "2026-01-15 11:00:00", "callSeconds": 0, "callDuration": "0'0\"", "callStatus": 0, # 0=未接 (no answer) "incomingCall": 1, # 1=呼入 (incoming) -> incoming_call=1 "userId": "staff_user_001", "userIdName": "Alice Wang", "departmentId": "dept_sales_east", "callNumber": "8613800000002", "simPhone": "8613800000111", "simNumber": "8986000112345678901", "imei": "356938035643801", "recordFile": None, # no recording -> record_file None "isUpload": 0, "downloadCount": 0, "aiAsr": None, "qcStatus": 0, "planCustomerName": "Lead B", "version": "v1.20260731.pc2002", }, { # Incoming connected call with recording + AI ASR text. "id": "pc_2003", "startCallTime": "2026-01-15 12:00:00", "callSeconds": 45, "callDuration": "0'45\"", "callStatus": 2, # connected "incomingCall": 1, # incoming -> incoming_call=1 "userId": "staff_user_002", "userIdName": "Bob Chen", "departmentId": "dept_sales_west", "callNumber": "8613800000003", "simPhone": "8613800000222", "simNumber": "8986000223456789012", "imei": "356938035643802", "recordFile": ( "https://yunke-pcfile.oss-cn-beijing.aliyuncs.com/phone-call/" "8m0atd/imei002/1700000003000_1700000045000.mp3" ), "isUpload": 1, "downloadCount": 1, "aiAsr": "好的,我考虑一下。", "qcStatus": 2, "planCustomerName": "Customer C", "version": "v1.20260731.pc2003", }, { # Outgoing connected call (the other polarity pair). "id": "pc_2004", "startCallTime": "2026-01-16 09:00:00", "callSeconds": 207, "callDuration": "3'27\"", "callStatus": 2, # connected "incomingCall": 0, # outgoing -> incoming_call=0 "userId": "staff_user_001", "userIdName": "Alice Wang", "departmentId": "dept_sales_east", "callNumber": "8613800000004", "simPhone": "8613800000111", "simNumber": "8986000112345678901", "imei": "356938035643801", "recordFile": ( "https://yunke-pcfile.oss-cn-beijing.aliyuncs.com/phone-call/" "8m0atd/imei001/1700000005000_1700000212000.mp3" ), "isUpload": 1, "downloadCount": 2, "aiAsr": None, "qcStatus": 0, "planCustomerName": "Customer D", "version": "v1.20260731.pc2004", }, ] # Default page size for the fixture (and the scraper). totalCount=4 means the # fixture fills exactly 2 pages of size 2 plus a terminating empty page. TOTAL_COUNT = len(CALL_RECORDS) DEFAULT_PAGE_SIZE = 2 # Mirrors the live capture's declared page count (purely cosmetic for the # fixture; the paginator relies on the empty-page stop signal, not pageCount). PAGE_COUNT = 2 def make_call_log_response( records: list[dict[str, Any]], *, total_count: int, page_num: int = 1, page_size: int = DEFAULT_PAGE_SIZE, page_count: int | None = None, trace_id: str | None = None, ) -> dict[str, Any]: """Build a capture-shaped ``callLog/list`` response envelope. Shape (row list under ``data.data``; row count on ``data.totalCount``; page count on ``data.pageCount``):: {"message":"调用成功","code":"10000","success":true,"traceId":"...", "data":{"data":[...], "page":.., "pageCount":.., "totalCount":.., "phoneHidden":false, "hideRecordPlay":false}} """ return { "message": "调用成功", "code": "10000", "success": True, "traceId": trace_id or f"trace-{page_num}", "data": { "data": list(records), "page": page_num, "pageCount": page_count if page_count is not None else page_count_for(page_size, total_count), "totalCount": total_count, "phoneHidden": False, "hideRecordPlay": False, }, } def page_count_for(page_size: int, total_count: int) -> int: """Compute the declared page count for a fixture (ceil(total/size)). Helper so the envelope's ``pageCount`` is internally consistent with the declared ``totalCount`` + page size without forcing each test to recompute it. The paginator does NOT consume ``pageCount`` (it relies on the empty-page stop signal), so this is cosmetic — but it keeps the fixture capture-shaped. """ if page_size <= 0: return 0 return (total_count + page_size - 1) // page_size