PluginProbe ʕ •ᴥ•ʔ
CommerceBird – AI Command Center, ERP Integrations & B2B for WooCommerce (Zoho, Exact Online). / trunk
CommerceBird – AI Command Center, ERP Integrations & B2B for WooCommerce (Zoho, Exact Online). vtrunk
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 week ago purchase-orders 3 weeks ago quotes 2 months ago zoho-crm 7 months ago zoho-inventory 1 week ago class-api-handler-zoho.php 3 weeks ago class-auth-zoho.php 4 months ago class-common.php 2 months ago class-mcp.php 1 week ago class-plugin.php 1 week ago class-wc-api.php 5 months ago index.php 1 year ago
class-mcp.php
995 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 * - commercebird/create-post
16 * - commercebird/edit-post
17 * - commercebird/delete-post
18 * - commercebird/create-media
19 *
20 * @package CommerceBird\Includes
21 */
22 namespace CommerceBird;
23
24 defined( 'ABSPATH' ) || exit;
25
26 class CMBIRD_MCP {
27
28 public static function init(): void {
29 add_action( 'wp_abilities_api_categories_init', array( __CLASS__, 'register_categories' ) );
30 add_action( 'wp_abilities_api_init', array( __CLASS__, 'register_abilities' ) );
31 add_filter( 'woocommerce_mcp_include_ability', array( __CLASS__, 'include_ability_in_mcp' ), 10, 2 );
32 }
33
34 public static function register_categories(): void {
35 if ( ! function_exists( 'wp_register_ability_category' ) ) {
36 return;
37 }
38 wp_register_ability_category(
39 'commercebird',
40 array(
41 'label' => __( 'CommerceBird', 'commercebird' ),
42 'description' => __( 'Abilities for managing CommerceBird purchase orders, quotes, and WordPress posts.', 'commercebird' ),
43 )
44 );
45 }
46
47 public static function include_ability_in_mcp( bool $include, string $ability_id ): bool {
48 if ( str_starts_with( $ability_id, 'commercebird/' ) ) {
49 return true;
50 }
51 return $include;
52 }
53
54 public static function register_abilities(): void {
55 if ( ! function_exists( 'wp_register_ability' ) ) {
56 return;
57 }
58 wp_register_ability(
59 'commercebird/create-purchase-order',
60 array(
61 'label' => __( 'Create Purchase Order', 'commercebird' ),
62 'category' => 'commercebird',
63 'description' => __( 'Creates a new purchase order (type: shop_purchase) with optional line items, status, and a note.', 'commercebird' ),
64 'input_schema' => array(
65 'type' => 'object',
66 'properties' => array(
67 'status' => array(
68 'type' => 'string',
69 'description' => 'Initial PO status (without wc- prefix).',
70 'enum' => array( 'awaiting-approval', 'approved', 'part-received', 'received' ),
71 'default' => 'awaiting-approval',
72 ),
73 'line_items' => array(
74 'type' => 'array',
75 'description' => 'Products to add to the purchase order.',
76 'items' => array(
77 'type' => 'object',
78 'properties' => array(
79 'product_id' => array(
80 'type' => 'integer',
81 'description' => 'WooCommerce product ID.',
82 ),
83 'quantity' => array(
84 'type' => 'integer',
85 'description' => 'Quantity to order.',
86 'minimum' => 1,
87 ),
88 'price' => array(
89 'type' => 'number',
90 'description' => 'Unit cost price override. Defaults to the product price.',
91 ),
92 ),
93 'required' => array( 'product_id', 'quantity' ),
94 ),
95 ),
96 'note' => array(
97 'type' => 'string',
98 'description' => 'Internal order note visible to admin.',
99 ),
100 ),
101 ),
102 'output_schema' => array(
103 'type' => 'object',
104 'properties' => array(
105 'id' => array(
106 'type' => 'integer',
107 'description' => 'New purchase order ID.',
108 ),
109 'status' => array( 'type' => 'string' ),
110 'total' => array(
111 'type' => 'string',
112 'description' => 'Order total as a decimal string.',
113 ),
114 ),
115 'required' => array( 'id', 'status', 'total' ),
116 ),
117 'execute_callback' => array( __CLASS__, 'execute_create_purchase_order' ),
118 'permission_callback' => array( __CLASS__, 'check_permission' ),
119 'meta' => array(
120 'show_in_rest' => true,
121 'mcp' => array(
122 'public' => true,
123 'type' => 'tool',
124 ),
125 'expose_in_deprecated_woocommerce_mcp' => true,
126 ),
127 )
128 );
129 wp_register_ability(
130 'commercebird/update-purchase-order',
131 array(
132 'label' => __( 'Update Purchase Order', 'commercebird' ),
133 'category' => 'commercebird',
134 'description' => __( 'Updates the status, line items, or notes of an existing purchase order. Providing line_items replaces all existing items.', 'commercebird' ),
135 'input_schema' => array(
136 'type' => 'object',
137 'properties' => array(
138 'id' => array(
139 'type' => 'integer',
140 'description' => 'ID of the purchase order to update.',
141 ),
142 'status' => array(
143 'type' => 'string',
144 'description' => 'New PO status (without wc- prefix).',
145 'enum' => array( 'awaiting-approval', 'approved', 'part-received', 'received', 'cancelled' ),
146 ),
147 'line_items' => array(
148 'type' => 'array',
149 'description' => 'When provided, replaces all existing line items.',
150 'items' => array(
151 'type' => 'object',
152 'properties' => array(
153 'product_id' => array( 'type' => 'integer' ),
154 'quantity' => array(
155 'type' => 'integer',
156 'minimum' => 1,
157 ),
158 'price' => array( 'type' => 'number' ),
159 ),
160 'required' => array( 'product_id', 'quantity' ),
161 ),
162 ),
163 'note' => array(
164 'type' => 'string',
165 'description' => 'Internal order note to append.',
166 ),
167 ),
168 'required' => array( 'id' ),
169 ),
170 'output_schema' => array(
171 'type' => 'object',
172 'properties' => array(
173 'id' => array( 'type' => 'integer' ),
174 'status' => array( 'type' => 'string' ),
175 'total' => array( 'type' => 'string' ),
176 ),
177 'required' => array( 'id', 'status', 'total' ),
178 ),
179 'execute_callback' => array( __CLASS__, 'execute_update_purchase_order' ),
180 'permission_callback' => array( __CLASS__, 'check_permission' ),
181 'meta' => array(
182 'show_in_rest' => true,
183 'mcp' => array(
184 'public' => true,
185 'type' => 'tool',
186 ),
187 'expose_in_deprecated_woocommerce_mcp' => true,
188 ),
189 )
190 );
191 wp_register_ability(
192 'commercebird/delete-purchase-order',
193 array(
194 'label' => __( 'Delete Purchase Order', 'commercebird' ),
195 'category' => 'commercebird',
196 'description' => __( 'Moves a purchase order to trash.', 'commercebird' ),
197 'input_schema' => array(
198 'type' => 'object',
199 'properties' => array(
200 'id' => array(
201 'type' => 'integer',
202 'description' => 'ID of the purchase order to delete.',
203 ),
204 ),
205 'required' => array( 'id' ),
206 ),
207 'output_schema' => array(
208 'type' => 'object',
209 'properties' => array(
210 'deleted' => array( 'type' => 'boolean' ),
211 'id' => array( 'type' => 'integer' ),
212 ),
213 'required' => array( 'deleted', 'id' ),
214 ),
215 'execute_callback' => array( __CLASS__, 'execute_delete_purchase_order' ),
216 'permission_callback' => array( __CLASS__, 'check_permission' ),
217 'meta' => array(
218 'show_in_rest' => true,
219 'mcp' => array(
220 'public' => true,
221 'type' => 'tool',
222 ),
223 'expose_in_deprecated_woocommerce_mcp' => true,
224 ),
225 )
226 );
227 wp_register_ability(
228 'commercebird/create-quote',
229 array(
230 'label' => __( 'Create Quote', 'commercebird' ),
231 'category' => 'commercebird',
232 'description' => __( 'Creates a new WooCommerce order with status on-quote. Advance it to quoted once pricing is finalised.', 'commercebird' ),
233 'input_schema' => array(
234 'type' => 'object',
235 'properties' => array(
236 'customer_id' => array(
237 'type' => 'integer',
238 'description' => 'Existing WooCommerce customer user ID. Use 0 for guest.',
239 'default' => 0,
240 ),
241 'line_items' => array(
242 'type' => 'array',
243 'description' => 'Products to include in the quote.',
244 'items' => array(
245 'type' => 'object',
246 'properties' => array(
247 'product_id' => array(
248 'type' => 'integer',
249 'description' => 'WooCommerce product ID.',
250 ),
251 'quantity' => array(
252 'type' => 'integer',
253 'minimum' => 1,
254 ),
255 'price' => array(
256 'type' => 'number',
257 'description' => 'Unit price override.',
258 ),
259 ),
260 'required' => array( 'product_id', 'quantity' ),
261 ),
262 ),
263 'note' => array(
264 'type' => 'string',
265 'description' => 'Internal note about this quote.',
266 ),
267 ),
268 ),
269 'output_schema' => array(
270 'type' => 'object',
271 'properties' => array(
272 'id' => array(
273 'type' => 'integer',
274 'description' => 'New quote order ID.',
275 ),
276 'status' => array( 'type' => 'string' ),
277 'total' => array( 'type' => 'string' ),
278 'validity' => array(
279 'type' => 'string',
280 'description' => 'Quote validity date (Y-m-d H:i:s), empty if not yet set.',
281 ),
282 ),
283 'required' => array( 'id', 'status', 'total' ),
284 ),
285 'execute_callback' => array( __CLASS__, 'execute_create_quote' ),
286 'permission_callback' => array( __CLASS__, 'check_permission' ),
287 'meta' => array(
288 'show_in_rest' => true,
289 'mcp' => array(
290 'public' => true,
291 'type' => 'tool',
292 ),
293 'expose_in_deprecated_woocommerce_mcp' => true,
294 ),
295 )
296 );
297 wp_register_ability(
298 'commercebird/update-quote',
299 array(
300 'label' => __( 'Update Quote', 'commercebird' ),
301 'category' => 'commercebird',
302 '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' ),
303 'input_schema' => array(
304 'type' => 'object',
305 'properties' => array(
306 'id' => array(
307 'type' => 'integer',
308 'description' => 'ID of the quote order to update.',
309 ),
310 'status' => array(
311 'type' => 'string',
312 'description' => 'New quote status (without wc- prefix).',
313 'enum' => array( 'on-quote', 'quoted', 'cancelled' ),
314 ),
315 'line_items' => array(
316 'type' => 'array',
317 'description' => 'When provided, replaces all existing line items.',
318 'items' => array(
319 'type' => 'object',
320 'properties' => array(
321 'product_id' => array( 'type' => 'integer' ),
322 'quantity' => array(
323 'type' => 'integer',
324 'minimum' => 1,
325 ),
326 'price' => array( 'type' => 'number' ),
327 ),
328 'required' => array( 'product_id', 'quantity' ),
329 ),
330 ),
331 'validity_days' => array(
332 'type' => 'integer',
333 'description' => 'Number of days from today the quote remains valid. Overwrites the existing validity date.',
334 'minimum' => 1,
335 ),
336 'note' => array(
337 'type' => 'string',
338 'description' => 'Internal note to append.',
339 ),
340 ),
341 'required' => array( 'id' ),
342 ),
343 'output_schema' => array(
344 'type' => 'object',
345 'properties' => array(
346 'id' => array( 'type' => 'integer' ),
347 'status' => array( 'type' => 'string' ),
348 'total' => array( 'type' => 'string' ),
349 'validity' => array( 'type' => 'string' ),
350 ),
351 'required' => array( 'id', 'status', 'total' ),
352 ),
353 'execute_callback' => array( __CLASS__, 'execute_update_quote' ),
354 'permission_callback' => array( __CLASS__, 'check_permission' ),
355 'meta' => array(
356 'show_in_rest' => true,
357 'mcp' => array(
358 'public' => true,
359 'type' => 'tool',
360 ),
361 'expose_in_deprecated_woocommerce_mcp' => true,
362 ),
363 )
364 );
365 wp_register_ability(
366 'commercebird/delete-quote',
367 array(
368 'label' => __( 'Delete Quote', 'commercebird' ),
369 'category' => 'commercebird',
370 'description' => __( 'Moves a quote order (on-quote or quoted) to trash.', 'commercebird' ),
371 'input_schema' => array(
372 'type' => 'object',
373 'properties' => array(
374 'id' => array(
375 'type' => 'integer',
376 'description' => 'ID of the quote order to delete.',
377 ),
378 ),
379 'required' => array( 'id' ),
380 ),
381 'output_schema' => array(
382 'type' => 'object',
383 'properties' => array(
384 'deleted' => array( 'type' => 'boolean' ),
385 'id' => array( 'type' => 'integer' ),
386 ),
387 'required' => array( 'deleted', 'id' ),
388 ),
389 'execute_callback' => array( __CLASS__, 'execute_delete_quote' ),
390 'permission_callback' => array( __CLASS__, 'check_permission' ),
391 'meta' => array(
392 'show_in_rest' => true,
393 'mcp' => array(
394 'public' => true,
395 'type' => 'tool',
396 ),
397 'expose_in_deprecated_woocommerce_mcp' => true,
398 ),
399 )
400 );
401 wp_register_ability(
402 'commercebird/create-post',
403 array(
404 'label' => __( 'Create Post', 'commercebird' ),
405 'category' => 'commercebird',
406 'description' => __( 'Creates a new WordPress post with a title, content, excerpt, status, and optional featured image.', 'commercebird' ),
407 'input_schema' => array(
408 'type' => 'object',
409 'properties' => array(
410 'title' => array(
411 'type' => 'string',
412 'description' => 'Post title.',
413 ),
414 'content' => array(
415 'type' => 'string',
416 'description' => 'Post content (HTML allowed).',
417 ),
418 'excerpt' => array(
419 'type' => 'string',
420 'description' => 'Short summary of the post.',
421 ),
422 'status' => array(
423 'type' => 'string',
424 'description' => 'Post status.',
425 'enum' => array( 'draft', 'publish', 'pending', 'private' ),
426 'default' => 'draft',
427 ),
428 'featured_image' => array(
429 'type' => 'integer',
430 'description' => 'Media attachment ID for the featured image.',
431 ),
432 ),
433 'required' => array( 'title' ),
434 ),
435 'output_schema' => array(
436 'type' => 'object',
437 'properties' => array(
438 'id' => array(
439 'type' => 'integer',
440 'description' => 'New post ID.',
441 ),
442 'title' => array( 'type' => 'string' ),
443 'status' => array( 'type' => 'string' ),
444 'url' => array(
445 'type' => 'string',
446 'description' => 'Post permalink.',
447 ),
448 ),
449 'required' => array( 'id', 'title', 'status', 'url' ),
450 ),
451 'execute_callback' => array( __CLASS__, 'execute_create_post' ),
452 'permission_callback' => array( __CLASS__, 'check_permission' ),
453 'meta' => array(
454 'show_in_rest' => true,
455 'mcp' => array(
456 'public' => true,
457 'type' => 'tool',
458 ),
459 'expose_in_deprecated_woocommerce_mcp' => true,
460 ),
461 )
462 );
463 wp_register_ability(
464 'commercebird/edit-post',
465 array(
466 'label' => __( 'Edit Post', 'commercebird' ),
467 'category' => 'commercebird',
468 'description' => __( 'Updates an existing WordPress post — title, content, excerpt, status, or featured image.', 'commercebird' ),
469 'input_schema' => array(
470 'type' => 'object',
471 'properties' => array(
472 'id' => array(
473 'type' => 'integer',
474 'description' => 'ID of the post to edit.',
475 ),
476 'title' => array(
477 'type' => 'string',
478 'description' => 'New post title.',
479 ),
480 'content' => array(
481 'type' => 'string',
482 'description' => 'New post content (HTML allowed).',
483 ),
484 'excerpt' => array(
485 'type' => 'string',
486 'description' => 'New post excerpt.',
487 ),
488 'status' => array(
489 'type' => 'string',
490 'description' => 'New post status.',
491 'enum' => array( 'draft', 'publish', 'pending', 'private', 'trash' ),
492 ),
493 'featured_image' => array(
494 'type' => 'integer',
495 'description' => 'Media attachment ID for the featured image.',
496 ),
497 ),
498 'required' => array( 'id' ),
499 ),
500 'output_schema' => array(
501 'type' => 'object',
502 'properties' => array(
503 'id' => array( 'type' => 'integer' ),
504 'title' => array( 'type' => 'string' ),
505 'status' => array( 'type' => 'string' ),
506 'url' => array( 'type' => 'string' ),
507 ),
508 'required' => array( 'id', 'title', 'status', 'url' ),
509 ),
510 'execute_callback' => array( __CLASS__, 'execute_edit_post' ),
511 'permission_callback' => array( __CLASS__, 'check_permission' ),
512 'meta' => array(
513 'show_in_rest' => true,
514 'mcp' => array(
515 'public' => true,
516 'type' => 'tool',
517 ),
518 'expose_in_deprecated_woocommerce_mcp' => true,
519 ),
520 )
521 );
522 wp_register_ability(
523 'commercebird/delete-post',
524 array(
525 'label' => __( 'Delete Post', 'commercebird' ),
526 'category' => 'commercebird',
527 'description' => __( 'Moves a WordPress post to trash.', 'commercebird' ),
528 'input_schema' => array(
529 'type' => 'object',
530 'properties' => array(
531 'id' => array(
532 'type' => 'integer',
533 'description' => 'ID of the post to delete.',
534 ),
535 ),
536 'required' => array( 'id' ),
537 ),
538 'output_schema' => array(
539 'type' => 'object',
540 'properties' => array(
541 'deleted' => array( 'type' => 'boolean' ),
542 'id' => array( 'type' => 'integer' ),
543 ),
544 'required' => array( 'deleted', 'id' ),
545 ),
546 'execute_callback' => array( __CLASS__, 'execute_delete_post' ),
547 'permission_callback' => array( __CLASS__, 'check_permission' ),
548 'meta' => array(
549 'show_in_rest' => true,
550 'mcp' => array(
551 'public' => true,
552 'type' => 'tool',
553 ),
554 'expose_in_deprecated_woocommerce_mcp' => true,
555 ),
556 )
557 );
558 wp_register_ability(
559 'commercebird/create-media',
560 array(
561 'label' => __( 'Create Media', 'commercebird' ),
562 'category' => 'commercebird',
563 'description' => __( 'Uploads a base64-encoded image to the WordPress media library.', 'commercebird' ),
564 'input_schema' => array(
565 'type' => 'object',
566 'properties' => array(
567 'data' => array(
568 'type' => 'string',
569 'description' => 'Base64-encoded image data (without the data:image/…;base64, prefix).',
570 ),
571 'filename' => array(
572 'type' => 'string',
573 'description' => 'Desired filename including extension (e.g. photo.jpg).',
574 ),
575 'mime_type' => array(
576 'type' => 'string',
577 'description' => 'MIME type of the image (e.g. image/jpeg, image/png).',
578 ),
579 ),
580 'required' => array( 'data', 'filename', 'mime_type' ),
581 ),
582 'output_schema' => array(
583 'type' => 'object',
584 'properties' => array(
585 'id' => array(
586 'type' => 'integer',
587 'description' => 'New attachment ID.',
588 ),
589 'url' => array(
590 'type' => 'string',
591 'description' => 'Public URL of the uploaded image.',
592 ),
593 ),
594 'required' => array( 'id', 'url' ),
595 ),
596 'execute_callback' => array( __CLASS__, 'execute_create_media' ),
597 'permission_callback' => array( __CLASS__, 'check_permission' ),
598 'meta' => array(
599 'show_in_rest' => true,
600 'mcp' => array(
601 'public' => true,
602 'type' => 'tool',
603 ),
604 'expose_in_deprecated_woocommerce_mcp' => true,
605 ),
606 )
607 );
608 }
609
610 public static function check_permission(): bool {
611 return current_user_can( 'manage_woocommerce' );
612 }
613
614 public static function execute_create_purchase_order( array $input ): array|\WP_Error {
615 $allowed_statuses = array( 'awaiting-approval', 'approved', 'part-received', 'received' );
616 $status = $input['status'] ?? 'awaiting-approval';
617 if ( ! in_array( $status, $allowed_statuses, true ) ) {
618 return new \WP_Error( 'invalid_status', __( 'Invalid purchase order status.', 'commercebird' ) );
619 }
620
621 $order = new \CMBIRD_Purchase_Order();
622 $order->set_status( 'wc-' . $status );
623
624 foreach ( $input['line_items'] ?? array() as $item ) {
625 $product = wc_get_product( (int) $item['product_id'] );
626 if ( ! $product ) {
627 continue;
628 }
629 $unit_price = isset( $item['price'] ) ? (float) $item['price'] : (float) $product->get_price();
630 $qty = max( 1, (int) $item['quantity'] );
631 $order->add_product(
632 $product,
633 $qty,
634 array(
635 'subtotal' => $unit_price * $qty,
636 'total' => $unit_price * $qty,
637 )
638 );
639 }
640
641 $order->calculate_totals();
642 $order->save();
643
644 if ( ! $order->get_id() ) {
645 return new \WP_Error( 'creation_failed', __( 'Failed to create purchase order.', 'commercebird' ) );
646 }
647
648 if ( ! empty( $input['note'] ) ) {
649 $order->add_order_note( sanitize_textarea_field( $input['note'] ) );
650 }
651
652 return array(
653 'id' => $order->get_id(),
654 'status' => $order->get_status(),
655 'total' => (string) $order->get_total(),
656 );
657 }
658
659 public static function execute_update_purchase_order( array $input ): array|\WP_Error {
660 $allowed_statuses = array( 'awaiting-approval', 'approved', 'part-received', 'received', 'cancelled' );
661
662 if ( ! empty( $input['status'] ) && ! in_array( $input['status'], $allowed_statuses, true ) ) {
663 return new \WP_Error( 'invalid_status', __( 'Invalid purchase order status.', 'commercebird' ) );
664 }
665
666 $order = wc_get_order( (int) $input['id'] );
667
668 if ( ! $order || 'shop_purchase' !== $order->get_type() ) {
669 return new \WP_Error( 'not_found', __( 'Purchase order not found.', 'commercebird' ) );
670 }
671
672 if ( ! empty( $input['status'] ) ) {
673 $order->set_status( 'wc-' . $input['status'] );
674 }
675
676 if ( isset( $input['line_items'] ) ) {
677 $order->remove_order_items( 'line_item' );
678 foreach ( $input['line_items'] as $item ) {
679 $product = wc_get_product( (int) $item['product_id'] );
680 if ( ! $product ) {
681 continue;
682 }
683 $unit_price = isset( $item['price'] ) ? (float) $item['price'] : (float) $product->get_price();
684 $qty = max( 1, (int) $item['quantity'] );
685 $order->add_product(
686 $product,
687 $qty,
688 array(
689 'subtotal' => $unit_price * $qty,
690 'total' => $unit_price * $qty,
691 )
692 );
693 }
694 $order->calculate_totals();
695 }
696
697 $order->save();
698
699 if ( ! empty( $input['note'] ) ) {
700 $order->add_order_note( sanitize_textarea_field( $input['note'] ) );
701 }
702
703 return array(
704 'id' => $order->get_id(),
705 'status' => $order->get_status(),
706 'total' => (string) $order->get_total(),
707 );
708 }
709
710 public static function execute_delete_purchase_order( array $input ): array|\WP_Error {
711 $order = wc_get_order( (int) $input['id'] );
712
713 if ( ! $order || 'shop_purchase' !== $order->get_type() ) {
714 return new \WP_Error( 'not_found', __( 'Purchase order not found.', 'commercebird' ) );
715 }
716
717 $id = $order->get_id();
718 $order->delete();
719
720 return array(
721 'deleted' => true,
722 'id' => $id,
723 );
724 }
725
726 public static function execute_update_quote( array $input ): array|\WP_Error {
727 $allowed_statuses = array( 'on-quote', 'quoted', 'cancelled' );
728
729 if ( ! empty( $input['status'] ) && ! in_array( $input['status'], $allowed_statuses, true ) ) {
730 return new \WP_Error( 'invalid_status', __( 'Invalid quote status.', 'commercebird' ) );
731 }
732
733 $order = wc_get_order( (int) $input['id'] );
734
735 if ( ! $order || ! in_array( $order->get_status(), array( 'on-quote', 'quoted' ), true ) ) {
736 return new \WP_Error( 'not_found', __( 'Quote not found or not in a quote status.', 'commercebird' ) );
737 }
738
739 if ( ! empty( $input['status'] ) ) {
740 $order->set_status( 'wc-' . $input['status'] );
741 }
742
743 if ( isset( $input['line_items'] ) ) {
744 $order->remove_order_items( 'line_item' );
745 foreach ( $input['line_items'] as $item ) {
746 $product = wc_get_product( (int) $item['product_id'] );
747 if ( ! $product ) {
748 continue;
749 }
750 $unit_price = isset( $item['price'] ) ? (float) $item['price'] : (float) $product->get_price();
751 $qty = max( 1, (int) $item['quantity'] );
752 $order->add_product(
753 $product,
754 $qty,
755 array(
756 'subtotal' => $unit_price * $qty,
757 'total' => $unit_price * $qty,
758 )
759 );
760 }
761 $order->calculate_totals();
762 }
763
764 if ( ! empty( $input['validity_days'] ) ) {
765 $validity = gmdate( 'Y-m-d 23:59:59', strtotime( '+' . (int) $input['validity_days'] . ' days' ) );
766 $order->update_meta_data( '_cmbird_quotation_validity', $validity );
767 }
768
769 $order->save();
770
771 if ( ! empty( $input['note'] ) ) {
772 $order->add_order_note( sanitize_textarea_field( $input['note'] ) );
773 }
774
775 return array(
776 'id' => $order->get_id(),
777 'status' => $order->get_status(),
778 'total' => (string) $order->get_total(),
779 'validity' => (string) ( $order->get_meta( '_cmbird_quotation_validity' ) ?: '' ),
780 );
781 }
782
783 public static function execute_delete_quote( array $input ): array|\WP_Error {
784 $order = wc_get_order( (int) $input['id'] );
785
786 if ( ! $order || ! in_array( $order->get_status(), array( 'on-quote', 'quoted' ), true ) ) {
787 return new \WP_Error( 'not_found', __( 'Quote not found or not in a quote status.', 'commercebird' ) );
788 }
789
790 $id = $order->get_id();
791 $order->delete();
792
793 return array(
794 'deleted' => true,
795 'id' => $id,
796 );
797 }
798
799 public static function execute_create_quote( array $input ): array|\WP_Error {
800 $order = wc_create_order(
801 array(
802 'customer_id' => (int) ( $input['customer_id'] ?? 0 ),
803 )
804 );
805
806 if ( is_wp_error( $order ) ) {
807 return $order;
808 }
809
810 $order->set_status( 'wc-on-quote' );
811
812 foreach ( $input['line_items'] ?? array() as $item ) {
813 $product = wc_get_product( (int) $item['product_id'] );
814 if ( ! $product ) {
815 continue;
816 }
817 $unit_price = isset( $item['price'] ) ? (float) $item['price'] : (float) $product->get_price();
818 $qty = max( 1, (int) $item['quantity'] );
819 $order->add_product(
820 $product,
821 $qty,
822 array(
823 'subtotal' => $unit_price * $qty,
824 'total' => $unit_price * $qty,
825 )
826 );
827 }
828
829 $order->calculate_totals();
830 $order->save();
831
832 if ( ! $order->get_id() ) {
833 return new \WP_Error( 'creation_failed', __( 'Failed to create quote.', 'commercebird' ) );
834 }
835
836 if ( ! empty( $input['note'] ) ) {
837 $order->add_order_note( sanitize_textarea_field( $input['note'] ) );
838 }
839
840 return array(
841 'id' => $order->get_id(),
842 'status' => $order->get_status(),
843 'total' => (string) $order->get_total(),
844 'validity' => (string) ( $order->get_meta( '_cmbird_quotation_validity' ) ?: '' ),
845 );
846 }
847
848 public static function execute_create_post( array $input ): array|\WP_Error {
849 $allowed_statuses = array( 'draft', 'publish', 'pending', 'private' );
850 $status = $input['status'] ?? 'draft';
851 if ( ! in_array( $status, $allowed_statuses, true ) ) {
852 return new \WP_Error( 'invalid_status', __( 'Invalid post status.', 'commercebird' ) );
853 }
854
855 $post_data = array(
856 'post_title' => sanitize_text_field( $input['title'] ),
857 'post_content' => wp_kses_post( $input['content'] ?? '' ),
858 'post_excerpt' => sanitize_textarea_field( $input['excerpt'] ?? '' ),
859 'post_status' => $status,
860 'post_type' => 'post',
861 );
862
863 $post_id = wp_insert_post( $post_data, true );
864
865 if ( is_wp_error( $post_id ) ) {
866 return $post_id;
867 }
868
869 if ( ! empty( $input['featured_image'] ) ) {
870 set_post_thumbnail( $post_id, (int) $input['featured_image'] );
871 }
872
873 return array(
874 'id' => $post_id,
875 'title' => get_the_title( $post_id ),
876 'status' => get_post_status( $post_id ),
877 'url' => get_permalink( $post_id ),
878 );
879 }
880
881 public static function execute_edit_post( array $input ): array|\WP_Error {
882 $allowed_statuses = array( 'draft', 'publish', 'pending', 'private', 'trash' );
883
884 if ( ! empty( $input['status'] ) && ! in_array( $input['status'], $allowed_statuses, true ) ) {
885 return new \WP_Error( 'invalid_status', __( 'Invalid post status.', 'commercebird' ) );
886 }
887
888 $post = get_post( (int) $input['id'] );
889
890 if ( ! $post || 'post' !== $post->post_type ) {
891 return new \WP_Error( 'not_found', __( 'Post not found.', 'commercebird' ) );
892 }
893
894 $post_data = array(
895 'ID' => $post->ID,
896 );
897
898 if ( isset( $input['title'] ) ) {
899 $post_data['post_title'] = sanitize_text_field( $input['title'] );
900 }
901
902 if ( isset( $input['content'] ) ) {
903 $post_data['post_content'] = wp_kses_post( $input['content'] );
904 }
905
906 if ( isset( $input['excerpt'] ) ) {
907 $post_data['post_excerpt'] = sanitize_textarea_field( $input['excerpt'] );
908 }
909
910 if ( ! empty( $input['status'] ) ) {
911 $post_data['post_status'] = $input['status'];
912 }
913
914 $post_id = wp_update_post( $post_data, true );
915
916 if ( is_wp_error( $post_id ) ) {
917 return $post_id;
918 }
919
920 if ( isset( $input['featured_image'] ) ) {
921 if ( (int) $input['featured_image'] > 0 ) {
922 set_post_thumbnail( $post_id, (int) $input['featured_image'] );
923 } else {
924 delete_post_thumbnail( $post_id );
925 }
926 }
927
928 return array(
929 'id' => $post_id,
930 'title' => get_the_title( $post_id ),
931 'status' => get_post_status( $post_id ),
932 'url' => get_permalink( $post_id ),
933 );
934 }
935
936 public static function execute_delete_post( array $input ): array|\WP_Error {
937 $post = get_post( (int) $input['id'] );
938
939 if ( ! $post || 'post' !== $post->post_type ) {
940 return new \WP_Error( 'not_found', __( 'Post not found.', 'commercebird' ) );
941 }
942
943 $id = $post->ID;
944 wp_trash_post( $id );
945
946 return array(
947 'deleted' => true,
948 'id' => $id,
949 );
950 }
951
952 public static function execute_create_media( array $input ): array|\WP_Error {
953 $data = $input['data'];
954 $filename = sanitize_file_name( $input['filename'] );
955 $mime_type = sanitize_text_field( $input['mime_type'] );
956
957 // Decode the base64 string.
958 $decoded = base64_decode( $data, true );
959 if ( false === $decoded ) {
960 return new \WP_Error( 'invalid_data', __( 'Invalid base64-encoded image data.', 'commercebird' ) );
961 }
962
963 // Write the file to the uploads directory.
964 $upload = wp_upload_bits( $filename, null, $decoded );
965 if ( ! empty( $upload['error'] ) ) {
966 return new \WP_Error( 'upload_failed', $upload['error'] );
967 }
968
969 // Build attachment metadata.
970 $attachment = array(
971 'post_mime_type' => $mime_type,
972 'post_title' => pathinfo( $filename, PATHINFO_FILENAME ),
973 'post_content' => '',
974 'post_status' => 'inherit',
975 );
976
977 $attach_id = wp_insert_attachment( $attachment, $upload['file'] );
978 if ( 0 === $attach_id ) {
979 return new \WP_Error( 'attachment_failed', __( 'Failed to create attachment post.', 'commercebird' ) );
980 }
981
982 // Generate attachment metadata and update the record.
983 if ( ! function_exists( 'wp_generate_attachment_metadata' ) ) {
984 require_once ABSPATH . 'wp-admin/includes/image.php';
985 }
986 $attach_data = wp_generate_attachment_metadata( $attach_id, $upload['file'] );
987 wp_update_attachment_metadata( $attach_id, $attach_data );
988
989 return array(
990 'id' => $attach_id,
991 'url' => wp_get_attachment_url( $attach_id ),
992 );
993 }
994 }
995