合并冲突的行为级裁决:develop 侧 memberMatrixCombos 曾把 热转印→烫画
归一(旧别名方案),与 v2 线上一等公民词表(8903afd)矛盾,导致 v2 侧
三个矩阵用例失败。取 v2 版 family-recompute.service.{ts,spec}(即当前
线上行为)。另修复 v2 分支 structs.md 残留的冲突标记。
Tests: api 23 suites/217 passed (runInBand), admin 27 passed, tsc clean
502 lines
22 KiB
TypeScript
502 lines
22 KiB
TypeScript
import { Test } from '@nestjs/testing';
|
||
import { Prisma } from '@prisma/client';
|
||
import {
|
||
FamilyRecomputeService,
|
||
derivePriceMatrix,
|
||
memberMatrixCombos,
|
||
} from './family-recompute.service';
|
||
import { ProductFamiliesService } from './product-families.service';
|
||
import { OrganizeService } from './organize.service';
|
||
import { PrismaService } from '../prisma/prisma.service';
|
||
|
||
/**
|
||
* 集成测试(连真实库,与 goods.service.spec.ts 同模式):
|
||
* 验证并集尺码表/包装规则裁决、五维价格矩阵推导(最低价/来源累积/维度过滤)、
|
||
* 覆盖合并、autoManaged 锁定语义与 canonical detail 初始化。
|
||
*/
|
||
describe('FamilyRecomputeService', () => {
|
||
let service: FamilyRecomputeService;
|
||
let organize!: OrganizeService;
|
||
let prisma: PrismaService;
|
||
const stamp = Date.now();
|
||
const createdOriginGoodIds: bigint[] = [];
|
||
const createdFamilyIds: bigint[] = [];
|
||
const createdTagIds: bigint[] = [];
|
||
const createdTagGroupIds: bigint[] = [];
|
||
|
||
const mkOriginGood = async (over: {
|
||
goodName?: string;
|
||
source?: 'SDS' | 'CUSTOM';
|
||
craftLabel?: string | null;
|
||
logisticsLabel?: string | null;
|
||
variants?: Array<{
|
||
sdsVariantId: string;
|
||
sku: string;
|
||
sizeId?: string;
|
||
sizeName?: string;
|
||
colorId?: string;
|
||
colorName?: string;
|
||
price: number;
|
||
enabled?: boolean;
|
||
}>;
|
||
sizeChart?: object;
|
||
packageSpecs?: object;
|
||
}) => {
|
||
const og = await prisma.originGood.create({
|
||
data: {
|
||
sdsGoodId: `recompute-${stamp}-${createdOriginGoodIds.length}-${Math.random().toString(36).slice(2, 7)}`,
|
||
goodName: over.goodName ?? `测试链接-${stamp}`,
|
||
source: over.source ?? 'CUSTOM',
|
||
craftLabel: over.craftLabel ?? null,
|
||
logisticsLabel: over.logisticsLabel ?? null,
|
||
},
|
||
});
|
||
createdOriginGoodIds.push(og.id);
|
||
if (over.variants?.length) {
|
||
await prisma.originGoodVariant.createMany({
|
||
data: over.variants.map((v) => ({
|
||
originGoodId: og.id,
|
||
sdsVariantId: v.sdsVariantId,
|
||
sku: v.sku,
|
||
sizeId: v.sizeId ?? null,
|
||
sizeName: v.sizeName ?? null,
|
||
colorId: v.colorId ?? null,
|
||
colorName: v.colorName ?? null,
|
||
price: new Prisma.Decimal(v.price),
|
||
enabled: v.enabled ?? true,
|
||
})),
|
||
});
|
||
}
|
||
if (over.sizeChart || over.packageSpecs) {
|
||
await prisma.originGoodDetail.create({
|
||
data: {
|
||
originGoodId: og.id,
|
||
sizeChart: (over.sizeChart ?? undefined) as Prisma.InputJsonValue,
|
||
packageSpecs: (over.packageSpecs ?? undefined) as Prisma.InputJsonValue,
|
||
},
|
||
});
|
||
}
|
||
return og;
|
||
};
|
||
|
||
const mkFamily = async (over: {
|
||
primaryOriginGoodId?: bigint | null;
|
||
autoManaged?: boolean;
|
||
memberIds?: bigint[];
|
||
}) => {
|
||
const family = await prisma.productFamily.create({
|
||
data: {
|
||
familyName: `测试族-${stamp}-${createdFamilyIds.length}`,
|
||
autoManaged: over.autoManaged ?? true,
|
||
primaryOriginGoodId: over.primaryOriginGoodId ?? null,
|
||
},
|
||
});
|
||
createdFamilyIds.push(family.id);
|
||
if (over.memberIds?.length) {
|
||
await prisma.originGood.updateMany({
|
||
where: { id: { in: over.memberIds } },
|
||
data: { familyId: family.id },
|
||
});
|
||
}
|
||
return family;
|
||
};
|
||
|
||
beforeAll(async () => {
|
||
const moduleRef = await Test.createTestingModule({
|
||
providers: [FamilyRecomputeService, PrismaService],
|
||
}).compile();
|
||
service = moduleRef.get(FamilyRecomputeService);
|
||
organize = new OrganizeService(
|
||
moduleRef.get(PrismaService),
|
||
service,
|
||
new ProductFamiliesService(moduleRef.get(PrismaService), service),
|
||
);
|
||
prisma = moduleRef.get(PrismaService);
|
||
await prisma.onModuleInit();
|
||
});
|
||
|
||
afterAll(async () => {
|
||
await prisma.originGood.deleteMany({ where: { id: { in: createdOriginGoodIds } } });
|
||
await prisma.productFamily.deleteMany({ where: { id: { in: createdFamilyIds } } });
|
||
await prisma.tag.deleteMany({ where: { id: { in: createdTagIds } } });
|
||
await prisma.tagGroup.deleteMany({ where: { id: { in: createdTagGroupIds } } });
|
||
await prisma.$disconnect();
|
||
});
|
||
|
||
it('并集尺码表:覆盖最全的成员优先、主链接次之、按首现顺序合并', async () => {
|
||
// A:主链接,2 行;C:非主链接,3 行 → C 的 S 行胜出
|
||
const a = await mkOriginGood({
|
||
craftLabel: '单面印花',
|
||
logisticsLabel: '包邮',
|
||
sizeChart: { columns: [{ key: 'chest', name: '胸围' }], rows: [
|
||
{ sizeId: 'size_S', sizeName: 'S', chest: 100 },
|
||
{ sizeId: 'size_XL', sizeName: 'XL', chest: 108 },
|
||
] },
|
||
});
|
||
const c = await mkOriginGood({
|
||
craftLabel: '单面印花',
|
||
logisticsLabel: '包邮',
|
||
sizeChart: { columns: [{ key: 'chest', name: '胸围' }], rows: [
|
||
{ sizeId: 'size_S', sizeName: 'S', chest: 98 },
|
||
{ sizeId: 'size_XXL', sizeName: 'XXL', chest: 112 },
|
||
{ sizeId: 'size_XXXL', sizeName: 'XXXL', chest: 116 },
|
||
] },
|
||
});
|
||
const family = await mkFamily({ primaryOriginGoodId: a.id, memberIds: [a.id, c.id] });
|
||
|
||
await service.recomputeFamily(family.id);
|
||
|
||
const after = await prisma.productFamily.findUniqueOrThrow({ where: { id: family.id } });
|
||
const chart = after.sizeChart as { rows: Array<{ sizeName: string; chest: number }> };
|
||
const names = chart.rows.map((r) => r.sizeName);
|
||
expect(names).toEqual(['S', 'XXL', 'XXXL', 'XL']); // C(3行) 优先遍历,A 补充 XL
|
||
expect(chart.rows.find((r) => r.sizeName === 'S')?.chest).toBe(98); // 冲突取覆盖最全成员
|
||
expect(after.stale).toBe(false);
|
||
});
|
||
|
||
it('价格矩阵:五维格子取最低价并累积来源;维度取整理派生的链接标签;停用变体不参与', async () => {
|
||
const a = await mkOriginGood({
|
||
source: 'SDS',
|
||
goodName: '美国(包邮)测试A-PA1-单面印花',
|
||
variants: [
|
||
{ sdsVariantId: 'v1', sku: 'A-S-BLK', sizeId: 'size_S', sizeName: 'S', colorId: 'color_blk', colorName: '黑色', price: 25 },
|
||
{ sdsVariantId: 'v2', sku: 'A-XL-WHT', sizeId: 'size_XL', sizeName: 'XL', colorId: 'color_wht', colorName: '白色', price: 27 },
|
||
{ sdsVariantId: 'v3', sku: 'A-S-BLK-OFF', sizeId: 'size_S', sizeName: 'S', colorId: 'color_blk', colorName: '黑色', price: 20, enabled: false },
|
||
],
|
||
});
|
||
const b = await mkOriginGood({
|
||
source: 'SDS',
|
||
goodName: '美国(包邮)测试B-PB1-单面印花', // 同格子(不同仓库)
|
||
variants: [
|
||
{ sdsVariantId: 'v4', sku: 'B-S-BLK', sizeId: 'size_S', sizeName: 'S', colorId: 'color_blk', colorName: '黑色', price: 24.5 },
|
||
{ sdsVariantId: 'v5', sku: 'B-XXXL-BLK', sizeId: 'size_XXXL', sizeName: 'XXXL', colorId: 'color_blk', colorName: '黑色', price: 29 },
|
||
],
|
||
});
|
||
const d = await mkOriginGood({
|
||
source: 'SDS',
|
||
goodName: '美国(包邮)测试D-PD1-直喷双面',
|
||
variants: [
|
||
{ sdsVariantId: 'v6', sku: 'D-S-BLK', sizeId: 'size_S', sizeName: 'S', colorId: 'color_blk', colorName: '黑色', price: 30 },
|
||
],
|
||
});
|
||
const e = await mkOriginGood({
|
||
source: 'SDS',
|
||
goodName: '美国(不包邮)测试E-PE1-光板', // 整理派生:不打印 + 不包邮(无印花数量标签 → 结构化占位单面)
|
||
variants: [
|
||
{ sdsVariantId: 'v7', sku: 'E-S-BLK', sizeId: 'size_S', sizeName: 'S', colorId: 'color_blk', colorName: '黑色', price: 22 },
|
||
],
|
||
});
|
||
const family = await mkFamily({ primaryOriginGoodId: a.id, memberIds: [a.id, b.id, d.id, e.id] });
|
||
|
||
// 整理(显式派生标签)→ 重算(纯聚合)。运行时不解析名称,未派生的成员不进矩阵
|
||
await organize.deriveFamilyTags(family.id);
|
||
await service.recomputeFamily(family.id);
|
||
|
||
const after = await prisma.productFamily.findUniqueOrThrow({ where: { id: family.id } });
|
||
const matrix = after.priceMatrix as any;
|
||
const cell = matrix.rows.find(
|
||
(r: any) => r.sizeId === 'size_S' && r.colorId === 'color_blk'
|
||
&& r.printCount === '单面印花' && r.craft === '烫画' && r.logistics === '包邮',
|
||
);
|
||
expect(cell.price).toBe('24.5');
|
||
expect(cell.manual).toBe(false);
|
||
expect(cell.sources).toHaveLength(2); // a.v1 + b.v4;停用 v3 排除
|
||
// 直喷双面 → 印花数量/工艺两维正确拆分
|
||
const dg = matrix.rows.find((r: any) => r.printCount === '双面印花' && r.craft === '直喷' && r.price === '30');
|
||
expect(dg).toBeTruthy();
|
||
// 光板 → 工艺=不打印、印花数量占位单面、物流=不包邮
|
||
const blank = matrix.rows.find((r: any) => r.craft === '不打印' && r.logistics === '不包邮');
|
||
expect(blank).toBeTruthy();
|
||
expect(blank.printCount).toBe('单面印花');
|
||
expect(matrix.printCounts.sort()).toEqual(['单面印花', '双面印花']);
|
||
expect(matrix.crafts.sort()).toEqual(['不打印', '烫画', '直喷']);
|
||
expect(matrix.logistics.sort()).toEqual(['不包邮', '包邮']);
|
||
expect(matrix.sizes.map((s: any) => s.name).sort()).toEqual(['S', 'XL', 'XXXL']);
|
||
});
|
||
|
||
it('价格矩阵:未整理(无标签)的 SDS 成员不进矩阵', async () => {
|
||
const a = await mkOriginGood({
|
||
source: 'SDS',
|
||
goodName: '美国(包邮)测试U-PU1-单面印花',
|
||
variants: [
|
||
{ sdsVariantId: 'v1', sku: 'U-S-BLK', sizeId: 'size_S', sizeName: 'S', colorId: 'color_blk', colorName: '黑色', price: 25 },
|
||
],
|
||
});
|
||
const family = await mkFamily({ primaryOriginGoodId: a.id, memberIds: [a.id] });
|
||
// 只重算、不整理 → 无标签 → 空矩阵
|
||
await service.recomputeFamily(family.id);
|
||
let after = await prisma.productFamily.findUniqueOrThrow({ where: { id: family.id } });
|
||
expect((after.priceMatrix as any).rows).toHaveLength(0);
|
||
// 整理后进入矩阵
|
||
await organize.deriveFamilyTags(family.id);
|
||
await service.recomputeFamily(family.id);
|
||
after = await prisma.productFamily.findUniqueOrThrow({ where: { id: family.id } });
|
||
expect((after.priceMatrix as any).rows).toHaveLength(1);
|
||
});
|
||
|
||
it('价格矩阵:人工接管标签即权威维度(人工标签 > 一切)', async () => {
|
||
const a = await mkOriginGood({
|
||
goodName: '美国(包邮)测试M-PM1-单面印花', // 名称派生是 单面/烫画/包邮
|
||
variants: [
|
||
{ sdsVariantId: 'v1', sku: 'M-S-BLK', sizeId: 'size_S', sizeName: 'S', colorId: 'color_blk', colorName: '黑色', price: 25 },
|
||
],
|
||
});
|
||
// 人工把该链接标签改成 双面印花/直喷/不包邮(手工造标签行,模拟 updateTags 结果)
|
||
const tagIds: bigint[] = [];
|
||
for (const groupName of ['印花数量', '印刷工艺', '物流渠道']) {
|
||
const group = await prisma.tagGroup.findFirst({ where: { groupName } });
|
||
const g = group ?? (await prisma.tagGroup.create({ data: { groupName } }));
|
||
if (!group) createdTagGroupIds.push(g.id);
|
||
for (const tagName of groupName === '印花数量' ? ['双面印花'] : groupName === '印刷工艺' ? ['直喷'] : ['不包邮']) {
|
||
const existing = await prisma.tag.findFirst({ where: { tagName } });
|
||
const t = existing ?? (await prisma.tag.create({ data: { tagName, tagGroupId: g.id } }));
|
||
if (!existing) createdTagIds.push(t.id);
|
||
tagIds.push(t.id);
|
||
}
|
||
}
|
||
await prisma.originGoodTag.createMany({
|
||
data: tagIds.map((tagId) => ({ originGoodId: a.id, tagId, manual: true })),
|
||
});
|
||
const family = await mkFamily({ primaryOriginGoodId: a.id, memberIds: [a.id] });
|
||
|
||
await service.recomputeFamily(family.id);
|
||
|
||
const after = await prisma.productFamily.findUniqueOrThrow({ where: { id: family.id } });
|
||
const matrix = after.priceMatrix as any;
|
||
expect(matrix.rows).toHaveLength(1);
|
||
expect(matrix.rows[0].printCount).toBe('双面印花');
|
||
expect(matrix.rows[0].craft).toBe('直喷');
|
||
expect(matrix.rows[0].logistics).toBe('不包邮');
|
||
});
|
||
|
||
it('覆盖:命中改价 manual=true,未命中新增行并扩充选项', async () => {
|
||
const a = await mkOriginGood({
|
||
goodName: '美国(包邮)测试O-PO1-单面印花',
|
||
variants: [
|
||
{ sdsVariantId: 'v1', sku: 'A-S-BLK', sizeId: 'size_S', sizeName: 'S', colorId: 'color_blk', colorName: '黑色', price: 25 },
|
||
],
|
||
});
|
||
const family = await mkFamily({ primaryOriginGoodId: a.id, memberIds: [a.id] });
|
||
await prisma.familyPriceOverride.createMany({
|
||
data: [
|
||
{ familyId: family.id, sizeId: 'size_S', colorId: 'color_blk', printCount: '单面印花', craft: '烫画', logistics: '包邮', price: new Prisma.Decimal(23) },
|
||
{ familyId: family.id, sizeId: 'size_M', colorId: 'color_red', printCount: '四面印花', craft: '丝印', logistics: '海运', price: new Prisma.Decimal(40) },
|
||
],
|
||
});
|
||
|
||
await service.recomputeFamily(family.id);
|
||
|
||
const after = await prisma.productFamily.findUniqueOrThrow({ where: { id: family.id } });
|
||
const matrix = after.priceMatrix as any;
|
||
const hit = matrix.rows.find(
|
||
(r: any) => r.sizeId === 'size_S' && r.colorId === 'color_blk' && r.printCount === '单面印花' && r.craft === '烫画' && r.logistics === '包邮',
|
||
);
|
||
expect(hit.price).toBe('23');
|
||
expect(hit.manual).toBe(true);
|
||
const added = matrix.rows.find((r: any) => r.sizeId === 'size_M' && r.craft === '丝印');
|
||
expect(added).toBeTruthy();
|
||
expect(added.manual).toBe(true);
|
||
expect(added.printCount).toBe('四面印花');
|
||
expect(added.sources).toEqual([]);
|
||
expect(matrix.crafts).toContain('丝印');
|
||
expect(matrix.printCounts).toContain('四面印花');
|
||
expect(matrix.logistics).toContain('海运');
|
||
expect(matrix.sizes.some((s: any) => s.key === 'size_M')).toBe(true);
|
||
});
|
||
|
||
it('autoManaged=false:只置 stale,不覆盖物化字段', async () => {
|
||
const a = await mkOriginGood({
|
||
craftLabel: '单面印花',
|
||
logisticsLabel: '包邮',
|
||
variants: [
|
||
{ sdsVariantId: 'v1', sku: 'A-S-BLK', sizeId: 'size_S', sizeName: 'S', colorId: 'color_blk', colorName: '黑色', price: 25 },
|
||
],
|
||
});
|
||
const family = await mkFamily({ primaryOriginGoodId: a.id, memberIds: [a.id], autoManaged: false });
|
||
await prisma.productFamily.update({
|
||
where: { id: family.id },
|
||
data: { priceMatrix: { version: 'locked' } as Prisma.InputJsonValue },
|
||
});
|
||
|
||
await service.recomputeFamily(family.id);
|
||
|
||
const after = await prisma.productFamily.findUniqueOrThrow({ where: { id: family.id } });
|
||
expect(after.stale).toBe(true);
|
||
expect((after.priceMatrix as any).version).toBe('locked'); // 未被覆盖
|
||
});
|
||
|
||
it('canonical detail:为空时从主链接初始化一次,非空不动', async () => {
|
||
const a = await mkOriginGood({ craftLabel: '单面印花', logisticsLabel: '包邮' });
|
||
await prisma.originGoodDetail.create({
|
||
data: { originGoodId: a.id, englishName: 'Cotton Tee', materialDescription: '100% 棉' },
|
||
});
|
||
const b = await mkOriginGood({ craftLabel: '单面印花', logisticsLabel: '包邮' });
|
||
await prisma.originGoodDetail.create({
|
||
data: { originGoodId: b.id, englishName: 'Should Not Win' },
|
||
});
|
||
const family = await mkFamily({ primaryOriginGoodId: a.id, memberIds: [a.id, b.id] });
|
||
|
||
await service.recomputeFamily(family.id);
|
||
let after = await prisma.productFamily.findUniqueOrThrow({ where: { id: family.id } });
|
||
expect((after.detail as any).englishName).toBe('Cotton Tee');
|
||
expect((after.detail as any).materialDescription).toBe('100% 棉');
|
||
|
||
await prisma.productFamily.update({
|
||
where: { id: family.id },
|
||
data: { detail: { englishName: '人工已改' } as Prisma.InputJsonValue },
|
||
});
|
||
await service.recomputeFamily(family.id);
|
||
after = await prisma.productFamily.findUniqueOrThrow({ where: { id: family.id } });
|
||
expect((after.detail as any).englishName).toBe('人工已改');
|
||
});
|
||
|
||
it('族不存在时 no-op 不抛错', async () => {
|
||
await expect(service.recomputeFamily(999999n)).resolves.toBeUndefined();
|
||
});
|
||
|
||
it('enqueue 去重且串行执行', async () => {
|
||
const a = await mkOriginGood({
|
||
craftLabel: '单面印花',
|
||
logisticsLabel: '包邮',
|
||
variants: [{ sdsVariantId: 'v1', sku: 'A-S-BLK', sizeId: 'size_S', sizeName: 'S', colorId: 'color_blk', colorName: '黑色', price: 25 }],
|
||
});
|
||
const family = await mkFamily({ primaryOriginGoodId: a.id, memberIds: [a.id] });
|
||
const spy = jest.spyOn(service, 'recomputeFamily').mockResolvedValue();
|
||
|
||
service.enqueue(family.id);
|
||
service.enqueue(family.id);
|
||
service.enqueue(family.id);
|
||
await new Promise((r) => setTimeout(r, 50));
|
||
|
||
expect(spy).toHaveBeenCalledTimes(1);
|
||
spy.mockRestore();
|
||
});
|
||
});
|
||
|
||
/**
|
||
* derivePriceMatrix 纯函数单测:选项组排序(词表序 + 未知值沉底)。
|
||
* 业务确认的展示顺序:printCount 单面→双面;craft 烫画→直喷→不打印;logistics 不包邮→包邮。
|
||
* 词表外自由文本(CUSTOM craftLabel/logisticsLabel,如"海运")沉底,相互间保持首次遇到序。
|
||
*/
|
||
describe('derivePriceMatrix 选项组排序', () => {
|
||
const mkMember = (
|
||
id: number,
|
||
over: { tags?: string[]; source?: 'SDS' | 'CUSTOM'; craftLabel?: string | null; logisticsLabel?: string | null },
|
||
) =>
|
||
({
|
||
id: BigInt(id),
|
||
source: over.source ?? 'SDS',
|
||
originGoodTags: (over.tags ?? []).map((t) => ({ tag: { tagName: t } })),
|
||
variants: [],
|
||
craftLabel: over.craftLabel ?? null,
|
||
logisticsLabel: over.logisticsLabel ?? null,
|
||
}) as any;
|
||
|
||
it('成员顺序无关:三个维度恒为词表序', () => {
|
||
// 故意让"遇到序"与目标词表序全部相反(双面在前/直喷在前/不包邮在前)
|
||
const members = [
|
||
mkMember(1, { tags: ['直喷', '双面印花', '不包邮'] }),
|
||
mkMember(2, { tags: ['烫画', '单面印花', '包邮'] }),
|
||
];
|
||
const m = derivePriceMatrix(members, []);
|
||
expect(m.printCounts).toEqual(['单面印花', '双面印花']);
|
||
expect(m.crafts).toEqual(['烫画', '直喷']);
|
||
expect(m.logistics).toEqual(['不包邮', '包邮']);
|
||
});
|
||
|
||
it('子集保序:只出现部分取值时相对顺序不变', () => {
|
||
const m = derivePriceMatrix([mkMember(1, { tags: ['直喷', '双面印花', '不包邮'] })], []);
|
||
expect(m.printCounts).toEqual(['双面印花']);
|
||
expect(m.crafts).toEqual(['直喷']);
|
||
expect(m.logistics).toEqual(['不包邮']);
|
||
});
|
||
|
||
it('未知自由文本沉底且相互保持首次遇到序', () => {
|
||
const members = [
|
||
// CUSTOM:自由文本标签 + tags 提供印花数量(无印花维度不进矩阵的既有语义)
|
||
mkMember(1, { source: 'CUSTOM', tags: ['双面印花'], craftLabel: '丝印', logisticsLabel: '海运' }),
|
||
mkMember(2, { source: 'CUSTOM', tags: ['双面印花'], craftLabel: '水洗', logisticsLabel: '空运' }),
|
||
mkMember(3, { tags: ['烫画', '单面印花', '包邮'] }),
|
||
];
|
||
const m = derivePriceMatrix(members, []);
|
||
expect(m.printCounts).toEqual(['单面印花', '双面印花']);
|
||
expect(m.crafts).toEqual(['烫画', '丝印', '水洗']);
|
||
expect(m.logistics).toEqual(['包邮', '海运', '空运']);
|
||
});
|
||
|
||
it('人工覆盖新增取值参与同一排序', () => {
|
||
const members = [mkMember(1, { tags: ['烫画', '单面印花', '包邮'] })];
|
||
const overrides = [
|
||
{
|
||
sizeId: 'size_M',
|
||
colorId: 'color_red',
|
||
printCount: '四面印花',
|
||
craft: '丝印',
|
||
logistics: '海运',
|
||
price: '40',
|
||
},
|
||
] as any;
|
||
const m = derivePriceMatrix(members, overrides);
|
||
expect(m.printCounts).toEqual(['单面印花', '四面印花']);
|
||
expect(m.crafts).toEqual(['烫画', '丝印']);
|
||
expect(m.logistics).toEqual(['包邮', '海运']);
|
||
});
|
||
|
||
it('幂等:同输入多跑两遍输出全等(含顺序),与成员遍历顺序无关', () => {
|
||
const members = [
|
||
mkMember(1, { tags: ['直喷', '双面印花', '不包邮'] }),
|
||
mkMember(2, { source: 'CUSTOM', tags: ['双面印花'], craftLabel: '丝印', logisticsLabel: '海运' }),
|
||
mkMember(3, { tags: ['烫画', '单面印花', '包邮'] }),
|
||
];
|
||
const first = derivePriceMatrix(members, []);
|
||
const second = derivePriceMatrix(members, []);
|
||
const reversed = derivePriceMatrix([...members].reverse(), []);
|
||
expect(JSON.stringify(second)).toBe(JSON.stringify(first));
|
||
expect(JSON.stringify(reversed)).toBe(JSON.stringify(first));
|
||
});
|
||
});
|
||
|
||
/**
|
||
* 印刷工艺新增"热转印":矩阵维度词表放开准入 + 展示序追加末尾。
|
||
* 自动派生规则不改 —— SDS 链接只能人工打标;不打标的链接/族零影响。
|
||
*/
|
||
describe('热转印工艺', () => {
|
||
const mkMember = (
|
||
id: number,
|
||
over: { tags?: string[]; source?: 'SDS' | 'CUSTOM'; craftLabel?: string | null; logisticsLabel?: string | null },
|
||
) =>
|
||
({
|
||
id: BigInt(id),
|
||
source: over.source ?? 'SDS',
|
||
originGoodTags: (over.tags ?? []).map((t) => ({ tag: { tagName: t } })),
|
||
variants: [],
|
||
craftLabel: over.craftLabel ?? null,
|
||
logisticsLabel: over.logisticsLabel ?? null,
|
||
}) as any;
|
||
|
||
it('memberMatrixCombos:打标热转印的链接产出矩阵组合(此前被词表过滤为空)', () => {
|
||
const combos = memberMatrixCombos({ tagNames: ['单面印花', '热转印', '包邮'] });
|
||
expect(combos).toEqual([{ printCount: '单面印花', craft: '热转印', logistics: '包邮' }]);
|
||
});
|
||
|
||
it('derivePriceMatrix:crafts 展示序为 烫画→直喷→不打印→热转印(词表内,不再沉底)', () => {
|
||
const members = [
|
||
mkMember(1, { tags: ['热转印', '单面印花', '包邮'] }),
|
||
mkMember(2, { tags: ['烫画', '单面印花', '包邮'] }),
|
||
mkMember(3, { tags: ['直喷', '单面印花', '不包邮'] }),
|
||
mkMember(4, { tags: ['不打印', '单面印花', '包邮'] }),
|
||
];
|
||
const m = derivePriceMatrix(members, []);
|
||
expect(m.crafts).toEqual(['烫画', '直喷', '不打印', '热转印']);
|
||
});
|
||
|
||
it('词表外自由文本(丝印/水洗)仍然沉底,与热转印区分', () => {
|
||
const members = [
|
||
mkMember(1, { tags: ['热转印', '单面印花', '包邮'] }),
|
||
mkMember(2, { source: 'CUSTOM', tags: ['单面印花'], craftLabel: '丝印', logisticsLabel: '包邮' }),
|
||
];
|
||
const m = derivePriceMatrix(members, []);
|
||
expect(m.crafts).toEqual(['热转印', '丝印']);
|
||
});
|
||
});
|