e2e changes
This commit is contained in:
@@ -71,6 +71,11 @@ export const productsApi = {
|
||||
* SKU lookup in `importSheetProducts` then read as a product with no
|
||||
* `productid`: every sheet import resolved zero ids and wrote no locations
|
||||
* and no stock. Flattened here so no caller sees the grouping.
|
||||
*
|
||||
* Nothing calls this today — the importer that did now gets its ids from the
|
||||
* create response. Kept because it is the only wrapper for a real endpoint
|
||||
* and the grouping above is the sort of thing the next caller would be
|
||||
* caught by all over again.
|
||||
*/
|
||||
allProducts: (tenantid: number) =>
|
||||
api
|
||||
@@ -93,7 +98,18 @@ export const productsApi = {
|
||||
importFromCatalogue: (rows: ImportCatalogueProductRequest[]) =>
|
||||
api.post<unknown>(`${WEB}/products/importcatalogueproduct`, rows),
|
||||
|
||||
/** Single product only — the backend parses one object, not an array. */
|
||||
/**
|
||||
* Single product only — the backend parses one object, not an array.
|
||||
*
|
||||
* Answers with the created row, `productid` included. It used to echo back
|
||||
* the request body, which meant `productid: 0` every time: the id is
|
||||
* assigned by the database and nothing read it back. Callers that needed it
|
||||
* — and pricing and stocking a product both do — had to re-read the
|
||||
* catalogue and find their own row again by SKU.
|
||||
*
|
||||
* The payload arrives under `data` rather than `details`, which the client
|
||||
* already handles.
|
||||
*/
|
||||
createProduct: (product: Partial<Product>) =>
|
||||
api.post<Product>(`${WEB}/products/create`, product),
|
||||
|
||||
@@ -267,9 +283,12 @@ export async function importSheetProducts(
|
||||
const subcategoryIdFor = (row: SheetProductRow): number =>
|
||||
aisleIdForCategory(row.category, aisleIds) || Number(row.subcategoryid) || 0;
|
||||
|
||||
const locationRows: ProductLocationRequest[] = [];
|
||||
const stockRows: ProductStockRequest[] = [];
|
||||
|
||||
for (const [index, row] of rows.entries()) {
|
||||
try {
|
||||
await productsApi.createProduct({
|
||||
const created = await productsApi.createProduct({
|
||||
tenantid,
|
||||
productname: row.productname,
|
||||
productsku: row.productsku,
|
||||
@@ -286,51 +305,61 @@ export async function importSheetProducts(
|
||||
productdesc: row.productdesc,
|
||||
productstatus: 'Active',
|
||||
});
|
||||
|
||||
/*
|
||||
The id comes back from the create now.
|
||||
|
||||
This loop used to collect SKUs, then read the tenant's ENTIRE catalogue
|
||||
back, build a SKU→product map and match its own rows against it, because
|
||||
`POST /products/create` answered `productid: 0`. That is fixed on the
|
||||
backend — the id is the database's and it is returned — so the second
|
||||
read and the matching are both gone.
|
||||
|
||||
Worth saying what the old way actually cost, because it was not only the
|
||||
extra request. Matching on SKU means matching on a column nothing
|
||||
enforces: this importer creates duplicates on re-upload by design, and
|
||||
the map kept the LAST row for a SKU, so a second upload sent the new
|
||||
product's price and stock to whichever copy happened to win. A row whose
|
||||
SKU was blank, or trimmed differently by the sheet, could not be found at
|
||||
all and was reported as "Created, but could not be found again by SKU" —
|
||||
a message about the console's own bookkeeping that a shop could do
|
||||
nothing with.
|
||||
|
||||
A zero here would be worse than the old behaviour, so it is checked
|
||||
rather than assumed: the product exists either way, and saying so is more
|
||||
use than silently pricing product 0.
|
||||
*/
|
||||
if (!created?.productid) {
|
||||
failures.push({
|
||||
row,
|
||||
reason: 'Created, but the server did not return its id — price and stock were not set',
|
||||
});
|
||||
onProgress?.(index + 1, rows.length);
|
||||
continue;
|
||||
}
|
||||
|
||||
createdSkus.push(row.productsku);
|
||||
locationRows.push({
|
||||
tenantid,
|
||||
locationid,
|
||||
productid: created.productid,
|
||||
price: row.retailprice,
|
||||
status: 'available',
|
||||
});
|
||||
stockRows.push({
|
||||
tenantid,
|
||||
locationid,
|
||||
productid: created.productid,
|
||||
quantity: row.quantity,
|
||||
stocktype: 'in',
|
||||
status: 'Active',
|
||||
});
|
||||
} catch (error) {
|
||||
failures.push({ row, reason: error instanceof Error ? error.message : 'Create failed' });
|
||||
}
|
||||
onProgress?.(index + 1, rows.length);
|
||||
}
|
||||
|
||||
if (createdSkus.length === 0) {
|
||||
return { created: 0, linked: 0, stocked: 0, failures };
|
||||
}
|
||||
|
||||
// Resolve the ids the create endpoint refused to hand back.
|
||||
const all = await productsApi.allProducts(tenantid);
|
||||
const bySku = new Map<string, Product>();
|
||||
for (const product of all) {
|
||||
if (product.productsku) bySku.set(product.productsku, product);
|
||||
}
|
||||
|
||||
const locationRows: ProductLocationRequest[] = [];
|
||||
const stockRows: ProductStockRequest[] = [];
|
||||
|
||||
for (const row of rows) {
|
||||
if (!createdSkus.includes(row.productsku)) continue;
|
||||
const product = bySku.get(row.productsku);
|
||||
if (!product) {
|
||||
failures.push({ row, reason: 'Created, but could not be found again by SKU' });
|
||||
continue;
|
||||
}
|
||||
locationRows.push({
|
||||
tenantid,
|
||||
locationid,
|
||||
productid: product.productid,
|
||||
price: row.retailprice,
|
||||
status: 'available',
|
||||
});
|
||||
stockRows.push({
|
||||
tenantid,
|
||||
locationid,
|
||||
productid: product.productid,
|
||||
quantity: row.quantity,
|
||||
stocktype: 'in',
|
||||
status: 'Active',
|
||||
});
|
||||
}
|
||||
|
||||
if (locationRows.length > 0) await productsApi.createProductLocations(locationRows);
|
||||
if (stockRows.length > 0) await productsApi.createProductStock(stockRows);
|
||||
|
||||
|
||||
@@ -118,6 +118,19 @@ export interface TenantInfo {
|
||||
/** Capitalised on the wire. */
|
||||
Accountname?: string;
|
||||
status: string;
|
||||
/**
|
||||
* How many outlets this merchant has.
|
||||
*
|
||||
* Sent by `getalltenants` only, so it is optional — every other endpoint
|
||||
* returning a `TenantInfo` leaves it out.
|
||||
*
|
||||
* The store list used to work this out for itself, by counting how many
|
||||
* times a tenantid appeared, on the belief that the endpoint returned one
|
||||
* row per tenant-location pair. It returns one row per tenant and always
|
||||
* has, so the count was always 1 and the platform's "Branches" total was
|
||||
* really its tenant total.
|
||||
*/
|
||||
branchcount?: number;
|
||||
}
|
||||
|
||||
export interface TenantLocation {
|
||||
@@ -211,6 +224,21 @@ export interface Product {
|
||||
productimage?: string;
|
||||
/** A JSON-encoded array of URLs, held as a string. Parse before use. */
|
||||
productimages?: string;
|
||||
/**
|
||||
* What the global catalogue said about this product when it was imported —
|
||||
* a JSON-encoded object, held as a string. Parse with `catalogueFactsOf`.
|
||||
*
|
||||
* Carries the fields the product table has no columns for: the FSSAI
|
||||
* licence, nutrition, highlights, providers, the typical retail range and
|
||||
* the variant key. The import used to drop all of them and the drawer went
|
||||
* back to the catalogue on every open, which stopped working the moment a
|
||||
* re-scrape retired the source row — taking a licence number off a product
|
||||
* the shop was still selling.
|
||||
*
|
||||
* Absent on anything that did not come from the catalogue. The drawer still
|
||||
* falls back to the live lookup for those.
|
||||
*/
|
||||
cataloguefacts?: string;
|
||||
productdesc?: string;
|
||||
productsku?: string;
|
||||
brandid?: number;
|
||||
|
||||
Reference in New Issue
Block a user