feat(api): product families backfill script

This commit is contained in:
yeuimu
2026-08-28 12:36:39 +08:00
parent d20a933e34
commit d63f1a6f35
4 changed files with 159 additions and 76 deletions
+78 -73
View File
@@ -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);
});
});