feat(public): resolve goods by secondary sds id and merge variants

This commit is contained in:
yeuimu
2026-08-27 18:20:25 +08:00
parent d9ecd04747
commit f06dfffbda
2 changed files with 218 additions and 158 deletions
+189 -154
View File
@@ -1,5 +1,5 @@
import { Test } from '@nestjs/testing';
import { BadRequestException, NotFoundException } from '@nestjs/common';
import { BadRequestException, NotFoundException } from '@nestjs/common';
import { PublicService } from './public.service';
import { PrismaService } from '../prisma/prisma.service';
@@ -11,9 +11,9 @@ describe('PublicService', () => {
let categoryId: bigint;
let childCategoryId: bigint;
let otherCategoryId: bigint;
let tagId: bigint;
let filterGroupIds: bigint[] = [];
let filterTagIds: bigint[] = [];
let tagId: bigint;
let filterGroupIds: bigint[] = [];
let filterTagIds: bigint[] = [];
let originGoodId: bigint;
let goodIds: bigint[] = [];
@@ -98,51 +98,51 @@ describe('PublicService', () => {
goodPriority: 1,
},
});
goodIds = [g1.id, g2.id, g3.id];
const craftGroup = await prisma.tagGroup.create({
data: { groupName: `Pub Craft ${stamp}`, sortOrder: 100 },
});
const materialGroup = await prisma.tagGroup.create({
data: { groupName: `Pub Material ${stamp}`, sortOrder: 101 },
});
filterGroupIds = [craftGroup.id, materialGroup.id];
const craftA = await prisma.tag.create({
data: { tagName: `Pub Craft A ${stamp}`, tagGroupId: craftGroup.id },
});
const craftB = await prisma.tag.create({
data: { tagName: `Pub Craft B ${stamp}`, tagGroupId: craftGroup.id },
});
const cotton = await prisma.tag.create({
data: { tagName: `Pub Cotton ${stamp}`, tagGroupId: materialGroup.id },
});
filterTagIds = [craftA.id, craftB.id, cotton.id];
await prisma.goodTag.createMany({
data: [
{ goodId: g1.id, tagId: craftA.id },
{ goodId: g1.id, tagId: cotton.id },
{ goodId: g2.id, tagId: craftB.id },
],
});
await prisma.originGoodDetail.create({
data: {
originGoodId,
productCode: 'OZ10827003',
productionProcess: '白墨烫画',
sizeChart: { columns: [], rows: [{ sizeId: 'size_0', sizeName: 'S', measurements: [] }] },
packageSpecs: { rows: [{ sizeId: 'size_0', sizeName: 'S' }] },
},
});
await prisma.originGoodVariant.create({
data: {
originGoodId,
sdsVariantId: `pub-variant-${stamp}`,
sku: `OZ${stamp}`,
sizeName: 'S',
price: 38,
},
});
goodIds = [g1.id, g2.id, g3.id];
const craftGroup = await prisma.tagGroup.create({
data: { groupName: `Pub Craft ${stamp}`, sortOrder: 100 },
});
const materialGroup = await prisma.tagGroup.create({
data: { groupName: `Pub Material ${stamp}`, sortOrder: 101 },
});
filterGroupIds = [craftGroup.id, materialGroup.id];
const craftA = await prisma.tag.create({
data: { tagName: `Pub Craft A ${stamp}`, tagGroupId: craftGroup.id },
});
const craftB = await prisma.tag.create({
data: { tagName: `Pub Craft B ${stamp}`, tagGroupId: craftGroup.id },
});
const cotton = await prisma.tag.create({
data: { tagName: `Pub Cotton ${stamp}`, tagGroupId: materialGroup.id },
});
filterTagIds = [craftA.id, craftB.id, cotton.id];
await prisma.goodTag.createMany({
data: [
{ goodId: g1.id, tagId: craftA.id },
{ goodId: g1.id, tagId: cotton.id },
{ goodId: g2.id, tagId: craftB.id },
],
});
await prisma.originGoodDetail.create({
data: {
originGoodId,
productCode: 'OZ10827003',
productionProcess: '白墨烫画',
sizeChart: { columns: [], rows: [{ sizeId: 'size_0', sizeName: 'S', measurements: [] }] },
packageSpecs: { rows: [{ sizeId: 'size_0', sizeName: 'S' }] },
},
});
await prisma.originGoodVariant.create({
data: {
originGoodId,
sdsVariantId: `pub-variant-${stamp}`,
sku: `OZ${stamp}`,
sizeName: 'S',
price: 38,
},
});
// Seed a good in `otherCategory` so the "onlyHaveGoods" filter
// returns more than one category.
@@ -179,9 +179,9 @@ describe('PublicService', () => {
await prisma.position.deleteMany({
where: { countryId },
});
await prisma.tag.delete({ where: { id: tagId } });
await prisma.tag.deleteMany({ where: { id: { in: filterTagIds } } });
await prisma.tagGroup.deleteMany({ where: { id: { in: filterGroupIds } } });
await prisma.tag.delete({ where: { id: tagId } });
await prisma.tag.deleteMany({ where: { id: { in: filterTagIds } } });
await prisma.tagGroup.deleteMany({ where: { id: { in: filterGroupIds } } });
await prisma.originGood.delete({ where: { id: originGoodId } });
// Delete children before parent (FK self-relation is RESTRICT).
await prisma.category.delete({ where: { id: childCategoryId } });
@@ -220,139 +220,139 @@ describe('PublicService', () => {
const filtered = await service.getGoods({
page: 1,
pageSize: 50,
countryId: countryId.toString(),
categoryId: categoryId.toString(), // includes child
countryId: countryId.toString(),
categoryId: categoryId.toString(), // includes child
keyword: `Pub `,
});
expect(filtered.total).toBeGreaterThanOrEqual(4); // High, Mid, NoPos, ChildGood
expect(filtered.items.every((g) => g.country.id === countryId.toString())).toBe(true);
});
it('sorts by priority DESC, position.indexVal ASC, createdAt DESC', async () => {
it('sorts by priority DESC, position.indexVal ASC, createdAt DESC', async () => {
const result = await service.getGoods({
page: 1,
pageSize: 50,
countryId: countryId.toString(),
countryId: countryId.toString(),
keyword: `Pub `,
});
const priorities = result.items.map((g) => g.goodPriority);
// First verify primary descending priority.
const sorted = [...priorities].sort((a, b) => b - a);
expect(priorities).toEqual(sorted);
});
it('uses OR within one tag group and AND across tag groups', async () => {
const sameGroup = await service.getGoods({
page: 1,
pageSize: 50,
countryId: countryId.toString(),
keyword: `Pub `,
tags: [
{
tagGroupId: filterGroupIds[0].toString(),
tagIds: filterTagIds.slice(0, 2).map(String),
},
],
});
expect(sameGroup.items.map((item) => item.goodName)).toEqual(
expect.arrayContaining([`Pub High ${stamp}`, `Pub Mid ${stamp}`]),
);
const acrossGroups = await service.getGoods({
page: 1,
pageSize: 50,
countryId: countryId.toString(),
keyword: `Pub `,
tags: [
{
tagGroupId: filterGroupIds[0].toString(),
tagIds: filterTagIds.slice(0, 2).map(String),
},
{
tagGroupId: filterGroupIds[1].toString(),
tagIds: [filterTagIds[2].toString()],
},
],
});
expect(acrossGroups.items.map((item) => item.goodName)).toContain(`Pub High ${stamp}`);
expect(acrossGroups.items.map((item) => item.goodName)).not.toContain(`Pub Mid ${stamp}`);
});
it('rejects a tag paired with the wrong tag group', async () => {
await expect(
service.getGoods({
page: 1,
pageSize: 20,
tags: [
{
tagGroupId: filterGroupIds[1].toString(),
tagIds: [filterTagIds[0].toString()],
},
],
}),
).rejects.toBeInstanceOf(BadRequestException);
});
});
it('returns the SDS product id as the public product id', async () => {
it('uses OR within one tag group and AND across tag groups', async () => {
const sameGroup = await service.getGoods({
page: 1,
pageSize: 50,
countryId: countryId.toString(),
keyword: `Pub `,
tags: [
{
tagGroupId: filterGroupIds[0].toString(),
tagIds: filterTagIds.slice(0, 2).map(String),
},
],
});
expect(sameGroup.items.map((item) => item.goodName)).toEqual(
expect.arrayContaining([`Pub High ${stamp}`, `Pub Mid ${stamp}`]),
);
const acrossGroups = await service.getGoods({
page: 1,
pageSize: 50,
countryId: countryId.toString(),
keyword: `Pub `,
tags: [
{
tagGroupId: filterGroupIds[0].toString(),
tagIds: filterTagIds.slice(0, 2).map(String),
},
{
tagGroupId: filterGroupIds[1].toString(),
tagIds: [filterTagIds[2].toString()],
},
],
});
expect(acrossGroups.items.map((item) => item.goodName)).toContain(`Pub High ${stamp}`);
expect(acrossGroups.items.map((item) => item.goodName)).not.toContain(`Pub Mid ${stamp}`);
});
it('rejects a tag paired with the wrong tag group', async () => {
await expect(
service.getGoods({
page: 1,
pageSize: 20,
tags: [
{
tagGroupId: filterGroupIds[1].toString(),
tagIds: [filterTagIds[0].toString()],
},
],
}),
).rejects.toBeInstanceOf(BadRequestException);
});
it('returns the SDS product id as the public product id', async () => {
const result = await service.getGoods({
page: 1,
pageSize: 1,
countryId: countryId.toString(),
countryId: countryId.toString(),
keyword: `Pub High ${stamp}`,
});
expect(result.items).toHaveLength(1);
expect(result.items[0].goodId).toBe(`pub-sds-${stamp}`);
expect(result.items[0].goodId).not.toBe(goodIds[0].toString());
});
it('returns custom goods through the same public product contract', async () => {
const customPublicId = `custom-public-${stamp}`;
const origin = await prisma.originGood.create({
data: {
source: 'CUSTOM',
sdsGoodId: customPublicId,
goodName: `Pub Custom ${stamp}`,
goodPrice: 42,
detail: { create: { productCode: `CUSTOM-${stamp}` } },
},
});
const good = await prisma.good.create({
data: {
originGoodId: origin.id,
countryId,
categoryId,
goodName: `Pub Custom ${stamp}`,
},
});
try {
const detail = await service.getGood(customPublicId);
expect(detail.goodId).toBe(customPublicId);
expect(detail.goodName).toBe(`Pub Custom ${stamp}`);
expect(detail.productCode).toBe(`CUSTOM-${stamp}`);
} finally {
await prisma.good.delete({ where: { id: good.id } });
await prisma.originGood.delete({ where: { id: origin.id } });
}
});
expect(result.items[0].goodId).toBe(`pub-sds-${stamp}`);
expect(result.items[0].goodId).not.toBe(goodIds[0].toString());
});
it('returns custom goods through the same public product contract', async () => {
const customPublicId = `custom-public-${stamp}`;
const origin = await prisma.originGood.create({
data: {
source: 'CUSTOM',
sdsGoodId: customPublicId,
goodName: `Pub Custom ${stamp}`,
goodPrice: 42,
detail: { create: { productCode: `CUSTOM-${stamp}` } },
},
});
const good = await prisma.good.create({
data: {
originGoodId: origin.id,
countryId,
categoryId,
goodName: `Pub Custom ${stamp}`,
},
});
try {
const detail = await service.getGood(customPublicId);
expect(detail.goodId).toBe(customPublicId);
expect(detail.goodName).toBe(`Pub Custom ${stamp}`);
expect(detail.productCode).toBe(`CUSTOM-${stamp}`);
} finally {
await prisma.good.delete({ where: { id: good.id } });
await prisma.originGood.delete({ where: { id: origin.id } });
}
});
it('getGood returns detail and 404 for unknown id', async () => {
const first = await service.getGoods({
page: 1,
pageSize: 1,
countryId: countryId.toString(),
countryId: countryId.toString(),
keyword: `Pub `,
});
expect(first.items.length).toBe(1);
const detail = await service.getGood(`pub-sds-${stamp}`);
expect(detail.goodId).toBe(first.items[0].goodId);
expect(detail.productCode).toBe('OZ10827003');
expect(detail.details.productionProcess).toBe('白墨烫画');
expect((detail.sizeChart?.rows as unknown[])).toHaveLength(1);
expect((detail.packageSpecs?.rows as unknown[])).toHaveLength(1);
expect(detail.variants).toHaveLength(1);
await expect(service.getGood('99999999')).rejects.toBeInstanceOf(
const detail = await service.getGood(`pub-sds-${stamp}`);
expect(detail.goodId).toBe(first.items[0].goodId);
expect(detail.productCode).toBe('OZ10827003');
expect(detail.details.productionProcess).toBe('白墨烫画');
expect((detail.sizeChart?.rows as unknown[])).toHaveLength(1);
expect((detail.packageSpecs?.rows as unknown[])).toHaveLength(1);
expect(detail.variants).toHaveLength(1);
await expect(service.getGood('99999999')).rejects.toBeInstanceOf(
NotFoundException,
);
});
@@ -379,4 +379,39 @@ describe('PublicService', () => {
const sortOrders = groups.map((g) => g.sortOrder);
expect([...sortOrders].sort((a, b) => a - b)).toEqual(sortOrders);
});
describe('merged secondary origin goods', () => {
it('resolves a good by secondary sdsGoodId with merged variants', async () => {
const secondary = await prisma.originGood.create({
data: { sdsGoodId: `pub-secondary-${stamp}`, goodName: `Pub Secondary ${stamp}` },
});
const secVariant = await prisma.originGoodVariant.create({
data: {
originGoodId: secondary.id,
sdsVariantId: `pub-var-sec-${stamp}`,
sku: `PUB-SEC-${stamp}`,
colorId: 'black',
colorName: '黑色',
imageUrl: 'http://img/black-sec',
},
});
// Attach as secondary source of the highest-priority fixture good.
await prisma.goodOriginGood.create({
data: { goodId: goodIds[0], originGoodId: secondary.id },
});
try {
const detail = await service.getGood(`pub-secondary-${stamp}`);
expect(detail.goodId).toBe(`pub-sds-${stamp}`); // 对外 goodId 仍是主源
expect(detail.variants.length).toBeGreaterThanOrEqual(2);
const black = detail.mediaByColor.find((g) => g.colorName === '黑色');
expect(black).toBeTruthy();
expect(black!.images).toContain('http://img/black-sec');
} finally {
await prisma.goodOriginGood.deleteMany({ where: { originGoodId: secondary.id } });
await prisma.originGoodVariant.delete({ where: { id: secVariant.id } }).catch(() => undefined);
await prisma.originGood.delete({ where: { id: secondary.id } }).catch(() => undefined);
}
});
});
});