*/ public static function all(): array { return array_merge( self::shortcodeContracts(), self::functionContracts(), self::classAndMethodContracts(), self::modelFieldContracts(), self::assetLoaderContracts(), self::jsAndDomContracts(), self::g1kThemelessAndLoopContracts(), self::g1kFluentCartTemplatingContracts(), self::g2kProductRendererContracts(), self::g2kConsumerRendererClassContracts(), self::g2kConsumerShortcodeContracts(), self::g2kOrderModelContracts(), self::wp9CheckoutRendererContracts(), self::wp9CheckoutSchemaAndFormContracts(), self::wp9CheckoutFilterAndHookContracts(), self::wp9CheckoutDomAttributeContracts(), self::ssrDropdownAndPriceContracts() ); } /** * Run every contract's check (where possible) and report a status per entry. * * Statuses: 'ok' (check passed on a known-good/undetected version), * 'missing' (check failed on a known-good/undetected version: a real * break), 'uncheckable' (JS/DOM contract, see the E2E layer), or 'warn' * (SENTINEL GEEL, see VersionProfiles::evaluate() and ELITE-PLAN.md * V1.1-audit punt 2 par. 106): a check that passed on an unaudited * NEWER FluentCart version is downgraded from 'ok' to 'warn' rather than * reported as a fully audited pass, and a check that failed on an * audited OLDER version is downgraded from 'missing' to 'warn' rather * than reported as an unexpected break, since that is an expected * limitation of the older release. Neither downgrade ever turns a real * failure on a known-good/undetected version into anything softer than * 'missing', and neither ever blocks a feature by itself: 'warn' is * informational only, exactly like the rest of this capability-first * registry. * * @param string|null $activeVersion Forwarded to VersionProfiles::evaluate(); * defaults to the FLUENTCART_VERSION constant. * @return array */ public static function verify(?string $activeVersion = null): array { require_once __DIR__ . '/VersionProfiles.php'; $profile = VersionProfiles::evaluate($activeVersion); $results = []; foreach (self::all() as $contract) { if ($contract['check'] instanceof \Closure) { $ok = (bool) ($contract['check'])(); if ($ok && $profile['status'] === VersionProfiles::STATUS_UNKNOWN_NEWER) { $status = 'warn'; $detail = sprintf( '%s (ok on FluentCart %s, newer than the verified baseline %s; capability works, sentinel yellow: %s)', $contract['purpose'], (string) $profile['version'], $contract['verified_version'], $profile['message'] ); } elseif (!$ok && $profile['status'] === VersionProfiles::STATUS_OLDER_UNSUPPORTED) { $status = 'warn'; $detail = sprintf( '%s (missing on FluentCart %s, older than the verified baseline %s, expected limitation of an older install: %s)', $contract['purpose'], (string) $profile['version'], $contract['verified_version'], $profile['message'] ); } else { $status = $ok ? 'ok' : 'missing'; $detail = $ok ? $contract['purpose'] : sprintf( '%s (expected in %s, evidence: %s)', $contract['purpose'], $contract['verified_version'], $contract['evidence'] ); } } else { $status = 'uncheckable'; $detail = sprintf('%s (%s)', $contract['purpose'], $contract['note']); } $results[] = [ 'id' => $contract['id'], 'type' => $contract['type'], 'status' => $status, 'detail' => $detail, ]; } return $results; } /** * Whether the active FluentCart install satisfies a minimum verified version. */ public static function minVersionOk(string $minVersion = self::VERIFIED_VERSION): bool { if (!defined('FLUENTCART_VERSION')) { return false; } return version_compare((string) FLUENTCART_VERSION, $minVersion, '>='); } /** * @return array */ private static function shortcodeContracts(): array { $shortcodes = [ 'fluent_cart_add_to_cart_button' => [ 'Add-to-cart shortcode, used by Product Action.', 'app/Hooks/Handlers/ShortCodes/Buttons/AddToCartShortcode.php:15', ], 'fluent_cart_checkout_button' => [ 'Direct-checkout (buy now) shortcode, used by Product Action.', 'app/Hooks/Handlers/ShortCodes/Buttons/DirectCheckoutShortcode.php:16', ], 'fluent_cart_product_card' => [ 'Product card shortcode, used by Product Card and Single Product (advanced variation preview).', 'app/Hooks/Handlers/ShortCodes/ProductCardShortCode.php:14', ], 'fluent_cart_mini_cart' => [ 'Mini cart shortcode, used by Mini Cart.', 'app/Hooks/Handlers/ShortCodes/MiniCartShortcode.php:20', ], 'fluent_cart_products' => [ 'Product grid/list shortcode, used by Product Collection.', 'app/Hooks/Handlers/ShortCodes/ShopAppHandler.php:32', ], 'fluent_cart_cart' => [ 'Cart page shortcode, used by Commerce View (cart).', 'app/Hooks/Handlers/ShortCodes/CartShortcode.php:19', ], 'fluent_cart_checkout' => [ 'Checkout page shortcode, used by Commerce View (checkout).', 'app/Hooks/Handlers/Checkout/CheckoutPageHandler.php:17', ], 'fluent_cart_customer_profile' => [ 'Customer account/profile shortcode, used by Commerce View (account).', 'app/Hooks/Handlers/ShortCodes/CustomerProfileHandler.php:22', ], 'fluent_cart_receipt' => [ 'Order receipt shortcode, used by Commerce View (receipt).', 'app/Hooks/Handlers/ShortCodes/ReceiptHandler.php:29', ], ]; $contracts = []; foreach ($shortcodes as $tag => [$purpose, $evidence]) { $contracts[] = [ 'id' => $tag, 'type' => 'shortcode', 'purpose' => $purpose, 'verified_version' => self::VERIFIED_VERSION, 'evidence' => $evidence, 'check' => static function () use ($tag): bool { return function_exists('shortcode_exists') && shortcode_exists($tag); }, 'note' => '', ]; } // Uitzondering: de klasse SingleProductShortCode bestaat in 1.3.27, maar // FluentCart roept haar ::register() nergens aan, dus de shortcode is op // een echte install NIET actief. De adapter rendert daarom via // ProductRenderer::render() zodra de shortcode ontbreekt; het contract is // groen zolang de shortcode OF de renderklasse beschikbaar is. // // Bronvergelijking 1.5.3 (docs/audit/fc-1.5.3-source-comparison.md par. 1): // FluentCart 1.5.3's app/Hooks/actions.php:87 roept // SingleProductShortCode::register() nu WEL aan, dus de shortcode draait // sindsdien ook echt op een live install. De OR-fallback op de renderklasse // blijft bestaan (id/check ongewijzigd) omdat dit fork ook de oudere, // geverifieerde 1.3.27-baseline moet blijven ondersteunen; op 1.5.3 kiest de // check nu gewoon de eerste tak (shortcode_exists), niet de fallback. $contracts[] = [ 'id' => 'fluent_cart_single_product', 'type' => 'shortcode', 'purpose' => 'Full single-product layout, used by Single Product. Inactive in 1.3.27 (adapter falls back to ProductRenderer::render()); booted since 1.5.3 (app/Hooks/actions.php:87 now calls ::register()), the OR-check/fallback stays in place for 1.3.27 compat.', 'verified_version' => self::VERIFIED_VERSION, 'evidence' => 'app/Hooks/Handlers/ShortCodes/SingleProductShortCode.php:20 (class), app/Hooks/actions.php:72 (1.3.27, only ProductCardShortCode is registered) / app/Hooks/actions.php:87 (1.5.3, SingleProductShortCode::register() is now called, docs/audit/fc-1.5.3-source-comparison.md par. 1)', 'check' => static function (): bool { if (function_exists('shortcode_exists') && shortcode_exists('fluent_cart_single_product')) { return true; } return class_exists('FluentCart\\App\\Services\\Renderer\\ProductRenderer'); }, 'note' => 'Fallback path: FluentCartAdapter::renderSingleProduct(). On 1.3.27 this is the active path; on 1.5.3+ (shortcode now booted) this path is no longer needed but is safe to keep.', ]; return $contracts; } /** * @return array */ private static function functionContracts(): array { return [ [ 'id' => 'fluent_cart_get_current_product', 'type' => 'function', 'purpose' => 'Resolves the current FluentCart product from $GLOBALS[\'fct_product\'] or the queried post.', 'verified_version' => self::VERIFIED_VERSION, 'evidence' => 'boot/globals.php:199-210', 'check' => static function (): bool { return function_exists('fluent_cart_get_current_product'); }, 'note' => '', ], ]; } /** * @return array */ private static function classAndMethodContracts(): array { $productDataSetupClass = 'FluentCart\\App\\Modules\\Data\\ProductDataSetup'; $productClass = 'FluentCart\\App\\Models\\Product'; $productDetailClass = 'FluentCart\\App\\Models\\ProductDetail'; $productVariationClass = 'FluentCart\\App\\Models\\ProductVariation'; $helperClass = 'FluentCart\\App\\Helpers\\Helper'; $productRendererClass = 'FluentCart\\App\\Services\\Renderer\\ProductRenderer'; $contracts = [ [ 'id' => $productDataSetupClass . '::getProductModel', 'type' => 'method', 'purpose' => 'Cached product-model loader consumed by ProductContext::find().', 'verified_version' => self::VERIFIED_VERSION, 'evidence' => 'app/Modules/Data/ProductDataSetup.php:42-53', 'check' => static function () use ($productDataSetupClass): bool { return method_exists($productDataSetupClass, 'getProductModel'); }, 'note' => '', ], [ 'id' => $productClass, 'type' => 'class', 'purpose' => 'FluentCart product Eloquent-style model.', 'verified_version' => self::VERIFIED_VERSION, 'evidence' => 'app/Models/Product.php', 'check' => static function () use ($productClass): bool { return class_exists($productClass); }, 'note' => '', ], [ 'id' => $productClass . '::detail', 'type' => 'method', 'purpose' => 'Relation to ProductDetail (min/max price, stock availability, variation type).', 'verified_version' => self::VERIFIED_VERSION, 'evidence' => 'app/Models/Product.php:120-123', 'check' => static function () use ($productClass): bool { return method_exists($productClass, 'detail'); }, 'note' => '', ], [ 'id' => $productClass . '::variants', 'type' => 'method', 'purpose' => 'Relation to the product\'s ProductVariation collection.', 'verified_version' => self::VERIFIED_VERSION, 'evidence' => 'app/Models/Product.php:125-127', 'check' => static function () use ($productClass): bool { return method_exists($productClass, 'variants'); }, 'note' => '', ], [ 'id' => $productVariationClass, 'type' => 'class', 'purpose' => 'FluentCart product-variation model.', 'verified_version' => self::VERIFIED_VERSION, 'evidence' => 'app/Models/ProductVariation.php:27', 'check' => static function () use ($productVariationClass): bool { return class_exists($productVariationClass); }, 'note' => '', ], [ 'id' => $helperClass . '::toDecimal', 'type' => 'method', 'purpose' => 'Formats a minor-unit integer amount into a displayable, currency-aware price string.', 'verified_version' => self::VERIFIED_VERSION, 'evidence' => 'app/Helpers/Helper.php:414', 'check' => static function () use ($helperClass): bool { return method_exists($helperClass, 'toDecimal'); }, 'note' => '', ], [ 'id' => $helperClass . '::toCent', 'type' => 'method', 'purpose' => 'Converts a decimal amount into the minor-unit integer; used by the WP5 raw-price Dynamic Data field.', 'verified_version' => self::VERIFIED_VERSION, 'evidence' => 'app/Helpers/Helper.php:508', 'check' => static function () use ($helperClass): bool { return method_exists($helperClass, 'toCent'); }, 'note' => 'Guarded use in DynamicData; if missing, the field degrades to empty, no fatal error.', ], [ 'id' => $productVariationClass . '::getPurchaseUrl', 'type' => 'method', 'purpose' => 'Instance purchase URL for a variation; used by the WP5 buy-now-URL Dynamic Data field.', 'verified_version' => self::VERIFIED_VERSION, 'evidence' => 'app/Models/ProductVariation.php:299', 'check' => static function () use ($productVariationClass): bool { return method_exists($productVariationClass, 'getPurchaseUrl'); }, 'note' => 'Guarded use; if missing, the field degrades to empty, no fatal error.', ], [ 'id' => 'FluentCart\\App\\Services\\Renderer\\ProductCardRender::buildPackageInfoFromOtherInfo', 'type' => 'method', 'purpose' => 'Static package-info builder from a variation other_info payload; used by the WP5 package-info Dynamic Data field.', 'verified_version' => self::VERIFIED_VERSION, 'evidence' => 'app/Services/Renderer/ProductCardRender.php:78', 'check' => static function (): bool { return method_exists('FluentCart\\App\\Services\\Renderer\\ProductCardRender', 'buildPackageInfoFromOtherInfo'); }, 'note' => 'Guarded use; if missing, the field degrades to empty, no fatal error.', ], [ 'id' => $productRendererClass, 'type' => 'class', 'purpose' => 'Constructor Product $product, array $config = [] (config key default_variation_id only).', 'verified_version' => self::VERIFIED_VERSION, 'evidence' => 'app/Services/Renderer/ProductRenderer.php:54', 'check' => static function () use ($productRendererClass): bool { return class_exists($productRendererClass); }, 'note' => '', ], ]; $rendererMethods = [ 'renderTitle' => ['No-arg title renderer.', 'app/Services/Renderer/ProductRenderer.php:542'], 'renderPrices' => ['No-arg price-range renderer (default Product Part price mode).', 'app/Services/Renderer/ProductRenderer.php:713'], 'renderItemPrice' => ['No-arg selected-variation price renderer.', 'app/Services/Renderer/ProductRenderer.php:861'], 'renderStockAvailability' => ['Stock renderer, signature renderStockAvailability($wrapper_attributes = \'\').', 'app/Services/Renderer/ProductRenderer.php:551'], 'renderSku' => ['SKU renderer, real signature renderSku($wrapper_attributes = \'\', $showLabel = true, $label = \'\', $variant = null) (4 params, not 2).', 'docs/audit/fluentcart-contracts.md par. 6'], 'renderExcerpt' => ['No-arg excerpt renderer.', 'app/Services/Renderer/ProductRenderer.php:686'], 'renderGallery' => ['Gallery renderer, signature renderGallery($args = []).', 'app/Services/Renderer/ProductRenderer.php:506'], 'renderBuySection' => ['Buy-section renderer, signature renderBuySection($atts = []); only variation_atts and button_atts keys are read, selector_style is a no-op.', 'app/Services/Renderer/ProductRenderer.php:259-277'], ]; foreach ($rendererMethods as $method => [$purpose, $evidence]) { $contracts[] = [ 'id' => $productRendererClass . '::' . $method, 'type' => 'method', 'purpose' => $purpose, 'verified_version' => self::VERIFIED_VERSION, 'evidence' => $evidence, 'check' => static function () use ($productRendererClass, $method): bool { return method_exists($productRendererClass, $method); }, 'note' => '', ]; } return $contracts; } /** * Model fields are checked via each model's $fillable default property (reflection), * since these are dynamic/magic properties, not declared typed properties. * * @return array */ private static function modelFieldContracts(): array { $productDetailClass = 'FluentCart\\App\\Models\\ProductDetail'; $productVariationClass = 'FluentCart\\App\\Models\\ProductVariation'; $detailFields = [ 'min_price' => 'app/Models/ProductDetail.php:35-44', 'max_price' => 'app/Models/ProductDetail.php:35-44', 'stock_availability' => 'app/Models/ProductDetail.php:35-44', 'fulfillment_type' => 'app/Models/ProductDetail.php:35-44', 'default_variation_id' => 'app/Models/ProductDetail.php:35-44', 'variation_type' => 'app/Models/ProductDetail.php:35-44', ]; $variationFields = [ 'item_price' => 'app/Models/ProductVariation.php:34-51', 'compare_price' => 'app/Models/ProductVariation.php:34-51', 'payment_type' => 'app/Models/ProductVariation.php:34-51', 'other_info' => 'app/Models/ProductVariation.php:34-51', 'item_status' => 'app/Models/ProductVariation.php:34-51', 'serial_index' => 'app/Models/ProductVariation.php:34-51', 'sku' => 'app/Models/ProductVariation.php:34-51', 'post_id' => 'app/Models/ProductVariation.php:34-51', ]; $contracts = []; foreach ($detailFields as $field => $evidence) { $contracts[] = [ 'id' => 'ProductDetail::' . $field, 'type' => 'model-veld', 'purpose' => sprintf('ProductDetail fillable field "%s", consumed by FluentCartAdapter.', $field), 'verified_version' => self::VERIFIED_VERSION, 'evidence' => $evidence, 'check' => self::modelFieldCheck($productDetailClass, $field), 'note' => '', ]; } foreach ($variationFields as $field => $evidence) { $contracts[] = [ 'id' => 'ProductVariation::' . $field, 'type' => 'model-veld', 'purpose' => sprintf('ProductVariation fillable field "%s", consumed by FluentCartAdapter/ProductContext.', $field), 'verified_version' => self::VERIFIED_VERSION, 'evidence' => $evidence, 'check' => self::modelFieldCheck($productVariationClass, $field), 'note' => '', ]; } return $contracts; } private static function modelFieldCheck(string $class, string $field): \Closure { return static function () use ($class, $field): bool { if (!class_exists($class)) { return false; } try { $reflection = new \ReflectionClass($class); $defaults = $reflection->getDefaultProperties(); } catch (\ReflectionException $exception) { return false; } $fillable = $defaults['fillable'] ?? null; return is_array($fillable) && in_array($field, $fillable, true); }; } /** * @return array */ private static function assetLoaderContracts(): array { $loaderClass = 'FluentCart\\App\\Modules\\Templating\\AssetLoader'; $contracts = [ [ 'id' => $loaderClass, 'type' => 'class', 'purpose' => 'FluentCart\'s frontend asset-group loader, bridged from AssetManager.', 'verified_version' => self::VERIFIED_VERSION, 'evidence' => 'app/Modules/Templating/AssetLoader.php', 'check' => static function () use ($loaderClass): bool { return class_exists($loaderClass); }, 'note' => '', ], ]; $methods = [ 'loadSingleProductAssets' => 'app/Modules/Templating/AssetLoader.php:110', 'loadProductArchiveAssets' => 'app/Modules/Templating/AssetLoader.php:223', 'loadCartAssets' => 'app/Modules/Templating/AssetLoader.php:355', 'loadMiniCartAssets' => 'app/Modules/Templating/AssetLoader.php:602', 'loadCheckoutAssets' => 'app/Modules/Templating/AssetLoader.php:434', 'loadCustomerDashboardGlobalAssets' => 'app/Modules/Templating/AssetLoader.php:323', 'loadCustomerDashboardAssets' => 'app/Modules/Templating/AssetLoader.php:335', 'enqueueThankYouPageAssets' => 'app/Modules/Templating/AssetLoader.php:594', ]; foreach ($methods as $method => $evidence) { $contracts[] = [ 'id' => $loaderClass . '::' . $method, 'type' => 'asset-loader-method', 'purpose' => sprintf('Loads the FluentCart "%s" frontend asset group.', $method), 'verified_version' => self::VERIFIED_VERSION, 'evidence' => $evidence, 'check' => static function () use ($loaderClass, $method): bool { return method_exists($loaderClass, $method); }, 'note' => '', ]; } return $contracts; } /** * JS events, DOM selectors and dataset keys cannot be verified from a PHP request; they * are exercised by the jsdom/Playwright layer instead (docs/audit/fluentcart-contracts.md * par. 9, ELITE-PLAN.md WP13). * * @return array */ private static function jsAndDomContracts(): array { $e2eNote = 'uncheckable from PHP, verify in the E2E/jsdom layer (WP13)'; $entries = [ [ 'fluentCartSingleProductVariationChanged', 'js-event', 'Dispatched by FluentCart\'s SingleProduct.js on variation change, detail: {productId, variationId}.', 'fluent-cart/assets/SingleProduct.js', ], [ 'fluentCartNotifyCartDrawerItemChanged', 'js-event', 'Dispatched by FluentCartApp.js after cart mutations, detail: {response}.', 'fluent-cart/assets/FluentCartApp.js', ], [ '[data-fluent-cart-product-variant]', 'dom-attribute', 'Selector for a variant row carrying the dataset contract (sku, itemStock, paymentType, ...).', 'app/Services/Renderer/ProductRenderer.php:1514', ], [ '[data-cart-id]', 'dom-attribute', 'Carries the active variation id on buttons/variant rows.', 'app/Services/Renderer/ProductRenderer.php:1054,1167,1243,1301,1382,1515', ], [ '[data-fluent-cart-product-stock]', 'dom-attribute', 'Stock badge node updated by variation-sync.js.', 'app/Services/Renderer/ProductRenderer.php:575', ], [ '[data-fluent-cart-add-to-cart-button]', 'dom-attribute', 'Add-to-cart button node updated by variation-sync.js.', 'app/Services/Renderer/ProductRenderer.php:1300,1380', ], [ '[data-fluent-cart-direct-checkout-button]', 'dom-attribute', 'Buy-now/direct-checkout button node updated by variation-sync.js.', 'app/Services/Renderer/ProductRenderer.php:1162,1238', ], [ '.fluent-cart-product-variation-content[data-variation-id]', 'dom-attribute', 'Per-variation content block toggled by variation-sync.js.', 'app/Services/Renderer/ProductRenderer.php:883,885,967,969,995,996', ], [ 'dataset.sku', 'dataset-key', 'SKU value read off [data-fluent-cart-product-variant].', 'fluent-cart/assets/SingleProduct.js (dataset scan)', ], [ 'dataset.itemStock', 'dataset-key', 'Stock-status slug read off [data-fluent-cart-product-variant].', 'fluent-cart/assets/SingleProduct.js (dataset scan)', ], [ 'dataset.paymentType', 'dataset-key', 'Payment type (e.g. subscription) read off [data-fluent-cart-product-variant].', 'fluent-cart/assets/SingleProduct.js (dataset scan)', ], [ '[data-fluent-cart-product-pricing-section]', 'dom-attribute', 'Hotfix atomen-interactiviteit (2026-07-15). SingleProduct.js\'s single mount ' . 'point: document.querySelectorAll("[data-fluent-cart-product-pricing-section]") ' . '.forEach((el, i) => new PricingSectionClass().init(el, i)); every variant/tab ' . 'click, quantity button and add-to-cart/buy-now click handler is bound scoped to ' . 'that element (container.querySelectorAll()). Without this wrapper present ' . 'anywhere in the page, no click handler is ever bound (the live ' . 'sparkcore-shop.local/item/spark-media-optimizer/ repro: FC_Product_Variations/ ' . 'FC_Product_Quantity/FC_Add_To_Cart/FC_Buy_Now rendered standalone with no ' . 'ancestor emitting it). ProductRenderer::renderBuySectionWrapperStart() emits it ' . 'natively; this fork\'s AtomRenderer::render() $pricingSectionScoped parameter ' . 'and FC_Product_Builder/ssr.php both emit the same attribute set as a fallback ' . 'for atomized composition.', 'fluent-cart/assets/SingleProduct.js (minified, container init loop); native emitter ' . 'app/Services/Renderer/ProductRenderer.php:245-257', ], [ 'data-product-id (on [data-fluent-cart-product-pricing-section])', 'dom-attribute', 'Hotfix atomen-interactiviteit (2026-07-15). Read via ' . 'container.getAttribute("data-product-id") by SingleProduct.js\'s pricing-section ' . 'class; used as the productId in the fluentCartSingleProductVariationChanged ' . 'CustomEvent it dispatches and to gate variation-changed listeners elsewhere on ' . 'the page (the gallery class). Distinct from this fork\'s own ' . 'data-fcbd-product-context attribute (never read by FluentCart\'s own JS).', 'fluent-cart/assets/SingleProduct.js (minified)', ], [ '[data-subscription-terms]', 'dom-attribute', 'Bugfix reactieve subscription terms (2026-07-16). Carried on every SUBSCRIPTION ' . 'variant\'s own [data-fluent-cart-product-variant] node: the complete, translated, ' . 'already-formatted per-variation display string ($variant->getSubscriptionTermsText(true), ' . 'ProductVariation.php:278-297), e.g. ' . '"3 dagen gratis dan €69.99 per jaar" (live-verified, ' . 'sparkcore-shop.local/item/spark-speed/, 10 matches). Absent (no attribute at all) on a ' . 'one-time/non-subscription variant: ProductRenderer only sets it inside ' . '`if ($paymentType === \'subscription\')`, a normal/expected state, not a bug. Contains a ' . 'literal named HTML entity (e.g. "€") baked into the PHP string by ' . 'Helper::toDecimal()/CurrenciesHelper::getCurrencySign(); ProductRenderer::renderAttributes() ' . 'writes it through esc_attr() unchanged (esc_attr() does not double-encode an already-valid ' . 'named entity). Live-verified in a real browser that this is still safe to read via JS: the ' . 'HTML5 parser decodes character references inside attribute values during parsing (not only ' . 'in text content), so `.dataset.subscriptionTerms`/`.getAttribute()` already return the ' . 'plain decoded Unicode character (charCodeAt === 8364 for the euro sign), never the literal ' . '"€" text. assets/atoms-enhance.js\'s enhanceSubscriptionTerms() reads this attribute ' . 'and assigns it via .textContent only, never .innerHTML.', 'app/Services/Renderer/ProductRenderer.php:1528-1529 (emitter); ' . 'app/Models/ProductVariation.php:278-297 (text builder); ' . 'app/Services/Renderer/ProductRenderer.php:1644-1653 (renderAttributes()/esc_attr() escaping)', ], [ 'dataset.subscriptionTerms', 'dataset-key', 'Bugfix reactieve subscription terms (2026-07-16). Read off ' . '[data-fluent-cart-product-variant] by FluentCart\'s OWN SingleProduct.js, but the read is ' . 'DEAD CODE there: a discarded comma-operator expression ' . '(`s==null||s.dataset.subscriptionTerms;`, no assignment, value never used anywhere else in ' . 'that file, grep-verified) -- this is the root-cause confirmation that FluentCart\'s own JS ' . 'never makes FC_Subscription_Terms reactive, and this fork\'s own variation-sync.js does not ' . 'touch it either (it only updates sku/stock/action attributes). This fork\'s own ' . 'assets/atoms-enhance.js enhanceSubscriptionTerms() is therefore the only place this value ' . 'is actually consumed, reading it from the specific matched variant node (never a global ' . 'dataset scan) and writing it verbatim into FC_Subscription_Terms\' own SSR text node.', 'fluent-cart/assets/SingleProduct.js (minified; search marker "dataset.subscriptionTerms")', ], [ '[data-package-info]', 'dom-attribute', 'Bugfix reactieve subscription terms (2026-07-16), FC_Package_Info investigation (task brief ' . 'point 3). Carried on EVERY variant\'s own [data-fluent-cart-product-variant] node ' . '(not gated on payment type), but its VALUE is a JSON string (wp_json_encode of ' . 'name/dimensions/product_weight/shipping_weight), never a ready-to-display string, and it ' . 'is only non-empty when that variant\'s fulfillment_type is "physical" AND a package ' . 'resolves (getVariantPackageInfoJson()); otherwise ProductRenderer::renderAttributes()\'s ' . 'empty-value branch still emits the bare attribute name with no value at all (present, ' . 'empty). Live-verified on sparkcore-shop.local/item/spark-speed/ (a digital product): the ' . 'bare, empty attribute is present on all 10 variant nodes. This JSON shape is fundamentally ' . 'different from what this fork\'s FC_Package_Info atom renders ' . '(AtomRenderer::packageInfoText() -> ProductCardRender::buildPackageInfoFromOtherInfo(), a ' . 'single combined "Package: X (LxWxH cm, Wt: Y kg, Shipping wt: Z kg)" string built from ' . 'OTHER fields with OTHER labels/prefixes), so copying this attribute\'s value verbatim into ' . 'FC_Package_Info\'s text node would show raw JSON to visitors, and reformatting it ' . 'client-side would itself be the "compute/format ourselves" this fork\'s bridges must never ' . 'do. FC_Package_Info is therefore deliberately left static by this fix (reported, not ' . 'patched); see [data-fluent-cart-package-description] below for how FluentCart\'s own JS ' . 'actually consumes this JSON.', 'app/Services/Renderer/ProductRenderer.php:1525 (emitter), :627-684 (getVariantPackageInfoJson()), ' . ':1644-1653 (renderAttributes() bare-attribute-on-empty-value branch)', ], [ 'dataset.packageInfo', 'dataset-key', 'Bugfix reactieve subscription terms (2026-07-16), FC_Package_Info investigation. Read off the ' . 'selected [data-fluent-cart-product-variant] node by FluentCart\'s OWN SingleProduct.js, ' . 'JSON.parse()\'d, and (when non-empty) built into an HTML that REPLACES the ' . 'children of [data-fluent-cart-package-description] (see below); this IS live/reactive ' . 'already, entirely inside FluentCart\'s own code, but targets a different DOM contract than ' . 'this fork\'s own FC_Package_Info atom emits, so FC_Package_Info does not benefit from it ' . 'without adopting that contract (not done by this fix, see [data-package-info] above).', 'fluent-cart/assets/SingleProduct.js (minified; search marker "dataset.packageInfo")', ], [ '[data-fluent-cart-package-description]', 'dom-attribute', 'Bugfix reactieve subscription terms (2026-07-16), FC_Package_Info investigation. The node ' . 'FluentCart\'s own SingleProduct.js replaces the children of (and toggles display: none on ' . 'when empty) with a translated name/dimensions/weight table on variation change, scoped to ' . '`.closest(\'.fct-product-summary\') || .closest(\'.product-info-block-wrapper\') || ' . '.parentElement` of the pricing-section container. This fork\'s FC_Package_Info atom ' . '(bd-elements/FC_Package_Info/ssr.php) does not emit this attribute and is not nested in ' . 'either of those native ancestor wrappers when used standalone, so it is never targeted by ' . 'this native update path; not changed by this fix (task brief point 3: "if not, only ' . 'report").', 'fluent-cart/assets/SingleProduct.js (minified; search marker ' . '"data-fluent-cart-package-description")', ], ]; $contracts = []; foreach ($entries as [$id, $type, $purpose, $evidence]) { $contracts[] = [ 'id' => $id, 'type' => $type, 'purpose' => $purpose, 'verified_version' => self::VERIFIED_VERSION, 'evidence' => $evidence, 'check' => null, 'note' => $e2eNote, ]; } return $contracts; } /** * G1K kernel-commit (ELITE-PLAN.md par. 182-201, Golf 1): Breakdance-side * capabilities that WP3 (TemplateRegistry), WP4 (ProductContext v2 / FC * Product Builder) and WP5 (Query/ConditionRegistry/DynamicData) depend * on. Evidence and full analysis: docs/audit/g1k-probes.md. * * @return array */ private static function g1kThemelessAndLoopContracts(): array { $elementClass = 'Breakdance\\Elements\\Element'; $productBuilderClass = 'EssentialElements\\Productbuilder'; $loopControllerClass = 'Breakdance\\DynamicData\\LoopController'; return [ [ 'id' => 'cap.bd.template_include_takeover_marker', 'type' => 'capability', 'purpose' => 'Takeover detection: when Breakdance owns a template, maybe_override_the_theme_with_a_breakdance_template() returns the path themeless/theme-simulator/breakdance-template.php; TemplateRegistry::maybeSuppressAutoSingleProductPage() matches exactly on that file name to suppress FluentCart\'s auto-output in a scoped way.', 'verified_version' => self::VERIFIED_BREAKDANCE_VERSION, 'evidence' => 'plugin/themeless/override_theme.php:13-111 (return van breakdance-template.php); src/Breakdance/TemplateRegistry.php', 'check' => static function (): bool { $fn = '\\Breakdance\\Themeless\\maybe_override_the_theme_with_a_breakdance_template'; if (!function_exists($fn)) { return false; } try { $dir = dirname((new \ReflectionFunction($fn))->getFileName()); return file_exists($dir . '/theme-simulator/breakdance-template.php'); } catch (\ReflectionException $error) { return false; } }, 'note' => 'The file name marker is internal to Breakdance; if the path changes, the takeover suppression safely falls back to doing nothing (native FluentCart output remains).', ], [ 'id' => 'cap.bd.share_props_with_ssr_children', 'type' => 'capability', 'purpose' => 'SSR parent-property propagation: Element::ssr($props, $parentPropertiesData, ...) receives the nearest ancestor\'s properties whenever that ancestor\'s settings() sets sharePropsWithSSRChildren => true; this is the real (confirmed, not assumed) mechanism a Product Builder-style context element must use to hand its chosen product down to descendant atoms.', 'verified_version' => self::VERIFIED_BREAKDANCE_VERSION, 'evidence' => 'plugin/render/renderer.php:1277-1297,505; plugin/elements/Element.class.php:218; subplugins/breakdance-elements/elements/ProductBuilder/element.php:159; docs/audit/g1k-probes.md par. 1', 'check' => static function () use ($elementClass, $productBuilderClass): bool { if (!class_exists($elementClass) || !method_exists($elementClass, 'ssr')) { return false; } if (!class_exists($productBuilderClass) || !method_exists($productBuilderClass, 'settings')) { // The specific Woo Product Builder element is proOnly and may not be // loadable standalone; the base Element::ssr() signature is the load-bearing // part of this capability, so its presence alone is sufficient. return true; } $settings = $productBuilderClass::settings(); return is_array($settings) && array_key_exists('sharePropsWithSSRChildren', $settings); }, 'note' => '', ], [ 'id' => 'cap.bd.template_type_registration', 'type' => 'capability', 'purpose' => 'Breakdance\\Themeless\\registerTemplateType($category, $templateType) and registerCondition($condition) are the public APIs for adding new template types/rule conditions, both fired from the single breakdance_register_template_types_and_conditions action.', 'verified_version' => self::VERIFIED_BREAKDANCE_VERSION, 'evidence' => 'plugin/themeless/api.php:20-43; plugin/actions_filters/template_include.php:54; docs/audit/g1k-probes.md par. 2.1,2.3', 'check' => static function (): bool { return function_exists('Breakdance\\Themeless\\registerTemplateType') && function_exists('Breakdance\\Themeless\\registerCondition'); }, 'note' => '', ], [ 'id' => 'cap.bd.template_include_priority', 'type' => 'capability', 'purpose' => 'Breakdance\'s template_include takeover callback is registered at priority 1000000, the highest observed priority in the plugin, so it decides the final template after FluentCart\'s own (lower-priority) template_include filter has already run.', 'verified_version' => self::VERIFIED_BREAKDANCE_VERSION, 'evidence' => 'plugin/actions_filters/template_include.php:11; plugin/themeless/override_theme.php:13-111; docs/audit/g1k-probes.md par. 2.2', 'check' => static function (): bool { if (!function_exists('has_filter')) { return false; } $priority = has_filter('template_include', 'Breakdance\\ActionsFilters\\template_include'); return $priority === 1000000; }, 'note' => 'Uncheckable before WordPress has loaded template_include filters (e.g. in a bare CLI contract-check); returns false rather than fatal in that case.', ], [ 'id' => 'cap.bd.loop_controller', 'type' => 'capability', 'purpose' => 'Breakdance\\DynamicData\\LoopController::getInstance($id)->set()/get()/reset() is the per-id singleton loop-item registry a repeater-style dynamic-data field or Query Builder loop element must use so descendant fields can read the active loop item.', 'verified_version' => self::VERIFIED_BREAKDANCE_VERSION, 'evidence' => 'plugin/dynamic-data/loop-controller.php:1-59; plugin/dynamic-data/fields/metabox/metabox-group-field.php:27,78,82,94,96,100; docs/audit/g1k-probes.md par. 3', 'check' => static function () use ($loopControllerClass): bool { if (!class_exists($loopControllerClass)) { return false; } return method_exists($loopControllerClass, 'getInstance') && method_exists($loopControllerClass, 'set') && method_exists($loopControllerClass, 'get') && method_exists($loopControllerClass, 'reset'); }, 'note' => '', ], ]; } /** * G1K kernel-commit: FluentCart-side capabilities for the same three * Golf 1 forks. Evidence and full analysis: docs/audit/g1k-probes.md. * * @return array */ private static function g1kFluentCartTemplatingContracts(): array { $storeSettingsClass = 'FluentCart\\Api\\StoreSettings'; $templateActionsClass = 'FluentCart\\App\\Modules\\Templating\\TemplateActions'; return [ [ 'id' => 'cap.fc.shop_page_setting', 'type' => 'capability', 'purpose' => 'StoreSettings::getShopPageId()/getShopPage() resolve the designated virtual "Shop" page (a real wp page post whose id lives in the shop_page_id store setting); the fluent-products CPT archive is a redirect-only alias to this page once it is set.', 'verified_version' => self::VERIFIED_VERSION, 'evidence' => 'app/CPT/Pages.php:16-35,39-60,158-168; api/StoreSettings.php:1354-1368; app/Modules/Templating/TemplateLoader.php:37-49; docs/audit/g1k-probes.md par. 4', 'check' => static function () use ($storeSettingsClass): bool { return class_exists($storeSettingsClass) && method_exists($storeSettingsClass, 'getShopPageId') && method_exists($storeSettingsClass, 'getShopPage'); }, 'note' => '', ], [ 'id' => 'cap.fc.product_archive', 'type' => 'capability', 'purpose' => 'The fluent-products CPT is registered with a real WordPress archive (has_archive => true); only meaningful once WordPress\' init has fired and the CPT is actually registered.', 'verified_version' => self::VERIFIED_VERSION, 'evidence' => 'app/CPT/FluentProducts.php:193,226; docs/audit/g1k-probes.md par. 4', 'check' => static function (): bool { if (!function_exists('get_post_type_object')) { return false; } $postType = get_post_type_object('fluent-products'); if (!$postType) { // Not registered yet (e.g. checked before init, or FluentCart inactive). // Uncheckable-by-timing rather than a proven break; do not report 'missing'. return true; } return (bool) ($postType->has_archive ?? false); }, 'note' => 'Runtime-registration check: returns true (does not fail the contract) when the CPT is not yet registered, since that reflects request timing rather than a real capability break.', ], [ 'id' => 'cap.fc.json_ld_absent', 'type' => 'probe-negative-result', 'purpose' => 'FluentCart emits no product/offer JSON-LD or any schema.org markup anywhere in its own code (exhaustive grep for ld+json/schema.org/JsonLd/@type Product returned zero real hits). WP3 must emit its own Product/Offer JSON-LD; there is nothing of FluentCart\'s to preserve.', 'verified_version' => self::VERIFIED_VERSION, 'evidence' => 'docs/audit/g1k-probes.md par. 5 (grep evidence: zero matches across the fluent-cart plugin tree)', 'check' => null, 'note' => 'Documented negative-result probe, not a runtime-checkable capability: asserting "this pattern is absent from third-party source" is not something to re-derive at runtime. See docs/audit/g1k-probes.md par. 5.', ], [ 'id' => 'cap.fc.disable_auto_single_product_page', 'type' => 'capability', 'purpose' => 'The fluent_cart/disable_auto_single_product_page filter is checked at exactly 3 sites (the_title, get_the_excerpt, the_content), all inside TemplateActions::initSingleProductHooks()/filterSingleProductContent(); WP3\'s takeover suppression on the 1.3.27 profile hinges on both methods existing.', 'verified_version' => self::VERIFIED_VERSION, 'evidence' => 'app/Modules/Templating/TemplateActions.php:212-268; docs/audit/g1k-probes.md par. 7', 'check' => static function () use ($templateActionsClass): bool { return class_exists($templateActionsClass) && method_exists($templateActionsClass, 'initSingleProductHooks') && method_exists($templateActionsClass, 'filterSingleProductContent'); }, 'note' => '', ], [ 'id' => 'cap.fc.theme_support_gate', 'type' => 'runtime-theme-profile', 'purpose' => 'TemplateActions::initSingleProductHooks() (and therefore the disable_auto_single_product_page gate) only ever runs when TemplateLoader::hasThemeSupport() is FALSE for the active theme (i.e. not a block theme and not add_theme_support(\'fluent_cart\')); on a block theme or a theme declaring that support, the filter is never consulted at all. WP3 must re-verify this against the actual active theme per install, not assume the themeless-Breakdance profile universally.', 'verified_version' => self::VERIFIED_VERSION, 'evidence' => 'app/Modules/Templating/TemplateLoader.php:28-33,105-118,136-139; docs/audit/g1k-probes.md par. 7', 'check' => null, 'note' => 'Depends on live theme state (current_theme_supports()/wp_is_block_theme()), which is only meaningful mid-request against the real active theme; not asserted here to avoid a false pass/fail outside that context. See docs/audit/g1k-probes.md par. 7.', ], ]; } /** * G2K kernel-commit (ELITE-PLAN.md par. 235-239, Golf 2): additional * ProductRenderer methods the four Golf 2 forks (WP6 single-product * atoms, WP7 collections, WP8 cart, WP10 account/receipt) need beyond * the golf 0/1 subset already contracted in classAndMethodContracts(). * Each entry was verified directly against the live 1.3.27 source * (file:line below), not assumed from the task brief's naming. * * @return array */ private static function g2kProductRendererContracts(): array { $productRendererClass = 'FluentCart\\App\\Services\\Renderer\\ProductRenderer'; $methods = [ 'renderQuantity' => ['No-arg quantity-input renderer, used by WP6 FC Quantity.', 'app/Services/Renderer/ProductRenderer.php:1044'], 'renderVariationDisplay' => ['Variation-selector renderer, signature renderVariationDisplay($atts = []), used by WP6 FC Variations.', 'app/Services/Renderer/ProductRenderer.php:279'], 'renderPurchaseButtons' => ['Combined add-to-cart + buy-now button renderer, signature renderPurchaseButtons($atts = []), used by WP6 FC Buy Section.', 'app/Services/Renderer/ProductRenderer.php:1121'], 'renderBuyNowButton' => ['Buy-now/direct-checkout button renderer, signature renderBuyNowButton($atts = []), used by WP6 FC Buy Now.', 'app/Services/Renderer/ProductRenderer.php:1128'], 'renderAddToCartButton' => ['Add-to-cart button renderer, signature renderAddToCartButton($atts = []), used by WP6 FC Add to Cart.', 'app/Services/Renderer/ProductRenderer.php:1290'], 'renderDescription' => ['No-arg full product description renderer, used by WP6 FC Description.', 'app/Services/Renderer/ProductRenderer.php:700'], 'renderProductMeta' => ['No-arg product meta renderer. NOTE: in 1.3.27 this renders stock plus SKU, not category/brand (WP6 finding); FC_Product_Meta adds category/brand itself via get_the_term_list.', 'app/Services/Renderer/ProductRenderer.php:235-243'], ]; $contracts = []; foreach ($methods as $method => [$purpose, $evidence]) { $contracts[] = [ 'id' => $productRendererClass . '::' . $method, 'type' => 'method', 'purpose' => $purpose, 'verified_version' => self::VERIFIED_VERSION, 'evidence' => $evidence, 'check' => static function () use ($productRendererClass, $method): bool { return method_exists($productRendererClass, $method); }, 'note' => '', ]; } return $contracts; } /** * G2K kernel-commit: renderer classes for WP8 (Mini Cart/Cart drawer) and * WP7 (Product Categories, Search Bar) collections. Verified to exist as * of 1.3.27; none of these were previously contracted (golf 0/1 only used * MiniCartShortcode/ShopAppHandler at the shortcode level). * * @return array */ private static function g2kConsumerRendererClassContracts(): array { $classes = [ 'FluentCart\\App\\Services\\Renderer\\CartDrawerRenderer' => [ 'Offcanvas/drawer cart renderer, used by WP8 FC Mini Cart drawer mode.', 'app/Services/Renderer/CartDrawerRenderer.php:10', ], 'FluentCart\\App\\Services\\Renderer\\MiniCartRenderer' => [ 'Dropdown mini-cart renderer, used by WP8 FC Mini Cart dropdown mode.', 'app/Services/Renderer/MiniCartRenderer.php:9', ], 'FluentCart\\App\\Services\\Renderer\\ProductCategoriesListRenderer' => [ 'Product-categories list renderer, used by WP7 FC Product Categories.', 'app/Services/Renderer/ProductCategoriesListRenderer.php:7', ], 'FluentCart\\App\\Services\\Renderer\\SearchBarRenderer' => [ 'Product search bar renderer, used by WP7 FC Search Bar.', 'app/Services/Renderer/SearchBarRenderer.php:10', ], ]; $contracts = []; foreach ($classes as $class => [$purpose, $evidence]) { $contracts[] = [ 'id' => $class, 'type' => 'class', 'purpose' => $purpose, 'verified_version' => self::VERIFIED_VERSION, 'evidence' => $evidence, 'check' => static function () use ($class): bool { return class_exists($class); }, 'note' => '', ]; } return $contracts; } /** * G2K kernel-commit: additional shortcodes for WP10 (login/registration/ * dashboard button) and WP8/WP7 (coupon, product categories, search bar) * beyond the golf 0/1 subset already in shortcodeContracts(). Every tag * was verified directly against the live 1.3.27 source; none were * assumed from the task brief's naming alone. * * @return array */ private static function g2kConsumerShortcodeContracts(): array { $shortcodes = [ 'fluent_cart_related_products' => [ 'Related products shortcode (inline closure, att: id), used by WP7 FC Related Products mode A.', 'app/Modules/Templating/TemplateActions.php:78-107', ], 'fluent_cart_show_coupon' => [ 'Coupon form shortcode, registered directly via add_shortcode() (not a ShortCode subclass), used by WP8 FC Coupon.', 'app/Modules/Coupon/CouponHandler.php:13', ], 'fluent_cart_product_categories' => [ 'Product categories list shortcode, used by WP7 FC Product Categories.', 'app/Hooks/Handlers/ShortCodes/ProductCategoriesListShortcode.php:10', ], 'fluent_products_search_bar' => [ 'Product search bar shortcode, used by WP7 FC Search Bar.', 'app/Hooks/Handlers/ShortCodes/SearchBarShortCode.php:14', ], 'fluent_cart_customer_dashboard_button' => [ 'Customer dashboard button shortcode, used by WP10 FC Dashboard Button.', 'app/Hooks/Handlers/ShortCodes/CustomerDashboardButtonShortcode.php:9', ], ]; $contracts = []; foreach ($shortcodes as $tag => [$purpose, $evidence]) { $contracts[] = [ 'id' => $tag, 'type' => 'shortcode', 'purpose' => $purpose, 'verified_version' => self::VERIFIED_VERSION, 'evidence' => $evidence, 'check' => static function () use ($tag): bool { return function_exists('shortcode_exists') && shortcode_exists($tag); }, 'note' => '', ]; } // Zelfde 1.3.27-patroon als fluent_cart_single_product: de handlerklassen // bestaan, maar niets in de plugin-boot roept hun ::register() aan, dus de // shortcodes zijn op een echte install NIET actief (live geverifieerd op // sparkcore-shop). Contract is groen zolang de shortcode OF de handlerklasse // beschikbaar is; WP10 rendert via een guarded fallback op de handler. $inactiveShortcodes = [ 'fluent_cart_login_form' => [ 'Customer login form, used by WP10 FC Login Form. Shortcode is inactive in 1.3.27; render falls back to the CustomerLoginHandler class.', 'app/Hooks/Handlers/ShortCodes/CustomerLoginHandler.php:14 (class); no ::register() call in the plugin boot', 'FluentCart\\App\\Hooks\\Handlers\\ShortCodes\\CustomerLoginHandler', ], 'fluent_cart_registration_form' => [ 'Customer registration form, used by WP10 FC Registration Form. Shortcode is inactive in 1.3.27; render falls back to the CustomerRegistrationHandler class.', 'app/Hooks/Handlers/ShortCodes/CustomerRegistrationHandler.php:14 (class); no ::register() call in the plugin boot', 'FluentCart\\App\\Hooks\\Handlers\\ShortCodes\\CustomerRegistrationHandler', ], ]; foreach ($inactiveShortcodes as $tag => [$purpose, $evidence, $handlerClass]) { $contracts[] = [ 'id' => $tag, 'type' => 'shortcode', 'purpose' => $purpose, 'verified_version' => self::VERIFIED_VERSION, 'evidence' => $evidence, 'check' => static function () use ($tag, $handlerClass): bool { if (function_exists('shortcode_exists') && shortcode_exists($tag)) { return true; } return class_exists($handlerClass); }, 'note' => 'Fallbackpad: guarded render via de handlerklasse (WP10).', ]; } return $contracts; } /** * G2K kernel-commit: Order/OrderTransaction models for WP10's OrderContext * (receipt lookup). Verified directly against ReceiptHandler::renderRedirectPage() * (app/Hooks/Handlers/ShortCodes/ReceiptHandler.php:32-64), which is the * real lookup path: an `order_hash` request param resolves via * `(new Orders())->getBy('uuid', $orderHash)` against Order's own `uuid` * column, and a `trx_hash` request param resolves via * `OrderTransaction::query()->where('uuid', $transactionHash)->first()` * against OrderTransaction's own `uuid` column. Neither model declares a * static $fillable list (both rely on the framework's default * all-fillable behavior), so the fillable-reflection technique * modelFieldCheck() uses for ProductDetail/ProductVariation does not * apply here; these entries check class existence plus the migration * schema evidence for the `uuid` column instead. * * Discrepancy vs. a naive `order_hash`/`trx_hash` column-name assumption: * neither column is literally named that on either table. Both tables * use a plain `uuid` column; `order_hash`/`trx_hash` are only the * REQUEST/query-string parameter names ReceiptHandler reads, not the * database column names. Registering the real column name here (rather * than the request parameter name) is what "registreer DAT" in the task * brief calls for. * * @return array */ private static function g2kOrderModelContracts(): array { return [ [ 'id' => 'FluentCart\\App\\Models\\Order', 'type' => 'class', 'purpose' => 'FluentCart order model (table fct_orders); WP10 OrderContext resolves a receipt via this model\'s own uuid column, read off the order_hash request parameter.', 'verified_version' => self::VERIFIED_VERSION, 'evidence' => 'app/Models/Order.php:34 (class, $table = fct_orders); database/Migrations/OrdersMigrator.php:46 (`uuid` VARCHAR(100) NOT NULL); app/Hooks/Handlers/ShortCodes/ReceiptHandler.php:57 ((new Orders())->getBy(\'uuid\', $orderHash))', 'check' => static function (): bool { return class_exists('FluentCart\\App\\Models\\Order'); }, 'note' => '', ], [ 'id' => 'FluentCart\\App\\Models\\OrderTransaction', 'type' => 'class', 'purpose' => 'FluentCart order-transaction model (table fct_order_transactions); WP10 OrderContext resolves a receipt via this model\'s own uuid column, read off the trx_hash request parameter.', 'verified_version' => self::VERIFIED_VERSION, 'evidence' => 'app/Models/OrderTransaction.php:21 (class, $table = fct_order_transactions); database/Migrations/OrderTransactionsMigrator.php:31 (`uuid` VARCHAR(100) NULL DEFAULT \'\'); app/Hooks/Handlers/ShortCodes/ReceiptHandler.php:59-60 (OrderTransaction::query()->where(\'uuid\', $transactionHash)->first())', 'check' => static function (): bool { return class_exists('FluentCart\\App\\Models\\OrderTransaction'); }, 'note' => '', ], ]; } /** * WP9 (Golf 3, ELITE-PLAN.md par. 281-292): `CheckoutRenderer` is the load-bearing * class the FC_Checkout wrapper and its 12 children render through on the live path * (never a reimplementation of FluentCart's own checkout markup). Every method below * was verified directly against the live 1.3.27 source * (`app/Services/Renderer/CheckoutRenderer.php`), not assumed from the OxyFluentCart * inspiration source, which this fork's own audit found calls a slightly different * subset (e.g. oxy's `Ship_to_Different_Address/ssr.php` calls * `renderShipToDifferentField()` with no live/builder branch at all; this plugin's own * `renderAddressFields()`/`renderBillingAddressFields()`/ * `renderShippingAddressFields()` split is the one actually used here, see * `docs/audit/fluentcart-contracts.md` par. 9 for the full evidence trail). * * @return array */ private static function wp9CheckoutRendererContracts(): array { $checkoutRendererClass = 'FluentCart\\App\\Services\\Renderer\\CheckoutRenderer'; $cartHelperClass = 'FluentCart\\App\\Helpers\\CartHelper'; $cartRendererClass = 'FluentCart\\App\\Services\\Renderer\\CartRenderer'; $cartSummaryRenderClass = 'FluentCart\\App\\Services\\Renderer\\CartSummaryRender'; $modalCheckoutRendererClass = 'FluentCart\\App\\Services\\Renderer\\ModalCheckoutRenderer'; $contracts = [ [ 'id' => $cartHelperClass . '::getCart', 'type' => 'method', 'purpose' => 'Resolves the active FluentCart\\App\\Models\\Cart for the current request/hash; every checkout element gates its render on a non-empty cart from this single call, mirroring CheckoutPageHandler::render() exactly.', 'verified_version' => self::VERIFIED_VERSION, 'evidence' => 'app/Helpers/CartHelper.php:20-26', 'check' => static function () use ($cartHelperClass): bool { return method_exists($cartHelperClass, 'getCart'); }, 'note' => '', ], [ 'id' => $checkoutRendererClass, 'type' => 'class', 'purpose' => 'Constructor CheckoutRenderer(Cart $cart, array $config = []); the single live renderer every FC_Checkout* sub-element wraps.', 'verified_version' => self::VERIFIED_VERSION, 'evidence' => 'app/Services/Renderer/CheckoutRenderer.php:17-98', 'check' => static function () use ($checkoutRendererClass): bool { return class_exists($checkoutRendererClass); }, 'note' => '', ], [ 'id' => $cartRendererClass . '::renderEmpty', 'type' => 'method', 'purpose' => 'Empty-cart renderer, signature renderEmpty($emptyMessage = null, $continueShoppingUrl = null, $continueShoppingLabel = null, $continueShoppingAriaLabel = null); FC_Checkout renders this instead of a form when CartHelper::getCart() has no cart_data, mirroring CheckoutPageHandler::render()\'s own empty-cart branch bit for bit.', 'verified_version' => self::VERIFIED_VERSION, 'evidence' => 'app/Services/Renderer/CartRenderer.php:524-548; app/Hooks/Handlers/ShortCodes/Checkout/CheckoutPageHandler.php:29-31', 'check' => static function () use ($cartRendererClass): bool { return method_exists($cartRendererClass, 'renderEmpty'); }, 'note' => '', ], ]; $methods = [ 'wrapperStart' => ['Opens the outer checkout wrapper div with data-fluent-cart-checkout-page and fires fluent_cart/before_checkout_page_start / fluent_cart/after_checkout_page_start; FC_Checkout uses this instead of hand-writing the wrapper markup.', 'app/Services/Renderer/CheckoutRenderer.php:131-167'], 'wrapperEnd' => ['Closes the wrapper div and fires fluent_cart/before_checkout_page_close / fluent_cart/after_checkout_page_close.', 'app/Services/Renderer/CheckoutRenderer.php:267-278'], 'renderNotices' => ['Renders cart notices (__cart_notices + fluent_cart/checkout_page_notices filter) above the form; FC_Checkout calls this between wrapperStart() and the
tag.', 'app/Services/Renderer/CheckoutRenderer.php:169-198'], 'getFragment' => ['AJAX-fragment dispatcher (shipping_methods|payment_methods|cart_summary_fragment); not used by the atomized SSR path itself but registered here because FluentCart\'s own checkout-routes AJAX handler (which the atomized checkout\'s native JS still talks to) depends on it, so a break here breaks live re-render after a field change even though this plugin never calls it directly.', 'app/Services/Renderer/CheckoutRenderer.php:114-129'], 'renderNameFields' => ['Renders the Name/Email section via CheckoutFieldsSchema::getNameEmailFieldsSchema(). Live path for FC_Checkout_Name_Email.', 'app/Services/Renderer/CheckoutRenderer.php:311-318'], 'renderCreateAccountField' => ['Signature renderCreateAccountField($atts = []); guest-only, gated on StoreSettings::get(\'user_account_creation_mode\') === \'user_choice\'. Live path for FC_Checkout_Create_Account.', 'app/Services/Renderer/CheckoutRenderer.php:280-309'], 'renderBillingAddressFields' => ['Signature renderBillingAddressFields($section_title = \'\'). Live path for FC_Checkout_Billing.', 'app/Services/Renderer/CheckoutRenderer.php:423-474'], 'renderShipToDifferentField' => ['Signature renderShipToDifferentField($atts = []); atts accepts title/wrapper_atts exactly like the OxyFluentCart precedent. Live path for FC_Checkout_Ship_Different.', 'app/Services/Renderer/CheckoutRenderer.php:476-506'], 'renderShippingAddressFields' => ['Signature renderShippingAddressFields($section_title = \'\'); only meaningful output when Cart::requireShipping() is true. Live path for FC_Checkout_Shipping_Address.', 'app/Services/Renderer/CheckoutRenderer.php:508-556'], 'renderShippingOptions' => ['No-arg; renders available shipping methods via AddressHelper::getShippingMethods() + ShippingMethodsRender. Live path for FC_Checkout_Shipping_Methods.', 'app/Services/Renderer/CheckoutRenderer.php:613-641'], 'renderOrderNoteField' => ['Signature renderOrderNoteField($attr_title = \'\'); suppressed for pure-digital carts unless fluent_cart/disable_order_notes_for_digital_products is filtered false. Live path for FC_Checkout_Order_Notes.', 'app/Services/Renderer/CheckoutRenderer.php:558-611'], 'agreeTerms' => ['Signature agreeTerms($atts = []); gated on CheckoutFieldsSchema::isTermsVisible(). Live path for FC_Checkout_Agree_Terms.', 'app/Services/Renderer/CheckoutRenderer.php:872-910'], 'renderPaymentMethods' => ['Signature renderPaymentMethods($atts = []); returns empty string when the cart total (and any recurring total) is 0. Live path for FC_Checkout_Payment_Methods.', 'app/Services/Renderer/CheckoutRenderer.php:659-718'], 'renderCheckoutButton' => ['Signature renderCheckoutButton($atts = \'\'); reads fluent_cart/checkout_page_order_button_text. Live path for FC_Checkout_Submit.', 'app/Services/Renderer/CheckoutRenderer.php:720-753'], ]; foreach ($methods as $method => [$purpose, $evidence]) { $contracts[] = [ 'id' => $checkoutRendererClass . '::' . $method, 'type' => 'method', 'purpose' => $purpose, 'verified_version' => self::VERIFIED_VERSION, 'evidence' => $evidence, 'check' => static function () use ($checkoutRendererClass, $method): bool { return method_exists($checkoutRendererClass, $method); }, 'note' => '', ]; } $contracts[] = [ 'id' => $cartSummaryRenderClass, 'type' => 'class', 'purpose' => 'Constructor CartSummaryRender(Cart $cart); render($withWrapper = true) emits the data-fluent-cart-checkout-page-cart-items-wrapper block. Live path for FC_Checkout_Order_Summary.', 'verified_version' => self::VERIFIED_VERSION, 'evidence' => 'app/Services/Renderer/CartSummaryRender.php:11-47', 'check' => static function () use ($cartSummaryRenderClass): bool { return class_exists($cartSummaryRenderClass) && method_exists($cartSummaryRenderClass, 'render'); }, 'note' => '', ]; $contracts[] = [ 'id' => $modalCheckoutRendererClass, 'type' => 'class', 'purpose' => 'Constructor ModalCheckoutRenderer(Cart $cart, array $config = []); render() outputs the modal container/close-button/iframe shell (data-fct-checkout-modal-container). Used by FC_Modal_Checkout. NOTE (this fork\'s own audit): the modal\'s form content (renderForm()/renderCheckoutDetails()) is loaded into the iframe by FluentCart\'s own WebRoutes::handleModalCheckout() AJAX route, not by directly calling renderForm() from PHP output; FC_Modal_Checkout therefore only ever emits render()\'s container shell, exactly like FluentCart\'s own global wp_footer hook (WebRoutes::renderModalCheckout()) does.', 'verified_version' => self::VERIFIED_VERSION, 'evidence' => 'app/Services/Renderer/ModalCheckoutRenderer.php:21-107; app/Http/Routes/WebRoutes.php:38-60,277-322 (renderModalCheckout()/handleModalCheckout())', 'check' => static function () use ($modalCheckoutRendererClass): bool { return class_exists($modalCheckoutRendererClass) && method_exists($modalCheckoutRendererClass, 'render'); }, 'note' => '', ]; return $contracts; } /** * WP9: `CheckoutFieldsSchema`/`FormFieldRenderer` are the two classes the builder-path * schema reconstruction (task instruction 3, "$isBuilder schema-reconstructie") depends * on; both were independently re-verified against the live 1.3.27 source rather than * trusted from the OxyFluentCart reference alone. * * @return array */ private static function wp9CheckoutSchemaAndFormContracts(): array { $schemaClass = 'FluentCart\\App\\Services\\Renderer\\CheckoutFieldsSchema'; $formRendererClass = 'FluentCart\\App\\Services\\Renderer\\FormFieldRenderer'; $schemaMethods = [ 'getNameEmailFieldsSchema' => ['Signature getNameEmailFieldsSchema($cart = null, $scope = \'render\'); called with $cart = null, $scope = \'builder\' this returns the full field-schema shape with placeholder/label metadata but WITHOUT reading any real cart/customer values, exactly the anonymized builder-preview path this fork needs (verified: the customer-value branch is gated on `if ($cart && $scope === \'render\')`).', 'app/Services/Renderer/CheckoutFieldsSchema.php:37-99'], 'getAddressBaseFields' => ['Signature getAddressBaseFields($config, $scope = \'render\'); with $scope !== \'render\' the per-field requirement filtering (CheckoutFieldsSchema::getCheckoutFieldsRequirements()) is skipped entirely and every possible address field is returned, which is exactly the "verborgen secties zichtbaar in builder" behavior task instruction 3 requires, verified at the `if ($scope === \'render\')` gate.', 'app/Services/Renderer/CheckoutFieldsSchema.php:101-327 (gate at line 302)'], 'getCheckoutFieldsRequirements' => ['Signature getCheckoutFieldsRequirements($addressType = \'billing\', $productType = \'digital\', $withShipping = false); live-path-only (per above), used by CheckoutAdapter for the physical/digital field-requirement preview, never for gating whether a builder-path field renders.', 'app/Services/Renderer/CheckoutFieldsSchema.php:344-'], 'isTermsVisible' => ['No-arg; gates whether the Agree Terms section renders at all (both live and builder path).', 'app/Services/Renderer/CheckoutFieldsSchema.php:689-694'], 'getTermsText' => ['No-arg; the default terms-acceptance copy, used verbatim by both CheckoutRenderer::agreeTerms() and FC_Checkout_Agree_Terms\'s builder-path reconstruction.', 'app/Services/Renderer/CheckoutFieldsSchema.php:695-706'], 'getFieldsSettings' => ['No-arg; the raw store-configured field-settings tree (basic_info/shipping_address/etc.) that both getNameEmailFieldsSchema() and getAddressBaseFields() read from; consumed directly by CheckoutAdapter for the FC_Modal_Checkout billing preview.', 'app/Services/Renderer/CheckoutFieldsSchema.php:558-'], ]; $contracts = []; foreach ($schemaMethods as $method => [$purpose, $evidence]) { $contracts[] = [ 'id' => $schemaClass . '::' . $method, 'type' => 'method', 'purpose' => $purpose, 'verified_version' => self::VERIFIED_VERSION, 'evidence' => $evidence, 'check' => static function () use ($schemaClass, $method): bool { return method_exists($schemaClass, $method); }, 'note' => '', ]; } $formMethods = [ 'renderField' => ['Signature renderField($fieldData = []); renders one input/textarea/select/checkbox given a field-data array; used by CheckoutAdapter\'s guarded builder-path reconstruction for single fields (order notes textarea, checkboxes).', 'app/Services/Renderer/FormFieldRenderer.php:9-61'], 'renderSection' => ['Signature renderSection($sectionData = []); renders a titled fieldset from a fields[] map exactly as CheckoutFieldsSchema::getAddressBaseFields()/getNameEmailFieldsSchema() shape their return value; the core builder-path rendering primitive for FC_Checkout_Name_Email/Billing/Shipping_Address.', 'app/Services/Renderer/FormFieldRenderer.php:75-'], 'renderAddressSelect' => ['Signature renderAddressSelect($fieldData = []); only reachable on the live path when a logged-in customer has saved addresses (CheckoutRenderer::validateAddressField()); the builder path never has a real customer, so this is registered for completeness but is a live-only capability, never called from a builder-path reconstruction.', 'app/Services/Renderer/FormFieldRenderer.php:63-73'], ]; foreach ($formMethods as $method => [$purpose, $evidence]) { $contracts[] = [ 'id' => $formRendererClass . '::' . $method, 'type' => 'method', 'purpose' => $purpose, 'verified_version' => self::VERIFIED_VERSION, 'evidence' => $evidence, 'check' => static function () use ($formRendererClass, $method): bool { return method_exists($formRendererClass, $method); }, 'note' => '', ]; } return $contracts; } /** * WP9: the two submit-button filters plus the native checkout-form action hooks the * FC_Checkout wrapper and FC_Checkout_Submit fire alongside FluentCart's own render * calls, so third-party code hooked to the native checkout keeps working unchanged on * the atomized tree (architectuurprincipe 3, data-attribuut/hook-contract). * * Filters/actions cannot be existence-checked the way a class or shortcode tag can * (apply_filters/do_action always "exist" as language constructs; there is nothing to * reflect on ahead of the actual call), so every entry here is registered * 'uncheckable' with the exact call-site evidence, per the same discipline * jsAndDomContracts() already uses for JS events. * * @return array */ private static function wp9CheckoutFilterAndHookContracts(): array { $note = 'filter/action call-site, uncheckable ahead of invocation from PHP; verified by direct file:line read of the live 1.3.27 source'; $entries = [ [ 'fluent_cart/checkout_page_order_button_text', 'filter', 'Filters the Place Order button text; applied inside CheckoutRenderer::renderCheckoutButton() with the literal default \'Place order\'. FC_Checkout_Submit hooks this to apply its own button-text content control, exactly like the OxyFluentCart precedent (Checkout_Submit_Button/ssr.php).', 'app/Services/Renderer/CheckoutRenderer.php:723', ], [ 'fluent_cart/checkout/localize_data', 'filter', 'Filters the checkout page\'s localized JS data payload (ajaxurl, nonces, translations); applied in AssetLoader::loadCheckoutAssets(). This plugin does not currently need to filter it (no custom localize-data payload shipped), but the filter is registered here per task instruction 1 as a verified, available extension point for the checkout asset-loading path FC_Checkout depends on.', 'app/Modules/Templating/AssetLoader.php:555', ], [ 'fluent_cart/before_checkout_page_start', 'action', 'Fires immediately before CheckoutRenderer::wrapperStart() opens the wrapper div; FC_Checkout fires the equivalent native hook around its own wrapper markup so header-injecting third-party code keeps working.', 'app/Services/Renderer/CheckoutRenderer.php:155-157', ], [ 'fluent_cart/after_checkout_page_start', 'action', 'Fires immediately after the wrapper div opens (also handles the deprecated fluent_cart/afrer_checkout_page_start alias via do_action_deprecated).', 'app/Services/Renderer/CheckoutRenderer.php:162-166', ], [ 'fluent_cart/before_checkout_form', 'action', 'Fires immediately before the inner tag opens.', 'app/Services/Renderer/CheckoutRenderer.php:213', ], [ 'fluent_cart/checkout_form_opening', 'action', 'Fires immediately after the tag opens, before any field markup.', 'app/Services/Renderer/CheckoutRenderer.php:217', ], [ 'fluent_cart/before_payment_methods', 'action', 'Fires immediately before the payment-methods block; FC_Checkout_Payment_Methods fires this in front of its own dual-path output.', 'app/Services/Renderer/CheckoutRenderer.php:235', ], [ 'fluent_cart/after_payment_methods', 'action', 'Fires immediately after the payment-methods block.', 'app/Services/Renderer/CheckoutRenderer.php:241', ], [ 'fluent_cart/after_checkout_button', 'action', 'Fires immediately after the submit-button markup; FC_Checkout_Submit fires this after its own dual-path output.', 'app/Services/Renderer/CheckoutRenderer.php:245', ], [ 'fluent_cart/after_order_notes', 'action', 'Fires immediately after the order-notes field; FC_Checkout_Order_Notes fires this after its own dual-path output.', 'app/Services/Renderer/CheckoutRenderer.php:258', ], [ 'fluent_cart/after_checkout_form', 'action', 'Fires immediately after the closing tag, before wrapperEnd().', 'app/Services/Renderer/CheckoutRenderer.php:264', ], [ 'fluent_cart/before_checkout_page_close', 'action', 'Fires immediately before CheckoutRenderer::wrapperEnd() closes the wrapper div.', 'app/Services/Renderer/CheckoutRenderer.php:269-271', ], [ 'fluent_cart/after_checkout_page', 'action', 'Fires immediately after the wrapper div closes.', 'app/Services/Renderer/CheckoutRenderer.php:275-277', ], ]; $contracts = []; foreach ($entries as [$id, $type, $purpose, $evidence]) { $contracts[] = [ 'id' => $id, 'type' => $type, 'purpose' => $purpose, 'verified_version' => self::VERIFIED_VERSION, 'evidence' => $evidence, 'check' => null, 'note' => $note, ]; } return $contracts; } /** * WP9: the checkout data-attribute contract (architectuurprincipe 3) plus the modal * checkout open/close JS events, verified against the live 1.3.27 PHP renderer source * AND the shipped FluentCartApp.js bundle. Uncheckable from a bare PHP request for the * same reason as jsAndDomContracts(): these are DOM-level facts, exercised by the * E2E/jsdom layer (WP13), not this contract-check script. * * @return array */ private static function wp9CheckoutDomAttributeContracts(): array { $e2eNote = 'uncheckable from PHP, verify in the E2E/jsdom layer (WP13)'; $entries = [ ['data-fluent-cart-checkout-page', 'dom-attribute', 'Root checkout wrapper marker, emitted by CheckoutRenderer::wrapperStart(). FC_Checkout\'s own wrapper div carries this exact attribute.', 'app/Services/Renderer/CheckoutRenderer.php:152'], ['data-fluent-cart-checkout-page-checkout-form', 'dom-attribute', 'Marks the
element FluentCart\'s own checkout JS binds submit/validation/AJAX handlers to. FC_Checkout\'s own tag must carry this exact attribute for the native JS to function unchanged.', 'app/Services/Renderer/CheckoutRenderer.php:208'], ['data-fluent-cart-checkout-payment-methods', 'dom-attribute', 'Marks the payment-methods container, the fragment-replace target for the AJAX payment-method re-render. Emitted by both CheckoutRenderer::renderCheckoutForm() and ModalCheckoutRenderer.', 'app/Services/Renderer/CheckoutRenderer.php:237'], ['data-fluent-cart-checkout-page-checkout-button', 'dom-attribute', 'Marks the submit button; carries data-value with the localized button text FluentCart\'s own JS reads while showing a loading state.', 'app/Services/Renderer/CheckoutRenderer.php:728-730'], ['data-fluent-cart-checkout-page-form-section', 'dom-attribute', 'Marks the billing address form-section wrapper (role="region").', 'app/Services/Renderer/CheckoutRenderer.php:469'], ['data-fluent-cart-checkout-page-shipping-fields', 'dom-attribute', 'Marks the shipping-address form-section wrapper; carries an inline style toggling visibility based on the ship_to_different form value (native JS also toggles this).', 'app/Services/Renderer/CheckoutRenderer.php:549-552'], ['data-fluent-cart-checkout-page-form-input-wrapper', 'dom-attribute', 'Marks each individual billing/shipping input\'s wrapper div.', 'app/Services/Renderer/CheckoutRenderer.php:444,532'], ['data-fluent-cart-ship-to-different-address', 'dom-attribute', 'Marks the "ship to different address" checkbox input itself (value "yes"), with aria-controls pointing at the shipping section id. Native JS listens for its change event.', 'app/Services/Renderer/CheckoutRenderer.php:497-500'], ['data-fluent-cart-agree-terms', 'dom-attribute', 'Marks the agree-terms checkbox input (value "yes").', 'app/Services/Renderer/CheckoutRenderer.php:889'], ['data-fluent-cart-checkout-page-cart-items-wrapper', 'dom-attribute', 'Marks the order-summary wrapper, the fragment-replace target for the AJAX cart-summary re-render. Emitted by CartSummaryRender::render($withWrapper = true).', 'app/Services/Renderer/CartSummaryRender.php:28'], ['data-fluent-cart-checkout-page-checkout-form-order-summary', 'dom-attribute', 'Marks the order-summary aria region (aria-labelledby="order-summary-heading") inside the summary sidebar.', 'app/Services/Renderer/CheckoutRenderer.php:250'], ['data-fluent-cart-checkout-item-wrapper', 'dom-attribute', 'Marks the line-items list inside the order summary.', 'app/Services/Renderer/CartSummaryRender.php:36'], ['data-fct-checkout-modal-container', 'dom-attribute', 'The modal-checkout outer container, opacity/visibility toggled by ModalCheckoutOpen/Close.js. Emitted by ModalCheckoutRenderer::render(). FC_Modal_Checkout emits this exact container.', 'app/Services/Renderer/ModalCheckoutRenderer.php:86'], ['data-fct-checkout-modal', 'dom-attribute', 'The modal-checkout panel, gains class fct-checkout-modal-open while open.', 'app/Services/Renderer/ModalCheckoutRenderer.php:87'], ['data-fct-checkout-modal-close', 'dom-attribute', 'The modal close button, click-bound by FluentCartApp.js.', 'app/Services/Renderer/ModalCheckoutRenderer.php:92'], ['data-fct-checkout-modal-iframe', 'dom-attribute', 'The iframe that the modal form is actually loaded into (src set to the instant_checkout/modal_checkout URL on open).', 'app/Services/Renderer/ModalCheckoutRenderer.php:106'], ['data-fct-instant-checkout-button', 'dom-attribute', 'Any element with this attribute opens the modal checkout on click (delegated listener, also re-bound via MutationObserver for dynamically inserted buttons).', 'assets/FluentCartApp.js (modal-checkout controller class)'], ['fluentCartModalCheckoutOpened', 'js-event', 'Dispatched on window when the modal checkout iframe finishes opening, detail: {checkoutUrl}.', 'assets/FluentCartApp.js (modal-checkout controller openModal())'], ['fluentCartModalCheckoutClosed', 'js-event', 'Dispatched on window after the modal checkout close animation completes.', 'assets/FluentCartApp.js (modal-checkout controller closeModal())'], ['fluentCartCheckoutComplete', 'js-event', 'postMessage type sent from the checkout iframe to its parent window on successful order completion (detail.redirectUrl optional); the parent modal controller listens for this to redirect or close.', 'assets/FluentCartApp.js (modal-checkout controller message listener)'], ]; $contracts = []; foreach ($entries as [$id, $type, $purpose, $evidence]) { $contracts[] = [ 'id' => $id, 'type' => $type, 'purpose' => $purpose, 'verified_version' => self::VERIFIED_VERSION, 'evidence' => $evidence, 'check' => null, 'note' => $e2eNote, ]; } return $contracts; } /** * Bugfix SSR-dropdown en prijs-update (2026-07-15, CHANGELOG.md "## * Unreleased" > "### Fix SSR-dropdown en prijs-update"). Append-only * addition (this fork owns Contracts.php for adding new entries only, * never editing an existing one): the native DOM structure * bd-elements/FC_Product_Variations/ssr.php now reads directly (via a * read-only DOMDocument/DOMXPath walk of the buffered native markup) to * build its server-rendered dropdown ` immediately before it (options_style=dropdown SSR-dropdown bugfix, 2026-07-15).', 'app/Services/Renderer/ProductRenderer.php:1675 (renderTabNav)', ], [ '[data-tab][role="tab"] with an "active" class (period tab-nav-item)', 'dom-attribute', 'Each item\'s data-tab attribute (select option value), trimmed text content (option label) and ' . '"active" class (pre-selected option) are read by FC_Product_Variations/ssr.php to build the ' . 'server-rendered period immediately before it.', 'app/Services/Renderer/ProductRenderer.php:1697,1710 (renderTabPane); :836,842 (renderVariants)', ], [ '.fct-product-variant-title (inside [data-fluent-cart-product-variant])', 'dom-attribute', 'Each variant\'s visible label text, read by FC_Product_Variations/ssr.php (falling back to the ' . 'item\'s own full trimmed text if absent) for its server-rendered options