"""Local label history store. Since the Stamps SERA API has no list-labels endpoint, we persist every created label to appdata/label_history.json at print time. The history window reads from here rather than polling the API. The void/refund and SCAN form actions DO hit the API, using the label_id stored here. """ from __future__ import annotations import json import logging from dataclasses import asdict, dataclass from datetime import datetime from pathlib import Path log = logging.getLogger(__name__) @dataclass class LabelEntry: label_id: str tracking_number: str date_printed: str # ISO 8601 recipient: str # name / first line of address block service: str # display name, e.g. "Ground Advantage" package_type: str amount: float | None notes: str # invoice numbers / free-text notes address_block: str voided: bool = False shipment_status: str = "" date_delivered: str | None = None carrier: str = "" # "usps" | "fedex" @property def date_printed_display(self) -> str: try: return datetime.fromisoformat(self.date_printed).strftime("%m/%d/%Y") except Exception: return self.date_printed @property def date_delivered_display(self) -> str: if not self.date_delivered: return "" try: return datetime.fromisoformat(self.date_delivered).strftime("%m/%d/%Y") except Exception: return self.date_delivered class LabelHistory: """Load / save / query the local label history JSON file.""" def __init__(self) -> None: from .paths import appdata_dir self._path: Path = appdata_dir() / "label_history.json" self._entries: list[LabelEntry] = self._load() # ── Persistence ──────────────────────────────────────────────────────── def _load(self) -> list[LabelEntry]: try: if self._path.exists(): raw = json.loads(self._path.read_text(encoding="utf-8")) return [LabelEntry(**r) for r in raw] except Exception as exc: log.warning("Could not load label history: %s", exc) return [] def _save(self) -> None: try: data = [asdict(e) for e in self._entries] self._path.write_text( json.dumps(data, indent=2, default=str), encoding="utf-8", ) except Exception as exc: log.warning("Could not save label history: %s", exc) # ── Mutation ─────────────────────────────────────────────────────────── def add(self, entry: LabelEntry) -> None: self._entries.insert(0, entry) self._save() def mark_voided(self, label_id: str) -> None: for e in self._entries: if e.label_id == label_id: e.voided = True e.shipment_status = "Voided" self._save() def update_status(self, label_id: str, status: str, date_delivered: str | None = None) -> None: for e in self._entries: if e.label_id == label_id: e.shipment_status = status if date_delivered: e.date_delivered = date_delivered self._save() # ── Query ────────────────────────────────────────────────────────────── def all(self) -> list[LabelEntry]: return list(self._entries) def filter( self, search: str = "", status: str = "All", days: int | None = None, carrier: str = "", ) -> list[LabelEntry]: results = self._entries if carrier: results = [e for e in results if e.carrier == carrier] if days is not None: from datetime import timedelta, timezone cutoff = (datetime.now(timezone.utc) - timedelta(days=days)).isoformat() results = [e for e in results if e.date_printed >= cutoff] if status != "All": if status == "Voided": results = [e for e in results if e.voided] else: results = [e for e in results if not e.voided and e.shipment_status == status] if search: q = search.lower() results = [ e for e in results if q in e.recipient.lower() or q in e.notes.lower() or q in e.tracking_number.lower() ] return results