feat(goods): support merged secondary origin goods in create/update/detail

This commit is contained in:
yeuimu
2026-08-27 18:15:56 +08:00
parent 848eed0b6f
commit 9c279ae393
6 changed files with 729 additions and 418 deletions
+186 -57
View File
@@ -4,8 +4,8 @@ import {
NotFoundException,
} from '@nestjs/common';
import { GoodsService } from './goods.service';
import { PrismaService } from '../prisma/prisma.service';
import { SyncService } from '../sync/sync.service';
import { PrismaService } from '../prisma/prisma.service';
import { SyncService } from '../sync/sync.service';
describe('GoodsService', () => {
let service: GoodsService;
@@ -23,14 +23,14 @@ describe('GoodsService', () => {
beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
providers: [
GoodsService,
PrismaService,
{
provide: SyncService,
useValue: { queueProductDetailSync: jest.fn() },
},
],
providers: [
GoodsService,
PrismaService,
{
provide: SyncService,
useValue: { queueProductDetailSync: jest.fn() },
},
],
}).compile();
service = moduleRef.get(GoodsService);
prisma = moduleRef.get(PrismaService);
@@ -101,7 +101,7 @@ describe('GoodsService', () => {
expect(service).toBeDefined();
});
it('creates and reads back a good', async () => {
it('creates and reads back a good', async () => {
const created = await service.create({
goodName: `Goods Test ${stamp} basic`,
originGoodId: Number(originGoodIds[0]),
@@ -117,52 +117,52 @@ describe('GoodsService', () => {
const fetched = await service.findOne(BigInt(created.id));
expect(fetched.goodName).toBe(`Goods Test ${stamp} basic`);
});
it('creates, edits, and removes a fully editable custom good', async () => {
const created = await service.createCustom({
goodName: `Goods Test ${stamp} custom`,
goodImage: 'https://example.com/custom.png',
goodPrice: '29.90',
countryId: Number(countryId),
categoryId: Number(categoryId),
tagIds: [Number(tagId)],
detail: {
productCode: `CUSTOM-${stamp}`,
materialDescription: 'Cotton',
sizeChart: { columns: [], rows: [] },
packageSpecs: { rows: [] },
},
variants: [
{ sku: `CUSTOM-SKU-${stamp}`, sizeName: 'S', price: '29.90' },
],
});
expect(created.originGood?.source).toBe('CUSTOM');
expect(created.originGood?.isCustom).toBe(true);
expect(created.originGood?.goodPrice).toBe('29.9');
expect(created.variants).toHaveLength(1);
const updated = await service.updateCustomContent(BigInt(created.id), {
goodName: `Goods Test ${stamp} custom edited`,
goodPrice: '39.90',
detail: { materialDescription: 'Organic cotton' },
variants: [
{ sku: `CUSTOM-SKU-${stamp}-M`, sizeName: 'M', price: '39.90' },
],
});
expect(updated.goodName).toContain('custom edited');
expect(updated.originGood?.goodPrice).toBe('39.9');
expect(updated.originDetail?.materialDescription).toBe('Organic cotton');
expect(updated.originDetail?.productCode).toBe(`CUSTOM-${stamp}`);
expect(updated.variants[0]?.sizeName).toBe('M');
const customOriginId = BigInt(updated.originGoodId);
await service.remove(BigInt(updated.id));
await expect(
prisma.originGood.findUnique({ where: { id: customOriginId } }),
).resolves.toBeNull();
});
});
it('creates, edits, and removes a fully editable custom good', async () => {
const created = await service.createCustom({
goodName: `Goods Test ${stamp} custom`,
goodImage: 'https://example.com/custom.png',
goodPrice: '29.90',
countryId: Number(countryId),
categoryId: Number(categoryId),
tagIds: [Number(tagId)],
detail: {
productCode: `CUSTOM-${stamp}`,
materialDescription: 'Cotton',
sizeChart: { columns: [], rows: [] },
packageSpecs: { rows: [] },
},
variants: [
{ sku: `CUSTOM-SKU-${stamp}`, sizeName: 'S', price: '29.90' },
],
});
expect(created.originGood?.source).toBe('CUSTOM');
expect(created.originGood?.isCustom).toBe(true);
expect(created.originGood?.goodPrice).toBe('29.9');
expect(created.variants).toHaveLength(1);
const updated = await service.updateCustomContent(BigInt(created.id), {
goodName: `Goods Test ${stamp} custom edited`,
goodPrice: '39.90',
detail: { materialDescription: 'Organic cotton' },
variants: [
{ sku: `CUSTOM-SKU-${stamp}-M`, sizeName: 'M', price: '39.90' },
],
});
expect(updated.goodName).toContain('custom edited');
expect(updated.originGood?.goodPrice).toBe('39.9');
expect(updated.originDetail?.materialDescription).toBe('Organic cotton');
expect(updated.originDetail?.productCode).toBe(`CUSTOM-${stamp}`);
expect(updated.variants[0]?.sizeName).toBe('M');
const customOriginId = BigInt(updated.originGoodId);
await service.remove(BigInt(updated.id));
await expect(
prisma.originGood.findUnique({ where: { id: customOriginId } }),
).resolves.toBeNull();
});
it('filters by countryId, tagId, positionId and keyword', async () => {
const result = await service.findAll({
@@ -261,6 +261,135 @@ describe('GoodsService', () => {
expect(after.total).toBe(before.total);
});
describe('merged origin goods', () => {
it('creates a good with merged origin goods and reads them back', async () => {
const created = await service.create({
goodName: `Goods Test ${stamp} merged`,
originGoodId: Number(originGoodIds[1]),
mergedOriginGoodIds: [Number(originGoodIds[2]), Number(originGoodIds[3])],
countryId: Number(countryId),
categoryId: Number(categoryId),
});
expect(created.mergedOriginGoods.map((m) => m.id).sort()).toEqual(
[originGoodIds[2].toString(), originGoodIds[3].toString()].sort(),
);
const fetched = await service.findOne(BigInt(created.id));
expect(fetched.mergedOriginGoods.length).toBe(2);
});
it('rejects mergedOriginGoodIds containing the primary', async () => {
await expect(
service.create({
goodName: `Goods Test ${stamp} bad-primary`,
originGoodId: Number(originGoodIds[1]),
mergedOriginGoodIds: [Number(originGoodIds[1])],
countryId: Number(countryId),
categoryId: Number(categoryId),
}),
).rejects.toThrow(BadRequestException);
});
it('rejects mergedOriginGoodIds that do not exist', async () => {
await expect(
service.create({
goodName: `Goods Test ${stamp} bad-missing`,
originGoodId: Number(originGoodIds[1]),
mergedOriginGoodIds: [999999999],
countryId: Number(countryId),
categoryId: Number(categoryId),
}),
).rejects.toThrow(BadRequestException);
});
it('replaces merged origin goods on update', async () => {
const created = await service.create({
goodName: `Goods Test ${stamp} replace`,
originGoodId: Number(originGoodIds[1]),
mergedOriginGoodIds: [Number(originGoodIds[2])],
countryId: Number(countryId),
categoryId: Number(categoryId),
});
const updated = await service.update(BigInt(created.id), {
mergedOriginGoodIds: [Number(originGoodIds[3]), Number(originGoodIds[4])],
});
expect(updated.mergedOriginGoods.map((m) => m.id).sort()).toEqual(
[originGoodIds[3].toString(), originGoodIds[4].toString()].sort(),
);
});
it('moves old primary into merged list when switching primary', async () => {
const created = await service.create({
goodName: `Goods Test ${stamp} switch`,
originGoodId: Number(originGoodIds[1]),
mergedOriginGoodIds: [Number(originGoodIds[2])],
countryId: Number(countryId),
categoryId: Number(categoryId),
});
const updated = await service.update(BigInt(created.id), {
originGoodId: Number(originGoodIds[2]),
mergedOriginGoodIds: [Number(originGoodIds[1]), Number(originGoodIds[3])],
});
expect(updated.originGoodId).toBe(originGoodIds[2].toString());
expect(updated.mergedOriginGoods.map((m) => m.id).sort()).toEqual(
[originGoodIds[1].toString(), originGoodIds[3].toString()].sort(),
);
});
it('cascades merged rows on good removal', async () => {
const created = await service.create({
goodName: `Goods Test ${stamp} cascade`,
originGoodId: Number(originGoodIds[1]),
mergedOriginGoodIds: [Number(originGoodIds[2])],
countryId: Number(countryId),
categoryId: Number(categoryId),
});
await service.remove(BigInt(created.id));
const rows = await prisma.goodOriginGood.count({
where: { goodId: BigInt(created.id) },
});
expect(rows).toBe(0);
});
it('returns merged variants with source annotation in detail', async () => {
const v1 = await prisma.originGoodVariant.create({
data: {
originGoodId: originGoodIds[1],
sdsVariantId: `mv-pri-${stamp}`,
sku: `MV-PRI-${stamp}`,
colorName: '黑色',
},
});
const v2 = await prisma.originGoodVariant.create({
data: {
originGoodId: originGoodIds[2],
sdsVariantId: `mv-sec-${stamp}`,
sku: `MV-SEC-${stamp}`,
colorName: '白色',
},
});
try {
const created = await service.create({
goodName: `Goods Test ${stamp} variants`,
originGoodId: Number(originGoodIds[1]),
mergedOriginGoodIds: [Number(originGoodIds[2])],
countryId: Number(countryId),
categoryId: Number(categoryId),
});
const detail = await service.findOne(BigInt(created.id));
const sources = new Set(
detail.variants.map((v) => v['originGoodId'] as string),
);
expect(sources.has(originGoodIds[1].toString())).toBe(true);
expect(sources.has(originGoodIds[2].toString())).toBe(true);
expect(detail.variants).toHaveLength(2);
expect(detail.mergedOriginGoods.find((m) => m.id === originGoodIds[2].toString())?.variantCount).toBe(1);
} finally {
await prisma.originGoodVariant.delete({ where: { id: v1.id } });
await prisma.originGoodVariant.delete({ where: { id: v2.id } });
}
});
});
it('throws NotFoundException for unknown id', async () => {
await expect(service.findOne(BigInt(99999999))).rejects.toBeInstanceOf(
NotFoundException,