Cost Prices and Margin¶
A cost price is what one unit of a product or variant costs the shop. With cost prices, margin
(revenue less cost) can be measured per product and listing (Product
Engagement) and listings can be ranked for it (Ranking by What
Sells, goal margin). An Enterprise feature (merchandisingRules).
Commercially sensitive¶
Cost prices, and the margin worked out from them, never leave the platform:
| Where | Cost or margin? |
|---|---|
| Admin: Catalogue > Cost prices, margin on Search > Product engagement | Yes, to ADMIN, SUPERADMIN and FINANCE only (BadgerWebAuthTools.canSeeCosts). A merchandiser or content editor sees Product engagement without margin: it is removed from the data, not just hidden |
REST admin API (/v1/admin/catalogue/product-costs, margin on /v1/admin/search/product-engagement) |
Yes, admin roles only |
| Storefront, public REST, product responses | No: a Product has no cost field |
| Search index | No. Only rankMargin, a relative, ordinal score, from which no cost can be worked back |
| Site transfer, product exports | No |
| MCP and other AI tools | No: there is no MCP tool for costs, and siteTraffic productEngagement carries no margin |
| Logs | No: counts and ids only; ProductCost.toString() leaves the cost out |
This holds by construction: costs live only in the productCosts collection
(ProductCost: catalogue, SKU, cost in minor units, who set it and when), read and written only by
ProductCostService. Everything that serialises a product can't carry one because the product
doesn't have one. CostPricesStayInsideTest fails if Product or Variant gains a cost or margin
field or site transfer starts reading costs, and StatisticsToolsTest if the MCP engagement entry
gains one.
Setting cost prices¶
- By SKU: a product's or a variant's. A variant without its own cost uses its product's
(
ProductCostService.unitCost). - In the site's currency, per unit; 0 to £100,000. A blank (or null) cost clears it. A SKU that isn't in the catalogue is reported, not saved.
- Admin: Catalogue > Cost prices (
/admin/catalogue/costs, see the Catalogue guide): find products, type costs against each product and variant, and save; or import a CSV ofsku,costlines (cost in major units,12.50; a header line and quoted cells are fine; at most 50,000 lines, 5 MB). - REST:
GET /v1/admin/catalogue/product-costs?skus=A,B(at most 500; answers with the costs of those that have one and how many SKUs have a cost),PUT /v1/admin/catalogue/product-costswith{"costs": [{"sku": "A", "cost": 4550}, {"sku": "B", "cost": null}]}(minor units; at most 50,000; answers with counts, unknown SKUs and refused entries, never costs). - Feeds call
ProductCostService.setCoststhe same way.
Each save records who made it (updatedBy: a user id, or api:<siteId>).
Margin¶
When an order is placed (ProductEngagementService.recordOrderPlaced), each line's cost is looked
up (the variant's, else the product's), and each product's daily engagement counts:
marginRevenue: the revenue of its lines whose cost was known;margin: that revenue less those lines' cost (quantity × unit cost), which can be negative.
So margin uses the cost when the order was placed: changing a cost later doesn't rewrite past
margin. The margin rate is margin / marginRevenue, and reports apply it to all the revenue
(estimated margin), showing what share of revenue had a known cost. Orders placed before cost prices
were set have none.
For ranking, a product's margin rate is smoothed towards the site's with three average orders'
worth of revenue, so a product without a cost price counts at the site's margin rate, and a site with
no cost prices at all ranks for margin exactly as for revenue. A product sold at a loss scores 0
for margin.
Tests¶
ProductCostServiceImplTest: known and unknown SKUs, variants, clearing, limits, CSV parsing (header, quotes, currency symbols, blanks, bad numbers, too many decimals), coverage.ProductEngagementServiceImplTest: margin from the variant's cost else the product's, uncosted lines, unreadable costs.LearnedRankScorerTest: margin rewards a richer margin, an uncosted product at the site rate, a loss scoring 0.CostPricesStayInsideTest,StatisticsToolsTest: the guards above.CostPricesAdminTest,ProductEngagementRenderTest(margin shown, and dropped for other roles),ProductCostsAdminControllerTest(REST).