Files
Order-Manager/CLAUDE.md
T
madminandClaude Sonnet 5 d8f59b7725 Fix external_shipment_id placement and test-mode store_id ticket persistence in the return-label flow
external_shipment_id was at the top level of the POST /v2/labels payload in both
_create_dummy_outbound_label() and create_return_label_from_dummy() - ShipStation's
schema only supports it nested under shipment, so it was silently ignored and
auto-generated ("SEAuto-...") on every label this flow ever created. Moved it inside
the shipment dict in both functions. Confirmed end-to-end on a real production
ticket (AR-166098): Order # now correctly shows the real ticket number, and
ship_from/ship_to are correct for a return.

Also: store_id is now only required in production - ShipStation's sandbox
environment cannot have stores/Order Sources at all (confirmed in the real sandbox
dashboard), so test mode omits it from the request instead of requiring an
impossible value. And three per-ticket write-backs (mark_shipstation_sent,
save_dummy_outbound_label_id, save_pack_data) were hardcoded to source == "jira",
silently no-opping for source == "test" tickets created by Create Test Shipment -
now match on ticket_number alone, since source is only ever "jira" or "test" and
ticket_number is already unique across both.

CLAUDE.md records the full investigation and closes out the long-standing
"return-label flow real-world verification" known-pending item.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-10-01 15:53:54 -05:00

31 KiB

Order Manager - Project Context

PyQt6 desktop app for a daily order-processing workflow: JIRA tickets -> ShipStation labels -> Odoo fulfillment. One Order row per JIRA ticket; ShipStation only ever enriches existing rows, never creates them. Two companies (Signify Health, Oak Street Health) plus a subsidiary (RubiconMD) with its own naming quirk - see below.

This file exists because this project was built up over a very long conversation with Claude in claude.ai, iterating and debugging interactively. It's written so a fresh Claude Code session (or a new person) doesn't have to rediscover any of this the hard way. Read this before making changes, especially to shipstation_send.py, ticket_validation.py, or anything touching return labels.

Architecture

main.py                              entry point
app/
  adf.py                             Atlassian Document Format -> plain text parser
  companies.py                       SKU prefix -> company resolution (COMPANY_SKU_MAP)
  config.py                          SETTINGS_SCHEMA, defaults, test-mode-aware lookups
  database.py                        SQLAlchemy engine + auto-migration (ADD COLUMN on startup)
  models.py                          Order table (single source of truth for all fields)
  queries.py                         get_open_ticket_numbers()
  return_labels.py                   is_emailed_label_order() - SH007/OK012 detection
  schedule.py                        cutoff time / past-cutoff logic
  serial_suggestions.py              device-keyword -> serial-field suggestions for Pack Ticket
  status_rules.py                    active/cancelled status list parsing
  ticket_validation.py               SKU validation rules (company mismatch, return/device mismatch)
  tracking.py                        suggest_jira_status() from tracking numbers
  external_links.py                  JIRA/Google Maps URL builders (View in JIRA, Look Up Address)
  workers.py                         QThread workers + save_orders()/load_orders_by_view()/etc. -
                                      also create_test_shipment_order()/delete_test_shipments()
                                      (synthetic source="test" tickets for exercising the
                                      Create Return Label flow without a real JIRA ticket)
  services/
    base.py                         NormalizedOrder TypedDict, OrderService ABC
    jira_service.py                 JIRA REST API v3 /search/jql, paginated
    shipstation_service.py          ShipStation tracking-number pull (bulk fetch)
    shipstation_send.py             ALL ShipStation write operations - see below, this is the
                                     file with the most hard-won context in the whole project
    odoo_export.py                  CSV export
  ui/
    main_window.py                  Menu bar (File/Data/Orders) + slim toolbar, 3 tabs
    settings_dialog.py               Scrollable, grouped by SETTINGS_SCHEMA category
    widgets/
      dashboard.py                  Stat cards
      orders_table.py               QAbstractTableModel - see PERFORMANCE section below
      order_detail_dialog.py        Raw payload, tracking, validation issues, JIRA/Maps links
      pack_ticket_dialog.py         Serial number entry, barcode-scanner-friendly
      return_label_dialog.py        Two-step dummy-then-return UI - see RETURN LABELS below

