Ver código fonte

Add UPVP implementation plan for v1.5.3

Plan for new "Update product variant prices" section on Category
Manipulations page: category checkbox tree + variant attribute/value
comboboxes + new-price input + Dry Run/Apply buttons + live
affected-count label. Updates only product_variation regular_price
via WC API; never touches order items.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Dalibor Votruba 2 meses atrás
pai
commit
29ec819be6

+ 430 - 0
studiou-wc-product-cat-manage/inplementation-plan-upvp.md

@@ -0,0 +1,430 @@
+# 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_<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:
+
+```php
+<!-- 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:
+
+```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 `<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:
+
+```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.