Courier Tracking Status Update
Twice a day, this automation asks every carrier we use where our undelivered orders have got to, and writes the answer onto the fulfilment record in NetSuite. It is what makes the delivery status a member of staff sees current, without anyone opening seven different carrier websites. It only ever moves a delivery forward — where the carrier cannot be read, or where somebody has already recorded something newer by hand, NetSuite is left exactly as it was.
Rules & IDs
| Rule ID | Rule | Reading |
|---|---|---|
LI-BL-FUL-018 |
Which deliveries we chase | Business logic |
LI-BL-FUL-019 |
Matching a delivery to its carrier | Business logic |
LI-BL-FUL-020 |
Which event counts as "now" | Business logic |
LI-BL-FUL-021 |
Turning the carrier's words into our status | Business logic |
LI-BL-FUL-022 |
Never overwrite something good with nothing | Business logic |
LI-BL-FUL-023 |
Never move a delivery backwards | Business logic |
LI-BL-FUL-024 |
What actually gets written | Business logic |
LI-BL-FUL-025 |
Finish what fits, hand the rest to the next run | Business logic |
LI-BL-FUL-026 |
Proving each carrier can still be read | Business logic |
| What | Name in the system | |
|---|---|---|
| The Lambda — orchestrator, drivers and change detectorGitHub repository | lifeinteriors/courier-tracking-status @ d81a450 — courier-tracking-status | open at d81a450 |
| Saved search — undelivered fulfilments, all carriers in scopeNetSuite saved search | customsearch7756 needs confirming | open search 7756 |
The NetSuite links use the account-neutral host, which redirects a signed-in user to this account. Ids and names are exact, so a search stays findable by name even if a link does not resolve.
Three readings of the same rules. Nothing is duplicated between them. Printing gives you the business reading followed by the diagrams.
What it does. Twice a day it takes every delivery that has left the warehouse but has not yet been marked delivered, asks the carrier who is carrying it where it has got to, and records the answer against the delivery in NetSuite — the stage it has reached, the carrier's own words for the latest thing that happened, the date it was delivered if it has been, how many cartons the carrier is holding, and a link to the carrier's tracking page. Seven carriers are covered. Nothing is written unless the carrier is genuinely ahead of what NetSuite already holds.
Which couriers this covers. Only these. A delivery on anything else keeps whatever status it already has, indefinitely and without any warning that it has stopped moving — so a frozen status on a courier below the line is expected, not a fault to report.
| Courier | Kept up to date? | What to know |
|---|---|---|
| Hunter Express | Yes | |
| VT Freight Express | Yes | |
| Air Road | Yes | |
| Designer Transport | Yes | |
| Aramex | Yes | Still called Fastway by many people; both names are recognised |
| Australia Post | Yes, ours only | Deliveries booked under a supplier's own Australia Post account cannot be tracked — Australia Post only tells us about our own account. These show as no data every run |
| Couriers Please | No — not since 25 August 2026 | Couriers Please closed their tracking to us. Existing statuses are kept but will not move. Look these up on the Couriers Please website by hand until this is restored. Getting it back needs an account-manager conversation, not a fix at our end |
| Direct Freight | No | Their tracking blocks automated reading. Look up by hand |
| Xtreme Freight | No | Tracking requires a portal login |
| COPE | No | They publish no tracking at all |
Why it exists. Delivery status is the single question Customer Service is asked most, and until this ran, answering it meant opening a different website for every carrier and pasting in a consignment number. That is slow at best, and at worst it is wrong: staff quote the last thing anyone happened to look up. The cost of it going wrong runs both ways. Stale statuses mean customers are told a delivery is in transit when it failed three days ago, and the failure is only found when they call again. Statuses that are confidently wrong are worse — a delivery marked delivered when it has not been closes the record, stops the chasing, and the loss surfaces weeks later as a claim the carrier will no longer accept.
Who it affects. Customer Service notices first, because they read the status to answer "where is my order". The Fulfilment Team notices next, because failed deliveries and returns are theirs to act on and they only see them here. Customers feel it indirectly, through the accuracy of what they are told. The carriers themselves are affected in one narrow way: we are reading their public tracking pages, deliberately slowly and under a contact-carrying name, so that any of them can email us before they block us.
When it runs. On a schedule, twice a day — early morning and early evening. A delivery is eligible when it appears in the saved search of undelivered fulfilments, has a consignment number on it, and was booked on a ship method one of the seven carriers is registered against. Deliveries drop out of the search once they are marked delivered, so the automation naturally stops chasing them. Nothing about this runs on demand: a delivery that moved an hour ago will not show here until the next run.
How it decides.
- It starts from a list of undelivered deliveries and, for each one, works out which carrier to ask from the ship method the delivery was booked on. If no carrier matches the ship method, or there is no consignment number, the delivery is left alone entirely.
- It asks that carrier about the consignment and takes the most recent thing that has already happened — scheduled future steps are ignored, and where a delivery and an earlier step share a day, the delivery wins.
- It translates the carrier's own wording into our own twenty-stage list. Every carrier says it differently; "ATL", "POD captured" and "Authority To Leave" all mean delivered. Wording nothing recognises is left untranslated, and nothing is written.
- Before writing, it checks it is not making things worse. If the carrier could not be read at all and NetSuite already holds a sensible status, NetSuite is kept. If somebody has recorded something by hand more recently than the carrier's latest event, NetSuite is kept — unless the carrier is reporting a delivery or a return, which is progress rather than a regression.
- It writes only when the stage or the wording of the latest event has genuinely changed. A fresh timestamp on an unchanged event is not a change.
- It works to a clock. Reading carriers is deliberately slow, so if it runs out of time it writes back everything it has finished and hands the remainder to the next run, rather than being cut off with nothing saved.
Outcomes. For each delivery, exactly one of:
| Outcome | What is written | What it means |
|---|---|---|
| Updated | Status, comment, and where they apply the delivery date, carton count and tracking link | The carrier is ahead of NetSuite |
| No change | Nothing | The carrier agrees with what NetSuite already says |
| Kept | Nothing | The carrier could not be read, or NetSuite already holds something newer |
| No data | Nothing | The carrier does not recognise the consignment number |
| Not recognised | Nothing | The carrier answered, but in wording our list has no translation for |
| Skipped | Nothing | No consignment number, or a ship method no carrier is registered against |
| Deferred | Nothing this run | The run ran out of time before reaching this delivery |
Which deliveries we chase LI-BL-FUL-018
| # | If… | Then… | Because |
|---|---|---|---|
| 1 | The delivery is in the undelivered-fulfilments saved search | It is in scope for this run | The search is the only definition of scope; nothing else selects deliveries |
| 2 | The delivery has no consignment number | Skip it, write nothing | There is nothing to ask the carrier about |
| 3 | The delivery's ship method matches no registered carrier | Skip it, write nothing, and count it | It is not a fault in the delivery — it means a carrier has not been set up yet, and the count is how that gets noticed |
| 4 | The delivery has since been marked delivered | It has already left the search | Delivered deliveries stop being chased automatically, which is also what lets a backlog drain |
Matching a delivery to its carrier LI-BL-FUL-019
Seven couriers are set up — the list is above, along with the ones that are not. What follows is how a delivery is matched to one of them, which is worth understanding because the commonest way a delivery goes untracked is a ship method nobody recognised rather than a courier nobody supports.
| # | If… | Then… | Because |
|---|---|---|---|
| 1 | The ship method exactly matches one a carrier claims | Use that carrier | Exact names are unambiguous and are checked first |
| 2 | The ship method is not an exact match but mentions a carrier's name | Use that carrier | Ship methods are renamed constantly — new regions, new size bands, new prefixes — and a name-based fallback survives that where a fixed list does not |
| 3 | The ship method mentions no carrier we know | Skip the delivery | Guessing a carrier would produce a confidently wrong status, which is worse than none |
| 4 | Two carriers could both claim the ship method | The one registered earlier wins | The order the carriers are registered in is the tie-break; it is fixed, not alphabetical |
Which event counts as "now" LI-BL-FUL-020
A carrier returns a delivery's whole history, not its current state. Choosing which line of that history is the current state is a decision in its own right.
| # | If… | Then… | Because |
|---|---|---|---|
| 1 | An event is dated in the future | Ignore it | Some carriers list a scheduled delivery run alongside what has happened; treating one as current would mark a delivery as out for delivery tomorrow |
| 2 | Several events have already happened | Take the most recent | This is the ordinary case |
| 3 | A delivery or a return shares its day with a later-looking ordinary step | The delivery or return wins | Freight does not get un-delivered. A delivery recorded with a date but no time otherwise loses to an earlier step that carried a clock time, and the delivery would sit on the wrong status permanently |
| 4 | No event carries a date we can read | Take the last one the carrier listed | Something is better than nothing, and the carrier lists them in order |
Turning the carrier's words into our status LI-BL-FUL-021
| # | If… | Then… | Because |
|---|---|---|---|
| 1 | The carrier is one with a small, fixed vocabulary of its own | Match its exact wording first | Two carriers use words that mean something specific to them and nothing to anyone else — a run that has "started", a parcel "staged for delivery" |
| 2 | No exact match applies | Try the general wording patterns, in order, and take the first that fits | Most carriers describe the same handful of events in slightly different English |
| 3 | Still nothing fits, but the carrier gave an overall state | Fall back to that | A coarse answer beats no answer |
| 4 | Nothing fits at all | Write nothing, and record that the wording was not recognised | This is the signal that a carrier has introduced new wording. It is an engineering fix, not a data problem, and it must be visible as such |
Never overwrite something good with nothing LI-BL-FUL-022
| # | If… | Then… | Because |
|---|---|---|---|
| 1 | The carrier could not be read, and NetSuite already holds a recognised status | Keep what NetSuite has | A carrier's website being down for ten minutes must not erase a week of known progress |
| 2 | The reason was that the carrier's page could no longer be understood | Keep NetSuite's value, and raise it as a fault | This is how a carrier rebuilding their website shows up. Left silent it looks identical to a delivery with nothing new to report — which is exactly how one carrier's rebuild went unnoticed for weeks |
| 3 | The reason was a network or rate-limit error | Keep NetSuite's value, and note the error against the delivery | Expected, self-correcting, but never silent |
| 4 | The reason was that the carrier simply had nothing to say | Keep NetSuite's value | The ordinary quiet case |
Never move a delivery backwards LI-BL-FUL-023
| # | If… | Then… | Because |
|---|---|---|---|
| 1 | The last note on the delivery in NetSuite is newer than the carrier's latest event | Keep what NetSuite has | Somebody has been told something more recent, usually by phone. Overwriting it with older carrier data throws away better information |
| 2 | …unless the carrier is now reporting a delivery or a return we do not already hold | Write it | Arriving at delivered is progress, not a regression. Without this exception a delivery recorded with a date but no time reads as older than a note made that morning, and the delivery would never land no matter how many times we ran |
| 3 | The note in NetSuite carries no readable timestamp | Treat the carrier as newer | There is nothing to compare against, and the carrier is the better source by default |
What actually gets written LI-BL-FUL-024
| # | If… | Then… | Because |
|---|---|---|---|
| 1 | The stage or the wording of the latest event has changed | Write the stage and the comment | These are the two things anyone reads |
| 2 | Only the timestamp has changed | Write nothing | Every run would otherwise rewrite every delivery, which buries the real changes |
| 3 | The delivery has reached delivered and the carrier's event carried a readable date | Also write the delivery date | It is the date claims and reporting are counted from |
| 4 | The carrier publishes a carton count above zero | Also write it | Not all of them do; a blank leaves whatever was there |
| 5 | Anything at all is being written | Also write the link to the carrier's tracking page | So the next person does not have to work out which carrier it was |
| 6 | Nothing above applies | Write nothing at all | A no-op write still costs an audit-trail entry on the record |
Finish what fits, hand the rest to the next run LI-BL-FUL-025
| # | If… | Then… | Because |
|---|---|---|---|
| 1 | Time is running short | Stop taking on new deliveries | Reading carriers is deliberately slow, so a large day's list may not fit in one run |
| 2 | A carrier has finished its share | Write that carrier's results back immediately | A slower carrier still running must not cost us work already done |
| 3 | Time runs out mid-carrier | Write back what that carrier finished, and record how many were not reached | The unreached ones are deferred, not failed, and the distinction matters when reading a run |
| 4 | Deliveries were deferred | The next run picks them up | Anything written back drops out of the search, so successive runs drain the backlog instead of re-doing the same head of the list |
| 5 | One carrier fails outright | The others still complete and still write back | One carrier's bad day is not an outage |
Proving each carrier can still be read LI-BL-FUL-026
| # | If… | Then… | Because |
|---|---|---|---|
| 1 | Every run finishes | Re-check one long-settled delivery per carrier and confirm it still reads as expected | A carrier changing their website does not raise an error — deliveries just quietly stop updating. This turns silent staleness into one line anybody can search for |
| 2 | A check disagrees with what is expected | Record it as a failure against that carrier | The delivery chosen can no longer change, so a disagreement always means us, never the freight |
| 3 | The run is nearly out of time | Skip the checks | They are the part we can most afford to lose |
| 4 | A carrier has no settled delivery recorded to check against | It is not checked at all | Which is itself worth knowing, and is recorded as skipped rather than passed |
Field registry
Every field this automation reads or writes, by the name it carries in the system — what it means, and what changes when it changes. Field ids are in the developer reading.
Inputs — what it reads
Values the automation looks at to make its decision. Change one of these and the decision changes.
| Field | What it means | What it changes | Status |
|---|---|---|---|
| needs confirmingItem Fulfillment | The NetSuite record this delivery belongs to. | Identifies the record every write-back targets. A row without one cannot be written back at all. | needs confirming |
| needs confirmingItem Fulfillment | The fulfilment number staff quote to each other and to customers. | Used only to label the row in the run log, so one delivery can be found across runs. Nothing is decided from it. | needs confirming |
| needs confirmingItem Fulfillment | The carrier's own reference for this delivery — the number printed on the label. | This is what is looked up with the carrier. A blank one drops the row from the run entirely, and a number the carrier does not recognise ends the row with nothing written. | needs confirming |
| Ship ViaItem Fulfillment | Which carrier and service the delivery was booked on. | Decides which carrier is asked about the delivery. A ship method no carrier is registered against is skipped, so the delivery is never tracked until someone adds it. | needs confirming |
| needs confirmingItem Fulfillment | Where the carrier says this delivery has got to, on a fixed list of twenty stages. | The field the automation exists to keep current. It is read to decide whether the carrier is actually ahead of NetSuite, and only written when it is. Customer Service reads it to answer "where is my order". | needs confirming |
| needs confirmingItem Fulfillment | The carrier's latest tracking event in words, with the time it happened. | Carries the timestamp the no-regression rule compares against, so it decides whether a carrier update is allowed to land. Rewritten whenever the status or the wording of the latest event changes; a change to the timestamp alone is not enough to trigger a write. | needs confirming |
Outputs — what it writes
Values the automation puts back. Everything downstream of this rule reads them, so a wrong value here travels.
| Field | What it means | What it changes | Status |
|---|---|---|---|
| needs confirmingItem Fulfillment | Where the carrier says this delivery has got to, on a fixed list of twenty stages. | The field the automation exists to keep current. It is read to decide whether the carrier is actually ahead of NetSuite, and only written when it is. Customer Service reads it to answer "where is my order". | needs confirming |
| needs confirmingItem Fulfillment | The carrier's latest tracking event in words, with the time it happened. | Carries the timestamp the no-regression rule compares against, so it decides whether a carrier update is allowed to land. Rewritten whenever the status or the wording of the latest event changes; a change to the timestamp alone is not enough to trigger a write. | needs confirming |
| needs confirmingItem Fulfillment | The date the delivery actually completed. | Written only when the status becomes Delivered and the carrier's delivery event carried a date the automation could read. A delivered consignment whose event time cannot be parsed reaches Delivered with this left blank. | needs confirming |
| needs confirmingItem Fulfillment | How many cartons the carrier records against this consignment. | Written only where the carrier publishes an item count, and only when that count is above zero. Carriers that do not publish one leave whatever NetSuite already held. | needs confirming |
| needs confirmingItem Fulfillment | A link straight to the carrier's own tracking page for this delivery. | Written on every update, so anyone looking at the fulfilment can open the carrier's page without first working out which carrier it is. | needs confirming |
Inputs — saved search 7756, through the RESTlet
What it reads, and where from.
Rows are fetched with a signed GET against the NetSuite RESTlet, which runs the saved search named
by NSSS_IDS (default 7756; comma-separated for several). The RESTlet returns rows; column names
are normalised on arrival, because a saved search may return either the label or the internal id.
| Field / source | System | Type | Notes |
|---|---|---|---|
internalid |
NetSuite (search 7756) | integer | The write-back target. Falls back to internalID / id if the search labels it differently |
tranid |
NetSuite (search 7756) | text | Log label only |
consignmentNumber |
NetSuite (search 7756) | text | Blank ⇒ the row is skipped. The underlying NetSuite field id is not readable from this repository — the RESTlet that supplies it is not in version control (see Known limitations) |
Ship Via |
NetSuite (search 7756) | select | Read as Ship Via, then shipVia, then shipmethod, in that order |
custbody_delivery_status |
NetSuite (search 7756) | select (1–20) | Existing value, compared against the carrier's |
custbody_courier_tracking_comment |
NetSuite (search 7756) | text | Existing value; its Updated At stamp drives LI-BL-FUL-023 |
| Carrier tracking endpoint | AusPost API / 6 public endpoints | JSON or HTML | One per carrier; see Carriers and endpoints |
context.getRemainingTimeInMillis |
AWS Lambda | function | The time budget for LI-BL-FUL-025. Absent locally ⇒ unbounded |
Environment: NS_ACCOUNT_ID, NS_RESTLET_URL, NS_CONSUMER_KEY, NS_CONSUMER_SECRET,
NS_TOKEN_ID, NS_TOKEN_SECRET are required and the handler throws by name if any is missing.
AP_API_KEY / AP_API_PASSWORD / AP_ACCOUNT_ID are needed only when AusPost rows are in scope.
DRY_RUN, CANARY_ENABLED, CANARY_FAIL_MODE, WRITE_RESERVE_MS and AP_RATE_PER_MIN are
optional switches.
Carriers and endpoints
| Carrier | Mapping source | Access | Rate limit | Retries |
|---|---|---|---|---|
| AusPost | exact-match table | Enterprise API, Basic auth, batches of 10 | 45/min, burst 1 | 3 |
| Hunter Express | generic patterns | Public widget on a third-party portal (JSONP) | 10/min | 0 |
| VT Freight Express | generic patterns | Public portal page (HTML) | 5/min | 0 |
| Air Road | generic patterns + code table | Public JSON API | 10/min | 0 |
| Designer Transport | exact-match table | Public HTML page | 5/min | 2 |
| Couriers Please | generic patterns + node-type table | Public JSON API — blocked, see defects | default 5/min | 0 |
| Aramex (Fastway) | generic patterns + type-code table | Public JSON API | default 5/min | 0 |
Burst is the bucket's capacity, and it matters as much as the rate: a full bucket plus a minute of refill lets roughly twice the nominal rate through in the first sixty seconds, which is how a nominal 45/min still overran AusPost's measured ~49/min ceiling. Buckets are keyed by host, so carriers on different hosts run concurrently without interfering, and two carriers sharing a host share one bucket automatically.
Processing
What it does with that, step by step.
on scheduled run:
fetch rows from each configured saved search
normalise each row's column names
for each row: -- LI-BL-FUL-018
if no consignment number -> skip, log SKIP
find carrier by ship method -- LI-BL-FUL-019
normalise the ship method: decode HTML entities, collapse spaces, lowercase
try exact match against every name a carrier claims
else try each carrier's name pattern, in registration order
if no carrier -> skip, log SKIP, count the ship method
else add the row to that carrier's group
run all carrier groups at the same time; queue writes one at a time:
if out of time before starting -> defer the whole group -- LI-BL-FUL-025
ask the carrier about each consignment
(AusPost is asked about ten at a time; the rest one at a time)
stop taking new ones once out of time -> defer the remainder
for each answered row: -- LI-BL-FUL-020
choose the current event:
ignore events dated later than now
take the most recent of the rest
a delivery or return outranks anything on the same day or earlier
if none has a readable date, take the carrier's last listed event
translate it into our status list: -- LI-BL-FUL-021
for the two carriers with their own vocabulary, try their exact wording
else try the general patterns in order, first match wins
else fall back to the carrier's overall state
else no status
decide:
if no status and NetSuite holds a recognised one
-> keep NetSuite's -- LI-BL-FUL-022
reason = could not read the page
| carrier error
| nothing to say
else if NetSuite's note is newer than the carrier's event
and the carrier is not reporting a new delivery or return
-> keep NetSuite's -- LI-BL-FUL-023
else if stage changed or event wording changed
-> queue an update -- LI-BL-FUL-024
else -> no change
write this carrier's queue back, fifty at a time
if any row could not be read, raise one alert line for the carrier
write back anything still queued
re-check one settled consignment per carrier -- LI-BL-FUL-026
report per-carrier counts and anything deferred
The comment written alongside the status is built as
<event><, at location>. Updated At <timestamp>, where the timestamp is the carrier event's own
time where it has one, the event's date alone where it does not, the carrier's raw string where
neither parses, and the run time marked (scrape time) where the event carried no time at all.
Comparison for "has this changed" strips the Updated At … tail from both sides first, so a
re-stamped but otherwise identical comment is not a change (LI-BL-FUL-024 row 2).
Outputs & side effects
What it writes, and who reads it afterwards.
| Output | Written to | Downstream consumer |
|---|---|---|
custbody_delivery_status |
Item Fulfillment, via the RESTlet POST |
Customer Service, answering delivery questions |
custbody_courier_tracking_comment |
Item Fulfillment | Staff reading the record; also read back next run as the no-regression clock |
custbody_actual_delivery_date |
Item Fulfillment | Delivery performance reporting; carrier claims |
custbody_courier_carton_received |
Item Fulfillment | Fulfilment, reconciling cartons against what shipped |
custbody_tracking_link |
Item Fulfillment | Anyone opening the carrier's page from the record |
| One log line per row, with a verb | CloudWatch Logs | Engineering, and the metric filters below |
[ALERT] lines, per carrier |
CloudWatch Logs | The intended trigger for a metric filter |
[CANARY-OK] / [CANARY-FAIL] / [CANARY-SKIP] |
CloudWatch Logs | Engineering — carrier integration health |
Writes go out in batches of fifty. Tracking runs concurrently across carriers; writes are chained so
NetSuite only ever receives one POST at a time. A carrier's write failure is caught and does not
block the other carriers' writes.
The log verbs are deliberately greppable: [UPDATE], [NO-CHG], [PRESERVE], [NO-DATA],
[NO-MAP], [ERROR], [SKIP], [DEFER], [ALERT], [CANARY-FAIL].
Nothing outside NetSuite is written. Towards the carriers the automation is strictly read-only: it requests tracking pages under a User-Agent naming an ops contact address, so a carrier can email before blocking.
Worked examples
Real records carried end to end, so the logic can be checked against something that happened.
No production record has been traced. These are walked through the documented rules from the code at
d81a450, using the consignment numbers the drivers carry as their health-check references — those numbers are real, but their live status at the carrier has not been confirmed here, and neither has any NetSuite record. They are labelled as walkthroughs, not as evidence.
Example 1 — the ordinary case: a Hunter Express delivery completes
| Step | Value | Why |
|---|---|---|
| Input row | Ship Via AU WIDE - Hunter Express, consignment XVX618298, status Out for delivery, comment On delivery at SYDNEY. Updated At 21/04/2026 08:15 |
In the search, has a consignment, ship method matches Hunter Express exactly (LI-BL-FUL-019 row 1) |
| Carrier answer | Events ending Delivered at SYDNEY, 21/04/2026 14:32 |
— |
| Current event | The Delivered row |
Latest already-happened event (LI-BL-FUL-020 row 2) |
| Translation | Generic patterns → Delivered (id 7) |
delivered is the first generic pattern tried (LI-BL-FUL-021 row 2) |
| No-regression check | Carrier event 14:32 is later than the note's 08:15 → proceed | LI-BL-FUL-023 row 1 does not bite |
| Decision | Stage changed Out for delivery → Delivered; comment changed |
LI-BL-FUL-024 row 1 |
| Written | status 7; comment Delivered at SYDNEY. Updated At 21/04/2026 14:32; delivery date 2026-04-21; tracking link |
Delivery date because the stage is Delivered and the event date parsed (LI-BL-FUL-024 row 3) |
Example 2 — the awkward case: a same-day tie that used to freeze the row
Designer Transport stamps the last two cards of a delivery run on the same minute — consignment
721646 carries both started and delivered at 07:22 AM.
| Step | Value | Why |
|---|---|---|
| Carrier answer | Item Scanned in Depot 16/07/2026 12:03, started 17/07/2026 07:22, delivered 17/07/2026 07:22 |
Two events on an identical timestamp |
| Event ordering | The events are sorted chronologically, but an equal-timestamp group is reversed | Latest-event selection keeps the first of an equal group, so document order would hand back started |
| Current event | delivered |
It is terminal, and it is on the same day as the latest ordinary step (LI-BL-FUL-020 row 3) |
| Translation | Designer Transport's own vocabulary → Delivered (id 7) |
LI-BL-FUL-021 row 1 |
| Written | status 7, delivery date 2026-07-17 |
Without either the tie-break or the terminal rule, the row would read Out for delivery on a delivered parcel — indefinitely, and indistinguishably from a healthy unchanged row |
Example 3 — nothing is written, twice over
| Step | Value | Why |
|---|---|---|
| Input row | Ship Via yDrop Ship - Australia Post, status In transit to courier depot |
Matches the AusPost carrier on its name pattern |
| Carrier answer | No trackable items — the consignment is under the supplier's AusPost account, not ours | LI-BL-FUL-018's scope let it in; the carrier cannot answer |
| Translation | No event, no overall state → no status | — |
| Decision | NetSuite holds In transit to courier depot, which is one of the eleven recognised statuses → keep |
LI-BL-FUL-022 row 4 |
| Written | Nothing | Logged [NO-DATA]. The documented fix is to exclude these ship methods in saved search 7756 so the row never reaches the automation |
Test cases
What to run to know the behaviour is intact. Expected values, never “should work”.
| Case ID | Layer | Preconditions | Input | Expected output | Rule ref |
|---|---|---|---|---|---|
CT-TC-T1 |
The run groups rows | Row with consignmentNumber empty, Ship Via AU WIDE - Hunter Express |
— | Row is skipped, logged [SKIP] … empty consignment number; no carrier call |
LI-BL-FUL-018 |
CT-TC-T2 |
Carrier matching | Ship Via AU WIDE&nbsp;- Hunter Express (entity-encoded) |
— | Resolves to Hunter Express — entities decoded, whitespace collapsed, lowercased before matching | LI-BL-FUL-019 |
CT-TC-T3 |
Carrier matching | Ship Via METRO - Designer Transport - Standard NEW (not in the exact list) |
— | Resolves to Designer Transport on the name pattern | LI-BL-FUL-019 |
CT-TC-T4 |
Carrier matching | Ship Via AU WIDE - Direct Freight |
— | No carrier; [SKIP]; the ship method appears in the run's unmatched count |
LI-BL-FUL-019 |
CT-TC-T5 |
Current-event selection | Events In transit 20/07/2026 09:00 and Out for delivery 25/07/2026 06:00, run on 21/07/2026 |
— | Picks In transit — the 25/07 event has not happened yet |
LI-BL-FUL-020 |
CT-TC-T6 |
Current-event selection | Events started 17/07/2026 07:22 then delivered 17/07/2026 07:22 |
— | Picks delivered |
LI-BL-FUL-020 |
CT-TC-T7 |
Translation | Event text Onboard for delivery, AusPost |
— | Status id 6 (Out for delivery) from the AusPost exact table |
LI-BL-FUL-021 |
CT-TC-T8 |
Translation | Event text Staged for Delivery, Designer Transport |
— | Status id 5 (Ready for delivery) from Designer's own table — no generic pattern matches this wording |
LI-BL-FUL-021 |
CT-TC-T9 |
Translation | Event text Consignment repacked at hub |
— | No status; row logged [NO-MAP]; nothing written |
LI-BL-FUL-021 |
CT-TC-T10 |
Decision | Carrier throws HTTP 503; NetSuite holds Out for delivery |
— | Kept; reason carrier error; [PRESERVE] line carries the driver error |
LI-BL-FUL-022 |
CT-TC-T11 |
Decision | Carrier answers but the page cannot be parsed; NetSuite holds Out for delivery |
— | Kept; logged [ERROR] … parse failure, not [PRESERVE]; carrier's [ALERT] line raised |
LI-BL-FUL-022 |
CT-TC-T12 |
Decision | NetSuite comment … Updated At 21/04/2026 09:00; carrier's latest event In transit 20/04/2026 14:00 |
— | Kept; reason NS has newer comment |
LI-BL-FUL-023 |
CT-TC-T13 |
Decision | Same as T12, but the carrier's event is Delivered 20/04/2026 and NetSuite's status is not Delivered |
— | Written — terminal progression overrides the newer note | LI-BL-FUL-023 |
CT-TC-T14 |
Decision | Carrier repeats yesterday's event, with today's scrape time in the comment | — | No change — the Updated At tail is stripped before comparison |
LI-BL-FUL-024 |
CT-TC-T15 |
Write payload | Status becomes Delivered; event 21/04/2026 14:32 |
— | custbody_actual_delivery_date = 2026-04-21; status 7; tracking link present |
LI-BL-FUL-024 |
CT-TC-T16 |
Write payload | Status becomes Delivered; event time unparseable |
— | Status 7 written; custbody_actual_delivery_date absent from the payload |
LI-BL-FUL-024 |
CT-TC-T17 |
Write payload | Carrier returns carton count 0 |
— | custbody_courier_carton_received absent — zero is not written |
LI-BL-FUL-024 |
CT-TC-T18 |
Full run | 400 Designer Transport rows at 5/min, 15-minute invocation | — | Rows tracked until the reserve is reached, those written back, the remainder logged [DEFER]; run reports deferredRows > 0 |
LI-BL-FUL-025 |
CT-TC-T19 |
Full run | Hunter Express throws while Air Road is mid-run | — | Air Road completes and writes back; the run logs [ERROR] Hunter Express: carrier run failed |
LI-BL-FUL-025 |
CT-TC-T20 |
Canary | Designer Transport's page markup changes | — | [CANARY-FAIL] Designer Transport … could not parse the carrier's response |
LI-BL-FUL-026 |
CT-TC-T21 |
Canary | AusPost driver, whose health-check consignment is unset | — | [CANARY-SKIP] AusPost | no canary consignment declared on the driver — today's behaviour, and a defect (CT-3). Intended: a real settled AusPost consignment is checked and passes |
LI-BL-FUL-026CT-3 |
CT-TC-T22 |
Decision | NetSuite holds Delivery attempt failed; carrier unreadable |
— | Today: logged [NO-DATA]/[NO-MAP], not counted as preserved (defect CT-2). Intended: [PRESERVE], reason carrier error. Nothing is written either way |
LI-BL-FUL-022CT-2 |
UAT
What a person checks, by hand, before it is trusted.
No system access is needed beyond the fulfilment record itself.
| # | Step | What to check | Signed off by | Date |
|---|---|---|---|---|
| U1 | Pick a fulfilment you know was delivered yesterday, on any carrier | Its delivery status reads Delivered, and the actual delivery date is yesterday | ||
| U2 | On the same record, open the tracking link | It opens the right carrier's page, showing the same consignment | ||
| U3 | Read the tracking comment | It says the same thing the carrier's page says, and the time in it matches the carrier's last event | ||
| U4 | Pick a fulfilment still in transit; note its status; check it again after the next run | The status has either moved forward or stayed the same. It must never go backwards | ||
| U5 | Add a note by hand to a fulfilment's tracking comment, in the form … Updated At DD/MM/YYYY HH:MM, dated now |
After the next run, your note is still there and has not been overwritten by older carrier data | ||
| U6 | Pick a fulfilment on a ship method you believe is not covered — Direct Freight, or a supplier's own Australia Post account | Its status does not change. This is expected, not a fault: confirm it against the courier list in the business reading rather than raising it | ||
| U7 | Ask engineering for the last run's per-carrier summary | Every carrier you use appears, with a row count. A carrier missing entirely means no delivery matched its ship methods |
Edge cases
The inputs that sit at the boundary, and whether each is handled.
| Case | Behaviour | Handled? |
|---|---|---|
| Ship method arrives HTML-entity encoded from NetSuite | Decoded, collapsed and lowercased before matching | ✅ |
| Ship method renamed with a new region or size suffix | Name-pattern fallback still resolves the carrier | ✅ |
| Same consignment number on several fulfilments | De-duplicated before the carrier is called; every row is still decided and written | ✅ |
| Carrier lists a scheduled future delivery run | Future-dated events ignored when choosing the current one | ✅ |
| A delivery and an earlier step share a day, the delivery having no clock time | The delivery wins | ✅ |
| Carrier event has a date but no time | Parses to midnight; comment carries the date alone; delivery date still written | ✅ |
| Carrier event has no time at all | Comment stamped with the run time, marked (scrape time) |
✅ |
| Carrier's page changes shape | Detected as a parse failure, raised as [ERROR] + [ALERT], NetSuite kept |
✅ |
| Carrier returns 429 or 5xx | Backed off and retried where the driver declares retries; otherwise preserved as a carrier error | ✅ |
| Invocation times out mid-carrier | Finished work written, remainder deferred and reported | ✅ |
| One carrier throws outright | Other carriers complete and write back | ✅ |
| NetSuite write fails for one batch | Counted as failed, other batches and carriers continue | ✅ |
| Supplier-created consignment under the supplier's own carrier account | Carrier returns nothing; logged [NO-DATA]; nothing written |
✅ (by design) |
| Carrier introduces new event wording | [NO-MAP]; nothing written; needs a one-line mapping change |
✅ (detected, not self-healing) |
| NetSuite holds one of the nine statuses outside the recognised list, carrier unreadable | Nothing written — but reported as [NO-DATA]/[NO-MAP] rather than [PRESERVE], and not counted as preserved |
❌ — defect CT-2 |
| AusPost's API changes shape | Not caught by the per-run health check: AusPost has no check consignment set | ❌ — defect CT-3 |
| Carrier event times and NetSuite's note are compared across time zones | Depends on the Lambda's TZ, which is not in version control |
❌ — defect CT-4 |
| Consignment ages out of a carrier's system | Health check fails with a message telling you to pick a newer consignment | ✅ |
| Two carriers could match one ship method | First registered wins; there is no warning that it was ambiguous | ❌ — accepted, see limitations |
Failure modes
What breaks it, how that shows, and how to recover.
| Failure | Symptom | Detection | Recovery |
|---|---|---|---|
| Carrier rebuilds their tracking page | Rows stop updating; statuses go stale | [ERROR] … parse failure, [ALERT], and [CANARY-FAIL] for that carrier |
Engineering updates that carrier's parser. NetSuite values are intact meanwhile |
| Carrier introduces new event wording | One row at a time stops progressing | [NO-MAP], naming the unrecognised text |
Add the wording to the status map |
| Carrier withdraws API access | Every row for that carrier errors | [ERROR] per row, [CANARY-FAIL] |
Obtain a credential, or route via an aggregator. Currently live for Couriers Please — defect CT-1 |
| NetSuite credentials expire or the role loses Edit | Every write fails with 401 | NS RESTlet POST failed (401) in the run's write errors |
Regenerate the token; confirm the role can edit Item Fulfillment |
| Saved search changed or removed | Zero rows fetched, or the GET errors | [fetch] searchId=7756 → 0 rows |
Restore the search. Nothing alerts on a zero-row run — see below |
| Run exceeds its invocation time | Some rows never tracked | [DEFER] lines and a non-zero deferred count |
Next run picks them up. Persistently non-zero means the schedule or the reserve needs revisiting |
| Rate limit tripped at a carrier | 429s, then preserved rows | [retry] lines, then [PRESERVE] … carrier error |
Self-correcting. Lower that host's rate if it persists |
| Lambda itself errors | No run output at all | CloudWatch alarm → SNS topic, per the repository README | Unverified — the alarm and topic are not in version control (see limitations) |
Not detected automatically: a run that fetches zero rows, a carrier that disappears from the
saved search entirely, and a steady rise in [NO-DATA]. Each of these looks like a quiet, healthy
run. The per-carrier summary line is the only place they are visible, and nothing reads it.
Known limitations
Scope that was deliberately chosen. A limitation is a decision.
| Limitation | Why it is this way | What to do instead |
|---|---|---|
| Twice a day, never on demand | The carriers' public endpoints are rate-limited by us on purpose, and an on-demand path would be a second, differently-behaved way to write the same fields | Open the tracking link on the record for a live answer |
| Direct Freight is not covered | Their tracking sits behind Cloudflare bot protection; the options are a paid bypass (~$30/mo) or B2B API access | Track manually, or take up API access with the account manager |
| Deliveries on a supplier's own Australia Post account are not covered | AusPost's API returns only shipments registered to our account, and the public page sits behind bot protection | Exclude those ship methods in saved search 7756 so the rows never arrive; or share the supplier's account id; or use a paid aggregator |
| Xtreme Freight and COPE are not covered | Xtreme requires a portal login; COPE publishes no tracking at all | Track manually |
| Only five fields can ever be written | The RESTlet whitelists them and rejects anything else | Adding a sixth is a deliberate change in two places, by design |
| Ambiguous ship methods resolve silently | Registration order is the tie-break, and it is deterministic | Keep the name patterns mutually exclusive when adding a carrier |
| A carrier's new wording needs a code change | Guessing at unrecognised wording is exactly the confidently-wrong outcome this automation avoids | Read [NO-MAP] lines; each is one line of mapping |
| Deferred rows wait for the next scheduled run | There is no retry queue; the saved search is the queue, because written rows leave it | Nothing — this is the design. Persistent deferrals mean the schedule needs looking at |
| The RESTlet, the schedule, the alarm and the deployment scripts are not in this repository | The repository holds src/ and tools/ only. netsuite/LI_courier_status_update_rl.js, template.yaml, canary.mjs, local-run.mjs, build.sh and SETUP.md are all described in its README but none is tracked in git |
Treat anything in this entry about the schedule, the SNS alarm or the RESTlet's field whitelist as documented but unverified. Recorded as a gap on the Integration Map |
Known defects
Where it does something other than what was decided. A defect is a mistake.
| Ref | Status | Defect | Rule |
|---|---|---|---|
CT-1 |
BlockedOpen — blocked on Couriers Please | Couriers Please tracking is blocked. Their API has returned 403 "API access is only permitted from the frontend application" for every request since 2026-08-25. The response is identical with any User-Agent, Referer or Origin, so it is an access-control decision rather than bot detection, and their public page is a client-rendered app carrying no tracking data. The driver fails fast — one request per run, not one per consignment — and every Couriers Please row keeps its existing NetSuite status. The supported fix is an API credential via the CP account manager; an aggregator is the fallback. Their frontend's authentication must not be imitated. |
needs confirming |
CT-2 |
OpenOpen — reporting only, no data impact | The "recognised status" list covers eleven of the twenty. The check for "NetSuite already holds something sensible" tests against eleven labels. Nine real statuses are absent — among them Delivery attempt failed, Returned to seller, In Depot delay and the three Life Interiors depot statuses — so a row sitting on one of those, whose carrier cannot be read, is reported as [NO-DATA]/[NO-MAP] rather than [PRESERVE] and is not counted in the preserved total. Two of the eleven — In transit and Picked up — are not values on the NetSuite list at all (the list has In transit to courier depot and Received by courier), so they can never match. No data is lost either way: nothing is written in both paths. The damage is diagnostic — preserve counts understate, and genuinely stuck rows hide among the no-data traffic |
needs confirming |
CT-3 |
OpenOpen | AusPost has no per-run health check. Its check consignment is unset, so the run reports [CANARY-SKIP] for AusPost every time. AusPost is the highest-volume carrier and the only one behind a paid API, and a change in its response shape would not be caught by the mechanism built to catch exactly that. Needs a known long-delivered AusPost consignment recorded against the driver |
needs confirming |
CT-4 |
OpenOpen — unconfirmed, needs the deployed TZ |
Time zones are unproven. Carrier times arrive in two families: absolute (AusPost, Air Road) and bare wall-clock (the rest). Bare times are read as the runtime's local time and every timestamp is formatted in it. If the Lambda runs on UTC, absolute events land ten hours behind their AEST wall-clock reading, which would make AusPost and Air Road rows look older than NetSuite's note and preserve them spuriously (LI-BL-FUL-023), and could write an actual delivery date a day early for an evening delivery. This has not been reproduced — the Lambda's TZ setting is not in version control, so whether it is UTC or Australia/Sydney cannot be established from the repository. Confirm the setting before treating this as live |
needs confirming |
CT-5 |
BlockedOpen — blocked behind CT-1 |
The Couriers Please health check expects a failure state. Its consignment is recorded as expecting Exception, described in the code as "current state — update when you have a delivered CN". A check pinned to a state that can still change cannot distinguish our fault from the freight's — which is the one thing these checks exist to do |
needs confirming |
CT-6 |
OpenOpen — documentation only | The repository README is behind the code. It documents six carriers; seven are registered, Aramex/Fastway having been added without a README change. Anyone sizing the work from the README alone will under-count the carriers | needs confirming |
No Asana tickets are recorded against any of these. The references above are local to this entry.
Open questions
What could not be established. Recorded rather than guessed.
| Ref | Status | Question | Who can answer |
|---|---|---|---|
CT-Q1 |
Open | Saved search 7756's filters have not been read. Everything this entry says about scope comes from how the automation treats the rows it receives, not from the search's own definition. What makes a fulfilment appear — and whether supplier-account ship methods are already excluded — is unconfirmed. | needs confirming |
CT-Q2 |
Open | The RESTlet's field whitelist is unverified. The five-field limit is documented in the repository README; the script itself is not in version control. | needs confirming |
CT-Q3 |
Open | The schedule is unverified. Twice daily at 06:00 and 18:00 AEST is the README's account of an EventBridge rule that is not in version control. | needs confirming |
CT-Q4 |
Open | Nothing here has been checked against production. This entry is draft for that reason. |
needs confirming |
CT-Q5 |
Open | The old AusPost Lambda's status is unknown. The README describes a cutover from ap-tracking-status-update that leaves both running for a period. Whether that cutover completed — and so whether two automations are writing these fields — has not been established. |
needs confirming |
Source references (read-only)
The code and searches this reading was written from.
lifeinteriors/courier-tracking-status/src/index.mjs@d81a450— handler, environment, health-check switch (verified 2026-08-31)lifeinteriors/courier-tracking-status/src/core/orchestrator.mjs@d81a450— scope, grouping, concurrency, time budget, logging (LI-BL-FUL-018,LI-BL-FUL-025)lifeinteriors/courier-tracking-status/src/core/changeDetector.mjs@d81a450— the preserve rules and the write payload (LI-BL-FUL-022,LI-BL-FUL-023,LI-BL-FUL-024)lifeinteriors/courier-tracking-status/src/core/statusMap.mjs@d81a450— the twenty-value list and every mapping table (LI-BL-FUL-021)lifeinteriors/courier-tracking-status/src/core/timeUtils.mjs@d81a450— event-time parsing and current-event selection (LI-BL-FUL-020)lifeinteriors/courier-tracking-status/src/core/canaryCheck.mjs@d81a450— the per-run health check (LI-BL-FUL-026)lifeinteriors/courier-tracking-status/src/core/netsuiteClient.mjs@d81a450— signed RESTlet GET and batched POSTlifeinteriors/courier-tracking-status/src/drivers/index.mjs@d81a450— ship-method matching (LI-BL-FUL-019)lifeinteriors/courier-tracking-status/src/drivers/_shared.mjs@d81a450— rate limits, backoff, shared portal parsinglifeinteriors/courier-tracking-status/src/drivers/*.mjs@d81a450— the seven carrierslifeinteriors/courier-tracking-status/tools/dt-check.mjs@d81a450— Designer Transport diagnostic, sharing the driver's own parsers
Change history
What moved on this page, and when.
| Date | Change | Change request |
|---|---|---|
| 2026-09-28 | Developer sections reordered to the registry's skeleton (CONVENTIONS §15): Inputs · Processing · Decision tree · Outputs · Worked examples · Test cases · UAT · Edge cases · Failure modes · Known limitations · Known defects · Open questions · Source references · Change history. The entries had drifted into two house styles — four put the proof after the outputs, four put the problems there — so every shared heading sat at a different position depending on which entry you opened; Test cases alone appeared at five different ones. Every section moved whole and byte-identical: nothing inside any of them was touched, and no wording changed. The one-line description the site now prints under each heading is generated from build.mjs, not written here, so it reads the same on every entry. npm run check warns on a wrong order and on a missing section. No logic change, and last_reviewed is unchanged. |
— |
| 2026-09-28 | 22 test cases moved from the body table into the test_cases: front-matter register and restated in the shape a test pack uses — Case ID · Layer · Preconditions · Input · Expected output · Rule ref. Ids become CT-TC-<case>, which keeps every case id the table already used while making them unique across the registry. The build renders them back under ### Test cases, and they now also appear on the generated Test cases page, so a pack spanning two entries no longer has to be retyped. Every case names both its rule and its layer. No expected value was changed. No behaviour was re-documented and last_reviewed is unchanged. |
— |
| 2026-09-28 | Defects and open questions moved from body tables into the defects: and open_questions: front-matter registers, namespaced CT-: 6 defects (6 open) and 5 open questions (5 still open). The build renders them back under the same headings, so the page reads as it did; the prose around those tables is unchanged. They now also appear on the generated DEFECTS and Open questions pages, which is the point — the same list was unreadable spread across nine entries in five different column shapes. Every existing ref is preserved. No behaviour was re-documented, no defect status was reinterpreted, and last_reviewed is unchanged. |
— |
| 2026-08-31 | Placed on the order journey: journey_stage: delivery (Delivery), the stage confirmed with the business. The Organisation Map now groups and orders entries by journey stage rather than by folder. depends_on: [LI-BL-FUL-003] records what must already have run — a precondition, which is a weaker claim than runs_after and is drawn differently on the map. Front-matter only — no logic change, and last_reviewed is unchanged. |
— |
| 2026-08-31 | Added the courier coverage table to the business reading. The couriers were previously named only in prose inside LI-BL-FUL-019, and the business reading never said which couriers are not covered or that Couriers Please has not updated since 2026-08-25 — both facts existed only in the developer reading, where the people who need them do not look. A reader with zero system access could not tell a deliberately-unsupported courier from a broken one. Presentation only: no rule changed, and last_reviewed is unchanged. |
— |
| 2026-08-31 | Initial documentation of existing behaviour, from the code at d81a450. Nine rules recorded, LI-BL-FUL-018 to LI-BL-FUL-026. last_reviewed records the date the source was read, not a production verification — none has taken place, which is why status is draft. Six defects recorded: the Couriers Please block (CT-1) is the only one already known outside this entry; CT-2 to CT-6 were found while reading the code and none has an Asana ticket. CT-4 is recorded as unconfirmed on purpose — it cannot be reproduced from the repository, because the deployed time-zone setting is not in it. Seven carriers documented against the repository README's six: Aramex was added in code without a README change (CT-6), and production wins. Five glossary terms added, all unconfirmed. |
— |
Field registry — ids
Inputs
Outputs
| Field id | Field name | Record | Source | Type | Read / written | Used by |
|---|---|---|---|---|---|---|
custbody_delivery_status |
needs confirming | Item Fulfillment | Saved search 7756 | select | read-written | Turning the carrier's words into our statusLI-BL-FUL-021 Never overwrite something good with nothingLI-BL-FUL-022 Never move a delivery backwardsLI-BL-FUL-023 What actually gets writtenLI-BL-FUL-024 |
custbody_courier_tracking_comment |
needs confirming | Item Fulfillment | Saved search 7756 | text | read-written | Never overwrite something good with nothingLI-BL-FUL-022 Never move a delivery backwardsLI-BL-FUL-023 What actually gets writtenLI-BL-FUL-024 |
custbody_actual_delivery_date |
needs confirming | Item Fulfillment | Carrier tracking event | date | written | What actually gets writtenLI-BL-FUL-024 |
custbody_courier_carton_received |
needs confirming | Item Fulfillment | Carrier tracking response | integer | written | What actually gets writtenLI-BL-FUL-024 |
custbody_tracking_link |
needs confirming | Item Fulfillment | Built from the carrier and the consignment number | text | written | What actually gets writtenLI-BL-FUL-024 |
Hover any box and its explanation appears beside it. Click to pin it here.