category
This commit is contained in:
@@ -11,21 +11,40 @@ import { parseProductSheet } from './parseProductSheet';
|
||||
|
||||
const sheet = (csv: string) => new File([csv], 'products.csv', { type: 'text/csv' });
|
||||
|
||||
// The template ships a `Category` column holding a NAME, for the ingest service.
|
||||
// The template ships a `Category` column holding a NAME.
|
||||
//
|
||||
// It used to be aliased onto `categoryid`, run through toNumber, and become 0 —
|
||||
// silently, and while counting as a mapped column so it never showed up as
|
||||
// ignored either.
|
||||
test('a text Category column is not read as a category id', async () => {
|
||||
// ignored either. The column is read again now, but as a NAME feeding rule 1 of
|
||||
// the category ladder; the id it eventually gets is resolved per tenant at
|
||||
// import time. The guarantee that matters is unchanged and asserted below: a
|
||||
// text category never becomes a categoryid.
|
||||
test('a text Category column is read as a name, never as an id', async () => {
|
||||
const parsed = await parseProductSheet(
|
||||
sheet('Product Name,Brand,Category,MRP\nBritannia Marie Gold 250g,Britannia,Biscuits,30\n'),
|
||||
);
|
||||
|
||||
assert.equal(parsed.rows.length, 1);
|
||||
assert.equal(parsed.rows[0]?.categoryid, 0, 'the console supplies the category, not the sheet');
|
||||
assert.ok(
|
||||
parsed.unmappedColumns.some((column) => /category/i.test(column)),
|
||||
`Category should be reported as a column we do not read, got ${JSON.stringify(parsed.unmappedColumns)}`,
|
||||
assert.equal(parsed.rows[0]?.categoryid, 0, 'the id is resolved at import, not read from the sheet');
|
||||
assert.equal(parsed.rows[0]?.category, 'Biscuits', 'the sheet is authoritative for the name');
|
||||
});
|
||||
|
||||
test('a sheet with no Category column still gets one from the ladder', async () => {
|
||||
// Rule 2: the keyword registry, over the product name.
|
||||
const parsed = await parseProductSheet(
|
||||
sheet('Product Name,MRP\nAmul Gold Full Cream Milk 1L,60\n'),
|
||||
);
|
||||
assert.equal(parsed.rows[0]?.category, 'Dairy');
|
||||
assert.equal(parsed.rows[0]?.categoryid, 0);
|
||||
});
|
||||
|
||||
test('a Category column holding a number is treated as an id, not a name', async () => {
|
||||
// The ladder's all-digits guard, reached through the parser: an operator who
|
||||
// types 1001 in the Category column gets the ladder's answer, not "1001".
|
||||
const parsed = await parseProductSheet(
|
||||
sheet('Product Name,Category,MRP\nAmul Gold Full Cream Milk 1L,1001,60\n'),
|
||||
);
|
||||
assert.equal(parsed.rows[0]?.category, 'Dairy');
|
||||
});
|
||||
|
||||
// Even a literal categoryid column is ignored now: a number nobody can verify
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
*/
|
||||
|
||||
import type { SheetProductRow } from '@/api/products';
|
||||
import { resolveCategory } from '@/features/store-admin/productCategory';
|
||||
|
||||
/**
|
||||
* `xlsx` is around 400kB and only the spreadsheet path ever touches it, so it is
|
||||
@@ -40,6 +41,20 @@ const COLUMN_ALIASES: Record<keyof SheetProductRow, string[]> = {
|
||||
* service, which reads it as a name; it was never ours.
|
||||
*/
|
||||
categoryid: [],
|
||||
/**
|
||||
* The sheet's own category NAME — rule 1 of the ladder.
|
||||
*
|
||||
* This alias was removed once, for a good reason: it pointed at
|
||||
* `categoryid`, so a text cell reading "Biscuits" was parsed as a number,
|
||||
* failed, and became 0 — which hides a product from shoppers entirely,
|
||||
* because the customer app rejects categoryid 0.
|
||||
*
|
||||
* It is safe now because it no longer feeds an id. It feeds a NAME, which
|
||||
* `resolveCategory` treats as authoritative and `resolvecategories`
|
||||
* exchanges for a real per-tenant id at import time. The all-digits guard in
|
||||
* the ladder catches an operator who still types a number here.
|
||||
*/
|
||||
category: ['category', 'categoryname', 'aisle', 'department', 'section'],
|
||||
subcategoryid: [],
|
||||
retailprice: ['retailprice', 'price', 'mrp', 'sellingprice'],
|
||||
productcost: ['productcost', 'cost', 'purchaseprice', 'costprice'],
|
||||
@@ -140,12 +155,24 @@ export async function parseProductSheet(file: File): Promise<ParsedSheet> {
|
||||
const productcost = toNumber(picked.productcost);
|
||||
const taxpercent = toNumber(picked.taxpercent) ?? 0;
|
||||
const quantity = toNumber(picked.quantity) ?? 0;
|
||||
// Always zero — see COLUMN_ALIASES. The category is the operator's answer
|
||||
// for the whole upload, filled in by `buildImportRequests`, not a per-row
|
||||
// number read from the file.
|
||||
// Still zero. The id is per-tenant and cannot be known from a file — it is
|
||||
// resolved from the NAME below, once, at import time.
|
||||
const categoryid = 0;
|
||||
const subcategoryid = 0;
|
||||
|
||||
/*
|
||||
The category, by the same deterministic ladder the catalogue team runs:
|
||||
the sheet's own value, then the keyword registry, then the commodity
|
||||
lexicon, then "General". Worked out here rather than in the preview so
|
||||
the row an operator reviews and the row that is imported cannot disagree.
|
||||
*/
|
||||
const category = resolveCategory({
|
||||
title: productname,
|
||||
description: String(picked.productdesc ?? '').trim(),
|
||||
sheetValue: String(picked.category ?? '').trim(),
|
||||
packSize: [picked.unitvalue, picked.productunit].filter(Boolean).join(' '),
|
||||
}).category;
|
||||
|
||||
// Only the product name is required, and that is the ingest service's rule
|
||||
// rather than ours: everything else is optional and enriched when blank,
|
||||
// and even the brand is inferred from the name.
|
||||
@@ -190,6 +217,7 @@ export async function parseProductSheet(file: File): Promise<ParsedSheet> {
|
||||
unitvalue: String(picked.unitvalue ?? '').trim() || undefined,
|
||||
productbrand: String(picked.productbrand ?? '').trim() || undefined,
|
||||
productdesc: String(picked.productdesc ?? '').trim() || undefined,
|
||||
category,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user