Платформа

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.

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.

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.

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”.

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.

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.

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.

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.

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.

Посмотрите на своём собственном каталоге

Начните с подходящего тарифа, импортируйте товары из таблицы — и уже в тот же день у вас будет работающий магазин, касса и книга заказов.

Есть бесплатный тариф. Карта для пробы не нужна. Импорт перенесёт ваш нынешний каталог.