Add Create Test Shipment (Email SKU) for testing the return-label flow, plus test-mode/store_id investigation notes

Adds synthetic source="test" tickets (create_test_shipment_order/delete_test_shipments)
carrying a company's real emailed-return-label SKU, so Step 1/Step 2 can be exercised
against the sandbox without a real JIRA ticket or risking a real customer's. Gated to
Test Mode - the same flow against production would create a real paid shipment.

CLAUDE.md also captures this session's ShipStation sandbox findings: store_id is
schema-optional but empirically required for label visibility, the newer ship15 web UI's
URL is NOT the se- store ID (confirmed via direct probe - different failure shape than a
wrong-but-well-formed ID), and two separate test stores/carriers are needed per company,
mirroring production.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
2026-09-28 11:45:09 -05:00
co-authored by Claude Sonnet 5
parent 82e6148ca9
commit a7d2e7870c
3 changed files with 259 additions and 11 deletions
+78
View File
@@ -27,6 +27,7 @@ from PyQt6.QtWidgets import (
QTabWidget,
QFileDialog,
QDialog,
QInputDialog,
)
from app import config
@@ -53,6 +54,8 @@ from app.workers import (
save_dummy_outbound_label_id,
save_pack_data,
reset_local_database,
create_test_shipment_order,
delete_test_shipments,
)
@@ -184,6 +187,24 @@ class MainWindow(QMainWindow):
create_test_warehouses_action.triggered.connect(self._on_create_test_warehouses_clicked)
data_menu.addAction(create_test_warehouses_action)
data_menu.addSeparator()
create_test_shipment_action = QAction("Create Test Shipment (Email SKU)...", self)
create_test_shipment_action.setStatusTip(
"Test Mode only - creates a synthetic ticket carrying that company's emailed-"
"return-label SKU, so Create Return Label can be run against it in the sandbox"
)
create_test_shipment_action.triggered.connect(self._on_create_test_shipment_clicked)
data_menu.addAction(create_test_shipment_action)
delete_test_shipments_action = QAction("Delete Test Shipments...", self)
delete_test_shipments_action.setStatusTip(
"Removes every synthetic ticket created by Create Test Shipment - real JIRA "
"tickets are not affected"
)
delete_test_shipments_action.triggered.connect(self._on_delete_test_shipments_clicked)
data_menu.addAction(delete_test_shipments_action)
orders_menu = menu_bar.addMenu("&Orders")
self._pack_ticket_action = QAction("Pack Ticket", self)
@@ -386,6 +407,63 @@ class MainWindow(QMainWindow):
QMessageBox.information(self, "Create Test Warehouse(s)", "\n".join(lines))
def _on_create_test_shipment_clicked(self) -> None:
if not config.is_shipstation_test_mode():
QMessageBox.information(
self,
"Test Mode is off",
"Turn on ShipStation Test Mode first (Data menu). This ticket is meant "
"to be run through Create Return Label against the sandbox account, not "
"production - a real store_id/carrier under production settings would "
"create an actual, paid shipment to a fake address.",
)
return
company, ok = QInputDialog.getItem(
self,
"Create Test Shipment",
"Company (picks that company's configured emailed-return-label SKU):",
["Signify Health", "Oak Street Health"],
editable=False,
)
if not ok:
return
try:
order = create_test_shipment_order(company)
except ValueError as exc:
QMessageBox.critical(self, "Could not create test shipment", str(exc))
return
self._refresh_everything()
QMessageBox.information(
self,
"Test Shipment Created",
f"Created {order.ticket_number} ({company}, SKU {order.skus[0]}) on the "
"Active tab.\n\n"
"Select it there and use Create Return Label to run Step 1 (dummy "
"shipment) and Step 2 (return label) against the sandbox account, the "
"same code path a real ticket would use.\n\n"
"Use Delete Test Shipments (Data menu) to clean it up afterward.",
)
def _on_delete_test_shipments_clicked(self) -> None:
confirm = QMessageBox.question(
self,
"Delete Test Shipments",
"Removes every synthetic ticket created by Create Test Shipment "
"(ticket numbers starting with TEST-EMAIL-). Real JIRA tickets are not "
"affected.\n\nContinue?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No,
)
if confirm != QMessageBox.StandardButton.Yes:
return
count = delete_test_shipments()
self._refresh_everything()
QMessageBox.information(self, "Delete Test Shipments", f"Deleted {count} test shipment(s).")
def _on_order_double_clicked(self, order) -> None:
dialog = OrderDetailDialog(order, self)
dialog.exec()
+93
View File
@@ -9,11 +9,13 @@ pattern rather than blocking the UI.
from __future__ import annotations
import datetime as dt
import uuid
from typing import List, Tuple, TypedDict
from PyQt6.QtCore import QThread, pyqtSignal
from sqlalchemy import select, delete
from app import config
from app.database import get_session
from app.models import Order
from app.schedule import get_cutoff_time, is_past_cutoff_today
@@ -301,6 +303,97 @@ def reset_local_database() -> int:
session.close()
def _emailed_label_sku_for_company(company: str) -> str:
"""Picks whichever configured EMAILED_LABEL_SKUS entry actually
resolves to the given company via COMPANY_SKU_MAP, rather than
hardcoding SH007/OK012 - stays correct if either setting changes."""
from app.companies import parse_mapping, resolve_company_by_sku
from app.return_labels import get_emailed_label_skus
sku_map = parse_mapping(config.get("COMPANY_SKU_MAP", config.DEFAULT_COMPANY_SKU_MAP))
for sku in sorted(get_emailed_label_skus()):
if resolve_company_by_sku(sku, sku_map) == company:
return sku.upper()
return ""
def create_test_shipment_order(company: str) -> Order:
"""
Creates a synthetic, non-JIRA Order row carrying that company's
configured emailed-return-label SKU, purely so the existing "Create
Return Label" flow (return_label_dialog.py + shipstation_send.py) can
be exercised end-to-end - dummy shipment, then real return label -
against a throwaway ticket instead of risking a real customer's.
source="test" keeps this completely separate from real JIRA rows:
save_orders() only ever matches on source == "jira"/"shipstation", so
Import from JIRA can never touch or overwrite one of these, and
delete_test_shipments() cleans them up by that same marker.
status="Created" so it lands on the Active tab like a real open
ticket would - that's where staff would naturally go to select it and
run Create Return Label.
"""
sku = _emailed_label_sku_for_company(company)
if not sku:
raise ValueError(
f"No EMAILED_LABEL_SKUS entry resolves to '{company}' via COMPANY_SKU_MAP - "
"check both settings."
)
ticket_number = f"TEST-EMAIL-{uuid.uuid4().hex[:8].upper()}"
order = Order(
source="test",
external_id=ticket_number,
ticket_number=ticket_number,
company=company,
skus=[sku],
line_items=[{"sku": sku, "item_name": "TEST - Emailed Return Label"}],
shipping_info={
"name": "TEST ORDER - DO NOT SHIP",
"phone": "555-555-5555",
"email": "",
"address1": "123 Test St",
"address2": "",
"city": "Austin",
"state": "TX",
"zip": "78701",
},
creator="Test Shipment Generator",
assignee="",
description=(
"Synthetic test order created via Data > Create Test Shipment (Email SKU) - "
"not a real ticket. Safe to delete with Data > Delete Test Shipments."
),
summary=f"TEST - Emailed Return Label ({company})",
status="Created",
source_created_at=dt.datetime.now(),
)
session = get_session()
try:
session.add(order)
session.commit()
finally:
session.close()
return order
def delete_test_shipments() -> int:
"""Removes every synthetic order created by create_test_shipment_order()
(source == "test") - real JIRA-sourced rows are untouched, since those
always have source == "jira"."""
session = get_session()
try:
result = session.execute(delete(Order).where(Order.source == "test"))
session.commit()
return result.rowcount or 0
finally:
session.close()
def load_all_orders() -> List[Order]:
session = get_session()
try: