{"id":"eae374bb-678b-4e05-a652-1d01c08f23f8","entityType":"agent","slug":"clawhub-zmtucker-drivethru-adidas-click","name":"drivethru-adidas-click","canonicalUrl":"https://www.xpersona.co/agent/clawhub-zmtucker-drivethru-adidas-click","canonicalPath":"/agent/clawhub-zmtucker-drivethru-adidas-click","generatedAt":"2026-10-10T21:53:18.456Z","source":"CLAWHUB","claimStatus":"UNCLAIMED","verificationTier":"NONE","summary":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-10T17:25:39.199Z","emptyReason":null},"description":"Browser-driven adidas Click B2B toolkit — places purchase orders, runs live inventory / wholesale-pricing checks, and pulls shipment tracking numbers from the adidas Click portal with Playwright, since adidas exposes no public API. Ordering accepts style number, color, size, quantity, a purchase-order number, and a ship-to address, drives the cart/checkout, and (on confirm) submits the order. Checks reuse the same flow but never buy — inventory reads product pages (no cart) and reports the **re-stock date** for anything out of stock or backordered, pricing fills a throwaway \"DO NOT BUY\" cart, reads the priced checkout, and deletes it. Tracking searches the order book by PO number, opens every adidas order for it, and returns each delivery's carrier + tracking number (or, for an order that has not shipped, its expected ship dates). Use whenever the user needs to place/draft a PO on adidas Click, check live stock levels, restock/back-in-stock dates, or wholesale pricing, or find tracking numbers / ship dates for adidas POs.","descriptionLabel":"Source description","evidenceSummary":"Capability contract not published. No trust telemetry is available yet. 1.3K downloads reported by the source. Last updated 10/10/2026.","installCommand":"clawhub skill install s17fq291581evd78xzn1930b0n87bfny:drivethru-adidas-click","sourceUrl":"https://clawhub.ai/zmtucker/drivethru-adidas-click","homepage":"https://clawhub.ai/zmtucker/skills/drivethru-adidas-click","primaryLinks":[{"label":"View on ClawHub","url":"https://clawhub.ai/zmtucker/drivethru-adidas-click","kind":"source"},{"label":"Homepage","url":"https://clawhub.ai/zmtucker/skills/drivethru-adidas-click","kind":"homepage"}],"safetyScore":84,"overallRank":62,"popularityScore":62,"trustScore":null,"claimedByName":null,"isOwner":false,"seoDescription":"drivethru-adidas-click technical dossier on Xpersona with agent coverage, OPENCLEW support, and live trust metadata."},"coverage":{"evidence":{"source":"public-profile","verified":false,"confidence":"medium","updatedAt":"2026-10-10T17:25:39.199Z","emptyReason":null},"protocols":[{"protocol":"OPENCLEW","label":"OpenClaw","status":"self-declared","notes":"Declared in the public agent profile."}],"capabilities":[],"verifiedCount":0,"selfDeclaredCount":1,"capabilityMatrix":{"rows":[{"key":"OPENCLEW","type":"protocol","support":"unknown","confidenceSource":"profile","notes":"Listed on profile"}],"flattenedTokens":"protocol:OPENCLEW|unknown|profile"}},"adoption":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-10T17:25:39.199Z","emptyReason":null},"stars":null,"forks":null,"downloads":1319,"packageName":null,"latestVersion":"0.9.0","tractionLabel":"1.3K downloads"},"release":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-10T17:25:39.199Z","emptyReason":null},"lastUpdatedAt":"2026-10-10T17:25:39.199Z","lastCrawledAt":"2026-10-10T17:25:39.199Z","lastIndexedAt":null,"nextCrawlAt":"2026-10-11T17:25:39.199Z","lastVerifiedAt":null,"highlights":[{"version":"0.9.0","createdAt":"2026-08-21T14:26:06.368Z","changelog":"**Restock date and backorder date reporting added to inventory checks.** - Inventory checks now report restock or back-in-stock dates for out-of-stock or backordered items. - Skill description and documentation updated to include live back-in-stock date support. - Internal schemas and product-page scraping updated to extract restock dates as part of inventory data. - Obsolete file skill-card.md removed.","fileCount":10,"zipByteSize":90002},{"version":"0.8.0","createdAt":"2026-08-20T19:50:04.705Z","changelog":"**New tracking feature: Skill now pulls shipment tracking numbers from adidas Click POs.** - Added get-order-tracking action to search adidas Click orders by PO and extract carrier/tracking numbers or expected ship dates. - SKILL.md updated to document the new delivery tracking capability. - Scripts updated to implement order tracking browser flow. - Inventory/pricing and ordering functionality unchanged. - Removed outdated skill-card.md documentation.","fileCount":10,"zipByteSize":86532},{"version":"0.7.0","createdAt":"2026-08-11T16:50:40.734Z","changelog":"**Summary:** Improved error handling and input options for missing products and system dependencies. - Added on_missing_product option to control flow when a product is not found (pause, skip, or fail). - Enhanced host dependency checks: now automatically runs playwright install-deps and reports missing apt packages if system libraries are unavailable. - Updated action schema and docs to mention on_missing_product in both create-purchase-order and check-inventory-pricing. - Removed obsolete skill-card.md file.","fileCount":10,"zipByteSize":71125},{"version":"0.6.0","createdAt":"2026-07-31T01:24:08.284Z","changelog":"## drivethru-adidas-click v0.6.0 - Major update: completes and validates the full end-to-end adidas Click order flow using Playwright. - Added robust order processing: login, line/carts, checkout, net pricing calculation, and order confirmation steps verified. - Enhanced inventory and pricing checks now reuse the live checkout flow, stopping before actual order placement and cleaning up temporary carts. - Selector and flow references improved for reliability; see updated developer notes in `references/order_flow_notes.md`. - Removed legacy or unused files (`skill-card.md`).","fileCount":10,"zipByteSize":62251},{"version":"0.5.2","createdAt":"2026-07-30T20:54:44.951Z","changelog":"- Bumped version to 0.5.2. - Documentation updated in SKILL.md. - Removed obsolete skill-card.md file. - Internal script improvements in adidas_tools.py.","fileCount":10,"zipByteSize":60692},{"version":"0.5.1","createdAt":"2026-07-13T19:48:54.017Z","changelog":"- Improved Xvfb virtual display handling: now only starts Xvfb on Linux headless hosts; Windows/macOS run headed browser with no extra setup. - Updated environment variable documentation to clarify platform-specific display handling. - Removed the unused skill-card.md file.","fileCount":10,"zipByteSize":60787},{"version":"0.5.0","createdAt":"2026-07-13T18:48:01.656Z","changelog":"**Adds live inventory and wholesale pricing checks, reusing the validated browser flow.** - New `check-inventory-pricing` action: Reads live adidas Click stock and/or wholesale pricing without placing an order. - Inventory check: Reads availability by style/size directly from product pages (no cart interaction). - Pricing check: Creates a throwaway \"DO NOT BUY\" cart, runs checkout for net price (wholesale), then deletes the cart. - Both checks reuse the same browser steps and selectors as the ordering flow; no new scraping logic introduced. - Documentation expanded to explain new actions, inputs, and the exact browser flow/potential side effects.","fileCount":10,"zipByteSize":59965},{"version":"0.2.1","createdAt":"2026-07-07T23:37:57.314Z","changelog":"- Downgrade to version 0.2.1; roll back from 0.4.1. - Browser headless/headed flag rewritten: now defaults to headless; set ADIDAS_CLICK_HEADLESS to a falsey value for headed mode. - Removed the file skill-card.md. - Updated metadata and usage documentation in SKILL.md for new default behaviors and environment variables.","fileCount":10,"zipByteSize":48138}]},"execution":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No published capability contract is available yet."},"installCommand":"clawhub skill install s17fq291581evd78xzn1930b0n87bfny:drivethru-adidas-click","setupComplexity":"low","setupSteps":["Install using `clawhub skill install s17fq291581evd78xzn1930b0n87bfny:drivethru-adidas-click` in an isolated environment before connecting it to live workloads.","No published capability contract is available yet, so validate auth and request/response behavior manually.","Review the upstream CLAWHUB listing at https://clawhub.ai/zmtucker/drivethru-adidas-click before using production credentials."],"contract":{"contractStatus":"missing","authModes":[],"requires":[],"forbidden":[],"supportsMcp":false,"supportsA2a":false,"supportsStreaming":false,"inputSchemaRef":null,"outputSchemaRef":null,"dataRegion":null,"contractUpdatedAt":null,"sourceUpdatedAt":null,"freshnessSeconds":null},"invocationGuide":{"preferredApi":{"snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-zmtucker-drivethru-adidas-click/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-zmtucker-drivethru-adidas-click/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-zmtucker-drivethru-adidas-click/trust"},"curlExamples":["curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-zmtucker-drivethru-adidas-click/snapshot\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-zmtucker-drivethru-adidas-click/contract\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-zmtucker-drivethru-adidas-click/trust\""],"jsonRequestTemplate":{"query":"summarize this repo","constraints":{"maxLatencyMs":2000,"protocolPreference":["OPENCLEW"]}},"jsonResponseTemplate":{"ok":true,"result":{"summary":"...","confidence":0.9},"meta":{"source":"CLAWHUB","generatedAt":"2026-10-10T21:53:18.448Z"}},"retryPolicy":{"maxAttempts":3,"backoffMs":[500,1500,3500],"retryableConditions":["HTTP_429","HTTP_503","NETWORK_TIMEOUT"]}},"endpoints":{"dossierUrl":"https://www.xpersona.co/api/v1/agents/clawhub-zmtucker-drivethru-adidas-click/dossier","snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-zmtucker-drivethru-adidas-click/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-zmtucker-drivethru-adidas-click/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-zmtucker-drivethru-adidas-click/trust"}},"reliability":{"evidence":{"source":"runtime-metrics","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No trust, reliability, or runtime telemetry is available."},"trust":{"status":"unavailable","handshakeStatus":"UNKNOWN","verificationFreshnessHours":null,"reputationScore":null,"p95LatencyMs":null,"successRate30d":null,"fallbackRate":null,"attempts30d":null,"trustUpdatedAt":null,"trustConfidence":"unknown","sourceUpdatedAt":null,"freshnessSeconds":null},"decisionGuardrails":{"doNotUseIf":["Contract metadata is missing or unavailable for deterministic execution."],"safeUseWhen":[],"riskFlags":["missing_or_unavailable_contract","trust_data_unavailable","schema_references_missing"],"operationalConfidence":"low"},"executionMetrics":{"observedLatencyMsP50":null,"observedLatencyMsP95":null,"estimatedCostUsd":null,"uptime30d":null,"rateLimitRpm":null,"rateLimitBurst":null,"lastVerifiedAt":null,"verificationSource":null},"runtimeMetrics":{"successRate":null,"avgLatencyMs":null,"avgCostUsd":null,"hallucinationRate":null,"retryRate":null,"disputeRate":null,"p50Latency":null,"p95Latency":null,"lastUpdated":null}},"benchmarks":{"evidence":{"source":"no-benchmark-data","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No benchmark suites or observed failure patterns are available."},"suites":[],"failurePatterns":[]},"artifacts":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-10T17:25:39.199Z","emptyReason":null},"readme":"Skill: drivethru-adidas-click\n\nOwner: zmtucker\n\nSummary: Browser-driven adidas Click B2B toolkit — places purchase orders, runs live inventory / wholesale-pricing checks, and pulls shipment tracking numbers from the adidas Click portal with Playwright, since adidas exposes no public API. Ordering accepts style number, color, size, quantity, a purchase-order number, and a ship-to address, drives the cart/checkout, and (on confirm) submits the order. Checks reuse the same flow but never buy — inventory reads product pages (no cart) and reports the **re-stock date** for anything out of stock or backordered, pricing fills a throwaway \"DO NOT BUY\" cart, reads the priced checkout, and deletes it. Tracking searches the order book by PO number, opens every adidas order for it, and returns each delivery's carrier + tracking number (or, for an order that has not shipped, its expected ship dates). Use whenever the user needs to place/draft a PO on adidas Click, check live stock levels, restock/back-in-stock dates, or wholesale pricing, or find tracking numbers / ship dates for adidas POs.\n\nTags: latest:0.9.0\n\nVersion history:\n\nv0.9.0 | 2026-08-21T14:26:06.368Z | auto\n\n**Restock date and backorder date reporting added to inventory checks.**\n\n- Inventory checks now report restock or back-in-stock dates for out-of-stock or backordered items.\n- Skill description and documentation updated to include live back-in-stock date support.\n- Internal schemas and product-page scraping updated to extract restock dates as part of inventory data.\n- Obsolete file skill-card.md removed.\n\nv0.8.0 | 2026-08-20T19:50:04.705Z | auto\n\n**New tracking feature: Skill now pulls shipment tracking numbers from adidas Click POs.**\n\n- Added get-order-tracking action to search adidas Click orders by PO and extract carrier/tracking numbers or expected ship dates.\n- SKILL.md updated to document the new delivery tracking capability.\n- Scripts updated to implement order tracking browser flow.\n- Inventory/pricing and ordering functionality unchanged.\n- Removed outdated skill-card.md documentation.\n\nv0.7.0 | 2026-08-11T16:50:40.734Z | auto\n\n**Summary:**\nImproved error handling and input options for missing products and system dependencies.\n\n- Added on_missing_product option to control flow when a product is not found (pause, skip, or fail).\n- Enhanced host dependency checks: now automatically runs playwright install-deps and reports missing apt packages if system libraries are unavailable.\n- Updated action schema and docs to mention on_missing_product in both create-purchase-order and check-inventory-pricing.\n- Removed obsolete skill-card.md file.\n\nv0.6.0 | 2026-07-31T01:24:08.284Z | auto\n\n## drivethru-adidas-click v0.6.0\n\n- Major update: completes and validates the full end-to-end adidas Click order flow using Playwright.\n- Added robust order processing: login, line/carts, checkout, net pricing calculation, and order confirmation steps verified.\n- Enhanced inventory and pricing checks now reuse the live checkout flow, stopping before actual order placement and cleaning up temporary carts.\n- Selector and flow references improved for reliability; see updated developer notes in `references/order_flow_notes.md`.\n- Removed legacy or unused files (`skill-card.md`).\n\nv0.5.2 | 2026-07-30T20:54:44.951Z | auto\n\n- Bumped version to 0.5.2.\n- Documentation updated in SKILL.md.\n- Removed obsolete skill-card.md file.\n- Internal script improvements in adidas_tools.py.\n\nv0.5.1 | 2026-07-13T19:48:54.017Z | auto\n\n- Improved Xvfb virtual display handling: now only starts Xvfb on Linux headless hosts; Windows/macOS run headed browser with no extra setup.\n- Updated environment variable documentation to clarify platform-specific display handling.\n- Removed the unused skill-card.md file.\n\nv0.5.0 | 2026-07-13T18:48:01.656Z | auto\n\n**Adds live inventory and wholesale pricing checks, reusing the validated browser flow.**\n\n- New `check-inventory-pricing` action: Reads live adidas Click stock and/or wholesale pricing without placing an order.\n- Inventory check: Reads availability by style/size directly from product pages (no cart interaction).\n- Pricing check: Creates a throwaway \"DO NOT BUY\" cart, runs checkout for net price (wholesale), then deletes the cart.\n- Both checks reuse the same browser steps and selectors as the ordering flow; no new scraping logic introduced.\n- Documentation expanded to explain new actions, inputs, and the exact browser flow/potential side effects.\n\nv0.2.1 | 2026-07-07T23:37:57.314Z | auto\n\n- Downgrade to version 0.2.1; roll back from 0.4.1.\n- Browser headless/headed flag rewritten: now defaults to headless; set ADIDAS_CLICK_HEADLESS to a falsey value for headed mode.\n- Removed the file skill-card.md.\n- Updated metadata and usage documentation in SKILL.md for new default behaviors and environment variables.\n\nv0.4.1 | 2026-07-07T17:20:13.675Z | auto\n\n- Changed browser launch to default to headed mode (not headless), as adidas's Akamai Bot Manager stalls headless browsers.\n- Skill now automatically starts an Xvfb virtual display for headed operation on hosts without a display (requires the xvfb system package).\n- ADIDAS_CLICK_HEADLESS env var meaning reversed: set to a truthy value to force headless; otherwise, headed mode is used by default.\n- Updated documentation to reflect the new browser and display behavior.\n\nv0.3.0 | 2026-07-07T16:06:12.573Z | auto\n\n- Add support for two new environment variables:\n  - ADIDAS_CLICK_HEADLESS: run the browser headed when set to a falsey value\n  - ADIDAS_CLICK_USER_AGENT: override the default browser User-Agent string\n- Document new environment variables in SKILL.md for improved Akamai anti-bot compatibility and troubleshooting.\n- Bump version to 0.3.0.\n\nv0.2.0 | 2026-07-06T20:24:49.925Z | auto\n\n**Automatic Playwright browser binary installer on first run.**\n\n- No manual browser setup needed: automatically downloads and installs the Playwright Chromium browser binary if missing.\n- On first run, if the browser isn't present, the tool fetches it before execution.\n- If auto-install fails (e.g., missing libraries or network), returns a clear config_error with manual install instructions.\n- Updated docs to describe first-run browser binary installation and user experience.\n\nv0.1.0 | 2026-07-06T12:34:25.864Z | auto\n\nInitial release — browser-driven adidas Click B2B ordering toolkit.\n\n- Automates adidas Click B2B purchase orders using Playwright, due to lack of public API.\n- Accepts style, size, quantity, PO number, and ship-to address; simulates cart/checkout in the browser.\n- Handles full order flow: login, cart creation, product selection, net price calculation, and order submission.\n- Supports dry run mode (`confirm: false`) for review before live order placement.\n- Offers flexible out-of-stock handling options with clear status/results.\n- Credentials can be provided via environment variables or inline JSON.\n\nArchive index:\n\nArchive v0.9.0: 10 files, 90002 bytes\n\nFiles: references/order_flow_notes.md (42555b), scripts/_cli.py (6794b), scripts/adidas_browser.py (144995b), scripts/adidas_client.py (3199b), scripts/adidas_tools.py (14095b), scripts/adidas.py (3110b), scripts/schemas.py (15629b), skill-card.md (2527b), SKILL.md (30596b), _meta.json (141b)\n\nFile v0.9.0:SKILL.md\n\n---\nname: drivethru-adidas-click\ndescription: Browser-driven adidas Click B2B toolkit — places purchase orders, runs live inventory / wholesale-pricing checks, and pulls shipment tracking numbers from the adidas Click portal with Playwright, since adidas exposes no public API. Ordering accepts style number, color, size, quantity, a purchase-order number, and a ship-to address, drives the cart/checkout, and (on confirm) submits the order. Checks reuse the same flow but never buy — inventory reads product pages (no cart) and reports the **re-stock date** for anything out of stock or backordered, pricing fills a throwaway \"DO NOT BUY\" cart, reads the priced checkout, and deletes it. Tracking searches the order book by PO number, opens every adidas order for it, and returns each delivery's carrier + tracking number (or, for an order that has not shipped, its expected ship dates). Use whenever the user needs to place/draft a PO on adidas Click, check live stock levels, restock/back-in-stock dates, or wholesale pricing, or find tracking numbers / ship dates for adidas POs.\nversion: 0.9.0\nemoji: 👟\nhomepage: https://b2bportal.adidas-group.com\nmetadata:\n  openclaw:\n    requires:\n      bins: [python3]\n    envVars:\n      ADIDAS_CLICK_USERNAME:\n        required: false\n        description: >\n          adidas Click B2B portal username (typically an email). Optional —\n          may instead be passed inline in a tool's stdin JSON. Treat as a secret.\n      ADIDAS_CLICK_PASSWORD:\n        required: false\n        description: adidas Click portal password. Optional env cache; treat as a secret.\n      ADIDAS_CLICK_BASE_URL:\n        required: false\n        description: >\n          Override for the adidas Click base URL. Defaults to\n          `https://b2bportal.adidas-group.com`. Set this if the account uses a\n          region-specific host.\n      ADIDAS_CLICK_HEADLESS:\n        required: false\n        description: >\n          Force headless when set to a truthy value (`true`/`1`/`yes`/`on`).\n          Default is HEADED, because adidas's Akamai Bot Manager stalls headless\n          Chromium. On Windows/macOS a headed browser uses the native display, so\n          nothing extra is needed. Only on a **Linux** host with no display server\n          (e.g. a headless container) does the skill auto-start an Xvfb virtual\n          display (requiring the `xvfb` system package); Windows/macOS never need\n          it. Headless will most likely time out at the adidas login page.\n      ADIDAS_CLICK_USER_AGENT:\n        required: false\n        description: >\n          Override the browser User-Agent. Defaults to a current desktop-Chrome\n          UA so adidas's Akamai edge serves the automated browser instead of\n          stalling the response. Only change it if the default UA gets blocked.\n    install:\n      uv:\n        # Browser automation is the only surface. This installs the Playwright\n        # *package*; the Chromium *binary* (a separate ~150 MB download that pip\n        # can't fetch, and OpenClaw has no post-install hook for) is installed\n        # automatically on the first run — see the browser-binary note below.\n        - playwright>=1.40\n---\n\n# adidas Click B2B toolkit\n\nadidas Click is a **B2B ordering portal with no public API**, so this skill\nplaces purchase orders by driving the portal with **Playwright**. Every tool is\nreached through one CLI entrypoint:\n\n```bash\necho '<json-args>' | python3 scripts/adidas.py <action>\n```\n\nThe action is the **first CLI argument**; arguments are a JSON object on\n**stdin**. Each call prints a single JSON object on stdout, or\n`{\"error\": {\"type\": ..., \"message\": ...}}` with a non-zero exit code on failure.\n\nPlaywright is imported lazily, so the skill loads without it. The Playwright\n**package** is a declared `uv` dependency; its Chromium **browser binary** (a\nseparate ~150 MB download that pip/uv doesn't fetch, and for which OpenClaw has\nno install-time hook) is **installed automatically on the first run** if missing\n— so no manual `python -m playwright install chromium` is needed. That first run\ntherefore includes a one-time browser download.\n\n> **Host system libraries.** Chromium also needs OS-level shared libraries\n> (`libglib-2.0.so.0`, `libnss3`, …). On a missing-libraries launch failure the\n> tool best-effort runs `playwright install-deps` (needs root), then retries. If\n> the host still lacks them it returns a `config_error` listing the required apt\n> packages. Installing system libraries needs root, so on a locked-down agent\n> host this must be handled by the **environment** — the OpenClaw environment's\n> setup script or base image should install Chromium's deps (`python -m\n> playwright install --with-deps chromium`, or the apt packages the error\n> lists). The skill can fetch the browser binary at runtime, but it cannot\n> install OS packages without root.\n\n> **Build status — full order flow implemented.** The skill drives a purchase\n> order end to end: login → add line quantities to the active cart → checkout\n> (Customer PO, delivery location — default / saved / one-time dropship — and\n> shipping method) → Next → **Calc. Net Price** (waits for the \"Done!\" tell so\n> the wholesale discount always applies) → **Order Now** → confirmation number\n> parsed from the redirect URL. `confirm: false` is a full dry run; `confirm:\n> true` **places a real order** (no sandbox). The assembled flow has been built\n> and unit-checked selector-by-selector but not yet run against the live site in\n> one pass — watch the first real end-to-end run. See\n> [`references/order_flow_notes.md`](references/order_flow_notes.md) for the step\n> map and captured selectors.\n>\n> **Order tracking (`get-order-tracking`)** is a separate, read-only surface\n> (the order book / order detail / delivery tracking pages) built from captured\n> HTML: order-book search by PO → order detail → **Delivery Tracking** table, or\n> the article rows' expected ship dates when nothing has shipped. It has been\n> exercised end to end against a local replica of those captured pages, but not\n> yet against the live portal — watch the first real run. See the \"Delivery\n> tracking\" section of\n> [`references/order_flow_notes.md`](references/order_flow_notes.md).\n>\n> **Inventory / pricing checks (`check-inventory-pricing`)** reuse the same\n> captured steps — product navigation + size read (inventory) and, for pricing,\n> the throwaway-cart → checkout → **Calc. Net Price** walk — but stop before\n> **Order Now** and delete the cart. No new selectors: it composes the existing,\n> live-validated ordering steps, so its browser surface rides on the same\n> capture. See the \"Inventory & pricing checks\" section below.\n\n## When to use this skill\n\nReach for `create-purchase-order` when the request involves placing (or\ndrafting) a purchase order on **adidas Click**: given a style number, color,\nsize, quantity, a PO number, and a ship-to address, put the order into the\nportal cart/checkout and — on explicit confirmation — submit it.\n\nReach for `check-inventory-pricing` when the request is to look up **live stock\nlevels** and/or **wholesale (net) pricing** on adidas Click **without buying\nanything** — e.g. *\"how much JW4306 is in stock in L?\"* or *\"what's our\nwholesale cost on 24× JW4306 M?\"*. It reuses the ordering flow but never places\nan order (see below).\n\nReach for `get-order-tracking` when the request is about **shipments** for one\nor more adidas PO numbers — *\"where's PO P13434?\"*, *\"get me the tracking\nnumbers for these POs\"*, *\"has P13433 shipped yet?\"*. It searches the order\nbook per PO, opens each adidas order behind it, and returns the carrier +\ntracking number for every delivery — or, when an order has not shipped, its\n**expected** ship dates. It is read-only: no cart, no order, no writes.\n\nDo **not** use it for other footwear/apparel vendors, and never invent portal\nselectors from prose — the flow lives in the captured\n[`references/order_flow_notes.md`](references/order_flow_notes.md).\n\n## Actions\n\n| Action | Risk | stdin JSON (key fields) |\n| --- | --- | --- |\n| `create-purchase-order` | **high — portal write (browser)** | `{purchase_order: {po_number, lines: [{style, size, quantity}], ship_to?, delivery_location_id?, ship_method?, on_insufficient_stock?, on_missing_product?, spread_delivery?}, confirm}` |\n| `check-inventory-pricing` | **medium — read-only intent, but a pricing check briefly creates then deletes a throwaway cart (browser)** | `{check: \"inventory\"\\|\"pricing\"\\|\"both\", lines: [{style, size?, quantity?}], po_number?, on_missing_product?}` |\n| `get-order-tracking` | **low — read-only (browser); nothing is created or modified** | `{po_numbers: [\"P13434\", \"P13433\"]}` |\n\nRun `python3 scripts/adidas.py` with no action to print the action list.\n\n### `create-purchase-order` input\n\nThe order can be passed nested under `purchase_order` or as top-level fields:\n\n```json\n{\n  \"purchase_order\": {\n    \"po_number\": \"PO-12345\",\n    \"lines\": [\n      {\"style\": \"JW4306\", \"size\": \"M\", \"quantity\": 24},\n      {\"style\": \"JW4306\", \"size\": \"L\", \"quantity\": 12}\n    ],\n    \"ship_to\": null,\n    \"delivery_location_id\": null,\n    \"ship_method\": null,\n    \"spread_delivery\": false,\n    \"on_insufficient_stock\": \"pause\",\n    \"on_missing_product\": \"pause\",\n    \"notes\": null\n  },\n  \"confirm\": false\n}\n```\n\nEach line is `{style, size, quantity}`. **`color` is optional** — adidas article\nnumbers encode the color (e.g. `JW4306` = the black colorway), so navigating to\nthe article lands on the right color with no picker.\n\nDelivery location precedence: `delivery_location_id` (pick a saved location) >\n`ship_to` (add a one-time / dropship location) > default (leave the cart's\npreset). A one-time `ship_to.state` accepts a 2-letter abbreviation or the full\nname. `ship_method` is left at the cart default unless set, and accepts a FedEx\nservice code (`FDGP`, `FEDE`, `FED4`, …) or its exact label (\"FedEx Ground\").\n`po_number` is capped at **18 characters** and may contain only letters, numbers,\nspace, and `/ _ . ? &`; any other character (e.g. a hyphen) is auto-replaced with\n`_` and the substitution is reported in the result's `warnings`.\n\nBy default (`new_cart: true`) the run **creates its own fresh cart** (named with\nthe PO) right after login and makes it active, so on a shared account it never\nadds to — or checks out — a teammate's cart. If a cart with the same PO name\nalready exists (e.g. a leftover from an errored prior run) it is **deleted\nfirst** so the run never piles onto stale quantities — only carts with that exact\nPO name are removed, and any deletion is noted in `warnings`. Pass\n`new_cart: false` to use the current active cart instead.\n\nPass `screenshot_path` to capture the filled page for review.\n\n## Out-of-stock handling (read this)\n\nBefore entering quantities, the tool reads each size's availability (scrolling\nthe horizontal size row as needed to load off-screen sizes). A size is either\n**backorderable** (stock below the requested quantity, incl. `0` with a restock\ndate) or **not available** (the \"X\" cell — will never be restocked, cannot be\nordered at all; it can only be removed or substituted). When a line's **full\nquantity is not available**, behavior depends on `on_insufficient_stock`:\n\n- **`pause`** (default) — **nothing is ordered** (even with `confirm: true`). The\n  result comes back `status: \"needs_confirmation\"` with an `out_of_stock` list\n  (`{style, size, requested, available}`) and a `message`. The agent must **stop\n  and confirm with the user**, then re-run.\n- **`order`** — order the short line(s) anyway, accepting **delayed delivery**\n  (adidas spreads the backordered portion over future dates).\n- **`skip`** — **remove** the out-of-stock line(s) and order the rest.\n\n### Re-stock dates (always report them)\n\nA **backorderable** size is one adidas will replenish, and the portal says when:\nthe size tile carries a small calendar icon whose tooltip reads *\"Re-stock in\nNov 8, 2026\"*. The driver reads that date for every short line and returns it as\n`restock_date` (as shown) plus `restock_date_iso` (`2026-11-08`) — on each\n`out_of_stock` entry, on the order's line results, and on every check line.\n\n**Answer inventory and purchase requests with it.** \"3XL is out of stock\" is not\na usable answer on its own; \"3XL is out of stock, back **Nov 8, 2026**\" is. So\nwhenever a size comes back short or out of stock, state its restock date in the\nreply — and when the field is `null`, say *\"adidas posted no restock date\"*\nrather than leaving the reader to assume one exists. The per-line `note` already\nphrases both cases.\n\nTwo sizes never carry a date, and the difference matters:\n\n| Case | `status` | Restock date |\n| --- | --- | --- |\n| Out of stock / short, replenishment scheduled | `backorder` / `short` | the date — report it |\n| Out of stock / short, nothing scheduled | `backorder` / `short` | `null` — say \"no restock date posted\" |\n| The portal's **\"X\"** cell | `unavailable` | `null` **by definition** — it is never coming back, so offer removal or a substitute, never a date |\n\n> **Cost note.** The date lives only in a hover tooltip, so the driver hovers the\n> icon — one hover per **short** size. In-stock sizes are never hovered, so a\n> fully-in-stock check costs nothing extra.\n\n**Agent guidance:** if the user's request pre-authorizes a choice, set the flag\nand skip the pause — e.g. *\"go ahead and order anything out of stock\"* →\n`on_insufficient_stock: \"order\"`; *\"remove any out-of-stock items without\nasking\"* → `on_insufficient_stock: \"skip\"`. Otherwise leave it at `pause`; on a\n`needs_confirmation` result, **message the user** with the `out_of_stock` details\nand the three choices, wait for their answer, then resume:\n\n1. Order them anyway (delayed delivery) → re-run with `on_insufficient_stock: \"order\"`.\n2. Don't order them → re-run with `on_insufficient_stock: \"skip\"`.\n3. **Substitute** (e.g. a different size/style so items match) → edit the\n   `lines` accordingly and re-run (optionally with a policy for any still-short\n   substitutes).\n\n`spread_delivery` is the lower-level knob behind `order`: on a per-line spread\nprompt, `false` (default) declines (single delivery), `true` accepts.\n\n## Missing / unlisted product handling (read this)\n\nA style adidas has **no product listing** for — a mistyped or wrong article\nnumber, a base style missing its color code, or an article this account simply\nis not offered — is **not an error by default**: it comes back as an escalation\nso the caller can ask the user, exactly like the out-of-stock pause. Behavior is\nset by `on_missing_product`:\n\n- **`pause`** (default) — **nothing is ordered** (even with `confirm: true`).\n  The result is `status: \"needs_confirmation\"` with a `missing_products` list and\n  a `message`. Stop and ask the user.\n- **`skip`** — drop the missing style's line(s) and order the rest (the dropped\n  lines come back with `quantity: 0` and a `not offered …` note).\n- **`error`** — the pre-0.7 behavior: fail the run with an `api_error`.\n\nEach `missing_products` entry is\n`{style, sizes, requested, reason, detail}`. **`reason` matters:**\n\n| `reason` | Meaning | How to treat it |\n| --- | --- | --- |\n| `not_found` | The portal said so (a \"no results\" page, or it bounced off the product URL). | The style is genuinely not on this account. |\n| `unresolved` | The product page never rendered a size table **and** never said the product was missing. | **Unconfirmed, not proven absent** — could be a slow page or a portal change. Worth one retry before telling the user the style doesn't exist. |\n\n**Agent guidance:** on a `needs_confirmation` result, message the user with the\n`missing_products` details and these choices, then resume:\n\n1. **Correct the article number** → edit `lines` and re-run. adidas article\n   numbers encode the colorway (`JW4306`), so a bare style without the color\n   code will not resolve — this is the most common cause.\n2. **Drop it and order the rest** → re-run with `on_missing_product: \"skip\"`.\n3. **Substitute** a different article → edit `lines` and re-run.\n\nIf the user pre-authorized a choice (\"just skip anything adidas doesn't carry\"),\nset the flag up front and skip the pause. Do **not** re-run the same unchanged\nstyle expecting a different answer on a `not_found` reason.\n\nA check (`check-inventory-pricing`) never aborts on a missing style: the other\nlines are still read and priced, the missing ones come back as\n`status: \"not_found\"` lines, and the result's status becomes\n`needs_confirmation` (default) so the caller escalates — `on_missing_product:\n\"skip\"` downgrades that to a warning instead.\n\n> **Timing.** A missing product is detected by polling the product page and\n> bailing on the portal's own \"no results\" tell, capped at 15s — it does not sit\n> on the full 30s selector timeout per bad style. A wrong **size** on a style\n> that *does* exist is different: that raises immediately with the list of sizes\n> the style offers, because the product page loaded fine.\n\nThe result reports `total_quantity` (summed pieces, on dry runs too) and, on a\nplaced order (`confirm: true`), each line's net `line_total` and `unit_price`\n(net ÷ quantity) plus the order `order_total` (net wholesale), read from the\npriced review page.\n\n## Inventory & pricing checks (`check-inventory-pricing`)\n\nA **read-only lookup** that reuses the ordering flow but **never places an\norder**. Pick the mode with `check`:\n\n| `check` | What it does | Cart? |\n| --- | --- | --- |\n| `inventory` | Reads each line's live size-tile stock level straight off the product page, plus the **re-stock date** for anything out of stock (see \"Re-stock dates\" above). | **No cart created** — inventory needs no add-to-cart. |\n| `pricing` | Fills a **throwaway cart**, advances to the priced checkout screen (the *only* place wholesale net pricing shows), runs **Calc. Net Price**, reads the net unit/line prices + order total, then **deletes the cart**. | Yes — created, priced, deleted. |\n| `both` (default) | `pricing` plus the inventory levels read while filling the cart. | Yes — same as `pricing`. |\n\n```json\n{\n  \"check\": \"both\",\n  \"lines\": [\n    {\"style\": \"JW4306\", \"size\": \"M\", \"quantity\": 24},\n    {\"style\": \"JW4306\", \"size\": \"L\"}\n  ]\n}\n```\n\nEach line is `{style, size?, quantity?}`:\n\n- **`size` optional (inventory only):** omit it (or pass `\"*\"` / `\"all\"`) to\n  report **every** size of that style. For a pricing line a specific `size` is\n  needed (a price is per size).\n- **`quantity` optional:** defaults to `1`. It only affects a **pricing** line's\n  *line total* (unit price is the same regardless); an inventory read reports the\n  raw level independent of quantity.\n\n**Why a cart at all for pricing?** adidas only reveals the discounted **wholesale\nnet price** on the final checkout screen, after **Calc. Net Price** runs — there\nis no price API and no price on the product page beyond a \"from\" figure. So a\npricing check has to fill the cart and walk to that screen, exactly like an\norder, then stop before **Order Now** and delete the cart.\n\n**The \"DO NOT BUY\" marker.** A pricing check names its throwaway cart **and** the\nCustomer PO with a `DO NOT BUY {random}` marker (e.g. `DO NOT BUY 7F3K9`) so that\nif a run dies mid-flight, the leftover cart is unmistakably safe. The full\n*\"AUTOMATED CHECK - DO NOT PURCHASE - …\"* wording does **not fit**: adidas hard-\ncaps the Customer PO at **18 characters** and allows only letters, numbers,\nspace, and `/ _ . ? &`, so this is the clearest imperative that still leaves room\nfor a 5-char random suffix (cart names must be unique). Override the marker with\n`po_number` (still ≤18 chars, same charset).\n\n**Never pauses on stock.** Because a check never buys and deletes its cart, it\ndoes not stop on short/out-of-stock lines: a `pause` policy is upgraded to\n`order` so every orderable line still gets priced. Sizes that are flatly *not\navailable* (the portal's \"X\" cell) are reported with no price.\n\n**Never dies on an unlisted style.** A style adidas has no product page for is\nreported as a `not_found` line plus a `missing_products` entry; every other line\nis still read and priced. See \"Missing / unlisted product handling\" above.\n\n**Result** (`status: \"checked\"`, or `needs_confirmation` when a style was not\nfound): a `lines` list of\n`{style, size, color, requested_quantity, available, available_count, status\n(\"in_stock\"|\"backorder\"|\"unavailable\"|\"not_found\"), in_stock, restock_date,\nrestock_date_iso, unit_price, line_total, note}`, plus `order_total` (summed net, pricing only),\n`total_quantity`, `missing_products`, `po_number` (the marker used, `null` for\ninventory-only), and `cart_deleted` (`true`/`false` once a pricing check tried to\nremove its cart; `null` when no cart was made). If the throwaway cart could not\nbe auto-deleted (e.g. it was the account's only cart), a `warning` says so and\nnames it — remove it in the portal.\n\n## Order tracking (`get-order-tracking`)\n\nA **read-only** lookup that turns PO numbers into tracking numbers. It reuses\nthe same headed-browser login as the other actions, logs in **once**, then walks\nthe POs **one at a time**:\n\n1. **Search the order book** by PO —\n   `/adidas/reorder/my/order-book?searchText={PO}&page=0&size=20&filterByRDD=false`.\n   One PO commonly maps to **several** adidas orders; every result row is read\n   (the grid virtualizes rows, so the list is scrolled to the end, and further\n   pages are fetched by bumping `page` until nothing new comes back).\n2. **Open each order** — `/adidas/reorder/my/order-book/{order}`.\n3. **If the order has a Delivery Tracking link** (`#OrderTrackingButtonLink`),\n   click it and read the delivery table: **delivery note, ship date, carrier,\n   tracking number** (+ the carrier's tracking URL). An order can have several\n   deliveries, and one delivery note can carry more than one parcel — every row\n   is returned.\n4. **If there is no Delivery Tracking link, nothing has shipped.** The order's\n   article rows are expanded (the chevron toggle) and the **expected ship\n   dates** are read instead — reported as expected everywhere they appear, never\n   mixed in with real shipments.\n\n```json\n{\"po_numbers\": [\"P13434\", \"P13433\"]}\n```\n\n`po_numbers` accepts a list, or a single comma/whitespace-separated string;\n`po_number` (one PO) also works. Duplicates are de-duplicated.\n\n**Result** (`status: \"checked\"`, or `needs_confirmation` — see below):\n\n| Field | What it holds |\n| --- | --- |\n| `pos[]` | One entry per requested PO: `{po_number, status, orders[], order_count, shipment_count, note}`. PO `status` is `shipped` / `partial` / `not_shipped` / `unreadable` / `not_found`. |\n| `pos[].orders[]` | `{order_number, po_number, order_type, status, shipments[], expected_ship_dates[], expected_ship_date, note}`. Order `status` is `shipped` / `not_shipped` / `unreadable`. |\n| `pos[].orders[].shipments[]` | `{delivery_note, ship_date, carrier, carrier_name, tracking_number, tracking_url}`. `carrier` is adidas's raw code (`UPSN`); `carrier_name` is resolved from the tracking link's host (UPS, FedEx, …). |\n| `expected_ship_dates[]` | `[{article, dates: [...]}]` — only on an order that has **not** shipped. `expected_ship_date` is the earliest of them. |\n| `table` | A ready-to-render **Markdown table** of every PO / order / tracking number, with unshipped rows annotated `*(expected)*`. |\n| `total_orders`, `total_shipments`, `not_found_pos`, `warnings` | Run-level summary. |\n\n**Reporting back to the user:** hand back the `table` field (optionally\nre-formatted) rather than re-deriving one — it already groups by PO and marks\nwhich dates are *expected* rather than actual:\n\n| PO | Order # | Delivery Note | Ship Date | Carrier | Tracking # | Note |\n| --- | --- | --- | --- | --- | --- | --- |\n| P13434 | 6279266468 | 7342219301 | Aug 3, 2026 | UPS | 1Z2AT4600373532427 | shipped |\n| P13434 | 6279266469 | — | Feb 4, 2027 *(expected)* | — | not shipped yet | not shipped — expected ship date (IK1234) |\n\n**When the result is `needs_confirmation`** the lookup is **incomplete** —\neverything reported is accurate, but something is missing:\n\n- **`not_found_pos`** — the order book returned no order carrying that PO.\n  Usually a wrong/mistyped PO number. adidas's search also matches PO\n  *prefixes*, so if the search surfaced orders for *other* POs, a `warning`\n  names them (e.g. searching `P1343` surfaces `P13433` / `P13434`) — those are\n  **not** tracked, because they are not the PO that was asked for. Take the PO\n  number back to the user.\n- **an `unreadable` order** — the order page never rendered. That order's status\n  is unknown (it is never reported as \"not shipped\"); a `warning` names it. Worth\n  one retry before telling the user anything about it.\n\n**Security note:** each delivery row also carries a PDF download link whose URL\nembeds a **bearer access token**. That link is deliberately not read or\nreturned — do not go fetch it.\n\n## Credentials\n\nThe adidas Click portal login can be supplied three ways (**later wins**):\n\n1. **Environment variables** (preferred for a deployed agent):\n\n   ```bash\n   ADIDAS_CLICK_USERNAME=...\n   ADIDAS_CLICK_PASSWORD=...\n   ADIDAS_CLICK_BASE_URL=https://b2bportal.adidas-group.com   # override for region host\n   ```\n\n2. **Inline in the stdin JSON** — pass `username`, `password`, and `base_url`\n   alongside the tool's own arguments.\n\n3. **CLI flags** — `--username` / `--password` / `--base-url` after the action.\n   Handy for local runs with no env vars and no secrets in the order JSON:\n\n   ```bash\n   echo '{\"purchase_order\": {...}, \"confirm\": false}' \\\n     | python3 scripts/adidas.py create-purchase-order --username \"$U\" --password \"$P\"\n   ```\n\n   > Command-line arguments are visible to other processes (`ps`) and your shell\n   > history — for sensitive/automated use, prefer env vars or the stdin JSON.\n\nIf no credentials are present via any of these, the tool exits with\n`{\"error\": {\"type\": \"config_error\", ...}}` (exit code 2) — treat that as a\nsignal to ask the user for the missing fields. Never guess or reuse credentials\nacross accounts, or paste secrets the user did not provide.\n\n## Write safety\n\n`create-purchase-order` is the only order-placing action.\n\n- It requires `\"confirm\": true` to place the order. Without it, it logs in,\n  fills the cart + checkout, and returns a **dry-run** preview **without\n  submitting**. Confirm with the user before placing.\n- Placing an order on adidas Click is a **real purchase** — there is no\n  sandbox. Only run `confirm: true` for an order the user actually intends to\n  place.\n\n`get-order-tracking` is **read-only**: it only navigates and reads order-book,\norder, and delivery-tracking pages. No cart is created, nothing is ordered, and\nno portal state changes.\n\n`check-inventory-pricing` **never places an order** — there is no `confirm`\npath. `inventory` mode touches no cart at all. `pricing`/`both` create a\nthrowaway `DO NOT BUY …` cart only to reach the priced checkout screen, stop\n**before** Order Now, and **delete the cart** on the way out. It does mutate\nportal state briefly (the cart is created and removed), so it is not purely\nread-only, but it cannot buy anything.\n\n## Reachability & bot mitigation\n\nadidas Click sits behind **Akamai Bot Manager**, which fingerprints the client\nand *stalls the HTTP response* (rather than cleanly refusing it) for traffic\nthat looks automated. The tell is a navigation that hangs to a timeout\n(`Page.goto: Timeout … exceeded`) even though the network path is healthy — a\nbrowser-UA `curl` to the same host returns `200` in well under a second, while\na bare `curl` or a default headless Chromium (UA contains `HeadlessChrome`,\n`navigator.webdriver=true`) gets tarpitted.\n\nA spoofed User-Agent alone is **not** enough: a headless Chromium is still\nfingerprinted (TLS/JA3, `HeadlessChrome` internals, no GPU/canvas) and tarpitted\neven with a real UA. The reliable path is a **headed** browser on a **virtual\ndisplay**, which presents an ordinary browser fingerprint. So the skill:\n\n- runs **headed by default** (set `ADIDAS_CLICK_HEADLESS=true` to force headless,\n  which will most likely time out at the login page);\n- launches Chromium with a real desktop-Chrome User-Agent, a normal\n  viewport/locale, `--disable-blink-features=AutomationControlled`, a\n  `navigator.webdriver` mask, and a longer navigation timeout;\n- when headed on a **Linux** host with no display server, **auto-starts an Xvfb\n  virtual display** (spawned directly, so no `xauth` needed) and points\n  `$DISPLAY` at it — this requires the **`xvfb` system package** in the image\n  (`apt-get install -y xvfb`). This is Linux-only: the skill first checks the\n  platform, so on **Windows/macOS** (native GUI) and on a Linux host that already\n  has an X11/Wayland display, it launches headed directly and **never requires\n  Xvfb**.\n\nOverrides: `ADIDAS_CLICK_USER_AGENT` if the default UA ages out. If headed on a\nvirtual display *still* stalls, the block is at the **egress IP** (a datacenter\nIP Akamai distrusts) — route through an IP adidas allowlists, the same way\n`drivethru-sanmar` documents needing its caller IP allowlisted. No browser/code\nchange fixes an IP-reputation block.\n\n## Error model\n\nFailures print `{\"error\": {...}}` and exit non-zero:\n\n- `config_error` (exit 2) — missing/invalid credentials, a bad policy value, or\n  an un-captured step.\n- `api_error` — the portal rejected an action or a page did not match\n  expectations. Includes `surface`, `operation`, `retryable`. Note that a style\n  adidas does not carry is **not** an `api_error` by default — it comes back as\n  a `needs_confirmation` result (see \"Missing / unlisted product handling\"), and\n  only becomes an `api_error` under `on_missing_product: \"error\"`.\n- `connection_error` — network / browser-launch / navigation failure\n  (`retryable: true`).\n- `validation_error` — bad input JSON or a missing required field.\n- `usage` / `unknown_action` (exit 2) — bad CLI invocation; the message lists\n  the valid `actions`.\n\nSurface the human-readable `message` to the user. Do not retry on\n`config_error` or `validation_error`.\n\n## References\n\n- [`references/order_flow_notes.md`](references/order_flow_notes.md) — the\n  reverse-engineered portal flows: the ordering step map and captured selectors,\n  plus the read-only **delivery tracking** walk (order book → order → deliveries\n  / expected ship dates). **Start here to continue the build.**\n\nFile v0.9.0:_meta.json\n\n{\n  \"ownerId\": \"kn715tnf30wegyr6mdbfa17avd87bjr6\",\n  \"slug\": \"drivethru-adidas-click\",\n  \"version\": \"0.9.0\",\n  \"publishedAt\": 1787322366368\n}\n\nFile v0.9.0:references/order_flow_notes.md\n\n# adidas Click \"place a purchase order\" — reverse-engineered flow (working notes)\n\n> Captured interactively from the live adidas Click B2B portal. These notes\n> back the Playwright-driven `create-purchase-order` action. Selectors are what\n> the driver targets; anything dynamic is parameterized from stdin JSON.\n>\n> **Surface:** the adidas Click B2B ordering portal\n> (`https://b2bportal.adidas-group.com`). Driver creds:\n> `ADIDAS_CLICK_USERNAME` / `ADIDAS_CLICK_PASSWORD` (base URL override\n> `ADIDAS_CLICK_BASE_URL`).\n\n---\n\n## ⏯️ RESUME HERE (read this first)\n\n**Status:** ✅ Complete. All steps and sub-paths are implemented — the skill\ndrives a purchase order end to end: login → add line quantities to the active\ncart → set Customer PO / delivery location (default, saved, or one-time dropship\nwith State dropdown) / shipping method (code or label) → Next → Calc. Net Price\n(wait for \"Done!\") → Order Now → parse the confirmation number from the redirect\nURL. `confirm: false` is a full dry run; `confirm: true` **places a real order**\n(no sandbox).\n\n**Validated:** a real order has been placed end to end against the live site\n(cart dedup → product → size qty → checkout → Calc Net Price → Order Now →\nconfirmation number; net `order_total` confirmed). Several live-only issues were\nfixed along the way: login tell (Logout hidden in a dropdown), cart-name\nuniqueness + can't-delete-active-cart, lazy-rendered size tiles, and the cart\n\"Next\" button inactive until scrolled.\n\n**Method (progressive HTML capture — same as SanMar's `process-return`):** we\nbuild one step at a time. For each step the user pastes the page **URL** and the\nrelevant **HTML** (the form / table / controls), Claude extracts the selectors,\nimplements the driver method, and flips that step's `*_IMPLEMENTED` flag.\n\n**Build complete** — nothing left to capture for ordering. A second action,\n`check-inventory-pricing`, was added on top of these steps (inventory read /\nwholesale-pricing lookup that never orders) — see \"Inventory / pricing checks\"\nbelow. It captured **no new selectors**; it reuses steps 1–4.\n\nA third action, `get-order-tracking`, drives a **different, read-only surface**\n(order book → order detail → delivery tracking) and *did* capture new selectors\n— see \"Delivery tracking\" at the end of this file. It shares only Step 1\n(login) with the ordering flow.\n\n### Step map / gates (`scripts/adidas_browser.py`)\n\n| Step | Gate flag | Driver method | Status |\n| --- | --- | --- | --- |\n| 1. Login | `LOGIN_IMPLEMENTED` | `login()` | ✅ implemented |\n| 2. Add lines (product nav, size map, inventory, qty input) | `ADD_LINES_IMPLEMENTED` | `add_lines()` | ✅ implemented |\n| 3. Checkout (PO + delivery + shipping) | `CHECKOUT_IMPLEMENTED` | `fill_checkout()` | ✅ implemented (incl. dropship + shipping) |\n| 4. Checkout → calc net price → order now | `FINAL_SUBMIT_IMPLEMENTED` | `price_cart()` + `complete_submission()` | ✅ implemented |\n| Check. Inventory / pricing (no order) | — (reuses 1–4) | `read_inventory()` / `price_cart()` / `delete_cart()` / `check_inventory_pricing()` | ✅ implemented |\n| Track. Delivery tracking (read-only) | — (reuses 1 only) | `find_orders_for_po()` / `open_order()` / `read_delivery_tracking()` / `read_expected_ship_dates()` / `get_order_tracking()` | ✅ implemented (live run pending) |\n\n### Insufficient-availability decision point ✅ IMPLEMENTED\n\n`add_lines` reads availability for **every** line first (`_prepare_lines` opens\neach product once and reads the size-tile inventory indicator; `_parse_available`\ntreats `\"300+\"`/blank as sufficient, exact numbers as the count). A line is\n**short** when its exact available count < requested. Then per\n`OrderRequest.on_insufficient_stock`:\n\n- **`pause`** (default) + any shortfall → raise `_OrderPause`; the\n  entry point returns `status=\"needs_confirmation\"` with `out_of_stock`\n  (`[{style, size, requested, available}]`) and a `message` listing the choices,\n  and places **nothing** (even with `confirm: true`, because this happens before\n  the submit gate). The cart may hold nothing (short found in the read pass) — a\n  re-run's `new_cart` dedup cleans up regardless.\n- **`order`** → enter the short line anyway with the spread accepted (delayed\n  delivery); line note records it.\n- **`skip`** → do not enter the short line (quantity 0, \"removed\" note); order the\n  rest. If **all** lines are skipped → error (\"nothing to order\").\n\nThe **agent** turns a `needs_confirmation` result into a user message, then\nre-runs with `on_insufficient_stock: \"order\"` / `\"skip\"`, or **substitutes** by\nediting `lines` and re-running. Pre-authorizing phrases in the user's prompt\n(\"order anything out of stock\" / \"remove out-of-stock items\") let the agent set\nthe flag up front and skip the pause. See SKILL.md \"Out-of-stock handling\".\n\n### Missing-product decision point ✅ IMPLEMENTED\n\nSame shape, second axis: a style adidas serves **no product page** for is a\ndecision for the caller, not a crash. `_open_product(style, missing_ok=True)`\nreturns False and records the style in `driver.missing_products`\n(`[{style, sizes, requested, reason, detail}]`); `_prepare_lines` marks that\nstyle's lines `not_found`. Then per `OrderRequest.on_missing_product`:\n\n- **`pause`** (default) → the same `_OrderPause` (it carries both\n  `out_of_stock` and `missing_products`), so the entry point returns\n  `status=\"needs_confirmation\"` and places **nothing**.\n- **`skip`** → drop those lines (quantity 0, `not offered …` note), order the\n  rest. All lines missing → the \"No lines could be ordered\" error names the\n  styles.\n- **`error`** → `missing_ok=False`, so `_open_product` raises `AdidasAPIError`\n  (the pre-0.7 behavior).\n\n`check_inventory_pricing` forces `skip` internally (a check reports what it can\nread and never aborts mid-run) and then re-escalates itself: if anything landed\nin `missing_products`, the result flips to `status=\"needs_confirmation\"` unless\nthe caller asked for `skip`. Its `add_lines` fallback also catches\n`AdidasAPIError` now, so a line that fails to resolve can never strand the\nthrowaway `DO NOT BUY` cart.\n\nDetection details and the still-uncaptured not-found markup: see \"Style adidas\ndoes not carry\" under Step 2. See SKILL.md \"Missing / unlisted product\nhandling\".\n\n---\n\n## Step 1 — Login (`/login`)  ✅ IMPLEMENTED\n\nStandard username/password form at `https://b2bportal.adidas-group.com/login`.\nTitle \"Welcome to Click\". Implemented in `AdidasClickDriver.login()`.\n\n| Element | Selector | Notes |\n| --- | --- | --- |\n| Form | `#loginFormDsk` | POSTs to `/login` (`method=post`) |\n| Hidden | `input#queryString` (`name=queryString`) | dynamic; filling the live form carries it — never hardcode |\n| Username | `#usernameField` (`name=username`) | ← `ADIDAS_CLICK_USERNAME` |\n| Password | `#passwordField` (`name=password`) | ← `ADIDAS_CLICK_PASSWORD` |\n| Remember me | `#form-reminder` (checkbox) | left unchecked |\n| Submit | `#send2Dsk` (\"Login\", `type=submit`) | the real button — fill + click so JS validation/CSRF come along |\n| Bad-creds alert | `#login-error-alert` (\"Invalid username or password.\") | starts `display:none`; JS flips it visible **without navigation** |\n| SSO error alert | `#sso-login-error-alert` | SSO-only; also checked |\n| SSO button | `#send2SSO` (\"Single Sign On for Sales Managers\", `type=button`) | **separate IdP flow — not used by this skill** |\n| Forgot links | `#forgot-password-link` (`/forgotPassword`), `#forgot-username-link` (`/forgotUsername`) | not used |\n\n**Driver behavior:**\n- Fill visible fields → click `#send2Dsk` → `wait_for_load_state(\"networkidle\")`.\n- **Bad-credentials tell:** `#login-error-alert` (or `#sso-login-error-alert`)\n  becomes visible in place → raise `config_error`.\n- **Success tell (provisional):** a valid login **navigates away from `/login`**.\n  If `#loginFormDsk` is still present at a `/login` URL with no error → raise\n  `config_error` (possible SSO-only account or blocking interstitial).\n\n**Confirmed post-login (from the landing-page header capture):**\n- **Landing URL:** `/adidas/reorder/home`. No cookie / T&C interstitial appears.\n- **Logged-in tell:** wait for an always-visible authenticated shell control —\n  `_LOGGED_IN_MARKER` = `#ReorderNavLink, #CartOverviewNavLink,\n  #HeaderSearchButtonButton, #PersonalNavigationDropdownLabel` (any visible).\n  ⚠️ **Do NOT use `#LogoutNavLink`** — it lives inside the collapsed account\n  dropdown (`#PersonalNavigationDropdownList`), so it is present but *hidden*;\n  a visibility wait on it times out even though login succeeded (observed as a\n  false \"login did not complete\" on a real run).\n\n## Step 2 — Add product lines  *(IN PROGRESS)*\n\n### Step 2a — Header search (✅ wired in `_search_style`)\n\nGlobal site header `.o-siteHeader`. The search box lives in\n`section.o-siteHeader__searchBox` and is **collapsed by default** — click the\nmagnifier to reveal the input; the submit button is `disabled` until text is\nentered.\n\n| Element | Selector | Notes |\n| --- | --- | --- |\n| Open (magnifier) | `#HeaderSearchButtonButton` | expands the collapsed search |\n| Input | `#HeaderSearchSearchInput` (`name=search`) | placeholder \"Search for Article Numbers, Colors, Brand types\" — the **style / article number** goes here |\n| Submit | `#HeaderSearchSubmitButton` (`type=submit`) | starts `disabled`; enables once the input has text |\n| Close | `#HeaderSearchCloseButton` | not used |\n\nOther useful header nav (for later steps / fallbacks):\n`#CatalogNavLink` → `/adidas/reorder/catalog`,\n`#CartOverviewNavLink` → `/adidas/reorder/cart-overview`,\n`#OrderBookNavLink` → `/adidas/reorder/my/order-book`,\n`#QuickAddTileButton` (a \"Quick Add\" side panel — a possible bulk-entry\nalternative to search),\n`#HeaderCartsTileButton` (cart side panel).\n\n### Navigation — direct product URL (✅ preferred, `_open_product`)\n\n- **Search** lands on a results page regardless of exact match:\n  `/adidas/reorder/catalog?searchTerm={style}`.\n- **Recommended:** navigate straight to the product —\n  `/adidas/reorder/product/{STYLE}` (e.g. `/adidas/reorder/product/JW4306`).\n  **adidas article numbers encode the color** (JW4306 = \"M FLEECE CREW BLACK\"),\n  so there is **no color picker** — landing on the URL is landing on the color.\n  `_open_product` uses this and waits for `#CartModule-SizeTable`.\n\n#### Style adidas does not carry (⚠️ tell NOT captured — text-matched)\n\nA style this account isn't offered (or a wrong article number) has **no size\ntable to wait for**. The portal's empty/not-found product markup has **not been\ncaptured**, so — deliberately, rather than inventing a selector — `_open_product`\ndecides with two signals it can trust:\n\n1. **URL**: after `goto`, the page is no longer under `/reorder/product/`\n   (the portal bounced it) → `reason: \"not_found\"`.\n2. **Text**: the visible body matches `_PRODUCT_MISSING_TELL_RE` (\"no results\",\n   \"not found\", \"no longer available\", …) once `_LOGGED_IN_MARKER` has painted\n   → `reason: \"not_found\"`.\n\nNeither firing within `_PRODUCT_WAIT_MS` (15s, polled every 400ms rather than\none blocking `wait_for_selector`, so a bad style doesn't burn the full 30s\ntimeout) gives `reason: \"unresolved\"` — reported as *unconfirmed*, never as\n\"this style doesn't exist\". A false positive is safe by construction: it can\nonly route the style into the `on_missing_product` escalation, which places\nnothing and asks the user.\n\n**Next capture:** open a style the account isn't offered and record the real\nnot-found page's id/markup, then swap the text match for that selector (keeping\nthe text match as a fallback).\n\n### Step 2a — Availability date gate (✅ wired)\n\nAbove the size table sits a draggable date-tab strip\n(`#DateTabs-DraggableTable`); the active tab is `#DateTabButton` and its inner\ntext is a formatted date (e.g. `\"Jul 31, 2026\"`). This is the **earliest date\nthe product can ship**, and the size-tile inventory numbers refer to _that_\ndate. If it isn't today, the size table is describing future stock —\n**nothing is orderable now** regardless of what the tiles show.\n\n`_product_available_today()` reads `#DateTabButton`, parses `%b %d, %Y`, and\ncompares to today's date. Both the order flow (`_prepare_lines`) and the\ninventory read (`read_inventory`) call it right after `_open_product` and\nbefore touching any tile; when it returns False, every requested line for that\nstyle is classified `unavailable` with a `\"not available until {date}\"` note\nand the size-tile read is skipped. Fail-open on a missing/unparseable date\n(logs a warning) so a portal shape change doesn't silently block every order.\n\n### Step 2b — Size table (✅ mapping + inventory wired)\n\nContainer `#CartModule-SizeTable`. ⚠️ **The grid sits below the fold and\nlazy-renders its per-size tiles only when scrolled into view** — the size\n*labels* (SizeTranslation cells) render eagerly, but the\n`#CartModule-SizeTile-{style}-{code}` tiles do not attach until the table enters\nthe viewport. `_open_product` calls `_ensure_size_tiles_rendered(style)`\n(scrollIntoView center → wait for `[id^=\"CartModule-SizeTile-{style}-\"]`, with a\nnudge-scroll fallback) before any tile interaction, so a headless run (or an\nunscrolled headed run) doesn't time out on a not-yet-rendered tile.\n\nA horizontally-scrolling **size-run grid**: one column per size, a per-size cell\nper article row.\n\n⚠️ **Off-screen sizes lazy-load** — only the visible sizes' cells are in the DOM;\nthe rest render when the row's **arrow buttons** are clicked\n(`#CartModuleSizeBarNavigationForward{group}Button` /\n`…Back{group}Button`, `{group}` dynamic — targeted by id-prefix). `_ensure_size_\nin_view(style, code)` resets to the leftmost then scans forward (clicking the\nforward arrow, polling) until the size's wrapper cell\n(`#CartModule-SizeRow-SizeTile-Wrapper-{style}-{code}`) attaches. Called before\nclassifying a size and before entering its quantity.\n\n**Per-size cell shapes** (`_classify_size` → `ok` | `short` | `unavailable`):\n- **Orderable:** wrapper contains `#CartModule-SizeTile-{style}-{code}` (the\n  quantities div) + `#CartModule-SizeTile-InventoryIndicator-{style}-{code}`.\n  Stock `0` **with a restock date** still lives here — it's backorderable\n  (`short` when available < requested).\n- **Not available:** wrapper contains `#CartModule-SizeTile-Status-NotAvailable-\n  {style}-{code}` (the \"X\" cell, hashed class `notAvailable--…`) and **no**\n  quantity input — it will never be restocked and **cannot be ordered**\n  (`unavailable`). Dropped under every policy with a \"not available\" note; under\n  `order` it still can't be placed (only remove/substitute).\n\n> ⚠️ **Use stable ids, not classes.** The `*--xxxxx` classes (`sizeTile--jfyHK`,\n> `sizeTranslation--xFkgj`, …) are hashed CSS-module names that change on every\n> build. All selectors below use the semantic `id` prefixes.\n\n**Size label → numeric code** (from the size-bar header cells):\n`#CartModule-SizeBar-SizeTranslation-{group}-{code}` with the label in an\n`<ins>`. `{group}` is a conversion-group id (`51` in the sample). Observed map:\n\n| Label | Code | | Label | Code | | Label | Code |\n| --- | --- | --- | --- | --- | --- | --- | --- |\n| XS | 210 | | 3XL | 320 | | 3XLT | 410 |\n| S | 230 | | 4XL | 330 | | 4XLT | 420 |\n| M | 250 | | 5XL | 340 | | 5XLT | 430 |\n| L | 270 | | LT | 380 | | LT2 | 450 |\n| XL | 290 | | XLT | 390 | | XLT2 | 460 |\n| 2XL | 310 | | 2XLT | 400 | | 2XT2 | 470 |\n\n`_size_code_map()` reads these live (never hardcodes) into `{label: code}` and\nresolves the requested size label to its code.\n\n**Per-style, per-size tile** (`{style}` = article number, `{code}` = size code):\n\n| Element | Selector |\n| --- | --- |\n| Quantities cell | `#CartModule-SizeTile-{style}-{code}` |\n| Inventory text | `#CartModule-SizeTile-InventoryIndicator-{style}-{code}` (`300+`, `160`, `0`, …) — in-stock tiles also carry a `hasInventory--…` class (hashed; not used) |\n| Restock icon | `#CartModule-SizeTile-RestockIndicator-{style}-{code}` |\n| Article total qty | `#CartModule-MaterialRow-Summary-TotalQuantity-{style}` |\n| Article total price | `#CartModuleMaterialRowSummaryTotalPrice{style}Price` |\n| Product name | `#CartModule-TinyProduct-{style}-ProductName` (e.g. \"M FLEECE CREW BLACK\") |\n| Wholesale price | `#CartModuleTinyProductPrice{style}Price` (\"Wholesale Price from $22.50\") |\n\n`_read_inventory()` surfaces the availability text; `add_lines()` groups lines\nby style, opens each product once, and fills all its sizes.\n\n### Step 2b-2 — Re-stock date (✅ IMPLEMENTED, hover-read)\n\nA size that adidas will replenish carries a small calendar icon in the tile's\ntop-right corner. Captured markup:\n\n```html\n<div class=\"restockIcon--O65EH \" id=\"CartModule-SizeTile-RestockIndicator-KD5434-240\">\n  <div class=\"m-restockWeekIndicator\">\n    <svg viewBox=\"0 0 24 24\" class=\"a-icon -calendarInactive\">…</svg></div></div>\n```\n\nThe id follows the same `{style}-{code}` convention as the other tile ids\n(`_SIZE_TILE_RESTOCK_ID`), so its **presence** is a reliable \"this size gets\nrestocked\" tell. The **date is not in that markup** — the portal renders it only\nin a tooltip on hover (*\"Re-stock in Nov 8, 2026\"*).\n\n`_read_restock_date` therefore:\n\n1. checks `title` / `aria-label` / `data-tooltip` / `data-title` on the icon and\n   its descendants first (free, in case a build exposes it as an attribute);\n2. otherwise hovers the icon, waits 400 ms, and matches the page text against\n   `_RESTOCK_TOOLTIP_RE` — anchored on the word \"re-stock\" so an unrelated date\n   elsewhere on the page can never be picked up. The **tooltip's own markup is\n   not captured**, so no selector is invented for it;\n3. moves the mouse to (0, 0) in a `finally` so the tooltip closes and the next\n   size reads its own date, not this one fading out.\n\nCalled **only for a `short` size** (`_classify_size`), so an in-stock check\ncosts no hovers, and never for the `X` cell — a flatly unavailable size has no\nrestock date by definition.\n\nSurfaced as `restock_date` (as shown) + `restock_date_iso` (via `_iso_date`,\nwhich reuses the tracking flow's `%b %d, %Y` parser) on `OrderLineResult`,\n`CheckLineResult`, and each `out_of_stock` entry, and woven into the per-line\n`note` and the `needs_confirmation` message — including the explicit \"no restock\ndate posted\" wording when adidas showed none.\n\n### Step 2c — Quantity input (✅ IMPLEMENTED)\n\nClicking a size tile opens a **single shared floating overlay** `#quantityInput`\n(absolutely positioned via inline `left`/`top` over the active cell). Typing a\nvalue + **Enter** commits it; the overlay closes and the ordered qty renders in\nthe cell. Entering a quantity **is** adding to the working cart — everything is\nunder the `CartModule` namespace, and there is **no separate add-to-cart button**\non the product page.\n\n| Element | Selector | Notes |\n| --- | --- | --- |\n| Overlay | `#quantityInput` | shared; repositions over the clicked tile |\n| Quantity field | `#quantityInput input` | react-numeric-input, **no id of its own**; `.fill()` + `press(\"Enter\")` to commit |\n| Decrease / increase | `#QuantityInputDecreaseButton` / `#QuantityInputIncreaseButton` | not used (we type the value) |\n| **Committed qty readback** | `#CartModule-SizeTile-OrderedInDate-{style}-{code}` | appears in the cell after commit (e.g. \"10\") — the driver reads this to confirm |\n| Committed cell state | tile gains hashed class `orderedInThisDate--…` | hashed; not used |\n\n**Insufficient availability** → dialog `#CartModule-SizeItemProposals`\n(\"Insufficient availability — Do you want to spread your quantity over the\nfollowing dates?\"):\n\n| Button | Selector |\n| --- | --- |\n| Yes, spread delivery | `#CartModuleSpreadAcceptButton` |\n| No, just one delivery | `#CartModuleSpreadDeclineButton` |\n| Close | `#CartModuleSizeItemProposalsOverlayCloseButton` |\n\nDriver policy (`OrderRequest.spread_delivery`, default **false**): declines the\nspread (single delivery). `true` accepts it. `_handle_availability_proposal`\nshort-waits (2s) for the dialog after each commit and clicks accordingly;\n`_read_ordered_quantity` then reports what actually committed, and `add_lines`\nnotes any shortfall on the line result.\n\n> **Cart behavior (confirmed):** adidas Click supports **multiple carts**.\n> Quantities entered on the product page land in the **active** cart.\n> `/adidas/reorder/cart` opens that active cart. When a size's requested qty\n> exceeds availability **and the spread is declined**, adidas moves the overflow\n> into a **new, inactive cart** (\"A new cart with the additional quantities has\n> been created\") — it defaults to inactive, so it does not interfere with\n> checking out the active cart.\n\nImplemented in `_set_size_quantity` / `_handle_availability_proposal` /\n`_read_ordered_quantity`; `ADD_LINES_IMPLEMENTED = True`.\n\n## Step 2.5 — Fresh cart per run  ✅ IMPLEMENTED (`create_new_cart`)\n\nadidas Click supports **multiple carts**, and on a **shared account** entering\nquantities on a product page adds to whatever cart is *active* — which risks\npiling onto (and checking out) a teammate's cart. So by default\n(`new_cart: true`) the driver creates its own cart first, right after login.\n\nOpen the carts side panel with `#HeaderCartsTileButton`, then:\n\n| Element | Selector | Notes |\n| --- | --- | --- |\n| New Cart | `#CreateNewCartButton` | opens the name form |\n| Name field | `form.o-editCartForm input.m-input__field` | no own id; `maxlength=25`; same charset as the PO (`/ _ . ? &`) |\n| Save | `#EditCartSaveButton` (`type=submit`) | creates the cart **and switches the active cart to it** |\n| Cancel | `#EditCartCancelButton` | not used |\n\nThe cart **name is the personalReference — i.e. the Customer PO** (the carts\nlist column `personalReference` is headed \"Cart Name\"). The driver names the new\ncart with the (sanitized) PO. Pass `new_cart: false` to reuse the active cart.\n\n**Delete-first, then create.** Two hard rules interact: (a) **cart names must be\nunique** — the New-Cart **Save button stays `disabled`** if a cart with that name\nalready exists (so you can't create-then-delete); and (b) **the active cart\ncan't be deleted.** So the driver deletes any same-named leftover *first*, and if\nthat leftover is the active cart, activates a different cart before deleting it.\n\nFlow (`create_new_cart` → `_delete_carts_named`):\n1. `_cart_rows()` snapshots the carts **ag-grid** (`.o-headerCartsList`): per row\n   the `row-id`, name (`a.a-link[title=…]`, title = cart name = PO), active flag\n   (`.o-activeBadge` present), and activate-toggle id\n   (`div.a-toggle[id$=\"ToggleActiveCartToggle\"]`).\n2. `old_ids` = rows whose title == PO. If none, skip to create.\n3. If a same-named cart **is active**, click another cart's toggle\n   (`[id=\"{cartId}ToggleActiveCartToggle\"]`) to make it active — the active row\n   has no toggle. (If the leftover is the *only* cart → clear error; empty/rename\n   manually or `new_cart:false`.)\n4. `_delete_cart_rows(old_ids)` — tick each `div[role=row][row-id=…]\n   input.ag-checkbox-input` → mass-actions bar (`.o-agGridHeader.-massActions`)\n   appears → trash `#DeleteAllCartsButton` → confirm `#DeleteCartConfirmButton`\n   (\"Yes\").\n5. Now the name is free: click `#CreateNewCartButton`, fill the name, and — after\n   waiting for `#EditCartSaveButton:not([disabled])` (fails fast + clear if it\n   stays disabled) — Save. Saving activates the new cart.\n\nOnly carts with this **exact** PO name are matched — never a teammate's\ndifferently-named cart. Deletions are reported in the result `warnings`. (ag-grid\nvirtualizes rows, so this covers same-PO dups in the rendered set — normally one.)\n\n> Carts panel also exposes each cart's list price via `.o-cartPanelPriceRenderer`\n> and an active badge (`.o-activeBadge`) / activate toggle\n> (`#{cartId}ToggleActiveCartToggleInput`); the mass-actions bar also has\n> `#CartSubmissionButton` / `#DuplicateAllCartsButton` / `#CopyAllCartsToButton` /\n> `#DownloadAllCartsButton` / `#AnalyticsOfCartsButton` — not used here.\n\n## Step 3 — Checkout: PO + delivery + shipping  ✅ IMPLEMENTED\n\nNavigate to `/adidas/reorder/cart` (opens the **active** cart) and wait for the\nheader `#CartModule-CartHeader`. The three order settings sit in a row\n(`ul.orderSettings--…`). Heading `#CartHeaderHeading` = \"My Cart\"; the\n\"This is not a confirmed order\" note (`#CartModule-CartHeader-notConfirmedOrder`)\nconfirms nothing is placed yet.\n\n### Customer PO # (✅ `_set_customer_po`)\n\n| Element | Selector | Notes |\n| --- | --- | --- |\n| Field | `#CartModule-PersonalReference-InputField input` | **no own id**; `maxlength=18`; **defaults to a random string** that must be cleared + replaced each time |\n\nDriver validates length ≤ 18, `fill()`s the PO (replacing the default), presses\nEnter + blurs, then reads `input_value()` back to confirm it stuck.\n\n### Delivery Location (✅ default / saved / one-time)\n\nDropdown label `#DeliveryAddressOptionsDropdownLabel` shows the current location\n(id `6017069000` + name). Defaults to the account preset — **left untouched\nunless** a different location or a dropship is requested. Precedence:\n`delivery_location_id` > `ship_to` > default.\n\n- **Open** → `#DeliveryAddressOptionsDropdownContent`, containing:\n  - a search input (`input[type=search]`; note its id is a dynamic\n    `{number}SearchInput`, so target by type within the content),\n  - **Add one-time delivery location** button `#DeliveryAddressAddOneTimeShipToButton`,\n  - saved options `#Option{locId}` with button `#OptionButton{locId}Button`\n    (e.g. `#OptionButton6017069000Button`).\n- **Saved** (`_select_saved_location`): open → click `#OptionButton{id}Button`.\n- **One-time / dropship** (`_add_one_time_location`): open → click add button →\n  modal form `#CartModule-DeliveryAddress-OneTimeShipToAddressForm`:\n\n  | Field | Selector | Maps from `ship_to` |\n  | --- | --- | --- |\n  | Attention 1* | `#Attention1InputField` | `name` |\n  | Attention 2 | `#Attention2InputField` | `attention` |\n  | Street Address* | `#StreetInputField` | `address1` (+ `, address2`) |\n  | City/Town* | `#CityTownInputField` | `city` |\n  | State* (dropdown) | `#StateInputFieldDropdownLabel` | `state` — ⚠️ option markup **not captured** (see below) |\n  | ZIP code* | `#ZipcodeInputField` | `zip` |\n  | Country | *(static \"UNITED STATES\")* | fixed US |\n  | Submit | `#DeliveryAddressFormSubmitButton` (\"USE THIS ADDRESS\") | |\n\n  Rules from the form: mandatory fields marked `*`; Latin characters only;\n  **PO-Box addresses are cancelled**.\n\n### Shipping Method (✅ `_select_shipping_method`)\n\nDropdown label `#ShippingMethodsOptionsDropdownLabel` (defaults \"Default\"). Open →\n`#ShippingMethodsOptionsDropdownContent`. Each option is\n`<div class=\"option {CODE}\" id=\"Option{CODE}\"><button id=\"OptionButton{CODE}Button\"><span>{label}</span></button>`.\n`ship_method` accepts the **4-letter code** (stable id) or the **exact label**:\n\n| Code | Label | | Code | Label |\n| --- | --- | --- | --- | --- |\n| `DFLT` | Default | | `FED4` | FedEx 3 Day |\n| `FDGP` | FedEx Ground | | `FESO` | FedEx Next Business Day |\n| `FDGR` | FedEx Ground Residential Only | | `FEDN` | FedEx Next Business Day 10:30 |\n| `FEDE` | FedEx 2 Day | | `FEDY` | FedEx Next Business Day Residential Only |\n| `FEDZ` | FedEx 2 Day Residential Only | | `FEDB` | FedEx Saturday Delivery |\n\n### State dropdown (✅ `_select_state`)\n\nInside the one-time-address modal. Label `#StateInputFieldDropdownLabel`; open →\n`#StateInputFieldDropdownContent`. Options are\n`<button class=\"m-button -dropdown\" id=\"OptionButton{NameNoSpaces}Button\"><span>{Full State Name}</span></button>`\n— **full names** (\"Tennessee\", \"New York\", \"District of Columbia\"), plus\nterritories and Armed Forces entries. `_resolve_state_name` maps a 2-letter\nabbreviation (`OR`→Oregon, `TN`→Tennessee, …) to the full name; a full name\npasses through. Matched by **exact** text (so \"Virginia\" ≠ \"West Virginia\").\n\nBoth dropdowns: options are `button.m-button` elements; clicking one selects and\ncloses. A search input is present but not needed (all options are in the DOM).\n`_click_option_by_exact_text` iterates the option buttons and clicks the exact\n(case-insensitive whole-string) text match.\n\n## Step 4 — Checkout → calc net price → place order  ✅ IMPLEMENTED\n\n⚠️ Real submit — **no sandbox**; `confirm: true` places a real PO.\n`complete_submission()` runs the full sequence and `FINAL_SUBMIT_IMPLEMENTED` is\n**True**.\n\n**Sequence (captured controls):**\n\n| # | Action | Selector | Notes |\n| --- | --- | --- | --- |\n| 4a | **Next** (cart → checkout) | `#CartModuleCheckoutProgressNPCButton` | navigates to `/adidas/reorder/checkout`; **stays inactive until the cart body is scrolled into view** — the driver `_activate_by_scroll`s it (scroll-into-view + nudge + wait-for-enabled) before clicking |\n| 4b | **Calc. Net Price** | `#NPCCartSimulation` (`<a role=button>` in the summary-table header; also per row) | **applies our wholesale discounts — MUST run every time and finish before ordering; it is slow** |\n| 4c | **Order Now** | `#CartModuleCheckoutProgressBarSubmitOrderButton` | final submit |\n\n**Totals (✅ `_read_checkout_totals`):** after Calc, each order row on the review\npage has a net-price cell `#OrderReviewShardTotalsNetPrice{N}` (N = 1-based row\nindex) whose **first `<span>` is the net price** (e.g. `$35.08`) and second is\nthe retail comparison (`$45.00`). The driver collects the per-row nets, assigns\neach to its result line (`line_total`, positional — review rows follow entry\norder), derives `unit_price` = `line_total` ÷ quantity (`_unit_price`), and sums\nthe nets into `order_total` (`_sum_currency`). Best-effort; never\nblocks the order. `total_quantity` is computed from the committed line\nquantities (available on dry runs too). `order_total` / per-line `line_total`\nonly populate on `confirm: true` (they require the Calc step).\n\n**Calc-complete tell (✅ captured, `_await_net_price_calc`):** when 4b finishes,\n`#NPCCartSimulation` is replaced by a **\"Done!\"** message in the summary-table\nheader cell (`<span class=\"OrderConfirmationOrderSummaryRunNPCMessageDone--…\">Done!</span>`\n— class hashed, so match the **text**). The driver waits for the button to\ndetach and for \"Done!\" to appear (120s timeout) and **raises without ordering**\nif \"Done!\" never shows — so 4c never fires at list price. The same summary header\nalso carries TOTAL ORDER PROPOSALS / TOTAL ARTICLES / TOTAL RETAIL PRICE\n($45.00) / TOTAL PRICE ($17.54 net) — useful to scrape onto the result later.\n\n**Confirmation (✅ captured):** Order Now **auto-redirects** to\n`/adidas/reorder/order/{orderNumber}/confirmation` (e.g. order `25709165`) — no\nintermediate confirm dialog. `_read_confirmation_number` parses the number\nstraight from that URL (`_CONFIRMATION_URL_RE`); `complete_submission` waits for\n`**/adidas/reorder/order/*/confirmation` after Order Now. A **Qualtrics feedback\npopup** (\"rate your experience\", NPS 0–10) opens afterward — it is **ignored**\n(reading the main page URL is unaffected; no interaction needed).\n\n### Design decisions (locked in the scaffolding)\n\n- Final submit gated behind `confirm: true` (mirrors SanMar's\n  `create-purchase-order`) **and** `FINAL_SUBMIT_IMPLEMENTED`.\n- `confirm: false` → fill + validate + `dry_run` preview (nothing placed).\n- Credentials never hardcoded; env (`ADIDAS_CLICK_*`) or inline stdin JSON.\n- Input model: `{po_number (≤18 chars), lines: [{style, size, quantity, color?}],\n  ship_to?: {...}, delivery_location_id?, ship_method?, spread_delivery?,\n  requested_ship_date?, notes?}` (see `scripts/schemas.py`). `color` is optional\n  (the article number encodes it); `spread_delivery` (default false) declines the\n  over-availability spread; delivery precedence is\n  `delivery_location_id` > `ship_to` > default.\n\n## Inventory / pricing checks (`check-inventory-pricing`)  ✅ IMPLEMENTED\n\nA **read-only lookup** that reuses the ordering steps above and **never places\nan order** (there is no `confirm` path). Entry point:\n`adidas_browser.check_inventory_pricing()`; tool `adidas_check_inventory_pricing`;\nresult models `CheckResult` / `CheckLineResult` in `scripts/schemas.py`. **No new\nselectors were captured** — it composes the same live-validated steps, so it\ninherits their capture. The one refactor: the order flow's `complete_submission`\nwas split so its cart→checkout→**Calc. Net Price**→read-totals half is a reusable\n`price_cart()` that both the order flow (before Order Now) and the pricing check\n(before deleting the cart) call.\n\n**Modes (`check` arg):**\n\n| Mode | Path | Cart |\n| --- | --- | --- |\n| `inventory` | `read_inventory()` — open each product (`_open_product`), read the size-tile inventory indicator via `_classify_size` (the same read the order flow does in `_prepare_lines`), classify `in_stock` / `backorder` / `unavailable`. A blank `size` (or `\"*\"`/`\"all\"`) expands to **every** size of the style (iterate `_size_code_map()`). | **None** — reads product pages only. |\n| `pricing` | `create_new_cart(po)` → `add_lines(req)` → `fill_checkout(req)` (sets the DO-NOT-BUY Customer PO) → `price_cart()` (Next → Calc. Net Price → `_read_checkout_totals`) → `delete_cart(po)`. Stops **before** `#CartModuleCheckoutProgressBarSubmitOrderButton` (Order Now). | Throwaway; created, priced, deleted. |\n| `both` | Same as `pricing`; the per-line inventory indicators read during `add_lines` are surfaced alongside the prices. | Same as `pricing`. |\n\n**Cart cleanup (`delete_cart` → `_delete_carts_named`):** reuses the exact\ndelete path that `create_new_cart` uses for dedup — it activates a *different*\ncart first if the check cart is active (the active cart can't be deleted), then\nticks + trashes + confirms. If the check cart is the account's **only** cart,\nthe delete can't proceed (nothing to switch to) → `_safe_delete_cart` downgrades\nthat to a `warning` and sets `cart_deleted=false`; the scary cart name is the\nmitigation. Note this leaves the *other* cart active as a mild side effect (the\norder flow's dedup already does the same).\n\n**\"DO NOT BUY\" marker (`_generate_check_po`):** the throwaway cart name **and**\nCustomer PO get `DO NOT BUY {rand5}` (5 chars from an unambiguous alnum\nalphabet, e.g. `DO NOT BUY 7F3K9`, 16 chars). The fuller \"AUTOMATED CHECK - DO\nNOT PURCHASE - …\" can't fit the **18-char** Customer PO limit + the restricted\ncharset (`_PO_ALLOWED_RE`), so this is the clearest imperative that still leaves\nroom for the uniqueness suffix. Passes the sanitizer unchanged. Caller may\noverride via `po_number` (re-sanitized + length-checked).\n\n**Never pauses:** a check deletes its cart and never buys, so `pause` is upgraded\nto `order` before `add_lines` — every orderable (incl. backorderable) line gets\na price; flatly `unavailable` sizes are reported price-less. If **all** requested\nsizes are unavailable, `add_lines` raises `AdidasConfigError`, which the check\ncatches and falls back to an inventory-only report (then still deletes the cart).\n\n**Net-price mapping:** `_checkline_results` assigns the review-page nets\n(`line_net_prices`, positional over the lines actually entered, i.e. committed\nqty > 0) to those lines and derives `unit_price` = net ÷ qty; `order_total` is\ntheir sum. Same positional assumption as the order flow, but filtered to entered\nlines so dropped/unavailable lines don't shift the alignment.\n\n---\n\n## Delivery tracking (`get-order-tracking`)  ✅ IMPLEMENTED (live run pending)\n\nA **read-only** walk over a different part of the portal: the order book, an\norder's detail page, and its Delivery Tracking table. Shares only **Step 1\n(login)** with the ordering flow — no cart, no checkout, no writes. Entry point:\n`adidas_browser.get_order_tracking()`; tool `adidas_get_order_tracking`; result\nmodels `TrackingResult` / `TrackingPO` / `TrackingOrder` / `TrackingShipment` in\n`scripts/schemas.py`.\n\n**Status:** selectors captured from pasted HTML (order-book row, tracking-link\nbutton, delivery table, article week toggle/header) and the whole flow exercised\nend to end against a **local replica** of those pages (multi-order PO, multi-\ndelivery order, unshipped order, unknown PO, unreadable order). **Not yet run\nagainst the live portal** — watch the first real run, especially the order-book\ngrid (virtualized rows) and the article toggle's expand/collapse semantics.\n\n### T1 — Order book search (per PO, one PO at a time)\n\n`GET /adidas/reorder/my/order-book?searchText={PO}&page={n}&size=20&filterByRDD=false`\n\nSearching and paging are done **through the URL**, so no search-form or pager\nselectors were needed. Pages are walked (`page` 0,1,2…, cap `_ORDER_BOOK_MAX_PAGES`\n= 10) until a page returns fewer than `size` rows or adds nothing new.\n\nCaptured row (inside an ag-grid cell — the wrapping `id=\"cell-id-30\"` is\npositional and **not** used):\n\n```html\n<div class=\"m-orderHeaderData -overview\">\n  <h2 class=\"a-heading -s -regular\" id=\"OrderHeaderRow6279266468Heading\">6279266468</h2>\n  <ul class=\"m-orderHeaderData__items\">\n    <li class=\"m-orderHeaderData__item\"><span>P13433</span></li>\n    <li class=\"m-orderHeaderData__item\"><div class=\"a-orderType -reorder\">Re-Order</div></li>\n  </ul>\n</div>\n```\n\n| What | Selector |\n| --- | --- |\n| Row | `div.m-orderHeaderData` (`_ORDER_ROW`) |\n| Order number | `h2[id^=\"OrderHeaderRow\"]` text; the id (`OrderHeaderRow{order}Heading`) is the fallback |\n| Customer PO | `.m-orderHeaderData__item span` (first) |\n| Order type | `.m-orderHeaderData__item .a-orderType` (\"Re-Order\") |\n\nTwo live-site behaviours are handled up front:\n\n- **Virtualized rows** — `_collect_order_rows` scrolls and re-scans, deduping by\n  order number, until two consecutive passes add nothing. A PO's *later* orders\n  are exactly the ones a tracking lookup must not silently drop.\n- **Prefix matches** — adidas's `searchText` also matches PO prefixes (searching\n  `P1343` surfaces `P13433` **and** `P13434`). Rows are flagged `exact` on the\n  PO cell; only exact ones are tracked, and the rest are reported in a\n  `warning` so nobody gets tracking for a PO they did not ask about.\n\n### T2 — Order detail → \"has it shipped?\"\n\n`GET /adidas/reorder/my/order-book/{order}` (derived from the tracking link's\ncaptured href, which is that path + `/deliveries`).\n\nThe **presence** of the Delivery Tracking link is the portal's own \"this order\nhas shipments\" tell:\n\n```html\n<a class=\"m-button -tertiary -backButton\" id=\"OrderTrackingButtonLink\"\n   href=\"/adidas/reorder/my/order-book/6279266468/deliveries\">\n  <svg class=\"a-icon -delivery\">…</svg><span class=\"a-label -large -bold\">Delivery Tracking</span></a>\n```\n\n`open_order()` polls (`_ORDER_DETAIL_WAIT_MS` 20s) for **either**\n`#OrderTrackingButtonLink` (→ shipped) **or** the article rows\n(`.o-orderDetailArticle__weekToggle button` → page is up); once the article rows\nare up the link still gets a 2s grace window, so a slow render is never read as\n\"not shipped\". If neither appears the order is reported `unreadable` (never\n\"not shipped\") and the run continues with the other orders.\n\n### T3a — Shipped: the Delivery Tracking table\n\nClick the link (falls back to navigating its href /\n`/adidas/reorder/my/order-book/{order}/deliveries`), then read one row per\n`.o-deliveryTrackingOverview__item`:\n\n| Field | Selector (scoped to the item) |\n| --- | --- |\n| Delivery note | `a[id^=\"DownloadPdfCtaLink\"]` text (the id repeats across rows — always scope to the item) |\n| Ship date | `.o-deliveryTrackingOverview__shipDate` (\"Aug 3, 2026\") |\n| Carrier code | `.o-deliveryTrackingOverview__carrier` (\"UPSN\") |\n| Tracking number + URL | `.m-trackingNumber__trackingNumber a` (text + `href`) |\n\n`carrier_name` is derived from the tracking URL's host (`ups.com` → UPS,\n`fedex.com` → FedEx, …) since adidas shows only its own code. One order can have\nseveral deliveries, and **one delivery note can appear on more than one row**\n(multiple parcels) — every row is returned, nothing is deduped.\n\n> ⚠️ **The delivery-note PDF link carries a bearer token** in its href\n> (`…/download?access_token=Bearer 00D58…`). It is deliberately **not** read or\n> returned — only the note number is taken from the link's text.\n\n### T3b — Not shipped: expected ship dates\n\nNo tracking link ⇒ nothing has shipped. Each article row has a chevron toggle\nthat reveals its week list, whose header is the expected ship date:\n\n```html\n<div class=\"o-orderDetailArticle__weekToggle\"><button class=\"m-toggleLabel -isActive\">…</button></div>\n<header class=\"o-orderDetailArticle__weekItemHeader\">\n  <span class=\"o-orderDetailArticle__weekItemHeaderName\">Feb 4, 2027</span></header>\n```\n\n`read_expected_ship_dates()` clicks every toggle **that is not already\nexpanded** — expansion is decided by whether that article already shows a\n`…__weekItemHeaderName`, not by the `-isActive` class, whose polarity is not\ncaptured — so the pass is idempotent and cannot collapse an already-open row.\n\nThe article **container** (`.o-orderDetailArticle`) is *inferred* from the BEM\nblock of the two captured elements; it is used only to group dates per article\n(and to label them from the block's first `h2, h3, .a-heading`). If it does not\nmatch the live markup, every date found is still returned as one order-level\nentry — the dates are never lost, only their per-article grouping.\n\n`expected_ship_date` is the earliest date parsed with `%b %d, %Y` / `%B %d, %Y`,\nfalling back to the first date shown if the format ever changes.\n\n### Design decisions\n\n- **Read-only, and shaped like the other actions' escalations.** A PO with no\n  orders (`not_found`) or an order whose page would not load (`unreadable`)\n  flips the result to `needs_confirmation` with the details in `warnings` —\n  the same \"hand the gap back to the user\" contract as `missing_products`.\n- **One login, POs looped one at a time** (`po_numbers` is a list, a\n  comma/whitespace string, or a single `po_number`; duplicates collapse).\n- **A `table` field** (Markdown, PO → order → tracking rows, unshipped rows\n  annotated `*(expected)*`) is built server-side so the calling agent hands back\n  one consistent table instead of re-deriving one per call.\n- **Never guess a shipment.** Expected ship dates are labelled expected in the\n  data *and* in the table, and an unreadable order is never reported as \"not\n  shipped\".\n\nFile v0.9.0:skill-card.md\n\n## Description:\n\nBrowser-driven adidas Click B2B toolkit that places purchase orders, checks live inventory and wholesale pricing, and retrieves shipment tracking from the adidas Click portal through Playwright because adidas exposes no public API.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[zmtucker](https://clawhub.ai/user/zmtucker)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nExternal teams and purchasing agents use this skill with authorized adidas Click accounts to draft or submit purchase orders, check live stock, restock dates, and wholesale pricing, and retrieve shipment tracking for purchase orders.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: The skill can use live adidas Click credentials and submit real purchase orders.\n\nMitigation: Install it only for authorized accounts, require literal JSON true before submitting an order, and use confirm=false review runs before live submission when practical.\n\nRisk: Credentials and screenshots can expose sensitive account or order data.\n\nMitigation: Avoid inline or CLI passwords when possible, keep screenshots in a private controlled directory or disable them, and run the skill under an isolated unprivileged account.\n\nRisk: Browser automation depends on approved adidas hosts and may trigger anti-bot controls.\n\nMitigation: Restrict the base URL to approved adidas hosts, review automation use against adidas terms, and use an authorized network path for the account.\n\n## Reference(s):\n\n- [ClawHub skill page](https://clawhub.ai/zmtucker/skills/drivethru-adidas-click)\n- [adidas Click B2B portal](https://b2bportal.adidas-group.com)\n- [Order flow notes](references/order_flow_notes.md)\n\n## Skill Output:\n\n**Output Type(s):** [text, markdown, JSON, shell commands, configuration, guidance]\n\n**Output Format:** [JSON results, Markdown tracking tables, and concise operational guidance with shell command examples.]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Purchase-order submission requires confirm=true; inventory and tracking checks are read-oriented, while pricing briefly creates and deletes a DO NOT BUY cart.]\n\n## Skill Version(s):\n\n0.9.0 (source: server release and skill frontmatter)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment.\n\nArchive v0.8.0: 10 files, 86532 bytes\n\nFiles: references/order_flow_notes.md (40704b), scripts/_cli.py (6794b), scripts/adidas_browser.py (139610b), scripts/adidas_client.py (3199b), scripts/adidas_tools.py (14095b), scripts/adidas.py (3110b), scripts/schemas.py (14701b), skill-card.md (2726b), SKILL.md (28841b), _meta.json (141b)\n\nFile v0.8.0:SKILL.md\n\n---\nname: drivethru-adidas-click\ndescription: Browser-driven adidas Click B2B toolkit — places purchase orders, runs live inventory / wholesale-pricing checks, and pulls shipment tracking numbers from the adidas Click portal with Playwright, since adidas exposes no public API. Ordering accepts style number, color, size, quantity, a purchase-order number, and a ship-to address, drives the cart/checkout, and (on confirm) submits the order. Checks reuse the same flow but never buy — inventory reads product pages (no cart), pricing fills a throwaway \"DO NOT BUY\" cart, reads the priced checkout, and deletes it. Tracking searches the order book by PO number, opens every adidas order for it, and returns each delivery's carrier + tracking number (or, for an order that has not shipped, its expected ship dates). Use whenever the user needs to place/draft a PO on adidas Click, check live stock levels or wholesale pricing, or find tracking numbers / ship dates for adidas POs.\nversion: 0.8.0\nemoji: 👟\nhomepage: https://b2bportal.adidas-group.com\nmetadata:\n  openclaw:\n    requires:\n      bins: [python3]\n    envVars:\n      ADIDAS_CLICK_USERNAME:\n        required: false\n        description: >\n          adidas Click B2B portal username (typically an email). Optional —\n          may instead be passed inline in a tool's stdin JSON. Treat as a secret.\n      ADIDAS_CLICK_PASSWORD:\n        required: false\n        description: adidas Click portal password. Optional env cache; treat as a secret.\n      ADIDAS_CLICK_BASE_URL:\n        required: false\n        description: >\n          Override for the adidas Click base URL. Defaults to\n          `https://b2bportal.adidas-group.com`. Set this if the account uses a\n          region-specific host.\n      ADIDAS_CLICK_HEADLESS:\n        required: false\n        description: >\n          Force headless when set to a truthy value (`true`/`1`/`yes`/`on`).\n          Default is HEADED, because adidas's Akamai Bot Manager stalls headless\n          Chromium. On Windows/macOS a headed browser uses the native display, so\n          nothing extra is needed. Only on a **Linux** host with no display server\n          (e.g. a headless container) does the skill auto-start an Xvfb virtual\n          display (requiring the `xvfb` system package); Windows/macOS never need\n          it. Headless will most likely time out at the adidas login page.\n      ADIDAS_CLICK_USER_AGENT:\n        required: false\n        description: >\n          Override the browser User-Agent. Defaults to a current desktop-Chrome\n          UA so adidas's Akamai edge serves the automated browser instead of\n          stalling the response. Only change it if the default UA gets blocked.\n    install:\n      uv:\n        # Browser automation is the only surface. This installs the Playwright\n        # *package*; the Chromium *binary* (a separate ~150 MB download that pip\n        # can't fetch, and OpenClaw has no post-install hook for) is installed\n        # automatically on the first run — see the browser-binary note below.\n        - playwright>=1.40\n---\n\n# adidas Click B2B toolkit\n\nadidas Click is a **B2B ordering portal with no public API**, so this skill\nplaces purchase orders by driving the portal with **Playwright**. Every tool is\nreached through one CLI entrypoint:\n\n```bash\necho '<json-args>' | python3 scripts/adidas.py <action>\n```\n\nThe action is the **first CLI argument**; arguments are a JSON object on\n**stdin**. Each call prints a single JSON object on stdout, or\n`{\"error\": {\"type\": ..., \"message\": ...}}` with a non-zero exit code on failure.\n\nPlaywright is imported lazily, so the skill loads without it. The Playwright\n**package** is a declared `uv` dependency; its Chromium **browser binary** (a\nseparate ~150 MB download that pip/uv doesn't fetch, and for which OpenClaw has\nno install-time hook) is **installed automatically on the first run** if missing\n— so no manual `python -m playwright install chromium` is needed. That first run\ntherefore includes a one-time browser download.\n\n> **Host system libraries.** Chromium also needs OS-level shared libraries\n> (`libglib-2.0.so.0`, `libnss3`, …). On a missing-libraries launch failure the\n> tool best-effort runs `playwright install-deps` (needs root), then retries. If\n> the host still lacks them it returns a `config_error` listing the required apt\n> packages. Installing system libraries needs root, so on a locked-down agent\n> host this must be handled by the **environment** — the OpenClaw environment's\n> setup script or base image should install Chromium's deps (`python -m\n> playwright install --with-deps chromium`, or the apt packages the error\n> lists). The skill can fetch the browser binary at runtime, but it cannot\n> install OS packages without root.\n\n> **Build status — full order flow implemented.** The skill drives a purchase\n> order end to end: login → add line quantities to the active cart → checkout\n> (Customer PO, delivery location — default / saved / one-time dropship — and\n> shipping method) → Next → **Calc. Net Price** (waits for the \"Done!\" tell so\n> the wholesale discount always applies) → **Order Now** → confirmation number\n> parsed from the redirect URL. `confirm: false` is a full dry run; `confirm:\n> true` **places a real order** (no sandbox). The assembled flow has been built\n> and unit-checked selector-by-selector but not yet run against the live site in\n> one pass — watch the first real end-to-end run. See\n> [`references/order_flow_notes.md`](references/order_flow_notes.md) for the step\n> map and captured selectors.\n>\n> **Order tracking (`get-order-tracking`)** is a separate, read-only surface\n> (the order book / order detail / delivery tracking pages) built from captured\n> HTML: order-book search by PO → order detail → **Delivery Tracking** table, or\n> the article rows' expected ship dates when nothing has shipped. It has been\n> exercised end to end against a local replica of those captured pages, but not\n> yet against the live portal — watch the first real run. See the \"Delivery\n> tracking\" section of\n> [`references/order_flow_notes.md`](references/order_flow_notes.md).\n>\n> **Inventory / pricing checks (`check-inventory-pricing`)** reuse the same\n> captured steps — product navigation + size read (inventory) and, for pricing,\n> the throwaway-cart → checkout → **Calc. Net Price** walk — but stop before\n> **Order Now** and delete the cart. No new selectors: it composes the existing,\n> live-validated ordering steps, so its browser surface rides on the same\n> capture. See the \"Inventory & pricing checks\" section below.\n\n## When to use this skill\n\nReach for `create-purchase-order` when the request involves placing (or\ndrafting) a purchase order on **adidas Click**: given a style number, color,\nsize, quantity, a PO number, and a ship-to address, put the order into the\nportal cart/checkout and — on explicit confirmation — submit it.\n\nReach for `check-inventory-pricing` when the request is to look up **live stock\nlevels** and/or **wholesale (net) pricing** on adidas Click **without buying\nanything** — e.g. *\"how much JW4306 is in stock in L?\"* or *\"what's our\nwholesale cost on 24× JW4306 M?\"*. It reuses the ordering flow but never places\nan order (see below).\n\nReach for `get-order-tracking` when the request is about **shipments** for one\nor more adidas PO numbers — *\"where's PO P13434?\"*, *\"get me the tracking\nnumbers for these POs\"*, *\"has P13433 shipped yet?\"*. It searches the order\nbook per PO, opens each adidas order behind it, and returns the carrier +\ntracking number for every delivery — or, when an order has not shipped, its\n**expected** ship dates. It is read-only: no cart, no order, no writes.\n\nDo **not** use it for other footwear/apparel vendors, and never invent portal\nselectors from prose — the flow lives in the captured\n[`references/order_flow_notes.md`](references/order_flow_notes.md).\n\n## Actions\n\n| Action | Risk | stdin JSON (key fields) |\n| --- | --- | --- |\n| `create-purchase-order` | **high — portal write (browser)** | `{purchase_order: {po_number, lines: [{style, size, quantity}], ship_to?, delivery_location_id?, ship_method?, on_insufficient_stock?, on_missing_product?, spread_delivery?}, confirm}` |\n| `check-inventory-pricing` | **medium — read-only intent, but a pricing check briefly creates then deletes a throwaway cart (browser)** | `{check: \"inventory\"\\|\"pricing\"\\|\"both\", lines: [{style, size?, quantity?}], po_number?, on_missing_product?}` |\n| `get-order-tracking` | **low — read-only (browser); nothing is created or modified** | `{po_numbers: [\"P13434\", \"P13433\"]}` |\n\nRun `python3 scripts/adidas.py` with no action to print the action list.\n\n### `create-purchase-order` input\n\nThe order can be passed nested under `purchase_order` or as top-level fields:\n\n```json\n{\n  \"purchase_order\": {\n    \"po_number\": \"PO-12345\",\n    \"lines\": [\n      {\"style\": \"JW4306\", \"size\": \"M\", \"quantity\": 24},\n      {\"style\": \"JW4306\", \"size\": \"L\", \"quantity\": 12}\n    ],\n    \"ship_to\": null,\n    \"delivery_location_id\": null,\n    \"ship_method\": null,\n    \"spread_delivery\": false,\n    \"on_insufficient_stock\": \"pause\",\n    \"on_missing_product\": \"pause\",\n    \"notes\": null\n  },\n  \"confirm\": false\n}\n```\n\nEach line is `{style, size, quantity}`. **`color` is optional** — adidas article\nnumbers encode the color (e.g. `JW4306` = the black colorway), so navigating to\nthe article lands on the right color with no picker.\n\nDelivery location precedence: `delivery_location_id` (pick a saved location) >\n`ship_to` (add a one-time / dropship location) > default (leave the cart's\npreset). A one-time `ship_to.state` accepts a 2-letter abbreviation or the full\nname. `ship_method` is left at the cart default unless set, and accepts a FedEx\nservice code (`FDGP`, `FEDE`, `FED4`, …) or its exact label (\"FedEx Ground\").\n`po_number` is capped at **18 characters** and may contain only letters, numbers,\nspace, and `/ _ . ? &`; any other character (e.g. a hyphen) is auto-replaced with\n`_` and the substitution is reported in the result's `warnings`.\n\nBy default (`new_cart: true`) the run **creates its own fresh cart** (named with\nthe PO) right after login and makes it active, so on a shared account it never\nadds to — or checks out — a teammate's cart. If a cart with the same PO name\nalready exists (e.g. a leftover from an errored prior run) it is **deleted\nfirst** so the run never piles onto stale quantities — only carts with that exact\nPO name are removed, and any deletion is noted in `warnings`. Pass\n`new_cart: false` to use the current active cart instead.\n\nPass `screenshot_path` to capture the filled page for review.\n\n## Out-of-stock handling (read this)\n\nBefore entering quantities, the tool reads each size's availability (scrolling\nthe horizontal size row as needed to load off-screen sizes). A size is either\n**backorderable** (stock below the requested quantity, incl. `0` with a restock\ndate) or **not available** (the \"X\" cell — will never be restocked, cannot be\nordered at all; it can only be removed or substituted). When a line's **full\nquantity is not available**, behavior depends on `on_insufficient_stock`:\n\n- **`pause`** (default) — **nothing is ordered** (even with `confirm: true`). The\n  result comes back `status: \"needs_confirmation\"` with an `out_of_stock` list\n  (`{style, size, requested, available}`) and a `message`. The agent must **stop\n  and confirm with the user**, then re-run.\n- **`order`** — order the short line(s) anyway, accepting **delayed delivery**\n  (adidas spreads the backordered portion over future dates).\n- **`skip`** — **remove** the out-of-stock line(s) and order the rest.\n\n**Agent guidance:** if the user's request pre-authorizes a choice, set the flag\nand skip the pause — e.g. *\"go ahead and order anything out of stock\"* →\n`on_insufficient_stock: \"order\"`; *\"remove any out-of-stock items without\nasking\"* → `on_insufficient_stock: \"skip\"`. Otherwise leave it at `pause`; on a\n`needs_confirmation` result, **message the user** with the `out_of_stock` details\nand the three choices, wait for their answer, then resume:\n\n1. Order them anyway (delayed delivery) → re-run with `on_insufficient_stock: \"order\"`.\n2. Don't order them → re-run with `on_insufficient_stock: \"skip\"`.\n3. **Substitute** (e.g. a different size/style so items match) → edit the\n   `lines` accordingly and re-run (optionally with a policy for any still-short\n   substitutes).\n\n`spread_delivery` is the lower-level knob behind `order`: on a per-line spread\nprompt, `false` (default) declines (single delivery), `true` accepts.\n\n## Missing / unlisted product handling (read this)\n\nA style adidas has **no product listing** for — a mistyped or wrong article\nnumber, a base style missing its color code, or an article this account simply\nis not offered — is **not an error by default**: it comes back as an escalation\nso the caller can ask the user, exactly like the out-of-stock pause. Behavior is\nset by `on_missing_product`:\n\n- **`pause`** (default) — **nothing is ordered** (even with `confirm: true`).\n  The result is `status: \"needs_confirmation\"` with a `missing_products` list and\n  a `message`. Stop and ask the user.\n- **`skip`** — drop the missing style's line(s) and order the rest (the dropped\n  lines come back with `quantity: 0` and a `not offered …` note).\n- **`error`** — the pre-0.7 behavior: fail the run with an `api_error`.\n\nEach `missing_products` entry is\n`{style, sizes, requested, reason, detail}`. **`reason` matters:**\n\n| `reason` | Meaning | How to treat it |\n| --- | --- | --- |\n| `not_found` | The portal said so (a \"no results\" page, or it bounced off the product URL). | The style is genuinely not on this account. |\n| `unresolved` | The product page never rendered a size table **and** never said the product was missing. | **Unconfirmed, not proven absent** — could be a slow page or a portal change. Worth one retry before telling the user the style doesn't exist. |\n\n**Agent guidance:** on a `needs_confirmation` result, message the user with the\n`missing_products` details and these choices, then resume:\n\n1. **Correct the article number** → edit `lines` and re-run. adidas article\n   numbers encode the colorway (`JW4306`), so a bare style without the color\n   code will not resolve — this is the most common cause.\n2. **Drop it and order the rest** → re-run with `on_missing_product: \"skip\"`.\n3. **Substitute** a different article → edit `lines` and re-run.\n\nIf the user pre-authorized a choice (\"just skip anything adidas doesn't carry\"),\nset the flag up front and skip the pause. Do **not** re-run the same unchanged\nstyle expecting a different answer on a `not_found` reason.\n\nA check (`check-inventory-pricing`) never aborts on a missing style: the other\nlines are still read and priced, the missing ones come back as\n`status: \"not_found\"` lines, and the result's status becomes\n`needs_confirmation` (default) so the caller escalates — `on_missing_product:\n\"skip\"` downgrades that to a warning instead.\n\n> **Timing.** A missing product is detected by polling the product page and\n> bailing on the portal's own \"no results\" tell, capped at 15s — it does not sit\n> on the full 30s selector timeout per bad style. A wrong **size** on a style\n> that *does* exist is different: that raises immediately with the list of sizes\n> the style offers, because the product page loaded fine.\n\nThe result reports `total_quantity` (summed pieces, on dry runs too) and, on a\nplaced order (`confirm: true`), each line's net `line_total` and `unit_price`\n(net ÷ quantity) plus the order `order_total` (net wholesale), read from the\npriced review page.\n\n## Inventory & pricing checks (`check-inventory-pricing`)\n\nA **read-only lookup** that reuses the ordering flow but **never places an\norder**. Pick the mode with `check`:\n\n| `check` | What it does | Cart? |\n| --- | --- | --- |\n| `inventory` | Reads each line's live size-tile stock level straight off the product page. | **No cart created** — inventory needs no add-to-cart. |\n| `pricing` | Fills a **throwaway cart**, advances to the priced checkout screen (the *only* place wholesale net pricing shows), runs **Calc. Net Price**, reads the net unit/line prices + order total, then **deletes the cart**. | Yes — created, priced, deleted. |\n| `both` (default) | `pricing` plus the inventory levels read while filling the cart. | Yes — same as `pricing`. |\n\n```json\n{\n  \"check\": \"both\",\n  \"lines\": [\n    {\"style\": \"JW4306\", \"size\": \"M\", \"quantity\": 24},\n    {\"style\": \"JW4306\", \"size\": \"L\"}\n  ]\n}\n```\n\nEach line is `{style, size?, quantity?}`:\n\n- **`size` optional (inventory only):** omit it (or pass `\"*\"` / `\"all\"`) to\n  report **every** size of that style. For a pricing line a specific `size` is\n  needed (a price is per size).\n- **`quantity` optional:** defaults to `1`. It only affects a **pricing** line's\n  *line total* (unit price is the same regardless); an inventory read reports the\n  raw level independent of quantity.\n\n**Why a cart at all for pricing?** adidas only reveals the discounted **wholesale\nnet price** on the final checkout screen, after **Calc. Net Price** runs — there\nis no price API and no price on the product page beyond a \"from\" figure. So a\npricing check has to fill the cart and walk to that screen, exactly like an\norder, then stop before **Order Now** and delete the cart.\n\n**The \"DO NOT BUY\" marker.** A pricing check names its throwaway cart **and** the\nCustomer PO with a `DO NOT BUY {random}` marker (e.g. `DO NOT BUY 7F3K9`) so that\nif a run dies mid-flight, the leftover cart is unmistakably safe. The full\n*\"AUTOMATED CHECK - DO NOT PURCHASE - …\"* wording does **not fit**: adidas hard-\ncaps the Customer PO at **18 characters** and allows only letters, numbers,\nspace, and `/ _ . ? &`, so this is the clearest imperative that still leaves room\nfor a 5-char random suffix (cart names must be unique). Override the marker with\n`po_number` (still ≤18 chars, same charset).\n\n**Never pauses on stock.** Because a check never buys and deletes its cart, it\ndoes not stop on short/out-of-stock lines: a `pause` policy is upgraded to\n`order` so every orderable line still gets priced. Sizes that are flatly *not\navailable* (the portal's \"X\" cell) are reported with no price.\n\n**Never dies on an unlisted style.** A style adidas has no product page for is\nreported as a `not_found` line plus a `missing_products` entry; every other line\nis still read and priced. See \"Missing / unlisted product handling\" above.\n\n**Result** (`status: \"checked\"`, or `needs_confirmation` when a style was not\nfound): a `lines` list of\n`{style, size, color, requested_quantity, available, available_count, status\n(\"in_stock\"|\"backorder\"|\"unavailable\"|\"not_found\"), in_stock, unit_price,\nline_total, note}`, plus `order_total` (summed net, pricing only),\n`total_quantity`, `missing_products`, `po_number` (the marker used, `null` for\ninventory-only), and `cart_deleted` (`true`/`false` once a pricing check tried to\nremove its cart; `null` when no cart was made). If the throwaway cart could not\nbe auto-deleted (e.g. it was the account's only cart), a `warning` says so and\nnames it — remove it in the portal.\n\n## Order tracking (`get-order-tracking`)\n\nA **read-only** lookup that turns PO numbers into tracking numbers. It reuses\nthe same headed-browser login as the other actions, logs in **once**, then walks\nthe POs **one at a time**:\n\n1. **Search the order book** by PO —\n   `/adidas/reorder/my/order-book?searchText={PO}&page=0&size=20&filterByRDD=false`.\n   One PO commonly maps to **several** adidas orders; every result row is read\n   (the grid virtualizes rows, so the list is scrolled to the end, and further\n   pages are fetched by bumping `page` until nothing new comes back).\n2. **Open each order** — `/adidas/reorder/my/order-book/{order}`.\n3. **If the order has a Delivery Tracking link** (`#OrderTrackingButtonLink`),\n   click it and read the delivery table: **delivery note, ship date, carrier,\n   tracking number** (+ the carrier's tracking URL). An order can have several\n   deliveries, and one delivery note can carry more than one parcel — every row\n   is returned.\n4. **If there is no Delivery Tracking link, nothing has shipped.** The order's\n   article rows are expanded (the chevron toggle) and the **expected ship\n   dates** are read instead — reported as expected everywhere they appear, never\n   mixed in with real shipments.\n\n```json\n{\"po_numbers\": [\"P13434\", \"P13433\"]}\n```\n\n`po_numbers` accepts a list, or a single comma/whitespace-separated string;\n`po_number` (one PO) also works. Duplicates are de-duplicated.\n\n**Result** (`status: \"checked\"`, or `needs_confirmation` — see below):\n\n| Field | What it holds |\n| --- | --- |\n| `pos[]` | One entry per requested PO: `{po_number, status, orders[], order_count, shipment_count, note}`. PO `status` is `shipped` / `partial` / `not_shipped` / `unreadable` / `not_found`. |\n| `pos[].orders[]` | `{order_number, po_number, order_type, status, shipments[], expected_ship_dates[], expected_ship_date, note}`. Order `status` is `shipped` / `not_shipped` / `unreadable`. |\n| `pos[].orders[].shipments[]` | `{delivery_note, ship_date, carrier, carrier_name, tracking_number, tracking_url}`. `carrier` is adidas's raw code (`UPSN`); `carrier_name` is resolved from the tracking link's host (UPS, FedEx, …). |\n| `expected_ship_dates[]` | `[{article, dates: [...]}]` — only on an order that has **not** shipped. `expected_ship_date` is the earliest of them. |\n| `table` | A ready-to-render **Markdown table** of every PO / order / tracking number, with unshipped rows annotated `*(expected)*`. |\n| `total_orders`, `total_shipments`, `not_found_pos`, `warnings` | Run-level summary. |\n\n**Reporting back to the user:** hand back the `table` field (optionally\nre-formatted) rather than re-deriving one — it already groups by PO and marks\nwhich dates are *expected* rather than actual:\n\n| PO | Order # | Delivery Note | Ship Date | Carrier | Tracking # | Note |\n| --- | --- | --- | --- | --- | --- | --- |\n| P13434 | 6279266468 | 7342219301 | Aug 3, 2026 | UPS | 1Z2AT4600373532427 | shipped |\n| P13434 | 6279266469 | — | Feb 4, 2027 *(expected)* | — | not shipped yet | not shipped — expected ship date (IK1234) |\n\n**When the result is `needs_confirmation`** the lookup is **incomplete** —\neverything reported is accurate, but something is missing:\n\n- **`not_found_pos`** — the order book returned no order carrying that PO.\n  Usually a wrong/mistyped PO number. adidas's search also matches PO\n  *prefixes*, so if the search surfaced orders for *other* POs, a `warning`\n  names them (e.g. searching `P1343` surfaces `P13433` / `P13434`) — those are\n  **not** tracked, because they are not the PO that was asked for. Take the PO\n  number back to the user.\n- **an `unreadable` order** — the order page never rendered. That order's status\n  is unknown (it is never reported as \"not shipped\"); a `warning` names it. Worth\n  one retry before telling the user anything about it.\n\n**Security note:** each delivery row also carries a PDF download link whose URL\nembeds a **bearer access token**. That link is deliberately not read or\nreturned — do not go fetch it.\n\n## Credentials\n\nThe adidas Click portal login can be supplied three ways (**later wins**):\n\n1. **Environment variables** (preferred for a deployed agent):\n\n   ```bash\n   ADIDAS_CLICK_USERNAME=...\n   ADIDAS_CLICK_PASSWORD=...\n   ADIDAS_CLICK_BASE_URL=https://b2bportal.adidas-group.com   # override for region host\n   ```\n\n2. **Inline in the stdin JSON** — pass `username`, `password`, and `base_url`\n   alongside the tool's own arguments.\n\n3. **CLI flags** — `--username` / `--password` / `--base-url` after the action.\n   Handy for local runs with no env vars and no secrets in the order JSON:\n\n   ```bash\n   echo '{\"purchase_order\": {...}, \"confirm\": false}' \\\n     | python3 scripts/adidas.py create-purchase-order --username \"$U\" --password \"$P\"\n   ```\n\n   > Command-line arguments are visible to other processes (`ps`) and your shell\n   > history — for sensitive/automated use, prefer env vars or the stdin JSON.\n\nIf no credentials are present via any of these, the tool exits with\n`{\"error\": {\"type\": \"config_error\", ...}}` (exit code 2) — treat that as a\nsignal to ask the user for the missing fields. Never guess or reuse credentials\nacross accounts, or paste secrets the user did not provide.\n\n## Write safety\n\n`create-purchase-order` is the only order-placing action.\n\n- It requires `\"confirm\": true` to place the order. Without it, it logs in,\n  fills the cart + checkout, and returns a **dry-run** preview **without\n  submitting**. Confirm with the user before placing.\n- Placing an order on adidas Click is a **real purchase** — there is no\n  sandbox. Only run `confirm: true` for an order the user actually intends to\n  place.\n\n`get-order-tracking` is **read-only**: it only navigates and reads order-book,\norder, and delivery-tracking pages. No cart is created, nothing is ordered, and\nno portal state changes.\n\n`check-inventory-pricing` **never places an order** — there is no `confirm`\npath. `inventory` mode touches no cart at all. `pricing`/`both` create a\nthrowaway `DO NOT BUY …` cart only to reach the priced checkout screen, stop\n**before** Order Now, and **delete the cart** on the way out. It does mutate\nportal state briefly (the cart is created and removed), so it is not purely\nread-only, but it cannot buy anything.\n\n## Reachability & bot mitigation\n\nadidas Click sits behind **Akamai Bot Manager**, which fingerprints the client\nand *stalls the HTTP response* (rather than cleanly refusing it) for traffic\nthat looks automated. The tell is a navigation that hangs to a timeout\n(`Page.goto: Timeout … exceeded`) even though the network path is healthy — a\nbrowser-UA `curl` to the same host returns `200` in well under a second, while\na bare `curl` or a default headless Chromium (UA contains `HeadlessChrome`,\n`navigator.webdriver=true`) gets tarpitted.\n\nA spoofed User-Agent alone is **not** enough: a headless Chromium is still\nfingerprinted (TLS/JA3, `HeadlessChrome` internals, no GPU/canvas) and tarpitted\neven with a real UA. The reliable path is a **headed** browser on a **virtual\ndisplay**, which presents an ordinary browser fingerprint. So the skill:\n\n- runs **headed by default** (set `ADIDAS_CLICK_HEADLESS=true` to force headless,\n  which will most likely time out at the login page);\n- launches Chromium with a real desktop-Chrome User-Agent, a normal\n  viewport/locale, `--disable-blink-features=AutomationControlled`, a\n  `navigator.webdriver` mask, and a longer navigation timeout;\n- when headed on a **Linux** host with no display server, **auto-starts an Xvfb\n  virtual display** (spawned directly, so no `xauth` needed) and points\n  `$DISPLAY` at it — this requires the **`xvfb` system package** in the image\n  (`apt-get install -y xvfb`). This is Linux-only: the skill first checks the\n  platform, so on **Windows/macOS** (native GUI) and on a Linux host that already\n  has an X11/Wayland display, it launches headed directly and **never requires\n  Xvfb**.\n\nOverrides: `ADIDAS_CLICK_USER_AGENT` if the default UA ages out. If headed on a\nvirtual display *still* stalls, the block is at the **egress IP** (a datacenter\nIP Akamai distrusts) — route through an IP adidas allowlists, the same way\n`drivethru-sanmar` documents needing its caller IP allowlisted. No browser/code\nchange fixes an IP-reputation block.\n\n## Error model\n\nFailures print `{\"error\": {...}}` and exit non-zero:\n\n- `config_error` (exit 2) — missing/invalid credentials, a bad policy value, or\n  an un-captured step.\n- `api_error` — the portal rejected an action or a page did not match\n  expectations. Includes `surface`, `operation`, `retryable`. Note that a style\n  adidas does not carry is **not** an `api_error` by default — it comes back as\n  a `needs_confirmation` result (see \"Missing / unlisted product handling\"), and\n  only becomes an `api_error` under `on_missing_product: \"error\"`.\n- `connection_error` — network / browser-launch / navigation failure\n  (`retryable: true`).\n- `validation_error` — bad input JSON or a missing required field.\n- `usage` / `unknown_action` (exit 2) — bad CLI invocation; the message lists\n  the valid `actions`.\n\nSurface the human-readable `message` to the user. Do not retry on\n`config_error` or `validation_error`.\n\n## References\n\n- [`references/order_flow_notes.md`](references/order_flow_notes.md) — the\n  reverse-engineered portal flows: the ordering step map and captured selectors,\n  plus the read-only **delivery tracking** walk (order book → order → deliveries\n  / expected ship dates). **Start here to continue the build.**\n\nFile v0.8.0:_meta.json\n\n{\n  \"ownerId\": \"kn715tnf30wegyr6mdbfa17avd87bjr6\",\n  \"slug\": \"drivethru-adidas-click\",\n  \"version\": \"0.8.0\",\n  \"publishedAt\": 1787255404705\n}\n\nFile v0.8.0:references/order_flow_notes.md\n\n# adidas Click \"place a purchase order\" — reverse-engineered flow (working notes)\n\n> Captured interactively from the live adidas Click B2B portal. These notes\n> back the Playwright-driven `create-purchase-order` action. Selectors are what\n> the driver targets; anything dynamic is parameterized from stdin JSON.\n>\n> **Surface:** the adidas Click B2B ordering portal\n> (`https://b2bportal.adidas-group.com`). Driver creds:\n> `ADIDAS_CLICK_USERNAME` / `ADIDAS_CLICK_PASSWORD` (base URL override\n> `ADIDAS_CLICK_BASE_URL`).\n\n---\n\n## ⏯️ RESUME HERE (read this first)\n\n**Status:** ✅ Complete. All steps and sub-paths are implemented — the skill\ndrives a purchase order end to end: login → add line quantities to the active\ncart → set Customer PO / delivery location (default, saved, or one-time dropship\nwith State dropdown) / shipping method (code or label) → Next → Calc. Net Price\n(wait for \"Done!\") → Order Now → parse the confirmation number from the redirect\nURL. `confirm: false` is a full dry run; `confirm: true` **places a real order**\n(no sandbox).\n\n**Validated:** a real order has been placed end to end against the live site\n(cart dedup → product → size qty → checkout → Calc Net Price → Order Now →\nconfirmation number; net `order_total` confirmed). Several live-only issues were\nfixed along the way: login tell (Logout hidden in a dropdown), cart-name\nuniqueness + can't-delete-active-cart, lazy-rendered size tiles, and the cart\n\"Next\" button inactive until scrolled.\n\n**Method (progressive HTML capture — same as SanMar's `process-return`):** we\nbuild one step at a time. For each step the user pastes the page **URL** and the\nrelevant **HTML** (the form / table / controls), Claude extracts the selectors,\nimplements the driver method, and flips that step's `*_IMPLEMENTED` flag.\n\n**Build complete** — nothing left to capture for ordering. A second action,\n`check-inventory-pricing`, was added on top of these steps (inventory read /\nwholesale-pricing lookup that never orders) — see \"Inventory / pricing checks\"\nbelow. It captured **no new selectors**; it reuses steps 1–4.\n\nA third action, `get-order-tracking`, drives a **different, read-only surface**\n(order book → order detail → delivery tracking) and *did* capture new selectors\n— see \"Delivery tracking\" at the end of this file. It shares only Step 1\n(login) with the ordering flow.\n\n### Step map / gates (`scripts/adidas_browser.py`)\n\n| Step | Gate flag | Driver method | Status |\n| --- | --- | --- | --- |\n| 1. Login | `LOGIN_IMPLEMENTED` | `login()` | ✅ implemented |\n| 2. Add lines (product nav, size map, inventory, qty input) | `ADD_LINES_IMPLEMENTED` | `add_lines()` | ✅ implemented |\n| 3. Checkout (PO + delivery + shipping) | `CHECKOUT_IMPLEMENTED` | `fill_checkout()` | ✅ implemented (incl. dropship + shipping) |\n| 4. Checkout → calc net price → order now | `FINAL_SUBMIT_IMPLEMENTED` | `price_cart()` + `complete_submission()` | ✅ implemented |\n| Check. Inventory / pricing (no order) | — (reuses 1–4) | `read_inventory()` / `price_cart()` / `delete_cart()` / `check_inventory_pricing()` | ✅ implemented |\n| Track. Delivery tracking (read-only) | — (reuses 1 only) | `find_orders_for_po()` / `open_order()` / `read_delivery_tracking()` / `read_expected_ship_dates()` / `get_order_tracking()` | ✅ implemented (live run pending) |\n\n### Insufficient-availability decision point ✅ IMPLEMENTED\n\n`add_lines` reads availability for **every** line first (`_prepare_lines` opens\neach product once and reads the size-tile inventory indicator; `_parse_available`\ntreats `\"300+\"`/blank as sufficient, exact numbers as the count). A line is\n**short** when its exact available count < requested. Then per\n`OrderRequest.on_insufficient_stock`:\n\n- **`pause`** (default) + any shortfall → raise `_OrderPause`; the\n  entry point returns `status=\"needs_confirmation\"` with `out_of_stock`\n  (`[{style, size, requested, available}]`) and a `message` listing the choices,\n  and places **nothing** (even with `confirm: true`, because this happens before\n  the submit gate). The cart may hold nothing (short found in the read pass) — a\n  re-run's `new_cart` dedup cleans up regardless.\n- **`order`** → enter the short line anyway with the spread accepted (delayed\n  delivery); line note records it.\n- **`skip`** → do not enter the short line (quantity 0, \"removed\" note); order the\n  rest. If **all** lines are skipped → error (\"nothing to order\").\n\nThe **agent** turns a `needs_confirmation` result into a user message, then\nre-runs with `on_insufficient_stock: \"order\"` / `\"skip\"`, or **substitutes** by\nediting `lines` and re-running. Pre-authorizing phrases in the user's prompt\n(\"order anything out of stock\" / \"remove out-of-stock items\") let the agent set\nthe flag up front and skip the pause. See SKILL.md \"Out-of-stock handling\".\n\n### Missing-product decision point ✅ IMPLEMENTED\n\nSame shape, second axis: a style adidas serves **no product page** for is a\ndecision for the caller, not a crash. `_open_product(style, missing_ok=True)`\nreturns False and records the style in `driver.missing_products`\n(`[{style, sizes, requested, reason, detail}]`); `_prepare_lines` marks that\nstyle's lines `not_found`. Then per `OrderRequest.on_missing_product`:\n\n- **`pause`** (default) → the same `_OrderPause` (it carries both\n  `out_of_stock` and `missing_products`), so the entry point returns\n  `status=\"needs_confirmation\"` and places **nothing**.\n- **`skip`** → drop those lines (quantity 0, `not offered …` note), order the\n  rest. All lines missing → the \"No lines could be ordered\" error names the\n  styles.\n- **`error`** → `missing_ok=False`, so `_open_product` raises `AdidasAPIError`\n  (the pre-0.7 behavior).\n\n`check_inventory_pricing` forces `skip` internally (a check reports what it can\nread and never aborts mid-run) and then re-escalates itself: if anything landed\nin `missing_products`, the result flips to `status=\"needs_confirmation\"` unless\nthe caller asked for `skip`. Its `add_lines` fallback also catches\n`AdidasAPIError` now, so a line that fails to resolve can never strand the\nthrowaway `DO NOT BUY` cart.\n\nDetection details and the still-uncaptured not-found markup: see \"Style adidas\ndoes not carry\" under Step 2. See SKILL.md \"Missing / unlisted product\nhandling\".\n\n---\n\n## Step 1 — Login (`/login`)  ✅ IMPLEMENTED\n\nStandard username/password form at `https://b2bportal.adidas-group.com/login`.\nTitle \"Welcome to Click\". Implemented in `AdidasClickDriver.login()`.\n\n| Element | Selector | Notes |\n| --- | --- | --- |\n| Form | `#loginFormDsk` | POSTs to `/login` (`method=post`) |\n| Hidden | `input#queryString` (`name=queryString`) | dynamic; filling the live form carries it — never hardcode |\n| Username | `#usernameField` (`name=username`) | ← `ADIDAS_CLICK_USERNAME` |\n| Password | `#passwordField` (`name=password`) | ← `ADIDAS_CLICK_PASSWORD` |\n| Remember me | `#form-reminder` (checkbox) | left unchecked |\n| Submit | `#send2Dsk` (\"Login\", `type=submit`) | the real button — fill + click so JS validation/CSRF come along |\n| Bad-creds alert | `#login-error-alert` (\"Invalid username or password.\") | starts `display:none`; JS flips it visible **without navigation** |\n| SSO error alert | `#sso-login-error-alert` | SSO-only; also checked |\n| SSO button | `#send2SSO` (\"Single Sign On for Sales Managers\", `type=button`) | **separate IdP flow — not used by this skill** |\n| Forgot links | `#forgot-password-link` (`/forgotPassword`), `#forgot-username-link` (`/forgotUsername`) | not used |\n\n**Driver behavior:**\n- Fill visible fields → click `#send2Dsk` → `wait_for_load_state(\"networkidle\")`.\n- **Bad-credentials tell:** `#login-error-alert` (or `#sso-login-error-alert`)\n  becomes visible in place → raise `config_error`.\n- **Success tell (provisional):** a valid login **navigates away from `/login`**.\n  If `#loginFormDsk` is still present at a `/login` URL with no error → raise\n  `config_error` (possible SSO-only account or blocking interstitial).\n\n**Confirmed post-login (from the landing-page header capture):**\n- **Landing URL:** `/adidas/reorder/home`. No cookie / T&C interstitial appears.\n- **Logged-in tell:** wait for an always-visible authenticated shell control —\n  `_LOGGED_IN_MARKER` = `#ReorderNavLink, #CartOverviewNavLink,\n  #HeaderSearchButtonButton, #PersonalNavigationDropdownLabel` (any visible).\n  ⚠️ **Do NOT use `#LogoutNavLink`** — it lives inside the collapsed account\n  dropdown (`#PersonalNavigationDropdownList`), so it is present but *hidden*;\n  a visibility wait on it times out even though login succeeded (observed as a\n  false \"login did not complete\" on a real run).\n\n## Step 2 — Add product lines  *(IN PROGRESS)*\n\n### Step 2a — Header search (✅ wired in `_search_style`)\n\nGlobal site header `.o-siteHeader`. The search box lives in\n`section.o-siteHeader__searchBox` and is **collapsed by default** — click the\nmagnifier to reveal the input; the submit button is `disabled` until text is\nentered.\n\n| Element | Selector | Notes |\n| --- | --- | --- |\n| Open (magnifier) | `#HeaderSearchButtonButton` | expands the collapsed search |\n| Input | `#HeaderSearchSearchInput` (`name=search`) | placeholder \"Search for Article Numbers, Colors, Brand types\" — the **style / article number** goes here |\n| Submit | `#HeaderSearchSubmitButton` (`type=submit`) | starts `disabled`; enables once the input has text |\n| Close | `#HeaderSearchCloseButton` | not used |\n\nOther useful header nav (for later steps / fallbacks):\n`#CatalogNavLink` → `/adidas/reorder/catalog`,\n`#CartOverviewNavLink` → `/adidas/reorder/cart-overview`,\n`#OrderBookNavLink` → `/adidas/reorder/my/order-book`,\n`#QuickAddTileButton` (a \"Quick Add\" side panel — a possible bulk-entry\nalternative to search),\n`#HeaderCartsTileButton` (cart side panel).\n\n### Navigation — direct product URL (✅ preferred, `_open_product`)\n\n- **Search** lands on a results page regardless of exact match:\n  `/adidas/reorder/catalog?searchTerm={style}`.\n- **Recommended:** navigate straight to the product —\n  `/adidas/reorder/product/{STYLE}` (e.g. `/adidas/reorder/product/JW4306`).\n  **adidas article numbers encode the color** (JW4306 = \"M FLEECE CREW BLACK\"),\n  so there is **no color picker** — landing on the URL is landing on the color.\n  `_open_product` uses this and waits for `#CartModule-SizeTable`.\n\n#### Style adidas does not carry (⚠️ tell NOT captured — text-matched)\n\nA style this account isn't offered (or a wrong article number) has **no size\ntable to wait for**. The portal's empty/not-found product markup has **not been\ncaptured**, so — deliberately, rather than inventing a selector — `_open_product`\ndecides with two signals it can trust:\n\n1. **URL**: after `goto`, the page is no longer under `/reorder/product/`\n   (the portal bounced it) → `reason: \"not_found\"`.\n2. **Text**: the visible body matches `_PRODUCT_MISSING_TELL_RE` (\"no results\",\n   \"not found\", \"no longer available\", …) once `_LOGGED_IN_MARKER` has painted\n   → `reason: \"not_found\"`.\n\nNeither firing within `_PRODUCT_WAIT_MS` (15s, polled every 400ms rather than\none blocking `wait_for_selector`, so a bad style doesn't burn the full 30s\ntimeout) gives `reason: \"unresolved\"` — reported as *unconfirmed*, never as\n\"this style doesn't exist\". A false positive is safe by construction: it can\nonly route the style into the `on_missing_product` escalation, which places\nnothing and asks the user.\n\n**Next capture:** open a style the account isn't offered and record the real\nnot-found page's id/markup, then swap the text match for that selector (keeping\nthe text match as a fallback).\n\n### Step 2a — Availability date gate (✅ wired)\n\nAbove the size table sits a draggable date-tab strip\n(`#DateTabs-DraggableTable`); the active tab is `#DateTabButton` and its inner\ntext is a formatted date (e.g. `\"Jul 31, 2026\"`). This is the **earliest date\nthe product can ship**, and the size-tile inventory numbers refer to _that_\ndate. If it isn't today, the size table is describing future stock —\n**nothing is orderable now** regardless of what the tiles show.\n\n`_product_available_today()` reads `#DateTabButton`, parses `%b %d, %Y`, and\ncompares to today's date. Both the order flow (`_prepare_lines`) and the\ninventory read (`read_inventory`) call it right after `_open_product` and\nbefore touching any tile; when it returns False, every requested line for that\nstyle is classified `unavailable` with a `\"not available until {date}\"` note\nand the size-tile read is skipped. Fail-open on a missing/unparseable date\n(logs a warning) so a portal shape change doesn't silently block every order.\n\n### Step 2b — Size table (✅ mapping + inventory wired)\n\nContainer `#CartModule-SizeTable`. ⚠️ **The grid sits below the fold and\nlazy-renders its per-size tiles only when scrolled into view** — the size\n*labels* (SizeTranslation cells) render eagerly, but the\n`#CartModule-SizeTile-{style}-{code}` tiles do not attach until the table enters\nthe viewport. `_open_product` calls `_ensure_size_tiles_rendered(style)`\n(scrollIntoView center → wait for `[id^=\"CartModule-SizeTile-{style}-\"]`, with a\nnudge-scroll fallback) before any tile interaction, so a headless run (or an\nunscrolled headed run) doesn't time out on a not-yet-rendered tile.\n\nA horizontally-scrolling **size-run grid**: one column per size, a per-size cell\nper article row.\n\n⚠️ **Off-screen sizes lazy-load** — only the visible sizes' cells are in the DOM;\nthe rest render when the row's **arrow buttons** are clicked\n(`#CartModuleSizeBarNavigationForward{group}Button` /\n`…Back{group}Button`, `{group}` dynamic — targeted by id-prefix). `_ensure_size_\nin_view(style, code)` resets to the leftmost then scans forward (clicking the\nforward arrow, polling) until the size's wrapper cell\n(`#CartModule-SizeRow-SizeTile-Wrapper-{style}-{code}`) attaches. Called before\nclassifying a size and before entering its quantity.\n\n**Per-size cell shapes** (`_classify_size` → `ok` | `short` | `unavailable`):\n- **Orderable:** wrapper contains `#CartModule-SizeTile-{style}-{code}` (the\n  quantities div) + `#CartModule-SizeTile-InventoryIndicator-{style}-{code}`.\n  Stock `0` **with a restock date** still lives here — it's backorderable\n  (`short` when available < requested).\n- **Not available:** wrapper contains `#CartModule-SizeTile-Status-NotAvailable-\n  {style}-{code}` (the \"X\" cell, hashed class `notAvailable--…`) and **no**\n  quantity input — it will never be restocked and **cannot be ordered**\n  (`unavailable`). Dropped under every policy with a \"not available\" note; under\n  `order` it still can't be placed (only remove/substitute).\n\n> ⚠️ **Use stable ids, not classes.** The `*--xxxxx` classes (`sizeTile--jfyHK`,\n> `sizeTranslation--xFkgj`, …) are hashed CSS-module names that change on every\n> build. All selectors below use the semantic `id` prefixes.\n\n**Size label → numeric code** (from the size-bar header cells):\n`#CartModule-SizeBar-SizeTranslation-{group}-{code}` with the label in an\n`<ins>`. `{group}` is a conversion-group id (`51` in the sample). Observed map:\n\n| Label | Code | | Label | Code | | Label | Code |\n| --- | --- | --- | --- | --- | --- | --- | --- |\n| XS | 210 | | 3XL | 320 | | 3XLT | 410 |\n| S | 230 | | 4XL | 330 | | 4XLT | 420 |\n| M | 250 | | 5XL | 340 | | 5XLT | 430 |\n| L | 270 | | LT | 380 | | LT2 | 450 |\n| XL | 290 | | XLT | 390 | | XLT2 | 460 |\n| 2XL | 310 | | 2XLT | 400 | | 2XT2 | 470 |\n\n`_size_code_map()` reads these live (never hardcodes) into `{label: code}` and\nresolves the requested size label to its code.\n\n**Per-style, per-size tile** (`{style}` = article number, `{code}` = size code):\n\n| Element | Selector |\n| --- | --- |\n| Quantities cell | `#CartModule-SizeTile-{style}-{code}` |\n| Inventory text | `#CartModule-SizeTile-InventoryIndicator-{style}-{code}` (`300+`, `160`, `0`, …) — in-stock tiles also carry a `hasInventory--…` class (hashed; not used) |\n| Restock icon | `#CartModule-SizeTile-RestockIndicator-{style}-{code}` |\n| Article total qty | `#CartModule-MaterialRow-Summary-TotalQuantity-{style}` |\n| Article total price | `#CartModuleMaterialRowSummaryTotalPrice{style}Price` |\n| Product name | `#CartModule-TinyProduct-{style}-ProductName` (e.g. \"M FLEECE CREW BLACK\") |\n| Wholesale price | `#CartModuleTinyProductPrice{style}Price` (\"Wholesale Price from $22.50\") |\n\n`_read_inventory()` surfaces the availability text; `add_lines()` groups lines\nby style, opens each product once, and fills all its sizes.\n\n### Step 2c — Quantity input (✅ IMPLEMENTED)\n\nClicking a size tile opens a **single shared floating overlay** `#quantityInput`\n(absolutely positioned via inline `left`/`top` over the active cell). Typing a\nvalue + **Enter** commits it; the overlay closes and the ordered qty renders in\nthe cell. Entering a quantity **is** adding to the working cart — everything is\nunder the `CartModule` namespace, and there is **no separate add-to-cart button**\non the product page.\n\n| Element | Selector | Notes |\n| --- | --- | --- |\n| Overlay | `#quantityInput` | shared; repositions over the clicked tile |\n| Quantity field | `#quantityInput input` | react-numeric-input, **no id of its own**; `.fill()` + `press(\"Enter\")` to commit |\n| Decrease / increase | `#QuantityInputDecreaseButton` / `#QuantityInputIncreaseButton` | not used (we type the value) |\n| **Committed qty readback** | `#CartModule-SizeTile-OrderedInDate-{style}-{code}` | appears in the cell after commit (e.g. \"10\") — the driver reads this to confirm |\n| Committed cell state | tile gains hashed class `orderedInThisDate--…` | hashed; not used |\n\n**Insufficient availability** → dialog `#CartModule-SizeItemProposals`\n(\"Insufficient availability — Do you want to spread your quantity over the\nfollowing dates?\"):\n\n| Button | Selector |\n| --- | --- |\n| Yes, spread delivery | `#CartModuleSpreadAcceptButton` |\n| No, just one delivery | `#CartModuleSpreadDeclineButton` |\n| Close | `#CartModuleSizeItemProposalsOverlayCloseButton` |\n\nDriver policy (`OrderRequest.spread_delivery`, default **false**): declines the\nspread (single delivery). `true` accepts it. `_handle_availability_proposal`\nshort-waits (2s) for the dialog after each commit and clicks accordingly;\n`_read_ordered_quantity` then reports what actually committed, and `add_lines`\nnotes any shortfall on the line result.\n\n> **Cart behavior (confirmed):** adidas Click supports **multiple carts**.\n> Quantities entered on the product page land in the **active** cart.\n> `/adidas/reorder/cart` opens that active cart. When a size's requested qty\n> exceeds availability **and the spread is declined**, adidas moves the overflow\n> into a **new, inactive cart** (\"A new cart with the additional quantities has\n> been created\") — it defaults to inactive, so it does not interfere with\n> checking out the active cart.\n\nImplemented in `_set_size_quantity` / `_handle_availability_proposal` /\n`_read_ordered_quantity`; `ADD_LINES_IMPLEMENTED = True`.\n\n## Step 2.5 — Fresh cart per run  ✅ IMPLEMENTED (`create_new_cart`)\n\nadidas Click supports **multiple carts**, and on a **shared account** entering\nquantities on a product page adds to whatever cart is *active* — which risks\npiling onto (and checking out) a teammate's cart. So by default\n(`new_cart: true`) the driver creates its own cart first, right after login.\n\nOpen the carts side panel with `#HeaderCartsTileButton`, then:\n\n| Element | Selector | Notes |\n| --- | --- | --- |\n| New Cart | `#CreateNewCartButton` | opens the name form |\n| Name field | `form.o-editCartForm input.m-input__field` | no own id; `maxlength=25`; same charset as the PO (`/ _ . ? &`) |\n| Save | `#EditCartSaveButton` (`type=submit`) | creates the cart **and switches the active cart to it** |\n| Cancel | `#EditCartCancelButton` | not used |\n\nThe cart **name is the personalReference — i.e. the Customer PO** (the carts\nlist column `personalReference` is headed \"Cart Name\"). The driver names the new\ncart with the (sanitized) PO. Pass `new_cart: false` to reuse the active cart.\n\n**Delete-first, then create.** Two hard rules interact: (a) **cart names must be\nunique** — the New-Cart **Save button stays `disabled`** if a cart with that name\nalready exists (so you can't create-then-delete); and (b) **the active cart\ncan't be deleted.** So the driver deletes any same-named leftover *first*, and if\nthat leftover is the active cart, activates a different cart before deleting it.\n\nFlow (`create_new_cart` → `_delete_carts_named`):\n1. `_cart_rows()` snapshots the carts **ag-grid** (`.o-headerCartsList`): per row\n   the `row-id`, name (`a.a-link[title=…]`, title = cart name = PO), active flag\n   (`.o-activeBadge` present), and activate-toggle id\n   (`div.a-toggle[id$=\"ToggleActiveCartToggle\"]`).\n2. `old_ids` = rows whose title == PO. If none, skip to create.\n3. If a same-named cart **is active**, click another cart's toggle\n   (`[id=\"{cartId}ToggleActiveCartToggle\"]`) to make it active — the active row\n   has no toggle. (If the leftover is the *only* cart → clear error; empty/rename\n   manually or `new_cart:false`.)\n4. `_delete_cart_rows(old_ids)` — tick each `div[role=row][row-id=…]\n   input.ag-checkbox-input` → mass-actions bar (`.o-agGridHeader.-massActions`)\n   appears → trash `#DeleteAllCartsButton` → confirm `#DeleteCartConfirmButton`\n   (\"Yes\").\n5. Now the name is free: click `#CreateNewCartButton`, fill the name, and — after\n   waiting for `#EditCartSaveButton:not([disabled])` (fails fast + clear if it\n   stays disabled) — Save. Saving activates the new cart.\n\nOnly carts with this **exact** PO name are matched — never a teammate's\ndifferently-named cart. Deletions are reported in the result `warnings`. (ag-grid\nvirtualizes rows, so this covers same-PO dups in the rendered set — normally one.)\n\n> Carts panel also exposes each cart's list price via `.o-cartPanelPriceRenderer`\n> and an active badge (`.o-activeBadge`) / activate toggle\n> (`#{cartId}ToggleActiveCartToggleInput`); the mass-actions bar also has\n> `#CartSubmissionButton` / `#DuplicateAllCartsButton` / `#CopyAllCartsToButton` /\n> `#DownloadAllCartsButton` / `#AnalyticsOfCartsButton` — not used here.\n\n## Step 3 — Checkout: PO + delivery + shipping  ✅ IMPLEMENTED\n\nNavigate to `/adidas/reorder/cart` (opens the **active** cart) and wait for the\nheader `#CartModule-CartHeader`. The three order settings sit in a row\n(`ul.orderSettings--…`). Heading `#CartHeaderHeading` = \"My Cart\"; the\n\"This is not a confirmed order\" note (`#CartModule-CartHeader-notConfirmedOrder`)\nconfirms nothing is placed yet.\n\n### Customer PO # (✅ `_set_customer_po`)\n\n| Element | Selector | Notes |\n| --- | --- | --- |\n| Field | `#CartModule-PersonalReference-InputField input` | **no own id**; `maxlength=18`; **defaults to a random string** that must be cleared + replaced each time |\n\nDriver validates length ≤ 18, `fill()`s the PO (replacing the default), presses\nEnter + blurs, then reads `input_value()` back to confirm it stuck.\n\n### Delivery Location (✅ default / saved / one-time)\n\nDropdown label `#DeliveryAddressOptionsDropdownLabel` shows the current location\n(id `6017069000` + name). Defaults to the account preset — **left untouched\nunless** a different location or a dropship is requested. Precedence:\n`delivery_location_id` > `ship_to` > default.\n\n- **Open** → `#DeliveryAddressOptionsDropdownContent`, containing:\n  - a search input (`input[type=search]`; note its id is a dynamic\n    `{number}SearchInput`, so target by type within the content),\n  - **Add one-time delivery location** button `#DeliveryAddressAddOneTimeShipToButton`,\n  - saved options `#Option{locId}` with button `#OptionButton{locId}Button`\n    (e.g. `#OptionButton6017069000Button`).\n- **Saved** (`_select_saved_location`): open → click `#OptionButton{id}Button`.\n- **One-time / dropship** (`_add_one_time_location`): open → click add button →\n  modal form `#CartModule-DeliveryAddress-OneTimeShipToAddressForm`:\n\n  | Field | Selector | Maps from `ship_to` |\n  | --- | --- | --- |\n  | Attention 1* | `#Attention1InputField` | `name` |\n  | Attention 2 | `#Attention2InputField` | `attention` |\n  | Street Address* | `#StreetInputField` | `address1` (+ `, address2`) |\n  | City/Town* | `#CityTownInputField` | `city` |\n  | State* (dropdown) | `#StateInputFieldDropdownLabel` | `state` — ⚠️ option markup **not captured** (see below) |\n  | ZIP code* | `#ZipcodeInputField` | `zip` |\n  | Country | *(static \"UNITED STATES\")* | fixed US |\n  | Submit | `#DeliveryAddressFormSubmitButton` (\"USE THIS ADDRESS\") | |\n\n  Rules from the form: mandatory fields marked `*`; Latin characters only;\n  **PO-Box addresses are cancelled**.\n\n### Shipping Method (✅ `_select_shipping_method`)\n\nDropdown label `#ShippingMethodsOptionsDropdownLabel` (defaults \"Default\"). Open →\n`#ShippingMethodsOptionsDropdownContent`. Each option is\n`<div class=\"option {CODE}\" id=\"Option{CODE}\"><button id=\"OptionButton{CODE}Button\"><span>{label}</span></button>`.\n`ship_method` accepts the **4-letter code** (stable id) or the **exact label**:\n\n| Code | Label | | Code | Label |\n| --- | --- | --- | --- | --- |\n| `DFLT` | Default | | `FED4` | FedEx 3 Day |\n| `FDGP` | FedEx Ground | | `FESO` | FedEx Next Business Day |\n| `FDGR` | FedEx Ground Residential Only | | `FEDN` | FedEx Next Business Day 10:30 |\n| `FEDE` | FedEx 2 Day | | `FEDY` | FedEx Next Business Day Residential Only |\n| `FEDZ` | FedEx 2 Day Residential Only | | `FEDB` | FedEx Saturday Delivery |\n\n### State dropdown (✅ `_select_state`)\n\nInside the one-time-address modal. Label `#StateInputFieldDropdownLabel`; open →\n`#StateInputFieldDropdownContent`. Options are\n`<button class=\"m-button -dropdown\" id=\"OptionButton{NameNoSpaces}Button\"><span>{Full State Name}</span></button>`\n— **full names** (\"Tennessee\", \"New York\", \"District of Columbia\"), plus\nterritories and Armed Forces entries. `_resolve_state_name` maps a 2-letter\nabbreviation (`OR`→Oregon, `TN`→Tennessee, …) to the full name; a full name\npasses through. Matched by **exact** text (so \"Virginia\" ≠ \"West Virginia\").\n\nBoth dropdowns: options are `button.m-button` elements; clicking one selects and\ncloses. A search input is present but not needed (all options are in the DOM).\n`_click_option_by_exact_text` iterates the option buttons and clicks the exact\n(case-insensitive whole-string) text match.\n\n## Step 4 — Checkout → calc net price → place order  ✅ IMPLEMENTED\n\n⚠️ Real submit — **no sandbox**; `confirm: true` places a real PO.\n`complete_submission()` runs the full sequence and `FINAL_SUBMIT_IMPLEMENTED` is\n**True**.\n\n**Sequence (captured controls):**\n\n| # | Action | Selector | Notes |\n| --- | --- | --- | --- |\n| 4a | **Next** (cart → checkout) | `#CartModuleCheckoutProgressNPCButton` | navigates to `/adidas/reorder/checkout`; **stays inactive until the cart body is scrolled into view** — the driver `_activate_by_scroll`s it (scroll-into-view + nudge + wait-for-enabled) before clicking |\n| 4b | **Calc. Net Price** | `#NPCCartSimulation` (`<a role=button>` in the summary-table header; also per row) | **applies our wholesale discounts — MUST run every time and finish before ordering; it is slow** |\n| 4c | **Order Now** | `#CartModuleCheckoutProgressBarSubmitOrderButton` | final submit |\n\n**Totals (✅ `_read_checkout_totals`):** after Calc, each order row on the review\npage has a net-price cell `#OrderReviewShardTotalsNetPrice{N}` (N = 1-based row\nindex) whose **first `<span>` is the net price** (e.g. `$35.08`) and second is\nthe retail comparison (`$45.00`). The driver collects the per-row nets, assigns\neach to its result line (`line_total`, positional — review rows follow entry\norder), derives `unit_price` = `line_total` ÷ quantity (`_unit_price`), and sums\nthe nets into `order_total` (`_sum_currency`). Best-effort; never\nblocks the order. `total_quantity` is computed from the committed line\nquantities (available on dry runs too). `order_total` / per-line `line_total`\nonly populate on `confirm: true` (they require the Calc step).\n\n**Calc-complete tell (✅ captured, `_await_net_price_calc`):** when 4b finishes,\n`#NPCCartSimulation` is replaced by a **\"Done!\"** message in the summary-table\nheader cell (`<span class=\"OrderConfirmationOrderSummaryRunNPCMessageDone--…\">Done!</span>`\n— class hashed, so match the **text**). The driver waits for the button to\ndetach and for \"Done!\" to appear (120s timeout) and **raises without ordering**\nif \"Done!\" never shows — so 4c never fires at list price. The same summary header\nalso carries TOTAL ORDER PROPOSALS / TOTAL ARTICLES / TOTAL RETAIL PRICE\n($45.00) / TOTAL PRICE ($17.54 net) — useful to scrape onto the result later.\n\n**Confirmation (✅ captured):** Order Now **auto-redirects** to\n`/adidas/reorder/order/{orderNumber}/confirmation` (e.g. order `25709165`) — no\nintermediate confirm dialog. `_read_confirmation_number` parses the number\nstraight from that URL (`_CONFIRMATION_URL_RE`); `complete_submission` waits for\n`**/adidas/reorder/order/*/confirmation` after Order Now. A **Qualtrics feedback\npopup** (\"rate your experience\", NPS 0–10) opens afterward — it is **ignored**\n(reading the main page URL is unaffected; no interaction needed).\n\n### Design decisions (locked in the scaffolding)\n\n- Final submit gated behind `confirm: true` (mirrors SanMar's\n  `create-purchase-order`) **and** `FINAL_SUBMIT_IMPLEMENTED`.\n- `confirm: false` → fill + validate + `dry_run` preview (nothing placed).\n- Credentials never hardcoded; env (`ADIDAS_CLICK_*`) or inline stdin JSON.\n- Input model: `{po_number (≤18 chars), lines: [{style, size, quantity, color?}],\n  ship_to?: {...}, delivery_location_id?, ship_method?, spread_delivery?,\n  requested_ship_date?, notes?}` (see `scripts/schemas.py`). `color` is optional\n  (the article number encodes it); `spread_delivery` (default false) declines the\n  over-availability spread; delivery precedence is\n  `delivery_location_id` > `ship_to` > default.\n\n## Inventory / pricing checks (`check-inventory-pricing`)  ✅ IMPLEMENTED\n\nA **read-only lookup** that reuses the ordering steps above and **never places\nan order** (there is no `confirm` path). Entry point:\n`adidas_browser.check_inventory_pricing()`; tool `adidas_check_inventory_pricing`;\nresult models `CheckResult` / `CheckLineResult` in `scripts/schemas.py`. **No new\nselectors were captured** — it composes the same live-validated steps, so it\ninherits their capture. The one refactor: the order flow's `complete_submission`\nwas split so its cart→checkout→**Calc. Net Price**→read-totals half is a reusable\n`price_cart()` that both the order flow (before Order Now) and the pricing check\n(before deleting the cart) call.\n\n**Modes (`check` arg):**\n\n| Mode | Path | Cart |\n| --- | --- | --- |\n| `inventory` | `read_inventory()` — open each product (`_open_product`), read the size-tile inventory indicator via `_classify_size` (the same read the order flow does in `_prepare_lines`), classify `in_stock` / `backorder` / `unavailable`. A blank `size` (or `\"*\"`/`\"all\"`) expands to **every** size of the style (iterate `_size_code_map()`). | **None** — reads product pages only. |\n| `pricing` | `create_new_cart(po)` → `add_lines(req)` → `fill_checkout(req)` (sets the DO-NOT-BUY Customer PO) → `price_cart()` (Next → Calc. Net Price → `_read_checkout_totals`) → `delete_cart(po)`. Stops **before** `#CartModuleCheckoutProgressBarSubmitOrderButton` (Order Now). | Throwaway; created, priced, deleted. |\n| `both` | Same as `pricing`; the per-line inventory indicators read during `add_lines` are surfaced alongside the prices. | Same as `pricing`. |\n\n**Cart cleanup (`delete_cart` → `_delete_carts_named`):** reuses the exact\ndelete path that `create_new_cart` uses for dedup — it activates a *different*\ncart first if the check cart is active (the active cart can't be deleted), then\nticks + trashes + confirms. If the check cart is the account's **only** cart,\nthe delete can't proceed (nothing to switch to) → `_safe_delete_cart` downgrades\nthat to a `warning` and sets `cart_deleted=false`; the scary cart name is the\nmitigation. Note this leaves the *other* cart active as a mild side effect (the\norder flow's dedup already does the same).\n\n**\"DO NOT BUY\" marker (`_generate_check_po`):** the throwaway cart name **and**\nCustomer PO get `DO NOT BUY {rand5}` (5 chars from an unambiguous alnum\nalphabet, e.g. `DO NOT BUY 7F3K9`, 16 chars). The fuller \"AUTOMATED CHECK - DO\nNOT PURCHASE - …\" can't fit the **18-char** Customer PO limit + the restricted\ncharset (`_PO_ALLOWED_RE`), so this is the clearest imperative that still leaves\nroom for the uniqueness suffix. Passes the sanitizer unchanged. Caller may\noverride via `po_number` (re-sanitized + length-checked).\n\n**Never pauses:** a check deletes its cart and never buys, so `pause` is upgraded\nto `order` before `add_lines` — every orderable (incl. backorderable) line gets\na price; flatly `unavailable` sizes are reported price-less. If **all** requested\nsizes are unavailable, `add_lines` raises `AdidasConfigError`, which the check\ncatches and falls back to an inventory-only report (then still deletes the cart).\n\n**Net-price mapping:** `_checkline_results` assigns the review-page nets\n(`line_net_prices`, positional over the lines actually entered, i.e. committed\nqty > 0) to those lines and derives `unit_price` = net ÷ qty; `order_total` is\ntheir sum. Same positional assumption as the order flow, but filtered to entered\nlines so dropped/unavailable lines don't shift the alignment.\n\n---\n\n## Delivery tracking (`get-order-tracking`)  ✅ IMPLEMENTED (live run pending)\n\nA **read-only** walk over a different part of the portal: the order book, an\norder's detail page, and its Delivery Tracking table. Shares only **Step 1\n(login)** with the ordering flow — no cart, no checkout, no writes. Entry point:\n`adidas_browser.get_order_tracking()`; tool `adidas_get_order_tracking`; result\nmodels `TrackingResult` / `TrackingPO` / `TrackingOrder` / `TrackingShipment` in\n`scripts/schemas.py`.\n\n**Status:** selectors captured from pasted HTML (order-book row, tracking-link\nbutton, delivery table, article week toggle/header) and the whole flow exercised\nend to end against a **local replica** of those pages (multi-order PO, multi-\ndelivery order, unshipped order, unknown PO, unreadable order). **Not yet run\nagainst the live portal** — watch the first real run, especially the order-book\ngrid (virtualized rows) and the article toggle's expand/collapse semantics.\n\n### T1 — Order book search (per PO, one PO at a time)\n\n`GET /adidas/reorder/my/order-book?searchText={PO}&page={n}&size=20&filterByRDD=false`\n\nSearching and paging are done **through the URL**, so no search-form or pager\nselectors were needed. Pages are walked (`page` 0,1,2…, cap `_ORDER_BOOK_MAX_PAGES`\n= 10) until a page returns fewer than `size` rows or adds nothing new.\n\nCaptured row (inside an ag-grid cell — the wrapping `id=\"cell-id-30\"` is\npositional and **not** used):\n\n```html\n<div class=\"m-orderHeaderData -overview\">\n  <h2 class=\"a-heading -s -regular\" id=\"OrderHeaderRow6279266468Heading\">6279266468</h2>\n  <ul class=\"m-orderHeaderData__items\">\n    <li class=\"m-orderHeaderData__item\"><span>P13433</span></li>\n    <li class=\"m-orderHeaderData__item\"><div class=\"a-orderType -reorder\">Re-Order</div></li>\n  </ul>\n</div>\n```\n\n| What | Selector |\n| --- | --- |\n| Row | `div.m-orderHeaderData` (`_ORDER_ROW`) |\n| Order number | `h2[id^=\"OrderHeaderRow\"]` text; the id (`OrderHeaderRow{order}Heading`) is the fallback |\n| Customer PO | `.m-orderHeaderData__item span` (first) |\n| Order type | `.m-orderHeaderData__item .a-orderType` (\"Re-Order\") |\n\nTwo live-site behaviours are handled up front:\n\n- **Virtualized rows** — `_collect_order_rows` scrolls and re-scans, deduping by\n  order number, until two consecutive passes add nothing. A PO's *later* orders\n  are exactly the ones a tracking lookup must not silently drop.\n- **Prefix matches** — adidas's `searchText` also matches PO prefixes (searching\n  `P1343` surfaces `P13433` **and** `P13434`). Rows are flagged `exact` on the\n  PO cell; only exact ones are tracked, and the rest are reported in a\n  `warning` so nobody gets tracking for a PO they did not ask about.\n\n### T2 — Order detail → \"has it shipped?\"\n\n`GET /adidas/reorder/my/order-book/{order}` (d\n\nArchive v0.7.0: 10 files, 71125 bytes\n\nFiles: references/order_flow_notes.md (33215b), scripts/_cli.py (6794b), scripts/adidas_browser.py (109813b), scripts/adidas_client.py (3199b), scripts/adidas_tools.py (11143b), scripts/adidas.py (2548b), scripts/schemas.py (10472b), skill-card.md (2497b), SKILL.md (23103b), _meta.json (141b)\n\nArchive v0.6.0: 10 files, 62251 bytes\n\nFiles: references/order_flow_notes.md (30480b), scripts/_cli.py (6794b), scripts/adidas_browser.py (91154b), scripts/adidas_client.py (3199b), scripts/adidas_tools.py (9973b), scripts/adidas.py (2548b), scripts/schemas.py (8605b), skill-card.md (2409b), SKILL.md (18989b), _meta.json (141b)\n\nArchive v0.5.2: 10 files, 60692 bytes\n\nFiles: references/order_flow_notes.md (29472b), scripts/_cli.py (6794b), scripts/adidas_browser.py (86595b), scripts/adidas_client.py (3199b), scripts/adidas_tools.py (9973b), scripts/adidas.py (2548b), scripts/schemas.py (8605b), skill-card.md (2709b), SKILL.md (18989b), _meta.json (141b)\n\nArchive v0.5.1: 10 files, 60787 bytes\n\nFiles: references/order_flow_notes.md (29472b), scripts/_cli.py (6794b), scripts/adidas_browser.py (86595b), scripts/adidas_client.py (3199b), scripts/adidas_tools.py (9972b), scripts/adidas.py (2548b), scripts/schemas.py (8605b), skill-card.md (2903b), SKILL.md (18989b), _meta.json (141b)\n\nArchive v0.5.0: 10 files, 59965 bytes\n\nFiles: references/order_flow_notes.md (29472b), scripts/_cli.py (6794b), scripts/adidas_browser.py (85600b), scripts/adidas_client.py (3199b), scripts/adidas_tools.py (9972b), scripts/adidas.py (2548b), scripts/schemas.py (8605b), skill-card.md (2623b), SKILL.md (18583b), _meta.json (141b)\n\nArchive v0.2.1: 10 files, 48138 bytes\n\nFiles: references/order_flow_notes.md (25534b), scripts/_cli.py (6794b), scripts/adidas_browser.py (65216b), scripts/adidas_client.py (3199b), scripts/adidas_tools.py (5903b), scripts/adidas.py (1957b), scripts/schemas.py (5987b), skill-card.md (2539b), SKILL.md (13037b), _meta.json (141b)\n\nArchive v0.4.1: 10 files, 50215 bytes\n\nFiles: references/order_flow_notes.md (25534b), scripts/_cli.py (6794b), scripts/adidas_browser.py (68889b), scripts/adidas_client.py (3199b), scripts/adidas_tools.py (5903b), scripts/adidas.py (1957b), scripts/schemas.py (5987b), skill-card.md (2659b), SKILL.md (13655b), _meta.json (141b)\n\nArchive v0.3.0: 10 files, 48216 bytes\n\nFiles: references/order_flow_notes.md (25534b), scripts/_cli.py (6794b), scripts/adidas_browser.py (65216b), scripts/adidas_client.py (3199b), scripts/adidas_tools.py (5903b), scripts/adidas.py (1957b), scripts/schemas.py (5987b), skill-card.md (2748b), SKILL.md (13037b), _meta.json (141b)","readmeExcerpt":"Skill: drivethru-adidas-click Owner: zmtucker Summary: Browser-driven adidas Click B2B toolkit — places purchase orders, runs live inventory / wholesale-pricing checks, and pulls shipment tracking numbers from the adidas Click portal with Playwright, since adidas exposes no public API. Ordering accepts style number, color, size, quantity, a purchase-order number, and a ship-to address, drives the cart/checkout, and (","codeSnippets":[],"executableExamples":[{"language":"bash","snippet":"echo '<json-args>' | python3 scripts/adidas.py <action>"},{"language":"json","snippet":"{\n  \"purchase_order\": {\n    \"po_number\": \"PO-12345\",\n    \"lines\": [\n      {\"style\": \"JW4306\", \"size\": \"M\", \"quantity\": 24},\n      {\"style\": \"JW4306\", \"size\": \"L\", \"quantity\": 12}\n    ],\n    \"ship_to\": null,\n    \"delivery_location_id\": null,\n    \"ship_method\": null,\n    \"spread_delivery\": false,\n    \"on_insufficient_stock\": \"pause\",\n    \"on_missing_product\": \"pause\",\n    \"notes\": null\n  },\n  \"confirm\": false\n}"},{"language":"json","snippet":"{\n  \"check\": \"both\",\n  \"lines\": [\n    {\"style\": \"JW4306\", \"size\": \"M\", \"quantity\": 24},\n    {\"style\": \"JW4306\", \"size\": \"L\"}\n  ]\n}"},{"language":"json","snippet":"{\"po_numbers\": [\"P13434\", \"P13433\"]}"},{"language":"bash","snippet":"ADIDAS_CLICK_USERNAME=...\n   ADIDAS_CLICK_PASSWORD=...\n   ADIDAS_CLICK_BASE_URL=https://b2bportal.adidas-group.com   # override for region host"},{"language":"bash","snippet":"echo '{\"purchase_order\": {...}, \"confirm\": false}' \\\n     | python3 scripts/adidas.py create-purchase-order --username \"$U\" --password \"$P\""}],"parameters":null,"dependencies":[],"permissions":[],"extractedFiles":[{"path":"SKILL.md","content":"---\nname: drivethru-adidas-click\ndescription: Browser-driven adidas Click B2B toolkit — places purchase orders, runs live inventory / wholesale-pricing checks, and pulls shipment tracking numbers from the adidas Click portal with Playwright, since adidas exposes no public API. Ordering accepts style number, color, size, quantity, a purchase-order number, and a ship-to address, drives the cart/checkout, and (on confirm) submits the order. Checks reuse the same flow but never buy — inventory reads product pages (no cart) and reports the **re-stock date** for anything out of stock or backordered, pricing fills a throwaway \"DO NOT BUY\" cart, reads the priced checkout, and deletes it. Tracking searches the order book by PO number, opens every adidas order for it, and returns each delivery's carrier + tracking number (or, for an order that has not shipped, its expected ship dates). Use whenever the user needs to place/draft a PO on adidas Click, check live stock levels, restock/back-in-stock dates, or wholesale pricing, or find tracking numbers / ship dates for adidas POs.\nversion: 0.9.0\nemoji: 👟\nhomepage: https://b2bportal.adidas-group.com\nmetadata:\n  openclaw:\n    requires:\n      bins: [python3]\n    envVars:\n      ADIDAS_CLICK_USERNAME:\n        required: false\n        description: >\n          adidas Click B2B portal username (typically an email). Optional —\n          may instead be passed inline in a tool's stdin JSON. Treat as a secret.\n      ADIDAS_CLICK_PASSWORD:\n        required: false\n        description: adidas Click portal password. Optional env cache; treat as a secret.\n      ADIDAS_CLICK_BASE_URL:\n        required: false\n        description: >\n          Override for the adidas Click base URL. Defaults to\n          `https://b2bportal.adidas-group.com`. Set this if the account uses a\n          region-specific host.\n      ADIDAS_CLICK_HEADLESS:\n        required: false\n        description: >\n          Force headless when set to a truthy value (`true`/`1`/`yes`/`on`).\n          Default is HEADED, because adidas's Akamai Bot Manager stalls headless\n          Chromium. On Windows/macOS a headed browser uses the native display, so\n          nothing extra is needed. Only on a **Linux** host with no display server\n          (e.g. a headless container) does the skill auto-start an Xvfb virtual\n          display (requiring the `xvfb` system package); Windows/macOS never need\n          it. Headless will most likely time out at the adidas login page.\n      ADIDAS_CLICK_USER_AGENT:\n        required: false\n        description: >\n          Override the browser User-Agent. Defaults to a current desktop-Chrome\n          UA so adidas's Akamai edge serves the automated browser instead of\n          stalling the response. Only change it if the default UA gets blocked.\n    install:\n      uv:\n        # Browser automation is the only surface. This installs the Playwright\n        # *package*; the Chromium *binary* (a separate ~150 MB download that pip\n        # can't"},{"path":"_meta.json","content":"{\n  \"ownerId\": \"kn715tnf30wegyr6mdbfa17avd87bjr6\",\n  \"slug\": \"drivethru-adidas-click\",\n  \"version\": \"0.9.0\",\n  \"publishedAt\": 1787322366368\n}"},{"path":"references/order_flow_notes.md","content":"# adidas Click \"place a purchase order\" — reverse-engineered flow (working notes)\n\n> Captured interactively from the live adidas Click B2B portal. These notes\n> back the Playwright-driven `create-purchase-order` action. Selectors are what\n> the driver targets; anything dynamic is parameterized from stdin JSON.\n>\n> **Surface:** the adidas Click B2B ordering portal\n> (`https://b2bportal.adidas-group.com`). Driver creds:\n> `ADIDAS_CLICK_USERNAME` / `ADIDAS_CLICK_PASSWORD` (base URL override\n> `ADIDAS_CLICK_BASE_URL`).\n\n---\n\n## ⏯️ RESUME HERE (read this first)\n\n**Status:** ✅ Complete. All steps and sub-paths are implemented — the skill\ndrives a purchase order end to end: login → add line quantities to the active\ncart → set Customer PO / delivery location (default, saved, or one-time dropship\nwith State dropdown) / shipping method (code or label) → Next → Calc. Net Price\n(wait for \"Done!\") → Order Now → parse the confirmation number from the redirect\nURL. `confirm: false` is a full dry run; `confirm: true` **places a real order**\n(no sandbox).\n\n**Validated:** a real order has been placed end to end against the live site\n(cart dedup → product → size qty → checkout → Calc Net Price → Order Now →\nconfirmation number; net `order_total` confirmed). Several live-only issues were\nfixed along the way: login tell (Logout hidden in a dropdown), cart-name\nuniqueness + can't-delete-active-cart, lazy-rendered size tiles, and the cart\n\"Next\" button inactive until scrolled.\n\n**Method (progressive HTML capture — same as SanMar's `process-return`):** we\nbuild one step at a time. For each step the user pastes the page **URL** and the\nrelevant **HTML** (the form / table / controls), Claude extracts the selectors,\nimplements the driver method, and flips that step's `*_IMPLEMENTED` flag.\n\n**Build complete** — nothing left to capture for ordering. A second action,\n`check-inventory-pricing`, was added on top of these steps (inventory read /\nwholesale-pricing lookup that never orders) — see \"Inventory / pricing checks\"\nbelow. It captured **no new selectors**; it reuses steps 1–4.\n\nA third action, `get-order-tracking`, drives a **different, read-only surface**\n(order book → order detail → delivery tracking) and *did* capture new selectors\n— see \"Delivery tracking\" at the end of this file. It shares only Step 1\n(login) with the ordering flow.\n\n### Step map / gates (`scripts/adidas_browser.py`)\n\n| Step | Gate flag | Driver method | Status |\n| --- | --- | --- | --- |\n| 1. Login | `LOGIN_IMPLEMENTED` | `login()` | ✅ implemented |\n| 2. Add lines (product nav, size map, inventory, qty input) | `ADD_LINES_IMPLEMENTED` | `add_lines()` | ✅ implemented |\n| 3. Checkout (PO + delivery + shipping) | `CHECKOUT_IMPLEMENTED` | `fill_checkout()` | ✅ implemented (incl. dropship + shipping) |\n| 4. Checkout → calc net price → order now | `FINAL_SUBMIT_IMPLEMENTED` | `price_cart()` + `complete_submission()` | ✅ implemented |\n| Check. Inventory / pricing (no order) | — (reuses 1–4) | `read_invento"},{"path":"skill-card.md","content":"## Description:\n\nBrowser-driven adidas Click B2B toolkit that places purchase orders, checks live inventory and wholesale pricing, and retrieves shipment tracking from the adidas Click portal through Playwright because adidas exposes no public API.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[zmtucker](https://clawhub.ai/user/zmtucker)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nExternal teams and purchasing agents use this skill with authorized adidas Click accounts to draft or submit purchase orders, check live stock, restock dates, and wholesale pricing, and retrieve shipment tracking for purchase orders.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: The skill can use live adidas Click credentials and submit real purchase orders.\n\nMitigation: Install it only for authorized accounts, require literal JSON true before submitting an order, and use confirm=false review runs before live submission when practical.\n\nRisk: Credentials and screenshots can expose sensitive account or order data.\n\nMitigation: Avoid inline or CLI passwords when possible, keep screenshots in a private controlled directory or disable them, and run the skill under an isolated unprivileged account.\n\nRisk: Browser automation depends on approved adidas hosts and may trigger anti-bot controls.\n\nMitigation: Restrict the base URL to approved adidas hosts, review automation use against adidas terms, and use an authorized network path for the account.\n\n## Reference(s):\n\n- [ClawHub skill page](https://clawhub.ai/zmtucker/skills/drivethru-adidas-click)\n- [adidas Click B2B portal](https://b2bportal.adidas-group.com)\n- [Order flow notes](references/order_flow_notes.md)\n\n## Skill Output:\n\n**Output Type(s):** [text, markdown, JSON, shell commands, configuration, guidance]\n\n**Output Format:** [JSON results, Markdown tracking tables, and concise operational guidance with shell command examples.]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Purchase-order submission requires confirm=true; inventory and tracking checks are read-oriented, while pricing briefly creates and deletes a DO NOT BUY cart.]\n\n## Skill Version(s):\n\n0.9.0 (source: server release and skill frontmatter)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment."}],"languages":[],"docsSourceLabel":"CLAWHUB","editorialOverview":null,"editorialQuality":{"score":100,"threshold":65,"status":"thin","wordCount":2164,"uniquenessScore":38,"reasons":["uniqueness-below-45"]}},"media":{"evidence":{"source":"no-media","verified":false,"confidence":"low","updatedAt":"2026-10-10T17:25:39.199Z","emptyReason":"No screenshots, media assets, or demo links are available."},"primaryImageUrl":null,"mediaAssetCount":0,"assets":[],"demoUrl":null},"ownerResources":{"evidence":{"source":"unclaimed","verified":false,"confidence":"low","updatedAt":"2026-10-10T17:25:39.199Z","emptyReason":"This page has not been claimed by the agent owner."},"hasCustomPage":false,"customPageUpdatedAt":null,"customLinks":[],"structuredLinks":{"docsUrl":null,"demoUrl":null,"supportUrl":null,"pricingUrl":null,"statusUrl":null},"customPage":null},"relatedAgents":{"evidence":{"source":"protocol-neighbors","verified":false,"confidence":"medium","updatedAt":"2026-10-10T21:53:18.456Z","emptyReason":null},"items":[{"id":"8ebccd8e-3863-4187-8355-c3f14e1f9edf","entityType":"agent","canonicalPath":"/agent/iofficeai-aionui","slug":"iofficeai-aionui","name":"AionUi","description":"Free, local, open-source 24/7 Cowork app and OpenClaw for Gemini CLI, Claude Code, Codex, OpenCode, Qwen Code, Goose CLI, Auggie, and more | 🌟 Star if you like it!","url":"https://github.com/iOfficeAI/AionUi","homepage":"https://www.aionui.com","source":"GITHUB_REPOS","protocols":["MCP","OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-10-09T19:11:12.944Z","createdAt":"2026-02-25T03:38:16.584Z","downloads":null},{"id":"b917f68a-ebff-438e-84f8-3f4b2494c0bc","entityType":"agent","canonicalPath":"/agent/activepieces-activepieces","slug":"activepieces-activepieces","name":"activepieces","description":"AI Agents & MCPs & AI Workflow Automation • (~400 MCP servers for AI agents) • AI Automation / AI Agent with MCPs • AI Workflows & AI Agents • MCPs for AI Agents","url":"https://github.com/activepieces/activepieces","homepage":"https://www.activepieces.com","source":"GITHUB_REPOS","protocols":["OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-15T02:22:12.426Z","createdAt":"2026-02-25T03:38:12.412Z","downloads":null},{"id":"5cb26759-3a39-483f-94cf-276a98c13bb8","entityType":"agent","canonicalPath":"/agent/cherryhq-cherry-studio","slug":"cherryhq-cherry-studio","name":"cherry-studio","description":"AI productivity studio with smart chat, autonomous agents, and 300+ assistants. Unified access to frontier LLMs","url":"https://github.com/CherryHQ/cherry-studio","homepage":"https://cherry-ai.com","source":"GITHUB_REPOS","protocols":["MCP","OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-11T14:38:40.986Z","createdAt":"2026-02-25T03:38:19.379Z","downloads":null},{"id":"6f6582d0-5d76-4f0f-b81d-86520247950b","entityType":"agent","canonicalPath":"/agent/copilotkit-copilotkit","slug":"copilotkit-copilotkit","name":"CopilotKit","description":"The Frontend for Agents & Generative UI. React + Angular","url":"https://github.com/CopilotKit/CopilotKit","homepage":"https://docs.copilotkit.ai","source":"GITHUB_REPOS","protocols":["OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-03-25T09:50:57.846Z","createdAt":"2026-02-25T03:39:14.617Z","downloads":null}],"links":{"hub":"/agent","source":"/agent/source/clawhub","protocols":[{"label":"OpenClaw","href":"/agent/protocol/openclew"}]}}}