| @@ -6,160 +6,221 @@ | ||
| 6 | 6 | use WC_Cart; |
| 7 | 7 | use WC_Product; |
| 8 | 8 | |
| 9 | 9 | class MinQuantity extends AbstractAddon { |
| 10 | - | |
| 10 | + | |
| 11 | 11 | public function getName(): string { |
| 12 | 12 | return __( 'Minimum order quantity validation', 'tier-pricing-table' ); |
| 13 | 13 | } |
| 14 | - | |
| 14 | + | |
| 15 | 15 | public function run() { |
| 16 | - | |
| 16 | + | |
| 17 | + ( new StoreApiValidation() )->run(); | |
| 18 | + | |
| 17 | 19 | add_action( 'woocommerce_before_calculate_totals', function ( WC_Cart $cart ) { |
| 18 | 20 | foreach ( $cart->get_cart_contents() as $cartItemKey => $cartItem ) { |
| 19 | 21 | if ( $cartItem['data'] instanceof WC_Product ) { |
| 20 | - | |
| 22 | + | |
| 21 | 23 | $productId = ! empty( $cartItem['variation_id'] ) ? $cartItem['variation_id'] : $cartItem['product_id']; |
| 22 | - | |
| 24 | + | |
| 23 | 25 | $pricingRule = PriceManager::getPricingRule( $productId ); |
| 24 | 26 | $min = $pricingRule->getMinimum(); |
| 25 | - | |
| 27 | + | |
| 26 | 28 | if ( ! $min ) { |
| 27 | - return; | |
| 29 | + continue; | |
| 28 | 30 | } |
| 29 | - | |
| 30 | - if ( $this->getProductCartQuantity( $cartItem['product_id'], 'product', $cart ) < $min ) { | |
| 31 | + | |
| 32 | + $cartQty = ! empty( $cartItem['variation_id'] ) | |
| 33 | + ? $this->getProductCartQuantity( $cartItem['variation_id'], 'variation', $cart ) | |
| 34 | + : $this->getProductCartQuantity( $cartItem['product_id'], 'product', $cart ); | |
| 35 | + | |
| 36 | + if ( $cartQty < $min ) { | |
| 31 | 37 | $cart->cart_contents[ $cartItemKey ]['quantity'] = $min; |
| 32 | - | |
| 38 | + | |
| 33 | 39 | // translators: %1$s: item name, %2$s: minimum quantity |
| 34 | 40 | wc_add_notice( sprintf( __( 'Minimum quantity for the %1$s is %2$d', 'tier-pricing-table' ), |
| 35 | - $cartItem['data']->get_name(), $min ), 'error' ); | |
| 41 | + $cartItem['data']->get_name(), $min ), 'error' ); | |
| 36 | 42 | } |
| 37 | 43 | } |
| 38 | 44 | } |
| 39 | 45 | } ); |
| 40 | - | |
| 46 | + | |
| 41 | 47 | add_filter( 'woocommerce_quantity_input_args', function ( $args, $product = null ) { |
| 42 | - | |
| 48 | + | |
| 43 | 49 | if ( $product instanceof WC_Product && TierPricingTablePlugin::isSimpleProductSupported( $product ) ) { |
| 44 | - $pricingRule = PriceManager::getPricingRule( $product->get_id() ); | |
| 45 | - $min = $pricingRule->getMinimum(); | |
| 46 | 50 | |
| 47 | - if ( ! $min ) { | |
| 51 | + if ( is_cart() && ! apply_filters( 'tiered_pricing_table/minimum_quantity/control_cart_quantity_field', true, $product ) ) { | |
| 48 | 52 | return $args; |
| 49 | 53 | } |
| 50 | - | |
| 51 | - $min = max( 1, $min - $this->getProductCartQuantity( $product->get_id() ) ); | |
| 52 | - $args['min_value'] = $min; | |
| 53 | - } | |
| 54 | - | |
| 55 | - return $args; | |
| 56 | - }, 9999, 2 ); | |
| 57 | - | |
| 58 | - // Quantity field in the cart | |
| 59 | - add_filter( 'woocommerce_quantity_input_args', function ( $args, ?WC_Product $product ) { | |
| 60 | - | |
| 61 | - if ( ! apply_filters( 'tiered_pricing_table/minimum_quantity/control_cart_quantity_field', true, | |
| 62 | - $product ) ) { | |
| 63 | - return $args; | |
| 64 | - } | |
| 65 | - | |
| 66 | - if ( ! is_cart() ) { | |
| 67 | - return $args; | |
| 68 | - } | |
| 69 | - | |
| 70 | - if ( $product instanceof WC_Product && TierPricingTablePlugin::isSimpleProductSupported( $product ) ) { | |
| 54 | + | |
| 71 | 55 | $pricingRule = PriceManager::getPricingRule( $product->get_id() ); |
| 72 | 56 | $min = $pricingRule->getMinimum(); |
| 73 | - | |
| 57 | + | |
| 74 | 58 | if ( ! $min ) { |
| 75 | 59 | return $args; |
| 76 | 60 | } |
| 77 | - | |
| 78 | - $args['min_value'] = $pricingRule->getMinimum(); | |
| 61 | + | |
| 62 | + if ( is_cart() ) { | |
| 63 | + $args['min_value'] = $min; | |
| 64 | + } else { | |
| 65 | + $min = max( 1, $min - $this->getProductCartQuantity( $product->get_id() ) ); | |
| 66 | + $args['min_value'] = $min; | |
| 67 | + } | |
| 79 | 68 | } |
| 80 | - | |
| 69 | + | |
| 81 | 70 | return $args; |
| 82 | - }, 10, 3 ); | |
| 83 | - | |
| 84 | - add_filter( 'woocommerce_add_to_cart_validation', function ( $passed, $productId, $quantity ) { | |
| 71 | + }, 9999, 2 ); | |
| 72 | + | |
| 73 | + add_filter( 'woocommerce_add_to_cart_validation', function ( $passed, $productId, $quantity, $variationId = 0 ) { | |
| 85 | 74 | $productId = intval( $productId ); |
| 86 | 75 | $quantity = intval( $quantity ); |
| 87 | - | |
| 88 | - $pricingRule = PriceManager::getPricingRule( $productId ); | |
| 89 | - $min = $pricingRule->getMinimum(); | |
| 90 | - | |
| 91 | - if ( ! $min ) { | |
| 92 | - return $passed; | |
| 93 | - } | |
| 94 | - | |
| 95 | - $min = max( 1, $min - $this->getProductCartQuantity( $productId ) ); | |
| 96 | - | |
| 97 | - if ( $quantity < $min ) { | |
| 76 | + | |
| 77 | + if ( $variationId ) { | |
| 78 | + $variationRule = PriceManager::getPricingRule( $variationId ); | |
| 79 | + $varMin = $variationRule->getMinimum(); | |
| 80 | + if ( $varMin ) { | |
| 81 | + $remainingMin = max( 1, $varMin - $this->getProductCartQuantity( $variationId, 'variation' ) ); | |
| 82 | + if ( $quantity < $remainingMin ) { | |
| 83 | + wc_add_notice( sprintf( __( 'Minimum quantity for the product is %s', 'tier-pricing-table' ), $varMin ), 'error' ); | |
| 84 | + return false; | |
| 85 | + } | |
| 86 | + } | |
| 98 | 87 | |
| 99 | - // translators: %s: minimum quantity | |
| 100 | - wc_add_notice( sprintf( __( 'Minimum quantity for the product is %s', 'tier-pricing-table' ), $min ), | |
| 101 | - 'error' ); | |
| 88 | + $pricingRule = PriceManager::getPricingRule( $productId ); | |
| 89 | + if ( $pricingRule->isMixAndMatchMinQuantity() ) { | |
| 90 | + return $passed; | |
| 91 | + } | |
| 92 | + } else { | |
| 93 | + $pricingRule = PriceManager::getPricingRule( $productId ); | |
| 94 | + $min = $pricingRule->getMinimum(); | |
| 102 | 95 | |
| 103 | - return false; | |
| 96 | + if ( $min ) { | |
| 97 | + $cartQty = $this->getProductCartQuantity( $productId, 'product' ); | |
| 98 | + $remainingMin = max( 1, $min - $cartQty ); | |
| 99 | + | |
| 100 | + if ( $quantity < $remainingMin ) { | |
| 101 | + // translators: %s: minimum quantity | |
| 102 | + wc_add_notice( sprintf( __( 'Minimum quantity for the product is %s', 'tier-pricing-table' ), $min ), 'error' ); | |
| 103 | + return false; | |
| 104 | + } | |
| 105 | + } | |
| 104 | 106 | } |
| 105 | - | |
| 107 | + | |
| 106 | 108 | return $passed; |
| 107 | - | |
| 108 | - }, 10, 3 ); | |
| 109 | - | |
| 109 | + | |
| 110 | + }, 10, 4 ); | |
| 111 | + | |
| 110 | 112 | add_filter( 'woocommerce_update_cart_validation', function ( $passed, $cart_item_key, $values, $quantity ) { |
| 111 | - | |
| 113 | + | |
| 112 | 114 | $product = $values['data'] ?? null; |
| 113 | - | |
| 115 | + | |
| 114 | 116 | if ( ! ( $product instanceof WC_Product ) ) { |
| 115 | 117 | return $passed; |
| 116 | 118 | } |
| 117 | - | |
| 119 | + | |
| 118 | 120 | $pricingRule = PriceManager::getPricingRule( $product->get_id() ); |
| 119 | - | |
| 121 | + | |
| 120 | 122 | if ( ! $pricingRule->getMinimum() ) { |
| 121 | 123 | return $passed; |
| 122 | 124 | } |
| 123 | - | |
| 124 | - $min = max( 1, $pricingRule->getMinimum() - $this->getProductCartQuantity( $values['product_id'] ) ); | |
| 125 | - | |
| 125 | + | |
| 126 | 126 | if ( $quantity && $quantity < $pricingRule->getMinimum() ) { |
| 127 | - | |
| 127 | + | |
| 128 | 128 | // translators: %s: minimum quantity |
| 129 | - wc_add_notice( sprintf( __( 'Minimum quantity for the product is %s', 'tier-pricing-table' ), $pricingRule->getMinimum() ), | |
| 130 | - 'error' ); | |
| 131 | - | |
| 129 | + wc_add_notice( sprintf( __( 'Minimum quantity for the product is %s', 'tier-pricing-table' ), | |
| 130 | + $pricingRule->getMinimum() ), 'error' ); | |
| 131 | + | |
| 132 | 132 | return false; |
| 133 | 133 | } |
| 134 | - | |
| 134 | + | |
| 135 | 135 | return $passed; |
| 136 | - | |
| 136 | + | |
| 137 | 137 | }, 10, 4 ); |
| 138 | - | |
| 138 | + | |
| 139 | 139 | add_filter( 'woocommerce_available_variation', function ( $variation ) { |
| 140 | 140 | $pricingRule = PriceManager::getPricingRule( (int) $variation['variation_id'] ); |
| 141 | - | |
| 141 | + | |
| 142 | 142 | $min = $pricingRule->getMinimum(); |
| 143 | - | |
| 143 | + | |
| 144 | 144 | if ( ! $min ) { |
| 145 | 145 | return $variation; |
| 146 | 146 | } |
| 147 | - | |
| 147 | + | |
| 148 | 148 | $min = max( 1, $min - $this->getProductCartQuantity( (int) $variation['variation_id'], 'variation' ) ); |
| 149 | - | |
| 149 | + | |
| 150 | 150 | $variation['min_qty'] = $min; |
| 151 | 151 | $variation['qty_value'] = $min; |
| 152 | - | |
| 152 | + | |
| 153 | 153 | return $variation; |
| 154 | 154 | } ); |
| 155 | - | |
| 155 | + | |
| 156 | + add_action( 'woocommerce_check_cart_items', function () { | |
| 157 | + $cart = wc()->cart; | |
| 158 | + if ( ! $cart ) { | |
| 159 | + return; | |
| 160 | + } | |
| 161 | + | |
| 162 | + $parentQuantities = array(); | |
| 163 | + | |
| 164 | + foreach ( $cart->get_cart_contents() as $cartItem ) { | |
| 165 | + if ( $cartItem['data'] instanceof WC_Product ) { | |
| 166 | + $itemPricingRule = PriceManager::getPricingRule( $cartItem['data']->get_id() ); | |
| 167 | + $itemMin = $itemPricingRule->getMinimum(); | |
| 168 | + if ( $itemMin && $cartItem['quantity'] < $itemMin ) { | |
| 169 | + // translators: 1: Product name, 2: Minimum quantity. | |
| 170 | + wc_add_notice( sprintf( __( 'Minimum quantity for %1$s is %2$s.', 'tier-pricing-table' ), | |
| 171 | + $cartItem['data']->get_name(), $itemMin ), 'error' ); | |
| 172 | + } | |
| 173 | + | |
| 174 | + $parentId = $cartItem['data']->get_parent_id(); | |
| 175 | + if ( $parentId ) { | |
| 176 | + if ( ! isset( $parentQuantities[ $parentId ] ) ) { | |
| 177 | + $parentQuantities[ $parentId ] = 0; | |
| 178 | + } | |
| 179 | + $parentQuantities[ $parentId ] += $cartItem['quantity']; | |
| 180 | + } | |
| 181 | + } | |
| 182 | + } | |
| 183 | + | |
| 184 | + foreach ( $parentQuantities as $parentId => $qty ) { | |
| 185 | + $pricingRule = PriceManager::getPricingRule( $parentId ); | |
| 186 | + if ( $pricingRule->isMixAndMatchMinQuantity() && $pricingRule->getMinimum() ) { | |
| 187 | + if ( $qty < $pricingRule->getMinimum() ) { | |
| 188 | + $product = wc_get_product( $parentId ); | |
| 189 | + // translators: 1: Minimum quantity, 2: Product name. | |
| 190 | + wc_add_notice( sprintf( __( 'You need to order at least %1$s items of %2$s to meet the minimum order quantity.', | |
| 191 | + 'tier-pricing-table' ), $pricingRule->getMinimum(), | |
| 192 | + $product ? $product->get_name() : '' ), 'error' ); | |
| 193 | + } | |
| 194 | + } | |
| 195 | + } | |
| 196 | + } ); | |
| 197 | + | |
| 198 | + add_action( 'woocommerce_add_to_cart', | |
| 199 | + function ( $cart_item_key, $productId, $quantity, $variationId, $variation, $cartItemData ) { | |
| 200 | + if ( $variationId ) { | |
| 201 | + $parentPricingRule = PriceManager::getPricingRule( $productId ); | |
| 202 | + if ( $parentPricingRule->isMixAndMatchMinQuantity() && $parentPricingRule->getMinimum() ) { | |
| 203 | + $cartQty = $this->getProductCartQuantity( $productId, 'product' ); | |
| 204 | + $min = $parentPricingRule->getMinimum(); | |
| 205 | + if ( $cartQty < $min ) { | |
| 206 | + $missing = $min - $cartQty; | |
| 207 | + $product = wc_get_product( $productId ); | |
| 208 | + // translators: 1: Missing quantity, 2: Product name, 3: Minimum quantity. | |
| 209 | + wc_add_notice( sprintf( __( 'You need %1$s more items of %2$s to meet the minimum order quantity of %3$s.', | |
| 210 | + 'tier-pricing-table' ), $missing, $product ? $product->get_name() : '', $min ), | |
| 211 | + 'notice' ); | |
| 212 | + } | |
| 213 | + } | |
| 214 | + } | |
| 215 | + }, 10, 6 ); | |
| 216 | + | |
| 156 | 217 | add_action( 'wp_head', function () { |
| 157 | - | |
| 218 | + | |
| 158 | 219 | if ( ! is_product() ) { |
| 159 | 220 | return; |
| 160 | 221 | } |
| 161 | - | |
| 222 | + | |
| 162 | 223 | ?> |
| 163 | 224 | <script> |
| 164 | 225 | // Handle Minimum Quantities by Tiered Pricing Table |
| 165 | 226 | (function ($) { |
| @@ -180,37 +241,41 @@ | ||
| 180 | 241 | </script> |
| 181 | 242 | <?php |
| 182 | 243 | } ); |
| 183 | 244 | } |
| 184 | - | |
| 245 | + | |
| 185 | 246 | protected function getProductCartQuantity( $productId, $type = 'product', $cart = null ) { |
| 186 | 247 | $qty = 0; |
| 187 | - | |
| 248 | + | |
| 188 | 249 | $cart = $cart ? $cart : wc()->cart; |
| 189 | - | |
| 250 | + | |
| 190 | 251 | if ( $cart && is_array( $cart->cart_contents ) ) { |
| 191 | 252 | foreach ( $cart->cart_contents as $cartItem ) { |
| 192 | - | |
| 253 | + | |
| 193 | 254 | if ( 'variation' === $type ) { |
| 194 | 255 | $compare = ! empty( $cartItem['variation_id'] ) ? $cartItem['variation_id'] : 0; |
| 195 | 256 | } else { |
| 196 | 257 | $compare = $cartItem['product_id']; |
| 197 | 258 | } |
| 198 | - | |
| 259 | + | |
| 199 | 260 | if ( $compare == $productId ) { |
| 200 | 261 | $qty += $cartItem['quantity']; |
| 201 | 262 | } |
| 202 | 263 | } |
| 203 | 264 | } |
| 204 | - | |
| 265 | + | |
| 205 | 266 | return apply_filters( 'tiered_pricing_table/minimum_quantity/item_quantity', $qty, $productId ); |
| 206 | 267 | } |
| 207 | - | |
| 268 | + | |
| 208 | 269 | public function getDescription(): string { |
| 209 | - return __( 'Turning it off might be useful in case of incompatibility with a 3rd-party MOQ plugin you might use.', | |
| 210 | - 'tier-pricing-table' ); | |
| 270 | + return __( 'Enforce minimum order quantities for products based on tiered pricing rules.', | |
| 271 | + 'tier-pricing-table' ); | |
| 211 | 272 | } |
| 212 | - | |
| 273 | + | |
| 274 | + public function getIcon(): string { | |
| 275 | + return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M7 18c-1.1 0-1.99.9-1.99 2S5.9 22 7 22s2-.9 2-2-.9-2-2-2zM1 2v2h2l3.6 7.59-1.35 2.45c-.16.28-.25.61-.25.96 0 1.1.9 2 2 2h12v-2H7.42c-.14 0-.25-.11-.25-.25l.03-.12.9-1.63h7.45c.75 0 1.41-.41 1.75-1.03l3.58-6.49c.08-.14.12-.31.12-.48 0-.55-.45-1-1-1H5.21l-.94-2H1zm16 16c-1.1 0-1.99.9-1.99 2s.89 2 1.99 2 2-.9 2-2-.9-2-2-2z"/></svg>'; | |
| 276 | + } | |
| 277 | + | |
| 213 | 278 | public function getSlug(): string { |
| 214 | 279 | return 'minimum-quantity'; |
| 215 | 280 | } |
| 216 | 281 | } |