Markdown Rules
Markdown Rules automatically identifies aging seasonal stock (e.g. SS25 stock unsold after 30 days) and calculates a tiered discount for it — you review the exact products and new prices in a Preview before ever touching a live price, then Apply the rule to write those new prices to your catalog in one click.
Overview
A rule is built from up to three discount tiers — Tier 1, Tier 2 and Tier 3 — each pairing a "days old" threshold with a discount percentage (e.g. 30 days → 10% off, 60 days → 20% off, 90 days → 30% off). Only Tier 1 is required; Tiers 2 and 3 are optional for a steeper multi-stage clearance.
A rule can optionally be scoped to a Season (e.g. "SS25") and/or a comma-separated list of Categories (e.g. "Shirts, Trousers, Jackets"); leaving both blank means the rule considers every active product in the catalog.
The "days old" a product is judged against is calculated from that product record's creation date in the system (Product.createdAt) — not a separate "date put on the floor" or "last restocked" date. A product record created long ago will keep aging even if you have since replenished its physical stock.
Every rule carries an Action label — "Suggest only" or "Priority — apply first when reviewing" — but this is a self-documenting label only: nothing in the backend runs on a schedule to apply rules automatically. Both action types require a staff member to open the rule and click Apply; the label just helps a team decide which rules to review first.
Only the single highest threshold a product has crossed applies — tiers do not stack. A product that is 95 days old on a rule with Tier 1 = 30 days/10% and Tier 3 = 90 days/30% gets the 30% discount only, never 10% + 30%.
Before You Start
- Requires the "promotions" plan feature to be enabled — the page is hidden entirely below that.
- At least Tier 1 Days and Tier 1 Percent must be set to save a rule; Tier 2 and Tier 3 are optional.
- Applying a rule permanently overwrites the
pricefield on every matching product — there is no built-in undo, so it is worth running Preview (and noting current prices, or exporting them from Products first) before Apply on a rule you have not run before.
Step-by-Step Guide
1 Create a markdown rule
- Click + New Rule.
- Enter a Rule Name * (e.g. "SS25 End-of-Season Clearance").
- Optionally set Season (e.g. "SS25") and Categories (comma-separated; leave blank to match every category).
- Set Tier 1 (First threshold) — Days old and Discount % — and optionally Tier 2 and Tier 3 for steeper multi-stage discounts as stock ages further.
- Choose an Action label: "Suggest only" or "Priority — apply first when reviewing" (see the note below — both require you to manually click Apply).
- Add any Notes and click Create Rule.
2 Preview a rule before touching any prices
- Click Preview on any rule card.
- A panel opens below the rule list showing "Preview — N products affected" and a table of every matching product: Product, SKU, Days Old, Current Price, New Price and the Discount % badge.
- Preview makes no changes whatsoever — it is a read-only simulation you can open and close (✕) as many times as you like without side effects.
3 Apply a rule (permanent price change)
- Click Apply on the rule card (or from an open Preview).
- Confirm the prompt "Apply this rule? Product prices will be updated permanently." — this is your only safety check.
- On success, a toast reports "Applied to N products" and the rule's "Last applied" date updates; every matching product's price is now overwritten with the discounted price shown in Preview.
4 Manage existing rules
- Click Disable/Enable on a rule to toggle it without deleting it — disabled rules are shown dimmed in the list, but they still work if you press Preview or Apply on them (disabling does not currently block those actions, it is purely a visual/organisational flag).
- Click Edit to change any field on an existing rule.
- Click Delete and confirm to permanently remove a rule.
Every Field & Button, Explained
| Field / Button | What it does |
|---|---|
+ New Rule | Opens a blank markdown rule form. |
Rule Name * | Required label for the rule, e.g. "SS25 End-of-Season Clearance". |
Season | Optional — restricts the rule to products whose Season field matches exactly; leave blank to ignore season. |
Categories | Optional comma-separated list (e.g. "Shirts, Trousers"); stored as a JSON array; leave blank to match every category. |
Action | "Suggest only" or "Priority — apply first when reviewing" — a label only; neither runs automatically, both require clicking Apply manually. |
Tier 1/2/3 — Days old | The minimum age (in days since the product record was created) a product must reach for that tier's discount to apply. Tier 1 is required; Tier 2/3 are optional. |
Tier 1/2/3 — Discount % | The percentage price cut applied once a product reaches that tier's day threshold. Only the single highest threshold crossed applies — tiers never stack. |
Notes | Free-text notes on the rule. |
Preview | Read-only simulation — lists every currently-matching product with its current price, calculated new price, discount % and days old, with zero effect on live data. |
Apply | Permanently writes the discounted price to every matching product's price field and stamps the rule's "Last applied" date; requires a confirmation click. |
Disable / Enable | Toggles the rule's isActive flag; dims the card in the list, but does not currently block Preview or Apply from still being run on a disabled rule. |
Last applied | Shows the date Apply was last successfully run on this rule, or "Never". |
Edit / Delete | Edit reopens the form pre-filled; Delete permanently removes the rule after a confirmation prompt. |
Tips & Best Practices
- Always click Preview before Apply on a rule you have not run before — it is the only way to see exactly which products and prices will change ahead of time.
- Use Categories to keep a rule tightly scoped (e.g. only "Winter Jackets") rather than leaving it blank for your whole catalog, especially the first few times you use a new rule.
- Because "days old" is based on when the product record was created — not when it was restocked — rules work best on products you create fresh each season rather than long-lived SKUs you keep restocking under the same product record.
- Set up a steep Tier 3 (e.g. 90 days / 40%) for genuinely dead stock rather than relying only on a gentle Tier 1 — the goal is to actually clear it, not just nudge the price.
Troubleshooting & FAQ
I set the Action to "Priority — apply first when reviewing" but prices never changed on their own.
Preview shows 0 products affected.
I applied a rule but the "applied" count was lower than the "affected" count in Preview.
Can I undo an Apply?
A product got a smaller discount than I expected given how old it is.
🧑💻 Developer Notes
Source component(s):
frontend-app/pos-app-garment/src/pages/garment/MarkdownRulesPage.jsx
Backend endpoints used:
GET /garment/markdown-rulesPOST /garment/markdown-rulesPUT /garment/markdown-rules/:idDELETE /garment/markdown-rules/:idGET /garment/markdown-rules/:id/previewPOST /garment/markdown-rules/:id/apply
Related tables (db-core repositories):
GarmentMarkdownRuleProduct
Redux slices:
None — local component state only (useState/useCallback)
Feature flag key: promotions
Backed by packages/pos-core/src/garment.js (previewMarkdownRule / applyMarkdownRule, ~line 375+). previewMarkdownRule computes daysOld from Product.createdAt and picks the applicable tier by iterating tiers highest-threshold-first, stopping at the first one daysOld satisfies (no stacking). applyMarkdownRule re-runs previewMarkdownRule internally then writes new prices in a simple loop with no transaction wrapper, skipping any item whose newPrice is null. previewMarkdownRule's product query now also filters by tenantId and by the rule's own storeId (matching the pattern already used by getStyleContribution/getBestsellerBySize) — a prior version queried every active product with no tenant/store scoping at all, which could preview/apply pricing changes against another tenant's products in a shared database; this has been fixed. The "Priority — apply first when reviewing" action value is stored and displayed but never read by any scheduler/cron in the codebase — by design it is a review-order label only, not automation, and the UI now says so explicitly.