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-swoSource 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:
- 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_shipTogetherWithIDcolumn β this is the "Ships together with another SO?" gate.) - Flow 2 (swo):
mergeSalesOrdersfollowsSO_shipTogetherWithIDto build a group, which:- ships together β
updateGroupedSalesOrderswrites 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).
- ships together β
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_shipTogetherWithIDto 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.jsoncall this "Module 5" β same stage, different naming. - nswo config keys:
queryHelper/rdsreferenceconfig.AWS_RDS_DBNAMEand several libs readconfig.AWS_REGION, but neither key is defined in nswo config (resolve toundefined). swo definesAWS_REGIONand 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=$LATESTselects 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 hasAWS_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 loophelper/courierScheduleClient.mjsβ buffers, CBM, date selectionhelper/formatter.mjsβ order formatting (swo addsmergeSalesOrders)controllers/ns.mjsβ NetSuite writes (swo addsupdateGroupedSalesOrders)controllers/aws.mjsβ postcode validation, S3, SEShelper/enums.mjsβ couriers & service levelsconfig/{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