feat: add customer travel records and browse history
This commit is contained in:
1 parent
0139fe6382
commit
6d9a546918
79 files changed
+12362
-112
No files matched your search
@@ -50,6 +50,7 @@ class CurrentSchemaInspector:
|
||||
"AuditLog",
|
||||
"ConciergeAdvisor",
|
||||
"Customer",
|
||||
"CustomerBrowseHistory",
|
||||
"DetailRecord",
|
||||
"HeroSlide",
|
||||
"HomeTeamBuilding",
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
from importlib.util import module_from_spec, spec_from_file_location
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
MIGRATION_PATH = Path(__file__).parents[1] / "alembic" / "versions" / "0036_customer_browse_history.py"
|
||||
SPEC = spec_from_file_location("customer_browse_history_migration", MIGRATION_PATH)
|
||||
assert SPEC and SPEC.loader
|
||||
MIGRATION = module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(MIGRATION)
|
||||
|
||||
|
||||
def test_customer_browse_history_migration_is_chained_after_current_head():
|
||||
assert MIGRATION.revision == "0036_customer_browse_history"
|
||||
assert MIGRATION.down_revision == "0035_concierge_details_jsonb"
|
||||
|
||||
|
||||
def test_customer_browse_history_table_and_indexes_are_created_only_when_missing():
|
||||
assert MIGRATION.should_create_table(set()) is True
|
||||
assert MIGRATION.should_create_table({MIGRATION.TABLE_NAME}) is False
|
||||
assert MIGRATION.CUSTOMER_HISTORY_INDEXES == {
|
||||
"ix_CustomerBrowseHistory_customerId",
|
||||
"ix_CustomerBrowseHistory_customer_visitedAt",
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
from datetime import datetime
|
||||
from types import SimpleNamespace
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from app.auth import require_customer
|
||||
from app.database import get_db
|
||||
from app.main import create_app
|
||||
from app.models import Customer, Lead, WanfaRoute
|
||||
|
||||
|
||||
class FakeScalarResult:
|
||||
def __init__(self, items):
|
||||
self.items = items
|
||||
|
||||
def all(self):
|
||||
return self.items
|
||||
|
||||
|
||||
class FakeDb:
|
||||
def __init__(self, *, scalar_results=None, scalar_values=None):
|
||||
self.scalar_results = list(scalar_results or [])
|
||||
self.scalar_values = list(scalar_values or [])
|
||||
self.added = []
|
||||
self.committed = False
|
||||
|
||||
def scalars(self, _statement):
|
||||
return FakeScalarResult(self.scalar_results.pop(0) if self.scalar_results else [])
|
||||
|
||||
def scalar(self, _statement):
|
||||
return self.scalar_values.pop(0) if self.scalar_values else None
|
||||
|
||||
def add(self, item):
|
||||
self.added.append(item)
|
||||
|
||||
def flush(self):
|
||||
for item in self.added:
|
||||
if not getattr(item, "id", None):
|
||||
item.id = "browse-history-test"
|
||||
|
||||
def commit(self):
|
||||
self.committed = True
|
||||
|
||||
def refresh(self, _item):
|
||||
return None
|
||||
|
||||
|
||||
def authenticated_app(fake_db, customer):
|
||||
app = create_app()
|
||||
app.dependency_overrides[get_db] = lambda: fake_db
|
||||
app.dependency_overrides[require_customer] = lambda: customer
|
||||
return app
|
||||
|
||||
|
||||
def make_vehicle_lead(**overrides):
|
||||
return Lead(
|
||||
id=overrides.get("id", "lead-1"),
|
||||
leadType="vehicle",
|
||||
contactName=overrides.get("contactName", "Damon"),
|
||||
customerId=overrides.get("customerId", "customer-1"),
|
||||
destination=overrides.get("destination", "黄果树景区"),
|
||||
phone=overrides.get("phone", "13800000000"),
|
||||
travelDate=overrides.get("travelDate", datetime(2026, 8, 25)),
|
||||
peopleCount=overrides.get("peopleCount", 5),
|
||||
note=overrides.get("note", "需要儿童座椅"),
|
||||
vehicleDemand=overrides.get(
|
||||
"vehicleDemand",
|
||||
{
|
||||
"serviceType": "charter",
|
||||
"charterDuration": "fullDay",
|
||||
"travelDate": "2026-08-25",
|
||||
"pickupLocation": "贵阳北站",
|
||||
"dropoffLocation": "黄果树景区",
|
||||
"peopleCount": 5,
|
||||
"luggageCount": 2,
|
||||
"vehicleOptionTitle": "9座精品商务车",
|
||||
"specialRequirements": "需要儿童座椅",
|
||||
},
|
||||
),
|
||||
status=overrides.get("status", "new"),
|
||||
createdAt=overrides.get("createdAt", datetime(2026, 8, 20, 10, 0)),
|
||||
updatedAt=overrides.get("updatedAt", datetime(2026, 8, 20, 10, 0)),
|
||||
)
|
||||
|
||||
|
||||
def test_public_vehicle_demands_are_paginated_and_phone_is_masked():
|
||||
customer = Customer(id="customer-1", phone="13800000000")
|
||||
lead = make_vehicle_lead()
|
||||
db = FakeDb(scalar_results=[[lead]], scalar_values=[1])
|
||||
app = authenticated_app(db, customer)
|
||||
|
||||
try:
|
||||
response = TestClient(app).get(
|
||||
"/api/public/customer/vehicle-demands?pageNum=2&pageSize=1"
|
||||
)
|
||||
finally:
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": {
|
||||
"items": [
|
||||
{
|
||||
"id": "lead-1",
|
||||
"status": "new",
|
||||
"contactName": "Damon",
|
||||
"phoneMasked": "138****0000",
|
||||
"destination": "黄果树景区",
|
||||
"travelDate": "2026-08-25T00:00:00",
|
||||
"peopleCount": 5,
|
||||
"note": "需要儿童座椅",
|
||||
"vehicleDemand": lead.vehicleDemand,
|
||||
"createdAt": "2026-08-20T10:00:00",
|
||||
"updatedAt": "2026-08-20T10:00:00",
|
||||
}
|
||||
],
|
||||
"total": 1,
|
||||
"pageNum": 2,
|
||||
"pageSize": 1,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_public_vehicle_demand_detail_only_returns_the_current_customer_record():
|
||||
customer = Customer(id="customer-1", phone="13800000000")
|
||||
lead = make_vehicle_lead()
|
||||
db = FakeDb(scalar_values=[lead])
|
||||
app = authenticated_app(db, customer)
|
||||
|
||||
try:
|
||||
response = TestClient(app).get("/api/public/customer/vehicle-demands/lead-1")
|
||||
finally:
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["data"]["id"] == "lead-1"
|
||||
assert response.json()["data"]["phoneMasked"] == "138****0000"
|
||||
assert "phone" not in response.json()["data"]
|
||||
|
||||
|
||||
def test_public_vehicle_demand_detail_hides_another_customers_record():
|
||||
customer = Customer(id="customer-1", phone="13800000000")
|
||||
db = FakeDb(scalar_values=[None])
|
||||
app = authenticated_app(db, customer)
|
||||
|
||||
try:
|
||||
response = TestClient(app).get("/api/public/customer/vehicle-demands/lead-other")
|
||||
finally:
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
def test_public_browse_history_is_paginated_for_the_current_customer():
|
||||
customer = Customer(id="customer-1", phone="13800000000")
|
||||
history = SimpleNamespace(
|
||||
id="history-1",
|
||||
customerId="customer-1",
|
||||
itemType="wanfa-route",
|
||||
itemId="family-water",
|
||||
title="亲子玩水",
|
||||
image="https://example.test/family-water.jpg",
|
||||
visitedAt=datetime(2026, 8, 26, 12, 0),
|
||||
)
|
||||
db = FakeDb(scalar_results=[[history]], scalar_values=[1])
|
||||
app = authenticated_app(db, customer)
|
||||
|
||||
try:
|
||||
response = TestClient(app).get(
|
||||
"/api/public/customer/browse-history?pageNum=1&pageSize=20"
|
||||
)
|
||||
finally:
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["data"] == {
|
||||
"items": [
|
||||
{
|
||||
"id": "history-1",
|
||||
"itemType": "wanfa-route",
|
||||
"itemId": "family-water",
|
||||
"title": "亲子玩水",
|
||||
"image": "https://example.test/family-water.jpg",
|
||||
"visitedAt": "2026-08-26T12:00:00",
|
||||
}
|
||||
],
|
||||
"total": 1,
|
||||
"pageNum": 1,
|
||||
"pageSize": 20,
|
||||
}
|
||||
|
||||
|
||||
def test_public_browse_history_upserts_a_valid_route_without_accepting_a_customer_id():
|
||||
customer = Customer(id="customer-1", phone="13800000000")
|
||||
route = WanfaRoute(
|
||||
id="family-water",
|
||||
categoryId="family-route",
|
||||
title="亲子玩水",
|
||||
subtitle="贵州·轻松节奏与自然课堂",
|
||||
image="https://example.test/family-water.jpg",
|
||||
routeCount=4,
|
||||
demandKeyword="亲子玩水",
|
||||
sortOrder=0,
|
||||
createdAt=datetime(2026, 1, 1),
|
||||
updatedAt=datetime(2026, 1, 2),
|
||||
)
|
||||
db = FakeDb(scalar_values=[route, None])
|
||||
app = authenticated_app(db, customer)
|
||||
|
||||
try:
|
||||
response = TestClient(app).post(
|
||||
"/api/public/customer/browse-history",
|
||||
json={
|
||||
"customerId": "customer-other",
|
||||
"itemType": "wanfa-route",
|
||||
"itemId": "family-water",
|
||||
},
|
||||
)
|
||||
finally:
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
assert response.status_code == 201
|
||||
history = db.added[0]
|
||||
assert history.customerId == "customer-1"
|
||||
assert history.itemType == "wanfa-route"
|
||||
assert history.itemId == "family-water"
|
||||
assert history.title == "亲子玩水"
|
||||
assert history.image == "https://example.test/family-water.jpg"
|
||||
assert response.json()["data"]["itemId"] == "family-water"
|
||||
|
||||
|
||||
def test_public_browse_history_updates_existing_item_instead_of_creating_a_duplicate():
|
||||
customer = Customer(id="customer-1", phone="13800000000")
|
||||
route = WanfaRoute(
|
||||
id="family-water",
|
||||
categoryId="family-route",
|
||||
title="亲子玩水·更新",
|
||||
subtitle="贵州·轻松节奏与自然课堂",
|
||||
image="https://example.test/family-water-new.jpg",
|
||||
routeCount=4,
|
||||
demandKeyword="亲子玩水",
|
||||
sortOrder=0,
|
||||
createdAt=datetime(2026, 1, 1),
|
||||
updatedAt=datetime(2026, 1, 2),
|
||||
)
|
||||
history = SimpleNamespace(
|
||||
id="history-1",
|
||||
customerId="customer-1",
|
||||
itemType="wanfa-route",
|
||||
itemId="family-water",
|
||||
title="亲子玩水",
|
||||
image="https://example.test/family-water-old.jpg",
|
||||
visitedAt=datetime(2026, 8, 25, 12, 0),
|
||||
)
|
||||
db = FakeDb(scalar_values=[route, history])
|
||||
app = authenticated_app(db, customer)
|
||||
|
||||
try:
|
||||
response = TestClient(app).post(
|
||||
"/api/public/customer/browse-history",
|
||||
json={"itemType": "wanfa-route", "itemId": "family-water"},
|
||||
)
|
||||
finally:
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
assert response.status_code == 201
|
||||
assert db.added == []
|
||||
assert history.title == "亲子玩水·更新"
|
||||
assert history.image == "https://example.test/family-water-new.jpg"
|
||||
assert history.visitedAt > datetime(2026, 8, 25, 12, 0)
|
||||
Reference in new issue
Block a user