customer ) { $email = sanitize_email( (string) WC()->customer->get_billing_email() ); } return array( 'user_id' => absint( $user_id ), 'email' => $email ); } /** * Sampled identities for one cart item, expanded for packs. * Non-pack: [sampled ID]. Pack: [pack ID, component IDs...]. * Falls back to the single sampled ID for old-style cart data. * * @param array $item Cart item. * @return int[] */ function tryv_cart_item_identities( $item ) { $ids = array(); if ( isset( $item['tryv_sampled_ids'] ) && is_array( $item['tryv_sampled_ids'] ) ) { $ids = array_map( 'absint', $item['tryv_sampled_ids'] ); } elseif ( isset( $item['tryv_sampled_id'] ) ) { $ids = array( absint( $item['tryv_sampled_id'] ) ); } // A pack line always includes its own ID, even if the cart data // was built before v1.0.1 stored it there. if ( ! empty( $item['tryv_is_pack'] ) && isset( $item['product_id'] ) ) { $ids[] = absint( $item['product_id'] ); } return array_values( array_unique( array_filter( $ids ) ) ); } /** * The product whose limit values apply to one identity in one cart * item: the parent product for a variation identity, the component * product for a pack-component identity, otherwise the item product. * * @param array $cart_item Cart item. * @param int $identity Sampled identity. * @return WC_Product|null */ function tryv_identity_limit_product( $cart_item, $identity ) { $parent = wc_get_product( $cart_item['product_id'] ); if ( ! $parent ) { return null; } if ( ! empty( $cart_item['tryv_is_pack'] ) ) { $variation_id = isset( $cart_item['variation_id'] ) ? absint( $cart_item['variation_id'] ) : 0; if ( absint( $identity ) !== absint( $cart_item['product_id'] ) && absint( $identity ) !== $variation_id ) { $component = wc_get_product( $identity ); if ( $component ) { return $component; } } } return $parent; } /** * Display name for one sampled identity, for error messages. * * @param int $identity Sampled identity. * @return string */ function tryv_identity_name( $identity ) { $product = wc_get_product( $identity ); if ( $product ) { return $product->get_name(); } return sprintf( /* translators: %d: product ID */ __( 'Product #%d', 'tryvertising' ), absint( $identity ) ); } /** * Sample price for a chosen product/variation, in this order: * variation sample price, variation regular price, parent sample * price, parent regular price. (QC fix M5: the old chain skipped * the variation regular price and jumped straight to the parent * sample price.) * * @param int $product_id Product ID. * @param int $variation_id Variation ID, 0 for simple products. * @return float */ function tryv_sample_price_for( $product_id, $variation_id ) { if ( $variation_id > 0 ) { $variation = wc_get_product( $variation_id ); if ( $variation ) { $raw = $variation->get_meta( '_tryv_sample_price', true ); if ( '' !== $raw && null !== $raw ) { return max( 0, (float) $raw ); } $regular = $variation->get_regular_price(); if ( '' !== $regular && null !== $regular ) { return max( 0, (float) $regular ); } } } $product = wc_get_product( $product_id ); if ( ! $product ) { return 0.0; } return tryv_get_sample_price( $product ); } /** * Render the sample button on the single product page. * * This hook runs inside WooCommerce's add-to-cart form, so the button * submits the chosen variation with it. No button is shown when * samples are disabled globally, when the product does not allow * samples, or when guests are not allowed and nobody is logged in. * Empty packs show no button either, because tryv_product_sample_enabled() * now returns false for a pack with no valid components (QC fix B2). */ function tryv_render_sample_button() { global $product; if ( ! $product instanceof WC_Product ) { return; } $settings = tryv_get_settings(); if ( 'yes' !== $settings['enabled'] ) { return; } if ( ! tryv_product_sample_enabled( $product ) ) { return; } if ( ! is_user_logged_in() && 'yes' !== $settings['allow_guests'] ) { echo '

' . esc_html__( 'Log in to order a sample.', 'tryvertising' ) . '

'; return; } $label = tryv_product_is_pack( $product ) ? __( 'Order swatch pack', 'tryvertising' ) : $settings['button_label']; wp_nonce_field( 'tryv_add_sample', 'tryv_sample_nonce' ); printf( '', esc_html( $label ) ); } /** * Tag a cart item as a sample, with the data later pricing and order * code needs. Fails closed: without a valid nonce, for a product * that does not allow samples, when samples are disabled globally, * or for a pack with no valid components, the item is added as a * normal item (and the validation hooks below will block a sample * add outright when the request is a sample request). * * @param array $cart_item_data Existing cart item data. * @param int $product_id Product ID. * @param int $variation_id Variation ID. * @param int $quantity Quantity. * @return array */ function tryv_add_cart_item_data( $cart_item_data, $product_id, $variation_id, $quantity ) { if ( ! tryv_is_sample_request() || ! tryv_verify_sample_nonce() ) { return $cart_item_data; } $settings = tryv_get_settings(); if ( 'yes' !== $settings['enabled'] ) { return $cart_item_data; } $product = wc_get_product( $product_id ); if ( ! $product || ! tryv_product_sample_enabled( $product ) ) { return $cart_item_data; } $sampled_id = tryv_sampled_id( $product_id, $variation_id ); $is_pack = tryv_product_is_pack( $product ); if ( $is_pack ) { // Pack identity is [pack ID, valid component IDs...] — see // the file header. Valid components only: a component that // is unpublished, out of stock, not purchasable, or itself a // pack is not sampled, and an empty valid list is not a pack. $components = tryv_pack_valid_component_ids( $product ); if ( empty( $components ) ) { return $cart_item_data; } $sampled_ids = array_values( array_unique( array_merge( array( absint( $product_id ) ), $components ) ) ); } else { $sampled_ids = array( $sampled_id ); } $cart_item_data['tryv_sample'] = true; $cart_item_data['tryv_sampled_id'] = $sampled_id; $cart_item_data['tryv_sampled_ids'] = $sampled_ids; $cart_item_data['tryv_is_pack'] = $is_pack; $cart_item_data['tryv_pack_id'] = $is_pack ? absint( $product_id ) : 0; $cart_item_data['tryv_sample_price'] = tryv_sample_price_for( $product_id, $variation_id ); // Keep sample and non-sample lines for the same product separate. $cart_item_data['unique_key'] = md5( 'tryv-sample-' . $product_id . '-' . $variation_id . '-' . ( $is_pack ? 'pack' : 'single' ) ); return $cart_item_data; } /** * Prior sample usage for one sampled identity, from pending, on-hold, * processing and completed orders. Used for lifetime limits and the * free allowance. * * Identity (M2): when both a user ID and an email are known, orders * matching EITHER are counted, deduplicated by order ID, so a * logged-in customer is not split from their guest orders under the * same email, and an email change does not erase their account * history. When only one is known, only that is used. When neither * is known, usage is 0 — guests are re-checked at checkout once a * billing email is known. * * Pack legacy (B1): orders created before v1.0.1 stored only * component IDs on the line, not the pack ID. A pack line is * therefore also counted when its product ID matches the queried * identity and the line is flagged as a pack, so old pack orders * still count against pack limits. * * Results are cached for the duration of one request, keyed by * identity + user + email, because pricing and validation may ask * for the same identity several times per cart. * * @param int $sampled_id Sampled product or variation ID. * @param int $user_id User ID, 0 for guests. * @param string $email Billing email for guests, '' otherwise. * @return array qty and free_qty */ function tryv_prior_sample_usage( $sampled_id, $user_id = 0, $email = '' ) { static $cache = array(); $sampled_id = absint( $sampled_id ); $user_id = absint( $user_id ); $email = '' !== $email ? strtolower( trim( $email ) ) : ''; $cache_key = $sampled_id . '|' . $user_id . '|' . $email; if ( isset( $cache[ $cache_key ] ) ) { return $cache[ $cache_key ]; } $usage = array( 'qty' => 0, 'free_qty' => 0 ); if ( $user_id <= 0 && '' === $email ) { $cache[ $cache_key ] = $usage; return $usage; } $base_query = array( 'limit' => -1, 'status' => tryv_limit_order_statuses(), 'orderby' => 'date', 'order' => 'ASC', ); $orders_by_id = array(); if ( $user_id > 0 ) { $query = $base_query; $query['customer_id'] = $user_id; foreach ( wc_get_orders( $query ) as $order ) { $orders_by_id[ $order->get_id() ] = $order; } } if ( '' !== $email ) { $query = $base_query; $query['billing_email'] = $email; foreach ( wc_get_orders( $query ) as $order ) { $orders_by_id[ $order->get_id() ] = $order; } } foreach ( $orders_by_id as $order ) { foreach ( $order->get_items() as $item ) { if ( 'yes' !== $item->get_meta( '_tryv_sample', true ) ) { continue; } $ids_raw = $item->get_meta( '_tryv_sampled_ids', true ); $ids = array(); if ( is_array( $ids_raw ) ) { $ids = $ids_raw; } elseif ( '' !== $ids_raw ) { $ids = explode( ',', (string) $ids_raw ); } $ids = array_map( 'absint', $ids ); $is_pack_line = 'yes' === $item->get_meta( '_tryv_is_pack', true ); $matches = in_array( $sampled_id, $ids, true ) || ( $is_pack_line && absint( $item->get_product_id() ) === $sampled_id ); if ( ! $matches ) { continue; } $usage['qty'] += absint( $item->get_quantity() ); $usage['free_qty'] += absint( $item->get_meta( '_tryv_sample_free_qty', true ) ); } } $cache[ $cache_key ] = $usage; return $usage; } /** * Quantity of samples for one sampled identity already in the cart. * A pack line counts toward every identity it contains (pack ID and * each component), not just its single sampled ID. * * @param WC_Cart $cart Cart object. * @param int $sampled_id Sampled ID. * @return int */ function tryv_cart_sample_qty( $cart, $sampled_id ) { $total = 0; foreach ( $cart->get_cart() as $item ) { if ( empty( $item['tryv_sample'] ) ) { continue; } if ( in_array( absint( $sampled_id ), tryv_cart_item_identities( $item ), true ) ) { $total += absint( $item['quantity'] ); } } return $total; } /** * Does the cart mix sample and non-sample items? * * @param WC_Cart $cart Cart object. * @return bool */ function tryv_cart_is_mixed( $cart ) { $has_sample = false; $has_non_sample = false; foreach ( $cart->get_cart() as $item ) { if ( ! empty( $item['tryv_sample'] ) ) { $has_sample = true; } else { $has_non_sample = true; } } return $has_sample && $has_non_sample; } /** * Validate adding a sample to the cart: enabled, guest rule, no mixing, * per-order maximum, and per-customer maximum. For a pack, every * identity (pack + each component) is checked against its own * maxima, using that identity's limit values. Guests are checked * again at checkout, once an email is known; logged-in shoppers are * checked here with both user ID and any known billing email. * * @param bool $passed Validation so far. * @param int $product_id Product ID. * @param int $quantity Quantity requested. * @param int $variation_id Variation ID. * @return bool */ function tryv_validate_add_to_cart( $passed, $product_id, $quantity, $variation_id = 0 ) { if ( ! tryv_is_sample_request() ) { // Normal adds are not blocked here. Mixing is caught at // checkout by tryv_validate_cart, and a sample add into a // non-sample cart is blocked below. return $passed; } if ( ! tryv_verify_sample_nonce() ) { wc_add_notice( __( 'The sample request could not be verified. Please try again.', 'tryvertising' ), 'error' ); return false; } $product = wc_get_product( $product_id ); $settings = tryv_get_settings(); if ( 'yes' !== $settings['enabled'] ) { wc_add_notice( __( 'Samples are currently unavailable.', 'tryvertising' ), 'error' ); return false; } if ( ! $product || ! tryv_product_sample_enabled( $product ) ) { if ( $product && tryv_product_is_pack( $product ) ) { wc_add_notice( __( 'This swatch pack has no available components, so it cannot be ordered as a sample.', 'tryvertising' ), 'error' ); } else { wc_add_notice( __( 'Samples are not available for this product.', 'tryvertising' ), 'error' ); } return false; } if ( ! is_user_logged_in() && 'yes' !== $settings['allow_guests'] ) { wc_add_notice( __( 'Log in to order a sample.', 'tryvertising' ), 'error' ); return false; } if ( WC()->cart ) { foreach ( WC()->cart->get_cart() as $item ) { if ( empty( $item['tryv_sample'] ) ) { wc_add_notice( __( 'Samples must be ordered on their own, not mixed with full products. Please check out your current cart first, or empty it.', 'tryvertising' ), 'error' ); return false; } } } $is_pack = tryv_product_is_pack( $product ); if ( $is_pack ) { $identities = array_values( array_unique( array_merge( array( absint( $product_id ) ), tryv_pack_valid_component_ids( $product ) ) ) ); } else { $identities = array( tryv_sampled_id( $product_id, $variation_id ) ); } $identity = tryv_customer_identity_for_cart(); $user_id = $identity['user_id']; $email = $identity['email']; $qty_wanted = absint( $quantity ); foreach ( $identities as $sampled_id ) { if ( $is_pack && absint( $sampled_id ) !== absint( $product_id ) ) { $limit_product = wc_get_product( $sampled_id ); } else { $limit_product = $product; } if ( ! $limit_product ) { continue; } $max_order = tryv_resolve_limit( $limit_product, '_tryv_sample_max_order', 'default_max_order' ); if ( $max_order > 0 && WC()->cart ) { $in_cart = tryv_cart_sample_qty( WC()->cart, $sampled_id ); if ( $in_cart + $qty_wanted > $max_order ) { wc_add_notice( sprintf( /* translators: %1$d maximum, %2$s product name */ __( 'You can order at most %1$d samples of %2$s per order.', 'tryvertising' ), $max_order, tryv_identity_name( $sampled_id ) ), 'error' ); return false; } } $max_customer = tryv_resolve_limit( $limit_product, '_tryv_sample_max_customer', 'default_max_customer' ); if ( $max_customer > 0 && ( $user_id > 0 || '' !== $email ) ) { $prior = tryv_prior_sample_usage( $sampled_id, $user_id, $email ); $in_cart = WC()->cart ? tryv_cart_sample_qty( WC()->cart, $sampled_id ) : 0; if ( $prior['qty'] + $in_cart + $qty_wanted > $max_customer ) { wc_add_notice( sprintf( /* translators: %1$d maximum, %2$s product name */ __( 'You can order at most %1$d samples of %2$s in total.', 'tryvertising' ), $max_customer, tryv_identity_name( $sampled_id ) ), 'error' ); return false; } } } return $passed; } /** * Apply tiered sample pricing across the cart, in cart order. * Free allowance is per sampled identity, per customer. For a pack, * the free quantity is the smallest remaining free allowance across * the pack and its components, and that quantity is consumed against * every identity, so a second pack (or a single component sample) * sees the reduced allowance. * * The sample price is recomputed from the current product/variation * on every pricing run (QC minor: stale sample price). A price * changed after the item was added to the cart therefore applies at * checkout, and the stored cart price is updated to match. * If samples are globally disabled, no sample pricing is applied at * all — in-flight sample carts are blocked at checkout by * tryv_validate_cart, and must not keep a free price in the meantime. * * @param WC_Cart $cart Cart object. */ function tryv_apply_sample_pricing( $cart ) { if ( is_admin() && ! defined( 'DOING_AJAX' ) ) { return; } if ( ! $cart || 0 === count( $cart->get_cart() ) ) { return; } $settings = tryv_get_settings(); if ( 'yes' !== $settings['enabled'] ) { return; } $identity = tryv_customer_identity_for_cart(); $user_id = $identity['user_id']; $email = $identity['email']; $free_used_in_cart = array(); foreach ( $cart->get_cart() as $key => $item ) { if ( empty( $item['tryv_sample'] ) ) { continue; } $product = wc_get_product( $item['product_id'] ); if ( ! $product || ! tryv_product_sample_enabled( $product ) ) { // No longer eligible: do not price it as a sample. // Checkout validation blocks the cart in this state. continue; } $identities = tryv_cart_item_identities( $item ); if ( empty( $identities ) ) { continue; } // Current price, not the price stored at add-to-cart time. $variation_id = isset( $item['variation_id'] ) ? absint( $item['variation_id'] ) : 0; $price = tryv_sample_price_for( absint( $item['product_id'] ), $variation_id ); $qty = absint( $item['quantity'] ); // Most restrictive remaining free allowance across identities. $remaining = null; foreach ( $identities as $sampled_id ) { $limit_product = tryv_identity_limit_product( $item, $sampled_id ); if ( ! $limit_product ) { continue; } $free_allow = tryv_resolve_limit( $limit_product, '_tryv_sample_free', 'default_free' ); $prior = tryv_prior_sample_usage( $sampled_id, $user_id, $email ); $used = isset( $free_used_in_cart[ $sampled_id ] ) ? $free_used_in_cart[ $sampled_id ] : 0; $this_remaining = max( 0, $free_allow - $prior['free_qty'] - $used ); $remaining = null === $remaining ? $this_remaining : min( $remaining, $this_remaining ); } if ( null === $remaining ) { $remaining = 0; } $free_qty = min( $qty, $remaining ); $paid_qty = $qty - $free_qty; $unit_price = $qty > 0 ? ( $paid_qty * $price ) / $qty : 0; $item['data']->set_price( $unit_price ); $cart->cart_contents[ $key ]['tryv_sample_price'] = $price; $cart->cart_contents[ $key ]['tryv_free_qty'] = $free_qty; $cart->cart_contents[ $key ]['tryv_paid_qty'] = $paid_qty; foreach ( $identities as $sampled_id ) { $free_used_in_cart[ $sampled_id ] = ( isset( $free_used_in_cart[ $sampled_id ] ) ? $free_used_in_cart[ $sampled_id ] : 0 ) + $free_qty; } } } /** * Show sample information on cart and checkout lines. * * @param array $item_data Existing display data. * @param array $cart_item Cart item. * @return array */ function tryv_display_cart_item_data( $item_data, $cart_item ) { if ( empty( $cart_item['tryv_sample'] ) ) { return $item_data; } if ( ! empty( $cart_item['tryv_is_pack'] ) ) { $item_data[] = array( 'key' => __( 'Swatch pack', 'tryvertising' ), 'value' => __( 'Sample pack — counts as a sample of each component product.', 'tryvertising' ), ); } else { $item_data[] = array( 'key' => __( 'Sample', 'tryvertising' ), 'value' => __( 'Sample item', 'tryvertising' ), ); } if ( isset( $cart_item['tryv_free_qty'] ) && isset( $cart_item['tryv_paid_qty'] ) ) { $free_qty = absint( $cart_item['tryv_free_qty'] ); $paid_qty = absint( $cart_item['tryv_paid_qty'] ); if ( $free_qty > 0 && $paid_qty > 0 ) { $item_data[] = array( 'key' => __( 'Sample pricing', 'tryvertising' ), 'value' => sprintf( /* translators: %1$d free units, %2$d paid units */ __( '%1$d free, %2$d paid', 'tryvertising' ), $free_qty, $paid_qty ), ); } elseif ( $free_qty > 0 ) { $item_data[] = array( 'key' => __( 'Sample pricing', 'tryvertising' ), 'value' => __( 'Free sample', 'tryvertising' ), ); } } return $item_data; } /** * Re-validate the whole cart at checkout: no mixing, global enable, * per-order and per-customer maximums, using both user ID and * billing email for the customer identity (so guests and logged-in * shoppers are not split). For a pack, every identity is checked. * This is the second validation path (the first is add-to-cart); * both now use the same statuses, the same identity expansion, and * the same union identity — see the file header (QC fix M3). */ function tryv_validate_cart() { if ( ! WC()->cart ) { return; } $cart = WC()->cart; $settings = tryv_get_settings(); $has_sample = false; foreach ( $cart->get_cart() as $item ) { if ( ! empty( $item['tryv_sample'] ) ) { $has_sample = true; } } if ( ! $has_sample ) { return; } if ( tryv_cart_is_mixed( $cart ) ) { wc_add_notice( __( 'Samples must be ordered on their own, not mixed with full products.', 'tryvertising' ), 'error' ); } if ( 'yes' !== $settings['enabled'] ) { // Global disable invalidates in-flight sample carts: they // cannot check out until samples are re-enabled or the // sample items are removed. Pricing is also not applied // while disabled (see tryv_apply_sample_pricing). wc_add_notice( __( 'Samples are currently unavailable. Please remove the sample items from your cart.', 'tryvertising' ), 'error' ); return; } if ( ! is_user_logged_in() && 'yes' !== $settings['allow_guests'] ) { wc_add_notice( __( 'Log in to order a sample.', 'tryvertising' ), 'error' ); return; } $identity = tryv_customer_identity_for_cart(); $user_id = $identity['user_id']; $email = $identity['email']; $qty_by_identity = array(); $limit_product_by_identity = array(); foreach ( $cart->get_cart() as $item ) { if ( empty( $item['tryv_sample'] ) ) { continue; } $product = wc_get_product( $item['product_id'] ); if ( ! $product || ! tryv_product_sample_enabled( $product ) ) { if ( $product && tryv_product_is_pack( $product ) ) { wc_add_notice( __( 'One of the swatch packs in your cart has no available components. Please remove it and try again.', 'tryvertising' ), 'error' ); } else { wc_add_notice( __( 'One of the samples in your cart is no longer available. Please remove it and try again.', 'tryvertising' ), 'error' ); } } foreach ( tryv_cart_item_identities( $item ) as $sampled_id ) { $qty_by_identity[ $sampled_id ] = ( isset( $qty_by_identity[ $sampled_id ] ) ? $qty_by_identity[ $sampled_id ] : 0 ) + absint( $item['quantity'] ); if ( ! isset( $limit_product_by_identity[ $sampled_id ] ) ) { $limit_product_by_identity[ $sampled_id ] = tryv_identity_limit_product( $item, $sampled_id ); } } } foreach ( $qty_by_identity as $sampled_id => $qty ) { $product = isset( $limit_product_by_identity[ $sampled_id ] ) ? $limit_product_by_identity[ $sampled_id ] : null; if ( ! $product ) { continue; } $max_order = tryv_resolve_limit( $product, '_tryv_sample_max_order', 'default_max_order' ); $max_customer = tryv_resolve_limit( $product, '_tryv_sample_max_customer', 'default_max_customer' ); if ( $max_order > 0 && $qty > $max_order ) { wc_add_notice( sprintf( /* translators: %1$d maximum, %2$s product name */ __( 'You can order at most %1$d samples of %2$s per order.', 'tryvertising' ), $max_order, tryv_identity_name( $sampled_id ) ), 'error' ); } if ( $max_customer > 0 && ( $user_id > 0 || '' !== $email ) ) { $prior = tryv_prior_sample_usage( $sampled_id, $user_id, $email ); if ( $prior['qty'] + $qty > $max_customer ) { wc_add_notice( sprintf( /* translators: %1$d maximum, %2$s product name */ __( 'You can order at most %1$d samples of %2$s in total.', 'tryvertising' ), $max_customer, tryv_identity_name( $sampled_id ) ), 'error' ); } } } } /** * Copy sample data onto the order line item. * * @param WC_Order_Item_Product $item Order line item. * @param string $cart_item_key Cart item key. * @param array $values Cart item values. * @param WC_Order $order Order object. */ function tryv_add_order_line_meta( $item, $cart_item_key, $values, $order ) { if ( empty( $values['tryv_sample'] ) ) { return; } $item->add_meta_data( '_tryv_sample', 'yes', true ); $sampled_ids = array(); if ( isset( $values['tryv_sampled_ids'] ) && is_array( $values['tryv_sampled_ids'] ) ) { $sampled_ids = array_map( 'absint', $values['tryv_sampled_ids'] ); } elseif ( isset( $values['tryv_sampled_id'] ) ) { $sampled_ids = array( absint( $values['tryv_sampled_id'] ) ); } if ( ! empty( $values['tryv_is_pack'] ) && isset( $values['product_id'] ) ) { $sampled_ids[] = absint( $values['product_id'] ); } $sampled_ids = array_values( array_unique( array_filter( $sampled_ids ) ) ); $item->add_meta_data( '_tryv_sampled_ids', implode( ',', $sampled_ids ), true ); $item->add_meta_data( '_tryv_sample_free_qty', isset( $values['tryv_free_qty'] ) ? absint( $values['tryv_free_qty'] ) : 0, true ); $item->add_meta_data( '_tryv_sample_paid_qty', isset( $values['tryv_paid_qty'] ) ? absint( $values['tryv_paid_qty'] ) : absint( $values['quantity'] ), true ); $item->add_meta_data( '_tryv_sample_price', isset( $values['tryv_sample_price'] ) ? (float) $values['tryv_sample_price'] : 0, true ); if ( ! empty( $values['tryv_is_pack'] ) ) { $item->add_meta_data( '_tryv_is_pack', 'yes', true ); $item->add_meta_data( '_tryv_pack_id', isset( $values['product_id'] ) ? absint( $values['product_id'] ) : 0, true ); } } /** * Flag a sample-only order and record its sampled products. * For a pack this includes the pack ID and its components; the * credit/report code filters the pack ID back out (see * tryv_line_sampled_ids), so this flag is for limit accounting. * * @param WC_Order $order Order object. * @param array $data Checkout data. */ function tryv_flag_sample_order( $order, $data ) { if ( ! WC()->cart ) { return; } $sampled = array(); foreach ( WC()->cart->get_cart() as $item ) { if ( empty( $item['tryv_sample'] ) ) { continue; } $ids = tryv_cart_item_identities( $item ); foreach ( $ids as $id ) { $sampled[] = absint( $id ); } } if ( empty( $sampled ) ) { return; } $order->update_meta_data( '_tryv_sample_order', 'yes' ); $order->update_meta_data( '_tryv_sample_products', implode( ',', array_values( array_unique( $sampled ) ) ) ); }