Skip to content

Commit a314f05

Browse files
layoutdCopilotclaudechihsuan
authored
Add coupon and refund ratio parameters to order generator (#182)
* Add --coupon-ratio parameter to order generator * Add --refund-ratio parameter to order generator * Improve refund logic to properly handle line items and fees * Fix refund tax calculation to properly handle tax rate IDs * Calculate explicit refund amount from line items * Fix refund ratio logic and add multiple refund support * Update partial refund reason to show products and items * Force partial refunds for orders with existing refunds * Recalculate order totals after applying coupon * Update includes/Generator/Order.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Doc update Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Fix array_rand edge case for orders with exactly 2 items * Refactor coupon creation to use Coupon::generate() * Move coupon retrieval logic to Coupon::get_random() * Use WordPress get_posts() API instead of raw SQL queries * Add discount_type parameter to Coupon generator * Refactor Order generator to use Coupon::batch() * Add discount_type parameter to CLI coupon command * Update README with new coupon and order parameters * Clarify that refunds are split evenly between partial and full * Fix backwards compatibility: only set discount_type when explicitly provided * Add input validation for coupon-ratio and refund-ratio parameters * Add performance comment for get_posts() in Coupon::get_random() * Clarify that --coupons flag is equivalent to --coupon-ratio=1.0 * Fix ratio probability calculation using integer-based random generation * Add check to prevent refunds with empty line items * Improve refund error handling and logging - Add detailed logging when orders cannot be refunded due to empty line items - Consolidate refund creation error logs into single formatted message - Add error logging for invalid order instance check These improvements will help diagnose why some completed orders aren't receiving refunds when --refund-ratio=1.0 is specified. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Add validation to prevent refunds with invalid amounts Adds check to ensure refund amount is greater than 0 before calling wc_create_refund(). This prevents "Invalid refund amount" errors that occur when: - Orders have 100% discount coupons (total = $0) - Line items have $0 totals - Calculation results in 0 or negative amount Logs order ID, calculated amount, and order total when skipping refund. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Fix refund amount validation to prevent exceeding order total The "Invalid refund amount" error occurred because calculated refund amounts slightly exceeded the available order total due to rounding errors in tax calculations. Changes: - Calculate maximum refundable amount (order total - already refunded) - Cap refund amount to maximum available before calling wc_create_refund() - Round both calculated refund and max refund to 2 decimal places - Improve error logging to show order total and already refunded amounts Example of the issue: - Order total: $24851.03 - Calculated refund (with 3 decimal tax): $24851.04 - Result: $0.01 over limit → "Invalid refund amount" error This fix ensures refunds never exceed the mathematically available amount, preventing WooCommerce validation errors. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Fix refund amount calculations to prevent rounding errors and ensure partial refunds stay under 50% For full refunds, use the order's actual total instead of summing line items to avoid rounding discrepancies that created tiny 0.01 refunds. For partial refunds, ensure the total stays below 50% of the order total by removing items if needed, preventing two partial refunds from fully refunding an order. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Fix partial refunds to track already-refunded quantities and prevent over-refunding When creating multiple refunds, the code was using original order quantities instead of accounting for items already refunded. This caused second refunds to exceed the original order quantities (e.g., 11 items refunded from an 8-item order). Now tracks refunded quantities per item and only refunds remaining quantities. All refund logic (full items, partial quantities, and fallback) now calculates remaining quantity = original - already_refunded before processing. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Fix code quality issues in refund calculations - Add division-by-zero guards before all $original_qty divisions - Change parameter checks from !empty() to isset() to support explicit 0 values - Remove unused variable $removed_item in refund amount calculation These changes improve robustness and prevent potential PHP warnings. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Add realistic date handling for refunds - First refunds are created within 2 months of order completion date - Second refunds are created within 1 month of first refund date - Update create_refund() to return refund object instead of boolean - Pass previous refund to second refund for proper date calculation This makes generated refund data more realistic for testing. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Refactor refund generation: extract constants and helper methods * Make default 'fixed_cart' Coupon generator explicit * Clarify memory impact * Clarify that decimal ratios for coupons and refunds are converted to percentages using integer rounding * Add error logging for order generation and coupon application failures * Fix indentation Co-authored-by: Chi-Hsuan Huang <chihsuan.tw@gmail.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Chi-Hsuan Huang <chihsuan.tw@gmail.com>
1 parent febe0d0 commit a314f05

File tree

4 files changed

+610
-17
lines changed

4 files changed

+610
-17
lines changed

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,16 @@ Generate orders with random dates between `--date-start` and `--date-end`.
3434
Generate orders with a specific status.
3535
- `wp wc generate orders <nr of orders> --status=completed`
3636

37+
Apply coupons to a percentage of generated orders (0.0-1.0). If no coupons exist, 6 will be created automatically (3 fixed cart, 3 percentage). Note: `--coupons` flag is equivalent to `--coupon-ratio=1.0`.
38+
39+
**Important:** Decimal ratios are converted to percentages using integer rounding. For example, `0.505` becomes 50% (not 50.5%) because the random generation uses integer comparison. Use whole percentages like `0.50` for precise 50% ratios.
40+
- `wp wc generate orders <nr of orders> --coupon-ratio=0.5`
41+
42+
Refund a percentage of completed orders (0.0-1.0). Refunds will be split evenly between partial and full, and 25% of partial refunds will receive a second partial refund.
43+
44+
**Note:** The same decimal ratio behavior applies to refund ratios as described above for coupon ratios.
45+
- `wp wc generate orders <nr of orders> --status=completed --refund-ratio=0.3`
46+
3747
#### Order Attribution
3848

3949
Order Attribution represents the origin of data for an order. By default, random values are generated and assigned to the order. Orders with a creation date before 2024-01-09 will not have attribution metadata added, as the feature was not available in WooCommerce at that time.
@@ -52,6 +62,9 @@ Generate coupons with a minimum discount amount.
5262
Generate coupons with a maximum discount amount.
5363
- `wp wc generate coupons <nr of coupons> --max=50`
5464

65+
Generate coupons with a specific discount type. Options are `fixed_cart` or `percent`. If not specified, defaults to WooCommerce default (fixed_cart).
66+
- `wp wc generate coupons <nr of coupons> --discount_type=percent --min=5 --max=25`
67+
5568
### Customers
5669

5770
Generate customers based on the number of customers parameter.

includes/CLI.php

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,19 @@ function () use ( $progress ) {
316316
array(
317317
'name' => 'coupons',
318318
'type' => 'flag',
319-
'description' => 'Create and apply a coupon to each generated order.',
319+
'description' => 'Create and apply a coupon to each generated order. Equivalent to --coupon-ratio=1.0.',
320+
'optional' => true,
321+
),
322+
array(
323+
'name' => 'coupon-ratio',
324+
'type' => 'assoc',
325+
'description' => 'Decimal ratio (0.0-1.0) of orders that should have coupons applied. If no coupons exist, 6 will be created (3 fixed value, 3 percentage). Note: Decimal values are converted to percentages using integer rounding (e.g., 0.505 becomes 50%).',
326+
'optional' => true,
327+
),
328+
array(
329+
'name' => 'refund-ratio',
330+
'type' => 'assoc',
331+
'description' => 'Decimal ratio (0.0-1.0) of completed orders that should be refunded (wholly or partially). Note: Decimal values are converted to percentages using integer rounding (e.g., 0.505 becomes 50%).',
320332
'optional' => true,
321333
),
322334
array(
@@ -381,8 +393,15 @@ function () use ( $progress ) {
381393
'optional' => true,
382394
'default' => 100,
383395
),
396+
array(
397+
'name' => 'discount_type',
398+
'type' => 'assoc',
399+
'description' => 'The type of discount for the coupon. If not specified, defaults to WooCommerce default (fixed_cart).',
400+
'optional' => true,
401+
'options' => array( 'fixed_cart', 'percent' ),
402+
),
384403
),
385-
'longdesc' => "## EXAMPLES\n\nwc generate coupons 10\n\nwc generate coupons 50 --min=1 --max=50",
404+
'longdesc' => "## EXAMPLES\n\nwc generate coupons 10\n\nwc generate coupons 50 --min=1 --max=50\n\nwc generate coupons 20 --discount_type=percent --min=5 --max=25",
386405
) );
387406

