> $lines [ [ product => WC_Product, quantity => int ] ]. * @param float $total Items total after discounts, incl. tax. * @param string $country Country code. * @param string $state State code. * @param int $user_id User ID (0 = guest). * @param string[] $chosen Chosen shipping rate IDs (e.g. flat_rate:3). * @return array */ public static function build( array $lines, $total, $country, $state, $user_id, array $chosen = array() ) { $categories = array(); $types = array(); $classes = array(); $quantity = 0; foreach ( $lines as $line ) { $product = $line['product'] ?? null; if ( ! $product instanceof WC_Product ) { continue; } $quantity += (int) ( $line['quantity'] ?? 1 ); $parent = $product->get_parent_id() ? $product->get_parent_id() : $product->get_id(); foreach ( wc_get_product_term_ids( $parent, 'product_cat' ) as $cat ) { // A product in "Hoodies" is also in its parents ("Clothing"). $categories = array_merge( $categories, array( (int) $cat ), array_map( 'intval', get_ancestors( $cat, 'product_cat', 'taxonomy' ) ) ); } $types[] = self::parent_type( $product ); if ( $product->is_virtual() ) { $types[] = 'virtual'; } if ( $product->is_downloadable() ) { $types[] = 'downloadable'; } if ( $product->get_shipping_class_id() ) { $classes[] = (int) $product->get_shipping_class_id(); } } $methods = array(); foreach ( $chosen as $rate ) { $methods[] = (string) $rate; $methods[] = strtok( (string) $rate, ':' ); // The type too: "flat_rate". } $roles = self::roles( (int) $user_id ); $context = array( 'cart_total' => (float) $total, 'quantity' => $quantity, 'category' => array_values( array_unique( $categories ) ), 'product_type' => array_values( array_unique( $types ) ), 'shipping_class' => array_values( array_unique( $classes ) ), 'shipping_method' => array_values( array_unique( array_filter( $methods ) ) ), 'country' => $country ? array( (string) $country ) : array(), 'state' => $country && $state ? array( $country . ':' . $state ) : array(), 'user_role' => $roles, ); /** * Filter the facts payment and shipping rules are checked against. * * @since 1.0.0 * @param array $context Context. * @param array> $lines Product lines. */ return (array) apply_filters( 'apwc_checkout_rules_context', $context, $lines ); } /** * A user's roles on this site. Guests, and logged-in users with no role here (e.g. a network * user who isn't a member of this site on multisite), count as "guest": an empty list would * slip past "is any of" deny rules. * * @since 1.0.0 * @param int $user_id User ID (0 = guest). * @return string[] */ public static function roles( $user_id ) { $user = $user_id > 0 ? get_userdata( $user_id ) : null; $roles = $user ? array_values( array_filter( array_map( 'strval', (array) $user->roles ) ) ) : array(); return $roles ? $roles : array( 'guest' ); } /** * Product type rules check: the parent's for a variation ("variable"), without loading the * parent (that would cost a query per cart request). Variation types from other plugins * (e.g. subscription variations) ask WooCommerce for the parent's type instead. * * @param WC_Product $product Product or variation. * @return string */ private static function parent_type( WC_Product $product ) { if ( ! $product->get_parent_id() ) { return $product->get_type(); } if ( 'variation' === $product->get_type() ) { return 'variable'; } $type = \WC_Product_Factory::get_product_type( $product->get_parent_id() ); return is_string( $type ) && '' !== $type ? $type : $product->get_type(); } /** * The shopper's address: shipping when set, else billing. * * @since 1.0.0 * @return array{0: string, 1: string} */ private static function customer_place() { $customer = function_exists( 'WC' ) ? WC()->customer : null; if ( ! $customer ) { return array( '', '' ); } $country = $customer->get_shipping_country() ? $customer->get_shipping_country() : $customer->get_billing_country(); $state = $customer->get_shipping_country() ? $customer->get_shipping_state() : $customer->get_billing_state(); return array( (string) $country, (string) $state ); } /** * Context from the cart. * * @since 1.0.0 * @return array|null Null without a cart. */ public static function from_cart() { $cart = function_exists( 'WC' ) ? WC()->cart : null; if ( ! $cart ) { return null; } $lines = array(); foreach ( $cart->get_cart() as $item ) { $lines[] = array( 'product' => $item['data'] ?? null, 'quantity' => (int) ( $item['quantity'] ?? 1 ), ); } list( $country, $state ) = self::customer_place(); return self::build( $lines, (float) $cart->get_cart_contents_total() + (float) $cart->get_cart_contents_tax(), $country, $state, get_current_user_id(), self::chosen_rates( $cart ) ); } /** * Chosen shipping rates of the cart's real packages. The session can hold more (the classic * checkout copies every posted `shipping_method[i]` into it), so only a choice for a package * WooCommerce calculated, that the package offers, counts. A cart that doesn't ship has none. * * @since 1.0.0 * @param \WC_Cart $cart Cart. * @return string[] */ private static function chosen_rates( $cart ) { if ( ! WC()->session || ! $cart->needs_shipping() ) { return array(); } $chosen = (array) WC()->session->get( 'chosen_shipping_methods', array() ); $rates = array(); foreach ( WC()->shipping()->get_packages() as $index => $package ) { $rate = $chosen[ $index ] ?? null; if ( is_string( $rate ) && '' !== $rate && is_array( $package['rates'] ?? null ) && isset( $package['rates'][ $rate ] ) ) { $rates[] = $rate; } } return $rates; } /** * Context from a shipping package. * * @since 1.0.0 * @param array $package Package. * @return array */ public static function from_package( array $package ) { $lines = array(); $total = 0.0; foreach ( (array) ( $package['contents'] ?? array() ) as $item ) { $lines[] = array( 'product' => $item['data'] ?? null, 'quantity' => (int) ( $item['quantity'] ?? 1 ), ); $total += (float) ( $item['line_total'] ?? 0 ) + (float) ( $item['line_tax'] ?? 0 ); } $dest = (array) ( $package['destination'] ?? array() ); $user = (int) ( $package['user']['ID'] ?? get_current_user_id() ); return self::build( $lines, $total, (string) ( $dest['country'] ?? '' ), (string) ( $dest['state'] ?? '' ), $user ); } /** * Context from an order (pay for order page). * * @since 1.0.0 * @param WC_Order $order Order. * @return array */ public static function from_order( WC_Order $order ) { $lines = array(); foreach ( $order->get_items() as $item ) { if ( $item instanceof \WC_Order_Item_Product ) { $lines[] = array( 'product' => $item->get_product(), 'quantity' => $item->get_quantity(), ); } } $country = $order->get_shipping_country() ? $order->get_shipping_country() : $order->get_billing_country(); $state = $order->get_shipping_country() ? $order->get_shipping_state() : $order->get_billing_state(); $chosen = array(); foreach ( $order->get_shipping_methods() as $shipping ) { $chosen[] = $shipping->get_method_id() . ':' . $shipping->get_instance_id(); } $total = (float) $order->get_subtotal() - (float) $order->get_discount_total() + (float) $order->get_cart_tax(); return self::build( $lines, $total, $country, $state, (int) $order->get_customer_id(), $chosen ); } }