inplementation-plan-upvp.md 26 KB

Implementation Plan — Update Product Variant Prices (UPVP)

Target version: 1.5.3 (bump from current 1.5.2) Scope: Add a new section "Update product variant prices" to the existing Category Manipulations admin page. Goal: Allow admins to bulk-update the regular price of product variations by filtering on (a) one or more product categories and (b) a single variation attribute name + value pair. Provide a Dry Run preview, an Apply commit, and a live affected-count label. Operation must touch only product_variation records — never order items, carts, or order history.


1. Feature summary

On the page Products → Category Manipulations, append a new card titled "Update product variant prices" placed before the Important Notes card and after the Remove Not Assigned Media card. It contains:

  1. Categories checkbox list — product-category tree (re-using the existing studiou-wcpcm-categories-list-container markup and styles). With Check All / Uncheck All buttons.
  2. Variant attribute combobox — populated with the list of attribute names (heads) that are used for variations across the WooCommerce store (global pa_* taxonomies + product-level custom attributes flagged "used for variations").
  3. Variant value combobox — populated dynamically based on the selected attribute (and scoped to the currently checked categories). Lists distinct values that exist on real variations.
  4. New Price textbox — numeric input (type=number, step=0.01, min=0); server normalises with wc_format_decimal.
  5. Dry Run button — counts and lists the variations that would be updated, without writing.
  6. Apply button — saves set_regular_price() to all matching variations via the WooCommerce API.
  7. Affected-count label — live counter; recalculated when filters change, after Dry Run, and after Apply.

Operation only modifies regular_price on product_variation posts. Sale prices, order line items, cart sessions and historical order data are not touched.


2. Files to create / modify

2.1 New files

  • (none) — all new logic fits into existing classes / views.

2.2 Modified files

File Change
studiou-wc-product-cat-manage.php Bump Version: header and STUDIOU_WCPCM_VERSION to 1.5.3. Extend the i18n map in wp_localize_script with new UPVP strings.
includes/class-studiou-wc-product-cat-manage-manipulator.php Add 4 new public AJAX handlers + 4 helper methods (see §3). Register the 4 actions in the constructor's add_action(...) block.
views/manipulations.php Append the new "Update product variant prices" card before the Important Notes card.
assets/css/admin.css Minor styling for the new section (results panel, affected-count label, preview table).
assets/js/admin.js Add initUpdateVariantPricesForm() wrapped in the existing safeInit() pattern.
languages/studiou-wc-product-cat-manage.pot Add new translation strings.
languages/studiou-wc-product-cat-manage-cs_CZ.po Add Czech translations.
languages/studiou-wc-product-cat-manage-cs_CZ.mo Regenerate (msgfmt).
readme.md Bump version, new section in Features, new usage section, Version 1.5.3 changelog entry.
CLAUDE.md Bump Current Version to 1.5.3, add 4 new methods to manipulator bullets, add 4 new AJAX endpoints, add bullet under Admin Menu Structure → Category Manipulations.
directory-structure.txt No structural change — verify it still matches reality.

3. Backend implementation

All backend work goes into class-studiou-wc-product-cat-manage-manipulator.php. We extend the existing manipulator because UPVP is conceptually a category manipulation and shares helpers (get_categories_with_counts, AJAX boilerplate) with the existing manipulator. No new class.

3.1 New AJAX action registrations

Add inside Studiou_WC_Product_Cat_Manage_Manipulator::__construct(), alongside the existing add_action('wp_ajax_studiou_wcpcm_*', ...) lines:

add_action('wp_ajax_studiou_wcpcm_upvp_get_attributes',       array($this, 'handle_upvp_get_attributes_ajax'));
add_action('wp_ajax_studiou_wcpcm_upvp_get_attribute_values', array($this, 'handle_upvp_get_attribute_values_ajax'));
add_action('wp_ajax_studiou_wcpcm_upvp_count',                array($this, 'handle_upvp_count_ajax'));
add_action('wp_ajax_studiou_wcpcm_upvp_apply',                array($this, 'handle_upvp_apply_ajax'));

