PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.4.1
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.4.1
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 / LabelTools.php

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

196 lines 8.2 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\Modules\MCP\Support\MCPHelper;
6 use FluentCart\App\Modules\MCP\Support\PermissionGate;
7
8 /**
9 * Label tools — the cross-entity tagging system.
10 *
11 * apply-labels attaches/detaches existing labels to an order, customer, or
12 * subscription. It works on label IDs (from list-reference-data with
13 * kind=labels), so it never needs to know how a label's value is stored.
14 *
15 * Parameter design: explicit add/remove delta arrays rather than a "set the
16 * full list" payload — the agent rarely knows the current set, and deltas are
17 * what it actually intends ("tag this order VIP"). We return the resulting set
18 * so the agent can confirm.
19 *
20 * Managing label DEFINITIONS (create/rename/delete the labels themselves) is
21 * deliberately not exposed yet — the underlying value column is a serialized
22 * blob whose shape we don't want an agent guessing at.
23 */
24 class LabelTools
25 {
26 const TYPE_MAP = ['order' => 'Order', 'customer' => 'Customer', 'subscription' => 'Subscription'];
27
28 public static function definitions()
29 {
30 return [
31 'fluent-cart/apply-labels' => [
32 'label' => __('Apply Labels', 'fluent-cart'),
33 'description' => __('Add or remove labels on an order, customer, or subscription. Pass add_label_ids and/or remove_label_ids; use list-reference-data with kind=labels to find label ids. Returns the resulting label set so you can confirm.', 'fluent-cart'),
34 'input_schema' => [
35 'type' => 'object',
36 'properties' => [
37 'entity_type' => ['type' => 'string', 'enum' => ['order', 'customer', 'subscription']],
38 'entity_id' => ['type' => 'integer'],
39 'add_label_ids' => ['type' => 'array', 'items' => ['type' => 'integer']],
40 'remove_label_ids' => ['type' => 'array', 'items' => ['type' => 'integer']],
41 ],
42 'required' => ['entity_type', 'entity_id'],
43 ],
44 'execute_callback' => [self::class, 'applyLabels'],
45 'permission_callback' => function () {
46 return PermissionGate::can('labels/manage');
47 },
48 'annotations' => ['bulk' => true],
49 ],
50 ];
51 }
52
53 public static function applyLabels($params = [])
54 {
55 $type = isset($params['entity_type']) ? sanitize_text_field($params['entity_type']) : '';
56 if (!isset(self::TYPE_MAP[$type])) {
57 return MCPHelper::error('invalid_entity_type', __('entity_type must be one of: order, customer, subscription.', 'fluent-cart'));
58 }
59
60 $entityId = isset($params['entity_id']) ? (int) $params['entity_id'] : 0;
61 if (!$entityId) {
62 return MCPHelper::error('missing_identifier', __('entity_id is required.', 'fluent-cart'));
63 }
64
65 $modelClass = 'FluentCart\\App\\Models\\' . self::TYPE_MAP[$type];
66 if (!class_exists($modelClass) || !$modelClass::query()->where('id', $entityId)->exists()) {
67 return MCPHelper::error('entity_not_found', __('No matching record found for entity_type and entity_id.', 'fluent-cart'));
68 }
69
70 $rel = 'FluentCart\\App\\Models\\LabelRelationship';
71 if (!class_exists($rel)) {
72 return MCPHelper::error('not_available', __('Labels are not available on this install.', 'fluent-cart'));
73 }
74
75 $add = array_values(array_unique(array_map('intval', (array) (isset($params['add_label_ids']) ? $params['add_label_ids'] : []))));
76 $remove = array_values(array_unique(array_map('intval', (array) (isset($params['remove_label_ids']) ? $params['remove_label_ids'] : []))));
77
78 if (!$add && !$remove) {
79 return MCPHelper::error('missing_param', __('Provide add_label_ids and/or remove_label_ids.', 'fluent-cart'), ['fields' => ['add_label_ids', 'remove_label_ids']]);
80 }
81
82 // The same id in both lists is a contradictory instruction (add then
83 // remove nets to nothing yet reports as both added and removed). Reject
84 // so the agent clarifies intent.
85 $overlap = array_values(array_intersect($add, $remove));
86 if ($overlap) {
87 return MCPHelper::error(
88 'conflicting_labels',
89 __('A label id cannot be in both add_label_ids and remove_label_ids.', 'fluent-cart'),
90 ['fields' => ['add_label_ids', 'remove_label_ids'], 'conflicting_label_ids' => $overlap]
91 );
92 }
93
94 // Reject label IDs to ADD that don't exist, so entities can't be tagged
95 // with phantom labels that render with a null title. (Removal of an
96 // unknown id is harmless — it just no-ops.)
97 if ($add && class_exists('\FluentCart\App\Models\Label')) {
98 $known = array_map('intval', (array) \FluentCart\App\Models\Label::query()->whereIn('id', $add)->pluck('id')->toArray());
99 $unknown = array_values(array_diff($add, $known));
100 if ($unknown) {
101 return MCPHelper::error(
102 'label_not_found',
103 __('One or more label IDs do not exist. Use list-reference-data with kind=labels to find valid ids.', 'fluent-cart'),
104 ['fields' => ['add_label_ids'], 'unknown_label_ids' => $unknown]
105 );
106 }
107 }
108
109 $added = [];
110 foreach ($add as $labelId) {
111 $exists = $rel::query()
112 ->where('label_id', $labelId)
113 ->where('labelable_id', $entityId)
114 ->where('labelable_type', $modelClass)
115 ->exists();
116 if (!$exists) {
117 $rel::query()->create([
118 'label_id' => $labelId,
119 'labelable_id' => $entityId,
120 'labelable_type' => $modelClass,
121 ]);
122 $added[] = $labelId;
123 }
124 }
125
126 $removed = [];
127 foreach ($remove as $labelId) {
128 $deleted = $rel::query()
129 ->where('label_id', $labelId)
130 ->where('labelable_id', $entityId)
131 ->where('labelable_type', $modelClass)
132 ->delete();
133 if ($deleted) {
134 $removed[] = $labelId;
135 }
136 }
137
138 $current = array_map('intval', (array) $rel::query()
139 ->where('labelable_id', $entityId)
140 ->where('labelable_type', $modelClass)
141 ->pluck('label_id')
142 ->toArray());
143
144 return MCPHelper::envelope(
145 sprintf(
146 /* translators: 1: labels added, 2: labels removed */
147 __('Labels updated: %1$d added, %2$d removed.', 'fluent-cart'),
148 count($added),
149 count($removed)
150 ),
151 [
152 'entity_type' => $type,
153 'entity_id' => $entityId,
154 'added' => $added,
155 'removed' => $removed,
156 'current_label_ids' => $current,
157 'current_labels' => self::resolveLabels($current),
158 ]
159 );
160 }
161
162 /**
163 * Resolve label IDs to {id, title} so the caller doesn't need a follow-up
164 * reference-data lookup. fct_label keeps its name in a (maybe-serialized)
165 * `value` column — a plain string or an array {title|value, color}.
166 */
167 private static function resolveLabels(array $ids)
168 {
169 if (!$ids) {
170 return [];
171 }
172
173 $labelClass = 'FluentCart\\App\\Models\\Label';
174 if (!class_exists($labelClass)) {
175 return array_map(function ($id) {
176 return ['id' => $id, 'title' => null];
177 }, $ids);
178 }
179
180 $titles = [];
181 foreach ($labelClass::query()->whereIn('id', $ids)->get() as $label) {
182 $val = $label->value;
183 $title = is_array($val)
184 ? (isset($val['title']) ? $val['title'] : (isset($val['value']) ? $val['value'] : null))
185 : $val;
186 $titles[(int) $label->id] = $title;
187 }
188
189 $out = [];
190 foreach ($ids as $id) {
191 $out[] = ['id' => $id, 'title' => isset($titles[$id]) ? $titles[$id] : null];
192 }
193 return $out;
194 }
195 }
196