feat(api): product families backfill script
This commit is contained in:
@@ -132,7 +132,7 @@ export class ProductFamiliesService {
|
||||
async autoGroup(apply: boolean) {
|
||||
const candidates = await this.prisma.originGood.findMany({
|
||||
where: { familyId: null, delisted: false },
|
||||
select: { id: true, goodName: true, goodImage: true },
|
||||
select: { id: true, goodName: true, goodImage: true, source: true },
|
||||
orderBy: { id: 'asc' },
|
||||
});
|
||||
const groups = new Map<string, typeof candidates>();
|
||||
@@ -160,10 +160,16 @@ export class ProductFamiliesService {
|
||||
let applied = 0;
|
||||
for (const members of groups.values()) {
|
||||
const parsed = parseOriginName(members[0].goodName);
|
||||
const fallbackCode =
|
||||
members[0].source === 'CUSTOM' ? `CUSTOM-${members[0].id}` : null;
|
||||
const family = await this.prisma.productFamily.create({
|
||||
data: {
|
||||
familyName: parsed.productName ?? parsed.country ?? originGroupKey(members[0].goodName),
|
||||
familyCode: parsed.skuCode ? await this.ensureUniqueCode(parsed.skuCode) : null,
|
||||
familyCode: parsed.skuCode
|
||||
? await this.ensureUniqueCode(parsed.skuCode)
|
||||
: fallbackCode
|
||||
? await this.ensureUniqueCode(fallbackCode)
|
||||
: null,
|
||||
familyImage: members[0].goodImage ?? null,
|
||||
primaryOriginGoodId: members[0].id,
|
||||
},
|
||||
|
||||
@@ -7,8 +7,9 @@ import {
|
||||
} from './sync.service';
|
||||
import { SdsClientService } from './sds-client.service';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { FamilyRecomputeService } from '../product-families/family-recompute.service';
|
||||
|
||||
describe('SyncService', () => {
|
||||
describe('SyncService', () => {
|
||||
let service: SyncService;
|
||||
let sds: jest.Mocked<SdsClientService>;
|
||||
let prisma: PrismaService;
|
||||
@@ -17,22 +18,23 @@ describe('SyncService', () => {
|
||||
|
||||
beforeAll(async () => {
|
||||
const sdsMock: Partial<SdsClientService> = {
|
||||
fetchCategoryTree: jest.fn(),
|
||||
fetchProductsPage: jest.fn(),
|
||||
fetchProductDetail: jest.fn(async (goodId: string | number) => ({ id: goodId })),
|
||||
fetchCategoryTree: jest.fn(),
|
||||
fetchProductsPage: jest.fn(),
|
||||
fetchProductDetail: jest.fn(async (goodId: string | number) => ({ id: goodId })),
|
||||
};
|
||||
const moduleRef = await Test.createTestingModule({
|
||||
imports: [ConfigModule.forRoot({ isGlobal: true })],
|
||||
providers: [
|
||||
SyncService,
|
||||
{ provide: SdsClientService, useValue: sdsMock },
|
||||
{ provide: FamilyRecomputeService, useValue: { enqueue: jest.fn() } },
|
||||
PrismaService,
|
||||
],
|
||||
}).compile();
|
||||
service = moduleRef.get(SyncService);
|
||||
jest
|
||||
.spyOn(service, 'syncConfiguredProductDetails')
|
||||
.mockResolvedValue({ synced: 0, failed: 0 });
|
||||
service = moduleRef.get(SyncService);
|
||||
jest
|
||||
.spyOn(service, 'syncConfiguredProductDetails')
|
||||
.mockResolvedValue({ synced: 0, failed: 0 });
|
||||
sds = moduleRef.get(SdsClientService) as jest.Mocked<SdsClientService>;
|
||||
prisma = moduleRef.get(PrismaService);
|
||||
await prisma.onModuleInit();
|
||||
@@ -275,68 +277,71 @@ describe('SyncService', () => {
|
||||
expect(logs.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('SyncService product detail scopes', () => {
|
||||
const originGoods = [
|
||||
{ id: 1n, sdsGoodId: 'all-1' },
|
||||
{ id: 2n, sdsGoodId: 'all-2' },
|
||||
];
|
||||
|
||||
function createService() {
|
||||
const prisma = {
|
||||
originGood: { findMany: jest.fn().mockResolvedValue(originGoods) },
|
||||
} as unknown as PrismaService;
|
||||
const sds = {
|
||||
fetchProductDetail: jest.fn(async (goodId: string) => ({ id: goodId })),
|
||||
} as unknown as SdsClientService;
|
||||
const scopedService = new SyncService(prisma, sds);
|
||||
jest
|
||||
.spyOn(scopedService as any, 'persistProductDetail')
|
||||
.mockResolvedValue(undefined);
|
||||
return { scopedService, prisma, sds };
|
||||
}
|
||||
|
||||
it('manual detail sync selects every active origin product', async () => {
|
||||
const { scopedService, prisma, sds } = createService();
|
||||
const result = await scopedService.syncAllProductDetails();
|
||||
|
||||
expect(prisma.originGood.findMany).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ where: { delisted: false, source: 'SDS' } }),
|
||||
);
|
||||
expect(sds.fetchProductDetail).toHaveBeenCalledTimes(2);
|
||||
expect(result).toEqual({ total: 2, synced: 2, failed: 0 });
|
||||
});
|
||||
|
||||
it('hourly detail refresh remains limited to configured products', async () => {
|
||||
const { scopedService, prisma } = createService();
|
||||
await scopedService.syncConfiguredProductDetails();
|
||||
|
||||
expect(prisma.originGood.findMany).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: { delisted: false, source: 'SDS', goods: { some: {} } },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps hourly category/product sync separate from the daily detail sync', async () => {
|
||||
const { scopedService } = createService();
|
||||
const categories = jest.spyOn(scopedService, 'syncCategories').mockResolvedValue({
|
||||
inserted: 0, updated: 0, total: 0, deletedStale: 0,
|
||||
});
|
||||
const products = jest.spyOn(scopedService, 'syncProducts').mockResolvedValue({
|
||||
inserted: 0, updated: 0, total: 0, leafCategories: 0, delisted: 0,
|
||||
});
|
||||
const details = jest.spyOn(scopedService, 'syncProductDetails').mockResolvedValue({
|
||||
total: 0, synced: 0, failed: 0,
|
||||
});
|
||||
|
||||
await scopedService.hourlyCron();
|
||||
expect(categories).toHaveBeenCalledTimes(1);
|
||||
expect(products).toHaveBeenCalledTimes(1);
|
||||
expect(details).not.toHaveBeenCalled();
|
||||
|
||||
await scopedService.dailyProductDetailCron();
|
||||
expect(details).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('SyncService product detail scopes', () => {
|
||||
const originGoods = [
|
||||
{ id: 1n, sdsGoodId: 'all-1' },
|
||||
{ id: 2n, sdsGoodId: 'all-2' },
|
||||
];
|
||||
|
||||
function createService() {
|
||||
const prisma = {
|
||||
originGood: { findMany: jest.fn().mockResolvedValue(originGoods) },
|
||||
} as unknown as PrismaService;
|
||||
const sds = {
|
||||
fetchProductDetail: jest.fn(async (goodId: string) => ({ id: goodId })),
|
||||
} as unknown as SdsClientService;
|
||||
const familyRecompute = {
|
||||
enqueue: jest.fn(),
|
||||
} as unknown as FamilyRecomputeService;
|
||||
const scopedService = new SyncService(prisma, sds, familyRecompute);
|
||||
jest
|
||||
.spyOn(scopedService as any, 'persistProductDetail')
|
||||
.mockResolvedValue(undefined);
|
||||
return { scopedService, prisma, sds };
|
||||
}
|
||||
|
||||
it('manual detail sync selects every active origin product', async () => {
|
||||
const { scopedService, prisma, sds } = createService();
|
||||
const result = await scopedService.syncAllProductDetails();
|
||||
|
||||
expect(prisma.originGood.findMany).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ where: { delisted: false, source: 'SDS' } }),
|
||||
);
|
||||
expect(sds.fetchProductDetail).toHaveBeenCalledTimes(2);
|
||||
expect(result).toEqual({ total: 2, synced: 2, failed: 0 });
|
||||
});
|
||||
|
||||
it('hourly detail refresh remains limited to configured products', async () => {
|
||||
const { scopedService, prisma } = createService();
|
||||
await scopedService.syncConfiguredProductDetails();
|
||||
|
||||
expect(prisma.originGood.findMany).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: { delisted: false, source: 'SDS', goods: { some: {} } },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps hourly category/product sync separate from the daily detail sync', async () => {
|
||||
const { scopedService } = createService();
|
||||
const categories = jest.spyOn(scopedService, 'syncCategories').mockResolvedValue({
|
||||
inserted: 0, updated: 0, total: 0, deletedStale: 0,
|
||||
});
|
||||
const products = jest.spyOn(scopedService, 'syncProducts').mockResolvedValue({
|
||||
inserted: 0, updated: 0, total: 0, leafCategories: 0, delisted: 0,
|
||||
});
|
||||
const details = jest.spyOn(scopedService, 'syncProductDetails').mockResolvedValue({
|
||||
total: 0, synced: 0, failed: 0,
|
||||
});
|
||||
|
||||
await scopedService.hourlyCron();
|
||||
expect(categories).toHaveBeenCalledTimes(1);
|
||||
expect(products).toHaveBeenCalledTimes(1);
|
||||
expect(details).not.toHaveBeenCalled();
|
||||
|
||||
await scopedService.dailyProductDetailCron();
|
||||
expect(details).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user