Each handler follows the established pattern in the file:

  • while (ob_get_level()) ob_end_clean(); ob_start();
  • error_reporting(0) save/restore.
  • wp_verify_nonce($_POST['nonce'], 'studiou-wcpcm-nonce').
  • current_user_can('manage_woocommerce').
  • try { ... } catch(Exception $e) { ... } finally { restore + ob_end_clean(); }.
  • Always-on error logging consistent with STUDIOU WC MEDIA: style — prefix STUDIOU WC UPVP:.

3.2 New methods on Studiou_WC_Product_Cat_Manage_Manipulator

/** Return list of attribute "heads" (names) usable for variations. */
public function get_variation_attribute_heads(): array
// Returns: [ ['key' => 'pa_color', 'label' => 'Color', 'is_taxonomy' => true], ... ]
// Source 1: every wc_get_attribute_taxonomies() entry (global attributes) -> key = 'pa_<slug>'
// Source 2: distinct meta_key starting with 'attribute_' on post_type='product_variation'
//           (covers custom non-taxonomy attributes flagged for variations)
//           -> key = substring after 'attribute_' (NOT prefixed with 'pa_')
// De-duplicate by key, sort by human label asc.

/** Return distinct values for a given attribute key, scoped to existing variations. */
public function get_variation_attribute_values(string $attribute_key, array $category_ids = []): array
// Returns: [ ['value' => 'red', 'label' => 'Red'], ... ] sorted by label.
// SQL strategy:
//   SELECT DISTINCT pm.meta_value
//     FROM {$wpdb->postmeta} pm
//     INNER JOIN {$wpdb->posts} v        ON v.ID = pm.post_id AND v.post_type = 'product_variation'
//                                                              AND v.post_status IN ('publish','private')
//     INNER JOIN {$wpdb->posts} p        ON p.ID = v.post_parent AND p.post_type = 'product'
//     [INNER JOIN {$wpdb->term_relationships} tr ON p.ID = tr.object_id
//      INNER JOIN {$wpdb->term_taxonomy}     tt ON tr.term_taxonomy_id = tt.term_taxonomy_id
//      WHERE tt.taxonomy = 'product_cat' AND tt.term_id IN (...)]
//    AND pm.meta_key = 'attribute_<sanitized key>'
//    AND pm.meta_value <> ''
// For taxonomy attributes (pa_*): meta_value is the term slug -> resolve labels via get_term_by('slug', ..., 'pa_<key>').
// For non-taxonomy: meta_value is already the label.
// All input goes through wc_sanitize_taxonomy_name() / sanitize_text_field. Use $wpdb->prepare with %d/%s placeholders.

/** Count + list variations that match the filter. */
public function find_matching_variations(array $category_ids, string $attribute_key, string $attribute_value): array
// Returns:
//   [
//     'count'  => int,
//     'ids'    => int[],          // all matching variation IDs
//     'sample' => array[]         // up to 20 entries:
//                                 //   [ 'id', 'sku', 'name', 'parent_name', 'current_price' ]
//   ]
// Same JOIN as above but selecting v.ID. Reads sample with WC API (wc_get_product) capped at 20.
// Uses $wpdb->prepare with implode(',', array_fill(0, count($category_ids), '%d')) for the IN list.

/** Apply new price to a list of variation IDs. */
public function apply_variant_price(array $variation_ids, string $new_price): array
// Returns ['updated'=>int, 'failed'=>int, 'failed_ids'=>int[], 'message'=>string].
// Steps for each ID:
//   $variation = wc_get_product($id);
//   if (!$variation || !$variation->is_type('variation')) { failed[] = id; continue; }
//   $variation->set_regular_price(wc_format_decimal($new_price));
//   $variation->save();
// After loop: foreach (unique parent_id) WC_Product_Variable::sync($parent_id);
//             wc_delete_product_transients($parent_id);
// Validation guard at entry: must be numeric, >= 0; otherwise return error.

3.3 Handler responsibilities (inputs → outputs)