Order model (key columns)

id, source, external_id, ticket_number, company, skus (JSON), line_items (JSON), shipping_info (JSON), creator, assignee, description, tracking_numbers (JSON), shipping_method, serial_numbers (JSON), packed, packed_at, summary, status, source_created_at, imported_at, fulfilled_at, cancelled_at, shipstation_sent_at, dummy_outbound_label_id, raw_data

dummy_outbound_label_id is the newest column - it persists step 1 of the return-label flow (see below) so step 2 can be triggered separately, even in a later session.

All timestamps use local time (dt.datetime.now()), not UTC - this was a real, confirmed bug early on (evening cancellations failed same-day checks under UTC).

The three tabs

  • Active: status in ACTIVE_STATUSES ("Created")
  • Cancelled: cancelled status AND cancelled_at.date() == today - ages into Done at midnight
  • Done: everything else

Return labels - the hard part of this project

This is the single most iterated-on feature and the one most likely to bite you if touched carelessly. Sequence of what was learned, in order, because the reasoning matters for not re-breaking it:

  1. A standalone return label (POST /v2/labels with is_return_label: true, no linked outbound shipment) reports status: completed but is invisible in ShipStation's UI. This is confirmed against the team's actual manual process, not just API docs: they always create a cheap "dummy" outbound shipment first, then generate the return label from it via ShipStation's own GUI.
  2. The fix: create_dummy_shipment() creates a minimal outbound label (1x1x1in, 1oz, cheapest service) first. Its label_id is passed as outbound_label_id when creating the real return label via create_return_label_from_dummy(). Both are in shipstation_send.py.
  3. This is deliberately a two-step UI flow, not one atomic action (return_label_dialog.py
    • main_window.py's _on_return_label_dialog_finished). Clicking "Create Return Label" the first time on a ticket only creates the dummy and stops - the button label changes to "Step 2" and the user is expected to verify the dummy in ShipStation before proceeding. This was an explicit ask: splitting the steps apart means a problem in one doesn't get masked by the other. dummy_outbound_label_id on the Order persists step 1's result across sessions.
  4. external_shipment_id populates ShipStation's "Order #" column - confirmed via ShipStation's own docs. It must be unique per account, so the dummy uses {ticket_number}-DUMMY and the real return label uses the bare {ticket_number}.
  5. warehouse_id and ship_from are mutually exclusive in a /v2/labels request - confirmed by a real ShipStation 400 error ("ship_from and warehouse_id cannot be provided in same request"). When a warehouse_id is configured, it's used alone (ShipStation resolves the address from the registered warehouse record); otherwise the code falls back to an explicit ship_from address. See _create_dummy_outbound_label().
  6. ShipStation's /v2/labels/{label_id}/return endpoint exists and auto-swaps ship_to/ship_from, but does NOT accept a custom shipment.packages override - it inherits the original outbound's package. That's why this project does NOT use that endpoint; it uses POST /v2/labels with is_return_label: true + outbound_label_id set instead (Method 1 in ShipStation's docs), which supports full custom packages on the return label itself. Multiple packages per return label already works this way (one call, packages array) - no additional work needed there.
  7. A carrier_id (or store_id/warehouse_id) missing the se- prefix produces a confusing "not found" error - _normalize_shipstation_id() in shipstation_send.py auto-prepends se- to any bare numeric ID at every lookup site (store, warehouse, carrier), since this is an easy typo when copying IDs out of ShipStation's UI.
  8. ShipStation's own error responses appear to strip the se- prefix when echoing back field_value, regardless of what was actually sent. Don't assume a bare number in an error message necessarily means the prefix is missing in your request - it may just mean the ID genuinely doesn't exist in that account (see Test Mode below, this is exactly what happened with carrier_id and warehouse_id there).
  9. RubiconMD (RMD-prefixed SKUs) is an Oak Street subsidiary: classified as "Oak Street Health" everywhere (dashboard, filters, company mismatch checks), but the return label's address name field is overridden to "Rubicon MD" (RUBICONMD_RETURN_NAME setting). Everything else (carrier, service, address, phone) is identical to Oak Street's - RubiconMD does NOT have its own shipping account. See _is_rubiconmd_order() / _return_address_for_order().
  10. The tracking number is never auto-copied to the clipboard - there's an explicit "Copy Tracking Number" button in the success dialog instead. This was a deliberate reversal: an earlier version auto-copied and that was flagged as risky (staff use the clipboard constantly for other things; silently overwriting it loses data).