388407
WP_CLI::add_command( 'wc generate terms', array( 'WC\SmoothGenerator\CLI', 'terms' ), array(

includes/Generator/Coupon.php

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,15 @@ public static function generate( $save = true, $assoc_args = array() ) {
2525
parent::maybe_initialize_generators();
2626

2727
$defaults = array(
28-
'min' => 5,
29-
'max' => 100,
28+
'min' => 5,
29+
'max' => 100,
30+
'discount_type' => 'fixed_cart',
3031
);
3132

33+
$args = wp_parse_args( $assoc_args, $defaults );
34+
3235
list( 'min' => $min, 'max' => $max ) = filter_var_array(
33-
wp_parse_args( $assoc_args, $defaults ),
36+
$args,
3437
array(
3538
'min' => array(
3639
'filter' => FILTER_VALIDATE_INT,
@@ -68,6 +71,15 @@ public static function generate( $save = true, $assoc_args = array() ) {
6871
);
6972
}
7073

74+
// Validate discount_type if provided
75+
$discount_type = ! empty( $args['discount_type'] ) ? $args['discount_type'] : '';
76+
if ( ! empty( $discount_type ) && ! in_array( $discount_type, array( 'fixed_cart', 'percent' ), true ) ) {
77+
return new \WP_Error(
78+
'smoothgenerator_coupon_invalid_discount_type',
79+
'The discount_type must be either "fixed_cart" or "percent".'
80+
);
81+
}
82+
7183
$code = substr( self::$faker->promotionCode( 1 ), 0, -1 ); // Omit the random digit.
7284
$amount = self::$faker->numberBetween( $min, $max );
7385
$coupon_code = sprintf(
@@ -76,11 +88,18 @@ public static function generate( $save = true, $assoc_args = array() ) {
7688
$amount
7789
);
7890

79-
$coupon = new \WC_Coupon( $coupon_code );
80-
$coupon->set_props( array(
91+
$props = array(
8192
'code' => $coupon_code,
8293
'amount' => $amount,
83-
) );
94+
);
95+
96+
// Only set discount_type if explicitly provided
97+
if ( ! empty( $discount_type ) ) {
98+
$props['discount_type'] = $discount_type;
99+
}
100+
101+
$coupon = new \WC_Coupon( $coupon_code );
102+
$coupon->set_props( $props );
84103

85104
if ( $save ) {
86105
$data_store = WC_Data_Store::load( 'coupon' );
@@ -125,5 +144,32 @@ public static function batch( $amount, array $args = array() ) {
125144

126145
return $coupon_ids;
127146
}
147+
148+
/**
149+
* Get a random existing coupon.
150+
*
151+
* @return \WC_Coupon|false Coupon object or false if none available.
152+
*/
153+
public static function get_random() {
154+
// Note: Using posts_per_page=-1 loads all coupon IDs into memory for random selection.
155+
// For stores with thousands of coupons, consider using direct SQL with RAND() for better performance.
156+
// This approach was chosen for consistency with WordPress APIs and to avoid raw SQL queries.
157+
$coupon_ids = get_posts(
158+
array(
159+
'post_type' => 'shop_coupon',
160+
'post_status' => 'publish',
161+
'posts_per_page' => -1,
162+
'fields' => 'ids',
163+
)
164+
);
165+
166+
if ( empty( $coupon_ids ) ) {
167+
return false;
168+
}
169+
170+
$random_coupon_id = $coupon_ids[ array_rand( $coupon_ids ) ];
171+
172+
return new \WC_Coupon( $random_coupon_id );
173+
}
128174
}
129175

0 commit comments

Comments
 (0)