['ALL']` and silently drops any key outside its own * whitelist. None of the conditions below need `query_builder`/`queryCallback` though: * "the current order" is never a WP_Query dimension (exactly one order exists per * receipt-page request, or none), so `supports` is always * `['element_display', 'templating']` -- both evaluated through the exact same * `OrderContext`-backed `callback`, matching the task-3 instruction that these * conditions are "allemaal op OrderContext" and "NOOIT query_builder". * * Every condition here reads ONLY through `OrderContext`'s typed accessors: no direct * FluentCart model access, no raw order id or uuid ever reaches a condition's own code * (task 1's "geen order-ID-enumeratie" / "geen orderdata in ... publieke output" rules * apply here too -- every callback below returns/compares only a short fixed-vocabulary * string or a boolean). * * Conditions registered (task instruction 3's "bijv." list, taken as the full binding * scope: order-status, payment-methode, bevat-product): * - `fcbd-order-status`: Order's own lifecycle `status` column * (processing/completed/on-hold/canceled/failed, `Status::ORDER_*`), never the * separate `payment_status` column (deliberately not also exposed here: the task * names exactly three dimensions, adding a fourth would be scope creep beyond the * binding brief). * - `fcbd-order-payment-method`: Order's own `payment_method` column, a free gateway * slug (stripe/paypal/mollie/manual/...) with no fixed vocabulary in FluentCart * core. Sinds de Templates-UI-hotfix van 2026-07-15 een DROPDOWN (distinct * payment_method uit de transacties + vangnetlijst): de templates-SPA's io-ts * accepteert geen valueInputType 'text', alleen * dropdown/datepicker/number/timepicker of weglaten. * - `fcbd-order-contains-product`: does the resolved order contain a specific, manually * chosen `fluent-products` post, via a real product-picker dropdown built with * Breakdance's own first-party `getPostsForMultiselect()` helper (verified: * `plugin/themeless/rules/conditions/singular.php:941-954`, the exact function the * real `post-dropdown-` conditions use), never an invented value shape. * * NOT wired into the plugin boot by this fork (task boundary: "Wijzig alleen bestanden * binnen jouw eigendom"; `src/Plugin.php` and `fluentcart-breakdance.php` are shared * files outside WP10's ownership). See this fork's final report for the exact two * `require` lines and one `add_action()` line an integration commit needs to add, * mirroring `ConditionRegistry::register()`/`TemplateRegistry::register()`'s own * `breakdance_register_template_types_and_conditions` hook point in `Plugin::hook()`. */ final class OrderConditions { public static function register(): void { if (!function_exists('\\Breakdance\\Themeless\\registerCondition')) { return; } self::registerOrderStatus(); self::registerPaymentMethod(); self::registerContainsProduct(); } private static function registerOrderStatus(): void { $is = defined('OPERAND_IS') ? OPERAND_IS : 'is'; $isNot = defined('OPERAND_IS_NOT') ? OPERAND_IS_NOT : 'is not'; $statuses = self::orderStatusVocabulary(); \Breakdance\Themeless\registerCondition([ 'proOnly' => false, 'supports' => ['element_display', 'templating'], 'availableForType' => self::orderConditionPostTypes(), 'slug' => 'fcbd-order-status', 'label' => __('Order status', 'sparkcore-commerce'), 'category' => __('FluentCart', 'sparkcore-commerce'), 'operands' => [$is, $isNot], 'values' => static fn(): array => [[ 'label' => __('Order status', 'sparkcore-commerce'), 'items' => array_map( static fn(string $value, string $label): array => ['text' => $label, 'value' => $value], array_keys($statuses), array_values($statuses) ), ]], 'callback' => static function ($operand, $value) use ($isNot): bool { if (!OrderContext::resolve()) { return false; } $matches = OrderContext::status() === (string) $value; return $operand === $isNot ? !$matches : $matches; }, 'templatePreviewableItems' => false, ]); } /** @return array Order status value => translated label. */ private static function orderStatusVocabulary(): array { if (class_exists('FluentCart\\App\\Helpers\\Status')) { $status = 'FluentCart\\App\\Helpers\\Status'; return [ $status::ORDER_PROCESSING => __('Processing', 'sparkcore-commerce'), $status::ORDER_COMPLETED => __('Completed', 'sparkcore-commerce'), $status::ORDER_ON_HOLD => __('On hold', 'sparkcore-commerce'), $status::ORDER_CANCELED => __('Canceled', 'sparkcore-commerce'), $status::ORDER_FAILED => __('Failed', 'sparkcore-commerce'), ]; } return [ 'processing' => __('Processing', 'sparkcore-commerce'), 'completed' => __('Completed', 'sparkcore-commerce'), 'on-hold' => __('On hold', 'sparkcore-commerce'), 'canceled' => __('Canceled', 'sparkcore-commerce'), 'failed' => __('Failed', 'sparkcore-commerce'), ]; } private static function registerPaymentMethod(): void { $is = defined('OPERAND_IS') ? OPERAND_IS : 'is'; $isNot = defined('OPERAND_IS_NOT') ? OPERAND_IS_NOT : 'is not'; \Breakdance\Themeless\registerCondition([ 'proOnly' => false, 'supports' => ['element_display', 'templating'], 'availableForType' => self::orderConditionPostTypes(), 'slug' => 'fcbd-order-payment-method', 'label' => __('Order payment method', 'sparkcore-commerce'), 'category' => __('FluentCart', 'sparkcore-commerce'), 'operands' => [$is, $isNot], // Geen valueInputType 'text': de templates-SPA valideert de // conditions-payload met io-ts en accepteert alleen // dropdown/datepicker/number/timepicker (productie-incident // 2026-07-15: "IO-TS validation failed" op conditions[..] // .valueInputType). Dropdown met echte waarden in plaats van // vrije tekst. 'values' => static function (): array { return [[ 'label' => __('Payment methods', 'sparkcore-commerce'), 'items' => self::paymentMethodItems(), ]]; }, 'callback' => static function ($operand, $value) use ($isNot): bool { if (!OrderContext::resolve()) { return false; } $value = is_array($value) ? (string) reset($value) : (string) $value; $matches = strtolower(OrderContext::paymentMethod()) === strtolower($value); return $operand === $isNot ? !$matches : $matches; }, 'templatePreviewableItems' => false, ]); } /** * Dropdown-items voor de betaalmethode-conditie: eerst de methoden die * ECHT in de transacties voorkomen (distinct payment_method op het * OrderTransaction-model, read-only), met als vangnet de gangbare * FluentCart-gateways zodat de dropdown op een verse shop niet leeg is. * * @return array */ private static function paymentMethodItems(): array { $methods = []; if (class_exists('FluentCart\\App\\Models\\OrderTransaction')) { try { $rows = \FluentCart\App\Models\OrderTransaction::query() ->select('payment_method') ->distinct() ->get(); foreach ($rows as $row) { $method = trim((string) ($row->payment_method ?? '')); if ($method !== '') { $methods[$method] = true; } } } catch (\Throwable $error) { // Read-only best effort; vangnet hieronder dekt de rest. $methods = []; } } if (!$methods) { foreach (['stripe', 'paypal', 'mollie', 'offline', 'test'] as $fallback) { $methods[$fallback] = true; } } $items = []; foreach (array_keys($methods) as $method) { $items[] = ['text' => ucfirst($method), 'value' => $method]; } return $items; } private static function registerContainsProduct(): void { $isOneOf = defined('OPERAND_ONE_OF') ? OPERAND_ONE_OF : 'is one of'; $isNoneOf = defined('OPERAND_NONE_OF') ? OPERAND_NONE_OF : 'is none of'; \Breakdance\Themeless\registerCondition([ 'proOnly' => false, 'supports' => ['element_display', 'templating'], 'availableForType' => self::orderConditionPostTypes(), 'slug' => 'fcbd-order-contains-product', 'label' => __('Order contains product', 'sparkcore-commerce'), 'category' => __('FluentCart', 'sparkcore-commerce'), 'operands' => [$isOneOf, $isNoneOf], 'values' => static function (): array { if (!function_exists('\\Breakdance\\Themeless\\Rules\\getPostsForMultiselect')) { return [['label' => __('Products', 'sparkcore-commerce'), 'items' => []]]; } return [[ 'label' => __('Products', 'sparkcore-commerce'), 'items' => \Breakdance\Themeless\Rules\getPostsForMultiselect(['post_type' => 'fluent-products']), ]]; }, 'callback' => static function ($operand, $value) use ($isNoneOf): bool { if (!OrderContext::resolve() || empty($value)) { return false; } $matches = false; foreach (array_map('absint', (array) $value) as $productId) { if (OrderContext::containsProduct($productId)) { $matches = true; break; } } return $operand === $isNoneOf ? !$matches : $matches; }, 'templatePreviewableItems' => false, ]); } /** * Post types this condition may be attached to. Unlike ConditionRegistry's own * product-scoped conditions (which are offered on `fluent-products`), a receipt/ * thank-you page is a normal WP `page` post -- FluentCart's own StoreSettings * assigns a `page` post as the thank-you/receipt destination, the same evidence * pattern `TemplateRegistry::explainWhyNoShopTemplateType()` already established * for the shop page -- plus Breakdance's own header/footer/block/popup CPTs so a * condition-guarded block embedded on that page via a global block still gets * offered the condition (mirrors `ConditionRegistry::productConditionPostTypes()` * exactly, `page` swapped in for `fluent-products`). * * @return string[] */ private static function orderConditionPostTypes(): array { $types = ['page']; foreach (['BREAKDANCE_HEADER_POST_TYPE', 'BREAKDANCE_BLOCK_POST_TYPE', 'BREAKDANCE_FOOTER_POST_TYPE', 'BREAKDANCE_POPUP_POST_TYPE', 'BREAKDANCE_ACF_BLOCK_POST_TYPE'] as $constant) { if (defined($constant)) { $types[] = constant($constant); } } return $types; } }