forked from wangziqi/gongxue-base
feat: add student crm push
This commit is contained in:
@@ -9463,6 +9463,102 @@ async function testReferralAndCrmGrowth() {
|
||||
assert.deepEqual(crmConfig.item?.assignmentPool, [TENANT_SALES_USER_ID, TENANT_AGENT_USER_ID], 'CRM config should persist assignment pool');
|
||||
assert.ok(!JSON.stringify(crmConfig).includes('crm-secret-smoke'), 'CRM config response should not leak secret');
|
||||
|
||||
const studentCrmPush = await request('/api/tenant-admin/students/crm-push', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'POST',
|
||||
body: {
|
||||
studentUserIds: [USER_ID, SECOND_STUDENT_USER_ID],
|
||||
source: 'integration_batch',
|
||||
title: '集成测试批量跟进',
|
||||
message: '请销售今日联系并记录转化情况',
|
||||
followupType: 'sales',
|
||||
priority: 'high',
|
||||
assignedToUserId: TENANT_SALES_USER_ID,
|
||||
dueAt: '2026-07-02T10:00:00.000Z',
|
||||
idempotencyKey: 'integration-student-crm-push',
|
||||
metadata: { source: 'integration-test' },
|
||||
},
|
||||
});
|
||||
assert.equal(studentCrmPush.total, 2, 'student CRM push should report total');
|
||||
assert.equal(studentCrmPush.successCount, 2, 'student CRM push should enqueue valid students');
|
||||
assert.equal(studentCrmPush.errorCount, 0, 'student CRM push should not fail valid students');
|
||||
assert.ok(studentCrmPush.items?.every(item => item.queueId && item.followupId), 'student CRM push should return queue and follow-up ids');
|
||||
|
||||
const repeatedStudentCrmPush = await request('/api/tenant-admin/students/crm-push', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'POST',
|
||||
body: {
|
||||
studentUserIds: [USER_ID, SECOND_STUDENT_USER_ID],
|
||||
title: '集成测试批量跟进',
|
||||
message: '重复提交不应重复创建队列',
|
||||
followupType: 'sales',
|
||||
priority: 'high',
|
||||
assignedToUserId: TENANT_SALES_USER_ID,
|
||||
idempotencyKey: 'integration-student-crm-push',
|
||||
},
|
||||
});
|
||||
assert.deepEqual(
|
||||
repeatedStudentCrmPush.items?.map(item => item.queueId).sort(),
|
||||
studentCrmPush.items?.map(item => item.queueId).sort(),
|
||||
'student CRM push idempotency should reuse queue ids',
|
||||
);
|
||||
assert.deepEqual(
|
||||
repeatedStudentCrmPush.items?.map(item => item.followupId).sort(),
|
||||
studentCrmPush.items?.map(item => item.followupId).sort(),
|
||||
'student CRM push idempotency should reuse follow-up ids',
|
||||
);
|
||||
|
||||
const studentCrmPushClassError = await request('/api/tenant-admin/students/crm-push', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'POST',
|
||||
body: {
|
||||
studentUserIds: [USER_ID, SECOND_STUDENT_USER_ID],
|
||||
title: '集成测试班级范围跟进',
|
||||
classId: ids.tenantClass,
|
||||
idempotencyKey: 'integration-student-crm-push-class-scope',
|
||||
},
|
||||
});
|
||||
assert.equal(studentCrmPushClassError.total, 2, 'student CRM class scoped push should report total');
|
||||
assert.equal(studentCrmPushClassError.successCount, 1, 'student CRM class scoped push should enqueue in-class student');
|
||||
assert.ok(
|
||||
studentCrmPushClassError.errors?.some(item => item.studentUserId === SECOND_STUDENT_USER_ID && item.code === 'STUDENT_CLASS_REQUIRED'),
|
||||
'student CRM class scoped push should reject students outside the selected class',
|
||||
);
|
||||
|
||||
const operatorCrmPushDenied = await request('/api/tenant-admin/students/crm-push', {
|
||||
userId: TENANT_OPERATOR_USER_ID,
|
||||
method: 'POST',
|
||||
body: {
|
||||
studentUserIds: [USER_ID],
|
||||
title: '不应允许普通运营推送',
|
||||
},
|
||||
expectStatus: 403,
|
||||
});
|
||||
assert.equal(operatorCrmPushDenied.code, 'TENANT_PERMISSION_REQUIRED', 'student CRM push should require crm:write permission');
|
||||
|
||||
const partnerStudentCrmPushDenied = await request('/api/tenant-admin/students/crm-push', {
|
||||
tenantId: PARTNER_TENANT_ID,
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'POST',
|
||||
body: {
|
||||
studentUserIds: [USER_ID],
|
||||
title: '不应跨租户推送',
|
||||
},
|
||||
expectStatus: 403,
|
||||
});
|
||||
assert.equal(partnerStudentCrmPushDenied.code, 'TENANT_ADMIN_REQUIRED', 'student CRM push must be tenant isolated');
|
||||
|
||||
const studentCrmPushDenied = await request('/api/tenant-admin/students/crm-push', {
|
||||
userId: USER_ID,
|
||||
method: 'POST',
|
||||
body: {
|
||||
studentUserIds: [USER_ID],
|
||||
title: '学生端不应推送 CRM',
|
||||
},
|
||||
expectStatus: 403,
|
||||
});
|
||||
assert.equal(studentCrmPushDenied.code, 'TENANT_ADMIN_REQUIRED', 'student CRM push should reject student users');
|
||||
|
||||
const invalidCrmAssignment = await request('/api/crm/config', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'PUT',
|
||||
@@ -9820,6 +9916,14 @@ async function testReferralAndCrmGrowth() {
|
||||
});
|
||||
assert.ok(crmQueue.items?.some(item => item.leadId === manual.lead.item.id || item.leadId === tracked.lead.item.id), 'CRM queue should include referral lead task');
|
||||
assert.ok(crmQueue.items?.some(item => item.payload?.assignee?.id === TENANT_AGENT_USER_ID), 'CRM queue payload should include assigned follow-up user');
|
||||
assert.ok(
|
||||
crmQueue.items?.some(item => item.payload?.eventType === 'student.crm_push' && item.payload?.assignee?.userId === TENANT_SALES_USER_ID),
|
||||
'CRM queue should include student CRM push task',
|
||||
);
|
||||
assert.ok(
|
||||
crmQueue.items?.some(item => item.payload?.eventType === 'student.crm_push' && item.payload?.student?.avatarPreset && !item.payload?.student?.avatarUrl),
|
||||
'student CRM push payload should use preset avatars only',
|
||||
);
|
||||
assert.ok(!JSON.stringify(crmQueue).includes('crm-secret-smoke'), 'CRM queue should not leak secret');
|
||||
|
||||
const partnerReferralDenied = await request('/api/referral/sales-stats', {
|
||||
|
||||
Reference in New Issue
Block a user