# BW 2Checkout Pricing — Session Log

Append-only history of development sessions. Newest entries at the top.

## Format

```
## YYYY-MM-DD HH:MM — <dev name>

**Goal:** What we set out to do
**Done:** What got done
**Left off at:** Where we stopped
**Notes:** Anything worth remembering
```

---

## 2026-07-13 — scaffold

**Goal:** Create initial plugin scaffold
**Done:** Scaffolded via tools/new-plugin.sh at version 0.1.0
**Left off at:** Ready to begin development
**Notes:** Standard BW plugin structure. Docs stubs in place, awaiting real content.

## 2026-07-13 — Step 2 kickoff: scaffold + core engine
- Scaffolded bw-2checkout-pricing via tools/new-plugin.sh; activated on dev site.
- Wrote real SPEC.md from the 2026-07-08 cPanel discovery (see /srv/apps/copernic/DISCOVERY-WORKSHEET.md).
- Config plumbing (includes/bw-2checkout-pricing-config.php): single-option settings; Secret Key and
  Merchant Code prefer wp-config constants (BW_2CHECKOUT_PRICING_SECRET_KEY / _MERCHANT_CODE).
- REST client (class-bw-2checkout-pricing-client.php): HMAC X-Avangate auth (SHA256), request(),
  get_products(), get_pricing_configurations() w/ transient cache, flush_cache(). HMAC per docs — VERIFY vs sandbox.
- Verified in WP: plugin active, class loads, config reads, is_configured=no (no creds yet).
- NEXT: settings admin screen, REST price route, shortcode widget + JS, then live test on dev site.
- BLOCKED ON: Merchant Code + Secret Key set as wp-config constants (rian to enter; never in chat).

## 2026-07-13 — Step 2: rough widget (mock pricing) end-to-end
- Pricing resolver (mock + live-stub) + ConvertPlus buy-link builder (class-...-pricing.php).
- Public REST route /wp-json/bw-2checkout-pricing/v1/price (class-...-rest.php).
- Shortcode [bw_2checkout_pricing product="cds"] + vanilla JS + rough CSS.
- Config: built-in default CDS map (3WL7JRHV69, EDITION_2/TERM/users); pricing_source (mock|live).
- Deployed to copernic; set non-secret checkout config (merchant COPERNIC, base shop.copernic.com,
  currencies USD,EUR,GBP,CAD). Test page: copernic.demoing.info/?page_id=11027.
- Verified: mock price computes, buy-link matches Phil's construct, REST route registered.
- Live path stubbed (get_live_price -> WP_Error -> mock fallback) until API access; flip
  pricing_source=live when creds land + pricingconfigurations JSON confirmed.
- Scans: cleanup clean; security clean (1 benign sanitized-$_GET warning).

## 2026-07-13 (cont'd) — Correctness fix: per-seat pricing, not flat. Locked-selector headings.
- CORRECTED my earlier "PricingSchema: FLAT means total" claim — that was read off a non-default
  (inactive) pricing config in a truncated JSON dump, not the live default. rian confirmed via
  real checkout (1->2 users increases total by the per-user rate) that pricing IS per-seat.
  Fixed get_live_price() to return unit rate x users = total (matches original mock design).
- Fixed a real bug: build_buy_link() hardcoded qty=1; now qty = actual user count, so the cart's
  own unit x qty math will match what the widget displays. This was wrong before the fix landed.
- Added selector "lock_<key>=value" shortcode support (e.g. lock_edition="advanced") -> renders
  that selector as a heading + hidden input instead of an editable control. Added 'key' aliases
  (edition/term/users) to the default products config.
- mock-note now reflects the ACTUAL per-response 'source' (JS toggles on data.source==='mock'),
  not just the static pricing_source setting -- correct even when live falls back to mock per-call.
- Test page (id 11027) now embeds the widget 4x: fully selectable + 3 with edition locked
  (basic/advanced/elite headings).
- Live API returned HTTP 502 on retry (3x) after two earlier successful live calls in this same
  session with unchanged auth code -- looks like a transient issue on 2Checkout's side, not ours.
  Automatic mock fallback covers this gracefully; re-test live later.
- Scans: cleanup clean; security clean (1 benign warning, same as before).
- NOT done yet (time/token-boxed): version bump + CHANGELOG entry for this fix.