Handler Input ($_POST) data on success
handle_upvp_get_attributes_ajax nonce attributes: [{key,label,is_taxonomy}]
handle_upvp_get_attribute_values_ajax nonce, attribute_key, category_ids[] (optional) values: [{value,label}]
handle_upvp_count_ajax nonce, category_ids[], attribute_key, attribute_value count, sample[]
handle_upvp_apply_ajax nonce, category_ids[], attribute_key, attribute_value, new_price updated, failed, failed_ids, message

All array inputs run through array_map('intval', ...); strings through sanitize_text_field; attribute_key additionally through wc_sanitize_taxonomy_name. The price stays raw, normalised in apply_variant_price.

3.4 Edge cases handled

  • Empty category list → error "Please select at least one category".
  • Empty attribute key → error.
  • Empty attribute value → error.
  • Non-numeric / negative price → error before any DB write.
  • Variation belongs to a non-variable parent (orphan) → counted as failed.
  • Attribute key collision between a taxonomy attribute and a same-labelled custom attribute — disambiguated by storing the full key (pa_color vs color) in the combobox value, not the label.
  • Order-line price preservation — operation never touches woocommerce_order_itemmeta. Document this explicitly in the Important Notes card and changelog.
  • Parent variable product price-range cache — re-synced via WC_Product_Variable::sync($parent_id) after the batch so the storefront price range stays correct.

4. Frontend implementation

4.1 View — views/manipulations.php

Insert a new card immediately before the existing <!-- Important Notes --> card:

<!-- Update Product Variant Prices Operation -->
<div class="studiou-wcpcm-card">
    <h2><?php echo esc_html__('Update product variant prices', 'studiou-wc-product-cat-manage'); ?></h2>

    <p><?php echo esc_html__('Bulk-update the regular price of product variations matching the selected categories and a chosen variation attribute. Only product records are modified — existing orders and cart contents are not affected.', 'studiou-wc-product-cat-manage'); ?></p>

    <form id="studiou-wcpcm-upvp-form" method="post" onsubmit="return false;">
        <table class="form-table">
            <!-- 1. Category checkbox list -->
            <tr>
                <th scope="row">
                    <label for="upvp_categories_list"><?php echo esc_html__('Select Categories', 'studiou-wc-product-cat-manage'); ?></label>
                </th>
                <td>
                    <div class="studiou-wcpcm-categories-list-container">
                        <div class="studiou-wcpcm-list-actions">
                            <button type="button" id="studiou-wcpcm-upvp-check-all" class="button">
                                <?php echo esc_html__('Check All', 'studiou-wc-product-cat-manage'); ?>
                            </button>
                            <button type="button" id="studiou-wcpcm-upvp-uncheck-all" class="button">
                                <?php echo esc_html__('Uncheck All', 'studiou-wc-product-cat-manage'); ?>
                            </button>
                        </div>
                        <div class="studiou-wcpcm-categories-list" id="upvp_categories_list">
                            <?php foreach ($categories as $category): ?>
                                <label class="studiou-wcpcm-category-item">
                                    <input type="checkbox" name="upvp_categories[]" value="<?php echo esc_attr($category['id']); ?>">
                                    <span><?php echo esc_html($category['display_name']); ?></span>
                                </label>
                            <?php endforeach; ?>
                        </div>
                    </div>
                </td>
            </tr>

            <!-- 2. Variant attribute combobox -->
            <tr>
                <th scope="row">
                    <label for="upvp_attribute"><?php echo esc_html__('Variant attribute', 'studiou-wc-product-cat-manage'); ?></label>
                </th>
                <td>
                    <select id="upvp_attribute" name="upvp_attribute" class="regular-text">
                        <option value=""><?php echo esc_html__('-- Select attribute --', 'studiou-wc-product-cat-manage'); ?></option>
                    </select>
                </td>
            </tr>

            <!-- 3. Variant value combobox -->
            <tr>
                <th scope="row">
                    <label for="upvp_attribute_value"><?php echo esc_html__('Variant attribute value', 'studiou-wc-product-cat-manage'); ?></label>
                </th>
                <td>
                    <select id="upvp_attribute_value" name="upvp_attribute_value" class="regular-text" disabled>
                        <option value=""><?php echo esc_html__('-- Select value --', 'studiou-wc-product-cat-manage'); ?></option>
                    </select>
                </td>
            </tr>

            <!-- 4. New price -->
            <tr>
                <th scope="row">
                    <label for="upvp_new_price"><?php echo esc_html__('New Price', 'studiou-wc-product-cat-manage'); ?></label>
                </th>
                <td>
                    <input type="number" step="0.01" min="0" id="upvp_new_price" name="upvp_new_price" class="regular-text">
                </td>
            </tr>

            <!-- 6. Affected-count label -->
            <tr>
                <th scope="row">
                    <?php echo esc_html__('Affected variations', 'studiou-wc-product-cat-manage'); ?>
                </th>
                <td>
                    <strong id="studiou-wcpcm-upvp-affected-count">0</strong>
                </td>
            </tr>
        </table>

        <!-- 5. Buttons -->
        <div class="submit-buttons">
            <button type="button" id="studiou-wcpcm-upvp-dry-run-button" class="button">
                <?php echo esc_html__('Dry Run', 'studiou-wc-product-cat-manage'); ?>
            </button>
            <button type="button" id="studiou-wcpcm-upvp-apply-button" class="button button-primary">
                <?php echo esc_html__('Apply', 'studiou-wc-product-cat-manage'); ?>
            </button>
            <span class="spinner"></span>
        </div>
    </form>

    <div class="studiou-wcpcm-upvp-results" style="display: none;">
        <h3><?php echo esc_html__('Results', 'studiou-wc-product-cat-manage'); ?></h3>
        <div id="studiou-wcpcm-upvp-message"></div>
    </div>
