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]>
454 lines
31 KiB
Markdown
454 lines
31 KiB
Markdown
# 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.
|