## 2026-07-13 (cont'd 2) — Monthly/annual display, currency own line
- Result now shows total/12 as the primary "/mo" figure, with the full billed total as
  reference underneath (matches Copernic's existing pricing page pattern). Computed in JS
  from the REST response's `amount`/`currency` (no backend change needed).
- Currency selector moved to its own row, separate from the product-option controls, with a
  note: "(selectable for testing, will autodetect)".
- Scans clean; deployed to copernic.

## 2026-07-15 — v0.1.1: monthly display, 52 currencies. 2Checkout API OUTAGE.
- Monthly display was already coded correctly; it wasn't showing because the asset ?ver= was
  still 0.1.0 so browsers served stale widget.js. Bumped to 0.1.1 via tools/bump-version.sh
  (busts cache). CHANGELOG entry written.
- Currencies: set all 52 from the account (option merge, credentials preserved). Static list in
  the setting for now; could be auto-derived from the API's pricing config later.
- LIVE PRICING BLOCKED BY 2CHECKOUT OUTAGE (not us): api.2checkout.com returns HTTP 502 for
  EVERY request incl. unauthenticated and the bare host, from curl on the host too, while
  www.2checkout.com returns 200. Unauth would be 401 if origin were alive. Imperva/Incapsula
  nginx 502 = their origin failing. Ruled out: credentials (secret_len=20, merchant COPERNIC),
  User-Agent (tested 4 variants), our code path.
- Our live path IS proven correct: earlier today it returned basic/1Y/1user=28.99 USD, which
  exactly matches "$28.99 billed annually" on Copernic's own live pricing page; and
  advanced/1Y/5users=43.99 (the 5-25 per-user tier). Will resume when their API returns.
- OPEN: monthly is amount/12 per rian's spec. If a 2Y/3Y table price is the TERM total rather
  than an annual rate, monthly should be /24 or /36. Needs confirming.

## 2026-07-15 — v0.1.2: term-aware monthly
- monthly = total / (12 x term years), was always /12 (2Y/3Y read 2-3x too high). Term length
  derived by scanning the selection for an "NY" code -> no hardcoded group name. Verified:
  basic 1Y=2.92/mo, 2Y=2.62/mo, 3Y=2.43/mo (rate correctly drops on longer terms).
- REST response now includes term_months. Bumped 0.1.2 (also busts widget.js asset cache).
- Server egress IP for 2CO support re: 502s = 184.66.107.113

## 2026-07-16 — v0.1.3: live pricing FUNCTIONALLY COMPLETE (all currencies)
- 2Checkout API back up. Live USD confirmed matching Copernic's page (basic 1Y=28.99, adv 1Y 5u=219.95).
- Currency problem solved: pricingconfigurations only stores USD (63 rows) + 2 overrides (ZAR,RSD).
  Other ~50 currencies are 2CO runtime FX. Probed PUT /orders/0/ (order preview): returns converted
  price but does NOT resolve the "users" scale tier (always base 44.99; multi-unit off => qty clamps
  to 1). So: USD tier rate from config (exact) x FX multiplier derived from order-preview(cur)/preview(USD).
  Added client->order_preview_unit_net() (cached, key ignores users). Verified EUR/GBP/CAD live.
- Buy-link corrected to Phil's construct: qty=1, opt=...,users:<raw count> (was qty=count + bucket code).
- STILL UNVERIFIED (needs one real DOTEST order): that the cart total for users:N actually equals
  displayed unit x N. Phil's construct + rian's 1->2 increment observation strongly support it.
- Scans clean (1 benign warning). Bumped 0.1.3, CHANGELOG written. Not released (rian's call).

## 2026-07-16 — v0.2.0: Gutenberg block + i18n
- Dynamic (server-rendered) block bw-2checkout-pricing/pricing. render_callback -> reuses render();
  no markup duplication, no npm/JSX build (block-editor.js is plain wp.element.createElement, uses
  wp.serverSideRender for the editor preview). Inspector: product select + per-select-selector Lock.
- i18n: code strings translatable (POT generated via wp i18n make-pot -> languages/*.pot, 58 msgids).
  widget.js strings localized from PHP (no wp-i18n dep on front end); editor.js uses wp.i18n +
  wp_set_script_translations. Config labels (product/selector/option) routed through new
  bw_2checkout_pricing_tr() which registers+translates via WPML String Translation when present,
  passthrough without WPML (no hard WPML dependency).
- Verified on copernic: block registered, render_block outputs widget w/ edition lock, editor script
  registered. Minor bump (new capability). Scans clean. Not released.
- To translate on copernic (WPML site): strings appear under WPML > String Translation, domain
  "BW 2Checkout Pricing" (config labels) + standard plugin domain (code strings via the .pot/.mo).

## 2026-07-16 — v0.2.1: locked selectors hidden, title removed
- Locked selectors render only the hidden input now (no visible heading); value still applied to
  price + buy-link. Removed the product-title <h3> from render(). Removed unused .bw2cp-locked-heading CSS.
- Verified via render_block(locks edition=elite): title gone, no locked heading, elite applied, Term+users shown.

## 2026-07-16 — v0.2.2: instant initial price (server-side prefill)
- render() computes the default-selection price server-side (default currency) and injects price/
  unit/buy-link into the markup + data-prefilled="1". widget.js skips its initial update() when
  prefilled. Removes the on-load "—" flash. Uses existing transient cache (default cur = USD = cached
  config lookup, no order-preview). Verified: initial_price="2.42 USD", unit="28.99 USD billed annually".
- NOTE: first switch to a *non-default* currency still does one JS fetch (+ cold order-preview),
  then cached. Could pre-warm all currencies if that lag is noticeable — not done.

## 2026-07-17 — v0.3.0: designed pricing card (Step 3) + tpl setting
- Buy-link tpl now a per-block setting ("Cart template (tpl)") + shortcode attr, threaded through
  build_buy_link($...,$tpl) / REST /price ?tpl / server prefill; falls back to plugin checkout_tpl.
- Designed card built from scratch (own block, not their existing one): accent header + plan
  heading, Term/edition as pill toggles, Users stepper, big-number price (int/dec sup/per), billed
  line, full-width Buy button. Accent via --bw2cp-accent per-block CSS var. New block attrs:
  plan, accent, buttonLabel, tpl. Editor inspector: Content (product/plan/lock*), Checkout (button
  label/tpl), Style (accent ColorPalette). SSR preview.
- Currency selector kept small in footer ("for testing, will autodetect") until geolocation lands.
- Deliberately NOT included yet: the feature-icon/extensions body (their existing blocks cover it;
  render() already outputs $content if we later add an InnerBlocks slot). Geolocation currency = next.
- Verified all 8 structural checks. Test page 11027 rebuilt as 3 locked columns + 1 selectable.
- Bumped 0.3.0 (minor: new design surface). POT regenerated. Scans clean. Not released.

## 2026-07-17 — v0.4.0: reproduce Copernic's pricing card design (Step 3 pass 1)
- Read their original block: acf/bw-desktop-pricing in kadence-child/includes/acf-blocks/ (template.php
  + style.css). Fields: title, has_ribbon/ribbon_title, payment_period, monthly_symbol/integer/
  fractional/symbol_prefix/period, pricing(annual), icons(repeater img), has_footer/footer_title/
  footer_lines(repeater)/footer_image, has_gmail_banner, button_text, button_link. Brand orange #f36923.
- Rebuilt OUR block/card to match that design exactly (square edges, header+flag ribbon, big-number
  price layout sign/int/frac(underline)/per, #ffeee6 panel, icon grid, footer, gmail, buy button).
  Price/symbol/buy-link API-driven; Term=pills, Users=stepper. New content attrs added (title, ribbon,
  per_month, annual_text, icons, footer_*, gmail_img, button_text). widget.js updates per-month
  structure + both header & buy links.
- Test page 11027 rebuilt by pulling the 3 real cards' content (title/ribbon/icons/footer/gmail) from
  the live pricing page (post 62) -> our block reproduces Basic/Advanced/Elite for side-by-side compare.
- NOT YET: block inspector controls for the new fields (esp. media gallery for icons) = pass 2.
  Symbol side currently always left (prefix); fine for $/€/£. Bumped 0.4.0. Scans clean. POT regen.

## 2026-07-17 — v0.5.0: full block editor UI + symbol-side inference (Step 3 pass 2)
- block-editor.js: inspector panels for every field, incl. MediaUpload gallery for icons and single
  MediaUpload for footer/gmail images (stored as {id,url}); footer rows via textarea (newline-split).
  render_block maps camelCase attrs -> shortcode args; img_url() extracts urls from {id,url} objects.
- Symbol side: bw_2checkout_pricing_symbol_side() (suffix set) -> price array 'symbol'/'symbol_side';
  price_html + widget.js place the sign left/right. USD/EUR=left, SEK/PLN=right (verified).
- Test page 11027 rebuilt as 3 SEPARATE wp:bw-2checkout-pricing/pricing block instances inside
  core/columns, populated from the live page's real content (post 62). Rendered = 3 cards.
- Deferred (per rian): long-tail currency formatting (comma decimals, long codes/symbols fitting).
  Design polish still pending ("close enough"). Bumped 0.5.0. Scans clean. POT regen. Not released.

## 2026-07-17 — v0.5.1: em-relative price + auto-fit to one line
- Price parts now em-relative to .bw2cp-per-month base font-size; white-space:nowrap. JS fitPrice()
  scales the base so symbol+integer+fraction+/month fit the card width on one line (measures
  offsetWidth vs price-area width; runs on load/after each price update/resize). Server sets a close
  base via base_price_size() heuristic to avoid flash. Verified: USD=84px, EUR=63px, VND=32px, SEK=73px.
- Cloudflare Q answered: CF-IPCountry header is readable at origin without account access (needs
  proxied/orange-cloud only); demoing.info is proxied so we can build+test geo there. Geo = next.

## 2026-07-17/18 — v0.5.2–0.5.4: price-fit fix, dropdown controls, controls-in-header polish
- 0.5.2: removed max-width:100% on .bw2cp-per-month (it capped the measured width so fitPrice never
  shrank; content overflowed). Now measures true content width. VND fits (base ~23-32px).
- 0.5.3: Term -> custom dropdown (.bw2cp-dropdown: toggle + hidden input[data-group] + ul menu; JS
  open/close/select, click-outside closes). Users -> compact .bw2cp-num box with stacked up/down.
  Label config default "Number of users" -> "Users". Removed pills.
- 0.5.4: Moved controls into the header under the title. Header is now a <div> (was <a>); title is
  .bw2cp-title-link. Controls restyled white-on-orange, matched 34px height, cleaner spacing.
- Chrome extension disconnected -> verified all structurally (render output), not visually.
- Open per rian: general polish continues; cross-card equal-height alignment ("one line across 3
  products"?) TBD; geolocation currency (CF-IPCountry, no CF account needed) still queued.
