Store Builder

Rule-based shipping rates

Rate tables built from rules, the priority order that decides which rule wins, Test fee to see what a shopper will pay before you save, and the explanation on every order.

Manage → Settings → Delivery (vi: Quản lý → Cài đặt → Vận chuyển). The screen introduces itself:

"The delivery fee shoppers see at checkout, calculated by rules you set."

This page is about the new, rule-based screen. If your Delivery screen is just a list of options with a fee and a free when the order is over box, the shop is still on the old screen — see Delivery and the last section of this page.

The thing to understand first: when several rules match an order, the order of the rows in the table decides nothing. The winning rule is picked by the priority order of the condition groups — one setting for the whole shop. The table simply lists its rows in that winning order, which is why it has no drag handles:

"Rows are listed in the order they win when several rules match. No dragging needed."

Methods, rules and the default fee

A delivery method is what the shopper picks at checkout ("Standard", "Express"…). The order of the method cards on the screen is the order shoppers see them in.

Open a method to see its rate table: each row is a rule — a few conditions and an outcome. An order matches a rule when it matches every condition on that row; an empty cell (shown as "—", or "Any", "Anywhere") matches every order.

When no rule matches, the method's Default fee applies — "Used when no rule matches." A method with no rules at all is a flat rate.

The six condition groups

The columns of a rate table are six ready-made groups; you never type a condition yourself:

Group What a rule sets
Payment COD · Prepaid · Any
Area Inside or Outside one or more zones, or Anywhere
Order amount A goods-subtotal range: From (≥) … To (<)
Weight A gram range
Item count A quantity range
Category Only · Contains · Excludes a list of categories

A range includes its lower bound and excludes its upper one: "from 200,000đ to 500,000đ" matches a 200,000đ order and not a 500,000đ one. Two rows that meet at the same boundary can therefore never both match one order.

The order amount is the goods subtotal before any discount code — as on the old screen, and for the same reason: discounts come off after shipping, so entering a code can never take away free shipping the shopper has already been shown.

A new site starts with only Payment and Area switched on; a group that is off has no column in the rate table. Turn more on in the Priority order card.

Priority order: which rule wins

"When several rules match, groups are compared from the top down. Applies to every method."

The matching rules are compared on the top group; if that group tells them apart it stops there, and on a tie it moves to the next one. The last group is always Fee ("Always last", "breaks ties left by the groups above"), and if even the fee is equal, the rule created first wins. So there is always exactly one answer, and the same order always gets the same one.

You do not have to read the configuration yourself: under the list of groups the screen writes it out as a sentence. With all six groups on in the default order it reads:

"Reads as: “COD” or “Prepaid” before “Any” → ward before province, province before anywhere → larger orders first → heavier parcels first → more items first → most matching categories first → lower fee first."

The sentence only mentions groups that are on, so on a new site it is much shorter. Reorder the groups (drag, or focus a handle and use ↑ ↓) or change a mode and the sentence follows — re-reading it is the quickest way to know what you just changed.

A specific value always beats an empty cell, in every mode. "COD" beats "Any", a zone beats "Anywhere", a range beats "no limit".

Modes within a group

