A product schema with a single `price` field. Listing page renders it as tax-inclusive; checkout treats it as tax-exclusive. Totals differ by the tax amount. Mesrai flagged the missing convention.
The vulnerable diff
// models/product.ts
interface Product {
id: string;
name: string;
price: number; // BUG: tax inclusive? tax exclusive? Code disagrees.
}
// listing/Card.tsx — treats price as inclusive
return <span>₹{product.price}</span>;
// checkout/total.ts — treats price as exclusive, adds 18% GST
const total = product.price * 1.18;What is wrong
Tax convention is a business / regulatory choice, but the code has to encode it consistently. Storing 'price' without specifying inclusive vs exclusive leaves the interpretation to whichever code path is reading. Over time different paths inevitably disagree. The fix is to pick one convention (typically: store exclusive of all taxes; compute tax + total at display per region), pin it in the field name (`basePricePaise`), and run the convention through types.
The attack
Symptom:
Product page: ₹1180 (treats price as inclusive — what user sees)
Cart line: ₹1180 (same)
Checkout total: ₹1392.40 (treats price as exclusive, adds 18% GST)
User: "why did my cart go up at checkout?"
Support: 'system bug, will be fixed soon'.Sometimes shows up as 'price changes between cart and checkout' bug reports.
Mesrai's review comment
mesraipilot · Bot · reviewed 1 min ago
[mesrai] [code-review] [Logic] [Tax-Convention] [medium]
`price` field has no tax convention. Code paths disagree. Pin to
one explicit convention.
Recommended shape: store exclusive, compute inclusive per region:
interface Product {
id: string;
name: string;
basePricePaise: number; // tax-exclusive, integer paise
}
interface TaxedPrice {
basePricePaise: number;
taxPaise: number;
totalPaise: number;
taxBp: number;
}
function applyTax(p: Product, region: TaxRegion): TaxedPrice {
const taxBp = region.gstBp; // 1800 for IGST 18%
const taxPaise = Math.round((p.basePricePaise * taxBp) / 10_000);
return {
basePricePaise: p.basePricePaise,
taxPaise,
totalPaise: p.basePricePaise + taxPaise,
taxBp,
};
}
Rendering, cart, checkout all consume `applyTax(p, region)` — single
source of truth. Cluster 8 post 71 (integer paise) and post 78
(unit naming) compose here.The fix
// models/product.ts — fixed
export interface Product {
id: string;
name: string;
basePricePaise: number; // exclusive, integer paise (named explicitly)
}
// pricing/apply-tax.ts
export interface TaxedPrice {
basePricePaise: number;
taxPaise: number;
totalPaise: number;
taxBp: number;
}
export function applyTax(p: Product, region: { gstBp: number }): TaxedPrice {
const taxPaise = Math.round((p.basePricePaise * region.gstBp) / 10_000);
return {
basePricePaise: p.basePricePaise,
taxPaise,
totalPaise: p.basePricePaise + taxPaise,
taxBp: region.gstBp,
};
}Field renamed to `basePricePaise` — convention encoded in name. Single `applyTax` function used everywhere — convention enforced at one boundary.
Why human review missed it
Tax-convention bugs are subtle because the field looks correct in isolation. The bug is at the integration between code paths. Mesrai catches `price` / `amount` fields without a convention suffix or type brand and recommends the renaming.
Related rules + further reading
Mesrai rule pack: logic/tax-convention-explicit — flags currency fields without inclusive/exclusive convention.
Indian GST Act: invoice fields must specify taxable value, tax, total.
Common in cross-border e-commerce.
Takeaway
Pick one tax convention. Encode in field name. Single derive function. Mesrai catches every ambiguous price field.