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