Each group has a mode selector:

  • Payment — Specific first or Custom.
  • Area — Most detailed: ward before province, province before "outside", "outside" before Anywhere. It looks at the part of the zone that matched this address, so a zone that mixes provinces and wards can be "detailed" for one address and not for another.
  • Order amount, Weight, Item count — High → low picks the highest threshold the order has passed; Low → high picks the narrowest band the order is still under.
  • Category — Most matches: "Only" before "Contains", "Contains" before "Excludes", then the rule matching more of the cart's categories.
  • Fee — Low → high (in the shopper's favour) or High → low.

Custom

Custom opens a list you order yourself — and it orders values, not rows: the zones, the categories, COD/Prepaid, or the amount, weight and quantity ranges already present in your rate tables. A group no rule uses yet has an empty list, because there is nothing to order.

"Empty cells (“Any”, “Anywhere”, no limit) always rank last."

Because it orders values rather than rows, a Custom group never "swallows" the groups below it: two rules with the same value in that group are still told apart by the next group.

A group in use cannot be switched off

A group that any rule sets has its switch locked — "Used by 3 rules". The reason: switching a group off does not remove the condition from those rules, it makes them broader. Turn Payment off and a "COD → 20,000đ" rule starts taking prepaid orders too; turn Area off and "Outside Hà Nội → Not supported" blocks the whole country. Edit those rules first.

One method with its own order

The priority order is shared. If one method really needs something different — "Standard" priced by weight, "Express" by area — open it and use Advanced → Use its own order: "Only this method uses a priority order different from the shop-wide one." Most shops never need it.

Building a rate table

Click a method to open it. Three tabs: Basics (name, description, carrier), Rate table, and Surcharges & free shipping.

Areas and zones

A rule's Area cell picks saved zones, with Inside or Outside. Zones are made in the Delivery zones card — a name for a group of provinces, or of individual wards — and can be reused in every rate table. There is a ready-made Inner city … (from the pickup address) choice that selects the wards around your shop's pickup address. If a rule needs a province or a few wards for itself alone, pick them straight in the cell; you do not have to make a zone first.

Two things about missing addresses, both deliberate:

  • An order with no address (a form that never asks for one) only matches "Anywhere" rules. Neither "Inside" nor "Outside" matches — so an "Outside the city → Not supported" rule does not block every form that has no address field.
  • An order with a province but no ward does not match a ward-level zone, either "Inside" or "Outside": without the ward there is no telling which side of the line it is on.

The second one matters because checkout does not ask the shopper for a ward yet. The zone dialog says so: "Checkout does not ask for a ward yet, so coverage will read “Partial” for this zone." If you use ward-level zones, make sure another rule (province-level, or the default fee) handles orders that arrive without a ward.

Deleting a zone that is in use asks first: "Deleting “{name}” removes it from N rules. A rule left with no zone is deleted."

Payment: a form that never asks counts as COD

The Payment group knows two classes: COD and Prepaid. Bank transfer, card and the payment gateways (SePay, VNPay, MoMo…) are prepaid. Everything else is COD — including a form that has no payment question, and including free-text labels from an old payment select field.

That is the safe side on purpose: classing a COD order as prepaid would give it "prepaid only" free shipping and tell the carrier to collect nothing, while the opposite mistake costs at most a fee.

Category

Only matches when every product in the cart is in the chosen categories; Contains when at least one is; Excludes when none is.

A rule's outcome

Outcome Meaning
Fixed fee One amount
By weight / By quantity A first-step fee plus a fee per further step; a part-step rounds up to a whole step
Percent of order value (%) A percentage of the goods subtotal, with a minimum and maximum fee
Free 0đ
Carrier rate The carrier's live price — only when the method picks a carrier on the Basics tab
Not supported "Orders matching this rule cannot be delivered; the method is hidden from the shopper."
Contact us "Shows “Contact us” instead of a fee; you quote the price later." — COD orders only

Not supported hides the method, not the shop. Only when every method is hidden for that address does the shopper see "We do not deliver to this area" and cannot order.

Contact us applies to COD only. On a prepaid order a Contact us rule is treated as Not supported — otherwise a shopper could pay by QR and then be phoned for more shipping money.

Each row also has an optional ETA cell ("2–3 days").

Carrier rate takes the price live from the carrier; when the carrier does not answer, the Fallback fee per zone on the Basics tab applies ("Charged when the carrier does not answer. Leave empty to use the default fee."). Carriers are connected in the Shipping app, through Manage connections on the Carriers card.

A fallback box left empty, or set to 0, means "use the default fee" — not free. To ship a zone free when the carrier stays silent, tick Free (0) next to the box; the box then reads "Free (0)" instead of "Default fee". The two are kept apart so that a box cleared by accident can never turn into free shipping.

One caveat when ranking by Fee: the carrier is not asked while the winner is being picked, so a "Carrier rate" row is compared at the method's default fee. Test fee shows you the real figure.

Free shipping over an amount

There is no separate "free when the order is over" box: conditional free shipping is a rule whose outcome is Free. The Free when order ≥ X button adds that rule for you; you only type the amount. Being an ordinary rule, it follows the priority order like every other — see the example below, where that is exactly what surprises people.

The Surcharges & free shipping tab holds only that shortcut for now: "COD, category and form-option surcharges are not available yet."

Two flags in the table itself

  • "covered by #2" — every order that matches this row also matches row 2, which ranks above it, so this row can never win. Only a row covered entirely by one other row is caught; a row covered by several together only shows up in Test fee.
  • "Duplicates #3" — two identical rules. "Delete one to save." The system never quietly picks one of the two.

Starter templates

A shop with no methods yet sees five templates: Flat rate, Inner city / outer, By region (North, Central, South), By weight, and GHN rate + free shipping. Use this template creates a filled-in method on the spot — with the zones it needs — and you adjust the numbers to your shop. Or Create an empty method.

GHN rate + free shipping also switches on the Order amount group, because its free rule is conditioned on the order amount.

Test fee

The Test fee button inside a method opens the tester: "Same calculation as checkout". Enter a province, ward, goods subtotal, weight, quantity, categories and payment, and it asks the server — the very function checkout calls, on the version you are editing and have not saved. It answers with the fee, the winning rule, and the group on which the runner-up lost; the winning row in the table wears a Wins with test order badge.

Example: one order, two ways to pay

Default fee 30,000đ, order Payment → Area → Order amount, and this rate table (in the order the screen lists it):

# Payment Area Order amount Outcome
1 COD Anywhere — 20,000đ
2 Any Inside "Hoàn Kiếm" — 15,000đ
3 Any Anywhere ≥ 500,000đ Free
4 Any Anywhere < 200,000đ 35,000đ

Test a 600,000đ order to Phường Hoàn Kiếm, Hà Nội:

  • COD → 20,000đ. Rows 1, 2 and 3 all match. The first group is Payment: "COD" is more specific than "Any", so row 1 wins straight away — "Beats rule #2 on the Payment group".
  • Prepaid → 15,000đ. Row 1 no longer matches. Rows 2 and 3 tie on Payment (both "Any"), so Area decides: a zone is more specific than "Anywhere" — row 2 wins, "Beats rule #3 on the Area group".

A 600,000đ order has passed the 500,000đ free-shipping threshold and still does not ship free, because Order amount ranks below Payment and Area. If what you mean is "orders from 500,000đ always ship free", drag Order amount to the top — the "Reads as" sentence will start with "larger orders first" — and test again.

Checks before saving

"Re-runs after every change. Never calls a carrier."

This card runs the configuration you are editing against sample orders for every province, both COD and prepaid, and reports:

  • Provinces that cannot be delivered to — "5 provinces cannot be delivered to", with an Add a rule for these provinces button. It is a warning, not a block: some shops only deliver locally.
  • Each province as Full, COD only, Prepaid only or Partial. "Partial" is usually a ward-level zone: "12 wards priced; orders without a ward will not match a ward-level zone".
  • Rules that can never win, duplicate rules, rules that use a deleted category.

Free shipping by accident

When a method has a 0đ default fee ("an order that matches no rule ships free") or a Free rule with no conditions at all, the card shows a red Shipping may be free by accident warning, and Save stays locked until you tick I understand and want to save anyway. Free shipping on every order is a valid setup — but it has to be something you said, not something that happened because a box was left empty.

You are only asked about what this save adds. A free case that was already there at the last save is an answer you have already given, so reloading the page or editing something else does not make you tick again; the warning comes back only when you create a new one — on another method, or on the same method for a different reason. Save then reads "Tick the free-shipping warning below to save." (inside a method: "…above to save."). The server applies exactly the same rule, so saving from somewhere else does not get round it.

Change summary

Press Save changes, and if the change moves the fee of a sample order, the screen shows Changes that will apply — "For sample orders, the delivery fee changes like this:" — one "method: old fee → new fee" line each (or Hidden), before Confirm and save. Dragging a priority group gets read back to you as money before it takes effect.

Configuration history

The History button at the top of the screen:

"The latest 50 versions are kept. Restoring creates a new version and never erases history."

Restoring replaces the whole shipping configuration — methods, rules, zones, priority order — with the chosen version, and the current one stays in the history, so a mistaken restore can itself be undone. A version that uses a deleted category or a carrier that is not connected cannot be restored: the whole restore is refused, the dialog says what is missing, and nothing changes — never a half-restore.

Under each version's date is one line saying what that version changed compared with the one before it, for example:

"Added Express · Renamed Fast delivery to Standard delivery · 2 rules edited · Priority changed"

The line is written once, at save time, and kept as it is — it records what happened then. A restore gets its own line too: what it changed compared with the configuration in use when you pressed it. A save that changed nothing reads "No changes". Two kinds of version have no line at all: versions saved before this feature existed (nothing guesses them after the fact), and the Converted to rules version — it only snapshots the old setup and changes no one's fee.

A hidden zone — provinces or wards picked directly inside one rule — counts towards that rule, not as a "zone".

Two people editing

The configuration carries a version number. If someone saves while you are editing, your save is refused rather than written over theirs:

"Someone else changed this configuration — Someone saved a newer version while you were editing. Your changes are kept and nothing was overwritten."

Press Reload: the newer version is loaded and your changes are applied on top of it — whatever the other person changed that you did not touch is kept. If you both changed the same thing differently, the screen names it — "You and someone else both changed: the group order. Saving writes your version." — so you decide before you press Save.

An open method is saved as a whole. So if the other person also just changed the very method you have open, the method sheet says so at the top, naming the parts they changed:

"Someone else also just changed this method: Rate table, Default fee. Saving overwrites their version."

Save stays available — the decision is yours. If the method was deleted, the notice reads "Someone else just deleted this method. Saving will not write your changes."

At checkout: when the shopper has to pick a delivery method

A cart of tickets only needs no delivery. An event ticket is admission, not a parcel: an order whose every line is a ticket ships at 0đ with no method, and the delivery chooser on the form is hidden. With nothing to choose there is nothing to block on — the send button is not locked.

A cart with both tickets and goods is priced as an order of the goods alone: the order amount, weight, quantity and categories the rules see all leave the ticket lines out. A 2,000,000đ ticket therefore never lifts an order over a free-shipping threshold. The cash-on-delivery amount is still the whole order, because that is what the courier actually collects.

The delivery chooser is required whenever delivery is needed. If the field is marked required on the form and the shopper leaves it empty, the order is accepted only when nothing needs delivering — a tickets-only cart, or a shop with no method switched on (the order then ships at 0đ). In every other case the order comes back with the same "required" error as any other required field, listed together with whatever else is missing, and nothing has been stored yet. When the system cannot tell — say the configuration could not be read — it assumes delivery is needed: asking the shopper for one more field is better than letting an order through with no delivery fee.

On the order: "How the shipping fee was calculated"

The order detail page has a How the shipping fee was calculated block: the method, the winning rule, the group that set it apart from the next rule, the matched zone, the ETA and the fee. An order on a Carrier rate where the carrier did not answer adds "The carrier did not respond, so the fallback fee was used".

The block is a record from when the order was placed, not a recalculation:

"Recorded when the order was placed on {date}. The current configuration may differ."

Editing a rule today does not make yesterday's order explain itself wrongly. Orders placed before these explanations existed show no block, or just "Calculated by the legacy shipping configuration".

Shops still on the old screen

A shop that set up delivery before rule-based rates keeps the old screen until support switches it. There is no button to switch yourself, and that is deliberate: before the switch, the old setup is rebuilt as rules and checked so that every order comes out at the same fee as before — changing the pricing engine must not change what any shopper pays. New shops get the rule-based screen from the start.

After the switch, the new screen opens with your old methods already written out as rules. The first save adds a Converted to rules version to the history: a snapshot of the old setup, from before it was turned into rules.

Updated 07/10/2026