| @@ -3,8 +3,9 @@ | ||
| 3 | 3 | namespace FluentCart\App\Modules\MCP\Support; |
| 4 | 4 | |
| 5 | 5 | use FluentCart\Api\ModuleSettings; |
| 6 | 6 | use FluentCart\App\App; |
| 7 | +use FluentCart\App\Modules\MCP\Tools\ContextTools; | |
| 7 | 8 | use FluentCart\App\Modules\Subscriptions\Services\Filter\SubscriptionFilter; |
| 8 | 9 | use FluentCart\App\Services\Filter\CustomerFilter; |
| 9 | 10 | use FluentCart\App\Services\Filter\LicenseFilter; |
| 10 | 11 | use FluentCart\App\Services\Filter\OrderFilter; |
| @@ -133,9 +134,9 @@ | ||
| 133 | 134 | return self::unknownEntityError($entity); |
| 134 | 135 | } |
| 135 | 136 | |
| 136 | 137 | $filterClass = Arr::get($spec, 'filter'); |
| 137 | - $catalog = self::catalog($filterClass); | |
| 138 | + $catalog = self::catalog($filterClass, $entity); | |
| 138 | 139 | if (!$catalog) { |
| 139 | 140 | return MCPHelper::error( |
| 140 | 141 | 'no_advanced_options', |
| 141 | 142 | sprintf( |
| @@ -278,13 +279,36 @@ | ||
| 278 | 279 | // Catalog — live property map per entity |
| 279 | 280 | // ----------------------------------------------------------------- |
| 280 | 281 | |
| 281 | 282 | /** |
| 283 | + * Status properties whose admin dropdown is a curated subset of the values | |
| 284 | + * the column can actually hold, mapped to the canonical enum that completes | |
| 285 | + * them. The admin UI ships the short list on purpose (those are the statuses | |
| 286 | + * a merchant filters by day to day); an agent needs the full set, because | |
| 287 | + * "find the failed orders" is a normal question and the engine executes | |
| 288 | + * WHERE status IN (...) against the raw column either way. | |
| 289 | + * | |
| 290 | + * MCP-side only — the shared fluent_cart/{name}_filter_options hook is left | |
| 291 | + * untouched so the admin dropdowns keep their curated lists. | |
| 292 | + */ | |
| 293 | + const CANONICAL_OPTIONS = [ | |
| 294 | + 'orders.order.status' => 'order_statuses', | |
| 295 | + 'orders.order.payment_status' => 'payment_statuses', | |
| 296 | + 'orders.order.type' => 'order_types', | |
| 297 | + 'subscriptions.subscription.status' => 'subscription_statuses', | |
| 298 | + 'subscriptions.subscription.billing_interval' => 'billing_intervals', | |
| 299 | + ]; | |
| 300 | + | |
| 301 | + /** | |
| 282 | 302 | * provider.property => { provider, property, def } for one filter class, |
| 283 | 303 | * through the same fluent_cart/{name}_filter_options hook the engine |
| 284 | 304 | * applies, so Pro/add-on providers appear here exactly as they execute. |
| 305 | + * | |
| 306 | + * $entity is used only to complete curated status option lists from the | |
| 307 | + * canonical enums (see CANONICAL_OPTIONS); pass it so the schema the agent | |
| 308 | + * reads and the values normalize() accepts stay the same list. | |
| 285 | 309 | */ |
| 286 | - private static function catalog($filterClass): array | |
| 310 | + private static function catalog($filterClass, $entity = ''): array | |
| 287 | 311 | { |
| 288 | 312 | if (!$filterClass || !class_exists($filterClass) || !is_callable([$filterClass, 'advanceFilterOptions'])) { |
| 289 | 313 | return []; |
| 290 | 314 | } |
| @@ -316,9 +340,9 @@ | ||
| 316 | 340 | } |
| 317 | 341 | $map[$key] = [ |
| 318 | 342 | 'provider' => (string) $providerKey, |
| 319 | 343 | 'property' => $property, |
| 320 | - 'def' => $child, | |
| 344 | + 'def' => self::completeOptions($entity, $key, $child), | |
| 321 | 345 | ]; |
| 322 | 346 | } |
| 323 | 347 | } |
| 324 | 348 | |
| @@ -324,8 +348,40 @@ | ||
| 324 | 348 | |
| 325 | 349 | return $map; |
| 326 | 350 | } |
| 327 | 351 | |
| 352 | + /** | |
| 353 | + * Union a curated status dropdown with its canonical enum, preserving the | |
| 354 | + * catalog's labels for the values it already had. Returns the definition | |
| 355 | + * unchanged for every property without a canonical counterpart. | |
| 356 | + */ | |
| 357 | + private static function completeOptions($entity, $key, array $def): array | |
| 358 | + { | |
| 359 | + $enumKey = Arr::get(self::CANONICAL_OPTIONS, $entity . '.' . $key); | |
| 360 | + if (!$enumKey) { | |
| 361 | + return $def; | |
| 362 | + } | |
| 363 | + // Only selection widgets carry an options map; leave anything else alone. | |
| 364 | + if (Arr::get($def, 'type') !== 'selections') { | |
| 365 | + return $def; | |
| 366 | + } | |
| 367 | + | |
| 368 | + $enums = ContextTools::enums(); | |
| 369 | + if (empty($enums[$enumKey])) { | |
| 370 | + return $def; | |
| 371 | + } | |
| 372 | + | |
| 373 | + $options = (array) Arr::get($def, 'options', []); | |
| 374 | + foreach ($enums[$enumKey] as $value) { | |
| 375 | + if (!array_key_exists($value, $options)) { | |
| 376 | + $options[$value] = $value; | |
| 377 | + } | |
| 378 | + } | |
| 379 | + $def['options'] = $options; | |
| 380 | + | |
| 381 | + return $def; | |
| 382 | + } | |
| 383 | + | |
| 328 | 384 | /** Money columns for the entity, so values can be documented/validated as store-currency decimals. */ |
| 329 | 385 | private static function centColumnsFor($filterClass): array |
| 330 | 386 | { |
| 331 | 387 | try { |
| @@ -442,9 +498,9 @@ | ||
| 442 | 498 | if (count($raw) > self::MAX_GROUPS) { |
| 443 | 499 | return self::limitError($entity); |
| 444 | 500 | } |
| 445 | 501 | |
| 446 | - $catalog = self::catalog($filterClass); | |
| 502 | + $catalog = self::catalog($filterClass, $entity); | |
| 447 | 503 | $centColumns = self::centColumnsFor($filterClass); |
| 448 | 504 | |
| 449 | 505 | $groups = []; |
| 450 | 506 | $warnings = []; |