Skip to content

EmbeddedEra storefront

Status: implementation in progress — isolated sandbox; not ready for live orders Updated: 2026-09-24 Related: embeddedera-site, mrp

2026-09-24 — Catalog mapping register (#274) and the social-login decision (#266)

Built scripts/mapping-register.py on the store sandbox branch (c8c840a, 9bb5655): a read-only join of the published catalog, config/erp-mapping-registry.json and the ERP by IPN, printing every reason a SKU would not complete an order, exit 1 when anything is wrong. Pure join logic checked by tests/mapping-register.py, 8 assertions, no docker or ERP needed. It writes nothing — fixing a row is a deliberate registry edit with a version bump, or an ERP change.

First run, registry 2026-09-21.1, 15 published SKUs: 12 are unmapped and return mapping_required (nine EE-C listings already carry a correct _ee_erp_part_id, so the mapping is unambiguous and merely absent; the three EE-DEMO SKUs have no ERP part at all). The bigger finding is that every ERP part behind them is salable = false, including the two that are mapped (198, 220) — order_sync.verify_erp_identities refuses a non-salable part outright and refresh-erp-catalog publishes 0 available, so fixing the registry alone would not make one SKU orderable. EE-C-0184-5PACK is a registry entry whose listing is in the trash.

The first cut of the register silently read production ERP because it took INVENTREE_URL as given; its sibling refresh-erp-catalog pins itself to the 8300 sandbox. It now prints which ERP answered and needs --allow-production to leave the sandbox. Read-only throughout; no registry entry added and no production salable flag touched, because both are decisions rather than lookups.

Decided the store's social login: Nextend free tier, Google and Facebook, Apple deferred — see store-social-login. Apple costs twice (€49 Pro plugin plus $99/yr Apple Developer Program) and the free providers do not fall short. A first research pass claimed miniOrange gave Apple away free, which would have changed the answer; the plugin page says otherwise. The durable rule out of it: never auto-link a social profile to an existing account on a matching email — require the password first, and only consider auto-link when the provider asserts email_verified.

Same day, after Angel's clarification — only the two LiPo cells (EE-C-0184, EE-C-0197) are real products; the other thirteen published listings are fixtures. Set salable = true on ERP parts 198 and 220 (production ERP, reversible, nothing else touched) and reworked the register so the ERP's Salable flag is the source of truth rather than a second hand-kept list: a salable part must be mapped and consistent, a part nobody marked salable must not be mapped. Register now exits 0 (a59fab5). Adding a real product later is one ERP tick plus a registry entry, and the register says so.

EE-C-0184-5PACK stays in the registry deliberately — order_sync resolves historical orders through it, so deleting the retired mapping would break replay of orders that used it (store-quantity-pricing already requires preserving historical pack orders). The register reports it as retired, not as a fault.

Neither battery has an ERP sale price break, so SOP-007's salable-no-price-break audit will flag both. The store charges $5.95 (0184) and $6.95 (0197) — those are the obvious candidates, but pricing is Angel's call and nothing was set.

SOP-002 was wrong and is fixed to v2.2: its checklist said "don't check Salable on a raw component", but the store sells raw components on their own. Salable is what the storefront treats as "this is a real product".

Fixture listings, 2026-09-24: the thirteen published non-product SKUs are EE-C-0001, 0083, 0086, 0151, 0152, 0164, 0165, 0167, 0183, EE-K-0001 and the three EE-DEMO-*. Angel: "Only 2 sellable parts the batteries for now the rest are fake listings." Do not "fix" them by mapping them; the register's old behaviour of reporting 12 of 15 listings as faults had made its exit code permanently 1 and therefore worthless (moved from memory store-sellable-parts, 2026-10-19).

Prices confirmed by Angel, 2026-09-24: EE-C-0184 $5.95, EE-C-0197 $6.95, both discounted at 5 or more. The storefront already carries exactly these breaks in _fixed_price_rules — 0184 drops to $4.99 at 5+, 0197 to $6.00 at 5+ — so the ERP side is a mirror of the store, not a new pricing decision. The ERP sale price breaks are not yet written: auto mode blocked the API write as a real-world transaction. Until they exist, SOP-007's salable-no-price-break audit flags both parts, and any ERP-side sales order has no price to resolve.

Second gap found the same day: both listings still carry _ee_erp_observed_available_quantity = 0, stale from when the parts were not salable. refresh-erp-catalog recomputes it but refuses any ERP but the 8300 sandbox, which is stopped by default (erp-sandbox-lifecycle), and nothing schedules it. So the store would show both batteries as unavailable even though the ERP holds 85 of each.

Store↔ERP sync designed — store-erp-sync. Three parallel research passes before any design: the ERP-owns-facts / store-owns-presentation split is standard MDM practice (one-way ERP→store is the norm, bidirectional product sync is a documented failure mode, PIM unwarranted below ~500 SKUs); no usable off-the-shelf InvenTree↔WooCommerce connector exists (the only repo is 4 commits/0 stars and syncs order status only); and timer-push carries a real documented overwrite race.

Both research passes recommended an InvenTree EventMixin plugin, and all the mixins exist in our 1.5.4 build — but the containers cannot reach each other (torrentvpn_default vs the deliberately internal embeddedera-store_internal), so the sync stays on the host, where ee-store-catalog-observe.timer already exists and is merely disabled. The safety buffer does double duty: production reserve plus absorbing orders WooCommerce has and the ERP has not yet learned about.

Corrections to earlier claims this session: wp-cli is available (2.12.0, in the scheduler container); the store timers are installed in user systemd, just disabled.

Later on 2026-09-24 — safety buffer set to 5 on both batteries (registry 2026-09-24.1, commit 796e4de): 85 unallocated − 5 = 80 published once the availability writer exists. ERP sale price breaks created to mirror the storefront exactly — EE-C-0184 $5.95/$4.99 at 5+, EE-C-0197 $6.95/$6.00 at 5+.

That exposed a live mispricing in the assistant. _resolve_sale_price read sale_price_min from InvenTree's pricing cache, which is the cheapest break on the part rather than the price for the quantity in hand. While the cells had no breaks the field was empty and the caller refused to price the line, so nothing looked wrong; adding correct price data turned that safe refusal into a silent undercharge — a single cell would have invoiced at $4.99 instead of $5.95. Fixed to select the dearest break the order actually reaches, with 7 regression tests; cherry-picked to main as 711830a (not merged — the assistant sandbox branch is 28 commits ahead and carries the cancelled inventory page). Assistant restarted, healthy. Verified live: qty 1 and 4 → $5.95/$6.95, qty 5 and 20 → $4.99/$6.00. order_sync was never affected; it prices from what the customer paid in WooCommerce.

Register blind spot closed (fadd5a2). It asked "is every listing mapped" and "is everything mapped sellable" but could not see the third case — sellable in the ERP with no listing. Four salable assemblies (Dashcam Drive, Robot Cow, Cow Remote, Cow Finger) were invisible, which is why the SOP-007 price-break audit counted four findings where two were expected. Now reported, never a fault. Open for Angel: are those four real products that need prices and listings, or was Salable set early?

The four salable assemblies, resolved. Angel: only the cow products are real. Dashcam Drive (EE-A-0001, part 179) was a Wi-Fi dashcam for the Tesla with zero stock — Salable unticked, nothing else changed, and it no longer appears anywhere. The three real products stay salable and have assembled stock on hand: Robot Cow EE-A-0002-A (21), Cow Remote EE-A-0004-A (32), Cow Finger EE-A-0005-A (41). None has a sale price break or a storefront listing yet, so the assistant will refuse to invoice them and the register reports them as sellable-but-unlisted. Next for them: approved prices, then listings.

The cow boards are distributor-only (Angel, 2026-09-24) — sold to one distributor, Paul, and not going on the website for now. Their Salable flag is therefore correct: InvenTree's Salable means "may appear on a sales order", which is a larger set than "sell it on the web". Recorded in config/storefront-channels.json so the register prints "distributor only" instead of nagging forever (2c12d80); a part not declared there still reads SELLABLE BUT UNLISTED, so a real gap stays loud.

Intended home for that flag is the ERP — a storefront key on each part's metadata — and it is not there because /api/part/<pk>/metadata/ refuses token-authenticated writes on this instance, and a PATCH of metadata on the part itself returns 200 and silently discards the field. Corrected 2026-09-24: this is NOT a config problem. The 403 blames INVENTREE_SITE_URL and INVENTREE_TRUSTED_ORIGINS, but both are correctly set (including http://127.0.0.1:8100) and six origin/referer combinations fail identically. It is a known upstream issue — inventree-mcp#32, fixed there by marking the view csrf_exempt (PR #43) — where the view loses its exemption and Django's real complaint is "CSRF cookie not set". Nothing to change in our ERP config; do not go looking. Part metadata stays unwritable by token-authenticated scripts until InvenTree fixes it or someone does a session-auth workaround, which is not worth it while the store-side file serves the need.

Blocking the distributor channel entirely: there is no customer record for Paul (the ERP has exactly one customer, LHG Contracting), no sale price breaks on the three boards, and no sales order has ever been raised. So 94 assembled boards — Robot Cow 21, Cow Remote 32, Cow Finger 41 — cannot be invoiced; _resolve_sale_price refuses a part with no break, which is correct behaviour rather than a fault. Next for that channel: create the customer per SOP-007, agree distributor pricing, then raise the first sales order.

Distributor product structure built, 2026-09-24. Paul Hale was already fully in the ERP — company 5 LHG Contracting, contact Paul Hale (Owner), plhale1105@gmail.com, +1 (919) 586-2158, 372 Pine Street, Fuquay-Varina NC 27526, B2B no sales tax. He is the only customer among 59 companies; nothing needed adding, and adding a second LHG record would have duplicated him. Open: Angel said "LHG Manufacturing" but the ERP says "LHG Contracting" — that name prints on every invoice. "LHG" appears nowhere in Paul's own emails, only in an internal Kanboard notification, so the ERP name has no independent corroboration.

Angel's prices: all three boards together $2,000, remote + finger set $750, Tie-down Roper $1,200 (its own product). InvenTree prices per part and no kit part existed, so two were created per SOP-001 (class A = sellable finished product; K is consumables/hardware bags, so not K):

  • EE-A-0006-A Cow System (pk 221) — BOM: Robot Cow, Cow Remote, Cow Finger ×1 each — $2,000
  • EE-A-0007-A Cow Remote Set (pk 222) — BOM: Cow Remote, Cow Finger ×1 each — $750

EE-A-0003 stays skipped; sequence numbers are never reused. The three boards needed component = True before InvenTree would accept them as BOM lines — it rejects a non-component sub_part with "object does not exist", which is a confusing error for a part that plainly exists.

Validation: the new structure reproduces Yara's legacy invoice 1002 exactly — 3 × Cow System at $2,000 plus 2 × Cow Remote Set at $750 = $7,500, matching the recorded total. The legacy identifiers EE000005/EE000004 map to these two kits.

EE-A-0008-A Tie-down Roper (pk 223) added — the same three boards as Cow System, sold at $1,200. Angel: "the same three boards, but then we apply a discount to get it to $1,200." Modelled as its own salable part with an identical BOM rather than a discount on the Cow System line, because the kits carry no stock of their own (the boards do), so two kits sharing components splits no inventory, and the invoice then names the product the customer actually ordered. Consequence to check: the invoice line will read "Tie-down Roper $1,200", not "Cow System $2,000 less $800". If the discount needs to be visible as a discount, this is the thing to change.

Decisions, 2026-09-24 (Angel): LHG Contracting is the correct company name — the ERP was right, no change. Kits only — Salable unticked on the three bare boards, so the sellable set is now exactly five parts: the two LiPo cells (storefront) and the three cow kits (distributor). component stays True on the boards because the BOMs need it; salable is what decides sellability. The distributor declarations in config/storefront-channels.json moved from the boards to the kits (e1a7c83), since a non-salable part never reaches that report. Form E-595E dropped — outreach stays on hold; recorded as a comment on Kanboard #52 rather than closing it, because it is a live tax question rather than an admin chore: NC treats a B2B sale as taxable unless it is for resale, so invoicing with no tax and no certificate leaves EmbeddedEra liable for the uncollected amount, on kits of $2,000 / $1,200 / $750. It does not block the first order and belongs in the CPA sales-tax hour that task already references.

Superseded: the three boards remain salable as spares with no price on file, so the assistant refuses to invoice them individually (correct, not a fault) — Angel needs to set spare prices or untick Salable on them. The Tie-down Roper at $1,200 has no part yet; its contents need defining. And the exemption-certificate outreach is still on hold, which matters because SOP-007 wants Form E-595E on file before the first invoice.

Availability writer built and proven (0d66b17). scripts/publish-availability.py pushes unallocated_stock − safety_buffer into WooCommerce _stock. Verified end to end against the sandbox: 100 → 80 on both cells, price and _ee_listing_release untouched, second run reports "already correct". A separate file from refresh-erp-catalog on purpose — that one asserts it changed nothing and the guarantee is worth keeping. It verifies every mapping before writing any row, refuses a product not managing stock, and re-reads price and release after each write. No WooCommerce REST key was needed: it reuses the existing wp eval path through the wpcli profile.

sellable() is pure and tested: a buffer wider than stock publishes 0 rather than a negative (WooCommerce reads a negative as a backorder), and a part that is no longer salable publishes 0 rather than being skipped — skipping would leave the stale figure in place, which is how a delisted part keeps selling. A test also caught whole() surfacing a raw decimal.InvalidOperation for a garbage ERP value instead of naming the bad field; fixed.

Sandbox ERP gotchas found while testing. erp-sandbox up --fresh failed at invoke update — InvenTree's internal pre-update dbbackup could not authenticate, because the sandbox's postgres volume had survived from an earlier run with a different password. erp-sandbox reset --from <stamp> (which does down -v) fixed it, and reusing an existing stamp avoids taking a second production backup. A reset also invalidates the API token, so .local/order-sync.env needs refreshing from erp-sandbox token afterwards; the store's tooling returns a bare 401 until that is done. Sandbox stopped again per erp-sandbox-lifecycle.

Launch checklist compiled — store-launch-checklist, from the live configuration rather than memory. Headline findings: shipping is entirely absent (zero zones, zero methods), which makes checkout impossible for physical goods and is the one gap nobody had named; the LiPo cells are Class 9 hazmat, so carrier service class and package marking must be settled before shipping is configured, and USPS now charges a $50 non-compliance fee with the mark format changing 31 December 2026; the only tax rate is a placeholder literally named "Sandbox tax — test only" at a flat 7% when NC is destination-based; and the store address is empty, which is both the tax and shipping origin.

Fixed in passing: woocommerce_refund_returns_page_id pointed at page 9 — WooCommerce's boilerplate "This is a sample page" draft — so the refund policy link led somewhere no visitor could see. Repointed to the real published policy (158) and the placeholder trashed.

Confirmed scope

WooCommerce; open-source and minimal recurring service fees; easy listing creation/editing; custom features must remain possible. Stocked products plus kits/made-to-order; US and international shipping; shared inventory with other sales channels. Exact other channels and supported countries remain unconfirmed. Implementation authorized on 2026-09-19 using Claude workers. Production launch and paid purchases remain pending integration acceptance.

Public shop at store.embeddedera.com with matching brand and links from the existing marketing site. WordPress/WooCommerce own the public catalog, cart, checkout and customer order view. InvenTree owns stock, component allocation, builds, and fulfillment. The payment processor owns payment settlement; accounting reconciles orders, fees, refunds and payouts. Public prices are a storefront requirement and do not automatically change the marketing site's pricing policy. Use a conventional WooCommerce storefront initially, a lightweight maintained theme, and independent custom plugins. Avoid core edits and a separate headless frontend until a demonstrated requirement justifies them. Choose maintained extensions compatible with current checkout blocks and High-Performance Order Storage (HPOS); test actual combinations.

Product and listing workflow

Select an ERP part and explicitly opt it into the public catalog; create a draft, enrich sales content, preview, publish. This is a desired workflow, not an existing integration. Use stable part IDs plus SKU/variant mappings; preserve mappings through renames. Internal costs, supplier details, notes and BOMs stay private unless specifically selected for publication. ERP owns identifiers, stock and manufacturing data. WooCommerce owns sales copy, photos, public specifications and selling prices by default. Choose any field-copy rules explicitly; stock updates never overwrite sales copy. Each variant maps to a sellable ERP identity. Archive discontinued listings rather than deleting order references. Listing template: photos, title, price, specifications, compatibility, contents, revision where applicable, manuals/download links, packed weight/dimensions, lead time, warranty/support links. Support phone editing and duplicate-as-draft. Determine English/Spanish launch scope before finalizing the editor. Stocked goods and prepacked kits sell from finished stock. Assemble-on-order kits reserve their actual components. Made-to-order products require material availability and production capacity; disable unlimited backorders. Freeze ordered configuration/revision so later catalog changes do not alter accepted orders.

Inventory and integration contract

Evaluate maintained community connectors against installed versions and the full lifecycle before choosing one. No complete production-ready InvenTree/WooCommerce connector has been verified in this planning session. Build only missing integration behavior if existing options fail documented requirements. Choose one reservation authority and define all stock transitions; never let WooCommerce and the ERP independently deduct the same physical units. Calculate sellable stock excluding allocations, quarantine, rejected tests, unsuitable locations and safety buffers. Reserve during final checkout, not when a casual visitor adds to cart. Release expired/failed reservations; handle late successful payment explicitly through re-reservation or an exception/refund path. Use unique channel/order/event identifiers to prevent duplicate orders, reservations and shipments when notifications repeat. Authenticate event deliveries, retry transient errors through a persistent queue, tolerate out-of-order events, and reconcile periodically. Payment success in the provider must be recorded even if ERP processing is unavailable; hold fulfillment and alert for unresolved allocation. Never infer payment success solely from the browser return page. Fail closed for new stock-dependent checkout when inventory cannot be verified, unless a separately allocated offline allowance is explicitly designed. Other channels may not support atomic reservations: use exclusive channel quantities or buffers, track update age, and define outage withdrawal behavior. Do not promise zero overselling from periodic quantity sync alone. InvenTree direct sales must allocate before the stock is promised. Refunds do not automatically restock; inspect returned goods before releasing them. Retain serial/batch-to-shipment traceability where applicable.

Checkout, shipping and payment defaults (proposed)

Guest checkout plus optional accounts; USD initially for all destinations. Show stock/lead time, shipping costs and any import-charge treatment before payment. Stripe official extension is the initial payment candidate; fees still apply. Use provider-handled card fields; never store card numbers in the ERP. Fixed-price stocked goods and standard made-to-order items can use normal checkout once availability and lead time are dependable. Bespoke engineering work uses the existing quote process until scope and payment terms are agreed. Do not rely on an indefinite card authorization for a long production job; authorization windows expire. Deposits remain a later, separately designed feature. Mixed carts ship together by default with the longest lead time disclosed; offer split shipments later only with accurate per-shipment charges and stock accounting. This default is not yet a user-approved shipping policy. Support US plus an explicit international country allow-list; do not enable every country automatically. Select destinations based on actual products, carrier services, applicable selling requirements, and workable duties/tax handling. Record customs description, origin, classification and package data. Destination-specific tax, consumer-policy and import requirements need verification before activation. Compare actual shipping quotes on representative packages before selecting flat rates versus live rates. Checkout rate calculation, label purchase and tracking updates are separate requirements. Never display a guessed free/zero rate when a shipping lookup fails. Customs classifications and commercial policies need business confirmation, not model invention.

Emails and customer care

Prioritize receipts, payment/order exceptions, delay updates and shipment emails over marketing. Configure authenticated sending, bounce handling and queue monitoring using an existing suitable service if capacity and purpose permit. Do not assume the current internal email allowance can also carry commerce mail. Proposed cart recovery: one reminder around 4 hours and another around 24 hours, no automatic coupon, only eligible addresses with appropriate consent/unsubscribe handling. Suppress after purchase, unsubscribe, stock loss or invalid address. Restore a cart only after revalidating prices and stock; do not reserve stock for abandoned carts indefinitely. Final timing and eligibility require extension validation. Publish shipping, cancellation, returns, warranty, privacy and contact information. Define standard versus custom-product handling and international returns before launch. Assign Sales/customer service ownership and a response target. Provide order lookup/tracking without exposing another customer's details.

Hosting and operations

Evaluate existing hardware before committing to production hosting. An existing-server pilot avoids a new hosting bill but must pass capacity, isolation, connectivity and recovery checks. Keep public WordPress isolated from ERP/database and unrelated server services; use scoped integration credentials. Keep administration protected while allowing public storefront and authenticated payment callbacks to function. Use TLS, individual staff accounts with minimum privileges and strong authentication, supported software and monitored updates. Cache public catalog pages only where appropriate; bypass personalized cart/checkout/account pages and callbacks. Use a real scheduler and monitor failed or delayed background jobs. Maintain staging with sandbox ERP, test payments, and technically blocked real customer email/channel writes. Deploy code changes without overwriting live orders with a staging database. Backup database, uploads, configuration and recovery material off-host; test restoration. Proposed recovery targets: at most 15 minutes of order-data loss and restoration within 4 hours, subject to feasibility and cost validation. Reconcile provider payments and channel orders after restoration; replaying imports must not duplicate orders. Dashboard: unfulfilled paid orders, overdue production, failed/stale sync, low stock, failed jobs/email, payment discrepancies and backup freshness. Alert only actionable exceptions through existing internal tooling. Document repeatable listing, fulfillment, returns and outage procedures in the existing SOP system during implementation, cross-referencing existing ERP procedures.

Low-cost component strategy

Core: self-hosted WordPress/WooCommerce, a maintained free theme, official Stripe extension. Evaluate the free Cart Abandonment Recovery extension for compatibility and sending behavior before adoption. Shipping labels/rates, kit/configurator handling, tax automation and channel connectors remain evaluation items; no promise that all are free. Avoid a paid bundles extension for simple prepacked kits represented as their own ERP stock item. Configurable/component kits require explicit integration tests with any selected extension. Maintain a small extension register: purpose, license/recurring fee, update owner, compatibility and replacement path. Do not install overlapping inventory or checkout engines. Budget separately for processing, postage, domain, email volume, infrastructure and maintenance effort. No automatic paid subscriptions.

Delivery sequence and acceptance gates

  1. Integration proof in sandbox: stocked item, shared-component kit, made-to-order item and actual other-channel order. Verify last-unit concurrency, shared-component competition, duplicate/out-of-order events, ERP outage, reservation expiry/late payment, cancellation and partial refund. This gate precedes major design work.
  2. Branded storefront and listing workflow: representative catalog, phone editing, guest checkout, test payments and candidate shipping services. Validate accessibility, search, readable specifications and totals on a phone.
  3. Operational completion: production/fulfillment, tracking, customer emails, cart recovery, reconciliation, policies, restricted international destinations, restore drill and staff procedures.
  4. Small public catalog launch after complete purchase-to-return validation, then expand destinations, channels and optional capabilities. Keep complex unsupported configurations quote-only until verified.

Remaining business inputs

Exact channels; first products and BOM examples; initial international destinations; carriers/accounts and representative parcels; existing payment account; production lead times/capacity; cancellation/returns/warranty terms; USD and mixed-cart defaults; language scope. These are pending inputs, not implied approvals. Continue independent technical planning without repeatedly asking all questions together.

Sources checked 2026-09-19

  • https://woocommerce.com/document/high-performance-order-storage/
  • https://developer.woocommerce.com/docs/best-practices/performance/configuring-caching-plugins
  • https://docs.inventree.org/en/stable/sales/sales_order/
  • https://woocommerce.com/document/stripe/setup-and-configuration/stripe-webhooks/
  • https://woocommerce.com/document/stripe/admin-experience/authorize-and-capture/
  • https://woocommerce.com/document/bundles/bundles-configuration/
  • https://wordpress.org/plugins/woo-cart-abandonment-recovery/
  • https://woocommerce.com/document/setting-up-shipping-zones/
  • https://woocommerce.com/document/setting-up-taxes-in-woocommerce/

Log

  • 2026-09-19: Recorded confirmed product/destination/multichannel scope and a recommended implementation plan. Architecture, operational defaults and launch gates remain proposals; no systems deployed or services purchased.
  • 2026-09-20: Opened dev.embeddedera.com as a public, non-indexed, read-only feedback preview while retaining Cloudflare Access on /wp-admin* and /wp-login.php. Anonymous commerce/account/B2B/review/waitlist writes are blocked; the rate-limited first-party feedback form stores private staff records without source IP. External access and mutation checks plus 15 native, 26 review-browser, 152 storefront and 28 search checks passed. SOP-040 owns triage.
  • 2026-09-20: Enabled the bounded public fictional sandbox through 2026-09-27 14:05 UTC. Visitors can create marked @example.invalid customers and use native cart, Stripe test/no-payment checkout, own-order history, pending B2B requests, verified reviews and feedback. Cloudflare now bypasses /create-account; account/checkout routes were externally DYNAMIC, while staff paths remained Access-gated. Public Stripe order 219 succeeded and appeared in its owner history, order 220 declined, and 219 was fully refunded; stock was checked and restored to 100. Both orders are permanently excluded from ERP sync and fulfillment, with empty stored IP/user-agent. Five concurrent real-database limiter calls admitted exactly three and stored no raw IP. Source commits 5215cc5 plus the deployment-status follow-up; SOP-035 v4.6 owns operation and kill switch.
  • 2026-09-20: Created production ERP part EE-C-0197/220 and development product227 for the photographed ULi-402535 3.7V 320mAh battery; see store-battery-320mah. Owner-reported purchase cost $1.50 is recorded pending supplier/landed-cost evidence. ERP stock item86 records an estimated opening count of about85 at Shop/Shelf A/Bin03 and requires recount before release. Store price is $6.95 each and $6.00 each for 5+; five-unit cart acceptance returned $30.00. Two professional actual-sample views, search, phone/desktop layout, explicit fictional checkout stock100 and pending production release verified.

  • 2026-10-13: production ingress read unhealthy for 2+ days while the store served fine. The health check fetched wp-login.php, which redirects to https://store.embeddedera.com/wp-admin/; busybox wget followed it and the container cannot reach the public host, so every check timed out. Check now fetches / (sandbox a0b9670, production 42e0ea5, test updated). Sandbox then production ingress recreated by Angel; both healthy, store and site 200.

Expanded scope: automated fulfillment, B2B and flexible payments

Added 2026-09-19 following further user direction. Keep the architecture open to the cases below; inclusion is a planning requirement, not authorization to grant credit, buy postage or deploy. User wants routine orders automated through shipping-label printing so physical work is packing and shipping. Develop toward comprehensive capability in stages rather than silently limiting product ownership/payment models.

Automated fulfillment target

Confirmed payment or approved account terms plus ready/released inventory triggers address/package/service validation, postage purchase and printing of carrier label and packing slip. Made-to-order waits for completion and quality release. Track label purchased, print submitted, packed, dispatched and carrier accepted separately. A printer accepting a job does not prove physical printing; ambiguous failures allow controlled reprint of the existing label, never automatic repurchase. Persist provider shipment/label identifiers and resolve unknown purchase outcomes before retrying. Add label void/refund handling for cancellations and changed shipments. Use packaging profiles, shipping spend limits, customs completeness and exception queues. Confirm existing Zebra model/media compatibility before promising 4x6 carrier printing. A final packing barcode scan is proposed. Test printer outage, duplicate events, label-purchase timeout, cancellation after label purchase and carrier tracking delays.

B2B customer portal

Company accounts with multiple users, roles, company separation enforced on every API/document, approved addresses and optional purchasing approvals. Portal inventory categories: ready, reserved, in production, awaiting materials, customer-owned and shipped; keep ownership, reservation, payment and quality state independent. Support customer-owned/consigned materials and finished stock as well as seller-owned reserved stock without assuming the current ERP model supports all distinctions natively. Investigate explicit ownership/allocation representation before implementation. Customers can request partial releases from held inventory or blanket orders, view quantities ordered/built/held/released/shipped/remaining, and see customer-facing production milestones and last-update timestamps. Release requests revalidate availability, authority, financial eligibility and shipping; do not recharge already-owned/paid goods. Storage/handling/postage charges have separate agreed line items. Support company catalogs/prices, accepted quote revision, customer PO references, repeat ordering, invoices/credits/statements, test reports, serial/batch traceability and warranty/return requests. Forecasts and low-stock replenishment proposals are supported planning cases; binding automatic orders require an explicit standing agreement with limits. Raw supplier data, internal costs/notes and other customers' inventory remain private.

Payment methods and account terms

Support cards/wallets, checks, domestic/international bank transfer/wire, and evaluate ACH integrations. WooCommerce core offline bank/check options record a payment choice and hold the order; they do not verify bank receipt or deposit a check. ACH debit and customer-initiated bank transfer are different integrations. Online provider confirmation, bank-feed/API reconciliation, or authorized finance confirmation must establish the payment state. A customer upload, screenshot, remittance email or claim of payment is not confirmation of funds. Check receipt/deposit/availability and returned-payment handling are separate states under the business's release policy; do not promise irrevocable settlement merely because funds appear available. Support deposits, milestone invoices, partial payments, one payment covering multiple invoices, split methods, credits, overpayments, shortages, transfer fees/currency differences, offline refunds and returned/reversed payments. Assign one accounting receivables ledger as authority after reviewing the existing accounting stack; WooCommerce and InvenTree consume matched status rather than independently declaring invoice balances. Preserve amount/currency/reference/date/source, allocation history and audited corrections. Ambiguous matches enter a finance exception queue; never match solely by amount. Approved companies may receive Net 15/30/60 or other negotiated terms with explicit credit approval, exposure limits, due dates, overdue holds and audit trail. A PO number is a reference, not payment or credit approval. Keep financial state (unpaid/part-paid/paid/overdue/refunded), fulfillment state and release eligibility separate. Do not mark a credit sale paid to make it shippable. Proposed default: payment before release for unapproved accounts; verified credit terms permit release while balance remains due. Production-start/deposit requirements may differ from shipment-release requirements and must be configured per agreement. Offline orders need explicit reservation deadlines, reminders and escalation; do not assume WooCommerce's pending-payment timeout expires on-hold bank/check orders. Rules can allow agreed long reservations for B2B orders without letting unpaid retail orders tie up all stock indefinitely. Automatic overdue reminders should stop on reconciliation/dispute and avoid duplicate customer messages. Payment instructions are controlled business configuration; restrict bank-detail edits and provide trusted verification for changes. Auto-reconciliation depends on actual bank/provider capabilities and may have costs; document manual fallback. Physical check handling cannot be promised fully automated without a bank deposit service. No bank instructions/credentials stored in this planning note.

Additional acceptance tests

Company A cannot view company B inventory/documents or request its stock, including by guessed IDs. Simultaneous partial releases cannot exceed held stock. Customer-owned goods are not billed twice. Deposit does not incorrectly mark full invoice paid. Approved credit shipment remains financially unpaid. Duplicate bank feed imports do not create duplicate payments. One remittance can allocate across invoices without losing balance. Returned payment triggers the correct hold and preserves audit history. Offline refunds are not marked executed until confirmed. Reminder and label queues honor financial and fulfillment states.

Payment references

  • https://woocommerce.com/document/bacs/
  • https://woocommerce.com/document/troubleshooting-payment-errors/
  • https://woocommerce.com/document/managing-orders/order-statuses/

Moved out: the 2026-09-19 implementation checkpoints and the Development preview log are in embeddedera-store-archive.

Next integration inputs and gates

Payment and carrier/label-account names were requested again after the verified checkpoint; no credentials should be sent in chat. Exact channels, initial products/part mappings, destinations, packaging, company ownership model and accounting authority are still needed. Continue with actual sandbox ERP order/allocation proof and provider test accounts; do not treat the fixtures as working live automation. Production launch, actual postage, bank reconciliation, printer/media proof, policies, alerts and restore acceptance remain open.

  • 2026-09-19: Source checkpoints committed: f4d8d6b (B2B portal) and b741ff3 (runtime/extensions/scheduler/fulfillment); source working tree clean. SOP-035 preview and production documentation deployments succeeded; the procedure remains draft. No storefront production deployment performed.

Moved out (continued): the 2026-09-19 implementation checkpoints and the Development preview log are in embeddedera-store-archive.

Open — customer social login

  • Deliver Google, Apple and Facebook customer login per store-social-login; Kanboard tasks #266, #267, #268, #269, #270, #271 own assignment and status. All in Backlog, Angel triage owner, default review2026-10-19; provider accounts/cost and acceptance gates recorded.

  • 2026-09-19: At Angel’s request, saved social-login plan and created six assigned dependency-linked register tasks; no plugin, networking or provider configuration changed.

Completed — performance

  • Activated and externally verified three documented Cloudflare Cache Rules plus Tiered Cache with Smart Topology. Ruleset a6975addc32a4bb9b435a0cec1f4cb72 caches anonymous public catalog HTML and public static assets while explicitly bypassing dynamic/private traffic. Required behavior and rollback remain in /home/angel/embeddedera-store/docs/PERFORMANCE.md.

Open — Cricket listing

  • Cricket store draft (EE-A-0003-A, no price, hidden): see store-cricket-listing. Needs the sandbox stack started to run scripts/configure-cricket.php, create the sandbox ERP record and preview; classifier denied it 2026-10-06. Owner decisions listed there.

Open — payments and launch

  • Complete store-launch gates, assigned Kanboard272–279; account/shipping/policy dependencies reuse23/25/52/138. Default review2026-10-19; Angel triage owner.

2026-09-19 — Expanded test catalog and launch tasks

Created and verified eight assigned payment/launch tasks272–279. Ten additional ERP-observed stocked components published as fictional test checkout listings101–110 across ten categories; see store-test-catalog. No production ERP mutation. Mixed browser order114 verified three lines/$31.94/unpaid/test then cancelled; new SKU synchronization correctly reports mapping_required pending task274. Recently viewed now has an explanatory empty state and archive placement immediately below catalog; source733f066, native fixture checks passed.

2026-09-19 — Navigation redesign direction

Angel rejected recently viewed and its squeezed product-page cards; remove recording/rendering entirely. Persistent Products/category navigation inspired by Waveshare and stable help/learning links inspired by Adafruit replace category ribbons. Appearance controls move to footer. Real development help/payment/shipping/FAQ pages115–118 published with test-state disclosures; no new live policies claimed. Task277 updated; task280 assigned to Angel/Engineering for useful product docs and a supported wiki/forum decision. Shared preference saved/mirrored.

Navigation acceptance:124browserchecks across375/1440passed (Products on every page, cross-category clicks, search, no recents/cookie, help, Escape, appearance persistence, no horizontal overflow); six additional checks passed Shop breadcrumb, excerpt spacing and actual native cart. Six native fixture checks passed bounded public-category navigation and retired history. Screenshots reviewed. Responsive catalog grid3/2/1 replaces oversized rows. Final small duplicate-search cleanup is validated separately.

Final source1d7e1a5: resolved native Woo grid specificity; independent desktop computed columns437/437/437px at1440, Products menu grid and single visible search confirmed. Worker final fixture suite7checks passed. Help bootstrap and excerpt separator committed; source tree clean. SOP-036v1.5 preview/main deployments complete.

2026-09-19 — Contained customer navigation

Home now stays at the store catalog; local About138, Contact139 and Engineering Notes140 pages preserve the shop header, cart and account context. Footer links use those local destinations. Explicit full-company and article-library links open new tabs. Main-site system font stack copied exactly; five-item cart renders a separated badge. Browser acceptance passed38checks across375/1440 with no errors or overflow; external-link audit found only the two explicit new-tab destinations. Catalog fixture suite7checks passed.

2026-09-19 — Modern customer account entry

Current WooCommerce11.1.1 still ships the classic side-by-side login/register template; this was not an old cloned repository. Theme override version9.9.0 keeps native Woo nonces, handlers and database while presenting one sign-in card. Create Account150 and Track Order151 are separate managed pages. Browser acceptance passed28checks at375/1440 plus a real fictional register→logout→email/password login cycle; fixture customers31–33 were confirmed orderless and deleted. Outgoing email/passwordless codes and social providers remain disabled.

2026-09-19 — Customer policies and battery shipping preparation

Published managed Terms of Sale155, Privacy3, Shipping157, Returns and Refunds158, and Warranty/Product Use159 in the gated preview; footer and Woo terms/privacy destinations point to them while development warnings remain. The policy installer preserves manual edits and handles the untouched WordPress privacy draft. Updated EE-C-0184 copy keeps $5.95 / 5+ at $4.99, approximate cell dimensions and conservative LiPo handling while omitting unverified charging/discharge limits. Production release remains pending on actual photo, manufacturer/test evidence, polarity, count, compliant package/carrier and ERP allocation. Source/live verifiers passed and 52 native browser checks passed at375/1440 with no overflow or JavaScript errors. SOP-038 documents the step-by-step real shipping setup; it remains draft pending a carrier/package walkthrough. Claude Haiku research with Sonnet synthesis and Claude Sonnet implementation were independently reviewed; CLI list-price estimates were $0.587287 and $0.869317, not subscription charges.

Replaced the header's basic WordPress search with pinned, self-hosted Advanced Woo Search3.71 in the gated development store. Exact and partial SKU, product name/content, category/tag, synonyms, autocomplete and typo correction are enabled; SKU and title have the highest relevance weights. Search analytics is disabled. Hidden demo products and a temporary draft fixture were excluded. Native acceptance passed11checks for EE-C-0184, 0184, EE-C-0165, batery, Batteries, configuration and visibility; browser acceptance passed16checks at375/1440 with no external requests, overflow or JavaScript errors. Large-catalog performance and zero-result reporting remain future gates. SOP-039 owns search maintenance. Claude CLI audit actually ran Sonnet5 despite the requested Haiku alias and reported a $0.5034726 list-price estimate, not a subscription charge.

  • Dark-theme search contrast correction: plugin focus CSS had forced a white field while theme text remained light. Theme now owns focused input background, text fill, placeholder, caret, clear control and search button colors. Expanded browser acceptance passed26checks at375/1440 across dark and light computed colors with no errors, external requests or overflow.

Pinned and installed Two Factor0.16.0, Simple History5.33.0, FluentSMTP2.4.0, Advanced Shipment Tracking4.0.3, Simple Cloudflare Turnstile1.43.2, The SEO Framework5.1.4, Filter Everything1.9.6 and Back In Stock Notifier7.4.3 in the gated development store. Safe defaults leave SMTP/Turnstile credentials absent, indexing off, staff2FA unenforced, shipment tracking unable to auto-complete orders and filters empty. Back-in-stock uses policy consent, subscription acknowledgement, ERP-style five-minute stock polling and theme-matched responsive form styling. Native43 checks passed exact versions, safeguards, scheduled stock polling, stored fixture subscription, one sandbox-intercepted notification and subscriber REST privacy; browser14checks passed375/1440 with no overflow/errors/external requests; search reran26/26. Temporary product/subscriber removed. Real mail, Turnstile,2FA recovery enrollment, carrier tracking and ERP-backed filters remain launch gates. Claude Haiku4.5 audit reported a $0.1961536 CLI list-price estimate, not a subscription charge; findings were independently checked against source and runtime.

2026-09-19 — Free storefront performance phase

Angel chose McMaster-like responsiveness as the store performance goal while retaining a free/open-source core. The warm origin already measured 1–3 ms TTFB, but the public HTML remained Cloudflare DYNAMIC, the socat relay produced long-tail failures under concurrent keep-alive traffic, and the theme downloaded both light/dark 35 KB logos. Replaced socat with pinned, read-only Caddy using bounded upstream reuse and Docker DNS; removed/disabled the obsolete address-refresh timer. The header now downloads one SVG and recolors it for light/Auto, reducing the phone resource transfer from about156.9KB/35requests to142.5KB/34requests. Added a fail-closed origin edge-cache policy: only anonymous public GET/HEAD HTML is eligible for a five-minute Cloudflare cache; commerce/account/B2B/feedback/API/query/session/error traffic sends no-store. Acceptance passed63 host unit,189 WordPress,30 live HTTP,13 performance-browser and172 storefront-browser checks; three independent 1,000-request/25-concurrent origin runs had zero failures,490–511req/s and68–92ms p99. Claude Sonnet5 implementation list-price estimates were $2.1326437 and $1.3163178, not subscription charges. After Angel expanded the scoped token permissions, Cloudflare ruleset a6975addc32a4bb9b435a0cec1f4cb72 was created with enabled ee_store_cache_public_html, ee_store_bypass_dynamic and ee_store_cache_static_assets rules; Tiered Cache and Smart Topology were enabled. External checks observed MISS then HIT for /shop/, /product/ee-c-0184/ and versioned CSS, JavaScript and images. Cart, account, B2B, feedback, REST, sorted/query catalog and session-cookie requests remained DYNAMIC with the bypass policy. /wp-admin/ and /wp-login.php still redirect to Cloudflare Access. Warm public HTML samples measured about64–132ms TTFB. Desktop browser samples measured shop/product LCP near400/596ms; a 4× CPU, 1.6Mbps/80ms mobile profile measured about1.34/2.04s with zero layout shift. Exact rules and rollback are in repo docs/PERFORMANCE.md.

2026-09-20 — Product media and frontend asset reduction

Implemented and deployed commit c3caa6c. Simple products with exactly one real image now keep WooCommerce's native tabs/reviews but omit zoom, FlexSlider, PhotoSwipe and their styles; no-image, multi-image, variable and video/ambiguous products retain the complete native gallery. A temporary multi-image fixture verified all five native gallery assets and was deleted. WordPress now creates WebP JPEG/PNG subsizes when supported, with a bounded idempotent bootstrap regeneration for existing public product attachments; originals remain. Battery53's displayed600×480 image fell from41KB PNG to7.7KB WebP. Public pages omit jQuery Migrate while retaining jQuery core, Woo/search/cart/attribution scripts. Catalog-navigation CSS is merged into the theme; the old file remains unreferenced for stale-cache compatibility. Three-run warm external medians: shop desktop33requests/144,792B/LCP372ms; product desktop30/132,519B/LCP304ms; throttled mobile shop32/136,612B/LCP1,320ms; product30/132,519B/LCP1,204ms; CLS0. The product improved from39requests/~274KB to30/~133KB, about52% fewer bytes and23% fewer requests. Final acceptance passed200 native cache,43 performance-browser,172 storefront,28 search-browser,26 public-review and19 edge/ingress checks. Older quantity/catalog/recommended-plugin fixture tests still fail at their pre-existing SKU-less product creation under the listing guard and were not counted. Implementation worker was Codex gpt-5.6-terra because fresh capacity showed Claude weekly80% and Fable94%; worker cost was unavailable. Exact behavior, verification and rollback: repo docs/PERFORMANCE.md.

2026-09-20 — Automatic edge purge, empty sessions and deployment budgets

Implemented work items4–6 on sandbox. WooCommerce's native Clear Customer Sessions When Empty and product-instance caching are enabled. Catalog events enqueue bounded exact development URLs in a concurrency-safe versioned/leased WordPress table; a host-only one-minute systemd worker purges Cloudflare and acknowledges only successful versions. Cloudflare ruleset a6975addc32a4bb9b435a0cec1f4cb72 now includes its internal PURGE method so exact invalidation actually evicts. Live product-event proof queued/acknowledged four URLs and changed EE-C-0184 from HIT to MISS then refill HIT; queue ended empty. start-sandbox.sh now fails if the three-run public browser/cache performance gate exceeds request, bytes, TTFB, mobile LCP or CLS budgets, or if dynamic routes are cached. Final medians: desktop Shop33requests/144,792B/LCP396ms and product30/132,519B/LCP260ms; throttled mobile Shop32/136,612B/LCP1,236ms and product30/132,519B/LCP1,188ms; TTFB≤25ms, CLS0. Acceptance passed74 host,16 live queue,204 WordPress cache,30 HTTP and172 storefront checks. Implementation/review used Codex gpt-5.6-terra due Claude weekly80%/Fable94%; CLI cost unavailable.

2026-09-20 — EE-C-0184 actual-sample media and pinout

Angel supplied an actual battery photograph and polarity reference. Product53 now uses an optimized featured image prepared from the sample and a technical diagram below Specifications. The diagram is explicitly oriented mating-face/latch-up and identifies left cavity black/GND (−) and right cavity red/VBAT (+); numerical pins and connector family remain withheld until the exact series is verified. The page keeps one gallery image for the lightweight Woo path; a 300-pixel WebP diagram links to the full-resolution WebP. Updated listing SOP requires oriented cavity assignments and meter verification. Policy/source checks and responsive phone/desktop review passed. Production release remains pending.

2026-09-20 — Stripe test credential checkpoint

Validated the user-provided Stripe test pair without exposing values, confirmed the secret against the US Stripe account, and successfully created/cancelled a test PaymentIntent. Stored the credentials in WooCommerce test settings and a mode-0600 host file; the gateway remains disabled. Bootstrap now preserves configured settings. WordPress still has deliberately blocked outbound HTTP/network access and no public signed webhook route, so native checkout/refund acceptance remains the next payment gate. Stripe reports live-account charges_enabled=false and payouts_enabled=false; production onboarding is not complete.

2026-09-20 — Stripe sandbox payment acceptance

Deployed official Stripe11.0.0 in test mode through a pinned exact-host Squid egress sidecar; WordPress/db/Redis stayed internal-only and general outbound HTTP/direct sockets remained blocked. Created the exact official test webhook event set for https://dev.embeddedera.com/?wc-api=wc_stripe; signed payment/refund events succeeded, live-shaped invalid input returned400/DYNAMIC, and admin/login retained Access302. Browser order212 paid$5.95 with Stripe's test Visa, saved payment evidence, then a native full refund moved it to refunded; fictional battery stock restored to100. Browser order214 used the generic decline card and remained failed/unpaid without stock loss. Proxy-down failed closed and recovered. No real funds, production ERP, postage, printer or customer email. Main source79d638d; implementation worker Codex gpt-5.6-terra, CLI cost unavailable; 68 host regressions and runtime extension checks passed. Production remains blocked on live Stripe onboarding plus inventory/shipping/tax/email/recovery gates.

2026-09-21 — Development sales-tax fixture

WooCommerce tax calculation had been disabled and its rate table was empty. The dev store now calculates a labeled 7% Sandbox tax — test only for US/NC shipping addresses, based on shipping destination. Native checkout showed $0.91 tax on a $12.90 two-item cart and a $13.81 total; direct Woo checks produced $0.49 before currency rounding on one $6.95 item, $2.10 on five at the $6.00 tier, and no fixture rate for Virginia. The fixture is idempotently managed by scripts/configure-sandbox-tax.php. It is explicitly not production-ready; current NC rates vary by destination county, and production remains gated on registrations/collection obligations plus an address-level tax service or complete official rate data.

Stripe Tax follow-up: installed official Stripe Tax for WooCommerce2.1.0, validated and stored the existing test key without exposing it, configured test mode with General — Tangible Goods (txcd_99999999), and created an active NC test registration (livemode=false). The plugin SDK now uses the existing exact-host Stripe proxy; WordPress still has no unrestricted egress. Stripe reports Tax status pending because head_office is missing. The account's existing support address is in California while the store default is NC, so it was not copied. Automatic Stripe Tax is disabled and the working 7% sandbox fixture was restored pending Angel's actual business head-office address. Test registration is simulation only, not legal registration.

2026-09-21 — Planned ERP-authoritative inventory in development

Angel selected launch item3 for the next development plan. The plan extends the existing repeat-safe Woo→InvenTree order bridge instead of creating a second stock engine: shared versioned SKU mapping; fresh read-only ERP availability; private credential-isolated reservation broker; durable reservation ledger; synchronous all-lines allocation before Stripe; idempotent expiry/cancellation release; late-payment re-reservation or fulfillment hold; and periodic three-way reconciliation. Initial enrollment is staff-only for EE-C-0184/ERP198 and EE-C-0197/ERP220 against the ERP sandbox. Required acceptance includes last-unit concurrency, partial-cart rollback, stale/outage closure, ambiguous-result inspection, decline/expiry release, late payment, cancellation, refund inspection and replay. Production remains excluded. Source plan: repo docs/ERP-INVENTORY-DEV-PLAN.md.

2026-09-21 — ERP inventory Phase 1 mapping and observation

Implemented the development-only Phase 1 checkpoint. A validated, versioned registry now maps EE-C-0184 to ERP198 and EE-C-0197 to ERP220 at one physical unit per store item; the retired EE-C-0184-5PACK mapping remains available only for historical order replay. New order snapshots preserve registry version and ERP identity, while pre-registry snapshots stay compatible only when part and multiplier match. The catalog observer reads the dedicated sandbox credential file, is hard-pinned to 127.0.0.1:8300, and records total, unallocated, salable and derived available quantities in private Woo metadata without changing fictional sellable stock, price or release. Both ERP parts currently report 85 total/unallocated and non-salable, so observed available is deliberately zero. Reservation remains disabled pending Phase 2.

2026-09-24 — assistant sandbox branch reviewed for promotion

"28 commits ahead" was misleading: the net difference is 12 files, because main was rebuilt from sandbox's work by cherry-pick and 26 of the 28 commits have byte-identical twins on main under different hashes. Only two commits carry unique content.

  • dc48850 "Let the assistant approve as itself, not as Angel" — the only real promotion candidate. Adds service_accounts.py (machine-principal registry, deliberately outside people.yml), content.approve_as_service(), and extracts _approve_and_schedule() so the browser route and the service path share one copy of the guards. 14 tests, almost all negative cases, fails closed throughout. Applies cleanly to main — no conflict with the shipped pricing fix, which touched only actions.py.
  • e25c17f inventory page — the feature Angel cancelled. Self-contained: three new files plus one router line in app.py and one tile in home.py. Never ship.

Held, not shipped, for three reasons. approve_as_service has no caller anywhere — grepped the whole tree, only the definition and its tests reference it — so promoting it puts an unreachable path into production and means shipping it twice. SERVICE_ACCOUNTS_FILE is unset in both env and env.sandbox, so both fall back to the same hardcoded /home/angel/.config/assistant/service-accounts.yml and a principal granted for sandbox testing would be simultaneously valid in production — set it in env.sandbox first, noting the registry fails closed when missing, so a sandbox registry must exist before that change. And it touches the approval path that schedules public social posts, which deserves a human reading the diff rather than a hash.

Also noted: the credential is already provisioned in production (service-accounts.yml and content-review.token, both 0600, created 2026-09-24 08:36) while the code that reads it exists only on sandbox. Harmless, but the ordering is backwards.

Correction to the review: it claimed a git merge sandbox would revert 189 lines of tester work that main has and sandbox lacks. That is wrong — simulating the merge with git merge-tree gives 792 insertions and zero deletions, because git's three-way merge keeps main's later commits. Merging is still the wrong move, but for different reasons: it would drag in the cancelled inventory page and produce conflicts in content.py, tester_api.py and tests/test_tester_api.py. Cherry-pick remains the right mechanism.

Next: resync sandbox from main (git merge main in the worktree) so the branches stop diverging and the next review is not another archaeology dig.

2026-09-24 — Cow Finger BOM, and what the zero stock actually means

The BOM (EE-A-0005-A) is structurally nearly complete. Against the schematic at ~/engineering/cow-remote-unified/hardware/Cow_finger_003, the 13 placed components are SW1–SW6 (matching the 6 × EE-C-0193 on the BOM), J1, H1–H4 and FID1–FID2. Angel confirmed J1 is no component — its footprint is SolderWire-0.1sqmm_1x05_P3.5mm_D1mm_OD, bare wire soldered to pads. Fiducials are correctly excluded. Mounting hardware for H1–H4 is still an open convention question.

Fixed: EE-C-0193 (B3SL-1002P) was marked purchaseable = false — wrong for a bought part, and it meant the switch could never appear on a purchase order. Now purchaseable, with the Omron datasheet linked and a manufacturer part created (Omron Electronics / B3SL-1002P). Note for later: Digi-Key now lists this line as "Aratas (formerly Omron Components)" after the components business was renamed; Mouser still says Omron. Recorded under Omron with the rename in the company description.

Still open on the BOM: neither EE-C-0193 nor EE-P-0005-A has a supplier part, so no PO is possible yet. part-lookup returns MPN, price, stock and lifecycle but not the distributor SKU, and SOP-002 requires the SKU on a supplier part — so the SKUs have to come from the Digi-Key/Mouser pages by hand, or part-lookup needs extending. Part is Active at both: Digi-Key 36,769 in stock at $0.99/1 and $0.70/100, Mouser 290 at $0.93/1. The BOM is also unvalidated — deliberately left for Angel, because validating through the API would record him as the checker.

The bigger finding: the zero stock is a counting gap, not missing inventory. EE-C-0193 has no stock item at all — not a zero-quantity record, none has ever been created. Across the ERP, 142 of 191 active components (74%) show zero stock, and the pattern is diagnostic: in the Cow Remote BOM the passive reels hold thousands (2992, 9984, 8982) while every switch, LED, connector, regulator and the STM32 reads zero. The opening stock count (SOP-023) evidently ran through the passives and stopped; 137 stock items exist across 23 locations. Until it is finished, no BOM in the ERP can say whether a build is possible — the shortages it reports are mostly fictional. The intake tooling for this already exists (2026-09-14: barcode → OCR → Haiku with a /stock confirm queue).

Cow Finger PCB fully entered, 2026-09-24. JLCPCB supplier part Y12-7663019A (design Cow_Finger_003_Y12), with pack_quantity 32 — one ordered unit is a 4x8 panel yielding 32 boards, so per-board cost and reorder quantities come out right without anyone doing the sum. $47.49 per 10 panels = $4.749/panel = $0.1484/board. JLCPCB was marked is_manufacturer as well as supplier, because it does fabricate our boards. Stock booked: 5 panels = 160 boards at Shop/Shelf A, batch 003, following the precedent set by EE-P-0002-A (268 boards counted individually, batch 004B).

Note for future stock entries: InvenTree refuses a stock item against a supplier part with a pack size unless use_pack_size is set, and with it the quantity is then counted in packs — pass 5 panels, not 160 boards, or you book 32x too much.

Both BOM parts now have images, rendered from CAD with the new ee-api renderer rather than photographed.

The Cow Finger BOM is one item from done: the switch EE-C-0193 still needs a supplier part, and Angel orders it from LCSC. The LCSC part number is in the packing-list PDFs attached to the LCSC shipping emails, which cannot be read without an attachment-download tool. Also still open: BOM line validation, deliberately left to Angel because doing it through the API would record him as the checker.

Cow Finger BOM is data-complete, 2026-09-24. The switch EE-C-0193 now has its LCSC supplier part C306880 (Omron B3SL-1002P), alongside the manufacturer part and the rendered image added earlier. Both lines now carry a supplier part, a manufacturer part, an image and a purchaseable flag:

Line Qty Supplier SKU Stock
EE-C-0193 tactile switch 6 LCSC C306880 0 (never counted)
EE-P-0005-A bare PCB 1 JLCPCB Y12-7663019A 160 boards

Two things remain, both deliberate. The BOM lines are unvalidated — left for Angel, because validating through the API would record him as the checker when he has not looked. And the switch shows zero stock because it has no stock item at all, which is the opening-count gap (74% of active components), not a shortage: the physical switches are believed to be on the shelf.

  • 2026-10-06: Stripe LIVE account ready: charges and payouts enabled, nothing currently/past/eventually due, 1 bank account, statement descriptor EMBEDDEDERA, company EmbeddedEra LLC (NC, EIN on file), support address NC. Stripe Tax active (head office Clayton NC, default txcd_99999999 General - Tangible Goods, NC registration 601765155 active live, collecting immediately). Keys on the server (0600): ~/.config/stripe/live-store.env (restricted 'API key for WooCommerce store', Standard third-party perms; publishable key still to add) and ~/.config/stripe/live-readonly.env (Claude read-only). Follow-ups: account MCC is 7392 (consulting) -> change industry to retail electronics; only one person on file (owner, verified, % blank) -> add Yara as 50% owner and set both percentages.
  • 2026-10-06: Stripe follow-ups done: both owners listed and verified (owners_provided true), industry now MCC 5399 (general merchandise), nothing due. Store key rolled (new key in live-store.env with the publishable key); old key to be confirmed revoked in the dashboard.
  • 2026-10-07: Store production launch plan P1-P3 done (dark): branch production, stack embeddedera-store-prod on 127.0.0.1:8510 (fresh install, guards, Cricket draft only), store backup set + restore drill PASS, 5-min health timer. Branch has no remote to push to. See store-production-launch-plan Phase status and map card embeddedera-store-prod.
  • 2026-10-07: Prod store admin adiaz: TOTP 2FA enrolled, owner changed the password (temporary one verified invalid), temporary password removed from ~/.config/embeddedera-store-prod/env. Setup access is the ssh tunnel to http://127.0.0.1:8510 (ingress now marks https only for store.embeddedera.com, commit 21b500c). Backup codes provider not enabled yet.
  • 2026-10-13: Lithium (Cricket, 1 installed Li-ion cell, 0.37 Wh) — USPS Pub 52 349.24x/349.221 (checked on pe.usps.com): mailable by Ground Advantage; ≤2.7 Wh tier; no lithium mark for ≤4 cells installed in the equipment they operate; rigid outer box, device individually cushioned; cell must have passed UN 38.3 → get the supplier's UN 38.3 test summary on file. Pirate Ship: new electronics with installed Li-ion → no hazmat checkbox. Notes say 3.7 V cell, owner said 3.3 V: reconcile the shipping-policy wording with the fitted cell. SOP-044 step 4 to be updated with this.
  • 2026-10-13: Owner wants fully automatic shipping (order → label bought → printed on the QL-1110NWB → tracking to customer, no clicks). Research: Pirate Ship has no API (support article 2309246) and its Woo integration is click-to-buy, so it cannot be the automatic path. Candidates driven by our own small server script (Woo webhook on Processing → label API → PDF → print over TCP 9100 → Woo order note/tracking): ShipEngine/ShipStation API Free plan ($0/mo, API key; USPS Ground Advantage availability + funding unconfirmed), EasyPost (free ≤3,000 labels, funded wallet), USPS Labels API v3 (free, needs CRID/MID/EPS — heaviest setup). Printing: brother_ql-inventree / brother_ql_next list QL-1110NWB + 103×164 over TCP (unproven on hardware); printer also speaks IPP/AirPrint (URF only). No free Woo plugin auto-buys + auto-prints. Decision pending with owner.
  • 2026-10-31: Product docs options presented (guide pages on site / separate docs site / wiki software / WooCommerce tabs / GitHub). Owner asked to see option 2: built a local Starlight sample (scratchpad docs-demo, not published) with real Cricket copy (from site i18n) and EE-C-0184/0197 facts from store notes; unverified specs shown as "Not published yet". Dark-only brand palette, copper tips, Pagefind search works, phone OK. No decision yet (Kanboard #280).