A Python tool that ingests a batch of order exceptions (JSON or CSV), processes each one against a rulebook, and generates ready-to-use operational outputs: Slack alerts, customer email drafts, Gorgias deep links, and a structured results file.
In omnichannel retail, order exceptions are a constant — inventory drops to zero after an order is placed, a customer types their address in a way the carrier won't accept, a gift note contains content that can't go on a packing slip. Each exception type has a different resolution path and a different team who needs to act on it.
Without a standardised handler, exceptions get triaged manually: someone reads an alert, decides what to do, drafts a message, finds the right Gorgias ticket, and posts to Slack. That process is slow, inconsistent, and easy to miss at volume.
This tool automates the triage layer. Given a file of exceptions, it applies the rulebook for each one — auto-resolving what it can, generating ready-to-send communications for the rest, and routing alerts to the right channel (CX, Ops, or Engineering) so the right person sees exactly what they need to act on.
For every exception, the processor outputs:
| Output | Description |
|---|---|
| Auto-resolution | Action taken automatically (e.g. address abbreviated, characters cleaned, gift note dropped) |
| Changes log | Itemised record of every auto-modification made |
| Slack alert | Fully formatted message ready to post, routed to the correct channel |
| Customer email draft | Pre-written email with order details, product name, and appropriate tone |
| Gorgias deep link | URL that opens a pre-populated new ticket in Gorgias (address and customer comms exceptions) |
| Order timeline note | Internal note to attach to the order record where applicable |
Results are written to a structured JSON or CSV file alongside the console summary.
Exceptions raised before the order has been sent to a fulfillment node.
| Subtype | Resolution |
|---|---|
| eCommerce | Reroute to single node if fulfillable in full → split across 2 nodes if partially available → cancel OOS line, hold receipt, CX Slack + customer email |
| In-Store | iPad push notification sent to store team; store resolves |
| BOPIS (Buy Online, Pick Up In Store) | CX Slack alert asking team to confirm with the guest whether they'll accept home shipment instead of in-store pickup |
| Endless Aisle | Same reroute logic as eCommerce |
| Ship-From-Store | Same reroute logic as eCommerce |
Exceptions raised after the order has entered the fulfillment pipeline.
- Ship complete if all units are available
- Partial ship if some units are available — OOS SKU dropped, order re-pushed, CX Slack + customer email
- Full OOS — SKU dropped, order re-pushed, CX Slack + customer email
Carrier systems enforce a 30-character limit per address line. The processor:
- Auto-applies standard abbreviations (
Boulevard → Blvd,Apartment → Apt,Suite → Ste, and 20+ more) - If the line is within 30 characters after abbreviation — auto-resolved, changes logged
- If still over 30 characters — CX Slack alert + Gorgias deep link + customer email asking for a shortened address
Address fails validation (unrecognised city, invalid postcode, etc.). Generates a CX Slack alert, a Gorgias deep link, and a pre-drafted customer email asking the customer to confirm or correct their address.
Non-ASCII characters in shipping address fields are classified and handled in three ways:
| Character type | Action |
|---|---|
| Emoji | Auto-dropped |
| Accented Latin (é, ü, ö, ñ…) | Auto-converted to ASCII base character (é → e) |
| Non-Latin / foreign script | Flagged — CX Slack alert + Gorgias deep link + customer email requesting Latin-character address |
All auto-changes are logged. If every character was auto-resolvable, the exception is marked resolved with no escalation.
Offensive content detected in the gift note field.
- Gift note is dropped entirely
- An order timeline note is added:
Gift note removed due to content policy - No customer outreach is generated
Exception type not recognised by the rulebook.
- Pre-push — routed to
#eng-order-exceptionsfor engineering investigation - Post-push — routed to
#ops-order-exceptionsfor CX/Ops manual triage
| Prefix | Market |
|---|---|
GL1- |
Domestic |
GL2- |
UK |
order-exception-handler/
├── processor.py # Main tool
├── sample_exceptions.json # 11 sample exceptions covering all types
├── sample_exceptions_results.json # Output generated from sample data
└── README.md
Requirements: Python 3.9+, no external dependencies.
# JSON input, default output (writes sample_exceptions_results.json)
python3 processor.py --input sample_exceptions.json
# Specify a Gorgias subdomain (replaces the 'yourstore' placeholder in deep links)
python3 processor.py --input sample_exceptions.json --gorgias-domain mybrand
# Write results to a specific file
python3 processor.py --input sample_exceptions.json --output results.json
# CSV input, CSV output
python3 processor.py --input exceptions.csv --output results.csvArguments:
| Flag | Short | Description |
|---|---|---|
--input |
-i |
Path to input file (.json or .csv) — required |
--output |
-o |
Path to output file — optional, defaults to <input>_results.json |
--gorgias-domain |
-g |
Your Gorgias subdomain (e.g. mybrand for mybrand.gorgias.com) |
Each exception record should include:
{
"order_number": "GL1-00042381",
"exception_type": "inventory_pre_push",
"exception_subtype": "ecom",
"shipment_status": "pre_push",
"customer_name": "Jane Smith",
"customer_email": "jane@example.com",
"sku": "SKU-001",
"product_name": "Product Name",
"quantity_ordered": 2,
"quantity_available": 0,
"store_id": null,
"gift_note": null,
"shipping_address": {
"name": "Jane Smith",
"line1": "123 Main Street",
"line2": "",
"city": "Portland",
"state": "OR",
"zip": "97201",
"country": "US"
},
"order_total": 120.00,
"created_at": "2026-04-09T08:00:00Z"
}exception_subtype is required for inventory_pre_push records. shipment_status (pre_push / post_push) is used to route unknown exceptions to the correct Slack channel.
Each result record contains:
{
"order_number": "GL1-00042381",
"exception_type": "inventory_pre_push",
"exception_subtype": "ecom",
"auto_resolved": false,
"resolution_action": "OOS line cancelled and receipt held. CX Slack alert sent + customer email drafted.",
"resolution_notes": "Ordered: 2 | Available: 0 — no fulfillment possible.",
"changes_log": [],
"effective_address": null,
"slack_alert": {
"channel": "#cx-order-exceptions",
"text": "..."
},
"customer_email_draft": {
"to": "jane@example.com",
"subject": "An update on your order GL1-00042381",
"body": "..."
},
"gorgias_deep_link": "https://mybrand.gorgias.com/app/ticket/new?...",
"order_timeline_note": null,
"processed_at": "2026-04-09T08:00:00+00:00"
}Brand name, support email, and Slack channel names are set as constants at the top of processor.py:
GORGIAS_DOMAIN = "yourstore"
CX_SLACK_CHANNEL = "#cx-order-exceptions"
OPS_SLACK_CHANNEL = "#ops-order-exceptions"
ENG_SLACK_CHANNEL = "#eng-order-exceptions"
SUPPORT_EMAIL = "support@yourstore.com"
BRAND_NAME = "YourBrand"
ADDRESS_LINE_MAX = 30sample_exceptions.json contains 11 synthetic records designed to exercise every exception type and resolution path. The data — order numbers, customer names, SKUs, store IDs, and address scenarios — is entirely fictional.
The exception types, routing logic, channel structure, and fulfillment terminology (pre/post push, endless aisle, ship-from-store, BOPIS, node rerouting) are modelled after real commerce operations workflows. The tool is built to reflect how these exceptions are actually handled in practice: what gets auto-resolved, what needs a human, who that human is, and what they need to act.