Shipping Automation β€” Module 5: Courier Approval & Set Ship Date

System: Shipping Automation (Module 5 / Asana "Part 2") Source repositories (read-only): lifeinteriors/shipping-v6-nswo, lifeinteriors/shipping-v6-swo Source of truth for intent: Miro board "Module 2 V6.1 β€” Courier Approval & Set Ship Date" Interactive documentation: https://claude.ai/code/artifact/56822bef-4dc2-4a69-b87b-bc913a87004b Status: Documented from code + Miro board + live saved-search data + fulfilment schedule sheet.

This entry captures the business logic of the courier-approval automation. Per the access policy, the logic is recorded here, not changed in the source repositories.


1. Summary

When a customer's order is paid and ready, the automation decides which courier should carry it and what date it ships, writes the approval back to NetSuite, and only sets aside the orders that don't fit the rules for a human to review.

It is delivered as two AWS Lambda functions with near-identical architecture, split by order type:

Repository Order type Ship Complete Service levels Order handling
shipping-v6-nswo Non-Ship-Whole-Order No Standard, Two Man Each order processed independently
shipping-v6-swo Ship-Whole-Order Yes Two Man, Full Service Related orders merged and shipped together

The two flows run as one system: Flow 1 (nswo) runs first, then Flow 2 (swo). An order can be promoted from Flow 1 to Flow 2.


2. Business logic (plain language)

Written for the fulfilment team.

In one line: the system looks at orders that are ready, works out the best courier and delivery day for each, and books it in automatically β€” asking a person to step in only when an order doesn't fit the rules.

The two piles of orders

  • Send as it's ready β€” parts of the order can go out as stock lands (Ship Complete = No).
  • Send all together β€” the customer's whole order must arrive in one go (Ship Complete = Yes).

An order can move from the first pile to the second if it's meant to travel with another order. The "send all together" pile is always handled second.

When an order is "ready" (all must be true)

  • Paid in full (nothing owing)
  • Not on hold
  • Not already booked with a courier
  • Not already approved or closed
  • No drop-ship items
  • Items are Standard or 2-Man goods
  • No ship date already set in the past
  • At least one item still to send

A "send all together" order also waits until every item across the linked orders is in stock.

Picking the courier

The paid service level (Standard / 2-Man / Full Service) sets the list of couriers to try, in preferred order. For each, the system checks whether it covers the delivery postcode; the first that covers it wins.

  • If none cover it β†’ use the courier the customer picked at checkout, unless that was "To Be Confirmed", in which case hold the order for review.
  • If a Standard order contains an item needing 2-Man handling β†’ upgrade the whole order to 2-Man.

Picking the day

Earliest sensible day, respecting the courier's notice period, the customer's requested date, and (for Life trucks) whether the truck for that region still has room that day.

  • NSW / ACT (local): ship and deliver on the same day.
  • QLD / VIC (interstate): goods travel by linehaul, so the ship date is set to the weekly linehaul day β€” Tuesday for Brisbane, Thursday for Melbourne.
  • Other states on a Life truck: not supported β€” left for review.

Outcomes