ShipStation Test Mode

Built so the team can test the whole return-label flow without spending real money or needing to void mistakes. Toggle: Data menu checkbox, also mirrored as a permanent orange status-bar banner when active (_test_mode_indicator).

Design: every account-specific ShipStation setting (API key, store/warehouse ID, return carrier/service code) has a TEST_-prefixed override (config.get_shipstation_setting() in config.py). When test mode is on, only the TEST_ value is used - no fallback to production. This used to fall back to the production value when the TEST_ override was blank, on the theory the sandbox might share IDs with production - confirmed against ShipStation's own docs that it never does ("Sandbox data is isolated from production data... anything you create in the sandbox will not be accessible in production, or vice-versa" - docs.shipstation.com/apis/shipengine/docs/getting-started/sandbox). That fallback is what produced three separate confusing "not found"/"invalid" errors in a row (carrier, then warehouse, then store) before it was caught and removed - a sandbox key being handed a production ID isn't a soft mismatch, it's a guaranteed rejection. The team's sandbox is a separate ShipEngine-heritage test account entirely, confirmed via list_carriers()/list_warehouses() diagnostics (Data menu - "List ShipStation Carriers/Warehouses..."). Confirmed real test carrier: se-6366092 (UPS) for both companies, since there's one shared sandbox account. Because there's no fallback now, every TEST_* setting a flow touches must be filled in explicitly, even ones that are plain strings rather than account-specific IDs (e.g. TEST_SIGNIFY_RETURN_SERVICE_CODE/ TEST_OAKSTREET_RETURN_SERVICE_CODE were blank and got set to ups_ground to match production - safe because a service code isn't sandbox/production-isolated data, it's just a carrier capability string).

Warehouses don't exist by default in a sandbox account - list_warehouses() correctly returning an empty list in test mode wasn't a bug, there was just nothing there yet to list. Fixed with create_warehouse() / create_test_warehouse_for_company() in shipstation_send.py (confirmed request shape against ShipStation's own POST /v2/warehouses docs - name + origin_address, with address_residential_indicator required) and a "Create Test Warehouse(s)..." Data-menu action that reuses the existing SIGNIFY_RETURN_*/OAKSTREET_RETURN_* address settings as the origin address (same physical address, just registered under the sandbox account) and auto-saves the resulting warehouse_id into TEST_SHIPSTATION_SIGNIFY_WAREHOUSE_ID / TEST_SHIPSTATION_OAKSTREET_WAREHOUSE_ID.

SOLVED, 2026-10-01: test mode can NEVER have a store_id - it's a platform limit, not a configuration gap. Confirmed directly in ShipEngine/ShipStation's own dashboard (the actual separate sandbox account, reached via its own distinct sandbox login - not ship15.shipstation.com, which is the production web UI regardless of how a store there is named): the Sandbox environment's "Connect order sources" flow explicitly says to switch to Production to connect order sources. Stores/Order Sources structurally do not exist in sandbox at all - unlike carriers and warehouses (both confirmed to have real sandbox equivalents), this is one ShipStation resource type the sandbox environment simply doesn't support. No amount of searching the UI, guessing IDs, or chasing V1 API tricks was ever going to produce a valid TEST_SHIPSTATION_*_STORE_ID, because there is no such thing to find.

The fix: store_id is now only enforced in PRODUCTION (_validate_return_label_prerequisites() in shipstation_send.py checks config.is_shipstation_test_mode() before raising), and left out of the request entirely when blank (_create_dummy_outbound_label() / create_return_label_from_dummy()) rather than sent empty. This was already known to be API-safe - shipment.store_id is not in ShipStation's own required-fields schema, and a live probe on 2026-09-18 confirmed omitting it succeeds cleanly (200, status: "completed"). Confirmed end-to-end, 2026-10-01: both Step 1 (dummy shipment) and Step 2 (return label) now succeed in test mode for both companies with real sandbox labels (se-206662629/se-206662656 Signify, se-206662683/se-206662696 Oak Street). Production behavior is unchanged - store_id is still required there, where it's achievable and still matters for the originally-confirmed visibility reason (Return Labels point 1 above).

Two stores were created in the wrong account along the way - ship15.shipstation.com turned out to be the unified web UI for all accounts, test or production, so creating a store while logged into the normal company login created it in production regardless of name (OAKYTEST id 381922, Signify Test id 381921 - confirmed in the same GET /stores V1 API response as the real production Signify/ Oak Street stores, proving shared account). Those two got typed into TEST_SHIPSTATION_*_STORE_ID before the mistake was caught, which actively broke things again until cleared back to blank - if Step 1 starts failing with "invalid store" again, check those two settings are still blank before anything else. list_stores() still raises a clear dead-end error for the unrelated reason it always did - GET /v2/stores plain doesn't exist in the V2 API (confirmed against ShipStation's own OpenAPI reference: no Stores/Marketplaces section at all, store_id only ever an input field) - that part of the finding stands, it just turned out not to matter once test mode stopped needing a store_id at all.

