diff --git a/.env.example b/.env.example index 375e14e..defb4db 100644 --- a/.env.example +++ b/.env.example @@ -120,20 +120,10 @@ OAKSTREET_RETURN_CITY=Lewisville OAKSTREET_RETURN_STATE=TX OAKSTREET_RETURN_ZIP=75067 -# RubiconMD - Oak Street subsidiary (RMD-prefixed SKUs). Still needs its -# real Carrier ID/Service Code (its own UPS account, not Oak Street's) - -# address defaults to the same shared warehouse, just under its own name, -# since that's the only difference confirmed so far. Change the address if -# it actually ships from somewhere else. -RUBICONMD_RETURN_CARRIER_ID= -RUBICONMD_RETURN_SERVICE_CODE= +# RubiconMD - Oak Street subsidiary (RMD-prefixed SKUs). Uses Oak Street's +# own shipping account/address in every respect - this is the one thing +# that's actually different, the name shown on the return label. RUBICONMD_RETURN_NAME=Rubicon MD -RUBICONMD_RETURN_PHONE= -RUBICONMD_RETURN_ADDRESS1=1000 Spinks Road Suite 100 -RUBICONMD_RETURN_ADDRESS2= -RUBICONMD_RETURN_CITY=Lewisville -RUBICONMD_RETURN_STATE=TX -RUBICONMD_RETURN_ZIP=75067 # --- Packing --- # Serial-number fields to suggest per device keyword found in a ticket's line diff --git a/app/config.py b/app/config.py index 4623991..f5d963c 100644 --- a/app/config.py +++ b/app/config.py @@ -140,21 +140,10 @@ SETTINGS_SCHEMA: Dict[str, tuple[str, str, bool]] = { "OAKSTREET_RETURN_ZIP": ("Oak Street Return Address: Zip", "Return Labels", False), # RubiconMD - an Oak Street subsidiary (RMD-prefixed SKUs). Classified as - # Oak Street Health everywhere else, but return labels use its own UPS - # account and its own name on the return address. - "RUBICONMD_RETURN_CARRIER_ID": ("RubiconMD Return: ShipStation Carrier ID", "Return Labels", False), - "RUBICONMD_RETURN_SERVICE_CODE": ( - "RubiconMD Return: Service code (e.g. ups_ground)", - "Return Labels", - False, - ), - "RUBICONMD_RETURN_NAME": ("RubiconMD Return Address: Name", "Return Labels", False), - "RUBICONMD_RETURN_PHONE": ("RubiconMD Return Address: Phone", "Return Labels", False), - "RUBICONMD_RETURN_ADDRESS1": ("RubiconMD Return Address: Address 1", "Return Labels", False), - "RUBICONMD_RETURN_ADDRESS2": ("RubiconMD Return Address: Address 2", "Return Labels", False), - "RUBICONMD_RETURN_CITY": ("RubiconMD Return Address: City", "Return Labels", False), - "RUBICONMD_RETURN_STATE": ("RubiconMD Return Address: State", "Return Labels", False), - "RUBICONMD_RETURN_ZIP": ("RubiconMD Return Address: Zip", "Return Labels", False), + # Oak Street Health everywhere else. It uses Oak Street's own shipping + # account and address in every respect - this is the one thing that's + # actually different, the name shown on the return label. + "RUBICONMD_RETURN_NAME": ("RubiconMD Return Address: Name (uses Oak Street's account/address otherwise)", "Return Labels", False), "ACTIVE_STATUSES": ( "Statuses that count as real active work (comma-separated) - " diff --git a/app/services/shipstation_send.py b/app/services/shipstation_send.py index e679f7b..9c16aa6 100644 --- a/app/services/shipstation_send.py +++ b/app/services/shipstation_send.py @@ -273,27 +273,21 @@ def send_order_to_shipstation_api(order: Order) -> dict: VALID_CHARGE_EVENTS = {"on_creation", "on_carrier_acceptance", "carrier_default"} -def _return_config_prefix(order: Order) -> str: - """ - Normally keyed off company, but RMD-prefixed SKUs (RubiconMD, an Oak - Street subsidiary) use their own UPS account and return address even - though the ticket is still classified as Oak Street Health for every - other purpose - so this checks the ticket's actual SKUs, not just - order.company. (The IE prefix seen in Signify's catalog is deprecated - and deliberately not handled here - not expected on current tickets.) - """ - if any((sku or "").strip().upper().startswith("RMD") for sku in (order.skus or [])): - return "RUBICONMD_RETURN_" - if order.company == "Signify Health": - return "SIGNIFY_RETURN_" - return "OAKSTREET_RETURN_" +def _is_rubiconmd_order(order: Order) -> bool: + return any((sku or "").strip().upper().startswith("RMD") for sku in (order.skus or [])) def _return_address_for_order(order: Order) -> dict: + """RubiconMD (RMD-prefixed SKUs) uses Oak Street's own address in every + respect except the name on the label - not a separate warehouse, just + a different name for the same shipping account/address.""" settings = config.load_settings() - prefix = _return_config_prefix(order) + prefix = "SIGNIFY_RETURN_" if order.company == "Signify Health" else "OAKSTREET_RETURN_" + name = settings.get(f"{prefix}NAME", "") + if _is_rubiconmd_order(order): + name = settings.get("RUBICONMD_RETURN_NAME", "") or name return { - "name": settings.get(f"{prefix}NAME", ""), + "name": name, "phone": settings.get(f"{prefix}PHONE", ""), "address_line1": settings.get(f"{prefix}ADDRESS1", ""), "address_line2": settings.get(f"{prefix}ADDRESS2", "") or None, @@ -305,11 +299,13 @@ def _return_address_for_order(order: Order) -> dict: def _return_carrier_for_order(order: Order) -> tuple[str, str]: - """Each shipping account (Signify, Oak Street, and RubiconMD as its own - subsidiary account) has its own UPS carrier_id, even where the physical - warehouse is shared.""" + """RubiconMD uses Oak Street's own UPS account - no separate carrier, + just a different name on the return address (see + _return_address_for_order). Each of the two REAL shipping accounts + (Signify, Oak Street) has its own carrier_id even though they share a + physical warehouse.""" settings = config.load_settings() - prefix = _return_config_prefix(order) + prefix = "SIGNIFY_RETURN_" if order.company == "Signify Health" else "OAKSTREET_RETURN_" return settings.get(f"{prefix}CARRIER_ID", ""), settings.get(f"{prefix}SERVICE_CODE", "") diff --git a/app/ticket_validation.py b/app/ticket_validation.py index f5a00ef..da41e65 100644 --- a/app/ticket_validation.py +++ b/app/ticket_validation.py @@ -68,6 +68,16 @@ def check_company_mismatch(skus: List[str]) -> Optional[TicketIssue]: def check_return_device_mismatch(line_items: List[dict]) -> List[TicketIssue]: + """ + A return SKU's implied device type needs to match SOMETHING else on the + ticket - either an actual device line item (break-fix: send the asset, + return the same type) OR 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" - both are "shipping" items by our + is-it-a-device check, and pairing them like this is correct, not a + mismatch - confirmed against real examples: SH002/SH011, OK001/OK006, + OK011/OK013). + """ keyword_map = get_device_field_suggestions() # {keyword: [serial fields]} - keys only, here exempt_keywords = get_return_device_exempt_keywords() issues: List[TicketIssue] = [] @@ -87,16 +97,22 @@ def check_return_device_mismatch(line_items: List[dict]) -> List[TicketIssue]: # nothing to check it against, so nothing to flag here either. continue - has_matching_device = any( + matches_a_device = any( matched_keyword in item.get("item_name", "").lower() for item in device_items ) - if not has_matching_device: + matches_another_shipping_item = any( + matched_keyword in other.get("item_name", "").lower() + for other in return_items + if other is not return_item + ) + if not matches_a_device and not matches_another_shipping_item: issues.append( TicketIssue( code="return_device_mismatch", message=( f"'{return_item.get('sku', '')}' ({return_item.get('item_name', '')}) " - f"expects a matching '{matched_keyword}' device, but none is on this ticket" + f"expects a matching '{matched_keyword}' device or box/label pair, " + "but none is on this ticket" ), ) )