diff --git a/includes/Generator/Order.php b/includes/Generator/Order.php index fac1e57..29418ce 100644 --- a/includes/Generator/Order.php +++ b/includes/Generator/Order.php @@ -33,6 +33,60 @@ class Order extends Generator { */ const SECOND_REFUND_MAX_DAYS = 30; + /** + * Refund type constants for memory-efficient batch operations. + */ + const REFUND_TYPE_NONE = 0; + const REFUND_TYPE_FULL = 1; + const REFUND_TYPE_PARTIAL = 2; + const REFUND_TYPE_MULTI = 3; + + /** + * Refund distribution ratios for batch generation with exact ratios. + * When generating refunds in batch mode: + * - 50% will be full refunds + * - 25% will be single partial refunds + * - 25% will be multi-partial refunds (two partial refunds) + */ + const REFUND_DISTRIBUTION_FULL_RATIO = 0.5; + const REFUND_DISTRIBUTION_PARTIAL_RATIO = 0.25; + + /** + * Maximum batch size for exact ratio distribution using pre-generated arrays. + * Above this threshold, falls back to probabilistic approach to manage memory usage. + */ + const EXACT_RATIO_BATCH_THRESHOLD = 10000; + + /** + * Pre-generated coupon flags for exact ratio distribution in batch mode. + * Each element is a boolean: true = apply coupon, false = skip. + * + * @var array|null + */ + protected static $batch_coupon_flags = null; + + /** + * Current index in the batch_coupon_flags array. + * + * @var int + */ + protected static $batch_coupon_index = 0; + + /** + * Pre-generated refund flags for exact ratio distribution in batch mode. + * Each element is an integer constant: REFUND_TYPE_NONE, REFUND_TYPE_FULL, etc. + * + * @var array|null + */ + protected static $batch_refund_flags = null; + + /** + * Current index in the batch_refund_flags array. + * + * @var int + */ + protected static $batch_refund_index = 0; + /** * Return a new order. * @@ -121,20 +175,49 @@ public static function generate( $save = true, $assoc_args = array() ) { // Handle --coupon-ratio parameter if ( isset( $assoc_args['coupon-ratio'] ) ) { - $coupon_ratio = floatval( $assoc_args['coupon-ratio'] ); + // Use exact ratio flag if in batch mode + if ( null !== self::$batch_coupon_flags ) { + // Validate index is within bounds + if ( self::$batch_coupon_index < count( self::$batch_coupon_flags ) ) { + $include_coupon = self::$batch_coupon_flags[ self::$batch_coupon_index ]; + self::$batch_coupon_index++; + } else { + // Index exceeded array bounds - log error and fall back to probabilistic + error_log( + sprintf( + 'Coupon batch index (%d) exceeded array size (%d). Falling back to probabilistic mode. This may indicate generate() was called more times than expected.', + self::$batch_coupon_index, + count( self::$batch_coupon_flags ) + ) + ); + // Fall back to probabilistic - continue to else block + $coupon_ratio = floatval( $assoc_args['coupon-ratio'] ); + $coupon_ratio = max( 0.0, min( 1.0, $coupon_ratio ) ); + if ( $coupon_ratio >= 1.0 ) { + $include_coupon = true; + } elseif ( $coupon_ratio > 0 && wp_rand( 1, 100 ) <= ( $coupon_ratio * 100 ) ) { + $include_coupon = true; + } else { + $include_coupon = false; + } + } + } else { + // Fall back to probabilistic approach for single order generation + $coupon_ratio = floatval( $assoc_args['coupon-ratio'] ); - // Validate ratio is between 0.0 and 1.0 - if ( $coupon_ratio < 0.0 || $coupon_ratio > 1.0 ) { - $coupon_ratio = max( 0.0, min( 1.0, $coupon_ratio ) ); - } + // Validate ratio is between 0.0 and 1.0 + if ( $coupon_ratio < 0.0 || $coupon_ratio > 1.0 ) { + $coupon_ratio = max( 0.0, min( 1.0, $coupon_ratio ) ); + } - // Apply coupon based on ratio - if ( $coupon_ratio >= 1.0 ) { - $include_coupon = true; - } elseif ( $coupon_ratio > 0 && wp_rand( 1, 100 ) <= ( $coupon_ratio * 100 ) ) { - $include_coupon = true; - } else { - $include_coupon = false; + // Apply coupon based on ratio + if ( $coupon_ratio >= 1.0 ) { + $include_coupon = true; + } elseif ( $coupon_ratio > 0 && wp_rand( 1, 100 ) <= ( $coupon_ratio * 100 ) ) { + $include_coupon = true; + } else { + $include_coupon = false; + } } } @@ -180,30 +263,68 @@ public static function generate( $save = true, $assoc_args = array() ) { // Handle --refund-ratio parameter for completed orders if ( isset( $assoc_args['refund-ratio'] ) && 'completed' === $status ) { - $refund_ratio = floatval( $assoc_args['refund-ratio'] ); - - // Validate ratio is between 0.0 and 1.0 - if ( $refund_ratio < 0.0 || $refund_ratio > 1.0 ) { - $refund_ratio = max( 0.0, min( 1.0, $refund_ratio ) ); - } + $refund_type = self::REFUND_TYPE_NONE; + + // Use exact ratio flag if in batch mode + if ( null !== self::$batch_refund_flags ) { + // Validate index is within bounds + if ( self::$batch_refund_index < count( self::$batch_refund_flags ) ) { + $refund_type = self::$batch_refund_flags[ self::$batch_refund_index ]; + self::$batch_refund_index++; + } else { + // Index exceeded array bounds - log error and fall back to probabilistic + error_log( + sprintf( + 'Refund batch index (%d) exceeded array size (%d). Falling back to probabilistic mode. This may indicate generate() was called more times than expected.', + self::$batch_refund_index, + count( self::$batch_refund_flags ) + ) + ); + // Fall back to probabilistic + $refund_ratio = floatval( $assoc_args['refund-ratio'] ); + $refund_ratio = max( 0.0, min( 1.0, $refund_ratio ) ); + if ( $refund_ratio >= 1.0 ) { + $refund_type = self::REFUND_TYPE_FULL; + } elseif ( $refund_ratio > 0 && wp_rand( 1, 100 ) <= ( $refund_ratio * 100 ) ) { + $refund_type = wp_rand( 0, 1 ) ? self::REFUND_TYPE_FULL : self::REFUND_TYPE_PARTIAL; + if ( self::REFUND_TYPE_PARTIAL === $refund_type && wp_rand( 1, 100 ) <= self::SECOND_REFUND_PROBABILITY ) { + $refund_type = self::REFUND_TYPE_MULTI; + } + } + } + } else { + // Fall back to probabilistic approach for single order generation + $refund_ratio = floatval( $assoc_args['refund-ratio'] ); - $should_refund = false; + // Validate ratio is between 0.0 and 1.0 + if ( $refund_ratio < 0.0 || $refund_ratio > 1.0 ) { + $refund_ratio = max( 0.0, min( 1.0, $refund_ratio ) ); + } - if ( $refund_ratio >= 1.0 ) { - // Always refund if ratio is 1.0 or higher - $should_refund = true; - } elseif ( $refund_ratio > 0 && wp_rand( 1, 100 ) <= ( $refund_ratio * 100 ) ) { - // Use random chance for ratios between 0 and 1 - $should_refund = true; + if ( $refund_ratio >= 1.0 ) { + // Always refund if ratio is 1.0 or higher + $refund_type = self::REFUND_TYPE_FULL; + } elseif ( $refund_ratio > 0 && wp_rand( 1, 100 ) <= ( $refund_ratio * 100 ) ) { + // Use random chance for ratios between 0 and 1 + // Split evenly between full and partial + $refund_type = wp_rand( 0, 1 ) ? self::REFUND_TYPE_FULL : self::REFUND_TYPE_PARTIAL; + + // 25% chance for multi-partial + if ( self::REFUND_TYPE_PARTIAL === $refund_type && wp_rand( 1, 100 ) <= self::SECOND_REFUND_PROBABILITY ) { + $refund_type = self::REFUND_TYPE_MULTI; + } + } } - if ( $should_refund ) { - // Create first refund with date within 2 months of completion - $first_refund = self::create_refund( $order ); - - // Some partial refunds get a second refund (always partial) - if ( $first_refund && is_object( $first_refund ) && wp_rand( 1, 100 ) <= self::SECOND_REFUND_PROBABILITY ) { - self::create_refund( $order, true, $first_refund ); + // Process refund based on type + if ( self::REFUND_TYPE_FULL === $refund_type ) { + self::create_refund( $order, false, null, true ); // Explicitly full + } elseif ( self::REFUND_TYPE_PARTIAL === $refund_type ) { + self::create_refund( $order, true, null, false ); // Explicitly partial + } elseif ( self::REFUND_TYPE_MULTI === $refund_type ) { + $first_refund = self::create_refund( $order, true, null, false ); // Explicitly partial + if ( $first_refund && is_object( $first_refund ) ) { + self::create_refund( $order, true, $first_refund, false ); // Explicitly partial } } } @@ -236,6 +357,9 @@ public static function batch( $amount, array $args = array() ) { return $amount; } + // Initialize exact ratio flags for deterministic distribution + self::init_ratio_flags( $amount, $args ); + $order_ids = array(); for ( $i = 1; $i <= $amount; $i ++ ) { @@ -247,6 +371,9 @@ public static function batch( $amount, array $args = array() ) { $order_ids[] = $order->get_id(); } + // Clear ratio flags after batch generation + self::clear_ratio_flags(); + return $order_ids; } @@ -258,8 +385,8 @@ public static function batch( $amount, array $args = array() ) { public static function get_customer() { global $wpdb; - $guest = (bool) wp_rand( 0, 1 ); - $existing = (bool) wp_rand( 0, 1 ); + $guest = wp_rand( 0, 1 ); + $existing = wp_rand( 0, 1 ); if ( $existing ) { $total_users = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->users}" ); @@ -443,11 +570,12 @@ protected static function get_or_create_coupon() { * Create a refund for an order (either full or partial). * * @param \WC_Order $order The order to refund. - * @param bool $force_partial Force partial refund only. + * @param bool $force_partial Force partial refund only (legacy parameter). * @param \WC_Order_Refund|null $previous_refund Previous refund to base date on (for second refunds). + * @param bool|null $force_full Explicitly force full refund (overrides random logic). * @return \WC_Order_Refund|false Refund object on success, false on failure. */ - protected static function create_refund( $order, $force_partial = false, $previous_refund = null ) { + protected static function create_refund( $order, $force_partial = false, $previous_refund = null, $force_full = null ) { if ( ! $order instanceof \WC_Order ) { error_log( "Error: Order is not an instance of \WC_Order: " . print_r( $order, true ) ); return false; @@ -457,13 +585,20 @@ protected static function create_refund( $order, $force_partial = false, $previo $existing_refunds = $order->get_refunds(); if ( ! empty( $existing_refunds ) ) { $force_partial = true; + $force_full = false; // Can't do full refund if already has refunds } // Calculate already refunded quantities $refunded_qty_by_item = self::calculate_refunded_quantities( $existing_refunds ); // Determine refund type (full or partial) - $is_full_refund = $force_partial ? false : (bool) wp_rand( 0, 1 ); + if ( null !== $force_full ) { + // Explicit full/partial specified (batch mode with exact ratios) + $is_full_refund = $force_full; + } else { + // Legacy random logic (single order generation or old code) + $is_full_refund = $force_partial ? false : wp_rand( 0, 1 ); + } // Build refund line items $line_items = $is_full_refund @@ -665,7 +800,7 @@ protected static function build_partial_refund_items( $order, $refunded_qty_by_i $line_items = array(); // Decide whether to refund full items or partial quantities - $refund_full_items = (bool) wp_rand( 0, 1 ); + $refund_full_items = wp_rand( 0, 1 ); if ( $refund_full_items && count( $items ) > 2 ) { // Refund a random subset of items completely (requires at least 3 items) @@ -790,4 +925,93 @@ protected static function calculate_refund_date( $order, $previous_refund = null $random_days = wp_rand( 0, $max_days ); return date( 'Y-m-d H:i:s', strtotime( $base_date->date( 'Y-m-d H:i:s' ) ) + ( $random_days * DAY_IN_SECONDS ) ); } + + /** + * Initialize exact ratio flags for batch generation. + * Creates pre-generated arrays for exact distribution of coupons and refunds. + * + * Memory Considerations: + * - Pre-generating arrays ensures exact ratio distribution but consumes memory + * - For batches > EXACT_RATIO_BATCH_THRESHOLD (10,000), falls back to probabilistic approach + * - Typical memory usage: ~100-150 bytes per element (PHP array overhead) + * - Example: 10,000 orders at 0.5 ratio ≈ 1-2MB per flag array + * + * @param int $count Number of orders to generate. + * @param array $args Arguments containing ratio parameters. + * @return void + */ + protected static function init_ratio_flags( $count, $args ) { + // Reset indices to 0 for new batch + self::$batch_coupon_index = 0; + self::$batch_refund_index = 0; + + // For large batches above threshold, skip exact ratio and use probabilistic approach + if ( $count > self::EXACT_RATIO_BATCH_THRESHOLD ) { + if ( class_exists( 'WP_CLI' ) ) { + \WP_CLI::log( + sprintf( + 'Batch size (%d) exceeds threshold (%d). Using probabilistic distribution instead of exact ratios to optimize memory usage.', + $count, + self::EXACT_RATIO_BATCH_THRESHOLD + ) + ); + } + return; + } + + // Initialize coupon flags if coupon-ratio is set + if ( isset( $args['coupon-ratio'] ) ) { + $coupon_ratio = floatval( $args['coupon-ratio'] ); + $coupon_ratio = max( 0.0, min( 1.0, $coupon_ratio ) ); + + $num_with_coupons = (int) round( $count * $coupon_ratio ); + $num_without = $count - $num_with_coupons; + + // Create array with exact counts + self::$batch_coupon_flags = array_merge( + array_fill( 0, $num_with_coupons, true ), + array_fill( 0, $num_without, false ) + ); + + // Shuffle for randomness + shuffle( self::$batch_coupon_flags ); + } + + // Initialize refund flags if refund-ratio is set and status is completed + if ( isset( $args['refund-ratio'] ) && 'completed' === ( $args['status'] ?? '' ) ) { + $refund_ratio = floatval( $args['refund-ratio'] ); + $refund_ratio = max( 0.0, min( 1.0, $refund_ratio ) ); + + $total_refunds = (int) round( $count * $refund_ratio ); + + // Split refunds: 50% full, 25% single partial, 25% multi-partial + $num_full = (int) floor( $total_refunds * self::REFUND_DISTRIBUTION_FULL_RATIO ); + $num_partial = (int) floor( $total_refunds * self::REFUND_DISTRIBUTION_PARTIAL_RATIO ); + $num_multi = $total_refunds - $num_full - $num_partial; // Remainder goes to multi + $num_none = $count - $total_refunds; + + // Create array with exact counts using integer constants for memory efficiency + self::$batch_refund_flags = array_merge( + array_fill( 0, $num_full, self::REFUND_TYPE_FULL ), + array_fill( 0, $num_partial, self::REFUND_TYPE_PARTIAL ), + array_fill( 0, $num_multi, self::REFUND_TYPE_MULTI ), + array_fill( 0, $num_none, self::REFUND_TYPE_NONE ) + ); + + // Shuffle for randomness + shuffle( self::$batch_refund_flags ); + } + } + + /** + * Clear ratio flags after batch generation is complete. + * + * @return void + */ + protected static function clear_ratio_flags() { + self::$batch_coupon_flags = null; + self::$batch_coupon_index = 0; + self::$batch_refund_flags = null; + self::$batch_refund_index = 0; + } }