test_enrollment_flow.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366
  1. from datetime import UTC, datetime
  2. from fastapi.testclient import TestClient
  3. from zbt.commands.seed import build_seed_manifest
  4. from zbt.core.config import Settings
  5. from zbt.core.passwords import PasswordService
  6. from zbt.domains.catalog.repository import InMemoryCatalogRepository
  7. from zbt.domains.enrollment.repository import InMemoryEnrollmentRepository
  8. from zbt.domains.identity.repository import InMemoryIdentityRepository
  9. from zbt.main import create_app
  10. def build_test_app():
  11. catalog = InMemoryCatalogRepository()
  12. identities = InMemoryIdentityRepository()
  13. manifest = build_seed_manifest(PasswordService().hash("zaq1XSW@"))
  14. for admin in manifest.admin_users:
  15. identities.save_admin_user(admin)
  16. for product in manifest.products:
  17. catalog.save_product(product)
  18. for version in manifest.versions:
  19. catalog.save_version(version)
  20. return create_app(
  21. settings=Settings(
  22. app_env="test",
  23. jwt_access_secret="a" * 32,
  24. jwt_refresh_secret="b" * 32,
  25. field_encryption_key="c" * 32,
  26. ),
  27. identity_repository=identities,
  28. catalog_repository=catalog,
  29. enrollment_repository=InMemoryEnrollmentRepository(),
  30. clock=lambda: datetime(2026, 7, 26, tzinfo=UTC),
  31. )
  32. def login_headers(client: TestClient) -> dict[str, str]:
  33. token = client.post(
  34. "/api/v1/h5/auth/login",
  35. json={"mobile": "18800000001", "code": "147258"},
  36. ).json()["data"]["tokens"]["access_token"]
  37. return {"Authorization": f"Bearer {token}"}
  38. def test_eligible_customer_receives_deterministic_quote() -> None:
  39. with TestClient(build_test_app()) as client:
  40. headers = login_headers(client)
  41. product = next(
  42. item
  43. for item in client.get("/api/v1/h5/products", headers=headers).json()["data"]["items"]
  44. if item["product_code"] == "MED-SENIOR"
  45. )
  46. response = client.post(
  47. "/api/v1/h5/quotes",
  48. headers=headers,
  49. json={
  50. "product_id": product["product_id"],
  51. "plan_id": product["plans"][0]["id"],
  52. "insured": {
  53. "age": 65,
  54. "region_code": "510100",
  55. "occupation_code": "GENERAL",
  56. },
  57. "relationship": "PARENT",
  58. },
  59. )
  60. assert response.status_code == 201
  61. assert response.json()["data"]["eligible"] is True
  62. assert response.json()["data"]["premium_cents"] == 19900
  63. assert response.json()["data"]["rate_version"] == "rate-v1"
  64. def test_confirmed_draft_creates_one_idempotent_order() -> None:
  65. with TestClient(build_test_app()) as client:
  66. headers = login_headers(client)
  67. product = next(
  68. item
  69. for item in client.get("/api/v1/h5/products", headers=headers).json()["data"]["items"]
  70. if item["product_code"] == "MED-SENIOR"
  71. )
  72. quote_id = client.post(
  73. "/api/v1/h5/quotes",
  74. headers=headers,
  75. json={
  76. "product_id": product["product_id"],
  77. "plan_id": product["plans"][0]["id"],
  78. "insured": {
  79. "age": 65,
  80. "region_code": "510100",
  81. "occupation_code": "GENERAL",
  82. },
  83. "relationship": "PARENT",
  84. },
  85. ).json()["data"]["quote_id"]
  86. draft = client.post(
  87. "/api/v1/h5/enrollment-drafts",
  88. headers=headers,
  89. json={
  90. "quote_id": quote_id,
  91. "applicant": {"name": "张三", "id_no": "510100199001010001"},
  92. "insured": {"name": "张父", "id_no": "510100196101010001"},
  93. "contact": {"mobile": "18800000001"},
  94. },
  95. )
  96. confirmation = client.post(
  97. f"/api/v1/h5/enrollment-drafts/{draft.json()['data']['draft_id']}/confirmation",
  98. headers=headers,
  99. )
  100. order_headers = {
  101. **headers,
  102. "Idempotency-Key": "order-client-001",
  103. }
  104. first = client.post(
  105. "/api/v1/h5/orders",
  106. headers=order_headers,
  107. json={
  108. "draft_id": draft.json()["data"]["draft_id"],
  109. "confirmation_token": confirmation.json()["data"]["confirmation_token"],
  110. },
  111. )
  112. repeated = client.post(
  113. "/api/v1/h5/orders",
  114. headers=order_headers,
  115. json={
  116. "draft_id": draft.json()["data"]["draft_id"],
  117. "confirmation_token": confirmation.json()["data"]["confirmation_token"],
  118. },
  119. )
  120. assert draft.status_code == 201
  121. assert confirmation.status_code == 201
  122. assert first.status_code == 201
  123. assert first.json()["data"]["status"] == "PENDING_PAYMENT"
  124. assert first.json()["data"]["amount_cents"] == 19900
  125. assert repeated.status_code == 200
  126. assert repeated.json()["data"]["order_id"] == first.json()["data"]["order_id"]
  127. def test_mock_payment_callback_is_idempotent_and_issues_one_policy() -> None:
  128. with TestClient(build_test_app()) as client:
  129. headers = login_headers(client)
  130. product = next(
  131. item
  132. for item in client.get("/api/v1/h5/products", headers=headers).json()["data"]["items"]
  133. if item["product_code"] == "MED-SENIOR"
  134. )
  135. quote_id = client.post(
  136. "/api/v1/h5/quotes",
  137. headers=headers,
  138. json={
  139. "product_id": product["product_id"],
  140. "plan_id": product["plans"][0]["id"],
  141. "insured": {
  142. "age": 65,
  143. "region_code": "510100",
  144. "occupation_code": "GENERAL",
  145. },
  146. "relationship": "PARENT",
  147. },
  148. ).json()["data"]["quote_id"]
  149. draft_id = client.post(
  150. "/api/v1/h5/enrollment-drafts",
  151. headers=headers,
  152. json={
  153. "quote_id": quote_id,
  154. "applicant": {"name": "张三", "id_no": "510100199001010001"},
  155. "insured": {"name": "张父", "id_no": "510100196101010001"},
  156. "contact": {"mobile": "18800000001"},
  157. },
  158. ).json()["data"]["draft_id"]
  159. confirmation_token = client.post(
  160. f"/api/v1/h5/enrollment-drafts/{draft_id}/confirmation",
  161. headers=headers,
  162. ).json()["data"]["confirmation_token"]
  163. order = client.post(
  164. "/api/v1/h5/orders",
  165. headers={**headers, "Idempotency-Key": "order-client-002"},
  166. json={
  167. "draft_id": draft_id,
  168. "confirmation_token": confirmation_token,
  169. },
  170. ).json()["data"]
  171. payment = client.post(
  172. f"/api/v1/h5/orders/{order['order_id']}/payments",
  173. headers={**headers, "Idempotency-Key": "payment-client-001"},
  174. )
  175. invalid_callback = client.post(
  176. "/api/v1/callbacks/mock-payment",
  177. headers={"X-Mock-Pay-Signature": "invalid-signature"},
  178. json={
  179. "callback_no": "CB-INVALID-001",
  180. "provider_transaction_no": "MP-INVALID-001",
  181. "payment_no": payment.json()["data"]["payment_no"],
  182. "status": "SUCCEEDED",
  183. "amount_cents": 19900,
  184. "occurred_at": "2026-07-25T08:30:00.000Z",
  185. },
  186. )
  187. first_callback = client.post(
  188. f"/api/v1/dev/mock-payments/{payment.json()['data']['payment_id']}/complete",
  189. headers=headers,
  190. )
  191. repeated_callback = client.post(
  192. f"/api/v1/dev/mock-payments/{payment.json()['data']['payment_id']}/complete",
  193. headers=headers,
  194. )
  195. policies = client.get("/api/v1/h5/policies", headers=headers)
  196. admin_token = client.post(
  197. "/api/v1/admin/auth/login",
  198. json={"username": "admin", "password": "zaq1XSW@"},
  199. ).json()["data"]["tokens"]["access_token"]
  200. admin_dashboard = client.get(
  201. "/api/v1/admin/dashboard",
  202. headers={"Authorization": f"Bearer {admin_token}"},
  203. )
  204. admin_orders = client.get(
  205. "/api/v1/admin/orders",
  206. headers={"Authorization": f"Bearer {admin_token}"},
  207. )
  208. assert payment.status_code == 201
  209. assert invalid_callback.status_code == 401
  210. assert first_callback.status_code == 200
  211. assert first_callback.json()["data"]["payment_status"] == "SUCCEEDED"
  212. assert first_callback.json()["data"]["order_status"] == "ISSUED"
  213. assert first_callback.json()["data"]["policy"]["status"] == "ACTIVE"
  214. assert (
  215. repeated_callback.json()["data"]["policy"]["policy_id"]
  216. == (first_callback.json()["data"]["policy"]["policy_id"])
  217. )
  218. assert policies.json()["data"]["total"] == 1
  219. policy = policies.json()["data"]["items"][0]
  220. assert policy["product_name"] == "银龄守护医疗险"
  221. assert policy["plan_name"] == "银龄标准计划"
  222. assert policy["relationship"] == "PARENT"
  223. assert policy["order_no"] == order["order_no"]
  224. assert policy["applicant"]["name"] == "张三"
  225. assert policy["insured"]["name"] == "张父"
  226. assert admin_dashboard.status_code == 200
  227. assert admin_dashboard.json()["data"]["order_count"] == 1
  228. assert admin_dashboard.json()["data"]["policy_count"] == 1
  229. admin_order = admin_orders.json()["data"]["items"][0]
  230. assert admin_order["product_name"] == "银龄守护医疗险"
  231. assert admin_order["plan_name"] == "银龄标准计划"
  232. assert admin_order["relationship"] == "PARENT"
  233. assert admin_order["applicant"]["name"] == "张三"
  234. assert admin_order["contact"]["mobile"] == "18800000001"
  235. assert admin_order["payment"]["status"] == "SUCCEEDED"
  236. assert admin_order["policy_no"] == policy["policy_no"]
  237. def test_super_admin_can_update_role_permissions() -> None:
  238. with TestClient(build_test_app()) as client:
  239. admin_token = client.post(
  240. "/api/v1/admin/auth/login",
  241. json={"username": "admin", "password": "zaq1XSW@"},
  242. ).json()["data"]["tokens"]["access_token"]
  243. headers = {"Authorization": f"Bearer {admin_token}"}
  244. roles = client.get("/api/v1/admin/roles", headers=headers)
  245. update = client.put(
  246. "/api/v1/admin/roles/OPERATOR",
  247. headers=headers,
  248. json={
  249. "permissions": [
  250. "dashboard:read",
  251. "order:read",
  252. "policy:read",
  253. "role:read",
  254. ],
  255. "data_scope": "MASKED_ALL",
  256. },
  257. )
  258. protected = client.put(
  259. "/api/v1/admin/roles/SUPER_ADMIN",
  260. headers=headers,
  261. json={"permissions": [], "data_scope": "SELF"},
  262. )
  263. operator_login = client.post(
  264. "/api/v1/admin/auth/login",
  265. json={"username": "operator01", "password": "zaq1XSW@"},
  266. )
  267. assert roles.status_code == 200
  268. assert len(roles.json()["data"]["items"]) == 4
  269. assert update.status_code == 200
  270. assert update.json()["data"]["permissions"] == [
  271. "dashboard:read",
  272. "order:read",
  273. "policy:read",
  274. "role:read",
  275. ]
  276. assert protected.status_code == 409
  277. assert operator_login.json()["data"]["user"]["permissions"] == [
  278. "dashboard:read",
  279. "order:read",
  280. "policy:read",
  281. "role:read",
  282. ]
  283. def test_super_admin_can_maintain_admin_accounts_and_customers() -> None:
  284. with TestClient(build_test_app()) as client:
  285. customer_login = client.post(
  286. "/api/v1/h5/auth/login",
  287. json={"mobile": "18800000001", "code": "147258"},
  288. )
  289. customer_id = customer_login.json()["data"]["user"]["id"]
  290. admin_token = client.post(
  291. "/api/v1/admin/auth/login",
  292. json={"username": "admin", "password": "zaq1XSW@"},
  293. ).json()["data"]["tokens"]["access_token"]
  294. headers = {"Authorization": f"Bearer {admin_token}"}
  295. created = client.post(
  296. "/api/v1/admin/users/admins",
  297. headers=headers,
  298. json={
  299. "username": "service01",
  300. "password": "Initial1!",
  301. "display_name": "客服专员",
  302. "role_code": "OPERATOR",
  303. },
  304. )
  305. created_user = created.json()["data"]
  306. updated = client.put(
  307. f"/api/v1/admin/users/admins/{created_user['id']}",
  308. headers=headers,
  309. json={
  310. "display_name": "客服主管",
  311. "status": "ACTIVE",
  312. "role_code": "REVIEWER",
  313. },
  314. )
  315. password_reset = client.post(
  316. f"/api/v1/admin/users/admins/{created_user['id']}/password",
  317. headers=headers,
  318. json={"new_password": "Changed1!"},
  319. )
  320. customer_disabled = client.put(
  321. f"/api/v1/admin/users/customers/{customer_id}/status",
  322. headers=headers,
  323. json={"status": "DISABLED"},
  324. )
  325. users = client.get("/api/v1/admin/users", headers=headers)
  326. new_login = client.post(
  327. "/api/v1/admin/auth/login",
  328. json={"username": "service01", "password": "Changed1!"},
  329. )
  330. disabled_customer_login = client.post(
  331. "/api/v1/h5/auth/login",
  332. json={"mobile": "18800000001", "code": "147258"},
  333. )
  334. assert created.status_code == 201
  335. assert updated.status_code == 200
  336. assert updated.json()["data"]["display_name"] == "客服主管"
  337. assert updated.json()["data"]["roles"] == ["REVIEWER"]
  338. assert password_reset.status_code == 200
  339. assert customer_disabled.json()["data"]["status"] == "DISABLED"
  340. assert len(users.json()["data"]["admin_users"]) == 10
  341. assert len(users.json()["data"]["customers"]) == 1
  342. assert new_login.status_code == 200
  343. assert disabled_customer_login.status_code == 403