From 670d193f36ef10aad32cd633e6bd95ca12810dfd Mon Sep 17 00:00:00 2001 From: abhishek Date: Fri, 28 Aug 2026 12:22:03 +0530 Subject: [PATCH] changes with app products --- scripts/appfix.mjs | 81 +++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 73 insertions(+), 8 deletions(-) diff --git a/scripts/appfix.mjs b/scripts/appfix.mjs index a530f0b..03fe151 100644 --- a/scripts/appfix.mjs +++ b/scripts/appfix.mjs @@ -78,7 +78,37 @@ async function tenantsWithOrphans() { const groups = (await get('/products/getallproducts', { tenantid: tenant.tenantid })) ?? []; const products = (Array.isArray(groups) ? groups : []).flatMap((g) => g?.products ?? []); const orphans = products.filter((p) => !p.categoryid); - if (orphans.length > 0) out.push({ tenant, orphans }); + if (orphans.length === 0) continue; + + /** + * Where each orphan already sits. + * + * `getallproducts` is tenant-wide and carries NO locationid — the first + * version of this read `p.locationid` off it, got undefined, sent 0, and + * every repair came back "missing required field(s): locationid". Nothing + * was written, which is the one good thing about that failure. + * + * The outlet matters beyond passing validation. Import writes a + * productlocations row, so naming an outlet the product is NOT on would put + * it on that shelf — silently extending the product's reach as a side + * effect of a repair. Only an outlet where it is already stocked is safe: + * there the upsert lands on the existing row. + */ + const locations = (await get('/tenants/gettenantlocations', { tenantid: tenant.tenantid })) ?? []; + const placement = new Map(); + for (const loc of Array.isArray(locations) ? locations : []) { + const rows = (await get('/products/getlocationproducts', { + tenantid: tenant.tenantid, + locationid: loc.locationid, + pageno: 1, + pagesize: 500, + })) ?? []; + for (const row of Array.isArray(rows) ? rows : []) { + if (!placement.has(row.productid)) placement.set(row.productid, { loc, row }); + } + } + + out.push({ tenant, orphans, placement }); } return out; } @@ -114,7 +144,7 @@ let repaired = 0; let unchanged = 0; let refused = 0; -for (const { tenant, orphans } of work) { +for (const { tenant, orphans, placement } of work) { const categoryid = await categoryFor(tenant.tenantid); console.log(`${tenant.tenantid} ${tenant.tenantname} — ${orphans.length} to repair`); @@ -136,25 +166,60 @@ for (const { tenant, orphans } of work) { continue; } + const at = placement.get(p.productid); + if (!at) { + console.log(` ${label} — SKIPPED: not stocked at any outlet, so there is no safe row to repair through`); + refused++; + continue; + } + + /** + * A product with no price must not be made visible. + * + * Repairing the category is what puts a product in front of shoppers, and + * the app has no price floor — `GetProducts` filters on category and outlet + * and nothing else. Fixing a product priced at 0 would not "restore" it; it + * would publish a free one. Idhayam Sesame Oil 500ml (7083) is in exactly + * this state, priced nowhere, and it wants a price before it wants a + * category. + */ + const price = Number(p.retailprice ?? 0); + if (!(price > 0)) { + console.log(` ${label} — SKIPPED: no price set. Repairing this would list it at ₹0. Price it first.`); + refused++; + continue; + } + if (!apply) { - console.log(` ${label} → categoryid ${categoryid}`); + console.log( + ` ${label} → categoryid ${categoryid} (via ${at.loc.locationname ?? at.loc.locationid}, price ₹${price} unchanged)`, + ); continue; } const result = await post('/products/importcatalogueproduct', [ { tenantid: tenant.tenantid, - // Any outlet the product already sits at; the upsert leaves the row - // alone and the category is written on `products`, which is tenant-wide. - locationid: p.locationid ?? p.applocationid ?? 0, + // An outlet the product ALREADY sits at, so the upsert lands on the + // existing row instead of putting it on a new shelf. The category + // itself is written to `products`, which is tenant-wide, so one call + // fixes the product everywhere. + locationid: at.loc.locationid, brand: p.productbrand, catalogueid: p.catalogueid, categoryid, subcategoryid: p.subcategoryid ?? 0, + // Zero, so CreateProductLocation writes no stock ledger entry — it only + // records stock when quantity > 0. These products already have their + // stock and a repair must not add to it. quantity: 0, stocktype: 'in', - status: p.productstatus || 'Active', - retailprice: p.retailprice ?? 0, + status: at.row.productstatus || p.productstatus || 'Active', + // The product's OWN current values, re-sent unchanged. The same call + // updates pricing, and the location upsert sets productlocations.price + // from retailprice — verified equal for every product being repaired + // here, so this round-trips rather than overwriting an outlet price. + retailprice: price, productcost: p.productcost ?? 0, taxpercent: p.taxpercent ?? 0, },