Shopify Functions bundle discount allocation: keep component prices exact at checkout
How Shopify Functions allocate bundle discounts across component lines — free-item vs spread modes, product vs order application, rounding, and why checkout-native math beats cart scripts.
Jeshua Leger
Founder, Leger Studio ·
Bundle offers fail in quiet ways. The storefront shows $79 for a kit. Checkout splits that kit into three lines and the discounts do not add up. Accounting sees weird unit costs. Finance asks why the “free” gift still carries $4.17 of discount. Ops reprints packing slips because line prices look wrong to the warehouse. None of that is a merchandising problem — it is a discount allocation problem.
Shopify Functions moved bundle pricing out of fragile cart scripts and into checkout. That is the right place for money math. But Functions alone do not decide *how* savings land on each component. You still need a clear allocation model, integer-cent rounding, and an application mode that matches how your team reads orders. This guide walks through those choices with merchant-facing examples so you can pick a policy before you scale packs across the catalog.
What “discount allocation” means for a Shopify bundle
A bundle has two prices that matter commercially:
- The customer-facing pack price (or the percent / amount off you advertised).
- The component line prices that appear on the order after the pack expands at checkout.
Allocation is the map from the first number to the second. If a kit’s natural sum is $100 and you sell it for $80, you have $20 of savings to place somewhere. Put it all on the gift, spread it by price share, or apply it as an order-level discount while lines stay at natural prices. Each choice changes what customers see, what finance exports, and what a 3PL interprets as “paid” versus “promo.”
Shopify Functions are the enforcement layer: they apply the discount automatically when the bundle is in cart, without a code and without a theme script guessing at totals. Allocation is the policy Functions should implement consistently every time.
Why cart scripts and theme math still get this wrong
Storefront scripts can redraw a cart UI to *look* like $79. They are weak at guaranteeing that same math survives checkout, draft orders, Shop Pay, and later edits. Race conditions, theme updates, and “works on my laptop” JavaScript are why scripted bundles still surprise merchants under load. We covered the storefront side in Why storefront scripts are the wrong way to sell Shopify bundles; the allocation lens is the same story told from the order document outward.
- Scripts often discount the wrong line when quantities change mid-session.
- Floating-point percent math creates off-by-one-cent totals that fail reconciliations.
- Gift lines sometimes stay at full price until a second discount code is stacked — confusing customers and support.
- Fulfillment still needs real SKUs; a single discounted “Kit” line hides what the warehouse picks.
Two allocation modes that cover most offers
Free item mode (BOGO / GWP)
Mark one or more components as free. The discount is absorbed by those lines first so paid items keep their natural prices when the gift value covers the savings. Customers see a $0.00 gift on the order — a clear merchandising signal that matches “buy the kit, get the stand free.”
Worked example: Hoodie $60 + Stand $20 = $80 natural. Bundle sells for $60 (stand free). Free-item allocation puts the full $20 savings on the stand. Order lines: Hoodie $60, Stand $0. Finance sees which SKU carried the promo. Fulfillment still picks both SKUs.
Edge case: if configured savings exceed the free items’ combined value, zero the free lines and spread the remainder across paid items — and surface a warning in the editor so merchandising can fix the offer before publish. That prevents silent underpricing of heroes when someone marks a $5 sample as free on a $40-off pack.
Spread mode (kits and percent-off sets)
Distribute the total bundle discount across all components in proportion to each item’s share of the combined price. A $20 discount on a kit where one SKU is 60% of value puts about $12 on that SKU and $8 across the rest (before cent rounding). Every line reflects a fair share of savings — useful when you sell a fixed-price pack and do not want a single “victim” line to look heavily discounted.
Worked example: Serum $50 + Cream $30 + Mini $20 = $100. Fixed pack price $80 → $20 savings. Spread allocation targets ≈ $10 / $6 / $4. After integer-cent rounding with a largest-remainder pass, the three line discounts still sum to exactly $20.
Spread mode fits percentage-off sets and “complete the routine” kits where every component is a paid hero. It is usually the wrong default for classic GWP, where you *want* the gift to read as free.
Application mode: product lines vs order discount
Allocation answers *which components* absorb savings. Application answers *where Shopify shows the discount* at checkout:
- Product-level application — each component line is priced at its allocated unit price. Shoppers see discounted lines directly. This is the right default for most DTC brands.
- Order-level application — component lines stay at natural prices; the bundle savings appear as an order discount. Useful when accounting or a 3PL prefers full list prices on every SKU line.
These axes are independent. You can run free-item allocation with product application (gift shows $0) or spread allocation with order application (lines at list, savings rolled up). Document the combination you standardize on so support and finance are not reverse-engineering screenshots.
Integer cents and largest-remainder rounding
Percent splits in floating point are how you get $19.999999 of intended savings and a one-cent gap on the order. Serious allocation runs in integer cents: convert dollars to cents once, allocate whole cents, then reconcile the remainder so the sum of line discounts equals the configured savings exactly.
- Compute each component’s ideal share of the discount in cents (possibly fractional).
- Floor each share to a whole cent.
- Distribute leftover cents with a largest-remainder (or similar) pass so totals match.
- Reject or warn on configs where savings exceed what free items can absorb, instead of inventing negative prices.
That discipline matters more as carts stack quantity, mix currencies’ minor units, or combine multiple bundles. If you are evaluating apps, ask how they round — “looks right in the cart drawer” is not the same as “sums cleanly on the paid order.”
How Shopify Functions change the operating model
With Functions, the discount is a checkout capability, not a theme dependency:
- No discount code for customers to forget, mistype, or share outside the intended pack.
- No storefront script tax on PDP performance for pricing logic.
- Orders expand into real component SKUs for packing and returns — paired with honest kit inventory, as in Shopify kit inventory sync.
- Pricing policy lives in the app configuration you can preview before publish, not in a snippet only one developer understands.
Functions do not remove the need for inventory truth. Pack availability should still derive from components so you never advertise a kit you cannot assemble. Allocation and inventory are sibling systems: one keeps money honest, the other keeps stock honest.
Choosing a default policy for your catalog
Pick defaults once, then override per offer when merchandising needs it:
- Gift with purchase / BOGO → free item mode + product application.
- Fixed-price kit or “complete set” → fixed pack price + spread mode + product application.
- Percentage-off multipack → percent pricing + spread mode.
- Clean list-price lines for accounting/3PL → any allocation + order application.
- Hero must keep a stable unit price on the order → free-item mode with the gift absorbing savings, or order application if every line must stay at list.
A checkout QA checklist for allocation
- Configure the offer and note expected line discounts in cents before you place the order.
- Add the bundle alone; confirm component SKUs, quantities, and that discounts sum to the advertised savings.
- Change quantity (2× pack) and confirm allocation scales without orphan cents.
- Add a standalone component of the same SKU and confirm the pack discount does not leak onto the solo line.
- If using free-item mode, verify the gift line reads $0 (or the intended remainder) and paid heroes stay intact.
- If using order application, verify lines at natural prices and a single order discount equal to pack savings.
- Refund or partial return one component and confirm your support playbook matches how Shopify restocks and reallocates.
- Export the order to whatever finance uses and confirm unit economics match the policy you documented.
Bake this checklist into Create your first bundle onboarding for anyone who publishes packs — not only the person who installed the app.
Reporting: what to watch after you switch
- Bundle AOV and units per order versus the two weeks before the offer.
- Discount given vs. incremental margin — allocation does not invent margin; it only places it.
- Support tickets about “price looked different at checkout.”
- 3PL or warehouse exceptions tied to unexpected line prices.
- Inventory exceptions on shared components (allocation will not fix a fake kit SKU).
If AOV rises but margin collapses, revisit which components absorb free-item discounts — a “free” hero is a different commercial decision than a free sample. For broader AOV tactics beyond allocation, see How product bundles increase average order value and bundle pricing psychology.
Where Better Bundles fits
Better Bundles is live on the Shopify App Store for native-style packs: real products on the storefront, Shopify Functions at checkout, free-item and spread allocation, product- or order-level application, and automatic inventory sync from component stock. Every plan includes a 15-day free trial billed through Shopify.
Install from apps.shopify.com/better-bundles, read Discount allocation & application modes, then ship one constrained offer with the QA checklist above. When multi-location imbalance — not pricing — is the blocker, keep FlowOps on your radar (in development) and use native transfers until launch.