背景:A3 需把行业 / 客户 / 客户组 / 客户级字段立为资产根节点;用户确认两级行业树与混搭 UI(行业 B / 客户列表 A / 详情 A / 新建 B / 客户组 B / 字段 A)。 改动: - 新增 ADR-002,行业改为两级树,客户挂末级行业;同步 PRD / 技术架构 / 开发计划。 - 新增 client 模块:industries / customers / customer-groups / customer-fields 接口、真实 PrismaCustomerDirectory、客户状态机与客户转交、客户组数据范围解析、跨客户 404。 - 新增迁移与幂等种子(电商/金融两级行业、2 客户、1 客户组、4 条字段)。 - 前端新增行业/客户/客户组/字段审批页面与路由、导航更名(客户管理 / 行业管理 / 客户组),复用现有 token。 - 补充 shared 契约单测、client 集成用例、A3 浏览器 e2e。 影响面:client 模块与 client 表、shared 契约、web 路由/导航、ScopeContext 数据范围、OpenAPI 与种子;A1 客户转交占位由 client 实现替换。 回滚:回滚本提交;数据库保留新增表或按报告执行 down SQL;CUSTOMER_DIRECTORY 可切回空集实现。 验证:format:check / lint / typecheck / test 全绿(shared 131 / api 90 / web 125,共 346);test:int 5 文件 39 条;A3 e2e 1/1;build 8/8;真实浏览器六页验证通过。
193 lines
7.1 KiB
TypeScript
193 lines
7.1 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
||
|
||
import {
|
||
applyCustomerFieldInputSchema,
|
||
approveCustomerFieldInputSchema,
|
||
createCustomerGroupInputSchema,
|
||
createCustomerInputSchema,
|
||
createIndustryInputSchema,
|
||
customerFieldCodeSchema,
|
||
customerFieldListQuerySchema,
|
||
customerGroupListQuerySchema,
|
||
customerListQuerySchema,
|
||
customerNameSchema,
|
||
customerShortNameSchema,
|
||
industryListQuerySchema,
|
||
setCustomerGroupsInputSchema,
|
||
setCustomerStatusInputSchema,
|
||
setIndustryStatusInputSchema,
|
||
transferCustomerInputSchema,
|
||
updateCustomerGroupInputSchema,
|
||
updateCustomerGroupMembersInputSchema,
|
||
updateCustomerInputSchema,
|
||
updateIndustryInputSchema,
|
||
} from './client.js';
|
||
|
||
const ULID = '01HZZZZZZZZZZZZZZZZZZZZZZ1';
|
||
const ULID2 = '01HZZZZZZZZZZZZZZZZZZZZZZ2';
|
||
|
||
describe('行业契约', () => {
|
||
it('新建:一级 parentId 缺省、默认排序;二级必须带 parentId', () => {
|
||
expect(createIndustryInputSchema.parse({ name: '电商' })).toEqual({
|
||
name: '电商',
|
||
sortOrder: 10,
|
||
remark: '',
|
||
});
|
||
expect(createIndustryInputSchema.parse({ name: '服饰', parentId: ULID })).toMatchObject({
|
||
parentId: ULID,
|
||
});
|
||
expect(createIndustryInputSchema.safeParse({ name: '电' }).success).toBe(false);
|
||
expect(createIndustryInputSchema.safeParse({ name: '电商', unknown: 1 }).success).toBe(false);
|
||
});
|
||
|
||
it('更新 / 状态 / 列表查询:层级与上级不可改,状态枚举与默认分页', () => {
|
||
expect(updateIndustryInputSchema.safeParse({ parentId: ULID }).success).toBe(false);
|
||
expect(
|
||
updateIndustryInputSchema.parse({ name: '电商2', sortOrder: 20, remark: '说明' }),
|
||
).toEqual({
|
||
name: '电商2',
|
||
sortOrder: 20,
|
||
remark: '说明',
|
||
});
|
||
expect(setIndustryStatusInputSchema.safeParse({ status: 'disabled' }).success).toBe(true);
|
||
expect(setIndustryStatusInputSchema.safeParse({ status: 'deleted' }).success).toBe(false);
|
||
expect(industryListQuerySchema.parse({})).toEqual({ limit: 20, status: 'all' });
|
||
});
|
||
});
|
||
|
||
describe('客户契约', () => {
|
||
it('名称 / 简称:简称必填且限制字符', () => {
|
||
expect(customerNameSchema.safeParse(' ').success).toBe(false);
|
||
expect(customerShortNameSchema.safeParse('HJ').success).toBe(true);
|
||
expect(customerShortNameSchema.safeParse('花间集').success).toBe(true);
|
||
expect(customerShortNameSchema.safeParse('H').success).toBe(false);
|
||
expect(customerShortNameSchema.safeParse('HJ!').success).toBe(false);
|
||
});
|
||
|
||
it('新建:默认不写偏好与基础字段,未知字段拒绝', () => {
|
||
const parsed = createCustomerInputSchema.parse({
|
||
industryId: ULID,
|
||
name: '花间集',
|
||
shortName: 'HJ',
|
||
ownerUserId: ULID2,
|
||
});
|
||
expect(parsed.preferences).toBeUndefined();
|
||
expect(parsed.baseFields).toBeUndefined();
|
||
expect(
|
||
createCustomerInputSchema.safeParse({
|
||
industryId: ULID,
|
||
name: '花间集',
|
||
shortName: 'HJ',
|
||
ownerUserId: ULID2,
|
||
extra: true,
|
||
}).success,
|
||
).toBe(false);
|
||
});
|
||
|
||
it('新建:基础字段 ≤ 10,接受完整偏好', () => {
|
||
const baseFields = Array.from({ length: 11 }, (_, index) => ({
|
||
key: `k${index}`,
|
||
value: `v${index}`,
|
||
}));
|
||
expect(
|
||
createCustomerInputSchema.safeParse({
|
||
industryId: ULID,
|
||
name: '花间集',
|
||
shortName: 'HJ',
|
||
ownerUserId: ULID2,
|
||
baseFields,
|
||
}).success,
|
||
).toBe(false);
|
||
const parsed = createCustomerInputSchema.parse({
|
||
industryId: ULID,
|
||
name: '花间集',
|
||
shortName: 'HJ',
|
||
ownerUserId: ULID2,
|
||
preferences: { naming: 'n', directory: 'd', specs: ['1080×1920'], taboo: ['绝对化用语'] },
|
||
baseFields: [{ key: '门店', value: '沪-001' }],
|
||
});
|
||
expect(parsed.preferences?.specs).toHaveLength(1);
|
||
});
|
||
|
||
it('更新不接受所属行业;状态 / 转交 / 组 / 列表查询口径', () => {
|
||
expect(updateCustomerInputSchema.safeParse({ industryId: ULID }).success).toBe(false);
|
||
expect(updateCustomerInputSchema.parse({ shortName: 'HJ2' })).toEqual({ shortName: 'HJ2' });
|
||
expect(setCustomerStatusInputSchema.safeParse({ status: 'paused' }).success).toBe(true);
|
||
expect(setCustomerStatusInputSchema.safeParse({ status: 'deleted' }).success).toBe(false);
|
||
expect(transferCustomerInputSchema.parse({ ownerUserId: ULID })).toEqual({ ownerUserId: ULID });
|
||
expect(setCustomerGroupsInputSchema.parse({ groupIds: [ULID] })).toEqual({ groupIds: [ULID] });
|
||
expect(customerListQuerySchema.parse({})).toEqual({ limit: 20, status: 'all' });
|
||
expect(customerListQuerySchema.parse({ status: 'ended', industry_id: ULID })).toMatchObject({
|
||
status: 'ended',
|
||
industry_id: ULID,
|
||
});
|
||
});
|
||
});
|
||
|
||
describe('客户组契约', () => {
|
||
it('名称 / 说明 / 成员批量操作 / 列表查询', () => {
|
||
expect(createCustomerGroupInputSchema.parse({ name: '电商核心组' })).toEqual({
|
||
name: '电商核心组',
|
||
});
|
||
expect(createCustomerGroupInputSchema.safeParse({ name: '组' }).success).toBe(false);
|
||
expect(updateCustomerGroupInputSchema.parse({ description: '说明' })).toEqual({
|
||
description: '说明',
|
||
});
|
||
expect(updateCustomerGroupMembersInputSchema.parse({ addCustomerIds: [ULID] })).toEqual({
|
||
addCustomerIds: [ULID],
|
||
removeCustomerIds: [],
|
||
});
|
||
expect(customerGroupListQuerySchema.parse({})).toEqual({ limit: 20 });
|
||
});
|
||
});
|
||
|
||
describe('客户字段契约', () => {
|
||
it('编码格式:小写字母开头,字母数字下划线', () => {
|
||
expect(customerFieldCodeSchema.parse('campaign_code')).toBe('campaign_code');
|
||
expect(customerFieldCodeSchema.safeParse('Campaign').success).toBe(false);
|
||
expect(customerFieldCodeSchema.safeParse('1abc').success).toBe(false);
|
||
expect(customerFieldCodeSchema.safeParse('a').success).toBe(false);
|
||
});
|
||
|
||
it('申请:单选必须有选项,进入生成默认 false', () => {
|
||
const parsed = applyCustomerFieldInputSchema.parse({
|
||
name: '活动编码',
|
||
code: 'campaign_code',
|
||
type: 'text',
|
||
purpose: '检索',
|
||
});
|
||
expect(parsed.entersGeneration).toBe(false);
|
||
expect(parsed.options).toEqual([]);
|
||
expect(
|
||
applyCustomerFieldInputSchema.safeParse({
|
||
name: '系列',
|
||
code: 'series',
|
||
type: 'select',
|
||
purpose: '检索',
|
||
}).success,
|
||
).toBe(false);
|
||
expect(
|
||
applyCustomerFieldInputSchema.parse({
|
||
name: '系列',
|
||
code: 'series',
|
||
type: 'select',
|
||
options: ['秋冬'],
|
||
purpose: '检索',
|
||
entersGeneration: true,
|
||
}).entersGeneration,
|
||
).toBe(true);
|
||
});
|
||
|
||
it('审批:驳回必须带原因;列表状态枚举', () => {
|
||
expect(approveCustomerFieldInputSchema.safeParse({ decision: 'reject' }).success).toBe(false);
|
||
expect(approveCustomerFieldInputSchema.parse({ decision: 'reject', reason: '重复' })).toEqual({
|
||
decision: 'reject',
|
||
reason: '重复',
|
||
});
|
||
expect(approveCustomerFieldInputSchema.parse({ decision: 'approve' })).toEqual({
|
||
decision: 'approve',
|
||
});
|
||
expect(customerFieldListQuerySchema.parse({})).toEqual({ limit: 20, status: 'all' });
|
||
});
|
||
});
|