PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.6.5
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.6.5
1.6.5 1.6.4 1.6.3 1.6.2 1.6.1 1.6.0 1.5.4 1.5.5 1.5.3 1.5.2 1.5.1 1.5.0 1.4.2 1.4.1 1.4.0 1.3.28 1.3.27 1.3.26 1.3.25 1.3.23 1.3.22 1.3.21 1.3.20 1.3.19 trunk All 48 releases
← All changes | app/Modules/MCP/Tools/ContextTools.php +111 -12 1.5.4 → 1.6.5 View file →
@@ -1,8 +1,10 @@
1 1 <?php
2 2
3 3 namespace FluentCart\App\Modules\MCP\Tools;
4 4
5 +use FluentCart\App\App;
6 +use FluentCart\App\Helpers\Status;
5 7 use FluentCart\App\Models\Order;
6 8 use FluentCart\App\Models\Customer;
7 9 use FluentCart\App\Models\Subscription;
8 10 use FluentCart\App\Modules\MCP\AbilitiesRegistrar;
@@ -33,22 +35,28 @@
33 35 const CACHE_TTL = 60;
34 36
35 37 const CACHE_PREFIX = 'fluent_cart_mcp_context_';
36 38
37 - // The verified FluentCart domain enums. Hardcoded (with a filter override)
38 - // rather than scraped, so the agent always gets the complete valid set even
39 - // if a status currently has zero rows.
39 + // Baseline domain enums. The status families are re-read from the canonical
40 + // Status helper at runtime by enums() — this literal is only the fallback for
41 + // a family Status cannot answer for. Do not hand-maintain the status lists
42 + // here: an enum the agent trusts but the column can never hold turns every
43 + // filter built from it into a silent zero-row result.
40 44 const ENUMS = [
41 - 'order_statuses' => ['draft', 'pending', 'on-hold', 'processing', 'completed', 'canceled', 'failed', 'refunded', 'partial-refund'],
42 - // Kept in sync with Status::getPaymentStatuses(); 'authorized' is a valid
43 - // persisted status (card authorized, not yet captured) and must be listed
44 - // so clients can filter authorized orders through list-orders.
45 - 'payment_statuses' => ['paid', 'pending', 'failed', 'refunded', 'partially_refunded', 'partially_paid', 'authorized'],
45 + // Status::getOrderStatuses() plus PERSISTED_ONLY_ORDER_STATUSES — see that
46 + // constant for why the canonical helper is not the whole set.
47 + // 'partial-refund' is deliberately absent: unlike the others below, no code
48 + // path writes it (the column COMMENT lists it, but nothing persists it).
49 + 'order_statuses' => ['draft', 'pending', 'processing', 'completed', 'on-hold', 'canceled', 'failed', 'refunded'],
50 + // Kept in sync with Status::getPaymentStatuses(); 'authorized' (card
51 + // authorized, not yet captured) and 'payment_scheduled' are valid
52 + // persisted statuses and must be listed so clients can filter them.
53 + 'payment_statuses' => ['pending', 'paid', 'partially_paid', 'failed', 'refunded', 'partially_refunded', 'authorized', 'payment_scheduled'],
46 54 // 'none' = no shipping required (e.g. digital orders); reported when the
47 55 // stored value is empty. It is read-only — change-order-status won't set it.
48 56 'shipping_statuses' => ['none', 'unshipped', 'shipped', 'delivered', 'unshippable'],
49 57 'order_types' => ['payment', 'renewal', 'subscription'],
50 - 'subscription_statuses' => ['active', 'trialing', 'paused', 'canceled', 'failing', 'expired', 'expiring', 'past_due', 'intended', 'pending', 'completed'],
58 + 'subscription_statuses' => ['pending', 'active', 'failing', 'paused', 'expired', 'expiring', 'canceled', 'trialing', 'intended', 'past_due', 'completed'],
51 59 // installment = fixed-term split-pay plan (a lifetime license paid off in
52 60 // a finite number of charges, bill_times > 0); recurring = open-ended
53 61 // subscription (bill_times = 0). Derived from bill_times, never the title.
54 62 'plan_types' => ['installment', 'recurring'],
@@ -57,8 +65,87 @@
57 65 'coupon_types' => ['fixed', 'percentage'],
58 66 'order_modes' => ['live', 'test'],
59 67 ];
60 68
69 + /**
70 + * Order statuses fct_orders.status genuinely holds that Status::getOrderStatuses()
71 + * does NOT list, because that helper answers "what may an admin SET an order to",
72 + * not "what can this column contain".
73 + *
74 + * An enum is wrong in two directions, and only one of them is loud. Listing a
75 + * value the column can never hold gives the agent a filter that silently returns
76 + * zero rows. OMITTING a value the column does hold is worse: those rows become
77 + * unreachable, and because the value is missing from the input_schema enum the
78 + * call is rejected outright, so the agent cannot even discover the rows exist.
79 + *
80 + * Each of these is written by a core path, verified in source:
81 + * - draft: the column DEFAULT (database/Migrations/OrdersMigrator.php).
82 + * - pending: every store-managed renewal invoice
83 + * (StoreManagedRenewal/Services/RenewalService.php:113).
84 + * - refunded: the WooCommerce migrator maps wc-refunded to it
85 + * (WooCommerceMigrator/Services/OrderMigrationService.php).
86 + *
87 + * So a store using store-managed renewals, or migrated from WooCommerce, has rows
88 + * the five-value helper cannot describe. Keep this list in step with the writers,
89 + * not with the admin dropdown.
90 + *
91 + * Note COD is NOT one of them: a COD checkout creates the order as 'on-hold' and
92 + * only its payment_status is pending. Cod::maybeUpdatePayments() looks like an
93 + * order-status writer but has no callers.
94 + */
95 + const PERSISTED_ONLY_ORDER_STATUSES = ['draft', 'pending', 'refunded'];
96 +
97 + /**
98 + * The enums the agent is told to trust, with every status family re-read from
99 + * the canonical Status helper so this payload can never drift from what the
100 + * columns actually hold (a drifted enum is worse than a missing one — the
101 + * agent builds a valid-looking filter that always returns zero rows).
102 + *
103 + * Status::get*Statuses() are themselves filtered, so a Pro/add-on status
104 + * registered through those hooks shows up here automatically.
105 + *
106 + * @return array
107 + */
108 + public static function enums()
109 + {
110 + $enums = self::ENUMS;
111 +
112 + $live = [
113 + 'order_statuses' => [Status::class, 'getOrderStatuses'],
114 + 'payment_statuses' => [Status::class, 'getPaymentStatuses'],
115 + 'shipping_statuses' => [Status::class, 'getShippingStatuses'],
116 + 'subscription_statuses' => [Status::class, 'getSubscriptionStatuses'],
117 + ];
118 +
119 + foreach ($live as $key => $callable) {
120 + try {
121 + $values = array_values(array_map('strval', array_keys((array) call_user_func($callable))));
122 + } catch (\Throwable $e) {
123 + // Keep the baseline rather than shipping an empty enum: an empty
124 + // list reads as "no valid values" and blocks every filter.
125 + continue;
126 + }
127 + if (!$values) {
128 + continue;
129 + }
130 + // 'none' is an MCP-only reported value (empty stored shipping status)
131 + // that Status has no constant for — re-add it after the live overlay.
132 + if ($key === 'shipping_statuses') {
133 + array_unshift($values, 'none');
134 + }
135 + // Statuses the column holds that the helper does not list. Unioned, not
136 + // overwritten: getOrderStatuses() is the settable list, so overwriting
137 + // would drop 'pending'/'draft'/'refunded' and make those real rows
138 + // unfilterable. See PERSISTED_ONLY_ORDER_STATUSES.
139 + if ($key === 'order_statuses') {
140 + $values = array_merge($values, self::PERSISTED_ONLY_ORDER_STATUSES);
141 + }
142 + $enums[$key] = array_values(array_unique($values));
143 + }
144 +
145 + return $enums;
146 + }
147 +
61 148 // Payment statuses that count as realized revenue. Centralized so every
62 149 // tool (context, reports, aggregates) agrees on what "paid" means.
63 150 const PAID_STATUSES = ['paid', 'partially_paid', 'partially_refunded'];
64 151
@@ -138,12 +225,19 @@
138 225 $store = [
139 226 'name' => get_bloginfo('name'),
140 227 'url' => site_url(),
141 228 'version' => defined('FLUENTCART_VERSION') ? FLUENTCART_VERSION : null,
142 - 'pro_active' => defined('FLUENT_CART_PRO') || defined('FLUENTCART_PRO_VERSION'),
229 + // Must agree with the App::isProActive() check the Pro-gated paths
230 + // (advanced_filters, get-search-schema) actually run — a false here on
231 + // a Pro store makes an agent skip the whole advanced-search surface.
232 + 'pro_active' => App::isProActive(),
143 233 'currency' => MCPHelper::currencyContext(),
144 234 'timezone' => wp_timezone_string(),
145 235 'current_time' => MCPHelper::toIso8601(DateTime::gmtNow()),
236 + // Named for the capability rather than the licence, so an agent does
237 + // not have to infer what pro_active buys it before spending a call on
238 + // get-search-schema (which rejects outright without Pro).
239 + 'advanced_search' => App::isProActive() ? 'available' : 'unavailable',
146 240 ];
147 241
148 242 // Headline stats are dashboard data: gate them on dashboard_stats/view so
149 243 // a narrow read role can still get context (enums, currency, permissions)
@@ -156,9 +250,9 @@
156 250 [
157 251 'you' => $you,
158 252 'store' => $store,
159 253 'stats' => $stats,
160 - 'enums' => apply_filters('fluent_cart/mcp_enums', self::ENUMS),
254 + 'enums' => apply_filters('fluent_cart/mcp_enums', self::enums()),
161 255 'reference_kinds' => self::referenceKinds(),
162 256 'tool_index' => self::toolIndex(),
163 257 'guidelines' => self::guidelines(),
164 258 ]
@@ -320,9 +414,14 @@
320 414 . 'Money is returned as both a number (amount) and a formatted string (display) — quote display, compare amount. '
321 415 . 'Dates are ISO-8601 UTC; pass a relative range (e.g. last_30_days) or explicit start_date/end_date to report tools. '
322 416 . 'Use the exact enum values from this payload — never invent a status. '
323 417 . 'Reports never sum across currencies; filter by one currency if the store has several. '
324 - . 'When a list tool\'s named filters cannot express a segmentation (OR groups, relative dates, per-property operators, relation properties like transactions/UTM/labels), call get-search-schema for the entity and pass advanced_filters to its list tool (requires Pro). '
418 + // Stated as a fact about THIS store, not a generic "requires Pro":
419 + // an agent that reads the generic form still builds the filter and
420 + // only discovers the gate when the call is rejected.
421 + . (App::isProActive()
422 + ? 'When a list tool\'s named filters cannot express a segmentation (OR groups, relative dates, per-property operators, relation properties like transactions/UTM/labels), call get-search-schema for the entity and pass advanced_filters to its list tool. '
423 + : 'Advanced search is UNAVAILABLE on this store (store.advanced_search = unavailable): FluentCart Pro is not active, so get-search-schema and the advanced_filters parameter will be rejected. Do not build advanced_filters — use the named filters on the list-* tools and the query-* tools for aggregation. ')
325 424 . 'Writes (refund-order, change-subscription-status:cancel) require a dry_run preview first.';
326 425
327 426 return apply_filters('fluent_cart/mcp_guidelines', $default);
328 427 }