Ceiling discovered 2026-10-02: omitting store_id also silently discards external_shipment_id, so sandbox can verify label creation but never ticket traceability. Confirmed by comparing real labels: querying actual historical production labels (e.g. real ticket AR-161051, created months ago) shows external_shipment_id round-tripping correctly - so the code's placement of that field is fine and this was never a general bug. But the two sandbox labels created once store_id started being omitted both came back with ShipStation's own auto-generated external_shipment_id (SEAuto-...), not the ticket number we sent - and external_order_id/"Order #" was blank on both, same as the "invisible in the UI" concern this app's store_id check was originally written to prevent. The mechanism: no store means no backing Order object, and ShipStation apparently won't honor a custom external_shipment_id without one to attach it to. This isn't fixable in code - it's the same platform ceiling as the store_id dead-end itself, just one layer deeper. Net effect: sandbox can prove the API calls mechanically succeed (labels get created, no errors), but can never prove ticket-number traceability works - that joins the branded-email step as something only verifiable in production. Treat a successful sandbox Step 1/Step 2 as "the request shape and credentials are right," not as "this ticket's correlation back to AR-###### will work" - the latter has only ever been confirmed in production.

FOUND AND FIXED, 2026-10-01: external_shipment_id was in the wrong place in the request the whole time - a real, longstanding bug, unrelated to any of the store_id/test-mode work above. Both _create_dummy_outbound_label() and create_return_label_from_dummy() put it at the TOP level of the POST /v2/labels payload. Confirmed against ShipStation's own request schema: it's only ever a field ON the shipment object (shipment.external_shipment_id) - there is no top-level equivalent for this endpoint, so ShipStation silently ignored ours and auto-generated its own SEAuto-... placeholder instead, on every single label this flow has ever created. Fixed by moving it inside the shipment dict in both functions. Confirmed end-to-end on a real production ticket (AR-166098, 2026-10-01): external_shipment_id now correctly reads AR-166098-DUMMY on the dummy and AR-166098 on the return label, and - the actual point of all of this - ShipStation's "Order #" column now correctly shows AR-166098, linked to the ticket's real pre-existing Order record. This had never been confirmed working before; CLAUDE.md's own "Known-pending" section had been flagging this exact end-to-end verification as outstanding for a reason. The dummy's -DUMMY-suffixed external_shipment_id will never show an Order # of its own (it doesn't match any real Order's number, and isn't meant to - it's throwaway per its own docstring); the real return label's bare ticket number is what matters, and that's the one that links up.

