Skip to content
Open
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
161c802
Add --coupon-ratio parameter to order generator
layoutd Oct 22, 2025
df0fda0
Add --refund-ratio parameter to order generator
layoutd Oct 22, 2025
4d3eb9d
Improve refund logic to properly handle line items and fees
layoutd Oct 22, 2025
4dd4a96
Fix refund tax calculation to properly handle tax rate IDs
layoutd Oct 22, 2025
84a81bc
Calculate explicit refund amount from line items
layoutd Oct 22, 2025
368e5b7
Fix refund ratio logic and add multiple refund support
layoutd Oct 22, 2025
479a1a8
Update partial refund reason to show products and items
layoutd Oct 22, 2025
4f98a67
Force partial refunds for orders with existing refunds
layoutd Oct 24, 2025
a3e1536
Recalculate order totals after applying coupon
layoutd Oct 24, 2025
74ae8e1
Update includes/Generator/Order.php
layoutd Oct 24, 2025
e2494ec
Doc update
layoutd Oct 24, 2025
ff277e2
Fix array_rand edge case for orders with exactly 2 items
layoutd Oct 24, 2025
f80b01e
Refactor coupon creation to use Coupon::generate()
layoutd Oct 24, 2025
ee9ff32
Move coupon retrieval logic to Coupon::get_random()
layoutd Oct 24, 2025
07daacb
Use WordPress get_posts() API instead of raw SQL queries
layoutd Oct 24, 2025
57daab8
Add discount_type parameter to Coupon generator
layoutd Oct 24, 2025
24b2493
Refactor Order generator to use Coupon::batch()
layoutd Oct 24, 2025
f8d718c
Add discount_type parameter to CLI coupon command
layoutd Oct 24, 2025
080ee99
Update README with new coupon and order parameters
layoutd Oct 24, 2025
b9c85f9
Clarify that refunds are split evenly between partial and full
layoutd Oct 24, 2025
86a499c
Fix backwards compatibility: only set discount_type when explicitly p…
layoutd Oct 24, 2025
2c9940e
Add input validation for coupon-ratio and refund-ratio parameters
layoutd Oct 24, 2025
5b53925
Add performance comment for get_posts() in Coupon::get_random()
layoutd Oct 24, 2025
0816747
Clarify that --coupons flag is equivalent to --coupon-ratio=1.0
layoutd Oct 24, 2025
0f5dd77
Fix ratio probability calculation using integer-based random generation
layoutd Oct 24, 2025
f69d126
Add check to prevent refunds with empty line items
layoutd Oct 24, 2025
73cdb1b
Improve refund error handling and logging
layoutd Oct 24, 2025
ae9b6a6
Add validation to prevent refunds with invalid amounts
layoutd Oct 24, 2025
ac5f820
Fix refund amount validation to prevent exceeding order total
layoutd Oct 24, 2025
0b3faa5
Fix refund amount calculations to prevent rounding errors and ensure …
layoutd Oct 24, 2025
9894e7e
Fix partial refunds to track already-refunded quantities and prevent …
layoutd Oct 24, 2025
c6d22b4
Fix code quality issues in refund calculations
layoutd Oct 24, 2025
323a1a7
Add realistic date handling for refunds
layoutd Oct 24, 2025
5734f42
Refactor refund generation: extract constants and helper methods
layoutd Oct 27, 2025
f08714c
Make default 'fixed_cart' Coupon generator explicit
layoutd Oct 28, 2025
20f415b
Clarify memory impact
layoutd Oct 28, 2025
553d847
Clarify that decimal ratios for coupons and refunds are converted to …
layoutd Oct 28, 2025
e17884b
Add error logging for order generation and coupon application failures
layoutd Oct 28, 2025
36d534e
Add exact ratio distribution for deterministic coupon and refund gene…
layoutd Oct 27, 2025
09da72f
Use integer constants instead of strings for refund types
layoutd Oct 27, 2025
dc91322
Fix refund type randomness to respect explicit full/partial flags
layoutd Oct 27, 2025
6e143d9
PHPCS
layoutd Oct 30, 2025
b91c278
Simplification
layoutd Oct 30, 2025
9d62477
Extract refund distribution ratios as named constants
layoutd Oct 30, 2025
201d302
Replace array_shift with index counters to prevent state mutation
layoutd Oct 30, 2025
db93ab6
Remove unnecessary (bool) casts from wp_rand calls
layoutd Oct 30, 2025
a033f04
Use floor() instead of round() to avoid double rounding in refund dis…
layoutd Oct 30, 2025
da23c52
Simplify status check in init_ratio_flags using null coalescing
layoutd Oct 30, 2025
91f6f06
Add batch size threshold constant for memory management
layoutd Oct 30, 2025
eeed7d5
Implement threshold-based fallback for large batch operations
layoutd Oct 30, 2025
f699246
Add documentation and user notification for memory-based fallback
layoutd Oct 30, 2025
65ba0a0
Add explicit bounds checking and error logging for batch index arrays
layoutd Oct 30, 2025
a02ee68
Merge remote-tracking branch 'origin/trunk' into exact-ratio-distribu…
layoutd Oct 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
300 changes: 262 additions & 38 deletions includes/Generator/Order.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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;
}
}
}

Expand Down Expand Up @@ -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
}
}
}
Expand Down Expand Up @@ -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 ++ ) {
Expand All @@ -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;
}

Expand All @@ -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}" );
Expand Down Expand Up @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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;
}
}