test_service_fulfillment.py 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687
  1. from __future__ import annotations
  2. import asyncio
  3. from datetime import timedelta
  4. from types import SimpleNamespace
  5. import pytest
  6. from aiohttp import CookieJar
  7. from aiohttp.test_utils import TestClient, TestServer
  8. def _user(user_id: int, *, username: str | None = None, name: str = "测试用户"):
  9. return SimpleNamespace(
  10. id=user_id,
  11. username=username,
  12. first_name=name,
  13. last_name=None,
  14. is_bot=False,
  15. )
  16. def _package(*, name: str = "基础护理", category: str = "护理"):
  17. return {
  18. "name": name,
  19. "category": category,
  20. "description": "标准线下服务套餐",
  21. "tags": ["线下", "护理"],
  22. "service_modes": ["at_store", "onsite"],
  23. "price_mode": "fixed",
  24. "currency": "CNY",
  25. "price_unit": "per_service",
  26. "min_price": "100",
  27. "max_price": "100",
  28. "duration_minutes": 60,
  29. "included_items": "基础服务",
  30. "excluded_items": "额外耗材",
  31. "addons": [
  32. {
  33. "name": "延长服务",
  34. "unit": "次",
  35. "price": "20",
  36. "extra_minutes": 15,
  37. }
  38. ],
  39. "travel_fee": {"mode": "quoted"},
  40. "service_radius_km": 20,
  41. "out_of_range_policy": "超区请重新询价",
  42. "preparation": "请提前沟通需求",
  43. }
  44. async def _technician(app_modules, service_db, *, user_id: int = 200, category: str = "护理"):
  45. directory_db = app_modules.load("wbb.utils.dbdirectory")
  46. await directory_db.upsert_directory_identity(
  47. user_id=user_id,
  48. username=f"technician{user_id}",
  49. first_name="认证",
  50. last_name="技师",
  51. )
  52. await directory_db.save_directory_location(
  53. user_id=user_id,
  54. longitude=116.4074,
  55. latitude=39.9042,
  56. source="test",
  57. actor_id=user_id,
  58. actor_name="认证技师",
  59. )
  60. await directory_db.upsert_directory_membership(
  61. bot_id="primary",
  62. chat_id=-100200,
  63. user_id=user_id,
  64. status="member",
  65. active=True,
  66. verified=True,
  67. )
  68. await directory_db.submit_teacher_application(
  69. user_id=user_id,
  70. source="test",
  71. bot_id="primary",
  72. chat_id=-100200,
  73. )
  74. await directory_db.decide_teacher_application(
  75. user_id=user_id,
  76. action="approve",
  77. actor_id="admin",
  78. actor_name="admin",
  79. source="test",
  80. )
  81. await directory_db.set_teacher_state(
  82. user_id=user_id,
  83. action="list",
  84. actor_id=user_id,
  85. actor_name="认证技师",
  86. source="test",
  87. )
  88. profile = await service_db.publish_technician_profile(
  89. user_id,
  90. {
  91. "headline": "专注线下护理",
  92. "bio": "提供到店和上门服务",
  93. "tags": ["护理"],
  94. "contact_hours": "10:00-20:00",
  95. "public_area_text": "北京朝阳区附近",
  96. "venue": {"name": "护理工作室", "address_hint": "测试路 100 号"},
  97. "onsite_policy": {"description": "20 公里内可上门"},
  98. "packages": [_package(category=category)],
  99. },
  100. actor_id=user_id,
  101. )
  102. return profile
  103. async def _accepted_customer(service_db, *, user_id: int = 300):
  104. customer = _user(user_id, username=None, name=f"顾客{user_id}")
  105. await service_db.observe_customer(customer, accepted_terms=True)
  106. return customer
  107. async def _order(service_db, customer, profile, *, service_mode: str = "onsite"):
  108. package = profile["service_profile"]["packages"][0]
  109. kwargs = {
  110. "customer": customer,
  111. "technician_id": profile["user_id"],
  112. "package_id": package["package_id"],
  113. "service_mode": service_mode,
  114. "requirements": "希望完成一次标准护理服务",
  115. }
  116. if service_mode == "onsite":
  117. kwargs.update(
  118. {
  119. "longitude": 116.4174,
  120. "latitude": 39.9042,
  121. "address_text": "测试街道 1 号 101 室",
  122. }
  123. )
  124. else:
  125. kwargs.update({"longitude": 116.4174, "latitude": 39.9042})
  126. return await service_db.create_service_order(**kwargs)
  127. async def test_public_customer_order_quote_address_and_completion_qr(app_modules):
  128. service_db = app_modules.load("wbb.utils.dbservice")
  129. profile = await _technician(app_modules, service_db)
  130. customer = await _accepted_customer(service_db)
  131. order = await _order(service_db, customer, profile)
  132. assert order["status"] == "requested"
  133. assert order["distance_band"] == "1 公里内"
  134. stored_address = await app_modules.wbb.control_db.service_order_addresses.find_one(
  135. {"order_id": order["order_id"]}
  136. )
  137. assert stored_address["redact_after"] is None
  138. assert "测试街道" not in stored_address["encrypted_payload"]
  139. hidden = await service_db.get_order_for_actor(
  140. order["order_id"], customer.id, reveal_address=True
  141. )
  142. assert "exact_address" not in hidden
  143. quoted_order, quote = await service_db.submit_service_quote(
  144. order["order_id"],
  145. profile["user_id"],
  146. {
  147. "base_amount": "100",
  148. "travel_fee": "10",
  149. "addons": [{"name": "延长服务", "quantity": 2, "unit_price": "20"}],
  150. "discount": "5",
  151. "currency": "CNY",
  152. "total_amount": "145",
  153. "note": "最终报价",
  154. },
  155. )
  156. assert quoted_order["status"] == "quoted"
  157. assert quote["total_amount"] == "145.00"
  158. confirmed = await service_db.confirm_service_quote(order["order_id"], customer.id)
  159. assert confirmed["status"] == "confirmed"
  160. revealed = await service_db.get_order_for_actor(
  161. order["order_id"], profile["user_id"], reveal_address=True
  162. )
  163. assert revealed["exact_address"]["address_text"] == "测试街道 1 号 101 室"
  164. started = await service_db.start_service_order(order["order_id"], profile["user_id"])
  165. assert started["status"] == "in_progress"
  166. qr, token = await service_db.issue_review_qr(order["order_id"], profile["user_id"])
  167. assert qr["status"] == "issued"
  168. assert len(token) >= 22
  169. assert token not in str(qr)
  170. with pytest.raises(service_db.ServiceDataError, match="不属于当前顾客"):
  171. await service_db.preview_review_qr(token, customer.id + 1)
  172. claim_results = await asyncio.gather(
  173. service_db.claim_review_qr(token, customer.id),
  174. service_db.claim_review_qr(token, customer.id),
  175. return_exceptions=True,
  176. )
  177. successful_claims = [item for item in claim_results if isinstance(item, tuple)]
  178. failed_claims = [item for item in claim_results if isinstance(item, Exception)]
  179. assert len(successful_claims) == 1
  180. assert len(failed_claims) == 1
  181. claimed, completed = successful_claims[0]
  182. assert claimed["status"] == "claimed"
  183. assert completed["status"] == "completed"
  184. assert "已使用" in str(failed_claims[0])
  185. resumable = await service_db.preview_review_qr(token, customer.id)
  186. assert resumable["status"] == "claimed"
  187. verified_review = await service_db.submit_review(
  188. customer=customer,
  189. technician_id=profile["user_id"],
  190. source="qr_verified",
  191. answers={
  192. "overall_experience": "excellent",
  193. "service_highlights": ["professional", "communication"],
  194. "comment": "已完成服务单评价",
  195. },
  196. anonymous=False,
  197. qr_id=claimed["qr_id"],
  198. )
  199. assert verified_review["source"] == "qr_verified"
  200. assert verified_review["order_id"] == order["order_id"]
  201. with pytest.raises(service_db.ServiceDataError, match="无效"):
  202. await service_db.preview_review_qr(token, customer.id)
  203. with pytest.raises(service_db.ServiceDataError, match="已经提交评价"):
  204. await service_db.submit_review(
  205. customer=customer,
  206. technician_id=profile["user_id"],
  207. source="qr_verified",
  208. answers={
  209. "overall_experience": "excellent",
  210. "service_highlights": ["professional"],
  211. },
  212. anonymous=True,
  213. qr_id=claimed["qr_id"],
  214. )
  215. address_after = await app_modules.wbb.control_db.service_order_addresses.find_one(
  216. {"order_id": order["order_id"]}
  217. )
  218. assert address_after["redact_after"] - completed["closed_at"] == timedelta(days=7)
  219. await app_modules.wbb.control_db.service_order_addresses.update_one(
  220. {"order_id": order["order_id"]},
  221. {"$set": {"redact_after": service_db.utc_now() - timedelta(seconds=1)}},
  222. )
  223. assert await service_db.redact_expired_addresses() == 1
  224. redacted_order = await app_modules.wbb.control_db.service_orders.find_one(
  225. {"order_id": order["order_id"]}
  226. )
  227. redacted_address = await app_modules.wbb.control_db.service_order_addresses.find_one(
  228. {"order_id": order["order_id"]}
  229. )
  230. assert redacted_order["address_redacted"] is True
  231. assert "distance_meters" not in redacted_order
  232. assert "encrypted_payload" not in redacted_address
  233. async def test_state_machine_cancellation_and_template_snapshot(app_modules):
  234. service_db = app_modules.load("wbb.utils.dbservice")
  235. profile = await _technician(app_modules, service_db)
  236. customer = await _accepted_customer(service_db)
  237. template = await service_db.create_package_template(
  238. {"template_name": "护理模板", "package": _package(name="模板初版")},
  239. actor_id="admin",
  240. )
  241. selected = await service_db.instantiate_package_template(template["template_id"])
  242. assert selected["source_template_version"] == 1
  243. await service_db.update_package_template(
  244. template["template_id"],
  245. {"template_name": "护理模板", "package": _package(name="模板新版")},
  246. actor_id="admin",
  247. )
  248. assert selected["name"] == "模板初版"
  249. order = await _order(service_db, customer, profile, service_mode="at_store")
  250. assert order["distance_meters"] > 0
  251. assert order["distance_band"] == "1 公里内"
  252. with pytest.raises(service_db.ServiceDataError, match="只有已确认"):
  253. await service_db.start_service_order(order["order_id"], profile["user_id"])
  254. canceled = await service_db.cancel_service_order(
  255. order["order_id"], customer.id, "顾客计划调整"
  256. )
  257. assert canceled["status"] == "canceled_customer"
  258. with pytest.raises(service_db.ServiceDataError, match="不能取消"):
  259. await service_db.cancel_service_order(
  260. order["order_id"], profile["user_id"], "重复取消"
  261. )
  262. pending_order = await _order(service_db, customer, profile)
  263. await service_db.submit_service_quote(
  264. pending_order["order_id"],
  265. profile["user_id"],
  266. {"base_amount": "100", "travel_fee": "0", "discount": "0"},
  267. )
  268. await service_db.confirm_service_quote(pending_order["order_id"], customer.id)
  269. await service_db.start_service_order(pending_order["order_id"], profile["user_id"])
  270. _, pending_token = await service_db.issue_review_qr(
  271. pending_order["order_id"], profile["user_id"]
  272. )
  273. canceled_pending = await service_db.cancel_service_order(
  274. pending_order["order_id"], profile["user_id"], "服务现场无法继续"
  275. )
  276. assert canceled_pending["status"] == "canceled_technician"
  277. with pytest.raises(service_db.ServiceDataError, match="无效"):
  278. await service_db.preview_review_qr(pending_token, customer.id)
  279. disputed_order = await _order(service_db, customer, profile)
  280. disputed = await service_db.dispute_service_order(
  281. disputed_order["order_id"], customer.id, "需要管理员协调"
  282. )
  283. assert disputed["status"] == "disputed"
  284. resumed = await service_db.resolve_service_dispute(
  285. disputed_order["order_id"],
  286. actor_id="admin",
  287. action="resume",
  288. reason="双方已达成一致",
  289. )
  290. assert resumed["status"] == "requested"
  291. async def test_order_requires_encryption_before_persisting(app_modules):
  292. service_db = app_modules.load("wbb.utils.dbservice")
  293. profile = await _technician(app_modules, service_db)
  294. customer = await _accepted_customer(service_db)
  295. original_key = app_modules.wbb.SERVICE_ADDRESS_ENCRYPTION_KEY
  296. app_modules.wbb.SERVICE_ADDRESS_ENCRYPTION_KEY = ""
  297. try:
  298. with pytest.raises(service_db.ServiceDataError, match="加密密钥"):
  299. await _order(service_db, customer, profile)
  300. finally:
  301. app_modules.wbb.SERVICE_ADDRESS_ENCRYPTION_KEY = original_key
  302. assert (
  303. await app_modules.wbb.control_db.service_orders.count_documents(
  304. {"customer_id": customer.id}
  305. )
  306. == 0
  307. )
  308. async def test_expired_review_qr_reopens_order_for_reissue(app_modules):
  309. service_db = app_modules.load("wbb.utils.dbservice")
  310. profile = await _technician(app_modules, service_db)
  311. customer = await _accepted_customer(service_db)
  312. order = await _order(service_db, customer, profile)
  313. await service_db.submit_service_quote(
  314. order["order_id"],
  315. profile["user_id"],
  316. {"base_amount": "100", "travel_fee": "0", "discount": "0"},
  317. )
  318. await service_db.confirm_service_quote(order["order_id"], customer.id)
  319. await service_db.start_service_order(order["order_id"], profile["user_id"])
  320. first_qr, first_token = await service_db.issue_review_qr(
  321. order["order_id"], profile["user_id"]
  322. )
  323. await app_modules.wbb.control_db.teacher_review_qr_records.update_one(
  324. {"qr_id": first_qr["qr_id"]},
  325. {"$set": {"expires_at": service_db.utc_now() - timedelta(seconds=1)}},
  326. )
  327. with pytest.raises(service_db.ServiceDataError, match="已过期"):
  328. await service_db.preview_review_qr(first_token, customer.id)
  329. reopened = await app_modules.wbb.control_db.service_orders.find_one(
  330. {"order_id": order["order_id"]}
  331. )
  332. assert reopened["status"] == "in_progress"
  333. assert "current_review_qr_id" not in reopened
  334. replacement, replacement_token = await service_db.issue_review_qr(
  335. order["order_id"], profile["user_id"]
  336. )
  337. assert replacement["qr_id"] != first_qr["qr_id"]
  338. assert replacement_token != first_token
  339. async def test_reviews_same_weight_anonymous_and_bayesian_leaderboard(app_modules):
  340. service_db = app_modules.load("wbb.utils.dbservice")
  341. profile = await _technician(app_modules, service_db)
  342. customer = await _accepted_customer(service_db)
  343. answers = {
  344. "overall_experience": "excellent",
  345. "service_highlights": ["professional", "communication"],
  346. "comment": "服务很专业",
  347. }
  348. template = await service_db.get_active_review_template()
  349. draft = await service_db.begin_review_draft(
  350. customer_id=customer.id,
  351. technician_id=profile["user_id"],
  352. source="student_initiated",
  353. template=template,
  354. )
  355. await service_db.save_review_draft_progress(
  356. customer.id,
  357. answers={"overall_experience": "excellent"},
  358. question_index=1,
  359. )
  360. resumed_draft = await service_db.begin_review_draft(
  361. customer_id=customer.id,
  362. technician_id=profile["user_id"],
  363. source="student_initiated",
  364. template=template,
  365. )
  366. assert resumed_draft["draft_id"] == draft["draft_id"]
  367. assert resumed_draft["answers"] == {"overall_experience": "excellent"}
  368. await service_db.delete_review_draft(customer.id)
  369. review_ids = []
  370. for _ in range(1):
  371. review = await service_db.submit_review(
  372. customer=customer,
  373. technician_id=profile["user_id"],
  374. source="student_initiated",
  375. answers=answers,
  376. anonymous=True,
  377. )
  378. review_ids.append(review["review_id"])
  379. assert "qr_id" not in review
  380. assert review["package_snapshot"]["name"] == "技师服务"
  381. assert review["category"] == "护理"
  382. assert len(set(review_ids)) == 1
  383. for review_id in review_ids:
  384. await service_db.moderate_review(
  385. review_id, "approve", actor_id="admin"
  386. )
  387. public, total = await service_db.list_public_reviews(profile["user_id"])
  388. assert total == 1
  389. assert all(item["customer_name"] == "匿名顾客" for item in public)
  390. assert all(item["source_label"] == "用户主动评价" for item in public)
  391. summary = await service_db.get_technician_review_summary(profile["user_id"])
  392. assert summary["review_count"] == 1
  393. pending, _ = await service_db.list_reviews(
  394. status="approved", source="student_initiated"
  395. )
  396. assert all(item["similar_content_count"] == 1 for item in pending)
  397. leaderboard, total = await service_db.list_leaderboard(category="护理")
  398. assert total == 1
  399. assert leaderboard[0]["review_count"] == 1
  400. assert leaderboard[0]["eligible"] is True
  401. assert leaderboard[0]["rank_score"] == pytest.approx(
  402. leaderboard[0]["average_score"]
  403. )
  404. async def test_direct_review_skips_package_selection(app_modules, monkeypatch):
  405. service_db = app_modules.load("wbb.utils.dbservice")
  406. service_module = app_modules.load("wbb.modules.technician_services")
  407. profile = await _technician(app_modules, service_db)
  408. customer = await _accepted_customer(service_db)
  409. captured = {}
  410. async def fake_begin_review(target, **values):
  411. captured.update(values)
  412. captured["customer_id"] = target.from_user.id
  413. monkeypatch.setattr(service_module, "_begin_review", fake_begin_review)
  414. await service_module._choose_review_package(
  415. SimpleNamespace(from_user=customer),
  416. profile["user_id"],
  417. )
  418. assert captured == {
  419. "customer_id": customer.id,
  420. "technician_id": profile["user_id"],
  421. "source": "student_initiated",
  422. }
  423. async def test_review_moderation_only_allows_defined_state_transitions(app_modules):
  424. service_db = app_modules.load("wbb.utils.dbservice")
  425. profile = await _technician(app_modules, service_db)
  426. customer = await _accepted_customer(service_db)
  427. package_id = profile["service_profile"]["packages"][0]["package_id"]
  428. review = await service_db.submit_review(
  429. customer=customer,
  430. technician_id=profile["user_id"],
  431. source="student_initiated",
  432. answers={
  433. "overall_experience": "excellent",
  434. "service_highlights": ["professional"],
  435. },
  436. anonymous=True,
  437. package_id=package_id,
  438. )
  439. approved = await service_db.moderate_review(
  440. review["review_id"], "approve", actor_id="admin"
  441. )
  442. assert approved["status"] == "approved"
  443. with pytest.raises(service_db.ServiceDataError, match="不能执行"):
  444. await service_db.moderate_review(
  445. review["review_id"], "reject", actor_id="admin", reason="重复处理"
  446. )
  447. voided = await service_db.moderate_review(
  448. review["review_id"], "void", actor_id="admin", reason="争议作废"
  449. )
  450. assert voided["status"] == "voided"
  451. with pytest.raises(service_db.ServiceDataError, match="不能执行"):
  452. await service_db.moderate_review(
  453. review["review_id"], "approve", actor_id="admin"
  454. )
  455. async def test_first_approved_review_creates_topic_and_is_published_once(
  456. app_modules,
  457. monkeypatch,
  458. ):
  459. service_db = app_modules.load("wbb.utils.dbservice")
  460. review_service = app_modules.load("wbb.services.technician_reviews")
  461. profile = await _technician(app_modules, service_db)
  462. customer = await _accepted_customer(service_db)
  463. package_id = profile["service_profile"]["packages"][0]["package_id"]
  464. await service_db.set_service_settings(
  465. {
  466. "review_forum_chat_id": "-1001234567890",
  467. "review_forum_username": "public_reviews",
  468. }
  469. )
  470. calls = []
  471. async def fake_bot_api(_token, method, payload):
  472. calls.append((method, payload))
  473. if method == "createForumTopic":
  474. return {"message_thread_id": 321}
  475. return {"message_id": 900 + len(calls)}
  476. monkeypatch.setattr(review_service, "_bot_api", fake_bot_api)
  477. review = await service_db.submit_review(
  478. customer=customer,
  479. technician_id=profile["user_id"],
  480. source="student_initiated",
  481. answers={
  482. "overall_experience": "excellent",
  483. "service_highlights": ["professional"],
  484. "comment": "服务很专业",
  485. },
  486. anonymous=True,
  487. package_id=package_id,
  488. )
  489. approved = await service_db.moderate_review(
  490. review["review_id"],
  491. "approve",
  492. actor_id="admin",
  493. )
  494. published = await review_service.publish_approved_review(
  495. "123456:test-token",
  496. approved,
  497. )
  498. assert published["status"] == "published"
  499. assert published["topic_url"] == "https://t.me/public_reviews/321"
  500. assert [method for method, _ in calls] == [
  501. "createForumTopic",
  502. "sendMessage",
  503. "sendMessage",
  504. ]
  505. repeated = await review_service.publish_approved_review(
  506. "123456:test-token",
  507. approved,
  508. )
  509. assert repeated["status"] == "published"
  510. assert len(calls) == 3
  511. async def test_technician_requires_active_managed_group_membership(app_modules):
  512. service_db = app_modules.load("wbb.utils.dbservice")
  513. profile = await _technician(app_modules, service_db)
  514. await app_modules.wbb.control_db.directory_memberships.update_many(
  515. {"user_id": profile["user_id"]}, {"$set": {"active": False}}
  516. )
  517. customer = await _accepted_customer(service_db)
  518. with pytest.raises(service_db.ServiceDataError, match="当前不满足接单资格"):
  519. await _order(service_db, customer, profile)
  520. async def test_technician_uses_public_area_without_precise_location(app_modules):
  521. service_db = app_modules.load("wbb.utils.dbservice")
  522. profile = await _technician(app_modules, service_db)
  523. await app_modules.wbb.control_db.directory_locations.delete_one(
  524. {"user_id": profile["user_id"]}
  525. )
  526. republished = await service_db.publish_technician_profile(
  527. profile["user_id"],
  528. profile["service_profile"],
  529. actor_id=profile["user_id"],
  530. )
  531. assert republished["service_profile"]["public_area_text"] == "北京朝阳区附近"
  532. technicians, total = await service_db.list_available_technicians()
  533. assert total == 1
  534. assert technicians[0]["public_area_text"] == "北京朝阳区附近"
  535. assert technicians[0]["distance_band"] == ""
  536. async def test_service_admin_api_settings_templates_and_permissions(app_modules):
  537. admin_api = app_modules.load("wbb.admin.api")
  538. app_modules.wbb.BOT_PERMISSIONS = {"teacher_directory.manage"}
  539. application = admin_api.build_admin_application()
  540. await application["admin_api"].initialize()
  541. client = TestClient(TestServer(application), cookie_jar=CookieJar(unsafe=True))
  542. await client.start_server()
  543. try:
  544. login = await client.post(
  545. "/api/admin/v1/auth/login",
  546. json={"username": "admin", "password": "qwe0.123456"},
  547. )
  548. login_data = (await login.json())["data"]
  549. changed = await client.put(
  550. "/api/admin/v1/auth/password",
  551. headers={"X-CSRF-Token": login_data["csrf_token"]},
  552. json={
  553. "current_password": "qwe0.123456",
  554. "new_password": "changed-pass-123",
  555. },
  556. )
  557. headers = {"X-CSRF-Token": (await changed.json())["data"]["csrf_token"]}
  558. unconfirmed = await client.put(
  559. "/api/admin/v1/directory/service-settings",
  560. headers=headers,
  561. json={"customer_max_open_orders": 4},
  562. )
  563. assert unconfirmed.status == 409
  564. saved = await client.put(
  565. "/api/admin/v1/directory/service-settings",
  566. headers=headers,
  567. json={
  568. "customer_max_open_orders": 4,
  569. "customer_daily_request_limit": 12,
  570. "review_qr_expiry_hours": 48,
  571. "confirm": True,
  572. },
  573. )
  574. assert saved.status == 200
  575. assert (await saved.json())["data"]["review_qr_expiry_hours"] == 48
  576. created = await client.post(
  577. "/api/admin/v1/directory/package-templates",
  578. headers=headers,
  579. json={
  580. "template_name": "后台护理模板",
  581. "package": _package(),
  582. "confirm": True,
  583. },
  584. )
  585. assert created.status == 201
  586. created_data = (await created.json())["data"]
  587. assert created_data["version"] == 1
  588. duplicated = await client.post(
  589. f"/api/admin/v1/directory/package-templates/{created_data['template_id']}/actions",
  590. headers=headers,
  591. json={"action": "duplicate", "confirm": True},
  592. )
  593. assert duplicated.status == 200
  594. assert (await duplicated.json())["data"]["status"] == "disabled"
  595. review_template = await client.get(
  596. "/api/admin/v1/directory/review-template"
  597. )
  598. assert review_template.status == 200
  599. template_data = (await review_template.json())["data"]
  600. assert {item["type"] for item in template_data["questions"]} == {
  601. "single_choice",
  602. "multiple_choice",
  603. "text",
  604. }
  605. assert sum(
  606. item.get("weight", 0)
  607. for item in template_data["questions"]
  608. if item["type"] == "single_choice"
  609. ) == 100
  610. metrics = await client.get(
  611. "/api/admin/v1/directory/fulfillment-metrics"
  612. )
  613. assert metrics.status == 200
  614. assert (await metrics.json())["data"]["scope_note"].startswith(
  615. "仅统计平台服务单"
  616. )
  617. app_modules.wbb.BOT_PERMISSIONS = set()
  618. denied = await client.get(
  619. "/api/admin/v1/directory/fulfillment-metrics"
  620. )
  621. assert denied.status == 403
  622. finally:
  623. await client.close()