1.7.0: shipping rule "Only" reserves a method, "Show" no longer does

Since 1.6.0 "Show" both whitelisted a method for its group and hid it from
everybody else. A group shown ordinary methods next to a free one therefore
took the ordinary ones away from every stranger, who was left with only the
free method. "Only" now carries the reservation; "Show" is just the group's
whitelist. Existing show rules are converted to only once on update so the
checkout does not change until the shop chooses.

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
kristianwind
2026-10-02 12:28:45 +02:00
co-authored by Claude Code
parent 5875638f2c
commit 0793a1ee82
11 changed files with 129 additions and 37 deletions
+6 -2
View File
@@ -51,7 +51,7 @@ class WC_EDM_Admin_Shipping {
$action = isset( $raw_rules[ $method_id ] ) ? $raw_rules[ $method_id ] : '';
if ( ! in_array( $action, array( 'show', 'hide' ), true ) ) {
if ( ! in_array( $action, array( 'show', 'hide', 'only' ), true ) ) {
continue;
}
@@ -84,7 +84,7 @@ class WC_EDM_Admin_Shipping {
?>
<h2><?php esc_html_e( 'Shipping Rules', 'wc-email-discount' ); ?></h2>
<p class="description">
<?php esc_html_e( 'Choose which shipping methods are shown or hidden for customers in a discount group. If a group has no rules, all shipping methods behave normally. If any method is set to "Show", only "Show" methods are offered.', 'wc-email-discount' ); ?>
<?php esc_html_e( 'Choose which shipping methods are shown or hidden for customers in a discount group. If a group has no rules, all shipping methods behave normally. If any method is set to "Show" or "Only", only those methods are offered to the group. "Only" also hides the method from every customer outside the group, which is what free delivery for one company needs; "Show" leaves it available to everybody.', 'wc-email-discount' ); ?>
</p>
<?php if ( empty( $groups ) ) : ?>
@@ -175,6 +175,10 @@ class WC_EDM_Admin_Shipping {
<input type="radio" name="rule[<?php echo esc_attr( $method_id ); ?>]" value="show" <?php checked( $action, 'show' ); ?>>
<?php esc_html_e( 'Show', 'wc-email-discount' ); ?>
</label>
<label title="<?php esc_attr_e( 'Shown to this group, and hidden from everybody else', 'wc-email-discount' ); ?>">
<input type="radio" name="rule[<?php echo esc_attr( $method_id ); ?>]" value="only" <?php checked( $action, 'only' ); ?>>
<?php esc_html_e( 'Only', 'wc-email-discount' ); ?>
</label>
<label>
<input type="radio" name="rule[<?php echo esc_attr( $method_id ); ?>]" value="hide" <?php checked( $action, 'hide' ); ?>>
<?php esc_html_e( 'Hide', 'wc-email-discount' ); ?>
+37 -7
View File
@@ -77,7 +77,7 @@ class WC_EDM_DB {
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
group_id bigint(20) unsigned NOT NULL,
shipping_method_id varchar(191) NOT NULL,
action enum('show','hide') NOT NULL DEFAULT 'show',
action enum('show','hide','only') NOT NULL DEFAULT 'show',
sort_order int(11) NOT NULL DEFAULT 0,
PRIMARY KEY (id),
KEY group_id (group_id)
@@ -102,10 +102,37 @@ class WC_EDM_DB {
dbDelta( $sql );
self::add_foreign_keys();
self::migrate_show_to_only();
update_option( 'wc_edm_db_version', WC_EDM_VERSION );
}
/**
* Once, when "only" was introduced: every existing "show" rule was reserved
* for its group, so it becomes "only" and the shop's checkout looks exactly
* as it did. Without this the update would silently put reserved free
* delivery back in front of every stranger.
*/
private static function migrate_show_to_only() {
global $wpdb;
if ( get_option( 'wc_edm_show_to_only_done' ) ) {
return;
}
$table = self::table( 'shipping_rules' );
$column = $wpdb->get_row( "SHOW COLUMNS FROM {$table} LIKE 'action'" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
// dbDelta did not widen the enum: converting now would store '' rather
// than 'only'. Leave the flag unset so the next load tries again.
if ( ! $column || false === strpos( (string) $column->Type, "'only'" ) ) {
return;
}
$wpdb->query( "UPDATE {$table} SET action = 'only' WHERE action = 'show'" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
update_option( 'wc_edm_show_to_only_done', 1 );
}
/**
* Run create_tables() again when the plugin was updated without
* re-activation (dbDelta adds any missing columns).
@@ -510,10 +537,13 @@ class WC_EDM_DB {
/**
* Every shipping method any group reserves for itself.
*
* A "show" rule means the group sees that method. It also has to mean
* nobody else does — otherwise reserving a free-delivery method for an
* approved group leaves it on the checkout for every other customer, which
* is what a shop notices as lost shipping revenue rather than as a bug.
* An "only" rule means the group sees that method AND nobody else does —
* otherwise reserving a free-delivery method for an approved group leaves
* it on the checkout for every other customer, which is what a shop
* notices as lost shipping revenue rather than as a bug. A plain "show"
* rule is a whitelist for its own group and reserves nothing: 1.6.0 made
* "show" do both, and a group that needed to show ordinary methods next to
* a reserved one took those ordinary methods away from everybody else.
*
* @return string[] Rate ids, e.g. "free_shipping:15".
*/
@@ -521,7 +551,7 @@ class WC_EDM_DB {
global $wpdb;
$table = self::table( 'shipping_rules' );
$ids = $wpdb->get_col( "SELECT DISTINCT shipping_method_id FROM {$table} WHERE action = 'show'" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$ids = $wpdb->get_col( "SELECT DISTINCT shipping_method_id FROM {$table} WHERE action = 'only'" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
return is_array( $ids ) ? $ids : array();
}
@@ -538,7 +568,7 @@ class WC_EDM_DB {
$wpdb->delete( self::table( 'shipping_rules' ), array( 'group_id' => $group_id ), array( '%d' ) );
foreach ( $rules as $rule ) {
if ( empty( $rule['shipping_method_id'] ) || ! in_array( $rule['action'], array( 'show', 'hide' ), true ) ) {
if ( empty( $rule['shipping_method_id'] ) || ! in_array( $rule['action'], array( 'show', 'hide', 'only' ), true ) ) {
continue;
}
$wpdb->insert(
+6 -4
View File
@@ -340,7 +340,7 @@ class WC_EDM_Frontend {
//
// This used to return $rates untouched, and a test asserted that
// as intended: "an unmatched customer sees every method". It made
// a "show" rule a whitelist FOR THE GROUP and nothing more, so
// a "show" rule (now "only") a whitelist FOR THE GROUP and nothing more, so
// reserving free delivery for approved addresses left it on the
// checkout for every other customer. Reported from a live shop,
// where a gmail address could pick a free-delivery method meant
@@ -389,7 +389,9 @@ class WC_EDM_Frontend {
* Apply a group's show/hide shipping rules.
*
* If the group has no rules, all rates pass through untouched.
* "show" rules act as a whitelist; "hide" rules remove rates.
* "show" and "only" rules act as a whitelist; "hide" rules remove rates.
* "only" additionally reserves the method for the group (see
* WC_EDM_DB::restricted_shipping_methods()).
* Rates are reordered to follow the rules' sort order.
*
* @param array $rates Available shipping rates (rate_id => WC_Shipping_Rate).
@@ -413,7 +415,7 @@ class WC_EDM_Frontend {
$hide = array();
foreach ( $rules as $rule ) {
if ( 'show' === $rule->action ) {
if ( 'show' === $rule->action || 'only' === $rule->action ) {
$show[] = $rule->shipping_method_id;
} else {
$hide[] = $rule->shipping_method_id;
@@ -422,7 +424,7 @@ class WC_EDM_Frontend {
$filtered = array();
// Reserved by some group, but not by THIS one.
// Reserved ("only") by some group, but not shown to THIS one.
$others = array_diff( WC_EDM_DB::restricted_shipping_methods(), $show );
foreach ( $rates as $rate_id => $rate ) {
+2 -2
View File
@@ -709,10 +709,10 @@ class WC_EDM_REST_API {
$method_id = isset( $rule['shipping_method_id'] ) ? sanitize_text_field( (string) $rule['shipping_method_id'] ) : '';
$action = isset( $rule['action'] ) ? sanitize_key( (string) $rule['action'] ) : '';
if ( '' === $method_id || ! in_array( $action, array( 'show', 'hide' ), true ) ) {
if ( '' === $method_id || ! in_array( $action, array( 'show', 'hide', 'only' ), true ) ) {
return new WP_Error(
'wc_edm_invalid',
__( 'Each rule needs a shipping_method_id and an action of "show" or "hide".', 'wc-email-discount' ),
__( 'Each rule needs a shipping_method_id and an action of "show", "hide" or "only".', 'wc-email-discount' ),
array( 'status' => 400 )
);
}