AkkadAkkad

Delivery

Pricing a delivery, then handing it to a courier

Delivery is two problems wearing one name. The first is arithmetic: what should this order cost to send, given what is in the basket and where it is going. The second is logistics: getting that order into a courier's system, in their vocabulary, and finding out later whether it actually arrived. Akkad treats them as two problems, and this page follows them in that order.

A shipping rule is built from the same parts as a pricing rule: a scope of categories, a type that decides what gets measured, and a list of conditions that turn a measurement into money. That symmetry is deliberate. A merchant who has already written a volume discount does not have to learn a second mental model to write a delivery charge.

Every condition shares one shape — a minimum, an optional maximum, a shipping cost, and an optional city. Types that do not need a field simply leave it at its default, which is why a per-city row still carries a minimum of zero and no maximum: an older app build that expects threshold conditions can still parse the row instead of choking on it. Compatibility here is not cosmetic. A misparsed shipping rule is a wrong number on a real invoice.

01

Four ways to price a delivery

The type decides what the rule measures. Every type reads from the same condition list, so switching one for another is a change of question, not a change of tooling.

TypeWhat it measuresIn practice
FixedNothing — it always matches, and charges the first condition's costA standing flat rate. With no conditions at all, delivery is free.
Per item countThe pack count of the items in the rule's categories“One to three pieces cost 4,000; four and above cost 6,000.”
Per order totalThe cart's products total“Under 50,000 costs 5,000; 100,000 and above ships free.”
Per cityThe recipient's city, matched by name against a list of city rows“Inside the city 3,000, the neighbouring governorate 5,000, everywhere else 8,000.”

Per city is the type that most systems get wrong, because they key their rows to a courier's internal city ID. Akkad matches on the name instead — city identity is not stable across seven different couriers, and a merchant who switches carrier should not lose their entire delivery price list.

02

How the matcher picks a price

Rules are not searched for the first hit. Every applicable rule is evaluated, and then the results are compared — which is the part that makes overlapping delivery offers behave sensibly rather than depending on the order somebody happened to create them in.

The sequence is fixed. Rules are first filtered to those whose channel list contains the order's channel, so a storefront-only delivery offer never reprices a counter sale. Quantities are then counted per category. A rule is eligible only if the basket actually contains at least one item in its categories; if no rule is eligible, shipping is zero rather than a guessed fallback. Each eligible rule then evaluates its own conditions — threshold types sort brackets by minimum descending and take the first match, exactly as the discount ladder does.

  • A rule whose brackets all miss contributes nothing rather than blocking the others.
  • Quantities are counted per category, so one rule's scope never leaks into another's threshold.
  • The final cost is the minimum across every rule that matched — the customer always gets the cheapest applicable shipping.
Flat rate — all goods5,000
Free over 100,0000
Baghdad — per city3,000
Charged — lowest of all matches0
Every eligible rule is evaluated, then the cheapest result is charged. A free-shipping threshold therefore overrides a standing flat rate without anyone having to delete it.

The cheapest applicable rule wins, and that is what makes free delivery work

Taking the minimum across all matching rules is a one-line decision with a large consequence. It means a free-delivery threshold does not need to know about the flat rate it is supposed to override, and the flat rate does not need an exception carved into it. Both rules match, both produce a number, and the smaller one is charged. Add a promotion, remove it, layer a third — the arithmetic stays predictable, and no merchant ever has to reason about rule ordering.

03

Free delivery, expressed as a bracket

There is no free-shipping switch, and its absence is the point. Free delivery is written as an ordinary bracket with a cost of zero and an open-ended maximum — a minimum of 100,000 with no ceiling reads as “free delivery over 100,000”. Because the matcher takes the minimum across everything that matched, that bracket automatically beats any standing rate the moment the basket clears the threshold, and stops beating it the moment the basket drops back below.

The same trick works on piece counts: a bracket at four packs and above with a cost of zero is a “buy four, we deliver” offer. And a fixed rule with an empty condition list is free delivery outright, which is the shortest way to say “we never charge for delivery in these categories”.

Expressing an offer as data rather than as a special case is what keeps the preview and the charge in agreement. There is no second code path for the free case that could drift from the paid one.

A box of twelve is one thing to carry

Shipping thresholds count packs, not base units. A box of twelve strips is one unit toward a per-item-count bracket, because the courier is carrying one box. Discount rules deliberately count the opposite way — twelve pieces — because a customer buying twelve of something has bought twelve of something. Two different questions, measured two different ways, on purpose. The order total follows the same logic: for a pack line the price used is the pack's own price multiplied by the number of packs, and the server resolves the pack itself rather than trusting a price sent by a browser, rejecting any pack that does not belong to the exact item and variant on that line.

04

The hard part

One city, many spellings

A merchant writes their city rows once, in their own handwriting. A customer types a city at checkout in whatever spelling they use. A courier's dropdown offers a third spelling. In Arabic these three can differ by diacritics, by tatweel, by which form of alif or ya was on the keyboard, and by whether the definite article was typed at all — and none of those differences mean anything to a human reading the address. So the matcher normalises both sides before it compares them, and then compares in four tiers rather than demanding equality.

