PluginProbe
Tiered Pricing Table for WooCommerce / 8.1.0
Tiered Pricing Table for WooCommerce v8.1.0
8.1.0 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 All 92 releases
tier-pricing-table / src / Addons / Tools / PricingTest / PricingTester.php

PricingTester.php in Tiered Pricing Table for WooCommerce 8.1.0, at src/Addons/Tools/PricingTest/PricingTester.php

252 lines 9.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php namespace TierPricingTable\Addons\Tools\PricingTest;
2
3 use TierPricingTable\Addons\AbstractAddon;
4 use TierPricingTable\Addons\GlobalTieredPricing\CPT\GlobalTieredPricingCPT;
5 use TierPricingTable\Addons\GlobalTieredPricing\GlobalPricingRule;
6 use TierPricingTable\Addons\GlobalTieredPricing\GlobalPricingRulesRepository;
7 use TierPricingTable\PriceManager;
8 use TierPricingTable\Settings\Settings;
9 use WC_Product;
10 use WP_User;
11
12 /**
13 * The "Pricing test" utility: what a customer gets for a product, which global pricing rules apply
14 * and why, and which one wins, without opening the storefront.
15 */
16 class PricingTester {
17
18 const TAB = 'pricing-test';
19
20 const RULE_PARAM = 'tpt_rule';
21
22 /**
23 * Link to the utility on the Advanced settings tab, optionally with a rule to highlight.
24 */
25 public static function getUrl( int $ruleId = 0 ): string {
26 $url = admin_url( 'admin.php?page=wc-settings&tab=' . Settings::SETTINGS_PAGE . '&section=advanced' );
27
28 if ( $ruleId ) {
29 $url = add_query_arg( self::RULE_PARAM, $ruleId, $url );
30 }
31
32 return $url . '#' . self::TAB;
33 }
34
35 public static function isAvailable(): bool {
36 return AbstractAddon::isAddonEnabled( 'tools' );
37 }
38
39 public static function rulesEnabled(): bool {
40 return AbstractAddon::isAddonEnabled( 'global-tier-pricing' );
41 }
42
43 /**
44 * Run the pricing for a product as the given customer (0 = visitor) would see it.
45 * Switches the current user for the rest of the request.
46 *
47 * @return array The result, or array{error: string}.
48 */
49 public function run( int $productId, int $userId, int $highlightRuleId = 0 ): array {
50 $user = $userId ? get_user_by( 'id', $userId ) : null;
51 $user = $user ? $user : new WP_User( 0 );
52
53 // the pricing providers read the current user through the plugin's accessor
54 wp_set_current_user( $user->ID );
55 $GLOBALS['tpt_current_user_id'] = $user->ID;
56 add_filter( 'tiered_pricing_table/current_user', function () use ( $user ) {
57 return $user;
58 }, 999 );
59
60 $product = wc_get_product( $productId );
61 if ( ! $product ) {
62 return array( 'error' => __( 'The product could not be loaded.', 'tier-pricing-table' ) );
63 }
64
65 $pricingRule = PriceManager::getPricingRule( $product->get_id() );
66 $provider = (string) ( $pricingRule->provider ?? '' );
67 $tester = new RuleTester();
68
69 $winner = self::rulesEnabled() ? GlobalPricingRulesRepository::getInstance()->getMatchedPricingRule( $product, $user ) : null;
70 // matched, but the rule's priority settings let product-level pricing win
71 $overridden = $winner && 'global-rules' !== $provider;
72
73 $tiers = $pricingRule->getRules();
74 $minimum = $pricingRule->getMinimum();
75 $maximum = ! empty( $pricingRule->data['maximum_quantity'] ) ? (int) $pricingRule->data['maximum_quantity'] : null;
76 $step = ! empty( $pricingRule->data['group_of_quantity'] ) ? (int) $pricingRule->data['group_of_quantity'] : null;
77
78 $basePrice = (float) wc_get_price_to_display( $product );
79 $quantities = array_unique( array_merge( array( 1 ), $minimum ? array( $minimum ) : array(), array_map( 'intval', array_keys( $tiers ) ) ) );
80 sort( $quantities );
81
82 $rows = array();
83 foreach ( $quantities as $quantity ) {
84 $tierPrice = $pricingRule->getTierPrice( $quantity );
85 $price = false !== $tierPrice ? (float) $tierPrice : $basePrice;
86 $rows[] = array(
87 'quantity' => $quantity,
88 'price' => $this->formatPrice( $price ),
89 'discount' => round( PriceManager::calculateDiscount( $basePrice, $price ), 2 ),
90 'total' => $this->formatPrice( $price * $quantity ),
91 'tier' => false !== $tierPrice,
92 );
93 }
94
95 $limits = array();
96 if ( $minimum ) {
97 /* translators: %d: minimum quantity */
98 $limits[] = sprintf( __( 'minimum %d', 'tier-pricing-table' ), $minimum );
99 }
100 if ( $maximum ) {
101 /* translators: %d: maximum quantity */
102 $limits[] = sprintf( __( 'maximum %d', 'tier-pricing-table' ), $maximum );
103 }
104 if ( $step ) {
105 /* translators: %d: quantity step */
106 $limits[] = sprintf( __( 'step %d', 'tier-pricing-table' ), $step );
107 }
108
109 return array(
110 'product' => array(
111 'id' => $product->get_id(),
112 'name' => $product->get_name(),
113 'url' => self::editUrl( $product->get_parent_id() ? $product->get_parent_id() : $product->get_id() ),
114 ),
115 'customer' => array(
116 'id' => $user->ID,
117 'label' => $user->ID ? $user->display_name . ( $user->user_email ? ' (' . $user->user_email . ')' : '' ) : __( 'Visitor (not logged in)', 'tier-pricing-table' ),
118 'roles' => $user->ID ? ( $user->roles ? $tester->roleNames( $user->roles ) : __( 'no role', 'tier-pricing-table' ) ) : '',
119 ),
120 'rulesEnabled' => self::rulesEnabled(),
121 'rules' => self::rulesEnabled() ? $this->rules( $tester, $product, $user, $winner, $overridden, $highlightRuleId ) : array(),
122 'winner' => array(
123 'id' => $winner ? $winner->getId() : 0,
124 'title' => $winner ? self::ruleTitle( $winner->getId() ) : '',
125 'url' => $winner ? self::editUrl( $winner->getId() ) : '',
126 'overridden' => (bool) $overridden,
127 ),
128 'pricing' => array(
129 'provider' => $provider,
130 'source' => RuleTester::providerLabel( $provider ),
131 'type' => $pricingRule->getType(),
132 'base' => $this->formatPrice( $basePrice ),
133 'limits' => $limits,
134 'rows' => $rows,
135 'log' => array_values( (array) $pricingRule->getPricingLog() ),
136 ),
137 );
138 }
139
140 /**
141 * Every rule, newest first (the order the storefront checks them in), with its verdict for this
142 * product and customer. Suspended and unpublished rules are listed too, so "why does my rule not
143 * apply?" has an answer.
144 */
145 protected function rules( RuleTester $tester, WC_Product $product, WP_User $user, ?GlobalPricingRule $winner, bool $overridden, int $highlightRuleId ): array {
146 $ids = get_posts( array(
147 'numberposts' => - 1,
148 'post_type' => GlobalTieredPricingCPT::SLUG,
149 'post_status' => array( 'publish', 'draft', 'pending', 'future', 'private' ),
150 'fields' => 'ids',
151 ) );
152
153 $rules = array_map( function ( $id ) {
154 return GlobalPricingRule::build( $id );
155 }, $ids );
156 usort( $rules, array( GlobalPricingRule::class, 'compareOrder' ) ); // the order the storefront checks them in
157
158 $format = get_option( 'date_format' );
159 $rows = array();
160 foreach ( $rules as $rule ) {
161 $id = $rule->getId();
162
163 $explanation = $tester->explain( $rule, $user, $product );
164 $matches = $rule->matchRequirements( $user, $product );
165 if ( $matches !== $explanation['matches'] ) {
166 // the explanation mirrors the matcher; if they ever disagree, the matcher's verdict stands
167 $explanation = array( 'matches' => $matches, 'reasons' => array( __( 'See the rule\'s products and customers tabs.', 'tier-pricing-table' ) ) );
168 }
169
170 $published = 'publish' === get_post_status( $id );
171 // active | suspended | scheduled | expired | draft
172 $status = ! $published ? 'draft' : ( $rule->isSuspended() ? 'suspended' : $rule->getScheduleStatus() );
173 $isWinner = $winner && $winner->getId() === $id;
174 $notes = array();
175
176 if ( ! $matches ) {
177 $verdict = 'no';
178 } elseif ( $isWinner ) {
179 $verdict = $overridden ? 'overridden' : 'wins';
180 if ( $overridden ) {
181 $notes[] = __( 'Because of the rule\'s priority settings, the product\'s own pricing is applied instead. The pricing log shows the decision.', 'tier-pricing-table' );
182 }
183 } elseif ( 'active' !== $status ) {
184 $verdict = 'inactive';
185 switch ( $status ) {
186 case 'draft':
187 $notes[] = __( 'The rule is not published, so the storefront ignores it.', 'tier-pricing-table' );
188 break;
189 case 'suspended':
190 $notes[] = __( 'The rule is suspended, so the storefront ignores it.', 'tier-pricing-table' );
191 break;
192 case 'scheduled':
193 /* translators: %s: date */
194 $notes[] = sprintf( __( 'The rule starts on %s, so the storefront ignores it until then.', 'tier-pricing-table' ), wp_date( $format, $rule->getStartTimestamp() ) );
195 break;
196 case 'expired':
197 /* translators: %s: date */
198 $notes[] = sprintf( __( 'The rule ended on %s, so the storefront ignores it.', 'tier-pricing-table' ), wp_date( $format, $rule->getEndTimestamp() ) );
199 break;
200 }
201 } elseif ( ! $rule->isValidPricing() ) {
202 $verdict = 'skipped';
203 $notes[] = __( 'The rule changes neither prices nor quantity limits, so the storefront skips it.', 'tier-pricing-table' );
204 } else {
205 $verdict = 'outranked';
206 $notes[] = __( 'Another matching rule has a higher priority and is applied instead. The rule with the lowest priority number is applied first; if two rules have the same number, the newer rule is applied first.', 'tier-pricing-table' );
207 }
208
209 $schedule = '';
210 if ( $rule->getStartDate() || $rule->getEndDate() ) {
211 $schedule = ( $rule->getStartDate() ? wp_date( $format, $rule->getStartTimestamp() ) : '…' )
212 . ' – ' . ( $rule->getEndDate() ? wp_date( $format, $rule->getEndTimestamp() ) : '…' );
213 }
214
215 $rows[] = array(
216 'id' => $id,
217 'title' => self::ruleTitle( $id ),
218 'url' => self::editUrl( $id ),
219 'status' => $status,
220 'priority' => $rule->getPriority(),
221 'schedule' => $schedule,
222 'matches' => $matches,
223 'verdict' => $verdict,
224 'reasons' => $explanation['reasons'],
225 'notes' => $notes,
226 'highlighted' => $id === $highlightRuleId,
227 );
228 }
229
230 return $rows;
231 }
232
233 /**
234 * The rule's title, or "Rule #ID" for a rule saved without one.
235 */
236 public static function ruleTitle( int $id ): string {
237 return GlobalPricingRule::titleOf( $id );
238 }
239
240 /**
241 * The edit screen of a post. Not get_edit_post_link(): the request runs as the tested customer from
242 * the user switch on, and that customer cannot edit posts, so it would return nothing.
243 */
244 protected static function editUrl( int $postId ): string {
245 return admin_url( 'post.php?post=' . $postId . '&action=edit' );
246 }
247
248 protected function formatPrice( float $price ): string {
249 return html_entity_decode( wp_strip_all_tags( wc_price( $price ) ), ENT_QUOTES, 'UTF-8' );
250 }
251 }
252