Rework of workflow, added the dock back. This is now the beginning of the SKU mismatch issues. FKN FUN TIMES AHEAD
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
"""
|
||||
Ticket validation - flags problems for staff to investigate and
|
||||
(manually, in JIRA, with a reason) cancel if confirmed. This module
|
||||
only ever FLAGS; it never writes anything back to JIRA or changes a
|
||||
ticket's status - that stays a deliberate human step.
|
||||
|
||||
Two rules right now, per the actual business logic:
|
||||
|
||||
1. Company mismatch: a ticket's SKUs resolve to more than one company
|
||||
(SH + OK present together). Simple and unambiguous - every SKU on a
|
||||
ticket should belong to the same company.
|
||||
|
||||
2. Return/device mismatch: a "Shipping - Return Label/Box X" SKU's
|
||||
implied device type doesn't match any other device SKU on the same
|
||||
ticket - covers both asset-recovery (box + return label, same
|
||||
device) and break-fix (asset + matching-type return label) flows.
|
||||
Some return types are exempt (see RETURN_DEVICE_EXEMPT_KEYWORDS) -
|
||||
emailed labels, DPS Device, and scheduled pickups don't have (or
|
||||
need) a matching device line item at all.
|
||||
|
||||
Device-type keywords are shared with app.serial_suggestions
|
||||
(DEVICE_FIELD_SUGGESTIONS) rather than duplicated - same vocabulary,
|
||||
different use.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import List, NamedTuple, Optional
|
||||
|
||||
from app import config
|
||||
from app.companies import parse_mapping, resolve_company_by_sku
|
||||
from app.serial_suggestions import get_device_field_suggestions
|
||||
|
||||
DEFAULT_RETURN_DEVICE_EXEMPT_KEYWORDS = "emailed,dps,scheduled pickup,padded envelope"
|
||||
|
||||
|
||||
class TicketIssue(NamedTuple):
|
||||
code: str
|
||||
message: str
|
||||
|
||||
|
||||
def get_return_device_exempt_keywords() -> list[str]:
|
||||
raw = config.get(
|
||||
"RETURN_DEVICE_EXEMPT_KEYWORDS", DEFAULT_RETURN_DEVICE_EXEMPT_KEYWORDS
|
||||
)
|
||||
return [k.strip().lower() for k in raw.split(",") if k.strip()]
|
||||
|
||||
|
||||
def _is_shipping_item(item_name: str) -> bool:
|
||||
return item_name.strip().lower().startswith("shipping")
|
||||
|
||||
|
||||
def check_company_mismatch(skus: List[str]) -> Optional[TicketIssue]:
|
||||
settings = config.load_settings()
|
||||
sku_map = parse_mapping(settings.get("COMPANY_SKU_MAP", ""))
|
||||
|
||||
companies = set()
|
||||
for sku in skus or []:
|
||||
company = resolve_company_by_sku(sku, sku_map)
|
||||
if company != "Unknown":
|
||||
companies.add(company)
|
||||
|
||||
if len(companies) > 1:
|
||||
return TicketIssue(
|
||||
code="company_mismatch",
|
||||
message=f"SKUs from multiple companies on one ticket: {', '.join(sorted(companies))}",
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def check_return_device_mismatch(line_items: List[dict]) -> List[TicketIssue]:
|
||||
keyword_map = get_device_field_suggestions() # {keyword: [serial fields]} - keys only, here
|
||||
exempt_keywords = get_return_device_exempt_keywords()
|
||||
issues: List[TicketIssue] = []
|
||||
|
||||
device_items = [item for item in (line_items or []) if not _is_shipping_item(item.get("item_name", ""))]
|
||||
return_items = [item for item in (line_items or []) if _is_shipping_item(item.get("item_name", ""))]
|
||||
|
||||
for return_item in return_items:
|
||||
return_text = return_item.get("item_name", "").lower()
|
||||
|
||||
if any(exempt in return_text for exempt in exempt_keywords):
|
||||
continue
|
||||
|
||||
matched_keyword = next((kw for kw in keyword_map if kw in return_text), None)
|
||||
if matched_keyword is None:
|
||||
# The return SKU doesn't mention any known device type at all -
|
||||
# nothing to check it against, so nothing to flag here either.
|
||||
continue
|
||||
|
||||
has_matching_device = any(
|
||||
matched_keyword in item.get("item_name", "").lower() for item in device_items
|
||||
)
|
||||
if not has_matching_device:
|
||||
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"
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
return issues
|
||||
|
||||
|
||||
def validate_ticket(skus: List[str], line_items: List[dict]) -> List[TicketIssue]:
|
||||
issues: List[TicketIssue] = []
|
||||
company_issue = check_company_mismatch(skus)
|
||||
if company_issue:
|
||||
issues.append(company_issue)
|
||||
issues.extend(check_return_device_mismatch(line_items))
|
||||
return issues
|
||||
Reference in New Issue
Block a user