</div>

$categories is already populated above via $manipulator->get_categories_with_counts().

4.2 JS — assets/js/admin.js

Add a single initUpdateVariantPricesForm() and register it inside the existing $(document).ready() block via the existing safeInit() wrapper:

safeInit('upvpForm', function() {
    if ($('#studiou-wcpcm-upvp-form').length) initUpdateVariantPricesForm();
});

Logic:

  1. On init — request studiou_wcpcm_upvp_get_attributes; populate #upvp_attribute with <option value="key" data-istax="0|1">label</option>.
  2. Check All / Uncheck All — same pattern as the existing batch-description and delete-products handlers.
  3. On #upvp_attribute change — clear #upvp_attribute_value; if non-empty attribute, call studiou_wcpcm_upvp_get_attribute_values (passing currently-checked categories so the value list is scoped); enable the value combobox after population. Trigger affected-count refresh.
  4. On any filter change (categories checkboxes, attribute, value) — debounced (300 ms) call to studiou_wcpcm_upvp_count; update #studiou-wcpcm-upvp-affected-count. Disable both Dry Run and Apply buttons while a count request is in-flight or while the count is 0.
  5. Dry Run click — call count endpoint with full filter; render a results panel containing a small <table class="studiou-wcpcm-upvp-preview-table"> listing the sample variations: ID, SKU, Name, Parent, Current → New Price.
  6. Apply click — JS confirm() ("This will update X variations. Continue?"); on accept, POST studiou_wcpcm_upvp_apply with the same filter + new price; on success render summary (updated, failed, failed_ids[]) and refresh the affected-count.

All XHR calls follow the existing AJAX boilerplate: spinner, notice area, console.log debug lines, showNotice() helper.

4.3 CSS — assets/css/admin.css

Append:

.studiou-wcpcm-upvp-results {
    background: #f9f9f9;
    padding: 15px;
    border: 1px solid #ddd;
    border-radius: 4px;
    margin-top: 30px;
}

#studiou-wcpcm-upvp-affected-count {
    font-size: 1.2em;
    color: #0073aa;
}

.studiou-wcpcm-upvp-preview-table {
    width: 100%;
    border-collapse: collapse;
    margin-top: 10px;
}

.studiou-wcpcm-upvp-preview-table th,
.studiou-wcpcm-upvp-preview-table td {
    padding: 6px 10px;
    border-bottom: 1px solid #eee;
    text-align: left;
}

