From 5345809951a7d23616c151d0c8a9dae063b57ebd Mon Sep 17 00:00:00 2001 From: abhishek Date: Thu, 3 Sep 2026 15:08:22 +0530 Subject: [PATCH] price fix for app --- src/features/store-admin/PublishDialog.tsx | 14 +++++- .../store-admin/pages/InventoryPage.tsx | 11 +++++ src/features/store-admin/publishPlan.test.ts | 46 ++++++++++++++++++- src/features/store-admin/publishPlan.ts | 22 ++++++--- .../store-user/pages/StoreProductsPage.tsx | 6 +++ 5 files changed, 90 insertions(+), 9 deletions(-) diff --git a/src/features/store-admin/PublishDialog.tsx b/src/features/store-admin/PublishDialog.tsx index 35d8871..ce6341a 100644 --- a/src/features/store-admin/PublishDialog.tsx +++ b/src/features/store-admin/PublishDialog.tsx @@ -68,7 +68,19 @@ export function PublishDialog({ products, tenantid, branchCount, onClose }: Publ * the selection. */ const needPrice = needingPrice(products); - const askForPrice = needPrice.length > 0; + /** + * Whether to show the price field. + * + * Always for ONE product, even when it already has a price — that is the + * "Change price" case, and a dialog that hides the field because the value + * exists is a dialog that cannot change it. That was the bug: the field was + * shown only when something LACKED a price, so a priced product opened a form + * with nothing to edit and re-published the figure it already had. + * + * For a batch, only when something lacks one: a spreadsheet import carries a + * price on every row, and asking again is asking a question already answered. + */ + const askForPrice = needPrice.length > 0 || !isBulk; const priceValue = Number(price); // The backend refuses `price <= 0` with "set a price before publishing — a diff --git a/src/features/store-admin/pages/InventoryPage.tsx b/src/features/store-admin/pages/InventoryPage.tsx index e5b9252..b20b19f 100644 --- a/src/features/store-admin/pages/InventoryPage.tsx +++ b/src/features/store-admin/pages/InventoryPage.tsx @@ -697,6 +697,16 @@ function StockPanel() { ) : ( + <> + {/* Said once, above the table. "Opening" reads to a shopkeeper as + "the stock I first loaded", and it is not — it is the balance + carried in from before today, so a delivery received this + morning shows under In and not Opening. The arithmetic is + right; only the word was ambiguous. */} + + Today’s ledger. Opening is what was on the shelf before today — + stock received today counts under In and becomes tomorrow’s opening. +
@@ -741,6 +751,7 @@ function StockPanel() {
+ )} diff --git a/src/features/store-admin/publishPlan.test.ts b/src/features/store-admin/publishPlan.test.ts index b5d4285..971af94 100644 --- a/src/features/store-admin/publishPlan.test.ts +++ b/src/features/store-admin/publishPlan.test.ts @@ -41,7 +41,10 @@ may carry only the master. `effectivePrice` already resolves that, and publish has to use the same resolution or it would "fill in" a price that exists. */ test('the master price counts as having a price', () => { - const plan = planPublish([product(1, 0, 175)], { price: 99, taxpercent: 0 }); + // Two products, because this is the BATCH rule: with one, the typed figure + // is an edit of that product and wins by design. The behaviour under test — + // that retailprice counts as "has a price" — is unchanged. + const plan = planPublish([product(1, 0, 175), product(2, 0)], { price: 99, taxpercent: 0 }); assert.equal(plan[0]?.price, 175); }); @@ -53,10 +56,49 @@ test('tax follows the same rule as price', () => { // Zero is a real tax rate and must survive, not fall through to the fallback. test('a tax rate of zero is kept, not treated as missing', () => { - const plan = planPublish([product(1, 100, 0, 0)], { price: 0, taxpercent: 18 }); + // Zero-rated goods are real — unpackaged staples are 0% GST — so 0 must not + // be read as "no tax set". Batch context, as above. + const plan = planPublish([product(1, 100, 0, 0), product(2, 100)], { price: 0, taxpercent: 18 }); assert.equal(plan[0]?.taxpercent, 0); }); test('only the unpriced products are counted as needing one', () => { assert.equal(needingPrice([product(1, 395), product(2, 0), product(3, 0, 88)]).length, 1); }); + +/* +Changing a price that already exists. + +`own > 0 ? own : typed` protected a bulk release — seventeen imported products +must not all take one typed figure — but it also made "Change price" impossible: +the dialog sent the product's existing price straight back, so a price could be +set once and never corrected. + +One product is an edit of that product. Several is a release of a batch. The two +need opposite defaults, and these tests pin both. +*/ + +test('one product takes the typed price, even when it already has one', () => { + const [line] = planPublish([product(7, 395)], { price: 250, taxpercent: 5 }); + assert.equal(line?.price, 250); +}); + +test('one product takes the typed tax too', () => { + const [line] = planPublish([product(7, 395)], { price: 250, taxpercent: 12 }); + assert.equal(line?.taxpercent, 12); +}); + +test('a batch still protects the prices it already has', () => { + // The failure this guards: ₹395 rice, ₹30 crisps and ₹105 cleaner released + // together, all set to whichever figure prefilled from the first. + const lines = planPublish( + [product(1, 395), product(2, 30), product(3, 105)], + { price: 999, taxpercent: 5 }, + ); + assert.deepEqual(lines.map((line) => line.price), [395, 30, 105]); +}); + +test('a batch still fills in the ones with no price', () => { + const lines = planPublish([product(1, 395), product(2, 0)], { price: 60, taxpercent: 5 }); + assert.deepEqual(lines.map((line) => line.price), [395, 60]); +}); diff --git a/src/features/store-admin/publishPlan.ts b/src/features/store-admin/publishPlan.ts index bdd0980..acad19a 100644 --- a/src/features/store-admin/publishPlan.ts +++ b/src/features/store-admin/publishPlan.ts @@ -10,9 +10,15 @@ import { effectivePrice } from './productState'; * with one typed figure sets all seventeen to it and loses the originals, with * nothing to restore them from. * - * The rule: a product that HAS a price is released at its own. The typed figure - * fills in only for one that has none, which after a spreadsheet import is - * usually nothing at all. + * The rule for a BATCH: a product that has a price is released at its own, and + * the typed figure fills in only for one that has none — which after a + * spreadsheet import is usually nothing at all. + * + * The rule for ONE product is the opposite, and it has to be: "Change price" + * opens on a single product, and a form whose typed value is discarded because + * the field already had a value is not a form. That was the bug — a price could + * be set once and never corrected, because `own > 0` sent the old figure back + * every time. */ export interface PublishLine { productid: number; @@ -22,15 +28,19 @@ export interface PublishLine { export function planPublish( products: readonly Product[], - fallback: { price: number; taxpercent: number }, + typed: { price: number; taxpercent: number }, ): PublishLine[] { + // One product is an edit of that product: the merchant is looking at its + // price, has typed another, and means it. + const isSingle = products.length === 1; + return products.map((product) => { const own = effectivePrice(product); const ownTax = Number(product.taxpercent ?? NaN); return { productid: product.productid, - price: own > 0 ? own : fallback.price, - taxpercent: Number.isFinite(ownTax) ? ownTax : fallback.taxpercent, + price: isSingle || own <= 0 ? typed.price : own, + taxpercent: isSingle || !Number.isFinite(ownTax) ? typed.taxpercent : ownTax, }; }); } diff --git a/src/features/store-user/pages/StoreProductsPage.tsx b/src/features/store-user/pages/StoreProductsPage.tsx index eddfa7e..d7f5751 100644 --- a/src/features/store-user/pages/StoreProductsPage.tsx +++ b/src/features/store-user/pages/StoreProductsPage.tsx @@ -528,6 +528,12 @@ function StockPanel({ rows, isLoading }: { rows: StockStatementRow[]; isLoading: {count(rows.length)} line{rows.length === 1 ? '' : 's'} + {/* See the same note on the admin ledger: "Opening" is the balance + carried in from before today, not the stock first loaded. */} + + Today’s ledger. Opening is what was on the shelf before today — stock + received today counts under In and becomes tomorrow’s opening. +