test_refund_settlement_transaction.py 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. from datetime import UTC, date, datetime
  2. import pytest
  3. from sqlalchemy import create_engine, event, select
  4. from sqlalchemy.orm import Session, sessionmaker
  5. from zbt.domains.enrollment.models import Policy
  6. from zbt.domains.surrender.models import (
  7. RefundAttempt,
  8. SurrenderRequest,
  9. SurrenderTimelineEvent,
  10. )
  11. from zbt.infrastructure.mysql.core_models import (
  12. CoreBase,
  13. PolicyRecord,
  14. RefundAttemptRecord,
  15. SurrenderRequestRecord,
  16. SurrenderTimelineEventRecord,
  17. )
  18. from zbt.infrastructure.mysql.refund_settlement import (
  19. SqlAlchemyRefundSettlementWriter,
  20. )
  21. NOW = datetime(2026, 7, 30, 10, 0, tzinfo=UTC)
  22. ATTEMPT_ID = "01KZ0000000000000000000201"
  23. REQUEST_ID = "01KZ0000000000000000000202"
  24. POLICY_ID = "01KZ0000000000000000000203"
  25. def session_factory() -> sessionmaker[Session]:
  26. engine = create_engine("sqlite+pysqlite:///:memory:")
  27. CoreBase.metadata.create_all(engine)
  28. factory = sessionmaker(bind=engine, expire_on_commit=False)
  29. with factory.begin() as session:
  30. session.add(
  31. PolicyRecord(
  32. id=POLICY_ID,
  33. policy_no="POL-20260730-TX01",
  34. order_id="01KZ0000000000000000000204",
  35. h5_user_id="01KZ0000000000000000000205",
  36. product_id="01KZ0000000000000000000206",
  37. product_version_id="01KZ0000000000000000000207",
  38. plan_id="01KZ0000000000000000000208",
  39. premium_cents=36_500,
  40. currency="CNY",
  41. coverage_start=date(2026, 7, 1),
  42. coverage_end=date(2027, 6, 30),
  43. status="SURRENDERING",
  44. issued_at=NOW,
  45. )
  46. )
  47. session.add(
  48. SurrenderRequestRecord(
  49. id=REQUEST_ID,
  50. request_no="ZBT-TB-20260730-2001",
  51. h5_user_id="01KZ0000000000000000000205",
  52. policy_id=POLICY_ID,
  53. idempotency_key="transaction-test",
  54. reason="保障调整",
  55. status="REFUND_PENDING",
  56. refund_amount_cents=30_000,
  57. currency="CNY",
  58. rule_version="surrender-v1",
  59. calculation_json={},
  60. workflow_thread_id="transaction-thread",
  61. risk_reasons_json=[],
  62. workflow_plan_json=[],
  63. created_at=NOW,
  64. )
  65. )
  66. session.add(
  67. RefundAttemptRecord(
  68. id=ATTEMPT_ID,
  69. surrender_request_id=REQUEST_ID,
  70. attempt_no=1,
  71. refund_no="ZBT-RF-20260730-2001",
  72. provider="INTERNAL_REFUND",
  73. idempotency_key="transaction-refund",
  74. amount_cents=30_000,
  75. currency="CNY",
  76. status="SUBMITTED",
  77. created_at=NOW,
  78. updated_at=NOW,
  79. )
  80. )
  81. return factory
  82. def settlement_values() -> tuple[
  83. RefundAttempt,
  84. SurrenderRequest,
  85. Policy,
  86. SurrenderTimelineEvent,
  87. ]:
  88. attempt = RefundAttempt(
  89. id=ATTEMPT_ID,
  90. surrender_request_id=REQUEST_ID,
  91. attempt_no=1,
  92. refund_no="ZBT-RF-20260730-2001",
  93. provider="INTERNAL_REFUND",
  94. idempotency_key="transaction-refund",
  95. amount_cents=30_000,
  96. currency="CNY",
  97. status="SUCCEEDED",
  98. provider_refund_no=None,
  99. error_code=None,
  100. error_message=None,
  101. created_at=NOW,
  102. updated_at=NOW,
  103. )
  104. request = SurrenderRequest(
  105. id=REQUEST_ID,
  106. request_no="ZBT-TB-20260730-2001",
  107. user_id="01KZ0000000000000000000205",
  108. policy_id=POLICY_ID,
  109. idempotency_key="transaction-test",
  110. reason="保障调整",
  111. status="REFUNDED",
  112. refund_amount_cents=30_000,
  113. currency="CNY",
  114. rule_version="surrender-v1",
  115. calculation={},
  116. workflow_thread_id="transaction-thread",
  117. created_at=NOW,
  118. completed_at=NOW,
  119. )
  120. policy = Policy(
  121. id=POLICY_ID,
  122. policy_no="POL-20260730-TX01",
  123. order_id="01KZ0000000000000000000204",
  124. user_id="01KZ0000000000000000000205",
  125. product_id="01KZ0000000000000000000206",
  126. product_version_id="01KZ0000000000000000000207",
  127. plan_id="01KZ0000000000000000000208",
  128. premium_cents=36_500,
  129. currency="CNY",
  130. coverage_start=date(2026, 7, 1),
  131. coverage_end=date(2027, 6, 30),
  132. status="SURRENDERED",
  133. issued_at=NOW,
  134. )
  135. timeline = SurrenderTimelineEvent(
  136. id="01KZ0000000000000000000209",
  137. surrender_request_id=REQUEST_ID,
  138. event_type="REFUND_SUCCEEDED",
  139. title="退款成功,保单已终止",
  140. detail={"refund_no": attempt.refund_no},
  141. actor_type="SYSTEM",
  142. actor_id=None,
  143. idempotency_key=f"{REQUEST_ID}:REFUND_SUCCEEDED",
  144. created_at=NOW,
  145. )
  146. return attempt, request, policy, timeline
  147. def test_refund_settlement_rolls_back_all_states_when_flush_fails() -> None:
  148. factory = session_factory()
  149. writer = SqlAlchemyRefundSettlementWriter(factory)
  150. attempt, request, policy, timeline = settlement_values()
  151. def fail_flush(
  152. session: Session,
  153. flush_context: object,
  154. instances: object,
  155. ) -> None:
  156. del session, flush_context, instances
  157. raise RuntimeError("模拟时间线写入失败")
  158. event.listen(factory.class_, "before_flush", fail_flush)
  159. try:
  160. with pytest.raises(RuntimeError, match="模拟时间线写入失败"):
  161. writer.complete(
  162. attempt=attempt,
  163. request=request,
  164. policy=policy,
  165. timeline=timeline,
  166. )
  167. finally:
  168. event.remove(factory.class_, "before_flush", fail_flush)
  169. with factory() as session:
  170. assert session.get(RefundAttemptRecord, ATTEMPT_ID).status == "SUBMITTED"
  171. assert session.get(SurrenderRequestRecord, REQUEST_ID).status == (
  172. "REFUND_PENDING"
  173. )
  174. assert session.get(PolicyRecord, POLICY_ID).status == "SURRENDERING"
  175. assert session.scalars(select(SurrenderTimelineEventRecord)).all() == []