PluginProbe ʕ •ᴥ•ʔ
CommerceBird – AI Command Center, ERP Integrations & B2B for WooCommerce (Zoho, Exact Online). / 3.0.1
CommerceBird – AI Command Center, ERP Integrations & B2B for WooCommerce (Zoho, Exact Online). v3.0.1
3.0.3 3.0.2 3.0.1 trunk 2.2.14 2.2.15 2.2.16 2.2.17 2.2.18 2.2.19 2.3.0 2.3.1 2.3.10 2.3.11 2.3.12 2.3.13 2.3.14 2.3.2 2.3.3 2.3.4 2.3.5 2.3.6 2.3.7 2.3.8 2.3.9 2.4.0 2.4.1 2.4.2 2.4.3 2.4.4 2.4.5 2.4.6 2.5.0 2.5.1 2.5.2 2.6.0 2.6.1 2.6.2 2.6.3 2.6.4 2.6.5 2.7.0 2.7.1 2.7.2 2.7.3 2.7.4 2.7.5 2.7.6 2.7.7 2.7.8 2.7.9 2.7.91 2.7.92 2.7.93 2.8.0 2.8.1 2.8.2 2.8.3 2.8.4 2.8.5 2.9.0 2.9.1 2.9.2 2.9.3 3.0.0
commercebird / includes / classes / class-mcp.php
commercebird / includes / classes Last commit date
ai 1 month ago apis 1 month ago purchase-orders 2 months ago quotes 2 months ago zoho-crm 7 months ago zoho-inventory 1 month ago class-api-handler-zoho.php 2 months ago class-auth-zoho.php 4 months ago class-common.php 2 months ago class-mcp.php 2 months ago class-plugin.php 1 month ago class-wc-api.php 5 months ago index.php 1 year ago
class-mcp.php
584 lines
1 <?php
2 /**
3 * CommerceBird MCP Abilities
4 *
5 * Registers WooCommerce MCP abilities for purchase orders and quotes via the
6 * WordPress Abilities API (wp_register_ability / wp_abilities_api_init action).
7 *
8 * Abilities registered:
9 * - commercebird/create-purchase-order
10 * - commercebird/update-purchase-order
11 * - commercebird/delete-purchase-order
12 * - commercebird/create-quote
13 * - commercebird/update-quote
14 * - commercebird/delete-quote
15 *
16 * @package CommerceBird\Includes
17 */
18 namespace CommerceBird;
19
20 defined( 'ABSPATH' ) || exit;
21
22 class CMBIRD_MCP {
23
24 public static function init(): void {
25 add_action( 'wp_abilities_api_init', array( __CLASS__, 'register_abilities' ) );
26 add_filter( 'woocommerce_mcp_include_ability', array( __CLASS__, 'include_ability_in_mcp' ), 10, 2 );
27 }
28
29 public static function include_ability_in_mcp( bool $include, string $ability_id ): bool {
30 if ( str_starts_with( $ability_id, 'commercebird/' ) ) {
31 return true;
32 }
33 return $include;
34 }
35
36 public static function register_abilities(): void {
37 if ( ! function_exists( 'wp_register_ability' ) ) {
38 return;
39 }
40 wp_register_ability(
41 'commercebird/create-purchase-order',
42 array(
43 'label' => __( 'Create Purchase Order', 'commercebird' ),
44 'category' => 'site',
45 'description' => __( 'Creates a new purchase order (type: shop_purchase) with optional line items, status, and a note.', 'commercebird' ),
46 'input_schema' => array(
47 'type' => 'object',
48 'properties' => array(
49 'status' => array(
50 'type' => 'string',
51 'description' => 'Initial PO status (without wc- prefix).',
52 'enum' => array( 'awaiting-approval', 'approved', 'part-received', 'received' ),
53 'default' => 'awaiting-approval',
54 ),
55 'line_items' => array(
56 'type' => 'array',
57 'description' => 'Products to add to the purchase order.',
58 'items' => array(
59 'type' => 'object',
60 'properties' => array(
61 'product_id' => array(
62 'type' => 'integer',
63 'description' => 'WooCommerce product ID.',
64 ),
65 'quantity' => array(
66 'type' => 'integer',
67 'description' => 'Quantity to order.',
68 'minimum' => 1,
69 ),
70 'price' => array(
71 'type' => 'number',
72 'description' => 'Unit cost price override. Defaults to the product price.',
73 ),
74 ),
75 'required' => array( 'product_id', 'quantity' ),
76 ),
77 ),
78 'note' => array(
79 'type' => 'string',
80 'description' => 'Internal order note visible to admin.',
81 ),
82 ),
83 ),
84 'output_schema' => array(
85 'type' => 'object',
86 'properties' => array(
87 'id' => array(
88 'type' => 'integer',
89 'description' => 'New purchase order ID.',
90 ),
91 'status' => array( 'type' => 'string' ),
92 'total' => array(
93 'type' => 'string',
94 'description' => 'Order total as a decimal string.',
95 ),
96 ),
97 'required' => array( 'id', 'status', 'total' ),
98 ),
99 'execute_callback' => array( __CLASS__, 'execute_create_purchase_order' ),
100 'permission_callback' => array( __CLASS__, 'check_permission' ),
101 'meta' => array(
102 'mcp' => array( 'public' => true ),
103 'show_in_rest' => true,
104 ),
105 )
106 );
107 wp_register_ability(
108 'commercebird/update-purchase-order',
109 array(
110 'label' => __( 'Update Purchase Order', 'commercebird' ),
111 'category' => 'site',
112 'description' => __( 'Updates the status, line items, or notes of an existing purchase order. Providing line_items replaces all existing items.', 'commercebird' ),
113 'input_schema' => array(
114 'type' => 'object',
115 'properties' => array(
116 'id' => array(
117 'type' => 'integer',
118 'description' => 'ID of the purchase order to update.',
119 ),
120 'status' => array(
121 'type' => 'string',
122 'description' => 'New PO status (without wc- prefix).',
123 'enum' => array( 'awaiting-approval', 'approved', 'part-received', 'received', 'cancelled' ),
124 ),
125 'line_items' => array(
126 'type' => 'array',
127 'description' => 'When provided, replaces all existing line items.',
128 'items' => array(
129 'type' => 'object',
130 'properties' => array(
131 'product_id' => array( 'type' => 'integer' ),
132 'quantity' => array(
133 'type' => 'integer',
134 'minimum' => 1,
135 ),
136 'price' => array( 'type' => 'number' ),
137 ),
138 'required' => array( 'product_id', 'quantity' ),
139 ),
140 ),
141 'note' => array(
142 'type' => 'string',
143 'description' => 'Internal order note to append.',
144 ),
145 ),
146 'required' => array( 'id' ),
147 ),
148 'output_schema' => array(
149 'type' => 'object',
150 'properties' => array(
151 'id' => array( 'type' => 'integer' ),
152 'status' => array( 'type' => 'string' ),
153 'total' => array( 'type' => 'string' ),
154 ),
155 'required' => array( 'id', 'status', 'total' ),
156 ),
157 'execute_callback' => array( __CLASS__, 'execute_update_purchase_order' ),
158 'permission_callback' => array( __CLASS__, 'check_permission' ),
159 'meta' => array( 'mcp' => array( 'public' => true ) ),
160 )
161 );
162 wp_register_ability(
163 'commercebird/delete-purchase-order',
164 array(
165 'label' => __( 'Delete Purchase Order', 'commercebird' ),
166 'category' => 'site',
167 'description' => __( 'Moves a purchase order to trash.', 'commercebird' ),
168 'input_schema' => array(
169 'type' => 'object',
170 'properties' => array(
171 'id' => array(
172 'type' => 'integer',
173 'description' => 'ID of the purchase order to delete.',
174 ),
175 ),
176 'required' => array( 'id' ),
177 ),
178 'output_schema' => array(
179 'type' => 'object',
180 'properties' => array(
181 'deleted' => array( 'type' => 'boolean' ),
182 'id' => array( 'type' => 'integer' ),
183 ),
184 'required' => array( 'deleted', 'id' ),
185 ),
186 'execute_callback' => array( __CLASS__, 'execute_delete_purchase_order' ),
187 'permission_callback' => array( __CLASS__, 'check_permission' ),
188 'meta' => array( 'mcp' => array( 'public' => true ) ),
189 )
190 );
191 wp_register_ability(
192 'commercebird/create-quote',
193 array(
194 'label' => __( 'Create Quote', 'commercebird' ),
195 'category' => 'site',
196 'description' => __( 'Creates a new WooCommerce order with status on-quote. Advance it to quoted once pricing is finalised.', 'commercebird' ),
197 'input_schema' => array(
198 'type' => 'object',
199 'properties' => array(
200 'customer_id' => array(
201 'type' => 'integer',
202 'description' => 'Existing WooCommerce customer user ID. Use 0 for guest.',
203 'default' => 0,
204 ),
205 'line_items' => array(
206 'type' => 'array',
207 'description' => 'Products to include in the quote.',
208 'items' => array(
209 'type' => 'object',
210 'properties' => array(
211 'product_id' => array(
212 'type' => 'integer',
213 'description' => 'WooCommerce product ID.',
214 ),
215 'quantity' => array(
216 'type' => 'integer',
217 'minimum' => 1,
218 ),
219 'price' => array(
220 'type' => 'number',
221 'description' => 'Unit price override.',
222 ),
223 ),
224 'required' => array( 'product_id', 'quantity' ),
225 ),
226 ),
227 'note' => array(
228 'type' => 'string',
229 'description' => 'Internal note about this quote.',
230 ),
231 ),
232 ),
233 'output_schema' => array(
234 'type' => 'object',
235 'properties' => array(
236 'id' => array(
237 'type' => 'integer',
238 'description' => 'New quote order ID.',
239 ),
240 'status' => array( 'type' => 'string' ),
241 'total' => array( 'type' => 'string' ),
242 'validity' => array(
243 'type' => 'string',
244 'description' => 'Quote validity date (Y-m-d H:i:s), empty if not yet set.',
245 ),
246 ),
247 'required' => array( 'id', 'status', 'total' ),
248 ),
249 'execute_callback' => array( __CLASS__, 'execute_create_quote' ),
250 'permission_callback' => array( __CLASS__, 'check_permission' ),
251 'meta' => array( 'mcp' => array( 'public' => true ) ),
252 )
253 );
254 wp_register_ability(
255 'commercebird/update-quote',
256 array(
257 'label' => __( 'Update Quote', 'commercebird' ),
258 'category' => 'site',
259 'description' => __( 'Updates status, line items, validity, or notes on a quote order (on-quote or quoted). Set status to quoted to formally send pricing to the customer.', 'commercebird' ),
260 'input_schema' => array(
261 'type' => 'object',
262 'properties' => array(
263 'id' => array(
264 'type' => 'integer',
265 'description' => 'ID of the quote order to update.',
266 ),
267 'status' => array(
268 'type' => 'string',
269 'description' => 'New quote status (without wc- prefix).',
270 'enum' => array( 'on-quote', 'quoted', 'cancelled' ),
271 ),
272 'line_items' => array(
273 'type' => 'array',
274 'description' => 'When provided, replaces all existing line items.',
275 'items' => array(
276 'type' => 'object',
277 'properties' => array(
278 'product_id' => array( 'type' => 'integer' ),
279 'quantity' => array(
280 'type' => 'integer',
281 'minimum' => 1,
282 ),
283 'price' => array( 'type' => 'number' ),
284 ),
285 'required' => array( 'product_id', 'quantity' ),
286 ),
287 ),
288 'validity_days' => array(
289 'type' => 'integer',
290 'description' => 'Number of days from today the quote remains valid. Overwrites the existing validity date.',
291 'minimum' => 1,
292 ),
293 'note' => array(
294 'type' => 'string',
295 'description' => 'Internal note to append.',
296 ),
297 ),
298 'required' => array( 'id' ),
299 ),
300 'output_schema' => array(
301 'type' => 'object',
302 'properties' => array(
303 'id' => array( 'type' => 'integer' ),
304 'status' => array( 'type' => 'string' ),
305 'total' => array( 'type' => 'string' ),
306 'validity' => array( 'type' => 'string' ),
307 ),
308 'required' => array( 'id', 'status', 'total' ),
309 ),
310 'execute_callback' => array( __CLASS__, 'execute_update_quote' ),
311 'permission_callback' => array( __CLASS__, 'check_permission' ),
312 'meta' => array( 'mcp' => array( 'public' => true ) ),
313 )
314 );
315 wp_register_ability(
316 'commercebird/delete-quote',
317 array(
318 'label' => __( 'Delete Quote', 'commercebird' ),
319 'category' => 'site',
320 'description' => __( 'Moves a quote order (on-quote or quoted) to trash.', 'commercebird' ),
321 'input_schema' => array(
322 'type' => 'object',
323 'properties' => array(
324 'id' => array(
325 'type' => 'integer',
326 'description' => 'ID of the quote order to delete.',
327 ),
328 ),
329 'required' => array( 'id' ),
330 ),
331 'output_schema' => array(
332 'type' => 'object',
333 'properties' => array(
334 'deleted' => array( 'type' => 'boolean' ),
335 'id' => array( 'type' => 'integer' ),
336 ),
337 'required' => array( 'deleted', 'id' ),
338 ),
339 'execute_callback' => array( __CLASS__, 'execute_delete_quote' ),
340 'permission_callback' => array( __CLASS__, 'check_permission' ),
341 'meta' => array( 'mcp' => array( 'public' => true ) ),
342 )
343 );
344 }
345
346 public static function check_permission(): bool {
347 return current_user_can( 'manage_woocommerce' );
348 }
349
350 public static function execute_create_purchase_order( array $input ): array|\WP_Error {
351 $allowed_statuses = array( 'awaiting-approval', 'approved', 'part-received', 'received' );
352 $status = $input['status'] ?? 'awaiting-approval';
353 if ( ! in_array( $status, $allowed_statuses, true ) ) {
354 return new \WP_Error( 'invalid_status', __( 'Invalid purchase order status.', 'commercebird' ) );
355 }
356
357 $order = new \CMBIRD_Purchase_Order();
358 $order->set_status( 'wc-' . $status );
359
360 foreach ( $input['line_items'] ?? array() as $item ) {
361 $product = wc_get_product( (int) $item['product_id'] );
362 if ( ! $product ) {
363 continue;
364 }
365 $unit_price = isset( $item['price'] ) ? (float) $item['price'] : (float) $product->get_price();
366 $qty = max( 1, (int) $item['quantity'] );
367 $order->add_product(
368 $product,
369 $qty,
370 array(
371 'subtotal' => $unit_price * $qty,
372 'total' => $unit_price * $qty,
373 )
374 );
375 }
376
377 $order->calculate_totals();
378 $order->save();
379
380 if ( ! $order->get_id() ) {
381 return new \WP_Error( 'creation_failed', __( 'Failed to create purchase order.', 'commercebird' ) );
382 }
383
384 if ( ! empty( $input['note'] ) ) {
385 $order->add_order_note( sanitize_textarea_field( $input['note'] ) );
386 }
387
388 return array(
389 'id' => $order->get_id(),
390 'status' => $order->get_status(),
391 'total' => (string) $order->get_total(),
392 );
393 }
394
395 public static function execute_update_purchase_order( array $input ): array|\WP_Error {
396 $allowed_statuses = array( 'awaiting-approval', 'approved', 'part-received', 'received', 'cancelled' );
397
398 if ( ! empty( $input['status'] ) && ! in_array( $input['status'], $allowed_statuses, true ) ) {
399 return new \WP_Error( 'invalid_status', __( 'Invalid purchase order status.', 'commercebird' ) );
400 }
401
402 $order = wc_get_order( (int) $input['id'] );
403
404 if ( ! $order || 'shop_purchase' !== $order->get_type() ) {
405 return new \WP_Error( 'not_found', __( 'Purchase order not found.', 'commercebird' ) );
406 }
407
408 if ( ! empty( $input['status'] ) ) {
409 $order->set_status( 'wc-' . $input['status'] );
410 }
411
412 if ( isset( $input['line_items'] ) ) {
413 $order->remove_order_items( 'line_item' );
414 foreach ( $input['line_items'] as $item ) {
415 $product = wc_get_product( (int) $item['product_id'] );
416 if ( ! $product ) {
417 continue;
418 }
419 $unit_price = isset( $item['price'] ) ? (float) $item['price'] : (float) $product->get_price();
420 $qty = max( 1, (int) $item['quantity'] );
421 $order->add_product(
422 $product,
423 $qty,
424 array(
425 'subtotal' => $unit_price * $qty,
426 'total' => $unit_price * $qty,
427 )
428 );
429 }
430 $order->calculate_totals();
431 }
432
433 $order->save();
434
435 if ( ! empty( $input['note'] ) ) {
436 $order->add_order_note( sanitize_textarea_field( $input['note'] ) );
437 }
438
439 return array(
440 'id' => $order->get_id(),
441 'status' => $order->get_status(),
442 'total' => (string) $order->get_total(),
443 );
444 }
445
446 public static function execute_delete_purchase_order( array $input ): array|\WP_Error {
447 $order = wc_get_order( (int) $input['id'] );
448
449 if ( ! $order || 'shop_purchase' !== $order->get_type() ) {
450 return new \WP_Error( 'not_found', __( 'Purchase order not found.', 'commercebird' ) );
451 }
452
453 $id = $order->get_id();
454 $order->delete();
455
456 return array(
457 'deleted' => true,
458 'id' => $id,
459 );
460 }
461
462 public static function execute_update_quote( array $input ): array|\WP_Error {
463 $allowed_statuses = array( 'on-quote', 'quoted', 'cancelled' );
464
465 if ( ! empty( $input['status'] ) && ! in_array( $input['status'], $allowed_statuses, true ) ) {
466 return new \WP_Error( 'invalid_status', __( 'Invalid quote status.', 'commercebird' ) );
467 }
468
469 $order = wc_get_order( (int) $input['id'] );
470
471 if ( ! $order || ! in_array( $order->get_status(), array( 'on-quote', 'quoted' ), true ) ) {
472 return new \WP_Error( 'not_found', __( 'Quote not found or not in a quote status.', 'commercebird' ) );
473 }
474
475 if ( ! empty( $input['status'] ) ) {
476 $order->set_status( 'wc-' . $input['status'] );
477 }
478
479 if ( isset( $input['line_items'] ) ) {
480 $order->remove_order_items( 'line_item' );
481 foreach ( $input['line_items'] as $item ) {
482 $product = wc_get_product( (int) $item['product_id'] );
483 if ( ! $product ) {
484 continue;
485 }
486 $unit_price = isset( $item['price'] ) ? (float) $item['price'] : (float) $product->get_price();
487 $qty = max( 1, (int) $item['quantity'] );
488 $order->add_product(
489 $product,
490 $qty,
491 array(
492 'subtotal' => $unit_price * $qty,
493 'total' => $unit_price * $qty,
494 )
495 );
496 }
497 $order->calculate_totals();
498 }
499
500 if ( ! empty( $input['validity_days'] ) ) {
501 $validity = gmdate( 'Y-m-d 23:59:59', strtotime( '+' . (int) $input['validity_days'] . ' days' ) );
502 $order->update_meta_data( '_cmbird_quotation_validity', $validity );
503 }
504
505 $order->save();
506
507 if ( ! empty( $input['note'] ) ) {
508 $order->add_order_note( sanitize_textarea_field( $input['note'] ) );
509 }
510
511 return array(
512 'id' => $order->get_id(),
513 'status' => $order->get_status(),
514 'total' => (string) $order->get_total(),
515 'validity' => (string) ( $order->get_meta( '_cmbird_quotation_validity' ) ?: '' ),
516 );
517 }
518
519 public static function execute_delete_quote( array $input ): array|\WP_Error {
520 $order = wc_get_order( (int) $input['id'] );
521
522 if ( ! $order || ! in_array( $order->get_status(), array( 'on-quote', 'quoted' ), true ) ) {
523 return new \WP_Error( 'not_found', __( 'Quote not found or not in a quote status.', 'commercebird' ) );
524 }
525
526 $id = $order->get_id();
527 $order->delete();
528
529 return array(
530 'deleted' => true,
531 'id' => $id,
532 );
533 }
534
535 public static function execute_create_quote( array $input ): array|\WP_Error {
536 $order = wc_create_order(
537 array(
538 'customer_id' => (int) ( $input['customer_id'] ?? 0 ),
539 )
540 );
541
542 if ( is_wp_error( $order ) ) {
543 return $order;
544 }
545
546 $order->set_status( 'wc-on-quote' );
547
548 foreach ( $input['line_items'] ?? array() as $item ) {
549 $product = wc_get_product( (int) $item['product_id'] );
550 if ( ! $product ) {
551 continue;
552 }
553 $unit_price = isset( $item['price'] ) ? (float) $item['price'] : (float) $product->get_price();
554 $qty = max( 1, (int) $item['quantity'] );
555 $order->add_product(
556 $product,
557 $qty,
558 array(
559 'subtotal' => $unit_price * $qty,
560 'total' => $unit_price * $qty,
561 )
562 );
563 }
564
565 $order->calculate_totals();
566 $order->save();
567
568 if ( ! $order->get_id() ) {
569 return new \WP_Error( 'creation_failed', __( 'Failed to create quote.', 'commercebird' ) );
570 }
571
572 if ( ! empty( $input['note'] ) ) {
573 $order->add_order_note( sanitize_textarea_field( $input['note'] ) );
574 }
575
576 return array(
577 'id' => $order->get_id(),
578 'status' => $order->get_status(),
579 'total' => (string) $order->get_total(),
580 'validity' => (string) ( $order->get_meta( '_cmbird_quotation_validity' ) ?: '' ),
581 );
582 }
583 }
584