@@ -1852,13 +1852,13 @@ def _extract_payment_allocations(
18521852 # Best-effort: switch the history filter from "Last 12 Months" to "All" so older payments
18531853 # are visible when scanning. If this fails, we proceed with the default selection.
18541854 self ._try_select_payment_activity_show_all (page )
1855- cancelled_payment_dates : set [date ] = set ()
1855+ non_posted_dates : dict [date , str ] = {}
18561856 try :
1857- cancelled_payment_dates = self ._cancelled_payment_dates_from_payment_activity_text (page .inner_text ("body" ))
1857+ non_posted_dates = self ._non_posted_payment_dates_from_payment_activity_text (page .inner_text ("body" ))
18581858 except Exception :
1859- cancelled_payment_dates = set ()
1860- if cancelled_payment_dates :
1861- logger .info ("Detected %d cancelled payment entries; skipping them." , len (cancelled_payment_dates ))
1859+ non_posted_dates = {}
1860+ if non_posted_dates :
1861+ logger .info ("Detected %d non-posted payment entries; skipping them." , len (non_posted_dates ))
18621862
18631863 # Primary strategy: click the Payment Date links in the history table (they are the most stable entry point).
18641864 # These appear as links like "11/26/2025".
@@ -1916,8 +1916,12 @@ def _collect_date_texts() -> list[str]:
19161916 )
19171917 break
19181918
1919- if payment_dt in cancelled_payment_dates :
1920- logger .info ("Skipping cancelled payment entry dated %s." , payment_dt .isoformat ())
1919+ if payment_dt in non_posted_dates :
1920+ logger .info (
1921+ "Skipping non-posted payment entry dated %s (status=%s)." ,
1922+ payment_dt .isoformat (),
1923+ non_posted_dates [payment_dt ],
1924+ )
19211925 continue
19221926
19231927 if opened >= max_payments_to_scan :
@@ -1951,11 +1955,19 @@ def _collect_date_texts() -> list[str]:
19511955 )
19521956 except Exception :
19531957 self ._save_debug (page , debug_dir = debug_dir , name_prefix = f"payment_detail_{ idx } _error" )
1954- raise
1958+ logger .warning (
1959+ "Failed to parse payment detail for date %s (idx=%d); skipping row." ,
1960+ dt_str , idx , exc_info = True ,
1961+ )
19551962 finally :
19561963 # Return to Payment Activity list without relying on browser history.
19571964 self ._close_payment_detail (page )
19581965
1966+ if not allocations and opened > 0 :
1967+ raise RuntimeError (
1968+ f"All { opened } payment detail rows failed to parse; aborting. "
1969+ "Check debug artifacts (payment_detail_*_error) for details."
1970+ )
19591971 return allocations
19601972
19611973 # Gather clickable \"View/Details\" elements.
@@ -2047,53 +2059,59 @@ def _try_select_payment_activity_show_all(self, page: Page) -> None:
20472059 except Exception :
20482060 pass
20492061
2050- def _cancelled_payment_dates_from_payment_activity_text (self , body_text : str ) -> set [date ]:
2062+ _NON_POSTED_STATUS_RE = re .compile (
2063+ r"\b(cancel+l?ed|pending|scheduled|processing)\b" , re .I
2064+ )
2065+
2066+ def _non_posted_payment_dates_from_payment_activity_text (
2067+ self , body_text : str
2068+ ) -> dict [date , str ]:
20512069 """
2052- Parse the Payment Activity list view and find payment dates whose status is "Cancelled".
2070+ Parse the Payment Activity list view and find payment dates whose status is non-posted
2071+ (cancelled, pending, scheduled, processing).
20532072
2054- This is used to avoid clicking into cancelled entries, since we only want posted/successful
2055- payment allocations .
2073+ Returns a dict mapping each non-posted date to the matched status keyword so callers
2074+ can log which status caused the skip .
20562075 """
20572076 lines = [ln .strip () for ln in (body_text or "" ).splitlines () if ln .strip ()]
20582077
20592078 date_start_re = re .compile (r"^(\d{1,2}/\d{1,2}/\d{4})\b" )
2060- cancelled_word_re = re .compile (r"\bcancel+l?ed\b" , re .I )
20612079
2062- def _block_is_cancelled (block_lines : list [str ]) -> bool :
2063- # Most commonly the status is its own line ("Cancelled"), but some layouts may inline it.
2064- for ln in block_lines :
2065- if re .fullmatch (r"cancel+l?ed" , ln , re .I ):
2066- return True
2080+ def _non_posted_status (block_lines : list [str ]) -> Optional [str ]:
20672081 for ln in block_lines :
2068- if cancelled_word_re .search (ln ) and "$" in ln :
2069- return True
2070- return False
2082+ m = self ._NON_POSTED_STATUS_RE .search (ln )
2083+ if m :
2084+ return m .group (1 ).lower ()
2085+ return None
20712086
2072- out : set [date ] = set ()
2087+ out : dict [date , str ] = {}
20732088 current_date : Optional [str ] = None
20742089 current_block : list [str ] = []
20752090
20762091 for ln in lines :
20772092 m = date_start_re .match (ln )
20782093 if m :
2079- # Finalize previous block.
2080- if current_date and _block_is_cancelled (current_block ):
2081- try :
2082- out .add (parse_us_date (current_date ))
2083- except Exception :
2084- pass
2094+ if current_date :
2095+ status = _non_posted_status (current_block )
2096+ if status :
2097+ try :
2098+ out [parse_us_date (current_date )] = status
2099+ except Exception :
2100+ pass
20852101 current_date = m .group (1 )
20862102 current_block = [ln ]
20872103 continue
20882104
20892105 if current_date is not None :
20902106 current_block .append (ln )
20912107
2092- if current_date and _block_is_cancelled (current_block ):
2093- try :
2094- out .add (parse_us_date (current_date ))
2095- except Exception :
2096- pass
2108+ if current_date :
2109+ status = _non_posted_status (current_block )
2110+ if status :
2111+ try :
2112+ out [parse_us_date (current_date )] = status
2113+ except Exception :
2114+ pass
20972115
20982116 return out
20992117
0 commit comments