5. Translations

Add the following new English → Czech strings; register in the .pot, the cs_CZ.po, and recompile cs_CZ.mo with msgfmt.

English Czech
Update product variant prices Aktualizace cen variant produktů
Bulk-update the regular price of product variations matching the selected categories and a chosen variation attribute. Only product records are modified — existing orders and cart contents are not affected. Hromadná aktualizace běžné ceny variant produktů odpovídajících vybraným kategoriím a zvolené vlastnosti varianty. Mění se pouze záznamy produktů — existující objednávky a obsah košíku nejsou dotčeny.
Variant attribute Vlastnost varianty
Variant attribute value Hodnota vlastnosti varianty
-- Select attribute -- -- Vyberte vlastnost --
-- Select value -- -- Vyberte hodnotu --
New Price Nová cena
Dry Run Zkušební běh
Apply Provést
Affected variations Dotčené varianty
Results Výsledky
No variations match the current filter. Žádné varianty neodpovídají vybranému filtru.
Successfully updated %d variations. Úspěšně aktualizováno %d variant.
Failed to update %d variations. Nepodařilo se aktualizovat %d variant.
Are you sure you want to update %d variations? Opravdu chcete aktualizovat %d variant?
Please select an attribute Vyberte prosím vlastnost
Please select an attribute value Vyberte prosím hodnotu vlastnosti
Please enter a valid new price Zadejte prosím platnou novou cenu
Loading attributes... Načítání vlastností...
Loading values... Načítání hodnot...
Dry run complete: %d variations would be updated to %s. Zkušební běh dokončen: %d variant bude aktualizováno na %s.
Variant prices updated successfully Ceny variant úspěšně aktualizovány
Variant prices update error Chyba aktualizace cen variant
The update product variant prices operation only modifies the regular price of product variations. Order line items, cart contents, and historical order data are never modified. Operace aktualizace cen variant mění pouze běžnou cenu variant produktů. Položky objednávek, obsah košíku ani historická data objednávek nejsou nikdy měněny.

Add matching keys to the i18n array in wp_localize_script(... 'studiouWcpcm', ...) in studiou-wc-product-cat-manage.php: upvpConfirmApply, upvpSelectAttribute, upvpSelectValue, upvpInvalidPrice, upvpLoadingAttributes, upvpLoadingValues, upvpDryRunComplete, upvpApplySuccess, upvpApplyError, upvpNoMatches.


6. Documentation updates

6.1 readme.md

  • Bump **Version: 1.5.2****Version: 1.5.3**.
  • Under Features → Category Management, add: **Update product variant prices** - bulk-update regular price of variations by category + attribute filter (NEW in v1.5.3).
  • Under Menu Structure → Category Management → Category Manipulations sub-list, add: Update product variant prices (NEW in v1.5.3).
  • Add a new usage section "Updating Product Variant Prices" mirroring the structure of the existing Manipulation usage sections, with the 6 controls listed and notes:
    • Operation modifies only regular_price of product_variation records.
    • Order line items, carts and historical data are not affected.
    • Always run Dry Run first to preview the affected count and sample list.
    • Parent variable products are re-synced (WC_Product_Variable::sync) after the batch so the storefront price range cache stays correct.
  • Append changelog entry Version 1.5.3 above 1.5.2:

    ### Version 1.5.3
    - **NEW: Update product variant prices** - bulk-update regular price of product variations
    - Filter by selected product categories (with Check All / Uncheck All)
    - Variant attribute combobox (heads), populated from variation-eligible attributes
    - Variant value combobox, dynamically scoped to the chosen attribute and selected categories
    - New Price textbox with numeric validation
    - Dry Run button shows affected count + sample preview, no DB writes
    - Apply button commits via WooCommerce API (`set_regular_price` + `WC_Product_Variable::sync`)
    - Live affected-count label updates on every filter change
    - 4 new AJAX handlers: `studiou_wcpcm_upvp_get_attributes`, `studiou_wcpcm_upvp_get_attribute_values`, `studiou_wcpcm_upvp_count`, `studiou_wcpcm_upvp_apply`
    - Operation never touches order items, cart sessions, or historical order data
    - Added 23 new translation strings (English/Czech)
    

