PluginProbe
Tiered Pricing Table for WooCommerce / trunk
Tiered Pricing Table for WooCommerce vtrunk
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 2.3.4 All 90 releases
tier-pricing-table / src / PricingTable.php

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

455 lines 17.0 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 $sale_price = $product->get_sale_price();
213
214 if ( $sale_price ) {
215 $sale_price = wc_get_price_to_display( $product, array(
216 'price' => $sale_price,
217 ) );
218 }
219
220 $regular_price = wc_get_price_to_display( $product, array(
221 'price' => $product->get_regular_price(),
222 ) );
223
224 $price = wc_get_price_to_display( $product, array(
225 'price' => $product->get_price(),
226 ) );
227
228 ?>
229 <div style="display:none;"
230 class="tiered-pricing-fallback-data"
231 data-product-id="<?php echo esc_attr( $product->get_id() ); ?>"
232 data-price-rules="{}"
233 data-minimum="<?php echo esc_attr( $priceRule->getMinimum( true ) ); ?>"
234 data-product-name="<?php echo esc_attr( $product->get_name() ); ?>"
235 data-regular-price="<?php echo esc_attr( $regular_price ); ?>"
236 data-sale-price="<?php echo esc_attr( $sale_price ); ?>"
237 data-price="<?php echo esc_attr( $price ); ?>"
238 data-product-price-suffix="<?php echo esc_attr( $product->get_price_suffix() ); ?>"
239 data-tiered-price="<?php echo esc_attr( $price ); ?>"
240 data-tiered-price-exclude-taxes="<?php echo esc_attr( $price_excl_taxes ); ?>"
241 data-tiered-price-include-taxes="<?php echo esc_attr( $price_incl_taxes ); ?>"
242 ></div>
243 <?php
244 }
245 }
246
247 protected function getDefaultSettings( $productId = false ): array {
248 $hasRules = true;
249
250 if ( $productId ) {
251 $product = wc_get_product( $productId );
252 if ( $product ) {
253 $hasRules = $this->productHasPricingRules( $product );
254 }
255 }
256
257 $showTotalPrice = $hasRules ? $this->getContainer()->getSettings()->get( 'show_total_price',
258 'no' ) === 'yes' : $this->getContainer()->getSettings()->get( 'show_total_price_non_tiered',
259 'no' ) === 'yes';
260
261 $settings = array(
262 'display_context' => 'product-page',
263 'display' => $this->getContainer()->getSettings()->get( 'display', 'yes' ) === 'yes',
264 'display_type' => $this->getContainer()->getSettings()->get( 'display_type', 'table' ),
265 'title' => $this->getContainer()->getSettings()->get( 'table_title', '' ),
266 'table_class' => $this->getContainer()->getSettings()->get( 'table_css_class', '' ),
267 'quantity_column_title' => $this->getContainer()->getSettings()->get( 'head_quantity_text',
268 __( 'Quantity', 'tier-pricing-table' ) ),
269 'price_column_title' => $this->getContainer()->getSettings()->get( 'head_price_text',
270 __( 'Price', 'tier-pricing-table' ) ),
271 'discount_column_title' => $this->getContainer()->getSettings()->get( 'head_discount_text',
272 __( 'Discount (%)', 'tier-pricing-table' ) ),
273 'quantity_type' => $this->getContainer()->getSettings()->get( 'quantity_type', 'range' ),
274 'show_discount_column' => $this->getContainer()->getSettings()->get( 'show_discount_column',
275 'yes' ) === 'yes',
276 'clickable_rows' => $this->getContainer()->getSettings()->get( 'clickable_table_rows',
277 'yes' ) === 'yes',
278 'active_tier_color' => $this->getContainer()->getSettings()->get( 'selected_quantity_color',
279 '#3858e9' ),
280 'tooltip_border' => $this->getContainer()->getSettings()->get( 'tooltip_border',
281 'yes' ) === 'yes',
282
283 'table_style' => GeneralSection::getPricingTableStyle(),
284 'compact_layout' => $this->getContainer()->getSettings()->get( 'compact_layout', 'no' ),
285 'blocks_style' => GeneralSection::getPricingBlocksStyle(),
286 'options_style' => GeneralSection::getPricingOptionsStyle(),
287
288 'options_show_total' => GeneralSection::isShowOptionTotal(),
289 'options_show_original_product_price' => GeneralSection::isShowOriginalProductPrice(),
290 'options_show_default_option' => GeneralSection::isDefaultOptionEnabled(),
291
292 'options_default_option_text' => GeneralSection::getDefaultOptionText(),
293 'options_option_text' => GeneralSection::getOptionText(),
294
295 'plain_text_show_default_option' => GeneralSection::isPlainTextFirstTierEnabled(),
296 'plain_text_option_text' => GeneralSection::getPlainTextTemplate(),
297 'plain_text_default_option_text' => GeneralSection::getPlainTextFirstTierTemplate(),
298
299 'update_price_on_product_page' => $this->getContainer()->getSettings()->get( 'update_price_on_product_page',
300 'yes' ) === 'yes',
301 'show_tiered_price_as_discount' => $this->getContainer()->getSettings()->get( 'show_tiered_price_as_discount',
302 'yes' ) === 'yes',
303 'show_total_price' => $showTotalPrice,
304 );
305
306 $default_quantity_measurement = array(
307 'singular' => '',
308 'plural' => '',
309 );
310
311 $quantity_measurement = $default_quantity_measurement;
312
313 if ( in_array( $settings['display_type'], array( 'table', 'horizontal-table', 'tooltip' ) ) ) {
314 $quantity_measurement = $this->getContainer()->getSettings()->get( 'table_quantity_measurement',
315 $default_quantity_measurement );
316 }
317
318 if ( 'blocks' === $settings['display_type'] ) {
319 $quantity_measurement = $this->getContainer()->getSettings()->get( 'blocks_quantity_measurement', array(
320 'singular' => _n( 'piece', 'pieces', 1, 'tier-pricing-table' ),
321 'plural' => _n( 'piece', 'pieces', 2, 'tier-pricing-table' ),
322 ) );
323 }
324
325 $settings['quantity_measurement_singular'] = $quantity_measurement['singular'];
326 $settings['quantity_measurement_plural'] = $quantity_measurement['plural'];
327
328 if ( $productId ) {
329 $template = TieredPricingTab::getProductTemplate( $productId );
330
331 if ( 'default' !== $template ) {
332 $settings['display_type'] = $template;
333 }
334
335 $baseUnitName = TieredPricingTab::getProductBaseUnitName( $productId );
336
337 if ( $baseUnitName['singular'] && $baseUnitName['plural'] ) {
338 $settings['quantity_measurement_singular'] = $baseUnitName['singular'];
339 $settings['quantity_measurement_plural'] = $baseUnitName['plural'];
340 }
341 }
342
343 return $settings;
344 }
345
346 protected function getUniqueTieredPricingId() {
347 return preg_replace( '/[0-9]+/', '', strtolower( wp_generate_password( 20, false ) ) );
348 }
349
350 protected function getDefaultVariation( WC_Product_Variable $product ): ?int {
351
352 $defaultVariation = AdvanceOptionsForVariableProduct::getDefaultVariation( $product->get_id() );
353
354 if ( $defaultVariation ) {
355 return $defaultVariation->get_id();
356 }
357
358 $defaultAttributes = $product->get_default_attributes();
359
360 if ( ! empty( $defaultAttributes ) ) {
361
362 $defaultAttributesKeys = array_map( function ( $attribute ) {
363 return 'attribute_' . $attribute;
364 }, array_keys( $defaultAttributes ) );
365
366 $defaultAttributes = array_combine( $defaultAttributesKeys, $defaultAttributes );
367
368 return ( new WC_Product_Data_Store_CPT() )->find_matching_product_variation( $product, $defaultAttributes );
369 }
370
371 return null;
372 }
373
374 public function productHasPricingRules( WC_Product $product ): bool {
375
376 if ( TierPricingTablePlugin::isSimpleProductSupported( $product ) ) {
377 $pricingRule = PriceManager::getPricingRule( $product->get_id() );
378
379 return ! empty( $pricingRule->getRules() );
380 }
381
382 if ( TierPricingTablePlugin::isVariableProductSupported( $product ) && $product instanceof WC_Product_Variable ) {
383
384 // Do not check if product has tiered pricing for variable product by default.
385 // If a variable product has many variations, it can take many resources to check every variation for rules existing.
386 // 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
387 if ( ! apply_filters( 'tiered_pricing_table/check_if_variable_product_has_rules', false, $product ) ) {
388 return true;
389 }
390
391 $hasTieredPricing = $this->getContainer()->getCache()->getProductData( $product, 'product_has_rules' );
392
393 if ( 'no' === $hasTieredPricing ) {
394 return false;
395 }
396
397 if ( 'yes' === $hasTieredPricing ) {
398 return true;
399 }
400
401 foreach ( $product->get_available_variations() as $productVariation ) {
402 $pricingRule = PriceManager::getPricingRule( $productVariation['variation_id'] );
403
404 if ( ! empty( $pricingRule->getRules() ) ) {
405 $this->getContainer()->getCache()->setProductData( $product, 'product_has_rules', 'yes' );
406
407 return true;
408 }
409 }
410
411 $this->getContainer()->getCache()->setProductData( $product, 'product_has_rules', 'no' );
412 }
413
414 return false;
415 }
416
417 public function checkForDuplicateQuantities( PricingRule $pricingRule ) {
418 // Show notice only for admin users
419 if ( ! current_user_can( 'manage_options' ) ) {
420 return;
421 }
422 $minimum = $pricingRule->getMinimum( true );
423 $pricingRules = $pricingRule->getRules();
424 $firstRuleQuantity = ! empty( $pricingRules ) ? array_keys( $pricingRules )[0] : false;
425
426 if ( $firstRuleQuantity && $firstRuleQuantity <= $minimum ) {
427 ?>
428
429 <div style="font-size: .8em;">
430 <div class="woocommerce-error" role="alert" style="margin-bottom: 0">
431 <?php esc_html_e( 'Pricing Conflict Detected: Your Minimum Order Quantity is equal to or greater than your first pricing tier.',
432 'tier-pricing-table' ); ?>
433 </div>
434 <div style="color: #555;
435 padding: 10px 12px;
436 border: 1px solid #b32c2e;
437 background: #fff5f5;">
438 <p style="padding: 0; margin: 0 0 10px 0;">
439 <?php esc_html_e( 'To fix this, please ensure your first tiered pricing rule starts at a higher quantity than your Minimum Order Quantity.',
440 'tier-pricing-table' ); ?>
441 </p>
442 <p style="padding: 0; margin: 0">
443 <strong><?php esc_html_e( 'Why?',
444 '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.',
445 'tier-pricing-table' ); ?>
446 </p>
447 <br>
448 <b><?php esc_html_e( 'This notice is only visible to store administrators.',
449 'tier-pricing-table' ); ?></b>
450 </div>
451 </div>
452 <?php
453 }
454 }
455 }