forked from erp-dev/erp
742 lines
30 KiB
Python
742 lines
30 KiB
Python
from datetime import datetime
|
|
|
|
from django.core.cache import cache
|
|
from django.contrib.auth import get_user_model
|
|
from django.contrib.auth.models import Permission
|
|
from django.contrib.contenttypes.models import ContentType
|
|
from django.test import TestCase
|
|
from django.utils import timezone
|
|
from rest_framework.test import APIClient
|
|
|
|
from basic_info import models as basic_models
|
|
from mission import models as mission_models
|
|
from printing import models as printing_models
|
|
|
|
|
|
def grant_permission(user, codename):
|
|
permission = Permission.objects.get(codename=codename)
|
|
user.user_permissions.add(permission)
|
|
|
|
|
|
class MissionV2APITest(TestCase):
|
|
def setUp(self):
|
|
cache.clear()
|
|
self.client = APIClient()
|
|
self.merchant = basic_models.Merchant.objects.create(
|
|
name="任务商户",
|
|
type=basic_models.MerchantTypeEnum.STORE,
|
|
)
|
|
self.other_merchant = basic_models.Merchant.objects.create(
|
|
name="其他商户",
|
|
type=basic_models.MerchantTypeEnum.STORE,
|
|
)
|
|
self.user = get_user_model().objects.create_user(username="mission-user", password="pass12345")
|
|
self.employee = basic_models.Employee.objects.create(
|
|
merchant=self.merchant,
|
|
sys_user=self.user,
|
|
name="任务员工",
|
|
)
|
|
self.default_category = mission_models.MissionCategory.objects.create(
|
|
merchant=self.merchant,
|
|
name="通用",
|
|
)
|
|
self.followup_category = mission_models.MissionCategory.objects.create(
|
|
merchant=self.merchant,
|
|
name="跟进",
|
|
)
|
|
self.other_category = mission_models.MissionCategory.objects.create(
|
|
merchant=self.other_merchant,
|
|
name="通用",
|
|
)
|
|
self.participant = basic_models.Employee.objects.create(
|
|
merchant=self.merchant,
|
|
name="参与者",
|
|
)
|
|
self.other_employee = basic_models.Employee.objects.create(
|
|
merchant=self.other_merchant,
|
|
name="其他员工",
|
|
)
|
|
self.customer = basic_models.Customer.objects.create(
|
|
merchant=self.merchant,
|
|
name="任务客户",
|
|
mobile="13800138000",
|
|
)
|
|
self.other_customer = basic_models.Customer.objects.create(
|
|
merchant=self.other_merchant,
|
|
name="其他客户",
|
|
mobile="13800138001",
|
|
)
|
|
self.printing_order = printing_models.PrintingOrder.objects.create(
|
|
merchant=self.merchant,
|
|
customer=self.customer,
|
|
fabric="测试面料",
|
|
width="150cm",
|
|
created_by=self.user,
|
|
)
|
|
self.other_printing_order = printing_models.PrintingOrder.objects.create(
|
|
merchant=self.other_merchant,
|
|
customer=self.other_customer,
|
|
fabric="其他面料",
|
|
width="160cm",
|
|
created_by=self.user,
|
|
)
|
|
self.printing_order_content_type = ContentType.objects.get_for_model(printing_models.PrintingOrder)
|
|
self.client.force_authenticate(user=self.user)
|
|
|
|
def test_create_mission_uses_default_status_and_current_employee(self):
|
|
resp = self.client.post(
|
|
"/api/v2/missions/",
|
|
{
|
|
"description": "跟进客户问题",
|
|
"extra": {"source": "api-test", "priority": 1},
|
|
"participant_ids": [self.participant.id],
|
|
},
|
|
format="json",
|
|
)
|
|
|
|
self.assertEqual(resp.status_code, 201)
|
|
mission = mission_models.Mission.objects.get(id=resp.data["id"])
|
|
self.assertEqual(mission.merchant, self.merchant)
|
|
self.assertEqual(mission.creator, self.employee)
|
|
self.assertEqual(mission.category, self.default_category)
|
|
self.assertFalse(mission.is_urgent)
|
|
self.assertFalse(mission.is_completed)
|
|
self.assertFalse(mission.is_cancelled)
|
|
self.assertFalse(mission.notify_if_unreplied)
|
|
self.assertEqual(mission.unreplied_notify_max_count, 5)
|
|
self.assertEqual(mission.extra, {"source": "api-test", "priority": 1})
|
|
self.assertEqual(resp.data["category"], self.default_category.id)
|
|
self.assertEqual(resp.data["category_name"], "通用")
|
|
self.assertEqual(resp.data["extra"], {"source": "api-test", "priority": 1})
|
|
self.assertEqual(list(mission.participants.values_list("employee_id", flat=True)), [self.participant.id])
|
|
|
|
def test_create_mission_supports_unreplied_notification_fields(self):
|
|
resp = self.client.post(
|
|
"/api/v2/missions/",
|
|
{
|
|
"description": "需要未回复提醒",
|
|
"notify_if_unreplied": True,
|
|
"unreplied_notify_interval_minutes": 30,
|
|
},
|
|
format="json",
|
|
)
|
|
|
|
self.assertEqual(resp.status_code, 201)
|
|
self.assertTrue(resp.data["notify_if_unreplied"])
|
|
self.assertEqual(resp.data["unreplied_notify_interval_minutes"], 30)
|
|
self.assertEqual(resp.data["unreplied_notify_max_count"], 5)
|
|
self.assertEqual(resp.data["unreplied_notify_sent_count"], 0)
|
|
|
|
def test_create_mission_rejects_missing_interval_when_unreplied_notification_enabled(self):
|
|
resp = self.client.post(
|
|
"/api/v2/missions/",
|
|
{
|
|
"description": "缺少提醒间隔",
|
|
"notify_if_unreplied": True,
|
|
},
|
|
format="json",
|
|
)
|
|
|
|
self.assertEqual(resp.status_code, 400)
|
|
self.assertIn("unreplied_notify_interval_minutes", resp.data)
|
|
|
|
def test_create_mission_rejects_status_fields(self):
|
|
resp = self.client.post(
|
|
"/api/v2/missions/",
|
|
{
|
|
"description": "非法状态",
|
|
"is_completed": True,
|
|
},
|
|
format="json",
|
|
)
|
|
|
|
self.assertEqual(resp.status_code, 400)
|
|
self.assertIn("is_completed", resp.data)
|
|
|
|
def test_create_mission_rejects_cross_merchant_category(self):
|
|
resp = self.client.post(
|
|
"/api/v2/missions/",
|
|
{
|
|
"description": "非法分类",
|
|
"category": self.other_category.id,
|
|
},
|
|
format="json",
|
|
)
|
|
|
|
self.assertEqual(resp.status_code, 400)
|
|
self.assertIn("category", resp.data)
|
|
|
|
def test_patch_mission_rejects_status_fields(self):
|
|
mission = self._create_mission()
|
|
|
|
resp = self.client.patch(
|
|
f"/api/v2/missions/{mission.id}/",
|
|
{"is_cancelled": True},
|
|
format="json",
|
|
)
|
|
|
|
self.assertEqual(resp.status_code, 400)
|
|
mission.refresh_from_db()
|
|
self.assertFalse(mission.is_cancelled)
|
|
|
|
def test_patch_mission_rejects_is_urgent(self):
|
|
mission = self._create_mission()
|
|
|
|
resp = self.client.patch(
|
|
f"/api/v2/missions/{mission.id}/",
|
|
{"is_urgent": True},
|
|
format="json",
|
|
)
|
|
|
|
self.assertEqual(resp.status_code, 400)
|
|
mission.refresh_from_db()
|
|
self.assertFalse(mission.is_urgent)
|
|
|
|
def test_patch_mission_updates_normal_fields_and_participants(self):
|
|
mission = self._create_mission()
|
|
|
|
resp = self.client.patch(
|
|
f"/api/v2/missions/{mission.id}/",
|
|
{
|
|
"description": "更新后的任务",
|
|
"category": self.followup_category.id,
|
|
"extra": {"channel": "wechat"},
|
|
"participant_ids": [self.participant.id],
|
|
},
|
|
format="json",
|
|
)
|
|
|
|
self.assertEqual(resp.status_code, 200)
|
|
mission.refresh_from_db()
|
|
self.assertEqual(mission.description, "更新后的任务")
|
|
self.assertEqual(mission.category, self.followup_category)
|
|
self.assertEqual(mission.extra, {"channel": "wechat"})
|
|
self.assertEqual(resp.data["category"], self.followup_category.id)
|
|
self.assertEqual(resp.data["category_name"], "跟进")
|
|
self.assertEqual(resp.data["extra"], {"channel": "wechat"})
|
|
self.assertEqual(list(mission.participants.values_list("employee_id", flat=True)), [self.participant.id])
|
|
|
|
def test_create_mission_reply_supports_extra(self):
|
|
mission = self._create_mission()
|
|
|
|
resp = self.client.post(
|
|
f"/api/v2/missions/{mission.id}/replies/",
|
|
{"content": "已记录", "extra": {"attachment_ids": [1, 2]}},
|
|
format="json",
|
|
)
|
|
|
|
self.assertEqual(resp.status_code, 201)
|
|
reply = mission_models.MissionReply.objects.get(id=resp.data["id"])
|
|
self.assertEqual(reply.extra, {"attachment_ids": [1, 2]})
|
|
self.assertEqual(resp.data["extra"], {"attachment_ids": [1, 2]})
|
|
|
|
def test_patch_mission_updates_unreplied_notification_fields(self):
|
|
mission = self._create_mission()
|
|
|
|
resp = self.client.patch(
|
|
f"/api/v2/missions/{mission.id}/",
|
|
{
|
|
"notify_if_unreplied": True,
|
|
"unreplied_notify_interval_minutes": 12,
|
|
"unreplied_notify_max_count": 7,
|
|
},
|
|
format="json",
|
|
)
|
|
|
|
self.assertEqual(resp.status_code, 200)
|
|
mission.refresh_from_db()
|
|
self.assertTrue(mission.notify_if_unreplied)
|
|
self.assertEqual(mission.unreplied_notify_interval_minutes, 12)
|
|
self.assertEqual(mission.unreplied_notify_max_count, 7)
|
|
|
|
def test_list_only_returns_current_merchant_missions(self):
|
|
visible = self._create_mission(description="可见任务")
|
|
mission_models.Mission.objects.create(
|
|
merchant=self.other_merchant,
|
|
category=self.other_category,
|
|
creator=self.other_employee,
|
|
description="不可见任务",
|
|
)
|
|
|
|
resp = self.client.get("/api/v2/missions/")
|
|
|
|
self.assertEqual(resp.status_code, 200)
|
|
self.assertEqual([item["id"] for item in resp.data["results"]], [visible.id])
|
|
self.assertEqual(resp.data["count"], 1)
|
|
|
|
def test_list_missions_supports_limit_offset_pagination(self):
|
|
_first = self._create_mission(description="第一条任务")
|
|
second = self._create_mission(description="第二条任务")
|
|
|
|
resp = self.client.get("/api/v2/missions/?limit=1&offset=0")
|
|
|
|
self.assertEqual(resp.status_code, 200)
|
|
self.assertEqual(resp.data["count"], 2)
|
|
self.assertEqual(len(resp.data["results"]), 1)
|
|
self.assertEqual(resp.data["results"][0]["id"], second.id)
|
|
self.assertIsNotNone(resp.data["next"])
|
|
|
|
def test_my_only_returns_related_missions_by_default(self):
|
|
created_by_me = self._create_mission(description="我创建的未完结任务")
|
|
participant_mission = mission_models.Mission.objects.create(
|
|
merchant=self.merchant,
|
|
category=self.default_category,
|
|
creator=self.participant,
|
|
description="我参与的未完结任务",
|
|
)
|
|
mission_models.MissionParticipant.objects.create(
|
|
merchant=self.merchant,
|
|
mission=participant_mission,
|
|
employee=self.employee,
|
|
)
|
|
unrelated = mission_models.Mission.objects.create(
|
|
merchant=self.merchant,
|
|
category=self.default_category,
|
|
creator=self.participant,
|
|
description="同商户但与我无关",
|
|
)
|
|
completed = self._create_mission(description="已完成任务")
|
|
completed.is_completed = True
|
|
completed.save(update_fields=["is_completed", "updated_at"])
|
|
cancelled = self._create_mission(description="已取消任务")
|
|
cancelled.is_cancelled = True
|
|
cancelled.save(update_fields=["is_cancelled", "updated_at"])
|
|
cross_merchant = mission_models.Mission.objects.create(
|
|
merchant=self.other_merchant,
|
|
category=self.other_category,
|
|
creator=self.other_employee,
|
|
description="跨商户任务",
|
|
)
|
|
|
|
resp = self.client.get("/api/v2/missions/my/")
|
|
|
|
self.assertEqual(resp.status_code, 200)
|
|
ids = {item["id"] for item in resp.data["results"]}
|
|
self.assertEqual(ids, {created_by_me.id, participant_mission.id, completed.id, cancelled.id})
|
|
self.assertNotIn(unrelated.id, ids)
|
|
self.assertNotIn(cross_merchant.id, ids)
|
|
self.assertEqual(resp.data["count"], 4)
|
|
|
|
def test_my_supports_status_urgent_created_by_me_created_at_range_and_pagination(self):
|
|
tz = timezone.get_current_timezone()
|
|
old_time = timezone.make_aware(datetime(2026, 7, 2, 10, 0, 0), tz)
|
|
day_time = timezone.make_aware(datetime(2026, 7, 3, 23, 30, 0), tz)
|
|
next_day_time = timezone.make_aware(datetime(2026, 7, 4, 0, 0, 0), tz)
|
|
|
|
old_urgent = self._create_mission(description="更早的紧急任务")
|
|
old_urgent.is_urgent = True
|
|
old_urgent.save(update_fields=["is_urgent", "updated_at"])
|
|
in_range_urgent = self._create_mission(description="当天紧急任务")
|
|
in_range_urgent.is_urgent = True
|
|
in_range_urgent.save(update_fields=["is_urgent", "updated_at"])
|
|
in_range_normal = self._create_mission(description="当天普通任务")
|
|
next_day_urgent = self._create_mission(description="次日紧急任务")
|
|
next_day_urgent.is_urgent = True
|
|
next_day_urgent.save(update_fields=["is_urgent", "updated_at"])
|
|
completed_urgent = self._create_mission(description="当天已完成紧急任务")
|
|
completed_urgent.is_urgent = True
|
|
completed_urgent.is_completed = True
|
|
completed_urgent.save(update_fields=["is_urgent", "is_completed", "updated_at"])
|
|
|
|
mission_models.Mission.objects.filter(id=old_urgent.id).update(created_at=old_time)
|
|
mission_models.Mission.objects.filter(id=in_range_urgent.id).update(created_at=day_time)
|
|
mission_models.Mission.objects.filter(id=in_range_normal.id).update(created_at=day_time)
|
|
mission_models.Mission.objects.filter(id=next_day_urgent.id).update(created_at=next_day_time)
|
|
mission_models.Mission.objects.filter(id=completed_urgent.id).update(created_at=day_time)
|
|
|
|
resp = self.client.get(
|
|
"/api/v2/missions/my/?created_by_me=true&is_completed=false&is_cancelled=false&is_urgent=true"
|
|
"&created_at_from=2026-07-03&created_at_to=2026-07-03&limit=1&offset=0"
|
|
)
|
|
|
|
self.assertEqual(resp.status_code, 200)
|
|
self.assertEqual(resp.data["count"], 1)
|
|
self.assertEqual([item["id"] for item in resp.data["results"]], [in_range_urgent.id])
|
|
|
|
def test_my_status_counts_groups_related_missions_without_duplicates(self):
|
|
open_created = self._create_mission(description="我创建的未完成任务")
|
|
mission_models.MissionParticipant.objects.create(
|
|
merchant=self.merchant,
|
|
mission=open_created,
|
|
employee=self.employee,
|
|
)
|
|
mission_models.MissionParticipant.objects.create(
|
|
merchant=self.merchant,
|
|
mission=open_created,
|
|
employee=self.participant,
|
|
)
|
|
|
|
completed_created = self._create_mission(description="我创建的已完成任务")
|
|
completed_created.is_completed = True
|
|
completed_created.save(update_fields=["is_completed", "updated_at"])
|
|
|
|
cancelled_participant = mission_models.Mission.objects.create(
|
|
merchant=self.merchant,
|
|
category=self.default_category,
|
|
creator=self.participant,
|
|
description="我参与的已取消任务",
|
|
is_cancelled=True,
|
|
)
|
|
mission_models.MissionParticipant.objects.create(
|
|
merchant=self.merchant,
|
|
mission=cancelled_participant,
|
|
employee=self.employee,
|
|
)
|
|
|
|
mission_models.Mission.objects.create(
|
|
merchant=self.merchant,
|
|
category=self.default_category,
|
|
creator=self.participant,
|
|
description="同商户但与我无关",
|
|
)
|
|
mission_models.Mission.objects.create(
|
|
merchant=self.other_merchant,
|
|
category=self.other_category,
|
|
creator=self.other_employee,
|
|
description="跨商户任务",
|
|
is_completed=True,
|
|
)
|
|
|
|
resp = self.client.get("/api/v2/missions/my/status-counts/")
|
|
|
|
self.assertEqual(resp.status_code, 200)
|
|
self.assertEqual(resp.data, {"total": 3, "open": 1, "completed": 1, "cancelled": 1})
|
|
|
|
def test_list_missions_by_printing_order_returns_nested_replies_and_extra(self):
|
|
mission = mission_models.Mission.objects.create(
|
|
merchant=self.merchant,
|
|
category=self.default_category,
|
|
creator=self.employee,
|
|
description="印花订单任务",
|
|
content_type=self.printing_order_content_type,
|
|
content_id=self.printing_order.id,
|
|
extra={"source": "printing-order"},
|
|
)
|
|
mission_models.MissionReply.objects.create(
|
|
merchant=self.merchant,
|
|
mission=mission,
|
|
responder=self.employee,
|
|
content="已跟进",
|
|
extra={"attachment_ids": [101]},
|
|
)
|
|
mission_models.Mission.objects.create(
|
|
merchant=self.merchant,
|
|
category=self.followup_category,
|
|
creator=self.employee,
|
|
description="其他订单任务",
|
|
content_type=self.printing_order_content_type,
|
|
content_id=self.other_printing_order.id,
|
|
)
|
|
|
|
resp = self.client.get(f"/api/v2/missions/by-printing-order/{self.printing_order.id}/")
|
|
|
|
self.assertEqual(resp.status_code, 200)
|
|
self.assertEqual(len(resp.data), 1)
|
|
self.assertEqual(resp.data[0]["id"], mission.id)
|
|
self.assertEqual(resp.data[0]["extra"], {"source": "printing-order"})
|
|
self.assertEqual(len(resp.data[0]["replies"]), 1)
|
|
self.assertEqual(resp.data[0]["replies"][0]["content"], "已跟进")
|
|
self.assertEqual(resp.data[0]["replies"][0]["extra"], {"attachment_ids": [101]})
|
|
|
|
def test_list_missions_by_printing_order_supports_category_filter(self):
|
|
mission_models.Mission.objects.create(
|
|
merchant=self.merchant,
|
|
category=self.default_category,
|
|
creator=self.employee,
|
|
description="通用任务",
|
|
content_type=self.printing_order_content_type,
|
|
content_id=self.printing_order.id,
|
|
)
|
|
followup_mission = mission_models.Mission.objects.create(
|
|
merchant=self.merchant,
|
|
category=self.followup_category,
|
|
creator=self.employee,
|
|
description="跟进任务",
|
|
content_type=self.printing_order_content_type,
|
|
content_id=self.printing_order.id,
|
|
)
|
|
|
|
resp = self.client.get(
|
|
f"/api/v2/missions/by-printing-order/{self.printing_order.id}/?category_ids={self.followup_category.id}"
|
|
)
|
|
|
|
self.assertEqual(resp.status_code, 200)
|
|
self.assertEqual([item["id"] for item in resp.data], [followup_mission.id])
|
|
|
|
def test_list_missions_by_printing_order_supports_lightweight_mode(self):
|
|
mission = mission_models.Mission.objects.create(
|
|
merchant=self.merchant,
|
|
category=self.default_category,
|
|
creator=self.employee,
|
|
description="轻量任务",
|
|
content_type=self.printing_order_content_type,
|
|
content_id=self.printing_order.id,
|
|
)
|
|
mission_models.MissionParticipant.objects.create(
|
|
merchant=self.merchant,
|
|
mission=mission,
|
|
employee=self.participant,
|
|
)
|
|
mission_models.MissionReply.objects.create(
|
|
merchant=self.merchant,
|
|
mission=mission,
|
|
responder=self.employee,
|
|
content="已处理",
|
|
ends_task=True,
|
|
)
|
|
|
|
resp = self.client.get(
|
|
f"/api/v2/missions/by-printing-order/{self.printing_order.id}/?include_details=false"
|
|
)
|
|
|
|
self.assertEqual(resp.status_code, 200)
|
|
self.assertEqual(len(resp.data), 1)
|
|
self.assertEqual(resp.data[0]["id"], mission.id)
|
|
self.assertNotIn("participants", resp.data[0])
|
|
self.assertNotIn("has_ending_reply", resp.data[0])
|
|
self.assertNotIn("can_reply", resp.data[0])
|
|
self.assertNotIn("replies", resp.data[0])
|
|
|
|
def test_list_missions_by_printing_order_rejects_invalid_include_details(self):
|
|
resp = self.client.get(
|
|
f"/api/v2/missions/by-printing-order/{self.printing_order.id}/?include_details=maybe"
|
|
)
|
|
|
|
self.assertEqual(resp.status_code, 400)
|
|
self.assertIn("include_details", resp.data)
|
|
|
|
def test_list_missions_by_printing_order_uses_separate_cache_by_detail_mode(self):
|
|
mission = mission_models.Mission.objects.create(
|
|
merchant=self.merchant,
|
|
category=self.default_category,
|
|
creator=self.employee,
|
|
description="缓存任务",
|
|
content_type=self.printing_order_content_type,
|
|
content_id=self.printing_order.id,
|
|
)
|
|
mission_models.MissionReply.objects.create(
|
|
merchant=self.merchant,
|
|
mission=mission,
|
|
responder=self.employee,
|
|
content="首次回复",
|
|
)
|
|
|
|
detailed_resp = self.client.get(f"/api/v2/missions/by-printing-order/{self.printing_order.id}/")
|
|
lightweight_resp = self.client.get(
|
|
f"/api/v2/missions/by-printing-order/{self.printing_order.id}/?include_details=false"
|
|
)
|
|
|
|
mission_models.MissionReply.objects.create(
|
|
merchant=self.merchant,
|
|
mission=mission,
|
|
responder=self.employee,
|
|
content="后续回复",
|
|
)
|
|
|
|
detailed_cached_resp = self.client.get(f"/api/v2/missions/by-printing-order/{self.printing_order.id}/")
|
|
lightweight_cached_resp = self.client.get(
|
|
f"/api/v2/missions/by-printing-order/{self.printing_order.id}/?include_details=false"
|
|
)
|
|
|
|
self.assertEqual(len(detailed_resp.data[0]["replies"]), 1)
|
|
self.assertEqual(len(detailed_cached_resp.data[0]["replies"]), 1)
|
|
self.assertNotIn("replies", lightweight_resp.data[0])
|
|
self.assertNotIn("replies", lightweight_cached_resp.data[0])
|
|
|
|
def test_list_missions_by_printing_order_rejects_cross_merchant_order(self):
|
|
resp = self.client.get(f"/api/v2/missions/by-printing-order/{self.other_printing_order.id}/")
|
|
|
|
self.assertEqual(resp.status_code, 404)
|
|
|
|
def test_delete_mission_is_not_allowed(self):
|
|
mission = self._create_mission()
|
|
|
|
resp = self.client.delete(f"/api/v2/missions/{mission.id}/")
|
|
|
|
self.assertEqual(resp.status_code, 405)
|
|
|
|
def test_reopen_mission_requires_permission(self):
|
|
mission = self._create_mission()
|
|
|
|
reply_resp = self.client.post(
|
|
f"/api/v2/missions/{mission.id}/replies/",
|
|
{"content": "已处理", "ends_task": True},
|
|
format="json",
|
|
)
|
|
self.assertEqual(reply_resp.status_code, 201)
|
|
mission.refresh_from_db()
|
|
self.assertTrue(mission.is_completed)
|
|
|
|
reopen_resp = self.client.post(f"/api/v2/missions/{mission.id}/reopen/", {}, format="json")
|
|
self.assertEqual(reopen_resp.status_code, 403)
|
|
|
|
def test_create_ending_reply_and_reopen_mission(self):
|
|
grant_permission(self.user, "reopen_mission")
|
|
mission = self._create_mission()
|
|
|
|
reply_resp = self.client.post(
|
|
f"/api/v2/missions/{mission.id}/replies/",
|
|
{"content": "已处理", "ends_task": True},
|
|
format="json",
|
|
)
|
|
self.assertEqual(reply_resp.status_code, 201)
|
|
mission.refresh_from_db()
|
|
self.assertTrue(mission.is_completed)
|
|
|
|
reopen_resp = self.client.post(f"/api/v2/missions/{mission.id}/reopen/", {}, format="json")
|
|
self.assertEqual(reopen_resp.status_code, 200)
|
|
mission.refresh_from_db()
|
|
reply = mission_models.MissionReply.objects.get(id=reply_resp.data["id"])
|
|
self.assertFalse(mission.is_completed)
|
|
self.assertFalse(reply.ends_task)
|
|
self.assertTrue(reply.is_rejected)
|
|
self.assertEqual(reply.rejected_by, self.employee)
|
|
|
|
def test_reject_reply_requires_permission(self):
|
|
mission = self._create_mission()
|
|
reply = mission_models.MissionReply.objects.create(
|
|
merchant=self.merchant,
|
|
mission=mission,
|
|
responder=self.employee,
|
|
content="结束",
|
|
ends_task=True,
|
|
)
|
|
mission.is_completed = True
|
|
mission.save(update_fields=["is_completed", "updated_at"])
|
|
|
|
resp = self.client.post(f"/api/v2/mission-replies/{reply.id}/reject/", {}, format="json")
|
|
self.assertEqual(resp.status_code, 403)
|
|
|
|
def test_reject_reply_api_reopens_mission_when_rejecting_ending_reply(self):
|
|
grant_permission(self.user, "reject_mission_reply")
|
|
mission = self._create_mission()
|
|
reply = mission_models.MissionReply.objects.create(
|
|
merchant=self.merchant,
|
|
mission=mission,
|
|
responder=self.employee,
|
|
content="结束",
|
|
ends_task=True,
|
|
)
|
|
mission.is_completed = True
|
|
mission.save(update_fields=["is_completed", "updated_at"])
|
|
|
|
resp = self.client.post(f"/api/v2/mission-replies/{reply.id}/reject/", {}, format="json")
|
|
self.assertEqual(resp.status_code, 200)
|
|
mission.refresh_from_db()
|
|
reply.refresh_from_db()
|
|
self.assertFalse(mission.is_completed)
|
|
self.assertTrue(reply.is_rejected)
|
|
self.assertEqual(reply.rejected_by, self.employee)
|
|
|
|
def test_cancel_mission_api_records_cancelled_by(self):
|
|
mission = self._create_mission()
|
|
|
|
resp = self.client.post(f"/api/v2/missions/{mission.id}/cancel/", {}, format="json")
|
|
|
|
self.assertEqual(resp.status_code, 200)
|
|
mission.refresh_from_db()
|
|
self.assertTrue(mission.is_cancelled)
|
|
self.assertEqual(mission.cancelled_by, self.employee)
|
|
self.assertIsNotNone(mission.cancelled_at)
|
|
|
|
def test_set_urgent_api_updates_urgent_status(self):
|
|
mission = self._create_mission()
|
|
|
|
resp = self.client.post(
|
|
f"/api/v2/missions/{mission.id}/set-urgent/",
|
|
{"is_urgent": True},
|
|
format="json",
|
|
)
|
|
|
|
self.assertEqual(resp.status_code, 200)
|
|
mission.refresh_from_db()
|
|
self.assertTrue(mission.is_urgent)
|
|
|
|
def test_cross_merchant_mission_detail_returns_404(self):
|
|
mission = mission_models.Mission.objects.create(
|
|
merchant=self.other_merchant,
|
|
category=self.other_category,
|
|
creator=self.other_employee,
|
|
description="其他商户任务",
|
|
)
|
|
|
|
resp = self.client.get(f"/api/v2/missions/{mission.id}/")
|
|
|
|
self.assertEqual(resp.status_code, 404)
|
|
|
|
def test_cross_merchant_participant_is_rejected(self):
|
|
resp = self.client.post(
|
|
"/api/v2/missions/",
|
|
{
|
|
"description": "跨商户参与者",
|
|
"participant_ids": [self.other_employee.id],
|
|
},
|
|
format="json",
|
|
)
|
|
|
|
self.assertEqual(resp.status_code, 400)
|
|
|
|
def test_mission_category_crud(self):
|
|
list_resp = self.client.get("/api/v2/mission-categories/")
|
|
self.assertEqual(list_resp.status_code, 200)
|
|
self.assertEqual(
|
|
[item["name"] for item in list_resp.data],
|
|
["通用", "跟进"],
|
|
)
|
|
self.assertEqual(list_resp.data[0]["payload_processor"], "")
|
|
self.assertFalse(list_resp.data[0]["speech_enabled"])
|
|
|
|
create_resp = self.client.post(
|
|
"/api/v2/mission-categories/",
|
|
{"name": "售后", "payload_processor": "structured_description_v1", "speech_enabled": True},
|
|
format="json",
|
|
)
|
|
self.assertEqual(create_resp.status_code, 201)
|
|
category_id = create_resp.data["id"]
|
|
self.assertEqual(create_resp.data["payload_processor"], "structured_description_v1")
|
|
self.assertTrue(create_resp.data["speech_enabled"])
|
|
|
|
detail_resp = self.client.get(f"/api/v2/mission-categories/{category_id}/")
|
|
self.assertEqual(detail_resp.status_code, 200)
|
|
self.assertEqual(detail_resp.data["name"], "售后")
|
|
self.assertEqual(detail_resp.data["payload_processor"], "structured_description_v1")
|
|
self.assertTrue(detail_resp.data["speech_enabled"])
|
|
|
|
patch_resp = self.client.patch(
|
|
f"/api/v2/mission-categories/{category_id}/",
|
|
{"name": "售后跟进", "payload_processor": "", "speech_enabled": False},
|
|
format="json",
|
|
)
|
|
self.assertEqual(patch_resp.status_code, 200)
|
|
self.assertEqual(patch_resp.data["name"], "售后跟进")
|
|
self.assertEqual(patch_resp.data["payload_processor"], "")
|
|
self.assertFalse(patch_resp.data["speech_enabled"])
|
|
|
|
delete_resp = self.client.delete(f"/api/v2/mission-categories/{category_id}/")
|
|
self.assertEqual(delete_resp.status_code, 204)
|
|
|
|
def test_mission_category_rejects_unknown_payload_processor(self):
|
|
resp = self.client.post(
|
|
"/api/v2/mission-categories/",
|
|
{"name": "售后", "payload_processor": "unknown_processor"},
|
|
format="json",
|
|
)
|
|
|
|
self.assertEqual(resp.status_code, 400)
|
|
self.assertIn("payload_processor", resp.data)
|
|
|
|
def test_delete_used_mission_category_is_rejected(self):
|
|
mission = self._create_mission()
|
|
|
|
resp = self.client.delete(f"/api/v2/mission-categories/{mission.category_id}/")
|
|
|
|
self.assertEqual(resp.status_code, 400)
|
|
|
|
def _create_mission(self, description="测试任务"):
|
|
return mission_models.Mission.objects.create(
|
|
merchant=self.merchant,
|
|
category=self.default_category,
|
|
creator=self.employee,
|
|
description=description,
|
|
)
|