Skip to content

feat(discounts): add Shopify-style discounts and a server-side cart - #254

Draft
NagariaHussain wants to merge 11 commits into
developfrom
feat/server-cart-discounts
Draft

NagariaHussain wants to merge 11 commits into
developfrom
feat/server-cart-discounts

Conversation

@NagariaHussain

@NagariaHussain NagariaHussain commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

Closes #223
Closes #246
Closes #247
Closes #249

Summary

Merchants can now create Shopify-style discounts in the dashboard: codes or automatic discounts that stack per line and per order on top of ERPNext pricing. The cart also moves to the server, so code and plugins can build a cart and give its lines attributes.

Why

ERPNext allows one coupon per document, and its Pricing Rule matcher fails when coupon rules overlap. A coupon rule can also cancel an automatic rule on the same item. Commera Discount records are outside that matcher, and their result goes into ERPNext's standard discount fields. The Sales Order, Sales Invoice and GL flow does not change.

What changed

  • New doctypes: Commera Discount (rules), Commera Discount Code (typed or generated single-use codes), Commera Discount Use (one row per discount per order), and Commera Cart (the shopper's server cart with line attributes).
  • Discount types: amount off products, amount off order, buy X get Y, and free shipping.
  • Rules that Shopify does not have natively: a percentage cap ("10% off, up to ₹200"), prepaid-only or cash-on-delivery-only, skipping items already on sale, spend tiers, and a budget.
  • Also: first order only, customer groups, minimum purchase, total and once-per-customer limits, dates, and Shopify-style combinations, with up to 5 codes per cart.
  • Quotation, Sales Order and Sales Invoice run a discount step on every save. It removes its own amounts before ERPNext prices the document and adds them back after. This keeps the discounts when an automatic Pricing Rule matches the line.
  • Shipping discounts change the delivery options after the plugin hooks. The cash on delivery summary drops prepaid-only discounts.
  • A placed order logs its discount uses when the Sales Order is created, because cash on delivery orders stay draft. Cancelling or deleting the order removes them.
  • The storefront cart loads the server cart on each page load and syncs changes back. A version check stops an old browser tab from overwriting server changes.
  • New Discounts screens in the dashboard, under commera.api.admin.discounts.
  • New SDK module commera.sdk.discounts, and new cart functions in commera.sdk.cart. Nothing was removed, so API_VERSION stays.
  • New custom fields on Quotation, Sales Order and Sales Invoice and their items. Run bench migrate.
  • Plugins: a new cart_discounts hook lets an app take an amount off a cart line or the order. It competes with the merchant's product discounts for a line and adds to order discounts, and orders keep what the cart got.
  • New cart_created and discount_used events. Order event snapshots now include discounts.
  • The SDK cart and order now return discounts and attributes for the order and for each line. The new cart.set_cart_attributes sets cart-level attributes. cart.set_cart_fields now also works before checkout, for guests too.
  • Plugins: a new validate_discount_code hook lets an app refuse a code with a message. This works for discount codes and the store coupon. At checkout a refused code is removed, and the shopper sees the message and the new total. Before this change, the whole cart was refused.
  • Cart links: ?mode=add adds the link's lines to the current cart instead of replacing it. ?return_to=cart, or a path on this site, sets the page that opens next. A return_to that would leave the site is ignored.
  • The checkout coupon box still uses one ERPNext coupon. A box for several codes will come in a later PR.

Details, diagrams and the SDK examples are in docs/implementation/discounts.md.

Screenshots

Discounts list

Order code with a cap, its results and a test cart

Conditions tab with collapsed sections

Collection discount with unique codes and a budget

Automatic spend tiers, and buy X get Y

Phone and dark theme

🤖 Generated with Claude Code

The cart lived only in the browser and the Quotation was rebuilt from it, so
a cart could not be built on the server and lines could not carry discounts
or attributes. ERPNext also allows one coupon per document, and its rule
matcher fails when coupon rules overlap.

Commera Cart now holds the shopper's lines, line attributes and codes. The
browser cart loads it on page load and syncs changes back, guarded by a
version. Commera Discount codes stack like Shopify's product and order
classes and are applied on top of ERPNext pricing on every save of a
Quotation, Sales Order and Sales Invoice, into the standard discount fields.
… rules

One code per discount could not give automatic discounts or unique codes,
and the rules covered only a percentage or amount on items or the order.

A Commera Discount now holds the rules and Commera Discount Code holds the
strings shoppers type, including generated single-use codes. Discounts can
be automatic, buy X get Y or free shipping, with a percentage cap, spend
tiers, skipping items already on sale, a minimum purchase, a payment method,
first-order or customer-group eligibility, total and once-per-customer
limits, and a budget. Each placed order logs a Commera Discount Use, which
drives the counts, the budget and once-per-customer. Shipping discounts
change the delivery options after the plugin hooks, and the cash on
delivery summary drops prepaid-only discounts.
The dashboard needs to list, edit and test discounts. The new endpoints in
commera.api.admin.discounts list discounts by status and code, save one with
its typed code, generate and download unique codes, refuse to delete a
discount orders used, price a test cart with an unsaved draft, and fill the
editor's pickers. The cart and the test cart share one preview Quotation.
Merchants could only manage discounts in Desk. Discounts in the sidebar now
lists them by status with uses and discount given. The editor opens on what
every discount needs, with conditions, limits and reporting in their own
tabs and optional sections collapsed to a one-line summary. Its side panel
summarises the rules in words, shows orders, discount given, sales and
budget left as number cards, and prices a test cart with the unsaved draft.
The list endpoint now returns the fields its summary reads.
Team code and plugins had no supported way to manage discounts or show a
discount's amount before checkout. commera.sdk.discounts can create, read,
list and update discounts, add or generate codes, preview a cart with codes
and automatic discounts, and read the discounts an order used. Writes need
the caller's own permission on Commera Discount. The new cart and discount
names join the frozen public SDK list; nothing was removed, so API_VERSION
stays.
Explains why discounts are not ERPNext coupons, the discount step on each
save, the cart sync, the dashboard screens, the SDK and the records, with
diagrams and screenshots.
An automatic discount with no qualifying item in the cart was still chosen,
and then blocked an order code it does not combine with. A discount now
takes part only when it would take something off the cart, and a shopper's
code that gives nothing says that nothing in the cart qualifies.
Plugins could build carts and discounts through the SDK but could not give
discounts of their own, read the discounts on a cart or order, or tag a
guest's cart before checkout.

The cart_discounts hook lets an app take an amount off a line or the order;
it competes with the merchant's product discounts per line and adds to
order discounts, and orders keep what the cart got. cart_created and
discount_used join commera_events, and order snapshots carry discounts.
The SDK Cart, PricedCart and Order now return discounts and attributes per
order and line. set_cart_attributes sets cart-level attributes, and
set_cart_fields now works before checkout by waiting on the server cart.
A cart link always replaced the cart and always went to checkout, so it
could not add a product to the current cart or land on the cart or a
product page. `?mode=add` now keeps the cart and adds the link's lines, and
`?return_to=cart` or a path on this site picks the page after; anything
else keeps the default. A return_to that would leave the site is ignored.
An app that limits a code could only refuse the whole cart at checkout and
ask the shopper to remove the code. The validate_discount_code hook takes
the cart and a code and returns a refusal, for discount codes and the
store coupon alike. A refused code cannot be added, is skipped while the
cart changes, and is removed at checkout with the app's message and the
new total. The removal is committed before the message so a retry does
not refuse again.
The summer theme has its own cart link page, and it did not pass is_merge. A link with mode=add replaced the cart there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment