Files
ha-freshharvest/docs/internals.md
T
flan 22e6499a8e
Validate / hassfest (push) Skipped
Validate / HACS (push) Skipped
Upstream compatibility / compat (push) Successful in 8s
Validate / pytest (push) Successful in 11s
Check the signed-in markup from a scheduled workflow
Move the authenticated drift check into the repository as
tools/compat_auth.py, run daily by .github/workflows/compat-auth.yml on the
maintainer's forge only. Credentials come from FH_EMAIL and FH_PASSWORD, the
output is pass/fail labels only because the run log is public, and a failed
run pushes the report to ntfy. Exit 5 means all hold, 10 drift, anything
else that it could not run.

Point compat.yml, tools/compat.py and the docs at it, add a README section,
and cut 0.5.1.
2026-09-21 19:01:13 +00:00

79 lines
3.7 KiB
Markdown

# Internals
How this integration talks to freshharvest.com. Nothing here is needed to *use*
it — see the [README](../README.md) for that. This is for anyone changing the
code, or working out why it broke.
## How it works
Fresh Harvest is not on Shopify, Farmigo, or Local Line — the page metadata
reports `Vy Technology - Custom Code`. It is a server-rendered jQuery site with
no JSON API and no mobile app, so this integration signs in and parses HTML.
Login is a two-step handshake:
1. `GET /s/popup/login` returns the form plus two hidden anti-replay fields,
`LoginSecurity` and `SubmitToken`, minted per session.
2. `POST /s/submit/login` with `LoginEmail`, `LoginPassword`, both tokens, and
an empty `Redirect`, yielding an `fh_session_authenticated` cookie.
The tokens are bound to the cookie issued by step 1, so both requests must
share a cookie jar.
A refresh is three GETs: `/p/dashboard/details` for the delivery day, next
arrival, both upcoming carts and their totals; `/p/dashboard/manage-subscriptions`
for standing orders; and `/p/dashboard/pause-deliveries` for vacation holds.
Three requests every six hours.
Write actions cost more, because nothing can be constructed offline — every
mutating endpoint is guarded by rotating per-render tokens, so each action
fetches the page that offers it, reads fresh tokens, checks they describe the
intended target, and only then submits.
## Markup notes
Six traps, none guessable from the outside:
- **HTTP status means nothing.** Every `/p/*` path returns 200, including
invented ones. Signed-in state is detected by the presence of a Sign Out
control, not by a status code.
- **`cart-contents-skipped` does not mean the order was skipped.** It marks the
locked cart — the one past its cutoff and arriving next. Treating it as
"skipped" reports the wrong delivery as cancelled. The reliable signal for
"can still be changed" is a non-empty `.cart-customize-wrapper`.
- **The free-delivery bar only renders on carts below the threshold.** An order
that already qualifies has no bar at all, so the threshold is read once from
whichever cart shows it and applied to every order.
- **Popups and AJAX replies are fragments, not pages.** They carry no
navigation, so a "am I still signed in?" check based on a Sign Out control
reads every one of them as logged out. Skip could not run at all until these
were fetched with that check disabled.
- **`openPopup` is written both `("x","y")` and `("x", "y")`.** A regex
requiring no space silently matches nothing on the pages that use the other
form — which is every basket page.
- **A `<select>`'s `id` is not its POST field.** The subscribe form's frequency
control is `id='FrequencyID'` but `name='popup-toggle'`. Posting
`FrequencyID` is accepted and does nothing.
## Consistency guarantees
Two invariants hold against the portal's own arithmetic, and tests assert both:
- `next_delivery_box_price` + `next_delivery_add_ons` == `next_delivery_subtotal`
- `open_order_free_delivery_remaining` reaching `0.00` always coincides with a
`0.00` delivery fee
## Drift detection
freshharvest.com has no API and no stability contract — this integration reads
HTML and posts to form endpoints, so a redesign can change what a value *means*
without changing its shape. [tools/compat.py](../tools/compat.py) records every
assumption and CI asserts them against the live site daily, refreshing this
table and opening an issue on drift.
That covers only what anyone can see. The markup behind the login, where every
real break so far has been, is checked by
[tools/compat_auth.py](../tools/compat_auth.py) from a daily workflow that runs
only on the maintainer's forge; see the README.