Approved Β· Approved (upgraded) Β· Approved (customer's online pick) Β· Held for review Β· Waiting (missing stock β€” retries next run).


3. Entry gate & flow

flowchart TD
  ST([Order is ready]) --> SC{Ship Complete?}
  SC -->|No| F1["Flow 1 Β· nswo (search 7132)"]
  SC -->|Yes| F2["Flow 2 Β· swo (search 7136)"]
  F1 --> STG{Ships together<br/>with another order?}
  STG -->|Yes| FLIP["Set Ship Complete = Yes<br/>β†’ picked up by Flow 2"]
  STG -->|No| CSL1[Courier Service Level]
  F2 --> AIC{All items committed?}
  AIC -->|No| IG[Skip - wait for next run]
  AIC -->|Yes| CSL2[Courier Service Level]
  FLIP -. next run .-> F2

Saved-search entry criteria ("Sales Orders NSSS")

An order only enters when all hold:

  • Sales order balance owing = 0
  • At least 1 item not shipped
  • "Exported to Courier" = false
  • "Hold Order" = false
  • "Courier Approval" = false
  • Orders / items not closed
  • No item has a Drop Ship PO
  • Item default shipping method is 2 Man Sensitive or Standard
  • Ship Date not on/before today OR Ship Date empty & "Reason for service change" empty

4. Courier routing by service level

First courier whose postcode check passes wins (each courier has a whitelist CSV in S3).

Service level Flow(s) Hierarchy (first valid postcode wins)
Full Service swo Life Full β†’ DT Full. TLC auto-approved (no postcode check).
Two Man nswo, swo Life 2 Man β†’ DT 2 Man β†’ Pedemont β†’ TT (TT has no postcode file β†’ always fails).
Standard β€” Life items nswo, swo Life Standard β†’ DT ATL.
Standard β€” AusPost nswo All-AusPost β†’ AusPost.
Standard β€” other nswo All / at-least-one Standard β†’ Allied.
Standard w/ a 2-Man item nswo, swo Upgrade to the Two Man hierarchy; reason code 21.

Postcode check & front-end rate-card fallback

flowchart TD
  C[Next courier in hierarchy] --> P{Postcode valid?}
  P -->|Valid| AP["Approve Β· reason 15"]
  P -->|Invalid| MORE{More couriers to try?}
  MORE -->|Yes| C
  MORE -->|No| FE{Front-end pick is TBC?}
  FE -->|Yes| DIS["Disapprove Β· reason 5 (board shows 10)"]
  FE -->|No| RC["Approve customer's rate-card pick Β· reason 14"]

5. Ship date, CBM capacity & linehaul

Base date = SO committed date / today, or the CX requested date when it is later than the SO committed date or the PO dispatch date (SO_cxRequestedDate in nswo; maxCxRequestedDate across the group in swo).

flowchart TD
  DC[Date Check base date] --> LIFE{Is Life courier?}
  LIFE -->|No| SHIP[Add Ship Date only]
  LIFE -->|Yes| STATE{Destination state}
  STATE -->|NSW / ACT| NSW["Ship Date = Scheduled Delivery Date (same day)"]
  STATE -->|QLD| QLD["Delivery date + Ship = Tuesday before + Linehaul SYD to BRIS"]
  STATE -->|VIC| VIC["Delivery date + Ship = Thursday before + Linehaul SYD to MEL"]
  • Buffer: non-Life count business days; Life trucks may count weekend days when marked available in the schedule sheet.
  • CBM: for Life trucks, capacity is checked against the schedule sheet per region (limited to the first few candidate dates); capacity is reserved on selection. Ordering resolved by which SO was placed first.
  • Schedule/CBM loop: schedule check β†’ if no available date, ignore order + error email; else CBM check β†’ fits β‡’ approve, else find next available date and repeat.
  • Life deliveries to states other than NSW/VIC/QLD are rejected (no partial update).

The above is the current behaviour. A revised date model is planned β€” see below.

Planned change β€” ship date, SDD & linehaul

Status: planned β€” not yet implemented in shipping-v6-nswo / shipping-v6-swo. Documented here ahead of the code change; until it ships, the current logic above applies.

The revised model computes the ship date first and derives the delivery date forward, and it removes the fixed weekly linehaul weekday (Tue/Thu) anchoring.

Courier Ship date Scheduled Delivery Date (SDD) Line Haul Date
non-LIFE today + 1 + buffer none β€”
LIFE Β· NSW/ACT today + 1 + buffer = Ship date (same day) β€”
LIFE Β· VIC/QLD today + 1 + buffer = Ship date + 1 + buffer = Ship date
flowchart TD
  B["today + 1 + buffer"]:::state --> LIFE{"Is Life courier?"}:::dec
  LIFE -->|"No"| SH1(["Ship Date β€” no SDD"]):::ok
  LIFE -->|"Yes Β· NSW/ACT"| SH2(["Ship Date = SDD"]):::ok
  LIFE -->|"Yes Β· VIC/QLD"| SH3(["Ship Date = Line Haul Date"]):::ok --> SDD3(["SDD = Ship Date + 1 + buffer"]):::ok
  classDef state fill:#e5e7eb,stroke:#c3cdd8,color:#1f2937
  classDef ok fill:#33a866,stroke:#268a52,color:#fff
  classDef dec fill:#f0932b,stroke:#c96a1f,color:#fff

What changes vs the current (OLD) logic:

Current (OLD) Planned (NEW)
Primary date computed SDD first (today + 1 + buffer) Ship date first (today + 1 + buffer)
LIFE Β· NSW/ACT Ship date = SDD SDD = Ship date (same result)
LIFE · VIC/QLD ship date pulled back to the weekly linehaul weekday — QLD → Tuesday (SYD→BRIS), VIC → Thursday (SYD→MEL) no weekday anchoring — ship date = today + 1 + buffer = Line Haul Date
LIFE Β· VIC/QLD delivery = the computed base date (SDD) pushed forward: SDD = Ship date + 1 + buffer

When this ships, Β§11 (Workflow map, diagram 4) and the linehaul test cases in Β§12 (group F) should be updated to match.


6. Approval reason codes

Board Meaning Config key Live value
15 Approved β€” Reviewed by the automation N_RFSC_APPR 15
21 Approved β€” Automation upgraded due to premium courier item N_RFSC_APPR_PREMITEM 21
14 Approved β€” Reselected via Front End Rate Card N_RFSC_APPR_RESLECTED 14
10 Disapproved β€” Postcode not covered N_RFSC_DIS_PCODENOTCOV 5

⚠️ Board vs code mismatch: the disapprove note is labelled (10) on the Miro board, but every deployed config sets N_RFSC_DIS_PCODENOTCOV = 5. Treat 5 as the live value; confirm in NetSuite.


7. Courier schedule sheet (fulfilment-owned)

A Google Sheet (single tab INTEGRATION SCHEUDULE) maintained by the fulfilment team. The Courier Schedule microservice reads it; the automation only reads it.

Layout: columns A–H are per-courier/truck settings; every following column is one calendar day with a TRUE/FALSE availability flag.

Column Meaning
COURIER Australia Post, Pedemonts, Allied, Designer Transport, Amaze, TLC, Life
REGIONS Life only β€” e.g. Syd R1 - East, Melb R4 - North East, Bris R3 - East
Netsuite Internal ID Ship-via IDs this row covers (Life rows: 201100 = 2 Man, 253939 = Full Service, 247628… = Standard)
Detrack Truck# Physical truck (Syd 01/02/03, Melb 01/02, Bris 01); a region can have several
TRUCK CBM CAPACITY N/A for non-Life; 5–16 mΒ³ for Life trucks
MAX DELIVERY STOPS Life only β€” 3–18
BUFFER DATE Lead days: non-Life = 1; Life = 2–3 (Sydney) or 7 (Melbourne/Brisbane)
Note ship date (non-Life) or Scheduled Delivery Date (Life)

Two dedicated linehaul rows mark departure days (toggleable per week):

  • LINEHAUL - VIC β€” every Thursday (SYD β†’ MEL)
  • LINEHAUL - QLD β€” every Tuesday (SYD β†’ BRIS)

Rules baked into the sheet:

  • Life availability sets the scheduled delivery date; other couriers set the ship date.
  • Buffers exclude weekends unless the weekend day is selected (e.g. order Thursday, buffer 3 β†’ Fri + Mon + Tue; if Saturday available β†’ Fri + Sat + Mon).
  • Tiebreak: prefer the lower-CBM truck β€” "all loaded first".
  • One Life row's three NetSuite IDs serve Standard, 2 Man, and Full Service ship-vias at once.

8. Input data β€” saved-search source

Each flow is fed by a NetSuite saved search returning one row per order line (rows sharing SO_internalID belong to the same order).

Saved search ID Feeds Distinctive column
Courier Approval Β· Ship Complete No 7132 nswo β€”
Courier Approval Β· Ship Complete Yes 7136 swo adds SO_shipTogetherWithID

SO_shipTogetherWithID (7136 only) is the internal ID of another SO this order must ship with β€” the field mergeSalesOrders follows to build ship-together groups. Search 7132 omits it, which is why nswo has no merge step.

Key columns: SO_postcode/SO_state (postcode + linehaul), SO_shipVia/SO_shipViaId (current courier), SO_lifeRegion/SO_lifeTruckNumber (scheduling/CBM), SO_cxRequestedDate, SO_courierServiceLevel (paid level), item_defaultShippingMethod (item mix), item_packageCBM (Γ— committed qty = CBM), item_qtyCommitted (drives the "all committed?" gate).

Ship-via reference (from live data): 3420 = TBC Β· 201100 = Life 2 Man Β· 247628 = Life Standard Β· 253939 = Life Full Service Β· 112729 = TLC Β· 112728 = Pedemont Β· 17186 = AusPost Β· 39973 = Allied Β· 2864 = Designer Transport.

Customer requested delivery date

Field: SO_cxRequestedDate β€” the date the customer asked to receive the order by. Present in both saved searches, format DD/MM/YYYY, and may be blank. For a merged swo group the automation uses maxCxRequestedDate β€” the latest requested date across all linked orders β€” so the whole group lands on or after the latest customer ask.

How it's used: it sets a floor on the scheduled date. The Date Check base date is the SO committed date / today, or the CX requested date when that is later than the committed / PO dispatch date. The chosen ship/delivery date is then the first valid courier day on or after that floor. Blank = no customer preference β†’ the courier buffer governs instead.

Example CX requested date What happens
S315316 (Life 2 Man, Syd R5) 30/10/2026 The 3-day buffer would allow early August, but the CX date is later, so the date becomes the first Syd R5 truck day on/after 30 Oct 2026.
S308550 (Life Full, Syd R4) (blank) No customer floor β†’ the 3-day buffer governs β†’ first Syd R4 truck day (Fri 7 Aug 2026 in the sample data).
S310492 + S310562 (group) maxCxRequestedDate = 5/7/2026 Latest ask across the group is in the past, so the buffer governs the whole group.

Ship together with this order

Field: SO_shipTogetherWithID β€” the internal ID of another sales order this order must be delivered with. Present only in the 7136 (swo) search; blank means the order ships on its own.

It drives two mechanisms:

  1. Flow 1 (nswo): an order flagged to ship together with another SO has its Ship Complete set to Yes, so it drops out of Flow 1 and is picked up by Flow 2 (swo) on the next run. (nswo's search has no SO_shipTogetherWithID column β€” this is the "Ships together with another SO?" gate.)
  2. Flow 2 (swo): mergeSalesOrders follows SO_shipTogetherWithID to build a group, which:
    • ships together β€” updateGroupedSalesOrders writes the same courier + dates to every linked SO;
    • only proceeds when every item across all linked SOs is committed (allSOsCommitted) β€” otherwise the whole group waits;
    • uses group totals β€” total CBM across all items, and maxCxRequestedDate;
    • pulls in a linked SO that isn't in the search results via SuiteQL (ns.getSalesOrder).
flowchart TD
  subgraph N["Flow 1 Β· nswo"]
    O1["Order flagged to ship<br/>together with another SO"]:::state --> Q{"Linked to<br/>another SO?"}:::dec
    Q -->|"Yes"| P["Set Ship Complete = Yes<br/>β†’ picked up by Flow 2"]:::branch
    Q -->|"No"| C1["Process on its own"]:::ok
  end
  subgraph S["Flow 2 Β· swo"]
    G["mergeSalesOrders follows<br/>SO_shipTogetherWithID"]:::state --> AC{"All items across<br/>the group committed?"}:::dec
    AC -->|"No"| W["Group waits<br/>(nothing booked)"]:::note
    AC -->|"Yes"| U["updateGroupedSalesOrders β€”<br/>same courier + dates to every linked SO"]:::ok
  end
  classDef state fill:#e5e7eb,stroke:#c3cdd8,color:#1f2937
  classDef dec fill:#f0932b,stroke:#c96a1f,color:#fff
  classDef branch fill:#fdf3c4,stroke:#e6d270,color:#5b4d00
  classDef ok fill:#33a866,stroke:#268a52,color:#fff
  classDef note fill:#9aa5b1,stroke:#7c8894,color:#fff

Example (reciprocal group): a Float modular sofa split across two orders that reference each other:

SO internalID shipTogetherWith Item Committed
S301287 13815215 14132070 Float 1.5 Seat Armless Piece 1 / 1
S301287 13815215 14132070 Float 4 Piece Modular Sofa 1 / 1
S306157 14132070 13815215 Float 3 Seat Armless Piece 0 / 1

They merge into one group (VIC 3754, Full Service). Because the third piece is 0/1, allSOsCommitted is false β†’ the group waits, nothing is booked, and it retries next run once the piece is committed.

Data quirks:

  • Self-reference: some rows set SO_shipTogetherWithID to their own internal ID (e.g. S311157, S312794) β€” a group of one; the merge logic tolerates it.
  • Chains: links can chain (A↔B, B↔C) β€” the group is the whole connected set of orders.

9. Worked examples

Real orders traced end-to-end (dates computed from a 31 Jul 2026 run against the schedule sheet).

A Β· Local 2-Man (nswo) β€” S315316

NSW 2112, Syd R5 - North West, paid 2 Man, 2 items (Standard-Life), CBM 4.8 mΒ³, CX date 30/10/2026. β†’ Life 2 Man; CBM 4.8 vs 6 mΒ³ truck (tight fit); CX date 30/10 overrides the 3-day buffer; NSW = local so ship = delivery on the first Syd R5 truck day on/after 30 Oct 2026. Approved (15).

B Β· Local Full Service (swo) β€” S308550

NSW 2040, Syd R4 - West, paid Full Service, 1 item, CBM 1.39 mΒ³, no CX date. β†’ Life Full; CBM fits 16 mΒ³; 3-day buffer β†’ ship & deliver Fri 7 Aug 2026. Approved (15).

C Β· Interstate QLD linehaul (swo, merged) β€” S310492 + S310562

QLD 4173, Bris R3 - East, Full Service, both orders committed, group CBM 0.74 mΒ³. β†’ Life Full; fits 13 mΒ³ truck; latest CX date (5/7/2026) is past so 7-day buffer applies; first Bris R3 truck day = deliver Tue 18 Aug 2026; QLD linehaul β†’ ship Tue 11 Aug 2026 (SYD β†’ BRIS), applied to both linked orders. Approved (15).

D Β· Merged group β€” waiting (swo) β€” S301287 + S306157

VIC 3754, Full Service, a Float modular sofa split across two linked orders; one piece 0/1 in stock. β†’ allSOsCommitted = false β†’ whole group skipped this run, retried next run when the last piece is committed. Waiting β€” nothing booked.


10. Known divergences & things to watch

  • Disapprove reason: board shows (10); config sets N_RFSC_DIS_PCODENOTCOV = 5. Confirm in NetSuite.
  • TT (Two Man): no postcode CSV β†’ always fails its postcode check in practice.
  • All-AusPost 2-Man orders: routed to manual review, not auto-processed.
  • Standard β†’ 2-Man upgrade: a margin loss (customer paid Standard); the front-end rate card is the noted pain point.
  • Board is titled "Module 2" while the repos' package.json call this "Module 5" β€” same stage, different naming.
  • nswo config keys: queryHelper/rds reference config.AWS_RDS_DBNAME and several libs read config.AWS_REGION, but neither key is defined in nswo config (resolve to undefined). swo defines AWS_REGION and sources the DB name from Secrets Manager.

11. Workflow map

The whole courier-approval flow as a colour-coded workflow, styled after the Module 2 V6.1 board. β‘ β‘‘β‘’ mark reusable sub-flows referenced across the map.

Legend: 🟨 start Β· 🟧 process / decision hub Β· ⬜ courier / state Β· 🟦 postcode check / ship-via Β· 🟩 approve / set date Β· ⬛ ignore Β· β‘ β‘‘β‘’ sub-flow

1 Β· Dispatch β€” which flow an order enters

flowchart TD
  START(["START"]):::start --> SC(("Ship Complete?")):::hub
  SC -->|"No β€” Flow 1 Β· nswo"| S2["Saved search 7132"]:::branch
  SC -->|"Yes β€” Flow 2 Β· swo"| S1["Saved search 7136"]:::branch
  S2 --> STG(("Ships together<br/>with another SO?")):::hub
  STG -->|"Yes"| FLIP["Set Ship Complete = Yes<br/>β†’ Flow 2 next run"]:::branch
  STG -->|"No"| CSLn(("Courier<br/>Service Level")):::hub
  S1 --> AIC(("All items<br/>committed?")):::hub
  AIC -->|"No"| IG["Ignore"]:::ignore
  AIC -->|"Yes"| CSLs(("Courier<br/>Service Level")):::hub
  classDef start fill:#f6d64a,stroke:#caa72a,color:#3a2f00
  classDef hub fill:#ef8a3c,stroke:#c96a1f,color:#fff
  classDef branch fill:#fdf3c4,stroke:#e6d270,color:#5b4d00
  classDef ignore fill:#1f2937,stroke:#111827,color:#fff

2 Β· Courier hierarchy & approval (Full Service Β· β‘  Two Man)

flowchart TD
  subgraph FS["Full Service β€” swo"]
    direction LR
    LFS(["Life Full Service"]):::state --> PFS["Postcode Check"]:::pc
    PFS -->|"Valid"| AFS(["Approve Β· 15"]):::ok
    PFS -->|"Invalid"| DFS(["DT Full Service"]):::state --> PDF["Postcode Check"]:::pc
    PDF -->|"Valid"| ADF(["Approve Β· 15"]):::ok
    TLC(["TLC β€” auto-approved"]):::state --> ATL(["Approve Β· 15"]):::ok
  end
  subgraph TM["β‘  Two Man β€” nswo + swo"]
    direction LR
    L2(["Life 2 Man"]):::state --> P2["Postcode Check"]:::pc
    P2 -->|"Valid"| A2(["Approve Β· 15"]):::ok
    P2 -->|"Invalid"| D2(["DT 2 Man"]):::state --> PD2["Postcode Check"]:::pc
    PD2 -->|"Valid"| A2
    PD2 -->|"Invalid"| PE(["Pedemont"]):::state --> PPE["Postcode Check"]:::pc
    PPE -->|"Valid"| A2
    PPE -->|"Invalid"| TT(["TT β€” no postcode file, fails"]):::state --> PTT["Postcode Check"]:::pc
    PTT -->|"Valid"| A2
    PTT -->|"Invalid"| FE{"FE courier<br/>= TBC?"}:::dec
    FE -->|"Yes"| DIS(["Disapprove Β· 5"]):::note
    FE -->|"No"| RC(["Approve β€” Rate Card Β· 14"]):::ok
  end
  classDef state fill:#e5e7eb,stroke:#c3cdd8,color:#1f2937
  classDef pc fill:#5b8def,stroke:#3a6fd0,color:#fff
  classDef ok fill:#33a866,stroke:#268a52,color:#fff
  classDef note fill:#9aa5b1,stroke:#7c8894,color:#fff
  classDef dec fill:#f0932b,stroke:#c96a1f,color:#fff

3 Β· Standard item routing (nswo)

flowchart TD
  CIL(("Check ITEM<br/>Logistic Service")):::hub
  CIL --> B1["1+ item is 2 Man"]:::branch --> J1(("β‘  jump to<br/>2-Man logic")):::hub --> N21(["Approve Β· 21<br/>premium upgrade"]):::ok
  CIL --> B2["β‰₯1 item Standard_Life"]:::branch --> PL["Postcode Β· Life Std"]:::pc
  PL -->|"Valid"| SVL["Ship Via β†’ Life Std"]:::pc --> AL(["Approve Β· 15"]):::ok
  PL -->|"Invalid"| PDA["Postcode Β· DT ATL"]:::pc
  PDA -->|"Valid"| SVD["Ship Via β†’ DT ATL"]:::pc --> AD(["Approve Β· 15"]):::ok
  PDA -->|"Invalid"| FE2{"FE = TBC?"}:::dec
  FE2 -->|"Yes"| DIS2(["Disapprove Β· 5"]):::note
  FE2 -->|"No"| RC2(["Approve β€” Rate Card Β· 14"]):::ok
  CIL --> B3["All Standard_Auspost"]:::branch --> AAP["Ship Via β†’ AusPost"]:::pc --> OKA(["Approve Β· 15"]):::ok
  CIL --> B4["All / β‰₯1 Standard"]:::branch --> AAL["Ship Via β†’ Allied"]:::pc --> OKL(["Approve Β· 15"]):::ok
  classDef hub fill:#ef8a3c,stroke:#c96a1f,color:#fff
  classDef branch fill:#fdf3c4,stroke:#e6d270,color:#5b4d00
  classDef pc fill:#5b8def,stroke:#3a6fd0,color:#fff
  classDef ok fill:#33a866,stroke:#268a52,color:#fff
  classDef note fill:#9aa5b1,stroke:#7c8894,color:#fff
  classDef dec fill:#f0932b,stroke:#c96a1f,color:#fff

4 Β· β‘‘ Set the ship date β€” CBM & linehaul

flowchart TD
  DC["Date Check<br/>SO committed / today / CX requested"]:::state --> LIFE{"Is Life courier?"}:::dec
  LIFE -->|"No"| G3a(("β‘’ Schedule<br/>+ CBM")):::hub --> SH(["Add Ship Date"]):::ok
  LIFE -->|"Yes"| NSW(["NSW + ACT"]):::state --> G3b(("β‘’")):::hub --> SHb(["Add Ship Date"]):::ok --> DVb(["Add Scheduled<br/>Delivery Date"]):::ok
  LIFE -->|"Yes"| QLD(["QLD"]):::state --> G3c(("β‘’")):::hub --> DVc(["Add Scheduled<br/>Delivery Date"]):::ok --> SHc(["Add Ship Date<br/>Tue before"]):::ok --> LHc(["Add Line Haul Date"]):::ok --> RTc(["Line Haul Route<br/>SYD β†’ BRIS"]):::ok
  LIFE -->|"Yes"| VIC(["VIC"]):::state --> G3d(("β‘’")):::hub --> DVd(["Add Scheduled<br/>Delivery Date"]):::ok --> SHd(["Add Ship Date<br/>Thu before"]):::ok --> LHd(["Add Line Haul Date"]):::ok --> RTd(["Line Haul Route<br/>SYD β†’ MEL"]):::ok
  classDef state fill:#e5e7eb,stroke:#c3cdd8,color:#1f2937
  classDef hub fill:#ef8a3c,stroke:#c96a1f,color:#fff
  classDef ok fill:#33a866,stroke:#268a52,color:#fff
  classDef dec fill:#f0932b,stroke:#c96a1f,color:#fff

5 Β· β‘’ Schedule & CBM sub-flow (Life trucks)

flowchart LR
  SCH["Schedule Check<br/>Google Sheet"]:::state --> AV{"Available<br/>date?"}:::dec
  AV -->|"No"| ERR(["Ignore + Error email"]):::ignore
  AV -->|"Yes"| CBM["CBM Check<br/>Google Sheet"]:::state --> FIT{"Fit CBM?"}:::dec
  FIT -->|"Yes"| OKK(["Approve"]):::ok
  FIT -->|"No"| NX["Find next<br/>available date"]:::state --> SCH
  classDef state fill:#e5e7eb,stroke:#c3cdd8,color:#1f2937
  classDef ok fill:#33a866,stroke:#268a52,color:#fff
  classDef dec fill:#f0932b,stroke:#c96a1f,color:#fff
  classDef ignore fill:#1f2937,stroke:#111827,color:#fff

12. Test cases & UAT

Acceptance tests β€” scenario, input, and expected outcome for each rule. The Result column doubles as a sign-off sheet. Reason codes: 15 approved Β· 21 upgraded Β· 14 rate-card Β· 5 disapproved (postcode).

How to run UAT (staging)

  • Environment: run on staging β€” AWS_LAMBDA_FUNCTION_VERSION=$LATEST selects the staging config (sandbox NetSuite, staging courier API).
  • Dry-run first: swo exposes N_QUEUER_DRY_RUN, NS_SAVED_SEARCH_DRY_RUN, COURIER_SCHEDULE_API_DRY_RUN; nswo has AWS_RDS_IN_DRY_RUN + a courier dry-run. Trace decisions without writing to NetSuite.
  • Seed: create a sandbox sales order that meets the entry criteria (paid, not on hold, not already approved, correct item shipping methods).
  • Trigger: invoke the Lambda (manual kickoff / test.index.mjs).
  • Verify in NetSuite: Courier Approval set, Ship Via updated, RFSC reason code, Ship Date + Scheduled Delivery Date (and linehaul fields for QLD/VIC). Check the logs; confirm no error email unless the case expects one.

A Β· Entry & eligibility (saved-search gating)

ID Scenario / input Expected outcome Result
E1 SO balance owing > 0 Not selected by the saved search β€” never processed ☐
E2 "Hold Order" = true Excluded β€” not processed ☐
E3 "Courier Approval" already true, or order/items closed Excluded β€” not reprocessed ☐
E4 All items have a Drop Ship PO Excluded (drop-ship items dropped; empty order skipped) ☐
E5 Ship Date on/before today with a reason for service change present Excluded β€” not re-dated ☐

B Β· Flow routing (ship complete & grouping)

ID Scenario / input Expected outcome Result
F1 Ship Complete = No, not linked to another SO Flow 1 (nswo, 7132) β†’ proceeds to courier selection ☐
F2 Ship Complete = No, ships together with another SO Flagged Ship Complete = Yes β†’ picked up by Flow 2 next run ☐
F3 Ship Complete = Yes, every item across the group committed Flow 2 (swo, 7136) β†’ proceeds; group updated together ☐
F4 Ship Complete = Yes, one item 0/1 committed (e.g. S301287 + S306157) Waiting β€” whole group skipped this run, no NetSuite update; retries next run ☐
F5 Merged group where a related SO isn't in the search results Related SO fetched (SuiteQL); group assembled; maxCxRequestedDate & total CBM across all items ☐

C Β· Courier selection by service level

ID Scenario / input Expected outcome Code Result
C1 Full Service, Life postcode valid (e.g. S308550) Courier = Life Full; approved 15 ☐
C2 Full Service, Life invalid, DT valid Courier = DT Full; approved 15 ☐
C3 2 Man, Life items, Life postcode valid (e.g. S315316) Courier = Life 2 Man; approved 15 ☐
C4 2 Man, Life invalid Falls through in order: DT 2 Man β†’ Pedemont β†’ TT 15 ☐
C5 Standard, β‰₯1 Life item, Life postcode valid Ship Via β†’ Life Standard; approved 15 ☐
C6 Standard, Life invalid, DT ATL valid Ship Via β†’ DT ATL; approved 15 ☐
C7 Standard, all items AusPost Ship Via β†’ AusPost; approved 15 ☐
C8 Standard, all / β‰₯1 Standard (non-AusPost, non-Life) Ship Via β†’ Allied; approved 15 ☐
C9 Paid Standard, but β‰₯1 item needs 2 Man Upgraded to Two Man hierarchy; approved as premium-item upgrade 21 ☐

D Β· Postcode validation & front-end fallback

ID Scenario / input Expected outcome Code Result
P1 No courier covers the postcode; front-end pick is a real courier Approve the front-end rate-card courier 14 ☐
P2 No courier covers the postcode; front-end pick = TBC Disapprove β€” postcode not covered 5 ☐
P3 TT courier (no postcode CSV) Its postcode check always fails β†’ next step β€” ☐

E Β· Scheduling, CBM & buffer

ID Scenario / input Expected outcome Result
S1 Life truck, CBM fits, no CX date (e.g. S308550, Syd R4) First available Syd R4 truck day on/after the 3-day buffer (Fri 7 Aug 2026 in sample data) ☐
S2 Life truck, order CBM exceeds remaining capacity that day Skips that day β†’ next available date that fits ☐
S3 No available date fits Order ignored + error email raised ☐
S4 CX requested date later than the buffer (e.g. S315316, CX 30/10) First truck day on/after the CX requested date ☐
S5 Life truck with a weekend day marked available Buffer counts that weekend day; otherwise weekends excluded ☐
S6 Two trucks/dates both qualify Lower-CBM truck chosen first ☐

F Β· Linehaul (Life couriers)

ID Scenario / input Expected outcome Result
L1 Life courier, ship state NSW or ACT Ship Date = Scheduled Delivery Date (local, same day) ☐
L2 Life courier, QLD (e.g. S310492 + S310562, Bris R3) Deliver Tue 18 Aug; Ship Date = Tue 11 Aug; Route SYD β†’ BRIS; applied to both linked SOs ☐
L3 Life courier, VIC Ship Date = Thursday before delivery; Route SYD β†’ MEL ☐
L4 Life courier, state not NSW/VIC/QLD (WA/SA/TAS) Rejected β€” no partial update ☐

G Β· Edge cases & resilience

ID Scenario / input Expected outcome Result
X1 2 Man order where every committed item is Standard_Auspost Sent to manual review (not auto-processed) ☐
X2 Any approval/disapproval Correct RFSC reason code written (15 / 21 / 14 / 5) β€” confirm against config ☐
X3 Run approaches the ~14-min Lambda limit Self-reinvokes async (retryAttempt+1, max 3); remaining orders processed next invocation; no duplicate updates ☐
X4 Courier Schedule service unavailable Falls back to cached schedule; if none, terminates gracefully with an error email (no bad dates written) ☐

UAT sign-off

  • ☐ Ran on staging (sandbox NetSuite) β€” dry-run traced, then a live staging write.
  • ☐ Entry gating (A) β€” ineligible orders correctly excluded.
  • ☐ Flow routing (B) β€” ship-together promotion & all-committed gate behave as expected.
  • ☐ Courier selection (C) β€” every service-level path picks the right courier; Standardβ†’2 Man upgrade fires.
  • ☐ Postcode fallback (D) β€” rate-card approve (14) and TBC disapprove (5) both correct.
  • ☐ Scheduling (E) & linehaul (F) β€” dates, CBM, buffer, and Tue/Thu linehaul verified.
  • ☐ Reason codes (X2) match config; no unintended NetSuite writes during dry-run.
  • ☐ Sign-off: name & date ____________________

13. Source references (read-only)

Business logic lives in these files (do not edit β€” read-only per the access policy):

  • shipping-v6-nswo/controllers/app.mjs, shipping-v6-swo/controllers/app.mjs β€” main decision loop
  • helper/courierScheduleClient.mjs β€” buffers, CBM, date selection
  • helper/formatter.mjs β€” order formatting (swo adds mergeSalesOrders)
  • controllers/ns.mjs β€” NetSuite writes (swo adds updateGroupedSalesOrders)
  • controllers/aws.mjs β€” postcode validation, S3, SES
  • helper/enums.mjs β€” couriers & service levels
  • config/{staging,production}.mjs β€” reason codes, saved-search IDs

Full architecture, repo differences, and file-by-file reference are in the interactive documentation: https://claude.ai/code/artifact/56822bef-4dc2-4a69-b87b-bc913a87004b