PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.5.4
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.5.4
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
fluent-cart / app / Modules / MCP / Tools / ContextTools.php

ContextTools.php in FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler 1.5.4, at app/Modules/MCP/Tools/ContextTools.php

568 lines 27.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentCart\App\Modules\MCP\Tools;
4
5 use FluentCart\App\Models\Order;
6 use FluentCart\App\Models\Customer;
7 use FluentCart\App\Models\Subscription;
8 use FluentCart\App\Modules\MCP\AbilitiesRegistrar;
9 use FluentCart\App\Modules\MCP\Support\MCPHelper;
10 use FluentCart\App\Modules\MCP\Support\PermissionGate;
11 use FluentCart\App\Services\DateTime\DateTime;
12 use FluentCart\App\Services\Permission\PermissionManager;
13
14 /**
15 * Discovery tools — the agent's entry point into a FluentCart store.
16 *
17 * `get-store-context` is the documented "call this first" tool. One call tells
18 * the agent who it is, what it's allowed to do, the store's money/time
19 * conventions, headline numbers, and every valid enum value — so it never has
20 * to guess a status string or invent a currency format. It's cached (60s) and
21 * invalidated when reference data changes, because it's called every session.
22 *
23 * `list-reference-data` is the on-demand lookup for the heavier reference lists
24 * (coupons, labels, tax/shipping config) the agent only sometimes needs — kept
25 * OUT of the context payload so the first call stays lean.
26 *
27 * Parameter philosophy: get-store-context takes nothing (zero friction, it's
28 * discovery). list-reference-data takes only `kinds[]` — the agent asks for
29 * exactly the lists it needs, and we return only the kinds its role can see.
30 */
31 class ContextTools
32 {
33 const CACHE_TTL = 60;
34
35 const CACHE_PREFIX = 'fluent_cart_mcp_context_';
36
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.
40 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'],
46 // 'none' = no shipping required (e.g. digital orders); reported when the
47 // stored value is empty. It is read-only — change-order-status won't set it.
48 'shipping_statuses' => ['none', 'unshipped', 'shipped', 'delivered', 'unshippable'],
49 'order_types' => ['payment', 'renewal', 'subscription'],
50 'subscription_statuses' => ['active', 'trialing', 'paused', 'canceled', 'failing', 'expired', 'expiring', 'past_due', 'intended', 'pending', 'completed'],
51 // installment = fixed-term split-pay plan (a lifetime license paid off in
52 // a finite number of charges, bill_times > 0); recurring = open-ended
53 // subscription (bill_times = 0). Derived from bill_times, never the title.
54 'plan_types' => ['installment', 'recurring'],
55 'billing_intervals' => ['daily', 'weekly', 'monthly', 'quarterly', 'half_yearly', 'yearly'],
56 'fulfillment_types' => ['physical', 'digital'],
57 'coupon_types' => ['fixed', 'percentage'],
58 'order_modes' => ['live', 'test'],
59 ];
60
61 // Payment statuses that count as realized revenue. Centralized so every
62 // tool (context, reports, aggregates) agrees on what "paid" means.
63 const PAID_STATUSES = ['paid', 'partially_paid', 'partially_refunded'];
64
65 /**
66 * Ability definitions for this domain. The registrar merges every tool
67 * class's definitions(), so a tool's schema lives next to its code.
68 */
69 public static function definitions()
70 {
71 return [
72 'fluent-cart/get-store-context' => [
73 'label' => __('Get Store Context', 'fluent-cart'),
74 'description' => __('START HERE — call once per session. Returns who you are and your permissions, the store currency/timezone conventions, headline stats, every valid enum value (order/payment/shipping/subscription statuses, intervals, types), and usage guidelines. Use this before any other tool so you never guess a status string or money format.', 'fluent-cart'),
75 'input_schema' => [
76 'type' => 'object',
77 'properties' => new \stdClass(),
78 ],
79 'execute_callback' => [self::class, 'getContext'],
80 'permission_callback' => function () {
81 return PermissionGate::can('dashboard_stats/view') || PermissionGate::canAny(PermissionGate::readRoleCaps());
82 },
83 'annotations' => ['readonly' => true],
84 ],
85
86 'fluent-cart/list-reference-data' => [
87 'label' => __('List Reference Data', 'fluent-cart'),
88 'description' => __('On-demand lookup lists kept out of get-store-context to keep it lean: coupons, labels, gateways, tax_classes, shipping_zones, product_categories. Pass kinds[] with only what you need. Kinds your role cannot see are reported in meta.warnings, not dropped silently. The coupons kind is a capped snapshot (newest 200, each with times_used) — to filter by status/code, paginate, or find usable-now coupons, use list-coupons instead.', 'fluent-cart'),
89 'input_schema' => [
90 'type' => 'object',
91 'properties' => [
92 'kinds' => [
93 'type' => 'array',
94 'description' => 'Which reference lists to return.',
95 'items' => ['type' => 'string', 'enum' => ['coupons', 'labels', 'gateways', 'tax_classes', 'shipping_zones', 'product_categories']],
96 ],
97 ],
98 'required' => ['kinds'],
99 ],
100 'execute_callback' => [self::class, 'listReferenceData'],
101 'permission_callback' => function () {
102 return PermissionGate::canAny(PermissionGate::readRoleCaps());
103 },
104 'annotations' => ['readonly' => true],
105 ],
106 ];
107 }
108
109 public static function getContext($params = [])
110 {
111 $userId = get_current_user_id();
112 $cacheKey = self::CACHE_PREFIX . $userId;
113
114 $cached = get_transient($cacheKey);
115 if (is_array($cached)) {
116 return $cached;
117 }
118
119 $context = self::buildContext($userId);
120 set_transient($cacheKey, $context, self::CACHE_TTL);
121
122 return $context;
123 }
124
125 private static function buildContext($userId)
126 {
127 $user = get_user_by('ID', $userId);
128 $isAdmin = $user && user_can($user, 'manage_options');
129
130 $you = [
131 'wp_user_id' => (int) $userId,
132 'name' => $user ? $user->display_name : null,
133 'email' => $user ? $user->user_email : null,
134 'is_admin' => (bool) $isAdmin,
135 'permissions' => array_values((array) PermissionManager::getUserPermissions()),
136 ];
137
138 $store = [
139 'name' => get_bloginfo('name'),
140 'url' => site_url(),
141 'version' => defined('FLUENTCART_VERSION') ? FLUENTCART_VERSION : null,
142 'pro_active' => defined('FLUENT_CART_PRO') || defined('FLUENTCART_PRO_VERSION'),
143 'currency' => MCPHelper::currencyContext(),
144 'timezone' => wp_timezone_string(),
145 'current_time' => MCPHelper::toIso8601(DateTime::gmtNow()),
146 ];
147
148 // Headline stats are dashboard data: gate them on dashboard_stats/view so
149 // a narrow read role can still get context (enums, currency, permissions)
150 // without seeing store-wide revenue/order/customer numbers.
151 $canStats = PermissionGate::can('dashboard_stats/view');
152 $stats = $canStats ? self::buildStats() : null;
153
154 return MCPHelper::envelope(
155 $canStats ? self::summary($stats) : __('Store context loaded.', 'fluent-cart'),
156 [
157 'you' => $you,
158 'store' => $store,
159 'stats' => $stats,
160 'enums' => apply_filters('fluent_cart/mcp_enums', self::ENUMS),
161 'reference_kinds' => self::referenceKinds(),
162 'tool_index' => self::toolIndex(),
163 'guidelines' => self::guidelines(),
164 ]
165 );
166 }
167
168 /**
169 * Headline numbers. Each metric is isolated in safeCount/safeSum so one
170 * failing query (e.g. a model that doesn't exist on a given install) yields
171 * null for that stat instead of breaking the whole discovery call.
172 */
173 private static function buildStats()
174 {
175 $since30 = DateTime::gmtNow()->modify('-30 days')->format('Y-m-d H:i:s');
176
177 return [
178 'orders_total' => self::safeCount(function () {
179 return Order::query()->count();
180 }),
181 'orders_last_30d' => self::safeCount(function () use ($since30) {
182 return Order::query()->where('created_at', '>=', $since30)->count();
183 }),
184 'revenue_last_30d' => self::safeMoney(function () use ($since30) {
185 return (int) Order::query()
186 ->whereIn('payment_status', self::PAID_STATUSES)
187 ->where('created_at', '>=', $since30)
188 ->sum('total_paid');
189 }),
190 'customers_total' => self::safeCount(function () {
191 return Customer::query()->count();
192 }),
193 'active_subscriptions' => self::safeCount(function () {
194 return Subscription::query()->where('status', 'active')->count();
195 }),
196 'products_published' => self::safeCount(function () {
197 if (!class_exists('\FluentCart\App\Models\Product')) {
198 return null;
199 }
200 // post_type is pinned by the model's global scope; a literal
201 // here (and the wrong singular one) would match nothing.
202 return \FluentCart\App\Models\Product::query()
203 ->where('post_status', 'publish')
204 ->count();
205 }),
206 ];
207 }
208
209 private static function safeCount(callable $fn)
210 {
211 try {
212 $val = $fn();
213 return $val === null ? null : (int) $val;
214 } catch (\Throwable $e) {
215 return null;
216 }
217 }
218
219 private static function safeMoney(callable $fn)
220 {
221 try {
222 return MCPHelper::money((int) $fn());
223 } catch (\Throwable $e) {
224 return null;
225 }
226 }
227
228 /* translators: %1$s: revenue amount, %2$d: orders in 30 days, %3$d: total customers */
229 private static function summary($stats)
230 {
231 $rev30 = isset($stats['revenue_last_30d']['display']) ? $stats['revenue_last_30d']['display'] : '—';
232 $orders30 = isset($stats['orders_last_30d']) ? (int) $stats['orders_last_30d'] : 0;
233 $customers = isset($stats['customers_total']) ? (int) $stats['customers_total'] : 0;
234
235 return sprintf(
236 /* translators: %1$s: 30-day revenue, %2$d: 30-day order count, %3$d: total customers */
237 __('Store snapshot — last 30 days: %1$s across %2$d orders; %3$d customers total.', 'fluent-cart'),
238 $rev30,
239 $orders30,
240 $customers
241 );
242 }
243
244 /** Tells the agent what `kinds` it can pass to list-reference-data. */
245 private static function referenceKinds()
246 {
247 return ['coupons', 'labels', 'gateways', 'tax_classes', 'shipping_zones', 'product_categories'];
248 }
249
250 /**
251 * Task → tool routing table so an agent picks the right ability among ~30
252 * without trial and error, grouped by intent (discovery / find / load /
253 * analytics / write).
254 *
255 * Derived from the LIVE registry so a newly registered tool can never
256 * silently go missing — each is annotated with a curated "reach for this
257 * when…" hint, and any tool without one still appears under its label.
258 * Filterable so pro / add-on tools can slot themselves in.
259 */
260 private static function toolIndex()
261 {
262 // [category, one-line "use this when…"], keyed by ability name.
263 $hints = [
264 'fluent-cart/get-store-context' => ['discovery', 'Call first — identity, permissions, currency, enums, headline stats, and this index.'],
265 'fluent-cart/list-reference-data' => ['discovery', 'Resolve names to ids: coupons, labels, gateways, tax classes, shipping zones, product categories.'],
266 'fluent-cart/get-search-schema' => ['discovery', 'The advanced_filters reference for one entity — every filterable property, operators, value formats. Call before building an advanced search.'],
267 'fluent-cart/list-orders' => ['find', 'Find orders by status / payment / customer / product / date.'],
268 'fluent-cart/list-customers' => ['find', 'Find customers by name / email / location / LTV.'],
269 'fluent-cart/list-products' => ['find', 'Find products by title / category / price.'],
270 'fluent-cart/list-subscriptions' => ['find', 'Find subscriptions by status / plan / product; summary_only for a fast aggregate.'],
271 'fluent-cart/list-coupons' => ['find', 'Find coupons by status / code, with usage counts.'],
272 'fluent-cart/list-transactions' => ['find', 'The payment ledger across records — refunds last week, failed charges for dunning, one customer\'s payment history.'],
273 'fluent-cart/get-inventory' => ['find', 'Products at or below their stock threshold, or out of stock.'],
274 'fluent-cart/get-order' => ['load', 'One order in full; include[] transactions / refunds / addresses / coupons / subscriptions.'],
275 'fluent-cart/get-order-activity' => ['load', 'The audit timeline for one order.'],
276 'fluent-cart/get-customer' => ['load', 'One customer profile; include[] orders / subscriptions.'],
277 'fluent-cart/get-product' => ['load', 'One product with variations; include[] sales / downloads.'],
278 'fluent-cart/get-subscription' => ['load', 'One subscription; include[] transactions / labels.'],
279 'fluent-cart/get-product-financials' => ['load', 'One product\'s money: one-time + installment + recurring, MRR / ARR, payment schedule.'],
280 'fluent-cart/get-sales-report' => ['analytics', 'The headline revenue number for a period, against the prior period.'],
281 'fluent-cart/get-sales-trend' => ['analytics', 'Revenue / order time series by hour / day / week / month.'],
282 'fluent-cart/get-top-products' => ['analytics', 'Best sellers by revenue or units.'],
283 'fluent-cart/get-refund-report' => ['analytics', 'Refund count, rate and amount for a period.'],
284 'fluent-cart/get-upcoming-payments' => ['analytics', 'Forward renewal cohort and at-risk revenue.'],
285 'fluent-cart/query-orders' => ['analytics', 'Flexible order metrics by dimension — revenue by payment_status / order_type / month.'],
286 'fluent-cart/query-products' => ['analytics', 'Product-line analytics — discount / margin leakage, by product / variation / order_type.'],
287 'fluent-cart/query-customers' => ['analytics', 'Customer analytics by country / state / status / cohort.'],
288 'fluent-cart/query-subscriptions' => ['analytics', 'Subscription analytics — contract vs recurring value, churn basis.'],
289 'fluent-cart/query-sources' => ['analytics', 'UTM attribution — revenue by source / medium / campaign.'],
290 'fluent-cart/change-order-status' => ['write', 'Set an order or shipping status.'],
291 'fluent-cart/add-order-note' => ['write', 'Add an internal note to an order.'],
292 'fluent-cart/refund-order' => ['write', 'Refund via the gateway — call dry_run first.'],
293 'fluent-cart/upsert-customer' => ['write', 'Create or update a customer.'],
294 'fluent-cart/change-subscription-status' => ['write', 'Cancel a subscription — call dry_run first.'],
295 'fluent-cart/manage-coupon' => ['write', 'Create, update or deactivate a coupon.'],
296 'fluent-cart/apply-labels' => ['write', 'Add or remove labels on an order / customer / subscription.'],
297 ];
298
299 // Preserve intent order; empty groups are dropped below.
300 $index = ['discovery' => [], 'find' => [], 'load' => [], 'analytics' => [], 'write' => [], 'other' => []];
301
302 foreach (AbilitiesRegistrar::getDefinitions() as $name => $def) {
303 $category = isset($hints[$name]) ? $hints[$name][0] : 'other';
304 $hint = isset($hints[$name]) ? $hints[$name][1] : (isset($def['label']) ? $def['label'] : $name);
305 $short = strpos($name, 'fluent-cart/') === 0 ? substr($name, strlen('fluent-cart/')) : $name;
306
307 $index[$category][$short] = $hint;
308 }
309
310 $index = array_filter($index, function ($group) {
311 return !empty($group);
312 });
313
314 return apply_filters('fluent_cart/mcp_tool_index', $index);
315 }
316
317 private static function guidelines()
318 {
319 $default = 'Call get-store-context once per session. Consult the tool_index in this payload to pick the right tool for a task, then use list-* and query-* tools to find and aggregate records and get-* tools to load one record fully. '
320 . 'Money is returned as both a number (amount) and a formatted string (display) — quote display, compare amount. '
321 . 'Dates are ISO-8601 UTC; pass a relative range (e.g. last_30_days) or explicit start_date/end_date to report tools. '
322 . 'Use the exact enum values from this payload — never invent a status. '
323 . '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). '
325 . 'Writes (refund-order, change-subscription-status:cancel) require a dry_run preview first.';
326
327 return apply_filters('fluent_cart/mcp_guidelines', $default);
328 }
329
330 /**
331 * `list-reference-data` — heavier lookup lists, fetched on demand.
332 *
333 * @param array $params { kinds: string[] } — which lists to return. Each
334 * kind is gated by its own capability; kinds the
335 * caller can't see are reported in `skipped`, not
336 * silently dropped, so the agent knows why.
337 */
338 public static function listReferenceData($params = [])
339 {
340 $kinds = isset($params['kinds']) ? (array) $params['kinds'] : [];
341 if (!$kinds) {
342 return MCPHelper::error(
343 'missing_kinds',
344 __('Provide one or more kinds. Valid: coupons, labels, gateways, tax_classes, shipping_zones, product_categories.', 'fluent-cart'),
345 ['valid_kinds' => self::referenceKinds()]
346 );
347 }
348
349 $gate = [
350 'coupons' => 'coupons/view',
351 'labels' => 'labels/view',
352 'gateways' => 'dashboard_stats/view',
353 'tax_classes' => 'store/settings',
354 'shipping_zones' => 'store/settings',
355 'product_categories' => 'products/view',
356 ];
357
358 $data = [];
359 $skipped = [];
360
361 foreach ($kinds as $kind) {
362 if (!isset($gate[$kind])) {
363 $skipped[$kind] = 'unknown_kind';
364 continue;
365 }
366 if (!PermissionGate::can($gate[$kind])) {
367 $skipped[$kind] = 'forbidden: requires ' . $gate[$kind];
368 continue;
369 }
370 $data[$kind] = self::fetchReferenceKind($kind);
371 }
372
373 $meta = $skipped ? ['warnings' => self::skipWarnings($skipped)] : [];
374
375 return MCPHelper::envelope(
376 sprintf(
377 /* translators: %d: number of reference lists returned */
378 _n('Returned %d reference list.', 'Returned %d reference lists.', count($data), 'fluent-cart'),
379 count($data)
380 ),
381 $data,
382 $meta
383 );
384 }
385
386 private static function skipWarnings($skipped)
387 {
388 $out = [];
389 foreach ($skipped as $kind => $reason) {
390 $out[] = $kind . ': ' . $reason;
391 }
392 return $out;
393 }
394
395 /**
396 * Each kind is fetched behind a class_exists guard so a model that isn't
397 * present on a given install returns [] rather than fataling.
398 */
399 private static function fetchReferenceKind($kind)
400 {
401 try {
402 if ($kind === 'coupons' && class_exists('\FluentCart\App\Models\Coupon')) {
403 $coupons = \FluentCart\App\Models\Coupon::query()
404 ->select(['id', 'code', 'title', 'type', 'amount', 'status', 'use_count'])
405 ->orderBy('id', 'DESC')
406 ->limit(200)
407 ->get();
408 $out = [];
409 foreach ($coupons as $c) {
410 // Match list-coupons: numeric amount; fixed coupons stored in
411 // cents are reported in store currency, percentage as-is.
412 $amount = ($c->type === 'fixed')
413 ? 0 + \FluentCart\App\Helpers\Helper::toDecimalWithoutComma((int) $c->amount)
414 : (is_numeric($c->amount) ? 0 + $c->amount : $c->amount);
415 $out[] = [
416 'id' => (int) $c->id,
417 'code' => $c->code,
418 'title' => $c->title,
419 'type' => $c->type,
420 'amount' => $amount,
421 'status' => $c->status,
422 // Usage count so "how many times was code X used" is
423 // answerable without a second call. Alias times_used matches
424 // list-coupons.
425 'use_count' => (int) $c->use_count,
426 'times_used' => (int) $c->use_count,
427 ];
428 }
429 return $out;
430 }
431
432 if ($kind === 'labels' && class_exists('\FluentCart\App\Models\Label')) {
433 // fct_label stores a single (maybe-serialized) `value` column —
434 // it may hold a plain title string or an array {title,color,…}.
435 // Labels are user-created and can grow large; cap like coupons
436 // so kinds[]=labels can't trigger an unbounded read/response.
437 $labels = \FluentCart\App\Models\Label::query()->orderBy('id', 'ASC')->limit(200)->get();
438 $out = [];
439 foreach ($labels as $label) {
440 $val = $label->value;
441 $entry = ['id' => (int) $label->id];
442 if (is_array($val)) {
443 $entry['title'] = isset($val['title']) ? $val['title'] : (isset($val['value']) ? $val['value'] : null);
444 if (isset($val['color'])) {
445 $entry['color'] = $val['color'];
446 }
447 } else {
448 $entry['title'] = $val;
449 }
450 $out[] = $entry;
451 }
452 return $out;
453 }
454
455 if ($kind === 'tax_classes' && class_exists('\FluentCart\App\Models\TaxClass')) {
456 // fct_tax_classes labels its name column `title`, not `name`.
457 return \FluentCart\App\Models\TaxClass::query()
458 ->select(['id', 'title'])
459 ->get()
460 ->toArray();
461 }
462
463 if ($kind === 'shipping_zones' && class_exists('\FluentCart\App\Models\ShippingZone')) {
464 // fct_shipping_zones labels its name column `name`, not `title`.
465 return \FluentCart\App\Models\ShippingZone::query()
466 ->select(['id', 'name', 'region'])
467 ->get()
468 ->toArray();
469 }
470
471 if ($kind === 'gateways') {
472 return self::enabledGateways();
473 }
474
475 if ($kind === 'product_categories') {
476 return self::productCategories();
477 }
478 } catch (\Throwable $e) {
479 return [];
480 }
481
482 return [];
483 }
484
485 /**
486 * Active payment gateways. Each gateway stores its own settings (there is no
487 * single payment_settings option), so we read the registered gateway
488 * instances from the GatewayManager and keep the ones with is_active=yes.
489 */
490 private static function enabledGateways()
491 {
492 $managerClass = '\FluentCart\App\Modules\PaymentMethods\Core\GatewayManager';
493 if (!class_exists($managerClass) || !method_exists($managerClass, 'getInstance')) {
494 return [];
495 }
496
497 try {
498 $gateways = $managerClass::getInstance()->all();
499 } catch (\Throwable $e) {
500 return [];
501 }
502
503 $out = [];
504 foreach ((array) $gateways as $gateway) {
505 if (!is_object($gateway) || !method_exists($gateway, 'getMeta')) {
506 continue;
507 }
508
509 $settings = (isset($gateway->settings) && is_object($gateway->settings) && method_exists($gateway->settings, 'get'))
510 ? (array) $gateway->settings->get()
511 : [];
512
513 $isActive = isset($settings['is_active'])
514 ? ($settings['is_active'] === 'yes')
515 : !empty($gateway->getMeta('status'));
516 if (!$isActive) {
517 continue;
518 }
519
520 $meta = (array) $gateway->getMeta();
521 $route = isset($meta['route']) ? $meta['route'] : null;
522 $out[] = [
523 'key' => $route,
524 'title' => isset($meta['title']) ? $meta['title'] : $route,
525 'mode' => isset($settings['payment_mode'])
526 ? $settings['payment_mode']
527 : (isset($settings['checkout_mode']) ? $settings['checkout_mode'] : null),
528 ];
529 }
530 return $out;
531 }
532
533 /** Product categories from the WP taxonomy (best-effort across naming). */
534 private static function productCategories()
535 {
536 foreach (['fluent-cart-category', 'product_cat', 'fluent_cart_category'] as $taxonomy) {
537 if (!taxonomy_exists($taxonomy)) {
538 continue;
539 }
540 $terms = get_terms(['taxonomy' => $taxonomy, 'hide_empty' => false, 'number' => 200]);
541 if (is_wp_error($terms)) {
542 continue;
543 }
544 $out = [];
545 foreach ($terms as $term) {
546 $out[] = ['id' => (int) $term->term_id, 'name' => $term->name, 'slug' => $term->slug, 'count' => (int) $term->count];
547 }
548 return $out;
549 }
550 return [];
551 }
552
553 /**
554 * Clear the cached context for all users. Hooked from MCPInit onto the
555 * events that change anything the context payload reports.
556 */
557 public static function invalidateCache()
558 {
559 global $wpdb;
560
561 $like = $wpdb->esc_like('_transient_' . self::CACHE_PREFIX) . '%';
562 $wpdb->query($wpdb->prepare("DELETE FROM {$wpdb->options} WHERE option_name LIKE %s", $like));
563
564 $like = $wpdb->esc_like('_transient_timeout_' . self::CACHE_PREFIX) . '%';
565 $wpdb->query($wpdb->prepare("DELETE FROM {$wpdb->options} WHERE option_name LIKE %s", $like));
566 }
567 }
568