Also confirmed correct on that same real ticket, despite how it reads in ShipStation's UI: the return label's ship_from/ship_to are right (ship_from = the real customer's address, ship_to = the company warehouse - exactly the direction a return should go), verified via a direct GET /v2/shipments/{id} call (the ground truth) rather than trusting ShipStation's own "Return Details" UI tab, which mislabels the warehouse/return-destination address as "Ship From Address" in that specific summary panel - confusing UI copy on ShipStation's end, not a bug in this code. If this ever looks wrong again, check the real shipment data via the API before assuming the code regressed.

Correlation note: this app's own tracking-number pull (shipstation_service.py::_extract_ticket_number()) matches shipment_number/ external_shipment_id against TICKET_NUMBER_REGEX using fullmatch, not a substring search - so the dummy's -DUMMY suffix was never going to match anyway, by design, regardless of the bug above. Only the real return label's bare ticket number needs to (and now does) match correctly.

A separate, unrelated test-mode gap found 2026-09-18: TEST_SIGNIFY_RETURN_CARRIER_ID/ TEST_OAKSTREET_RETURN_CARRIER_ID were blank (only the *_SERVICE_CODE halves had been filled in earlier), producing "No return-label Carrier ID/Service Code configured" on Step 1. Not a new discovery - just the confirmed shared sandbox test carrier (se-6366092, UPS) from earlier in this section, re-verified live and filled into both settings. Worth knowing for next time: TEST_SIGNIFY_RETURN_SERVICE_CODE/ TEST_OAKSTREET_RETURN_SERVICE_CODE being set doesn't imply their carrier-ID counterparts are - check both halves of a TEST_*_RETURN_* pair, not just one.

Production readiness check, 2026-09-16 (read-only, no labels created, no cost): before a first real production test of the return-label flow, ran GET /v2/carriers, GET /v2/warehouses, and GET /v2/carriers/{id}/services directly against the production API key and diffed the results against .env:

  • SIGNIFY_RETURN_CARRIER_ID (se-350817) and OAKSTREET_RETURN_CARRIER_ID (se-599657) both exist and are the expected UPS accounts.
  • SHIPSTATION_SIGNIFY_WAREHOUSE_ID (se-180473) and SHIPSTATION_OAKSTREET_WAREHOUSE_ID (se-437417) both exist (named "Signify" and "OAKM" respectively).
  • ups_ground (both *_RETURN_SERVICE_CODE settings) is a valid service code on both carrier accounts.
  • store_id could NOT be checked - no listing endpoint exists (see above). This is the one remaining unknown going into a real test. Per the "Known-pending" section below, end-to-end production verification of this flow was still unconfirmed as of this check - so a first real shipment is a genuine first test, not a regression check. If it fails, expect the same "invalid store" error shape as the test-mode one; the fix then is re-checking the store_id in ShipStation's own UI, not the code.

Testing the emailed-return-label SKU flow without a real ticket: "Create Test Shipment (Email SKU)..." (Data menu, Test Mode only) creates a synthetic source="test" Order carrying that company's configured emailed-return-label SKU (resolved from EMAILED_LABEL_SKUS + COMPANY_SKU_MAP at creation time, not hardcoded to SH007/OK012) and a placeholder shipping address, then drops it on the Active tab (status="Created") so it can be selected and run through the real Create Return Label flow - same code path a JIRA ticket uses, no separate test-only logic to keep in sync. source="test" guarantees save_orders() (which only ever matches on source == "jira") can never touch or overwrite one of these, so a real Import from JIRA is safe to run with test shipments still sitting on the board. "Delete Test Shipments..." cleans them up by that same source marker, real tickets untouched. Deliberately gated to Test Mode - the same flow against production settings would create a real, paid UPS shipment to a fake address, not just a sandbox test label.

Bug found and fixed, 2026-10-02: per-ticket write-backs were hardcoded to source == "jira", so they silently no-op'd for source="test" tickets. mark_shipstation_sent(), save_dummy_outbound_label_id(), and save_pack_data() in workers.py all looked up WHERE source == "jira" AND ticket_number == ... - harmless for real tickets, but meant step 1 of the return-label flow on a test ticket would genuinely succeed in ShipStation (a real label got created) while the app silently failed to remember it, making "Create Return Label" always restart at step 1 no matter how many times it was run - and the exact same symptom reproduced in PRODUCTION too, since it had nothing to do with store_id or test mode at all, just this lookup. Fixed by matching on ticket_number alone - source is only ever "jira" or "test", and ticket_number is already unique across both (test tickets use the TEST-EMAIL- prefix specifically so they can't collide with a real JIRA number), so there was never a real reason to restrict these three to "jira". Confirmed end-to-end after the fix: create dummy -> persist -> reload from DB -> create real return label all succeeded in one real (sandbox) run. Side effect worth knowing: because this bug made every "Create Return Label" click on an already-stepped-through test ticket silently restart step 1, testing this in PRODUCTION (while chasing what looked like a store_id problem) may have created an extra, real, paid dummy shipment under a TEST-EMAIL-* ticket - worth checking the production account for stray dummy shipments and voiding any unwanted ones.

