---
phase: 02-product-actions-fixes
plan: 03
type: execute
wave: 1
depends_on: ["02-01", "02-02"]
files_modified:
- themes/ayon/assets/js/custom.js
- themes/ayon/templates/catalog/product.tpl
autonomous: false
delegation: off
---
## Goal
Dokończyć end-to-end add-to-cart UX w nowym layoucie: (1) customization crop/mirror/dimensions zapisuje się w DB i jest widoczna w koszyku jako "Szczegóły", (2) success modal z opcjami "Kontynuuj zakupy / Przejdź do koszyka" pokazuje się po udanym dodaniu. Parzystość funkcjonalna ze starym layoutem dla tych dwóch punktów.
## Purpose
Plan 02-02 uruchomił POST, ale bez tych dwóch punktów nowy layout jest nieakceptowalny dla klienta końcowego: nie widzi potwierdzenia akcji (modal) ani co zamówił (customization details w cart). Jest to **blocker dla usunięcia IP gate** (`REMOTE_ADDR == '89.69.31.86'`) i publikacji layout'u szerszemu audytorium.
## Output
- Wszystkie wymagane squaremeter hidden inputs w POST payload (`discretion=on`, `dim`, `qty`, `qty_alth`, `product_total_price_calc`, `id_product_attribute`, +opcjonalne) obliczone z piece state + product data.
- Po udanym cart update → drugi POST do `/module/ps_shoppingcart/ajax?action=add-to-cart` → render modal z response.modal HTML w fancybox (lub native PS modal container).
- Koszyk po reload pokazuje "Szczegóły" button przy produkcie, klik rozwija customization data (wymiary, is_crop, is_reflection).
- Plan 02-01 override `totalpriceinfospecific` POZOSTAJE (nie ruszamy — bypass, nie restore). Cena kalkulacja może być nieprecyzyjna w tym planie (defer do Plan 02-04).
- Summary: `.paul/phases/02-product-actions-fixes/02-03-SUMMARY.md`.
## Project Context
@.paul/PROJECT.md
@.paul/ROADMAP.md
@.paul/STATE.md
## Prior Work
@.paul/phases/02-product-actions-fixes/02-01-SUMMARY.md
@.paul/phases/02-product-actions-fixes/02-02-SUMMARY.md
## Source Files
@themes/ayon/templates/catalog/product.tpl
@themes/ayon/assets/js/custom.js
@modules/squaremeter/squaremeter.php
# Line 843+: hookActionObjectCartUpdateBefore — gate'owany `discretion=on`, czyta:
# dim, qty, qty_alth, qty_alt, qty_altd, extrafeevalue, wastevalue,
# product_total_price_calc, calculated_total, grand_calculated_total,
# converted_ea, directinput
@modules/squaremeter/override/classes/Cart.php
# _addCustomization2(): INSERT INTO customization + customized_data
@modules/squaremeter/override/modules/ps_shoppingcart/controllers/front/ajax.php
# Endpoint dla `action=add-to-cart` → renderModal() zwraca modal HTML
@modules/squaremeter/views/templates/front/header_surface.tpl
# Stary flow squaremeter — inspiracja jakie fields + jak computed
## Required Skills
| Skill | Priority | When to Invoke | Loaded? |
|-------|----------|----------------|---------|
| Playwright MCP (`mcp__plugin_playwright_playwright__*`) | required | Task 1 (capture OLD layout POST), Task 3 regression | ○ |
| context-mode (`mcp__plugin_context-mode_context-mode__*`) | optional | Eksploracja squaremeter hooks + PS endpoints | ○ |
**BLOCKING:** Playwright MCP wymagany w Task 1 — musimy zobaczyć EXACT payload starego layoutu (fields + wartości) zanim zainżynierujemy mapowanie w nowym.
## Skill Invocation Checklist
- [ ] Playwright MCP dostępny
- [ ] Dostęp do testowego środowiska gdzie można przełączyć layouty (IP switch lub URL param)
## AC-1: Customization zapisuje się podczas add-to-cart
```gherkin
Given user skonfigurował piece (width=200, height=150) w nowym layoucie
And formularz ma is_crop=1, crop_width=200, crop_height=150, piece_bg_top/left
When user klika "Dodaj do koszyka"
Then POST payload zawiera `discretion=on&dim=200x150&qty=200&qty_alth=150&product_total_price_calc=&id_product_attribute=`
And response HTTP 200 z `id_customization > 0`
And w DB tabela `customization` ma nowy wiersz z `id_cart=&id_product=202&in_cart=1`
And w DB tabela `customized_data` ma wiersz z `index=&value='200x150'`
```
## AC-2: Success modal pokazuje się po dodaniu
```gherkin
Given udany cart update z AC-1
When backend potwierdzi success
Then wychodzi drugi POST do `/module/ps_shoppingcart/ajax?action=add-to-cart` z `id_product`, `id_product_attribute`, `id_customization`
And response zawiera `modal: '...'`
And modal renderuje się (w fancybox lub native container) z:
- Nazwą produktu
- Podziękowaniem / komunikatem success
- Dwoma przyciskami: "Kontynuuj zakupy" (zamyka modal) i "Przejdź do koszyka" (nawiguje do /pl/koszyk)
```
## AC-3: Koszyk pokazuje "Szczegóły" button + customization data
```gherkin
Given product dodany przez nowy layout z customization
When user nawiguje do /pl/koszyk
Then cart item ma button/link "Szczegóły" (lub zwinięty blok z danymi)
And kliknięcie rozwija customization:
- Wymiary: "200 x 150 cm"
- Czy fragment: tak/nie (is_crop)
- Czy odbicie lustrzane: tak/nie (is_reflection)
```
## AC-4: Brak double-add (handler idempotency po success)
```gherkin
Given user klika "Dodaj do koszyka" jedno klikniecie
When handler przetwarza
Then wychodzi dokładnie 1 POST do /koszyk + 1 POST do /module/ps_shoppingcart/ajax
And w koszyku jest 1 produkt (nie 2)
```
## AC-5: Zero regresji starego layoutu
```gherkin
Given strona produktu poza IP 89.69.31.86
When user konfiguruje piece + dimensions + klika "Dodaj do koszyka"
Then flow identyczny jak przed Plan 02-03 (baseline network capture)
And customization save nadal działa (existing feature)
And modal nadal działa (existing feature)
```
Task 1: Capture OLD layout POST payload (ground truth)
Nothing coded yet — live data capture. Musimy zobaczyć DOKŁADNIE jakie fields stary layout
wysyła przy add-to-cart, z jakimi wartościami, w jakiej kolejności. Bez tego ryzyko
przegapienia wymaganego pola (np. `directinput`, `qty_alt`, `calculated_total`).
Via Playwright MCP — OLD layout (IP != 89.69.31.86, np. przez URL param `?_test_old=1`
jeśli jest albo tymczasowo komentować IP check w product.tpl dla tej sesji):
1. Navigate: dowolny produkt na starym layoucie
2. Install diagnostic:
```js
window.__oldPayload = null;
const _ajax = jQuery.ajax;
jQuery.ajax = function(cfg) {
const url = (typeof cfg === 'string') ? cfg : cfg && cfg.url;
if (String(url).match(/cart|koszyk|ps_shoppingcart/) && cfg.type === 'POST') {
window.__oldPayload = { url, data: cfg.data, headers: cfg.headers };
}
return _ajax.apply(jQuery, arguments);
};
```
3. Sformułuj pełny user flow: wybierz wariant kolorystyczny + materiał + wprowadź wymiary (np. 200×150) + ustaw piece position + kliknij "Dodaj do koszyka"
4. Odczytaj `window.__oldPayload` + sprawdź Network tab dla wszystkich XHR/fetch do `cart` i `ps_shoppingcart`
5. Dokumentacja wszystkich parametrów:
- Pierwszy POST (cart update): wszystkie name=value pairs
- Drugi POST (modal fetch if exists): endpoint + payload
- Suffix pattern (`_` lub brak)
- Format `dim` (np. "200x150" vs "200X150" vs "200 x 150")
- Czy `product_total_price_calc` jest integer czy decimal
6. Zapisać w komentarzu checkpoint'a listę fields z przykładowymi wartościami
Resume-signal: wklej dokładne pole-wartość z OLD POST + zatwierdź że to jest wzór
który naśladujemy w Task 2.
Podaj fields z OLD POST payload (przykład: `discretion=on&dim=200x150&qty=200&qty_alth=150&product_total_price_calc=597.00&...`) LUB flag "use sensible defaults" jeśli nie masz dostępu do OLD layoutu na prod.
Task 2: Inject squaremeter fields do POST payload w nowym layoucie
themes/ayon/assets/js/custom.js, themes/ayon/templates/catalog/product.tpl
W OBU miejscach (handler w custom.js linie 993-1113 + inline mirror w product.tpl
linie 757+) zmodyfikuj payload construction:
Przed `$.ajax({ ... data: payload })`:
```js
// Phase 02 Plan 02-03: inject squaremeter fields for customization save
var pieceW = parseInt($('#piece-width').val(), 10) || 100;
var pieceH = parseInt($('#piece-height').val(), 10) || 100;
var idProductAttribute = parseInt($('input[name=id_product_attribute], #idCombination').val(), 10)
|| window.product_page_product_combination
|| 0;
// Price calc: use product base price × area (m²) as fallback.
// Exact price jest Plan 02-04 scope — tu chodzi tylko o niezero value żeby hook przeszedł.
var areaM2 = (pieceW / 100) * (pieceH / 100);
var basePrice = parseFloat($('#product_base_price, #product_fixed_price').val())
|| parseFloat($('.product-prices .current-price').first().text().replace(/[^\d.,]/g, '').replace(',', '.'))
|| 0;
var totalPriceCalc = (basePrice * areaM2).toFixed(2);
var sqFields = 'discretion=on'
+ '&dim=' + pieceW + 'x' + pieceH
+ '&qty=' + pieceW // squaremeter qty = width (NIE cart quantity!)
+ '&qty_alth=' + pieceH
+ '&product_total_price_calc=' + totalPriceCalc
+ '&id_product_attribute=' + idProductAttribute
+ '&calculated_total=' + areaM2.toFixed(4)
+ '&grand_calculated_total=' + areaM2.toFixed(4);
payload = sqFields + '&' + payload; // prepend, bo niektóre pola mogą się powtórzyć — ostatni wygrywa w PHP
```
**Adjust payload** żeby NASZ qty (cart quantity) nie był nadpisywany. PS uses `qty` jako
product quantity; squaremeter czyta `qty` jako dimension width. Konflikt!
Rozwiązanie: nie wysyłać cart `qty=1` bezpośrednio; zamiast tego dodać je jako
`quantity_wanted=1` lub upewnić się że nasz `qty` (dimension) jest the one in POST.
**Task 1 da odpowiedź co stary layout robi tutaj — może używa suffix** (`qty_`)
dla dimension a `qty` dla cart quantity.
Avoid:
- Zmiana nazwy cart quantity field (PS core by tego nie zaakceptował)
- Hardcode basePrice (read dynamically)
- Zakładanie że `id_product_attribute` jest w formie (może być w innej formie lub
jako Smarty var — Task 1 pokaże)
Po Task 1 adjust field list do tego co stary POST zawiera — możliwe że są DODATKOWE
pola których tu nie wymieniliśmy (np. `directinput`, `converted_ea`).
Playwright live test nowego layoutu:
1. Navigate + piece config
2. Click add-to-cart
3. Network tab: sprawdź POST do /koszyk — payload zawiera `discretion=on&dim=200x150&qty=200&qty_alth=150&product_total_price_calc=&id_product_attribute=`
4. Response: `success:true, id_customization: `
5. DB check (via PS admin SQL): `SELECT * FROM ps_customization WHERE id_cart = ` → 1 wiersz, `in_cart=1` po finalizacji
AC-1 satisfied.
Task 3: Chain POST do ps_shoppingcart/ajax + render modal
themes/ayon/assets/js/custom.js, themes/ayon/templates/catalog/product.tpl
W success callback po udanym cart POST (w OBU miejscach: custom.js + inline product.tpl),
po `emit('updatedCart')`, przed lub zamiast blockcart refresh:
```js
// Phase 02 Plan 02-03: fetch success modal
if (resp && resp.success && resp.id_product) {
$.ajax({
url: '/module/ps_shoppingcart/ajax',
type: 'POST',
data: {
action: 'add-to-cart',
id_product: resp.id_product,
id_product_attribute: resp.id_product_attribute,
id_customization: resp.id_customization
},
dataType: 'json',
success: function(modalResp) {
if (modalResp && modalResp.modal) {
$.fancybox({
content: modalResp.modal,
minWidth: 400,
maxWidth: 800,
padding: 20,
autoSize: true
});
}
if (modalResp && modalResp.preview) {
$('.blockcart').replaceWith(modalResp.preview);
}
}
});
}
```
**Uwaga:** endpoint path `/module/ps_shoppingcart/ajax` może być w PL jako
`/module/ps_shoppingcart/ajax` (path nie tłumaczony) ale warto sprawdzić w Task 1
(zobaczyć w Network czy URL jest exactly ten).
Jeśli modal HTML używa custom CSS (PS blockcart modal), może być problem z
stylowaniem w fancybox. Alternatywa: append `modal` HTML do `` + show jako
native PS modal (Bootstrap `.modal.show`). Task 1 pokaże jak stary layout renderuje.
Avoid:
- Wywołanie obu: modal fetch + blockcart refresh (redundantnie) — modal resp już ma `preview`
- Zakładanie `resp.id_product` — PS może używać `idProduct` (camelCase) w JSON response
Po modal render, zbędny staje się nasz custom `.added-flash` animation. Zostaw
jako fallback gdy modal się nie zarenderuje.
Playwright live test:
1. Piece config + click add-to-cart
2. Network: 2 POSTy — pierwszy do /koszyk, drugi do /module/ps_shoppingcart/ajax
3. DOM: modal z response.modal HTML pokazuje się jako fancybox lub native modal
4. Modal zawiera:
- Nazwę produktu
- Link "Kontynuuj zakupy" (zamyka modal)
- Link "Przejdź do koszyka" (wskazuje na /pl/koszyk)
5. Navigate to /pl/koszyk → cart pokazuje "Szczegóły" button → klik rozwija customization data
AC-2, AC-3, AC-4 satisfied.
## DO NOT CHANGE
- Plan 02-01 override `totalpriceinfospecific` / `prod` no-op — nadal potrzebne żeby piece popup nie crash'ował. NIE restaurować squaremeter flow tutaj (zbyt ryzykowne + duży scope).
- `modules/squaremeter/*` — nie modyfikować modułu, żaden override ani edit.
- `themes/ayon/templates/catalog/_partials/*` — shared partials, nie ruszać.
- Stara gałąź `{if ... != '89.69.31.86'}` w `product.tpl` — nietknięta.
- Istniejący `$('#add-to-cart-or-refresh button').on('click')` w custom.js:327 — zostaje (niedziała w nowym, ale aktywne w starym).
- Istniejący capture-phase handler z Plan 02-02 — rozbudowa w miejscu, nie rewrite.
## SCOPE LIMITS
- Ten plan NIE dodaje UI dla live price calculation — cena w modalu/cart może być pokazana jako calculated value (read-only) ale bez UI do zmiany dimensions po dodaniu do cart. **Plan 02-04 scope.**
- Ten plan NIE dodaje dimension UI w product page (dimension input niezależny od piece popup) — user podaje wymiar przez piece popup (obecny mechanizm).
- Ten plan NIE restauruje squaremeter JS flow (header_surface.tpl etc.) — używamy BYPASS podejścia (bezpośrednio injection squaremeter fields w POST).
- Ten plan NIE rozwiązuje systemowego cache-buster dla `custom.js` — inline mirror z Plan 02-02 pozostaje. **Plan 02-05 scope.**
- Brak nowych dependencies.
Przed declaration complete (UNIFY gate):
- [ ] AC-1 verified Playwright: POST zawiera wszystkie squaremeter fields, response `id_customization > 0`.
- [ ] AC-2 verified Playwright: modal pokazuje się po add-to-cart, ma nazwy produktu + linki.
- [ ] AC-3 verified Playwright: w /pl/koszyk widać "Szczegóły" button, klik rozwija dane.
- [ ] AC-4 verified Playwright: single add per click, cart qty = 1 (nie 2).
- [ ] AC-5 verified Playwright: stary layout bez regresji (baseline network capture identyczny).
- [ ] Git diff clean: zero zmian w `modules/squaremeter/`, shared partials, starym layoucie.
- [ ] Kod identyczny w OBU miejscach (custom.js + product.tpl inline mirror) — diff między nimi = zero funkcjonalnych różnic.
- Wszystkie 5 AC pass w live Playwright test.
- Zero regresji w starym layoucie.
- Zero modyfikacji w module squaremeter.
- Plan 02-01 override nadal aktywny (nie przywracamy squaremeter JS).
- SUMMARY.md dokumentuje co działa, co deferred (Plan 02-04 cena UI, Plan 02-05 cache-buster).