feat(F-114): completed feature
This commit is contained in:
91
project/scripts/seed-legacy-categories.mjs
Normal file
91
project/scripts/seed-legacy-categories.mjs
Normal file
@@ -0,0 +1,91 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* seed-legacy-categories — F-114
|
||||
*
|
||||
* Imports the legacy OpenCart `oc_category_description` rows into the
|
||||
* current catalog and brand tables. Reads the legacy list from the
|
||||
* pure helper at `src/modules/categories/legacy/legacy-catalog` so the
|
||||
* mapping stays in sync with the tests.
|
||||
*
|
||||
* Idempotent: skips entries whose name (case-insensitive) already exists in
|
||||
* the target table, and uniquifies slugs against existing ones.
|
||||
*
|
||||
* Usage:
|
||||
* npm run seed:legacy-categories # apply
|
||||
* DATABASE_URL=... node scripts/seed-legacy-categories.mjs --dry-run
|
||||
*/
|
||||
import pg from 'pg';
|
||||
import { buildImportPlan } from '../dist/modules/categories/legacy/legacy-catalog.js';
|
||||
|
||||
const DATABASE_URL = process.env.DATABASE_URL;
|
||||
if (!DATABASE_URL) {
|
||||
console.error('DATABASE_URL is required');
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
const args = new Set(process.argv.slice(2));
|
||||
const dryRun = args.has('--dry-run');
|
||||
|
||||
const pool = new pg.Pool({ connectionString: DATABASE_URL, max: 1 });
|
||||
|
||||
try {
|
||||
const categories = await pool.query('SELECT id, name, slug FROM categories_categories');
|
||||
const brands = await pool.query('SELECT id, name, slug FROM brands_brands');
|
||||
const categoryNameIndex = new Set(categories.rows.map((r) => r.name.toLowerCase()));
|
||||
const brandNameIndex = new Set(brands.rows.map((r) => r.name.toLowerCase()));
|
||||
const slugIndex = new Set(
|
||||
[...categories.rows, ...brands.rows].map((r) => r.slug.toLowerCase()),
|
||||
);
|
||||
const parentBySlug = new Map(categories.rows.map((r) => [r.slug, r.id]));
|
||||
|
||||
const plan = buildImportPlan(categoryNameIndex, brandNameIndex, slugIndex);
|
||||
|
||||
// Validate that every parentSlug resolves to an existing category.
|
||||
const missingParents = new Set();
|
||||
for (const cat of plan.categories) {
|
||||
if (cat.parentSlug && !parentBySlug.has(cat.parentSlug)) missingParents.add(cat.parentSlug);
|
||||
}
|
||||
if (missingParents.size > 0) {
|
||||
console.error(`Missing parent categories: ${[...missingParents].join(', ')}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`Planned: ${plan.categories.length} categories, ${plan.brands.length} brands`);
|
||||
if (dryRun) {
|
||||
console.log('DRY RUN — nothing will be written.');
|
||||
for (const cat of plan.categories) {
|
||||
console.log(` + category ${cat.name} (slug=${cat.slug}, parent=${cat.parentSlug ?? 'ROOT'})`);
|
||||
}
|
||||
for (const brand of plan.brands) {
|
||||
console.log(` + brand ${brand.name} (slug=${brand.slug})`);
|
||||
}
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
for (const cat of plan.categories) {
|
||||
const parentId = cat.parentSlug ? parentBySlug.get(cat.parentSlug) : null;
|
||||
await pool.query(
|
||||
`INSERT INTO categories_categories (parent_id, name, slug, is_parent)
|
||||
VALUES ($1, $2, $3, false)
|
||||
ON CONFLICT (slug) DO NOTHING`,
|
||||
[parentId, cat.name, cat.slug],
|
||||
);
|
||||
console.log(` + category ${cat.name} (slug=${cat.slug}, parent=${cat.parentSlug ?? 'ROOT'})`);
|
||||
}
|
||||
|
||||
for (const brand of plan.brands) {
|
||||
await pool.query(
|
||||
`INSERT INTO brands_brands (name, slug) VALUES ($1, $2)
|
||||
ON CONFLICT (slug) DO NOTHING`,
|
||||
[brand.name, brand.slug],
|
||||
);
|
||||
console.log(` + brand ${brand.name} (slug=${brand.slug})`);
|
||||
}
|
||||
|
||||
console.log(`\nDone. Inserted ${plan.categories.length} categories and ${plan.brands.length} brands.`);
|
||||
} catch (error) {
|
||||
console.error('Error:', error.message);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await pool.end();
|
||||
}
|
||||
235
project/src/modules/categories/legacy/legacy-catalog.ts
Normal file
235
project/src/modules/categories/legacy/legacy-catalog.ts
Normal file
@@ -0,0 +1,235 @@
|
||||
/**
|
||||
* Legacy OpenCart catalog import (F-114).
|
||||
*
|
||||
* This module is a pure helper that:
|
||||
* - declares the legacy categories from `oc_category_description`
|
||||
* - normalises names (HTML entities, whitespace, case)
|
||||
* - classifies each entry as a real category, a brand, or excluded
|
||||
* - picks a parent category for each new category
|
||||
*
|
||||
* The seed script (`scripts/seed-legacy-categories.mjs`) consumes this helper
|
||||
* to import the entries idempotently. Domain logic is kept here so it can be
|
||||
* unit tested without touching the database.
|
||||
*/
|
||||
|
||||
export type LegacyKind = 'category' | 'brand' | 'exclude';
|
||||
|
||||
/**
|
||||
* A parent assignment. We resolve by `slug` against the existing categories
|
||||
* at runtime so the seed script can look up the UUID once, and so the
|
||||
* mapping is easy to read in tests.
|
||||
*/
|
||||
export interface LegacyEntry {
|
||||
/** Original OpenCart category_id. */
|
||||
ocId: number;
|
||||
/** Decoded, trimmed name. */
|
||||
name: string;
|
||||
/** Classification (category / brand / exclude). */
|
||||
kind: LegacyKind;
|
||||
/** Parent slug for new categories. undefined = root. */
|
||||
parentSlug?: string;
|
||||
}
|
||||
|
||||
/** Lowercases + collapses whitespace + decodes HTML entities. */
|
||||
export function normalizeName(raw: string): string {
|
||||
const decoded = decodeHtmlEntities(raw)
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
return decoded;
|
||||
}
|
||||
|
||||
/** Case-insensitive comparison for "already exists" checks. */
|
||||
export function isSameName(left: string, right: string): boolean {
|
||||
return normalizeName(left).toLowerCase() === normalizeName(right).toLowerCase();
|
||||
}
|
||||
|
||||
/** Minimal HTML entity decoder for the entities we know appear in the legacy table. */
|
||||
export function decodeHtmlEntities(raw: string): string {
|
||||
const named: Record<string, string> = {
|
||||
amp: '&',
|
||||
lt: '<',
|
||||
gt: '>',
|
||||
quot: '"',
|
||||
apos: "'",
|
||||
nbsp: ' ',
|
||||
};
|
||||
return raw.replace(/&([a-zA-Z]+|#\d+|#x[0-9a-fA-F]+);/g, (entity, body) => {
|
||||
if (body in named) return named[body] ?? entity;
|
||||
if (body.startsWith('#x') || body.startsWith('#X')) {
|
||||
const code = parseInt(body.slice(2), 16);
|
||||
if (Number.isFinite(code)) return String.fromCodePoint(code);
|
||||
} else if (body.startsWith('#')) {
|
||||
const code = parseInt(body.slice(1), 10);
|
||||
if (Number.isFinite(code)) return String.fromCodePoint(code);
|
||||
}
|
||||
return entity;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a name into a URL slug matching the categories/brands validator:
|
||||
* `^[a-z0-9]+(?:-[a-z0-9]+)*$`. Strips diacritics, drops non-alphanumerics.
|
||||
*/
|
||||
export function slugify(name: string): string {
|
||||
const stripped = normalizeName(name)
|
||||
.normalize('NFD')
|
||||
.replace(/[\u0300-\u036f]/g, '')
|
||||
.toLowerCase()
|
||||
.replace(/&/g, ' and ')
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '');
|
||||
return stripped || 'entry';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a slug that does not collide with `existing` (case-insensitive).
|
||||
* Appends `-2`, `-3`, … until a free one is found.
|
||||
*/
|
||||
export function uniqueSlug(base: string, existing: ReadonlySet<string>): string {
|
||||
const lower = base.toLowerCase();
|
||||
if (!existing.has(lower)) return base;
|
||||
let i = 2;
|
||||
while (existing.has(`${lower}-${i}`)) i += 1;
|
||||
return `${base}-${i}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* The legacy OpenCart rows. Each entry is classified and (for categories)
|
||||
* pinned to a parent by slug so the import is deterministic.
|
||||
* Excluded entries are legacy markers (`00 - SIN CODIGO`, etc.) that we
|
||||
* do not want to surface in the new catalog.
|
||||
*/
|
||||
export const LEGACY_ENTRIES: ReadonlyArray<LegacyEntry> = [
|
||||
{ ocId: 66, name: '00 - SIN CODIGO', kind: 'exclude' },
|
||||
{ ocId: 67, name: 'HERBALIST', kind: 'category', parentSlug: 'hierbas-infusiones' },
|
||||
{ ocId: 68, name: 'FOOD', kind: 'category', parentSlug: 'alimentacion' },
|
||||
{ ocId: 69, name: 'COSMETICS', kind: 'category', parentSlug: 'cosmetica-e-higiene' },
|
||||
{ ocId: 70, name: 'DIET AND NUTRITION', kind: 'category', parentSlug: 'suplementos' },
|
||||
{ ocId: 71, name: 'NUTS & SEEDS', kind: 'category', parentSlug: 'frutos-secos' },
|
||||
{ ocId: 72, name: 'BREAD & PASTRIES', kind: 'category', parentSlug: 'alimentacion' },
|
||||
{ ocId: 75, name: 'LEGUMES', kind: 'category', parentSlug: 'alimentacion' },
|
||||
{ ocId: 76, name: 'FLOUR & CEREALS', kind: 'category', parentSlug: 'alimentacion' },
|
||||
{ ocId: 77, name: 'PASTA & RICE', kind: 'category', parentSlug: 'alimentacion' },
|
||||
{ ocId: 78, name: 'CREAMS & JAMS', kind: 'category', parentSlug: 'cremas' },
|
||||
{ ocId: 79, name: 'FRUTAS Y VERDURAS', kind: 'category', parentSlug: 'alimentacion' },
|
||||
{ ocId: 80, name: 'SUGAR & SWEETENERS', kind: 'category', parentSlug: 'alimentacion' },
|
||||
{ ocId: 81, name: 'BEVERAGES', kind: 'category', parentSlug: 'alimentacion' },
|
||||
{ ocId: 82, name: 'OIL AND VINEGAR', kind: 'category', parentSlug: 'aceites' },
|
||||
{ ocId: 83, name: 'RAW FOOD', kind: 'category', parentSlug: 'alimentacion' },
|
||||
{ ocId: 84, name: 'HIERBAS MEDICINALES', kind: 'category', parentSlug: 'hierbas-infusiones' },
|
||||
{ ocId: 85, name: 'SUPLEMENTS', kind: 'category', parentSlug: 'suplementos' },
|
||||
{ ocId: 86, name: 'OILS & EXTRACTS', kind: 'category', parentSlug: 'aceites' },
|
||||
{ ocId: 87, name: 'FACIAL', kind: 'category', parentSlug: 'cosmetica-e-higiene' },
|
||||
{ ocId: 88, name: 'CORPORAL', kind: 'category', parentSlug: 'cosmetica-e-higiene' },
|
||||
{ ocId: 89, name: 'ASEO PERSONAL', kind: 'category', parentSlug: 'cosmetica-e-higiene' },
|
||||
{ ocId: 90, name: 'HOME', kind: 'category', parentSlug: 'hogar-y-mascotas' },
|
||||
{ ocId: 91, name: 'MACROBIOTIC', kind: 'category', parentSlug: 'alimentacion' },
|
||||
{ ocId: 93, name: 'SNACKS', kind: 'category', parentSlug: 'alimentacion' },
|
||||
{ ocId: 95, name: 'BOOKS', kind: 'category', parentSlug: 'hogar-y-mascotas' },
|
||||
{ ocId: 97, name: 'FRESH PRODUCTS', kind: 'category', parentSlug: 'alimentacion' },
|
||||
{ ocId: 102, name: 'WINE', kind: 'category', parentSlug: 'alimentacion' },
|
||||
{ ocId: 103, name: 'BEER', kind: 'category', parentSlug: 'alimentacion' },
|
||||
{ ocId: 104, name: 'VEGETAL MILKS', kind: 'category', parentSlug: 'alimentacion' },
|
||||
{ ocId: 105, name: 'JUICES', kind: 'category', parentSlug: 'alimentacion' },
|
||||
{ ocId: 106, name: 'SODAS', kind: 'category', parentSlug: 'alimentacion' },
|
||||
{ ocId: 107, name: 'CHILDREN', kind: 'category', parentSlug: 'alimentacion' },
|
||||
{ ocId: 108, name: 'BABYS & KIDS', kind: 'category', parentSlug: 'cosmetica-e-higiene' },
|
||||
{ ocId: 115, name: 'TEA & INFUSIONS', kind: 'category', parentSlug: 'hierbas-infusiones' },
|
||||
{ ocId: 116, name: 'SPICE & CONDIMENTS', kind: 'category', parentSlug: 'alimentacion' },
|
||||
{ ocId: 117, name: 'CHOCOLATE & SWEETS', kind: 'category', parentSlug: 'alimentacion' },
|
||||
{ ocId: 129, name: 'CLEANING', kind: 'category', parentSlug: 'limpieza-ecologica' },
|
||||
{ ocId: 127, name: 'PROVEEDORES', kind: 'category' }, // root
|
||||
// ── Marcas ───────────────────────────────────────────────────────────────
|
||||
{ ocId: 98, name: 'PROVEEDORES', kind: 'brand' },
|
||||
{ ocId: 99, name: 'BIOSPIRIT', kind: 'brand' },
|
||||
{ ocId: 100, name: 'SALUD VIVA', kind: 'brand' },
|
||||
{ ocId: 101, name: 'GLOBO NATURA', kind: 'brand' },
|
||||
{ ocId: 102, name: 'SOLGAR', kind: 'brand' },
|
||||
{ ocId: 103, name: 'VITAFOOD', kind: 'brand' },
|
||||
{ ocId: 104, name: 'EL GRANERO INTEGRAL', kind: 'brand' },
|
||||
{ ocId: 105, name: 'A.VOGEL', kind: 'brand' },
|
||||
{ ocId: 106, name: 'BIOCOP', kind: 'brand' },
|
||||
{ ocId: 107, name: 'QBIO', kind: 'brand' },
|
||||
{ ocId: 108, name: 'LA FINESTRA SUL CIELO', kind: 'brand' },
|
||||
{ ocId: 109, name: 'CADIDIET', kind: 'brand' },
|
||||
{ ocId: 110, name: 'COMPLEMENTOS Y NUTRICIÓN', kind: 'brand' },
|
||||
{ ocId: 111, name: 'CHISVERT', kind: 'brand' },
|
||||
{ ocId: 112, name: 'SUPERFOOD', kind: 'brand' },
|
||||
{ ocId: 113, name: 'DIETISUR', kind: 'brand' },
|
||||
{ ocId: 114, name: 'GOURMET BIO', kind: 'brand' },
|
||||
{ ocId: 116, name: 'NATURALMENTE MEDITERRANEO', kind: 'brand' },
|
||||
{ ocId: 117, name: 'NATURAL', kind: 'brand' },
|
||||
{ ocId: 126, name: 'GOURMET CASH', kind: 'brand' },
|
||||
{ ocId: 127, name: 'NUTRINAT', kind: 'brand' },
|
||||
{ ocId: 131, name: 'COMERCIAL GARZA', kind: 'brand' },
|
||||
{ ocId: 132, name: 'ARTESANÍA AGRÍCOLA', kind: 'brand' },
|
||||
{ ocId: 133, name: 'DAS BROT', kind: 'brand' },
|
||||
{ ocId: 134, name: 'DULCES LISSEN', kind: 'brand' },
|
||||
{ ocId: 135, name: 'LEMONPHARMA', kind: 'brand' },
|
||||
{ ocId: 136, name: 'VEKINE', kind: 'brand' },
|
||||
{ ocId: 137, name: 'NAAY BOTANICALS', kind: 'brand' },
|
||||
{ ocId: 138, name: 'LAMBERTS', kind: 'brand' },
|
||||
{ ocId: 139, name: 'YODETIENDAS', kind: 'brand' },
|
||||
{ ocId: 140, name: 'NATURCOSMETIKA', kind: 'brand' },
|
||||
{ ocId: 142, name: 'TONGIL', kind: 'brand' },
|
||||
{ ocId: 96, name: 'SUPLEMENTS', kind: 'exclude' }, // duplicate of 85 (we keep 85 as category)
|
||||
{ ocId: 141, name: '01 - PRODUCTOS DESCATALOGADOS', kind: 'exclude' },
|
||||
{ ocId: 143, name: '02 - PRODUCTOS RAPIDOS', kind: 'exclude' },
|
||||
{ ocId: 145, name: 'GRANOLA', kind: 'category', parentSlug: 'alimentacion' },
|
||||
];
|
||||
|
||||
export interface ImportPlanCategory {
|
||||
ocId: number;
|
||||
name: string;
|
||||
slug: string;
|
||||
parentSlug?: string;
|
||||
}
|
||||
|
||||
export interface ImportPlanBrand {
|
||||
ocId: number;
|
||||
name: string;
|
||||
slug: string;
|
||||
}
|
||||
|
||||
export interface ImportPlan {
|
||||
categories: ImportPlanCategory[];
|
||||
brands: ImportPlanBrand[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the import plan from `LEGACY_ENTRIES`, skipping entries whose
|
||||
* name (case-insensitive) is already present in `existingCategoryNames` or
|
||||
* `existingBrandNames`. Slugs are uniqued against `existingSlugs`.
|
||||
*/
|
||||
export function buildImportPlan(
|
||||
existingCategoryNames: ReadonlySet<string>,
|
||||
existingBrandNames: ReadonlySet<string>,
|
||||
existingSlugs: ReadonlySet<string>,
|
||||
): ImportPlan {
|
||||
const categories: ImportPlanCategory[] = [];
|
||||
const brands: ImportPlanBrand[] = [];
|
||||
const usedSlugs = new Set<string>(existingSlugs);
|
||||
|
||||
for (const entry of LEGACY_ENTRIES) {
|
||||
if (entry.kind === 'exclude') continue;
|
||||
if (entry.kind === 'category') {
|
||||
if (existingCategoryNames.has(entry.name.toLowerCase())) continue;
|
||||
const base = slugify(entry.name);
|
||||
const slug = uniqueSlug(base, usedSlugs);
|
||||
usedSlugs.add(slug.toLowerCase());
|
||||
categories.push({
|
||||
ocId: entry.ocId,
|
||||
name: entry.name,
|
||||
slug,
|
||||
parentSlug: entry.parentSlug,
|
||||
});
|
||||
} else if (entry.kind === 'brand') {
|
||||
if (existingBrandNames.has(entry.name.toLowerCase())) continue;
|
||||
const base = slugify(entry.name);
|
||||
const slug = uniqueSlug(base, usedSlugs);
|
||||
usedSlugs.add(slug.toLowerCase());
|
||||
brands.push({ ocId: entry.ocId, name: entry.name, slug });
|
||||
}
|
||||
}
|
||||
return { categories, brands };
|
||||
}
|
||||
105
project/src/modules/categories/tests/legacy-catalog.test.ts
Normal file
105
project/src/modules/categories/tests/legacy-catalog.test.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
buildImportPlan,
|
||||
decodeHtmlEntities,
|
||||
isSameName,
|
||||
LEGACY_ENTRIES,
|
||||
normalizeName,
|
||||
slugify,
|
||||
uniqueSlug,
|
||||
} from '../legacy/legacy-catalog.js';
|
||||
|
||||
describe('normalizeName', () => {
|
||||
it('trims and collapses whitespace', () => {
|
||||
expect(normalizeName(' hello world ')).toBe('hello world');
|
||||
});
|
||||
|
||||
it('decodes HTML entities', () => {
|
||||
expect(normalizeName('NUTS & SEEDS')).toBe('NUTS & SEEDS');
|
||||
expect(normalizeName('A & B C')).toBe('A & B C');
|
||||
});
|
||||
|
||||
it('keeps unknown entities as-is', () => {
|
||||
expect(normalizeName('foo &bogus; bar')).toBe('foo &bogus; bar');
|
||||
});
|
||||
});
|
||||
|
||||
describe('decodeHtmlEntities', () => {
|
||||
it('decodes numeric entities', () => {
|
||||
expect(decodeHtmlEntities('&')).toBe('&');
|
||||
expect(decodeHtmlEntities('&')).toBe('&');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isSameName', () => {
|
||||
it('compares case-insensitively after normalization', () => {
|
||||
expect(isSameName('nuez', 'NUEZ')).toBe(true);
|
||||
expect(isSameName('NUTS & SEEDS', 'nuts & seeds')).toBe(true);
|
||||
expect(isSameName('Hierbas', 'Hierba')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('slugify', () => {
|
||||
it('lowercases, strips diacritics and replaces non-alphanumerics', () => {
|
||||
expect(slugify('Frutas y Verduras')).toBe('frutas-y-verduras');
|
||||
expect(slugify('NUTS & SEEDS')).toBe('nuts-and-seeds');
|
||||
expect(slugify('A. Vogel')).toBe('a-vogel');
|
||||
expect(slugify('La Finestra Sul Cielo')).toBe('la-finestra-sul-cielo');
|
||||
});
|
||||
|
||||
it('falls back to "entry" for empty input', () => {
|
||||
expect(slugify('---')).toBe('entry');
|
||||
});
|
||||
});
|
||||
|
||||
describe('uniqueSlug', () => {
|
||||
it('returns the base when free', () => {
|
||||
expect(uniqueSlug('aceites', new Set(['frutos-secos']))).toBe('aceites');
|
||||
});
|
||||
|
||||
it('appends a numeric suffix on collision', () => {
|
||||
expect(uniqueSlug('aceites', new Set(['aceites', 'aceites-2']))).toBe('aceites-3');
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildImportPlan', () => {
|
||||
it('classifies every legacy entry as category, brand, or exclude', () => {
|
||||
const kinds = new Set(LEGACY_ENTRIES.map((e) => e.kind));
|
||||
expect(kinds.has('category')).toBe(true);
|
||||
expect(kinds.has('brand')).toBe(true);
|
||||
expect(kinds.has('exclude')).toBe(true);
|
||||
});
|
||||
|
||||
it('skips entries whose name already exists in the catalog (case-insensitive)', () => {
|
||||
const existingCategories = new Set(['nuts & seeds']);
|
||||
const existingBrands = new Set<string>();
|
||||
const existingSlugs = new Set<string>();
|
||||
const plan = buildImportPlan(existingCategories, existingBrands, existingSlugs);
|
||||
expect(plan.categories.find((c) => c.name === 'NUTS & SEEDS')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('skips excluded legacy markers', () => {
|
||||
const plan = buildImportPlan(new Set(), new Set(), new Set());
|
||||
expect(plan.categories.find((c) => c.name.startsWith('00 -'))).toBeUndefined();
|
||||
expect(plan.categories.find((c) => c.name.startsWith('01 -'))).toBeUndefined();
|
||||
expect(plan.categories.find((c) => c.name.startsWith('02 -'))).toBeUndefined();
|
||||
});
|
||||
|
||||
it('produces unique slugs across categories and brands', () => {
|
||||
const plan = buildImportPlan(new Set(), new Set(), new Set());
|
||||
const slugs = [...plan.categories.map((c) => c.slug), ...plan.brands.map((b) => b.slug)];
|
||||
expect(new Set(slugs).size).toBe(slugs.length);
|
||||
});
|
||||
|
||||
it('attaches the declared parent to each category', () => {
|
||||
const plan = buildImportPlan(new Set(), new Set(), new Set());
|
||||
const nuts = plan.categories.find((c) => c.name === 'NUTS & SEEDS');
|
||||
expect(nuts?.parentSlug).toBe('frutos-secos');
|
||||
});
|
||||
|
||||
it('does not include brands as categories', () => {
|
||||
const plan = buildImportPlan(new Set(), new Set(), new Set());
|
||||
expect(plan.categories.find((c) => c.name === 'SOLGAR')).toBeUndefined();
|
||||
expect(plan.brands.find((b) => b.name === 'SOLGAR')).toBeDefined();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user