| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206 |
- from datetime import UTC, datetime
- from fastapi.testclient import TestClient
- from zbt.commands.seed import build_seed_manifest
- from zbt.core.config import Settings
- from zbt.core.passwords import PasswordService
- from zbt.domains.agent.repository import InMemoryAgentRepository
- from zbt.domains.attribution.models import (
- OrderAttribution,
- PromotionCode,
- Salesperson,
- )
- from zbt.domains.attribution.repository import InMemoryAttributionRepository
- from zbt.domains.attribution.service import AttributionService
- from zbt.domains.catalog.repository import InMemoryCatalogRepository
- from zbt.domains.enrollment.models import EnrollmentOrder
- from zbt.domains.enrollment.repository import InMemoryEnrollmentRepository
- from zbt.domains.identity.models import AdminUser, H5User
- from zbt.domains.identity.repository import InMemoryIdentityRepository
- from zbt.main import create_app
- NOW = datetime(2026, 7, 26, 9, tzinfo=UTC)
- def _seed_attribution(
- repository: InMemoryAttributionRepository,
- *,
- admin_user_id: str = "01ADMIN0000000000000000001",
- ) -> tuple[Salesperson, PromotionCode]:
- salesperson = Salesperson(
- id="01SALES0000000000000000001",
- admin_user_id=admin_user_id,
- code="SALES-A",
- name="业务员A",
- status="ACTIVE",
- created_at=NOW,
- )
- code = PromotionCode(
- id="01PROMO0000000000000000001",
- code="ZBT-SALES-A",
- salesperson_id=salesperson.id,
- channel="GROUND_PROMOTION",
- status="ACTIVE",
- created_at=NOW,
- )
- repository.save_salesperson(salesperson)
- repository.save_promotion_code(code)
- return salesperson, code
- def _order(order_id: str, user_id: str, amount_cents: int = 19900) -> EnrollmentOrder:
- return EnrollmentOrder(
- id=order_id,
- order_no=f"ORD-{order_id[-4:]}",
- user_id=user_id,
- quote_id=f"QUOTE-{order_id}"[-26:],
- draft_id=f"DRAFT-{order_id}"[-26:],
- confirmation_id=f"CONFIRM-{order_id}"[-26:],
- idempotency_key=f"IDEMPOTENCY-{order_id}",
- product_id="01PRODUCT00000000000000001",
- product_version_id="01VERSION00000000000000001",
- plan_id="01PLAN0000000000000000001",
- applicant_snapshot={
- "name": "周明远",
- "id_no": "510104198801011234",
- },
- insured_snapshot={
- "name": "周建国",
- "id_no": "510104196001011234",
- },
- amount_cents=amount_cents,
- currency="CNY",
- status="ISSUED",
- created_at=NOW,
- )
- def test_first_touch_attribution_is_stable_and_order_snapshot_is_idempotent() -> None:
- repository = InMemoryAttributionRepository()
- salesperson, code = _seed_attribution(repository)
- service = AttributionService(repository, lambda: NOW)
- user = H5User(
- id="01H5USER000000000000000001",
- mobile="18800000001",
- mobile_masked="188****0001",
- display_name="体验用户",
- status="ACTIVE",
- created_at=NOW,
- )
- first = service.capture_visit(user, code.code)
- second = service.capture_visit(user, code.code)
- order = _order("01ORDER0000000000000000001", user.id)
- service.record_order(order)
- service.record_order(order)
- assert first["first_touch"] is True
- assert second["first_touch"] is False
- lead = repository.get_lead_by_user(user.id)
- assert lead is not None
- assert lead.salesperson_id == salesperson.id
- assert lead.visit_count == 2
- performance = service.performance(salesperson.admin_user_id)
- assert performance["visit_count"] == 2
- assert performance["lead_count"] == 1
- assert performance["order_count"] == 1
- assert performance["premium_cents"] == 19900
- assert service.order_ids(salesperson.admin_user_id) == {order.id}
- def test_h5_referral_login_and_admin_self_scope_form_a_closed_loop() -> None:
- identities = InMemoryIdentityRepository()
- admin = AdminUser(
- id="01ADMIN0000000000000000001",
- username="sales_a",
- password_hash=PasswordService().hash("zaq1XSW@"),
- display_name="业务员A",
- status="ACTIVE",
- roles=("SALESPERSON",),
- permissions=(
- "dashboard:read",
- "order:read",
- "attribution:read",
- "product:read",
- "product:write",
- ),
- data_scope="SELF",
- )
- identities.save_admin_user(admin)
- attribution = InMemoryAttributionRepository()
- salesperson, code = _seed_attribution(attribution, admin_user_id=admin.id)
- catalog = InMemoryCatalogRepository()
- manifest = build_seed_manifest()
- for product in manifest.products:
- catalog.save_product(product)
- for version in manifest.versions:
- catalog.save_version(version)
- enrollment = InMemoryEnrollmentRepository()
- app = create_app(
- settings=Settings(app_env="test"),
- identity_repository=identities,
- catalog_repository=catalog,
- agent_repository=InMemoryAgentRepository(),
- enrollment_repository=enrollment,
- attribution_repository=attribution,
- )
- with TestClient(app) as client:
- h5_login = client.post(
- "/api/v1/h5/auth/login",
- json={
- "mobile": "18800000001",
- "code": "147258",
- "referral_code": code.code,
- },
- )
- assert h5_login.status_code == 200
- h5_user_id = h5_login.json()["data"]["user"]["id"]
- assert h5_login.json()["data"]["attribution"]["attributed"] is True
- owned_order = _order("01ORDER0000000000000000001", h5_user_id)
- other_order = _order("01ORDER0000000000000000002", "01OTHERUSER0000000000000001")
- enrollment.save_order(owned_order)
- enrollment.save_order(other_order)
- attribution.save_order_attribution(
- OrderAttribution(
- id="01ATTR00000000000000000001",
- order_id=owned_order.id,
- h5_user_id=h5_user_id,
- salesperson_id=salesperson.id,
- promotion_code_id=code.id,
- source_code=code.code,
- amount_cents=owned_order.amount_cents,
- attributed_at=NOW,
- )
- )
- admin_login = client.post(
- "/api/v1/admin/auth/login",
- json={"username": "sales_a", "password": "zaq1XSW@"},
- )
- headers = {
- "Authorization": (f"Bearer {admin_login.json()['data']['tokens']['access_token']}")
- }
- orders = client.get("/api/v1/admin/orders", headers=headers)
- overview = client.get(
- "/api/v1/admin/attribution/overview",
- headers=headers,
- )
- products = client.get("/api/v1/admin/products", headers=headers)
- product_id = products.json()["data"]["items"][0]["product_id"]
- disabled = client.put(
- f"/api/v1/admin/products/{product_id}/status",
- headers=headers,
- json={"status": "INACTIVE"},
- )
- assert orders.status_code == 200
- assert orders.json()["data"]["total"] == 1
- visible_order = orders.json()["data"]["items"][0]
- assert visible_order["order_id"] == owned_order.id
- assert visible_order["applicant"]["id_no"] == "510104********1234"
- assert overview.json()["data"]["salesperson_count"] == 1
- assert overview.json()["data"]["order_count"] == 1
- assert disabled.status_code == 200
- assert disabled.json()["data"]["status"] == "INACTIVE"
|