Real limitations of ShipStation's sandbox (confirmed via their own docs, not assumed) that constrain what test mode can actually verify:

  • Branded Labels / Branded Tracking Pages are NOT available in sandbox. This directly affects the emailed-return-label feature: the "Send Return Label" branded-email step (the actual point of the feature) cannot be tested end-to-end in test mode. Only label creation can be verified there; the branded email step still needs a real production test eventually.
  • 20 requests/minute rate limit (much lower than production) - plausible to hit during heavy iterative testing, and would likely present as a confusing generic-looking error.
  • Tracking events require real packages in a real carrier network - sandbox can't simulate them, so Pull Tracking Numbers won't return anything for test-mode shipments. Expected, not a bug.
  • Sales Orders API (used by the emergency "Send to ShipStation" feature specifically, not the return-label flow) is in beta and may not work in sandbox at all.

Performance - a real, fixed bug worth knowing about

ticket_validation.py's validate_ticket() and return_labels.py's is_emailed_label_order() both read Settings from disk internally (config.load_settings() / config.get(), which re-parse .env every call). Calling either of these inside OrdersTableModel.data() - which Qt invokes constantly, for every visible cell, every repaint - was a severe, real performance bug (measured: 2.7 seconds of UI freeze per refresh with 300 orders, and visibly janky scrolling).

Fixed by computing both once per set_orders() call (not per cell) and caching the result per order.id (self._issues_by_id, self._is_emailed_label_by_id in orders_table.py). Also hoisted the underlying Settings reads themselves out of the per-order loop via make_validation_context() (fetches sku_map/keyword_map/exempt_keywords once, passed into every validate_ticket() call in the batch) - measured improvement: 2749ms -> 19ms for a 300-order refresh. If you add a new per-order computed column, compute it once in set_orders() and cache it - never call anything that reads Settings from inside data().

Ticket validation rules (flagging only, never auto-cancels)

ticket_validation.py - two rules, confirmed against real business logic and real SKU catalogs (Oak Street + Signify deliverables spreadsheets, not guessed):

  1. Company mismatch: a ticket's SKUs resolve to more than one company (SH + OK present together). Simple, unambiguous.
  2. Return/device mismatch: a "Shipping - Return Label/Box X" line item's implied device type doesn't match anything else on the ticket. Two valid pairings, both treated as satisfying the rule:
    • An actual device line item of the same type (break-fix: send the asset, return the same type).
    • Another shipping item of the same type (asset recovery: a box AND a label for the same device, e.g. SH002 "Return Box iPad" + SH011 "Return Label iPad" - confirmed as a valid combination, NOT a mismatch, against real examples: SH002/SH011, OK001/OK006, OK011/OK013).
    • Exempt return types (no device expected at all): emailed labels, DPS Device, scheduled pickups, padded envelopes - RETURN_DEVICE_EXEMPT_KEYWORDS setting.
    • Device-type keywords are shared with serial_suggestions.py's DEVICE_FIELD_SUGGESTIONS (same vocabulary, different use) - includes an IE prefix note: Signify's IE400/IE401 SKUs are deprecated, deliberately not added to COMPANY_SKU_MAP.