الحلةexact
حلهfolded
Hillasimilarity
normalize → fold → compare
Matched ruleBabil — 4,000
Diacritics, tatweel, letter variants and the definite article are folded away, then exact, containment and similarity tiers run in order. Three spellings price as one city.

Normalisation first

Text is lowercased, Arabic diacritics and tatweel are stripped, letter variants are folded together — including the Farsi and Kurdish keyboard forms — the definite article is removed from words of five characters or more, and the tokens are joined with no separator. “الحلة”, “حله” and “Hilla” all resolve to the same row.

Then four tiers, best match wins

An exact match after normalisation scores highest, then substring containment, then a Levenshtein similarity of 0.8 or better, then no match at all. The highest-scoring row wins; where two rows tie, the first one written wins, so the result is stable rather than arbitrary.

A wildcard for everywhere else

A row written as “*” means all other cities. Specific rows always beat it, so a merchant lists the cities they care about and catches the rest with one line. If nothing matches and there is no wildcard, the rule simply does not apply — it never falls back to a nearby price.

The same algorithm in six places

The matcher is mirrored byte for byte in the storefront, in landing pages, and in all four apps. A shopper's checkout preview and the server's authoritative charge are running the same comparison, so they cannot disagree about which city row applied.

TierTestWhat it catches
3 — exactThe two normalised strings are identicalDifferent spellings that normalise to the same thing: alif forms, a missing definite article, diacritics, Latin against Arabic script.
2 — containedOne normalised string contains the other, shorter side at least three charactersA name typed with an extra qualifier attached to it, in either direction.
1 — similarLevenshtein similarity of 0.8 or higherOrdinary typing errors — a transposed pair, a dropped or doubled letter.
0 — no matchNothing above matchedThe row is not a candidate. The wildcard row, if one exists, applies instead.

The three-character floor on containment is there to stop very short fragments from matching half the country. Similarity is the last resort, not the first, so a genuine second city is never quietly priced as its neighbour.

Per-city pricing is resolved from the courier's own list, not the typed name

When a checkout used the courier's city dropdown, the order carries that courier's city ID. The server re-resolves the city name from the courier's own list and prices against that, rather than against the name the browser sent. Without this, a forged name-and-ID pair could buy cheap-city pricing and expensive-city delivery — the customer pays the near rate and the courier delivers to the far one, and the difference comes out of the merchant's pocket.

05

Editing a rule without breaking it

Shipping rules are edited rarely and under time pressure, usually because a rate changed this morning. The write path is therefore deliberately strict about the changes that quietly mis-price everything.

Every city row must name a city

A per-city condition has to carry a non-empty city, or the explicit “*” wildcard. An empty city field is not treated as “any” — it is rejected, because “any” is a decision worth typing.

Changing a rule's type must ship new conditions

Conditions kept from the old type will parse under the new one and produce numbers that look plausible and are wrong. A threshold of 100,000 read as a piece count fires on almost nothing; a piece count read as an order total fires on almost everything. So a type change without new conditions is refused outright.

Converting a per-city rule takes an explicit confirmation

Turning a per-city rule into any other type destroys every city row it holds. That conversion requires an explicit confirmation flag — which older app builds that predate per-city pricing cannot send, so their accidental conversion is rejected rather than silently obeyed. A merchant's whole delivery price list is not something an out-of-date binary should be able to delete.

Writing rules is its own permission

Managing shipping cost rules is a separate permission from managing orders or discounts. Staff who take orders all day do not automatically get the ability to change what delivery costs.

06

Seven carriers, one interface

Pricing answers what to charge. The second half is handing the order to somebody who will carry it. Akkad integrates seven Iraqi couriers directly, each with its own authentication model, all behind one interface in the app — you pick a courier, not a protocol.

CourierAuthenticationNotes
Al Waseet (الوسيط)Username and password exchanged for a token, plus an Akkad merchant token on every callSends the replacement flag for exchange deliveries.
Modon ExpressUsername and password exchanged for a tokenSends the replacement flag.
Hi-ExpressAPI key — validated by fetching the city list rather than by a login callNo replacement flag.
Al Zaeem ExpressUsername and password plus a per-merchant system codeShares the Jenni V2 API family with Alsai.
AlsaiUsername and password plus a per-merchant system codeShares the Jenni V2 API family with Al Zaeem.
TasheelUsername and password exchanged for a tokenShipments are created as a multipart payload.
PrimeBuilt in — the merchant enters no credentials at allMatches cities against a built-in table of 18 governorates with their Arabic spelling variants, rather than a live city API.

The shared system code used by Al Zaeem and Alsai is validated against a strict format and upper-cased on the way in, because a lower-case paste from an email is the single most common reason a merchant's first shipment fails.

07

What every integration does

