Certificate of Analysis (COA) management for WooCommerce — by Beenacle.
Built for WooCommerce shops that publish third‑party lab results — peptide, supplement, and research‑chemical stores — to store, display, and scan‑import Certificates of Analysis per product, size, and variation.
Plugin-owned custom tables, simple and variable product support, multi-COA per size/variation, a frontend display (block / shortcode / auto-inject with per-variation swap), an admin editor, and a REST API.
Status: v0.2.3. Core is self-contained and distribution-oriented. Adds a sortable catalog list, a smarter scan/import (prefers the real lab link, warns on multi-cert files), and a Re-read data action that re-runs the AI reader on an already-attached certificate and shows a review/diff to backfill or correct figures. Legacy data import lives in a separate, optional COA Vault — Migration companion plugin, so the shippable core carries no site-specific code. See CHANGELOG.md.
- PHP 8.1+
- WordPress 6.4+ / WooCommerce 8.0+ (HPOS-compatible; declares compatibility)
coa-vault.php Bootstrap: header, HPOS declare, autoloader, activation
src/Plugin.php Container; wires data + REST + frontend + admin (exposes records()/aliases())
src/Support/ Normalize · Vocab · Report · Hash (one source of truth)
src/Data/ Schema (3 tables) · Installer · DTOs · Repositories · SizeAliasBuilder · RecordInput
src/Rest/ RestServiceProvider · ProductCoaController · CoaController · RecordSchema
src/Frontend/ RenderService · VariationInjector · Shortcode · Block · AutoInject · Assets
src/Admin/ ProductPanel · BatchController · ScanController · AdminRenderer · ListTable · AdminMenu · ProductCoverage · Settings · Assets
src/Ingest/ ClaudeClient (optional AI certificate reading)
assets/ · blocks/ frontend + admin JS/CSS (incl. bundled jsQR), coa-vault/panel block
GET /products/{id}/coas[?size=&latest=]— all COAs for a productGET /products/{id}/resolve?variation_id=&size=— the variation→COA matching contract (returns records + rendered html)GET /coas?lab=&purity_max=&site=&page=— catalog-wide reportingGET /coas/{id}·POST/PUT/DELETE /coas(writes requireedit_products)
- Block:
coa-vault/panel - Shortcode:
[coa_vault](current product),[coa_vault product_id="N"], or[coa_vault all="true"]for a catalog archive of every published product's COAs. - Auto-inject: opt-out via
coa_vault_autoinjectoption/filter; placement onwoocommerce_single_product_summary. - Selecting a variation lazy-fetches that size's COAs via REST (no page bloat).
wp_coa_vault_records— one row per COA batch (product_id+ nullablevariation_id+ normalizedsize_token).wp_coa_vault_characteristics— normalized purity/mass/custom measurements.wp_coa_vault_size_aliases— per-product size_token → real variation/term map (exact hybrid matching).
Activate the plugin — tables are created on activation and kept current on update
(dbDelta, versioned via coa_vault_db_version). Add COAs from the product editor's
Certificates of Analysis box, or place [coa_vault] / the coa-vault/panel block.
- Settings: COA → Settings (storefront display, auto-placement, delete-data-on-uninstall).
- Scan / import: on the product COA box, drop a certificate image/PDF — the QR verify link
is read in-browser, the file is sideloaded, and a new COA is pre-filled for review (never
auto-saved). Set an Anthropic key (COA → Settings, or the
COA_VAULT_ANTHROPIC_KEYconstant) to also read batch/purity/mass/date off the document viaclaude-haiku-4-5(override with thecoa_vault_claude_modelfilter); without a key, QR + file only.
- Storage: custom tables (not ACF/CPT/meta).
- Binding: hybrid
product_id+ nullablevariation_id+size_token; resolution ladder variation → size → product-level. - Scope: core COA/lab data; certificate file and lab verify link kept as distinct fields.
- Labs: free-text with autocomplete; custom labs get a slug so they're filterable; the curated set (labels + aliases + verify/report hosts, for autocomplete and host inference) lives in
Support/Vocab.php. - Media: local uploads.
COA Vault updates itself from this repo's GitHub Releases — it is not on the
WordPress.org directory. Each site checks releases/latest (cached 6h to respect
GitHub's rate limit) and offers new versions from Dashboard → Updates, just like
any other plugin. No tokens or secrets are needed for a public repo. The logic lives
in src/Update/GitHubUpdater.php; the Update URI header keeps any same-slug
WordPress.org plugin from hijacking the update.
Cutting a release is one tagged commit — CI does the rest:
- Bump the version in two places: the
Version:header andCOA_VAULT_VERSIONincoa-vault.php(keep them equal; the constant is the runtime source of truth). - Commit, then tag and push:
git commit -am "Release vX.Y.Z" git tag vX.Y.Z && git push origin main --tags
.github/workflows/release.ymlbuildscoa-vault.zip(dev files stripped via.distignore, the tag version stamped into the header as a safety net) and publishes it as the release asset.
Within ~6h (or immediately via Dashboard → Updates → Check again) every site sees
the new version and can update in one click. Use vX.Y.Z-rc1-style pre-release tags
for testing — the updater only follows the latest full (non-prerelease) release.
Importing legacy ACF / lab-result CPT / native coa data is handled by a separate
COA Vault — Migration companion plugin. Install it only where a one-time import is
needed; the core plugin has no dependency on it.
Files are namespaced; unqualified calls to WordPress/WooCommerce functions resolve
to the global namespace at runtime (standard for WP plugins). Load WP/WooCommerce
stubs (e.g. php-stubs/wordpress-stubs, php-stubs/woocommerce-stubs) in your IDE
to silence "undefined function" notices.