# 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: ```php 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` ```php /** 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_' // 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_' // AND pm.meta_value <> '' // For taxonomy attributes (pa_*): meta_value is the term slug -> resolve labels via get_term_by('slug', ..., 'pa_'). // 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 `` card: ```php

0
``` `$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: ```js safeInit('upvpForm', function() { if ($('#studiou-wcpcm-upvp-form').length) initUpdateVariantPricesForm(); }); ``` Logic: 1. **On init** — request `studiou_wcpcm_upvp_get_attributes`; populate `#upvp_attribute` with ``. 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 `` 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: ```css .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.2` → `1.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.