enrollment.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. from typing import Annotated, Any
  2. from fastapi import APIRouter, Depends, Header, Request, Response, status
  3. from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
  4. from pydantic import BaseModel, Field
  5. from app.api.responses import success_response
  6. from app.core.config import Settings
  7. from app.core.errors import AppError
  8. from app.domains.enrollment.service import EnrollmentService
  9. from app.domains.identity.models import H5User
  10. from app.domains.identity.service import H5AuthService
  11. enrollment_bearer_scheme = HTTPBearer(auto_error=False)
  12. class InsuredInput(BaseModel):
  13. age: int = Field(ge=0, le=120)
  14. region_code: str = Field(min_length=6, max_length=12)
  15. occupation_code: str = Field(min_length=1, max_length=32)
  16. class CreateQuoteRequest(BaseModel):
  17. product_id: str = Field(min_length=26, max_length=26)
  18. plan_id: str = Field(min_length=26, max_length=26)
  19. insured: InsuredInput
  20. relationship: str = Field(pattern="^(SELF|PARENT|SPOUSE|CHILD)$")
  21. class PersonInput(BaseModel):
  22. name: str = Field(min_length=1, max_length=64)
  23. id_no: str = Field(min_length=6, max_length=32)
  24. class ContactInput(BaseModel):
  25. mobile: str = Field(min_length=11, max_length=11)
  26. class CreateDraftRequest(BaseModel):
  27. quote_id: str = Field(min_length=26, max_length=26)
  28. applicant: PersonInput
  29. insured: PersonInput
  30. contact: ContactInput
  31. class CreateOrderRequest(BaseModel):
  32. draft_id: str = Field(min_length=26, max_length=26)
  33. confirmation_token: str = Field(min_length=32, max_length=128)
  34. class MockPaymentCallbackRequest(BaseModel):
  35. callback_no: str = Field(min_length=1, max_length=64)
  36. provider_transaction_no: str = Field(min_length=1, max_length=64)
  37. payment_no: str = Field(min_length=1, max_length=40)
  38. status: str = Field(pattern="^(SUCCEEDED|FAILED)$")
  39. amount_cents: int = Field(gt=0)
  40. occurred_at: str = Field(min_length=10, max_length=64)
  41. def create_enrollment_router(
  42. service: EnrollmentService,
  43. h5_auth_service: H5AuthService,
  44. settings: Settings,
  45. ) -> APIRouter:
  46. router = APIRouter(tags=["enrollment"])
  47. def current_h5_user(
  48. credentials: Annotated[
  49. HTTPAuthorizationCredentials | None,
  50. Depends(enrollment_bearer_scheme),
  51. ],
  52. ) -> H5User:
  53. if credentials is None:
  54. raise AppError("AUTH_REQUIRED", "请先登录", 401)
  55. return h5_auth_service.authenticate_access(credentials.credentials)
  56. @router.post("/h5/quotes", status_code=status.HTTP_201_CREATED)
  57. def create_quote(
  58. payload: CreateQuoteRequest,
  59. request: Request,
  60. user: Annotated[H5User, Depends(current_h5_user)],
  61. ) -> dict[str, Any]:
  62. return success_response(
  63. request,
  64. service.create_quote(
  65. user,
  66. product_id=payload.product_id,
  67. plan_id=payload.plan_id,
  68. age=payload.insured.age,
  69. region_code=payload.insured.region_code,
  70. occupation_code=payload.insured.occupation_code,
  71. relationship=payload.relationship,
  72. ),
  73. )
  74. @router.post("/h5/enrollment-drafts", status_code=status.HTTP_201_CREATED)
  75. def create_draft(
  76. payload: CreateDraftRequest,
  77. request: Request,
  78. user: Annotated[H5User, Depends(current_h5_user)],
  79. ) -> dict[str, Any]:
  80. return success_response(
  81. request,
  82. service.create_draft(
  83. user,
  84. quote_id=payload.quote_id,
  85. applicant=payload.applicant.model_dump(),
  86. insured=payload.insured.model_dump(),
  87. contact=payload.contact.model_dump(),
  88. ),
  89. )
  90. @router.post(
  91. "/h5/enrollment-drafts/{draft_id}/confirmation",
  92. status_code=status.HTTP_201_CREATED,
  93. )
  94. def confirm_draft(
  95. draft_id: str,
  96. request: Request,
  97. user: Annotated[H5User, Depends(current_h5_user)],
  98. ) -> dict[str, Any]:
  99. return success_response(request, service.confirm_draft(user, draft_id))
  100. @router.post("/h5/orders", status_code=status.HTTP_201_CREATED)
  101. def create_order(
  102. payload: CreateOrderRequest,
  103. request: Request,
  104. response: Response,
  105. user: Annotated[H5User, Depends(current_h5_user)],
  106. idempotency_key: Annotated[
  107. str,
  108. Header(alias="Idempotency-Key", min_length=1, max_length=64),
  109. ],
  110. ) -> dict[str, Any]:
  111. data, created = service.create_order(
  112. user,
  113. draft_id=payload.draft_id,
  114. confirmation_token=payload.confirmation_token,
  115. idempotency_key=idempotency_key,
  116. )
  117. response.status_code = status.HTTP_201_CREATED if created else status.HTTP_200_OK
  118. return success_response(request, data)
  119. @router.post(
  120. "/h5/orders/{order_id}/payments",
  121. status_code=status.HTTP_201_CREATED,
  122. )
  123. def create_payment(
  124. order_id: str,
  125. request: Request,
  126. response: Response,
  127. user: Annotated[H5User, Depends(current_h5_user)],
  128. idempotency_key: Annotated[
  129. str,
  130. Header(alias="Idempotency-Key", min_length=1, max_length=64),
  131. ],
  132. ) -> dict[str, Any]:
  133. data, created = service.create_payment(
  134. user,
  135. order_id=order_id,
  136. idempotency_key=idempotency_key,
  137. )
  138. response.status_code = status.HTTP_201_CREATED if created else status.HTTP_200_OK
  139. return success_response(request, data)
  140. @router.post("/dev/mock-payments/{payment_id}/complete")
  141. def complete_mock_payment(
  142. payment_id: str,
  143. request: Request,
  144. user: Annotated[H5User, Depends(current_h5_user)],
  145. ) -> dict[str, Any]:
  146. if not settings.dev_tools_enabled or settings.app_env == "production":
  147. raise AppError("DEV_TOOL_DISABLED", "本地模拟支付未启用", 404)
  148. return success_response(
  149. request,
  150. service.complete_mock_payment(user, payment_id=payment_id),
  151. )
  152. @router.post("/callbacks/mock-payment")
  153. def mock_payment_callback(
  154. payload: MockPaymentCallbackRequest,
  155. request: Request,
  156. signature: Annotated[
  157. str,
  158. Header(alias="X-Mock-Pay-Signature", min_length=1, max_length=128),
  159. ],
  160. ) -> dict[str, Any]:
  161. return success_response(
  162. request,
  163. service.handle_mock_callback(payload.model_dump(), signature),
  164. )
  165. @router.get("/h5/policies")
  166. def list_policies(
  167. request: Request,
  168. user: Annotated[H5User, Depends(current_h5_user)],
  169. ) -> dict[str, Any]:
  170. return success_response(request, service.list_policies(user))
  171. @router.get("/h5/orders")
  172. def list_orders(
  173. request: Request,
  174. user: Annotated[H5User, Depends(current_h5_user)],
  175. ) -> dict[str, Any]:
  176. return success_response(request, service.list_orders(user.id))
  177. return router