PluginProbe ʕ •ᴥ•ʔ
CommerceBird – AI Command Center, ERP Integrations & B2B for WooCommerce (Zoho, Exact Online). / 2.8.4
CommerceBird – AI Command Center, ERP Integrations & B2B for WooCommerce (Zoho, Exact Online). v2.8.4
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 / quotes / class-cmbird-quotes.php
commercebird / includes / classes / quotes Last commit date
emails 2 months ago class-cmbird-payment-gateway-quotation.php 2 months ago class-cmbird-quotes.php 2 months ago index.php 3 months ago
class-cmbird-quotes.php
584 lines
1 <?php
2 /**
3 * CommerceBird Quotes
4 *
5 * Registers the "on-quote" and "quoted" custom order statuses, the quotation
6 * payment gateway, all related transactional emails, the Quotations tab on
7 * the my-account page, and supporting admin/frontend hooks.
8 *
9 * Meta key used: _cmbird_quotation_validity (Y-m-d 23:59:59)
10 * Option keys : cmbird_enable_quotations, cmbird_quotation_validity_days,
11 * cmbird_account_quotations_endpoint
12 *
13 * @package CommerceBird\Includes\Quotes
14 */
15
16 defined( 'ABSPATH' ) || exit;
17
18 /**
19 * CMBIRD_Quotes Class
20 */
21 class CMBIRD_Quotes {
22
23 /**
24 * Bootstrap all hooks. Call once from the main plugin file on plugins_loaded.
25 */
26 public static function init(): void {
27 $instance = new self();
28 $instance->register_hooks();
29 }
30
31 /**
32 * Register all hooks and filters.
33 */
34 private function register_hooks(): void {
35 // Custom order statuses.
36 add_filter( 'woocommerce_register_shop_order_post_statuses', array( $this, 'register_order_statuses' ) );
37 add_filter( 'wc_order_statuses', array( $this, 'add_order_statuses' ) );
38 add_filter( 'wc_order_is_editable', array( $this, 'make_order_statuses_editable' ), 10, 2 );
39
40 // My-account rewrite endpoint (must fire on init).
41 add_action( 'init', array( $this, 'add_quotations_endpoint' ) );
42
43 // Payment gateway.
44 add_filter( 'woocommerce_payment_gateways', array( $this, 'add_gateway' ) );
45
46 // Transactional emails.
47 add_filter( 'woocommerce_email_actions', array( $this, 'set_email_actions' ) );
48 add_filter( 'woocommerce_email_classes', array( $this, 'set_email_classes' ) );
49 add_action( 'woocommerce_email_footer', array( $this, 'quoted_email_validity' ) );
50
51 // Stock management.
52 add_filter( 'woocommerce_can_reduce_order_stock', array( $this, 'prevent_stock_reduction_on_quotation' ), 10, 2 );
53 add_action( 'woocommerce_order_status_changed', array( $this, 'stock_reduction_on_quotation' ), 20, 4 );
54
55 // Quotation validity.
56 add_action( 'woocommerce_order_status_changed', array( $this, 'set_quotation_validity' ), 10, 4 );
57
58 // Payment / cancellation eligibility.
59 add_filter( 'woocommerce_valid_order_statuses_for_payment', array( $this, 'quotations_can_be_paid' ), 10, 2 );
60 add_filter( 'woocommerce_valid_order_statuses_for_cancel', array( $this, 'quotations_can_be_cancelled' ) );
61
62 // Suppress standard new-order admin email for quote requests.
63 add_filter( 'woocommerce_email_recipient_new_order', array( $this, 'prevent_new_order_email' ), 20, 2 );
64
65 // Admin order screen: show validity date.
66 add_action( 'woocommerce_admin_order_data_after_payment_info', array( $this, 'display_quotation_validity_admin' ) );
67
68 // My-account tab features (always enabled; set option to 'no' to disable).
69 if ( 'no' !== get_option( 'cmbird_enable_quotations', 'yes' ) ) {
70 add_action( 'woocommerce_account_quotations_endpoint', array( $this, 'quotations_endpoint_content' ) );
71 add_filter( 'woocommerce_endpoint_quotations_title', array( $this, 'quotations_endpoint_title' ), 10, 2 );
72 add_filter( 'woocommerce_get_endpoint_url', array( $this, 'quotations_pagination' ), 10, 4 );
73 add_filter( 'woocommerce_get_query_vars', array( $this, 'add_query_vars' ) );
74 add_filter( 'woocommerce_account_menu_items', array( $this, 'add_quotations_menu' ) );
75 add_filter( 'woocommerce_account_menu_item_classes', array( $this, 'active_quotations_menu' ), 10, 2 );
76 add_filter( 'woocommerce_my_account_my_orders_query', array( $this, 'remove_quotations_from_orders' ) );
77 add_action( 'woocommerce_view_order', array( $this, 'add_payment_button' ), 99 );
78 add_action( 'woocommerce_view_order', array( $this, 'add_quotation_validity' ), 99 );
79 add_filter( 'woocommerce_get_order_item_totals', array( $this, 'remove_quotations_payment_method' ), 10, 3 );
80 add_filter( 'woocommerce_get_return_url', array( $this, 'redirect_to_quotations' ), 10, 2 );
81 }
82 }
83
84 // -------------------------------------------------------------------------
85 // Order status registration
86 // -------------------------------------------------------------------------
87
88 /**
89 * Register wc-on-quote and wc-quoted post statuses.
90 *
91 * @param array $order_statuses Existing custom statuses.
92 * @return array
93 */
94 public function register_order_statuses( array $order_statuses ): array {
95 $order_statuses['wc-on-quote'] = array(
96 'label' => __( 'On quote', 'commercebird' ),
97 'public' => true,
98 'show_in_admin_status_list' => true,
99 'show_in_admin_all_list' => true,
100 'exclude_from_search' => false,
101 /* translators: %s: Number of orders */
102 'label_count' => _n_noop(
103 'On quote <span class="count">(%s)</span>',
104 'On quote <span class="count">(%s)</span>',
105 'commercebird'
106 ),
107 );
108 $order_statuses['wc-quoted'] = array(
109 'label' => __( 'Quoted', 'commercebird' ),
110 'public' => true,
111 'show_in_admin_status_list' => true,
112 'show_in_admin_all_list' => true,
113 'exclude_from_search' => false,
114 /* translators: %s: Number of orders */
115 'label_count' => _n_noop(
116 'Quoted <span class="count">(%s)</span>',
117 'Quoted <span class="count">(%s)</span>',
118 'commercebird'
119 ),
120 );
121 return $order_statuses;
122 }
123
124 /**
125 * Add custom statuses to the WooCommerce order status list.
126 *
127 * @param array $order_statuses Existing statuses.
128 * @return array
129 */
130 public function add_order_statuses( array $order_statuses ): array {
131 $order_statuses['wc-on-quote'] = __( 'On quote', 'commercebird' );
132 $order_statuses['wc-quoted'] = __( 'Quoted', 'commercebird' );
133 return $order_statuses;
134 }
135
136 /**
137 * Allow on-quote orders to be edited in the admin.
138 *
139 * @param bool $editable Whether the order is editable.
140 * @param WC_Order $order Order object.
141 * @return bool
142 */
143 public function make_order_statuses_editable( bool $editable, WC_Order $order ): bool {
144 if ( in_array( $order->get_status(), array( 'on-quote' ), true ) ) {
145 return true;
146 }
147 return $editable;
148 }
149
150 // -------------------------------------------------------------------------
151 // Payment gateway
152 // -------------------------------------------------------------------------
153
154 /**
155 * Register the quotation payment gateway with WooCommerce.
156 *
157 * @param array $gateways Registered gateway class names.
158 * @return array
159 */
160 public function add_gateway( array $gateways ): array {
161 $gateways[] = 'CMBIRD_Gateway_Quotation';
162 return $gateways;
163 }
164
165 // -------------------------------------------------------------------------
166 // Transactional emails
167 // -------------------------------------------------------------------------
168
169 /**
170 * Register email trigger action hooks with WooCommerce mailer.
171 *
172 * @param array $emails Registered email action slugs.
173 * @return array
174 */
175 public function set_email_actions( array $emails ): array {
176 $emails[] = 'woocommerce_order_status_pending_to_on-quote';
177 $emails[] = 'woocommerce_order_status_on-quote';
178 $emails[] = 'woocommerce_order_status_quoted';
179 return $emails;
180 }
181
182 /**
183 * Register quote email classes with WooCommerce.
184 *
185 * @param array $emails Registered WC_Email instances.
186 * @return array
187 */
188 public function set_email_classes( array $emails ): array {
189 $emails['CMBIRD_Email_New_Quote'] = new CMBIRD_Email_New_Quote();
190 $emails['CMBIRD_Email_Customer_OnQuote_Order'] = new CMBIRD_Email_Customer_OnQuote_Order();
191 $emails['CMBIRD_Email_Customer_Quoted_Order'] = new CMBIRD_Email_Customer_Quoted_Order();
192 return $emails;
193 }
194
195 /**
196 * Append quotation validity date to the footer of quoted order emails.
197 *
198 * @param WC_Email $email Current email instance.
199 */
200 public function quoted_email_validity( WC_Email $email ): void {
201 if ( 'cmbird_customer_quoted_order' !== $email->id || ! is_a( $email->object, 'WC_Order' ) ) {
202 return;
203 }
204 $order = wc_get_order( $email->object->get_id() );
205 if ( ! $order || ! $order->has_status( 'quoted' ) ) {
206 return;
207 }
208 $valid_until = $order->get_meta( '_cmbird_quotation_validity', true );
209 if ( $valid_until ) {
210 printf(
211 '<p><strong>%s:</strong> %s</p>',
212 esc_html__( 'Quotation valid until', 'commercebird' ),
213 esc_html( date_i18n( get_option( 'date_format' ), strtotime( $valid_until ) ) )
214 );
215 }
216 }
217
218 // -------------------------------------------------------------------------
219 // Stock management
220 // -------------------------------------------------------------------------
221
222 /**
223 * Prevent stock from being reduced when an order is placed as a quote.
224 *
225 * @param bool $reduce_stock Whether to reduce stock.
226 * @param WC_Order $order Order object.
227 * @return bool
228 */
229 public function prevent_stock_reduction_on_quotation( bool $reduce_stock, WC_Order $order ): bool {
230 if ( $order->has_status( 'on-quote' ) ) {
231 return false;
232 }
233 return $reduce_stock;
234 }
235
236 /**
237 * Reduce stock when an on-quote order transitions to quoted status.
238 *
239 * @param int $order_id Order ID.
240 * @param string $old_status Previous status slug.
241 * @param string $new_status New status slug.
242 * @param WC_Order $order Order object.
243 */
244 public function stock_reduction_on_quotation( int $order_id, string $old_status, string $new_status, WC_Order $order ): void {
245 if ( 'quoted' === $new_status ) {
246 $stock_reduced = $order->get_meta( '_order_stock_reduced', true );
247 if ( empty( $stock_reduced ) ) {
248 wc_maybe_reduce_stock_levels( $order_id );
249 }
250 }
251 }
252
253 // -------------------------------------------------------------------------
254 // Quotation validity
255 // -------------------------------------------------------------------------
256
257 /**
258 * Calculate and store the quotation expiry date when an order becomes quoted.
259 *
260 * @param int $order_id Order ID.
261 * @param string $old_status Previous status slug.
262 * @param string $new_status New status slug.
263 * @param WC_Order $order Order object.
264 */
265 public function set_quotation_validity( int $order_id, string $old_status, string $new_status, WC_Order $order ): void {
266 if ( 'quoted' !== $new_status ) {
267 return;
268 }
269 $validity_days = (int) get_option( 'cmbird_quotation_validity_days', 0 );
270 if ( $validity_days > 0 ) {
271 $valid_until = ( new DateTime( 'now' ) )
272 ->modify( '+' . $validity_days . ' days' )
273 ->format( 'Y-m-d 23:59:59' );
274 $order->update_meta_data( '_cmbird_quotation_validity', $valid_until );
275 $order->save();
276 }
277 }
278
279 // -------------------------------------------------------------------------
280 // Payment / cancellation eligibility
281 // -------------------------------------------------------------------------
282
283 /**
284 * Allow quoted orders to be paid if the validity period has not expired.
285 *
286 * @param array $statuses Payable status slugs.
287 * @param WC_Order $order Order object.
288 * @return array
289 */
290 public function quotations_can_be_paid( array $statuses, WC_Order $order ): array {
291 if ( ! $order->has_status( 'quoted' ) ) {
292 return $statuses;
293 }
294 $valid_until = $order->get_meta( '_cmbird_quotation_validity', true );
295 if ( $valid_until ) {
296 $now = new DateTime();
297 $validity = new DateTime( $valid_until );
298 if ( $validity < $now ) {
299 return $statuses; // Expired — do not allow payment.
300 }
301 }
302 $statuses[] = 'quoted';
303 return $statuses;
304 }
305
306 /**
307 * Allow on-quote and quoted orders to be cancelled by the customer.
308 *
309 * @param array $statuses Cancellable status slugs.
310 * @return array
311 */
312 public function quotations_can_be_cancelled( array $statuses ): array {
313 $statuses[] = 'on-quote';
314 $statuses[] = 'quoted';
315 return $statuses;
316 }
317
318 // -------------------------------------------------------------------------
319 // Email suppression
320 // -------------------------------------------------------------------------
321
322 /**
323 * Suppress the standard WooCommerce new-order admin email for quote requests.
324 *
325 * @param string $recipient Comma-separated email recipients.
326 * @param WC_Order $order Order object.
327 * @return string
328 */
329 public function prevent_new_order_email( string $recipient, $order ): string {
330 if ( is_a( $order, 'WC_Order' ) && $order->has_status( 'on-quote' ) ) {
331 return '';
332 }
333 return $recipient;
334 }
335
336 // -------------------------------------------------------------------------
337 // Admin order screen
338 // -------------------------------------------------------------------------
339
340 /**
341 * Display the quotation validity date in the admin order edit screen.
342 *
343 * @param WC_Order $order Order object.
344 */
345 public function display_quotation_validity_admin( WC_Order $order ): void {
346 $valid_until = $order->get_meta( '_cmbird_quotation_validity', true );
347 if ( $valid_until ) {
348 printf(
349 '<p><strong>%s:</strong> %s</p>',
350 esc_html__( 'Quotation valid until', 'commercebird' ),
351 esc_html( date_i18n( get_option( 'date_format' ), strtotime( $valid_until ) ) )
352 );
353 }
354 }
355
356 // -------------------------------------------------------------------------
357 // My-account: endpoint setup
358 // -------------------------------------------------------------------------
359
360 /**
361 * Register the quotations rewrite endpoint.
362 */
363 public function add_quotations_endpoint(): void {
364 add_rewrite_endpoint(
365 get_option( 'cmbird_account_quotations_endpoint', 'quotations' ),
366 EP_ROOT | EP_PAGES
367 );
368 }
369
370 /**
371 * Register the quotations query variable with WooCommerce.
372 *
373 * @param array $query_vars Existing WC query vars.
374 * @return array
375 */
376 public function add_query_vars( array $query_vars ): array {
377 $query_vars['quotations'] = get_option( 'cmbird_account_quotations_endpoint', 'quotations' );
378 return $query_vars;
379 }
380
381 // -------------------------------------------------------------------------
382 // My-account: menu
383 // -------------------------------------------------------------------------
384
385 /**
386 * Insert the Quotations link after Dashboard in the my-account menu.
387 *
388 * @param array $menu_links Existing menu items.
389 * @return array
390 */
391 public function add_quotations_menu( array $menu_links ): array {
392 $menu_links = array_slice( $menu_links, 0, 1, true )
393 + array( 'quotations' => __( 'Quotations', 'commercebird' ) )
394 + array_slice( $menu_links, 1, count( $menu_links ) - 1, true );
395 return $menu_links;
396 }
397
398 /**
399 * Highlight the Quotations menu item when viewing a quote order detail page.
400 *
401 * @param array $classes CSS classes for the menu item.
402 * @param string $endpoint My-account endpoint slug.
403 * @return array
404 */
405 public function active_quotations_menu( array $classes, string $endpoint ): array {
406 global $wp;
407
408 if ( ! isset( $wp->query_vars['view-order'] ) ) {
409 return $classes;
410 }
411
412 $order = wc_get_order( $wp->query_vars['view-order'] );
413 if ( ! $order ) {
414 return $classes;
415 }
416
417 if ( 'quotations' === $endpoint && $order->has_status( array( 'quoted', 'on-quote' ) ) ) {
418 $classes[] = 'is-active';
419 }
420 if ( 'orders' === $endpoint && $order->has_status( array( 'quoted', 'on-quote' ) ) ) {
421 $key = array_search( 'is-active', $classes, true );
422 if ( false !== $key ) {
423 unset( $classes[ $key ] );
424 }
425 }
426
427 return $classes;
428 }
429
430 // -------------------------------------------------------------------------
431 // My-account: content
432 // -------------------------------------------------------------------------
433
434 /**
435 * Render the quotations list on the my-account quotations page.
436 *
437 * @param int $current_page Current pagination page.
438 */
439 public function quotations_endpoint_content( string $current_page ): void {
440 $current_page = empty( $current_page ) ? 1 : absint( $current_page );
441 $customer_orders = wc_get_orders(
442 array(
443 'customer' => get_current_user_id(),
444 'page' => $current_page,
445 'paginate' => true,
446 'post_status' => array( 'wc-on-quote', 'wc-quoted' ),
447 )
448 );
449
450 wc_get_template(
451 'myaccount/orders.php',
452 array(
453 'current_page' => absint( $current_page ),
454 'customer_orders' => $customer_orders,
455 'has_orders' => 0 < $customer_orders->total,
456 'wp_button_class' => wc_wp_theme_get_element_class_name( 'button' )
457 ? ' ' . wc_wp_theme_get_element_class_name( 'button' )
458 : '',
459 )
460 );
461 }
462
463 /**
464 * Set the page title for the quotations endpoint.
465 *
466 * @param string $title Current title.
467 * @param string $endpoint Endpoint slug.
468 * @return string
469 */
470 public function quotations_endpoint_title( string $title, string $endpoint ): string {
471 return esc_html__( 'Quotations', 'commercebird' );
472 }
473
474 /**
475 * Rewrite pagination URLs to stay on the quotations endpoint.
476 *
477 * @param string $url Current endpoint URL.
478 * @param string $endpoint Endpoint slug.
479 * @param int $value Page number.
480 * @param string $permalink Current permalink.
481 * @return string
482 */
483 public function quotations_pagination( string $url, string $endpoint, string $value, string $permalink ): string {
484 if ( did_action( 'woocommerce_before_account_orders_pagination' ) ) {
485 if ( 'orders' === $endpoint && is_wc_endpoint_url( 'quotations' ) ) {
486 return wc_get_endpoint_url( 'quotations', $value );
487 }
488 }
489 return $url;
490 }
491
492 /**
493 * Exclude on-quote and quoted orders from the standard my-account orders list.
494 *
495 * @param array $args wc_get_orders() arguments.
496 * @return array
497 */
498 public function remove_quotations_from_orders( array $args ): array {
499 $args['post_status'] = array_diff(
500 array_keys( wc_get_order_statuses() ),
501 array( 'wc-on-quote', 'wc-quoted', 'wc-checkout-draft' )
502 );
503 return $args;
504 }
505
506 // -------------------------------------------------------------------------
507 // My-account: order view page
508 // -------------------------------------------------------------------------
509
510 /**
511 * Show a Pay Now button on the order view page for valid quoted orders.
512 *
513 * @param int $order_id Order ID.
514 */
515 public function add_payment_button( int $order_id ): void {
516 $order = wc_get_order( $order_id );
517 if ( ! $order || ! $order->has_status( 'quoted' ) ) {
518 return;
519 }
520 $valid_until = $order->get_meta( '_cmbird_quotation_validity', true );
521 if ( $valid_until ) {
522 $now = new DateTime();
523 $validity = new DateTime( $valid_until );
524 if ( $validity < $now ) {
525 return; // Expired.
526 }
527 }
528 printf(
529 '<a href="%s" class="button">%s</a>',
530 esc_url( $order->get_checkout_payment_url() ),
531 esc_html__( 'Pay now', 'commercebird' )
532 );
533 }
534
535 /**
536 * Show the quotation validity date on the order view page.
537 *
538 * @param int $order_id Order ID.
539 */
540 public function add_quotation_validity( int $order_id ): void {
541 $order = wc_get_order( $order_id );
542 if ( ! $order || ! $order->has_status( 'quoted' ) ) {
543 return;
544 }
545 $valid_until = $order->get_meta( '_cmbird_quotation_validity', true );
546 if ( $valid_until ) {
547 printf(
548 '<p><strong>%s:</strong> %s</p>',
549 esc_html__( 'Quotation valid until', 'commercebird' ),
550 esc_html( date_i18n( get_option( 'date_format' ), strtotime( $valid_until ) ) )
551 );
552 }
553 }
554
555 /**
556 * Hide the payment method row in the order totals table for quote orders.
557 *
558 * @param array $rows Order total rows.
559 * @param WC_Order $order Order object.
560 * @param bool $tax_display Tax display setting.
561 * @return array
562 */
563 public function remove_quotations_payment_method( array $rows, WC_Order $order, bool $tax_display ): array {
564 if ( $order->has_status( array( 'quoted', 'on-quote' ) ) ) {
565 unset( $rows['payment_method'] );
566 }
567 return $rows;
568 }
569
570 /**
571 * Redirect the customer to their quotations tab after placing a quote.
572 *
573 * @param string $return_url Default return URL.
574 * @param WC_Order $order Order object.
575 * @return string
576 */
577 public function redirect_to_quotations( string $return_url, ?WC_Order $order ): string {
578 if ( $order?->has_status( 'on-quote' ) ) {
579 return wc_get_account_endpoint_url( 'quotations' );
580 }
581 return $return_url;
582 }
583 }
584