PluginProbe
Tiered Pricing Table for WooCommerce / 7.1.1
Tiered Pricing Table for WooCommerce v7.1.1
8.0.2 7.1.7 7.1.5 7.1.4 7.1.1 6.5.0 6.4.0 6.1.0 trunk 1.0 1.1 2.0 2.0.1 2.0.2 2.1 2.1.1 2.1.2 2.2.0 2.2.1 2.2.2 2.2.3 2.3.0 2.3.1 2.3.2 2.3.3 All 91 releases
tier-pricing-table / src / PricingTable.php

PricingTable.php in Tiered Pricing Table for WooCommerce 7.1.1, at src/PricingTable.php

438 lines 16.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php namespace TierPricingTable;
2
3 use TierPricingTable\Admin\ProductPage\AdvanceOptionsForVariableProduct;
4 use TierPricingTable\Admin\ProductPage\TieredPricingTab;
5 use TierPricingTable\Core\ServiceContainerTrait;
6 use TierPricingTable\Settings\Sections\GeneralSection\GeneralSection;
7 use WC_Product;
8 use WC_Product_Data_Store_CPT;
9 use WC_Product_Variable;
10 use WC_Product_Variation;
11
12 class PricingTable {
13
14 use ServiceContainerTrait;
15
16 private static $instance;
17
18 private function __construct() {}
19
20 public static function getInstance(): self {
21 if ( is_null( self::$instance ) ) {
22 self::$instance = new self();
23 }
24
25 return self::$instance;
26 }
27
28 /**
29 * Main function for rendering pricing table for product
30 *
31 * @param int $parentProductID
32 * @param int $variationID
33 * @param array $settings
34 */
35 public function renderPricingTable( $parentProductID = 0, $variationID = null, array $settings = array() ) {
36
37 if ( apply_filters( 'tiered_pricing_table/should_render_pricing_table', true, $parentProductID, $variationID,
38 $settings ) === false ) {
39 return;
40 }
41
42 $parentProduct = wc_get_product( $parentProductID );
43
44 if ( ! $parentProduct ) {
45 return;
46 }
47
48 $settings = wp_parse_args( $settings, $this->getDefaultSettings( $parentProductID ) );
49
50 // If the product is variable, but no specific variation is passed - check for default
51 if ( ! $variationID && TierPricingTablePlugin::isVariableProductSupported( $parentProduct ) ) {
52 $variationID = $this->getDefaultVariation( $parentProduct );
53 }
54
55 $ajaxVariationsThreshold = apply_filters( 'tiered_pricing_table/ajax_variations_threshold', 10 );
56
57 // Use variation if exists, otherwise the parent product is used
58 $productId = $variationID ? $variationID : $parentProduct->get_id();
59 $product = wc_get_product( $productId );
60
61 $supportedTypes = array_merge( TierPricingTablePlugin::getSupportedSimpleProductTypes(),
62 TierPricingTablePlugin::getSupportedVariableProductTypes() );
63
64 // Exit if product is not valid
65 if ( ! $product || ! in_array( $parentProduct->get_type(), $supportedTypes ) ) {
66 return;
67 }
68
69 $settings = apply_filters( 'tiered_pricing_table/display_settings', $settings, $productId );
70
71 if ( 'tooltip' === $settings['display_type'] ) {
72 wp_enqueue_script( 'jquery-ui-tooltip' );
73 }
74
75 $hidden = ( 'tooltip' === $settings['display_type'] || ! $settings['display'] );
76
77 do_action( 'tiered_pricing_table/before_rendering_tiered_pricing', $parentProduct, $variationID, $settings );
78
79 $loadVariationByAjax = true;
80
81 ?>
82 <div class="clear"></div>
83 <div class="tpt__tiered-pricing <?php echo esc_attr( $hidden ? 'tpt__hidden' : '' ); ?>"
84 data-settings="<?php echo esc_attr( json_encode( $settings ) ); ?>"
85 data-display-context="<?php echo esc_attr( $settings['display_context'] ); ?>"
86 data-display-type="<?php echo esc_attr( $settings['display_type'] ); ?>"
87 data-product-id="<?php echo esc_attr( $parentProduct->get_id() ); ?>"
88 data-product-type="<?php echo esc_attr( $parentProduct->get_type() ); ?>"
89
90 <?php if ( TierPricingTablePlugin::isVariableProductSupported( $parentProduct ) ) : ?>
91 <?php
92 $loadVariationByAjax = count( $parentProduct->get_children() ) > $ajaxVariationsThreshold;
93 $variableProductSamePrices = AdvanceOptionsForVariableProduct::isVariableProductSamePrices( $parentProduct->get_id() );
94 ?>
95 data-load-variation-by-ajax="<?php echo esc_attr( wc_bool_to_string( $loadVariationByAjax ) ); ?>"
96 data-variable-product-same-prices="<?php echo esc_attr( wc_bool_to_string( $variableProductSamePrices ) ); ?>"
97 <?php endif; ?>>
98
99 <?php $this->renderPricingTableHTML( $parentProduct, $product, $settings ); ?>
100 </div>
101
102 <?php if ( TierPricingTablePlugin::isVariableProductSupported( $parentProduct ) && ! $loadVariationByAjax ) : ?>
103 <div class="tpt__tiered-pricing-preloaded-variations" style="display:none">
104 <?php foreach ( $parentProduct->get_available_variations( 'objects' ) as $childProduct ) : ?>
105 <div class="tiered-pricing-preloaded-variation"
106 data-variation-id="<?php echo esc_attr( $childProduct->get_id() ); ?>"
107 data-display-context="<?php echo esc_attr( $settings['display_context'] ); ?>">
108 <?php $this->renderPricingTableHTML( $parentProduct, $childProduct, $settings ); ?>
109 </div>
110 <?php endforeach; ?>
111 </div>
112 <?php endif; ?>
113 <?php
114 }
115
116 /**
117 * Render pricing table without wrapper
118 *
119 * @param WC_Product $parentProduct
120 * @param WC_Product $product
121 * @param array $settings
122 */
123 public function renderPricingTableHTML(
124 WC_Product $parentProduct,
125 WC_Product $product,
126 array $settings = array()
127 ) {
128
129 $settings = wp_parse_args( $settings, $this->getDefaultSettings( $parentProduct->get_id() ) );
130
131 // Don't get rules from variable product when variation is empty
132 if ( in_array( $parentProduct->get_type(),
133 TierPricingTablePlugin::getSupportedVariableProductTypes() ) && ! ( $product instanceof WC_Product_Variation ) ) {
134 // Empty rule
135 $priceRule = new PricingRule( $product->get_id() );
136 } else {
137 $priceRule = PriceManager::getPricingRule( $product->get_id() );
138 }
139
140 // If rules are not empty
141 if ( ! empty( $priceRule->getRules() ) ) {
142
143 $displayType = $settings['display_type'];
144 $availableTemplates = TierPricingTablePlugin::getAvailablePricingLayouts();
145
146 $templateType = array_key_exists( $settings['display_type'], $availableTemplates ) ? $displayType : 'table';
147
148 if ( 'tooltip' === $displayType ) {
149 $templateType = 'table';
150 }
151
152 if ( 'table' === $displayType ) {
153 $style = $settings['table_style'];
154
155 if ( $style !== 'default' ) {
156 $templateType = 'table-' . $style;
157 } else {
158 $templateType = 'table';
159 }
160 }
161
162 if ( 'blocks' === $displayType ) {
163 $style = $settings['blocks_style'];
164
165 if ( $style !== 'default' ) {
166 $templateType = 'blocks-' . $style;
167 } else {
168 $templateType = 'blocks';
169 }
170 }
171
172 if ( 'options' === $displayType ) {
173 $style = $settings['options_style'];
174
175 if ( $style !== 'default' ) {
176 $templateType = 'options-' . $style;
177 } else {
178 $templateType = 'options';
179 }
180 }
181
182 $template = "tiered-pricing-$templateType.php";
183
184 do_action( 'tiered_pricing_table/before_rendering_tiered_pricing/inner', $priceRule, $product, $settings );
185
186 $this->checkForDuplicateQuantities( $priceRule );
187
188 $this->getContainer()->getFileManager()->includeTemplate( 'frontend/' . $template, array(
189 'pricing_rule' => $priceRule,
190 'price_rules' => $priceRule->getRules(),
191 'real_price' => $product->get_price(),
192 'product_name' => $product->get_name(),
193 'product_id' => $product->get_id(),
194 'product' => $product,
195 'minimum' => $priceRule->getMinimum( true ),
196 'settings' => $settings,
197 'pricing_type' => $priceRule->getType(),
198 'id' => $this->getUniqueTieredPricingId(),
199 ) );
200 } else {
201 // Provide fallback data for products without pricing rules,
202 // which allows the frontend JS (e.g. dynamic totals, you save, etc.)
203 // to function for all products via the 'tiered_price_update' event.
204 $price_incl_taxes = wc_get_price_including_tax( $product, array(
205 'price' => $product->get_price(),
206 ) );
207
208 $price_excl_taxes = wc_get_price_excluding_tax( $product, array(
209 'price' => $product->get_price(),
210 ) );
211 ?>
212 <div style="display:none;"
213 class="tiered-pricing-fallback-data"
214 data-product-id="<?php echo esc_attr( $product->get_id() ); ?>"
215 data-price-rules="{}"
216 data-minimum="1"
217 data-product-name="<?php echo esc_attr( $product->get_name() ); ?>"
218 data-regular-price="<?php echo esc_attr( $product->get_regular_price() ); ?>"
219 data-sale-price="<?php echo esc_attr( $product->get_sale_price() ); ?>"
220 data-price="<?php echo esc_attr( $product->get_price() ); ?>"
221 data-product-price-suffix="<?php echo esc_attr( $product->get_price_suffix() ); ?>"
222 data-tiered-price="<?php echo esc_attr( $product->get_price() ); ?>"
223 data-tiered-price-exclude-taxes="<?php echo esc_attr( $price_excl_taxes ); ?>"
224 data-tiered-price-include-taxes="<?php echo esc_attr( $price_incl_taxes ); ?>"
225 ></div>
226 <?php
227 }
228 }
229
230 protected function getDefaultSettings( $productId = false ): array {
231 $hasRules = true;
232
233 if ( $productId ) {
234 $product = wc_get_product( $productId );
235 if ( $product ) {
236 $hasRules = $this->productHasPricingRules( $product );
237 }
238 }
239
240 $showTotalPrice = $hasRules ? $this->getContainer()->getSettings()->get( 'show_total_price',
241 'no' ) === 'yes' : $this->getContainer()->getSettings()->get( 'show_total_price_non_tiered',
242 'no' ) === 'yes';
243
244 $settings = array(
245 'display_context' => 'product-page',
246 'display' => $this->getContainer()->getSettings()->get( 'display', 'yes' ) === 'yes',
247 'display_type' => $this->getContainer()->getSettings()->get( 'display_type', 'table' ),
248 'title' => $this->getContainer()->getSettings()->get( 'table_title', '' ),
249 'table_class' => $this->getContainer()->getSettings()->get( 'table_css_class', '' ),
250 'quantity_column_title' => $this->getContainer()->getSettings()->get( 'head_quantity_text',
251 __( 'Quantity', 'tier-pricing-table' ) ),
252 'price_column_title' => $this->getContainer()->getSettings()->get( 'head_price_text',
253 __( 'Price', 'tier-pricing-table' ) ),
254 'discount_column_title' => $this->getContainer()->getSettings()->get( 'head_discount_text',
255 __( 'Discount (%)', 'tier-pricing-table' ) ),
256 'quantity_type' => $this->getContainer()->getSettings()->get( 'quantity_type', 'range' ),
257 'show_discount_column' => $this->getContainer()->getSettings()->get( 'show_discount_column',
258 'yes' ) === 'yes',
259 'clickable_rows' => $this->getContainer()->getSettings()->get( 'clickable_table_rows',
260 'yes' ) === 'yes',
261 'active_tier_color' => $this->getContainer()->getSettings()->get( 'selected_quantity_color',
262 '#3858e9' ),
263 'tooltip_border' => $this->getContainer()->getSettings()->get( 'tooltip_border',
264 'yes' ) === 'yes',
265
266 'table_style' => GeneralSection::getPricingTableStyle(),
267 'compact_layout' => $this->getContainer()->getSettings()->get( 'compact_layout', 'no' ),
268 'blocks_style' => GeneralSection::getPricingBlocksStyle(),
269 'options_style' => GeneralSection::getPricingOptionsStyle(),
270
271 'options_show_total' => GeneralSection::isShowOptionTotal(),
272 'options_show_original_product_price' => GeneralSection::isShowOriginalProductPrice(),
273 'options_show_default_option' => GeneralSection::isDefaultOptionEnabled(),
274
275 'options_default_option_text' => GeneralSection::getDefaultOptionText(),
276 'options_option_text' => GeneralSection::getOptionText(),
277
278 'plain_text_show_default_option' => GeneralSection::isPlainTextFirstTierEnabled(),
279 'plain_text_option_text' => GeneralSection::getPlainTextTemplate(),
280 'plain_text_default_option_text' => GeneralSection::getPlainTextFirstTierTemplate(),
281
282 'update_price_on_product_page' => $this->getContainer()->getSettings()->get( 'update_price_on_product_page',
283 'yes' ) === 'yes',
284 'show_tiered_price_as_discount' => $this->getContainer()->getSettings()->get( 'show_tiered_price_as_discount',
285 'yes' ) === 'yes',
286 'show_total_price' => $showTotalPrice,
287 );
288
289 $default_quantity_measurement = array(
290 'singular' => '',
291 'plural' => '',
292 );
293
294 $quantity_measurement = $default_quantity_measurement;
295
296 if ( in_array( $settings['display_type'], array( 'table', 'horizontal-table', 'tooltip' ) ) ) {
297 $quantity_measurement = $this->getContainer()->getSettings()->get( 'table_quantity_measurement',
298 $default_quantity_measurement );
299 }
300
301 if ( 'blocks' === $settings['display_type'] ) {
302 $quantity_measurement = $this->getContainer()->getSettings()->get( 'blocks_quantity_measurement', array(
303 'singular' => _n( 'piece', 'pieces', 1, 'tier-pricing-table' ),
304 'plural' => _n( 'piece', 'pieces', 2, 'tier-pricing-table' ),
305 ) );
306 }
307
308 $settings['quantity_measurement_singular'] = $quantity_measurement['singular'];
309 $settings['quantity_measurement_plural'] = $quantity_measurement['plural'];
310
311 if ( $productId ) {
312 $template = TieredPricingTab::getProductTemplate( $productId );
313
314 if ( 'default' !== $template ) {
315 $settings['display_type'] = $template;
316 }
317
318 $baseUnitName = TieredPricingTab::getProductBaseUnitName( $productId );
319
320 if ( $baseUnitName['singular'] && $baseUnitName['plural'] ) {
321 $settings['quantity_measurement_singular'] = $baseUnitName['singular'];
322 $settings['quantity_measurement_plural'] = $baseUnitName['plural'];
323 }
324 }
325
326 return $settings;
327 }
328
329 protected function getUniqueTieredPricingId() {
330 return preg_replace( '/[0-9]+/', '', strtolower( wp_generate_password( 20, false ) ) );
331 }
332
333 protected function getDefaultVariation( WC_Product_Variable $product ): ?int {
334
335 $defaultVariation = AdvanceOptionsForVariableProduct::getDefaultVariation( $product->get_id() );
336
337 if ( $defaultVariation ) {
338 return $defaultVariation->get_id();
339 }
340
341 $defaultAttributes = $product->get_default_attributes();
342
343 if ( ! empty( $defaultAttributes ) ) {
344
345 $defaultAttributesKeys = array_map( function ( $attribute ) {
346 return 'attribute_' . $attribute;
347 }, array_keys( $defaultAttributes ) );
348
349 $defaultAttributes = array_combine( $defaultAttributesKeys, $defaultAttributes );
350
351 return ( new WC_Product_Data_Store_CPT() )->find_matching_product_variation( $product, $defaultAttributes );
352 }
353
354 return null;
355 }
356
357 public function productHasPricingRules( WC_Product $product ): bool {
358
359 if ( TierPricingTablePlugin::isSimpleProductSupported( $product ) ) {
360 $pricingRule = PriceManager::getPricingRule( $product->get_id() );
361
362 return ! empty( $pricingRule->getRules() );
363 }
364
365 if ( TierPricingTablePlugin::isVariableProductSupported( $product ) && $product instanceof WC_Product_Variable ) {
366
367 // Do not check if product has tiered pricing for variable product by default.
368 // If a variable product has many variations, it can take many resources to check every variation for rules existing.
369 // The downside is that the plugin will send an AJAX request for each variant selection on the product page even if a product does not have any tiered pricing
370 if ( ! apply_filters( 'tiered_pricing_table/check_if_variable_product_has_rules', false, $product ) ) {
371 return true;
372 }
373
374 $hasTieredPricing = $this->getContainer()->getCache()->getProductData( $product, 'product_has_rules' );
375
376 if ( 'no' === $hasTieredPricing ) {
377 return false;
378 }
379
380 if ( 'yes' === $hasTieredPricing ) {
381 return true;
382 }
383
384 foreach ( $product->get_available_variations() as $productVariation ) {
385 $pricingRule = PriceManager::getPricingRule( $productVariation['variation_id'] );
386
387 if ( ! empty( $pricingRule->getRules() ) ) {
388 $this->getContainer()->getCache()->setProductData( $product, 'product_has_rules', 'yes' );
389
390 return true;
391 }
392 }
393
394 $this->getContainer()->getCache()->setProductData( $product, 'product_has_rules', 'no' );
395 }
396
397 return false;
398 }
399
400 public function checkForDuplicateQuantities( PricingRule $pricingRule ) {
401 // Show notice only for admin users
402 if ( ! current_user_can( 'manage_options' ) ) {
403 return;
404 }
405 $minimum = $pricingRule->getMinimum( true );
406 $pricingRules = $pricingRule->getRules();
407 $firstRuleQuantity = ! empty( $pricingRules ) ? array_keys( $pricingRules )[0] : false;
408
409 if ( $firstRuleQuantity && $firstRuleQuantity <= $minimum ) {
410 ?>
411
412 <div style="font-size: .8em;">
413 <div class="woocommerce-error" role="alert" style="margin-bottom: 0">
414 <?php esc_html_e( 'Pricing Conflict Detected: Your Minimum Order Quantity is equal to or greater than your first pricing tier.',
415 'tier-pricing-table' ); ?>
416 </div>
417 <div style="color: #555;
418 padding: 10px 12px;
419 border: 1px solid #b32c2e;
420 background: #fff5f5;">
421 <p style="padding: 0; margin: 0 0 10px 0;">
422 <?php esc_html_e( 'To fix this, please ensure your first tiered pricing rule starts at a higher quantity than your Minimum Order Quantity.',
423 'tier-pricing-table' ); ?>
424 </p>
425 <p style="padding: 0; margin: 0">
426 <strong><?php esc_html_e( 'Why?',
427 'tier-pricing-table' ); ?></strong> <?php esc_html_e( 'The plugin automatically creates a base tier using your standard WooCommerce product price. This base tier covers any quantities ordered between the Minimum Order Quantity and your first discounted tier.',
428 'tier-pricing-table' ); ?>
429 </p>
430 <br>
431 <b><?php esc_html_e( 'This notice is only visible to store administrators.',
432 'tier-pricing-table' ); ?></b>
433 </div>
434 </div>
435 <?php
436 }
437 }
438 }