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:
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user