6.2 CLAUDE.md

  • Bump Current Version 1.5.21.5.3.
  • Under class-studiou-wc-product-cat-manage-manipulator.php bullet list, add: Update product variant prices (regular_price only, by category + attribute filter) (NEW in v1.5.3).
  • Under Views (views/) → manipulations.php, add: Update product variant prices (checkbox list + 2 comboboxes + price input + Dry Run/Apply buttons + live affected-count label) (NEW in v1.5.3).
  • Under Admin Menu Structure → Category Manipulations, add bullet: Update product variant prices (NEW in v1.5.3).
  • Under AJAX Handlers → Category Operations, append entries 11–14:
    • studiou_wcpcm_upvp_get_attributes — list variation attribute heads (NEW in v1.5.3)
    • studiou_wcpcm_upvp_get_attribute_values — list values for selected attribute (NEW in v1.5.3)
    • studiou_wcpcm_upvp_count — count + sample of variations matching current filter (NEW in v1.5.3)
    • studiou_wcpcm_upvp_apply — commit new regular price to matching variations (NEW in v1.5.3)
  • Renumber the Product Operations section (currently starts at 11) accordingly.
  • Add Testing Notes → Update Product Variant Prices Operations (NEW in v1.5.3) section listing the test cases from §7 below.

6.3 directory-structure.txt

  • No structural change. Verify it is still accurate (existing classes get methods added; no new files).

7. Step-by-step execution order

  1. Bump version constant + plugin header to 1.5.3.
  2. Implement get_variation_attribute_heads, get_variation_attribute_values, find_matching_variations, apply_variant_price in Studiou_WC_Product_Cat_Manage_Manipulator (no AJAX yet).
  3. Add the 4 AJAX handler methods + register them in the constructor.
  4. Append the UPVP card to views/manipulations.php (before Important Notes).
  5. Add CSS rules.
  6. Add initUpdateVariantPricesForm() to admin.js, hook through existing safeInit().
  7. Extend wp_localize_script i18n map with new keys.
  8. Update .pot, cs_CZ.po; regenerate cs_CZ.mo with msgfmt.
  9. Update readme.md, CLAUDE.md. Verify directory-structure.txt.
  10. Manual smoke test — see §8.
  11. Hard-refresh in browser (studiou.cz strips ?ver= query strings — note in commit message).

8. Test plan

Manual UI tests on a dev WC store with at least one variable product per fixture below:

  1. Happy path (taxonomy attribute) — Pick 1 category containing variable products with pa_color = red. Pick attribute Color, value Red, price 19.99. Affected-count updates as filters change. Dry Run shows correct count + sample table. Apply → updated count matches; spot-check a variation in WP admin shows new regular price; parent variable product price range reflects the update.
  2. Happy path (custom non-taxonomy attribute) — Same flow on a product whose Size attribute is product-level (not global).
  3. Multi-category — Select two sibling categories; variations counted only once each.
  4. No matches — Filter that yields zero variations: Apply button stays disabled, label shows 0.
  5. Invalid price — Empty / negative / non-numeric → error notice, no DB write.
  6. Permission — User without manage_woocommerce cannot trigger AJAX.
  7. Nonce — Tampered nonce → Security check failed.
  8. Order data integrity — Place a test order with a variation, run UPVP, confirm the order line item retains the original price.
  9. Translations — Switch site to cs_CZ, confirm UPVP labels render in Czech.
  10. Cache busting — Asset version query string updates from 1.5.2 to 1.5.3. Hard-refresh on studiou.cz.
  11. Parent re-sync — After Apply, variable-product price range on storefront reflects the new regular price (sync transient cleared via wc_delete_product_transients).

9. Rollback

Purely additive change — no schema migrations, no destructive operations, no changes to existing handlers. To roll back: revert the listed files to 1.5.2 and re-enqueue assets. No data cleanup required because UPVP only writes to existing product_variation records' _regular_price postmeta via the WC API.