Stage 2. Completes the path from the SYB API to the archive.
sybimport.Sync pages a date range day by day and folds each detail line
through the existing idempotent ApplyDetail. The paging loop is driven by
listTotal, never by the list response's own total, which live data
confirmed is the page's row count rather than the filtered total. A day
whose completeness cannot be proven — a short page, a total that drifted
while paging, a detail response missing an order — stops the run instead
of reporting a partial import as a whole one. Rows already written stay:
they are idempotent on (order_code, detail_id), so a re-run overwrites.
sybimport.Connect reuses the cached session and only discards it when SYB
explicitly says it is invalid. Caching the numeric user id alongside the
cookies is required, not incidental: session validation calls
/am/user/get?id=, and a wrong id comes back as a business error rather
than a logout, so cookies alone can never be revalidated.
The import endpoint is single-flight and detached from the request
context, so closing the tab cannot abandon a half-finished range. It
reads only; no SYB write endpoint is reachable from GoAuto.
The page swaps its "no import entry" warning for a date-range dialog that
warns when the span is wide — a single week held over 7000 orders — and
reports partial progress when a run fails midway.
Verified: go build, go vet, go test ./... all pass. The drift check was
mutation-tested. Not verified: the MySQL migration for syb_session, and
any browser walkthrough.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Guarded by the syblive build tag, so `go test ./...` still runs entirely
against httptest fakes. Credentials come from the environment; the file
contains none, calls only read endpoints, and logs shapes and counts
rather than order codes, titles or amounts.
Verified against the live API. Confirmed from real data:
- login via OCR works unattended; session is exactly 24h
- §3.1 auth is cookie-based: the jar is populated after login
- §4.3 listTotal is the filtered total (7404 over 7 days) while
list.total is the current page size (20) — a paging loop that
treats the latter as the total stops after one page
- §5.1 productPrice is in yuan, not cents (max observed 403.00)
- §6.1 one order carries nested items, not repeated rows
- details[].id was unique across the batch, closing a gap #41
recorded as unverified
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Port the SYB ERP HTTP client from the upstream cmautobuy project. The
package is stdlib-only (no DB, no gin), so client.go, columns.go, ocr.go
and their httptest suites carry over almost verbatim; only the package
name and doc references changed.
Add on top of the port:
- models.SYBSession + gorm SessionStore, so a restart does not force a
fresh captcha. Cookies are credential-equivalent and carry json:"-".
Expired sessions read as absent because SYB has no rolling renewal.
- config.Extend.SYB. Non-secret settings live in settings.yml;
username and password come only from GOAUTO_SYB_* environment
variables, so no credential lands in a tracked file.
- docs/12-syb-erp-interface.md, the ported interface contract.
The guard tests were mutation-checked: reverting json:"-" and renaming a
settings.yml key each make their test fail.
No import endpoint yet — that needs real credentials and live network
verification, which is Stage 2 of #48.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The blanket "no OCR/VLM" rule was written for Android Agent collection.
Server-side SYB ERP login is a different domain and needs captcha OCR to
run unattended. Scope the prohibition to the Agent side and record the
SYB carve-out, including that captcha images leave the project.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>