test_service_request_service.py 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  1. from datetime import UTC, date, datetime
  2. import pytest
  3. from zbt.core.errors import AppError
  4. from zbt.domains.enrollment.models import Policy
  5. from zbt.domains.enrollment.repository import InMemoryEnrollmentRepository
  6. from zbt.domains.identity.models import H5User
  7. from zbt.domains.service_request.models import ServiceRequest
  8. from zbt.domains.service_request.repository import InMemoryServiceRequestRepository
  9. from zbt.domains.service_request.service import ServiceRequestService
  10. from zbt.domains.service_request.vision import MaterialExtraction
  11. class FakeVisionGateway:
  12. def recognize(
  13. self,
  14. *,
  15. content: bytes,
  16. media_type: str,
  17. material_type: str,
  18. ) -> MaterialExtraction:
  19. assert content == b"invoice-image"
  20. assert media_type == "image/jpeg"
  21. assert material_type == "MEDICAL_INVOICE"
  22. return MaterialExtraction(
  23. fields={
  24. "invoice_no": "INV-20260727-001",
  25. "hospital_name": "成都市第一人民医院",
  26. "visit_date": "2026-07-20",
  27. "amount_yuan": "1280.50",
  28. },
  29. confidence=0.94,
  30. warnings=("请人工核对票据号码",),
  31. )
  32. def test_user_recognizes_confirms_and_submits_service_request() -> None:
  33. enrollments = InMemoryEnrollmentRepository()
  34. enrollments.save_policy(_policy())
  35. repository = InMemoryServiceRequestRepository()
  36. service = ServiceRequestService(
  37. repository,
  38. enrollments,
  39. FakeVisionGateway(),
  40. lambda: datetime(2026, 7, 27, 6, 0, tzinfo=UTC),
  41. )
  42. user = H5User(
  43. id="01USER00000000000000000001",
  44. mobile="13800138000",
  45. mobile_masked="138****8000",
  46. display_name="张先生",
  47. status="ACTIVE",
  48. created_at=datetime(2026, 7, 1, tzinfo=UTC),
  49. )
  50. recognition = service.recognize_material(
  51. user,
  52. policy_id="01POLICY00000000000000001",
  53. material_type="MEDICAL_INVOICE",
  54. source_name="医疗发票.jpg",
  55. media_type="image/jpeg",
  56. content=b"invoice-image",
  57. )
  58. request = service.submit_request(
  59. user,
  60. policy_id="01POLICY00000000000000001",
  61. request_type="MEDICAL_REIMBURSEMENT_ASSISTANCE",
  62. recognition_id=recognition["recognition_id"],
  63. confirmed_fields={
  64. **recognition["fields"],
  65. "contact_mobile": "13800138000",
  66. },
  67. )
  68. assert recognition["status"] == "PENDING_CONFIRMATION"
  69. assert "confidence" not in recognition
  70. assert request["request_no"].startswith("ZBT-SVC-20260727-")
  71. assert request["status"] == "SUBMITTED"
  72. assert service.list_user_requests(user)["total"] == 1
  73. assert "recognition" not in service.list_user_requests(user)["items"][0]
  74. admin_request = service.list_all_requests()["items"][0]
  75. assert admin_request["recognition"]["confidence"] == 0.94
  76. assert admin_request["recognition"]["material_available"] is True
  77. material, media_type, source_name = service.get_request_material(
  78. request["request_id"]
  79. )
  80. assert material.content == b"invoice-image"
  81. assert media_type == "image/jpeg"
  82. assert source_name == "医疗发票.jpg"
  83. reviewed = service.review_request(
  84. request["request_id"],
  85. decision="ACCEPTED",
  86. reviewer_id="01ADMIN000000000000000001",
  87. )
  88. assert reviewed["status"] == "ACCEPTED"
  89. assert reviewed["reviewed_by"] == "01ADMIN000000000000000001"
  90. assert reviewed["review_note"] is None
  91. assert service.list_user_requests(user)["items"][0]["status"] == "ACCEPTED"
  92. def test_user_cannot_recognize_material_for_another_users_policy() -> None:
  93. enrollments = InMemoryEnrollmentRepository()
  94. enrollments.save_policy(_policy())
  95. service = ServiceRequestService(
  96. InMemoryServiceRequestRepository(),
  97. enrollments,
  98. FakeVisionGateway(),
  99. lambda: datetime(2026, 7, 27, 6, 0, tzinfo=UTC),
  100. )
  101. another_user = H5User(
  102. id="01OTHER0000000000000000001",
  103. mobile="13900139000",
  104. mobile_masked="139****9000",
  105. display_name="李女士",
  106. status="ACTIVE",
  107. created_at=datetime(2026, 7, 1, tzinfo=UTC),
  108. )
  109. with pytest.raises(AppError) as caught:
  110. service.recognize_material(
  111. another_user,
  112. policy_id="01POLICY00000000000000001",
  113. material_type="MEDICAL_INVOICE",
  114. source_name="医疗发票.jpg",
  115. media_type="image/jpeg",
  116. content=b"invoice-image",
  117. )
  118. assert caught.value.code == "POLICY_NOT_FOUND"
  119. def test_rejected_request_uses_standard_reason() -> None:
  120. repository = InMemoryServiceRequestRepository()
  121. submitted_at = datetime(2026, 7, 27, 6, 0, tzinfo=UTC)
  122. repository.save_request(
  123. ServiceRequest(
  124. id="01SERVICE00000000000000001",
  125. request_no="ZBT-SVC-20260727-0001",
  126. user_id="01USER00000000000000000001",
  127. policy_id="01POLICY00000000000000001",
  128. request_type="MEDICAL_REIMBURSEMENT_ASSISTANCE",
  129. recognition_id="01RECOGNITION000000000001",
  130. confirmed_fields={},
  131. status="SUBMITTED",
  132. submitted_at=submitted_at,
  133. )
  134. )
  135. service = ServiceRequestService(
  136. repository,
  137. InMemoryEnrollmentRepository(),
  138. FakeVisionGateway(),
  139. lambda: submitted_at,
  140. )
  141. reviewed = service.review_request(
  142. "01SERVICE00000000000000001",
  143. decision="REJECTED",
  144. reviewer_id="01ADMIN000000000000000001",
  145. )
  146. assert reviewed["status"] == "REJECTED"
  147. assert reviewed["review_note"] == "不满足赔付条件"
  148. def _policy() -> Policy:
  149. return Policy(
  150. id="01POLICY00000000000000001",
  151. policy_no="POL-20260727-0001",
  152. order_id="01ORDER000000000000000001",
  153. user_id="01USER00000000000000000001",
  154. product_id="01PRODUCT0000000000000001",
  155. product_version_id="01VERSION0000000000000001",
  156. plan_id="01PLAN0000000000000000001",
  157. premium_cents=29900,
  158. currency="CNY",
  159. coverage_start=date(2026, 7, 28),
  160. coverage_end=date(2027, 7, 27),
  161. status="ACTIVE",
  162. issued_at=datetime(2026, 7, 27, 5, 0, tzinfo=UTC),
  163. )