Seven couriers, seven different APIs, one set of capabilities. Whatever you connect, these five things work the same way.

  1. 1

    Create the shipment

    Recipient name, primary and secondary phone formatted the way that courier expects, city and region IDs, item count, cash-on-delivery amount, nearest landmark, notes, SKU and a goods-type string derived from the order's categories. Rate-limited and retried, because courier APIs are not always available at the moment you press send.

  2. 2

    Reconcile the status

    Tracking numbers are queried in batches and each courier's own vocabulary is normalised into four buckets: delivered, not delivered, in transit, and needs review. Only delivered writes back to the order, setting the status and appending a log entry in the reader's language.

  3. 3

    Send anything unrecognised to review

    A status code the integration does not recognise maps to needs review rather than being guessed at. Shipments the courier cannot find land there too. A wrong guess about delivery is worse than no guess: it closes an order that is still moving, or reopens one that already arrived.

  4. 4

    Pull the address tree

    City lists and region or district lists come back from every courier as a standardised map, paginated where the courier paginates. That is what lets a checkout form show the courier's real address tree instead of a free-text box the driver has to interpret.

  5. 5

    Keep the label and the money straight

    The label and its URL are stored on the shipped order. Where a courier reports the amount it expects to collect, the reconciliation returns that figure alongside the order's own amount, so a mismatch is something you read rather than something you discover at the end of the month.

08

Shared behaviour across all seven

The details that only surface after a few thousand real shipments — each one written once and applied to every courier.

Cash on delivery goes to zero when the order was paid online

A gateway-prepaid order must not be charged again at the door. The zeroing keys on whether a verified payment webhook fired, not on the payment status field — because a merchant marking an order paid by hand is recording their own bookkeeping, and must not silently change what the courier collects.

Emoji are stripped from free text

Several courier backends run three-byte UTF-8 and either reject or crash on a four-byte character. Every free-text field is sanitised on the way out; IDs and phone numbers are never touched, because altering those would break the shipment rather than save it.

Failures come back readable

Raw errors are mapped to localised, merchant-readable messages that surface the courier's own parsed rejection reason. No stack traces reach the person trying to send a parcel.

A rejection diagnostic where one is needed

One courier masks every server-side failure behind a single catch-all network error. For that integration Akkad logs the payload, the city and region IDs, the price format, the notes length and which fields carried non-standard characters — so “it just failed” becomes an answerable question.

Credentials are testable before they are saved

Each courier has a validate endpoint that tries the credentials without storing them. You find out that a password is wrong while you are looking at the form, not when the first order needs to ship.

Three notes toggles, per merchant

Item names, SKUs and prices can each be appended into the courier's notes field independently. Some merchants want the driver to know what is in the box; others emphatically do not want the price written on it.

Sending is not one order at a time. A bulk send ships either a chosen list of orders or every order currently in processing, in a single sweep — and either way only processing orders are eligible, so a sweep cannot resurrect something cancelled or double-send something already gone. Where a team member's visibility is restricted to their own orders, that restriction is applied to the sweep too: a bulk action never becomes a way around a permission.

09

Questions

What happens if two shipping rules both match an order?

Both are evaluated and the cheaper cost is charged. That is the whole conflict-resolution model — there is no priority field to maintain and no dependence on the order the rules were created in. It also means a free-delivery threshold overrides a standing flat rate automatically.

What does an order cost to ship if no rule matches it?

Zero. A rule is only eligible if the basket contains at least one item in its categories, and if nothing is eligible, shipping is zero rather than an assumed default. Charging a number nobody configured is a worse failure than charging nothing.

Do I have to spell city names the same way my customers do?

No. Both sides are normalised — case, Arabic diacritics, tatweel, letter variants and the definite article are all folded away — and then compared across four tiers ending in fuzzy similarity. “الحلة”, “حله” and “Hilla” resolve to the same row. A “*” row catches everything you did not list.

Will the delivery price shown at checkout match what I am charged?

Yes. The matcher, including the city algorithm, is mirrored in the storefront, in landing pages and in all four apps, and the server recomputes the authoritative figure at order creation. Where the checkout used a courier's city dropdown, the server re-resolves the city from the courier's own list rather than the submitted name.

Can I switch couriers without redoing my delivery pricing?

Yes, and that is why per-city rules match on names rather than courier IDs. Your city rows are yours; the courier's ID scheme is the courier's. Switching integration changes which address tree the checkout shows, not what you charge.

What happens to a status my courier reports that Akkad does not recognise?

It is filed as needs review, along with any shipment the courier cannot find. Only a delivered status writes back to the order. An unrecognised code is never mapped to a best guess, because a guess about delivery is a guess about money.

Pricing rules

The same bracket model applied to discounts — four ways to measure a cart and three ways to reduce it.

Orders

The lifecycle a shipment attaches to: six statuses, stock rebalancing, editing and shopper-side tracking.

Online store

Where the checkout preview lives — carrier city pickers, form builder and server-side repricing.

इसे अपने ही कैटलॉग पर परखें

अपने हिसाब का प्लान चुनें, स्प्रेडशीट से प्रोडक्ट इम्पोर्ट करें, और उसी दिन चलता हुआ स्टोर, काउंटर और ऑर्डर बुक पाएँ।

फ़्री प्लान उपलब्ध है। आज़माने के लिए कार्ड की ज़रूरत नहीं। इम्पोर्ट आपका मौजूदा कैटलॉग साथ ले आता है।