feat(client): A3 客户与行业目录(两级行业、客户组、字段审批)
背景: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;真实浏览器六页验证通过。
This commit is contained in:
1 parent
8626996bb1
commit
140dc386c1
54 files changed
+9018
-147
No files matched your search
@@ -0,0 +1,192 @@
|
||||
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' });
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user