This is intentionally flag-only. Cancellation is a deliberate manual step in JIRA (requires a reason, is audited, requires someone's JIRA account) - there is no JIRA-write capability anywhere in this app, by design, and that's not expected to change without a deliberate separate conversation about it.

Other things worth knowing

  • Bitdefender ATC crash (Windows-only, resolved): PackTicketDialog and ReturnLabelDialog used to crash the whole process on close - confirmed via crash dump analysis to be Bitdefender Endpoint Security's Advanced Threat Control corrupting a stack frame inside Qt6Core.dll, not a bug in this code. Fixed by showing both dialogs non-modally (.show() instead of .exec() - avoids the nested event loop .exec() runs) and reusing a single persistent instance per dialog type via set_order() rather than constructing/destroying one per ticket. See the WORKAROUND NOTE docstrings in both dialog files before changing how they're shown.
  • Menu bar, not just a toolbar: File / Data / Orders menus hold everything; a slim toolbar duplicates only the 3 highest-frequency actions (Import from JIRA, Pack Ticket, Create Return Label) using the same QAction objects, so there's nothing to keep in sync between the two.
  • JIRA import uses the newer /rest/api/3/search/jql endpoint (the old /rest/api/3/search was removed by Atlassian) - paginated via nextPageToken, with defensive anti-loop guards (stops on empty batch, isLast, missing/repeated token, or a 200-page hard ceiling).
  • View in JIRA defensively strips any /rest/... API path that might have ended up baked into the JIRA_URL setting before building the browse link - the setting is meant to be the bare site domain.
  • SKU company map (COMPANY_SKU_MAP): SH:Signify Health,OK:Oak Street Health,RMD:Oak Street Health - RMD added after confirming it's a subsidiary, not its own company.

Known-pending / not yet built

  • RESOLVED 2026-10-01, was the longest-running open item: the warehouse_id and store_id test-mode gaps are both fixed and confirmed end-to-end. Warehouse_id just needed TEST_SHIPSTATION_*_WAREHOUSE_ID populated (done). Store_id turned out to be structurally impossible to fix via settings at all - ShipStation's sandbox environment cannot have stores/Order Sources, full stop (confirmed in the real sandbox dashboard, which says to switch to Production to connect one) - so the code now only requires store_id in production and omits it entirely in test mode. Both Step 1 and Step 2 of the emailed-return-label flow are confirmed working end-to-end in test mode for both companies as of this fix (see ShipStation Test Mode above for the real sandbox label IDs from that test). This flow is now genuinely testable in sandbox for the first time - if there's still an appetite to verify the branded-email step too, remember that part specifically still requires production (Branded Labels/Tracking Pages aren't available in sandbox - see the sandbox limitations list below).
  • EOD JIRA push: deliberately deferred. packed, serial_numbers, tracking_numbers, shipping_method, assignee are all captured and ready for it whenever it's prioritized - would need the exact JIRA custom field IDs (outbound tracking slots 1-4, RMA number, serial fields) from the team.
  • Bulk JIRA -> ShipStation import: still an external, separate process (their own CSV-conversion tool) - this app only handles the single-ticket emergency send case. A bigger, riskier build if ever tackled (rate limits, bulk automation behavior).
  • RESOLVED 2026-10-01: return-label flow real-world verification. Confirmed end-to-end on a real production ticket (AR-166098) for the first time: Step 1 (dummy) and Step 2 (return label) both succeeded, Order # correctly shows the real ticket number in ShipStation, and ship_from/ship_to are correct for a return. This surfaced and fixed a real bug along the way (external_shipment_id placement - see ShipStation Test Mode above) that had been silently breaking ticket traceability on every label this flow ever created, sandbox or production, until now. Test labels from this verification were voided by the team since real tickets were about to be worked for real.
  • JIRA write access / auto-cancellation: explicitly declined by the team so far (cancellation requires a JIRA account and an audited reason) - don't build this without a deliberate, separate conversation about it first.