From 6ec06ee31ab8ec1dc4226ba0dae721751f4c9b13 Mon Sep 17 00:00:00 2001 From: flan Date: Mon, 3 Aug 2026 20:35:49 +0000 Subject: [PATCH] Verify basket, subscribe and vacation-hold round trips against a live account MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three fixes the round trips found: The produce-box select reported the SUBSCRIPTION rather than the delivery. A one-off switch changes the delivery while the standing order keeps naming the old box, so during the exact week someone had changed it the entity showed the wrong box. It now reads the order. parse_vacation_holds looked for ISO dates. The page writes 'Tuesday, Dec 1 - Monday, Dec 7', so it reported no holds on an account that had one — which is indistinguishable from having none. Its test asserted the same wrong format, so the test passed while the parser was blind. Adds hold removal (POST /s/submit/pause-range-remove), whose popup only exists while a hold does. --- custom_components/freshharvest/actions.py | 53 ++++++++++++++++++++--- custom_components/freshharvest/select.py | 13 ++++-- tests/test_actions.py | 23 +++++++--- 3 files changed, 74 insertions(+), 15 deletions(-) diff --git a/custom_components/freshharvest/actions.py b/custom_components/freshharvest/actions.py index 6d4391e..575c479 100644 --- a/custom_components/freshharvest/actions.py +++ b/custom_components/freshharvest/actions.py @@ -44,6 +44,7 @@ SUBMIT_DONATE = "/s/submit/donate-basket" SUBMIT_SUBSCRIBE = "/s/submit/item-frequency" SUBMIT_HOLD = "/s/submit/pause-range-add" SUBMIT_BASKET = "/s/submit/select-basket" +SUBMIT_HOLD_REMOVE = "/s/submit/pause-range-remove" BASKET_TYPES = "/p/shop/basket-types" BASKET_GROUPS = ( "georgia-grown-baskets", @@ -162,14 +163,27 @@ def parse_subscriptions(html: str) -> list[Subscription]: def parse_vacation_holds(html: str) -> list[VacationHold]: - """Read the scheduled pauses off /p/dashboard/pause-deliveries.""" + """Read the scheduled pauses off /p/dashboard/pause-deliveries. + + The page renders a hold as "Tuesday, Dec 1 - Monday, Dec 7" — day names and + abbreviated months, never ISO. An earlier version looked for YYYY-MM-DD and + so reported no holds on an account that had one, which is indistinguishable + from having none. + """ soup = BeautifulSoup(html, "html.parser") + account = soup.select_one(".account") + if account is None: + return [] + text = re.sub(r"\s+", " ", account.get_text(" ", strip=True)) holds: list[VacationHold] = [] - for row in soup.select(".account-item-multi-fields, .account-item-container"): - text = row.get_text(" ", strip=True) - found = re.findall(r"\d{4}-\d{2}-\d{2}", text) - if len(found) >= 2: - holds.append(VacationHold(start=found[0], end=found[1], raw=text)) + pattern = re.compile( + r"[A-Z][a-z]+,\s*([A-Z][a-z]{2})\s+(\d{1,2})\s*-\s*" + r"[A-Z][a-z]+,\s*([A-Z][a-z]{2})\s+(\d{1,2})" + ) + for m in pattern.finditer(text): + start = f"{m.group(1)} {m.group(2)}" + end = f"{m.group(3)} {m.group(4)}" + holds.append(VacationHold(start=start, end=end, raw=m.group(0))) return holds @@ -452,6 +466,33 @@ class FreshHarvestActions: async def async_list_vacation_holds(self) -> list[VacationHold]: return parse_vacation_holds(await self._client.async_fetch(DASHBOARD_PAUSE)) + async def async_remove_vacation_hold(self, dry_run: bool = True) -> ActionResult: + """Lift the first scheduled hold. + + Its popup only exists while a hold does, so this raises when there is + nothing to lift rather than posting into the void. + """ + page = await self._client.async_fetch(DASHBOARD_PAUSE) + tok = re.search(r'openPopup\("pause-range-remove",\s*"([^"]+)"', page) + if not tok: + raise FreshHarvestActionError("no scheduled hold to remove") + popup = await self._client.async_fetch( + f"/x/popup/pause-range-remove/{tok.group(1)}", is_page=False + ) + soup = BeautifulSoup(popup, "html.parser") + form = _find_form(soup, SUBMIT_HOLD_REMOVE) + if form is None: + raise FreshHarvestActionError("hold-removal form not found") + payload = _hidden_fields(form) | {"Continue": "Confirm"} + result = ActionResult( + action="remove_vacation_hold", ok=True, + target=re.sub(r"\s+", " ", soup.get_text(" ", strip=True))[:80], + submitted=payload, dry_run=dry_run, detail="lift the scheduled hold", + ) + if not dry_run: + await self._post(SUBMIT_HOLD_REMOVE, payload) + return result + @dataclass class Basket: diff --git a/custom_components/freshharvest/select.py b/custom_components/freshharvest/select.py index 3f79dc8..20040bd 100644 --- a/custom_components/freshharvest/select.py +++ b/custom_components/freshharvest/select.py @@ -56,12 +56,17 @@ class FreshHarvestBoxSelect(FreshHarvestEntity, SelectEntity): @property def current_option(self) -> str | None: - """The subscribed box. + """The box actually arriving in the changeable delivery. - Read from the subscription rather than the switch popups: the box you - are on is the one the site offers no switch control for, so it has no - name there. + NOT the subscription. A one-off switch changes the delivery while the + standing order keeps naming the old box — verified live: after + switching the next delivery to Medium, the subscription still read + Small. Reporting the subscription here would show the wrong box for + exactly the week someone had changed it. """ + order = self.coordinator.data.open_order or self.coordinator.data.next_order + if order is not None and order.box_name: + return order.box_name subs = self.coordinator.data.subscriptions return subs[0].name if subs else None diff --git a/tests/test_actions.py b/tests/test_actions.py index 17143b7..25ba0cb 100644 --- a/tests/test_actions.py +++ b/tests/test_actions.py @@ -73,12 +73,25 @@ def test_no_subscriptions_is_empty_not_an_error(): assert actions.parse_subscriptions("nothing") == [] -def test_vacation_holds_need_two_dates(): - html = "
2026-09-01 to 2026-09-14
" +def test_vacation_hold_uses_the_sites_own_date_format(): + """The page writes "Tuesday, Dec 1 - Monday, Dec 7", never ISO. + + The first version of this test asserted ISO dates, so it passed against a + format the site does not produce while the parser reported zero holds on an + account that had one. + """ + html = ( + "
" + ) holds = actions.parse_vacation_holds(html) - assert len(holds) == 1 and holds[0].start == "2026-09-01" - assert holds[0].end == "2026-09-14" - assert actions.parse_vacation_holds("
none
") == [] + assert len(holds) == 1 + assert (holds[0].start, holds[0].end) == ("Dec 1", "Dec 7") + + +def test_no_holds_parses_empty(): + assert actions.parse_vacation_holds("
none
") == [] def test_frequency_names_map_to_site_values():