# FiloBlu_Rma (`filoblu/mod2-extrma`)

Estensione del modulo RMA di Magento Commerce (`Magento_Rma`). Aggiunge il flusso di **cambio
taglia**, il flusso **easy return**, la mappatura dei motivi di reso, le policy di reso e una serie
di endpoint REST usati dagli storefront headless.

```
modulo Magento    FiloBlu_Rma          setup_version 2.2.12
namespace         FiloBlu\Rma\         (psr-4 sulla radice del repo)
sequence          Magento_Rma, Magento_Sales, FiloBlu_Refilo
autore originale  Jordy De Rosa
```

> **Stato di questo documento**: parziale. È stato scritto il 04.08.2026 a valle di un'analisi
> concentrata sul **flusso di cambio taglia** (ticket MNTFOS-1337) e documenta bene quella parte;
> easy return, policy di reso, etichette di spedizione, griglie admin e blocchi frontend sono
> nominati ma non approfonditi. Quello che c'è è stato verificato sul codice e sul DB di
> antonymorato; quello che manca è segnato in fondo. Integrare liberamente.

Dimensioni indicative: ~206 file PHP, di cui Model 55, Block 42, Plugin 32, Helper 14, Handlers 9,
Controller 7, Setup 6, Console 4, Observer 3. Nessuna suite di test: sotto `Test/` c'è solo
materiale `Draft/`.

---

## Flusso cambio taglia

L'idea: il cliente apre un reso scegliendo come risoluzione *Change Size* e indicando la taglia
che vuole al posto di quella resa. Il modulo crea **un nuovo ordine Magento** con metodo di
pagamento `changesizepay` e stato iniziale `waiting_for_rma_receiving`; da lì l'ordine segue il
normale flusso di evasione.

### Punto di ingresso

`Plugin/Model/RmaModelRmaProcessor::afterSaveRma` (plugin su `Magento\Rma\Model\Rma::saveRma`)
orchestra tutto il post-salvataggio del reso, in quest'ordine:

1. `EasyReturnHandler::saveEasyReturnFields`
2. `RmaItemResolutionHistoryHelper::saveRmaItemsData`
3. `setChangeSizeAttributeValuesBeforeCreateChangeSizeOrder` (vedi trappola 2)
4. `ChangeSizeItemsHelper::saveData`
5. **`CreateChangeSizeOrderHandler::checkIfCreateChangeSizeOrder`** ← creazione ordine
6. `RmaEmailHandler::sendWrongOrDamagedRmaEmail`
7. `RmaItemStatusUpdateHandler::shouldUpdateStatus`
8. `RmaEmailHandler::sendRmaEmail`
9. `RmaPoliciesHandler::checkPolicies`

### Condizioni perché l'ordine venga creato

Tutte in `Handlers/CreateChangeSizeOrderHandler::checkIfCreateChangeSizeOrder`:

1. `isChangeSizeEnabled($websiteId)` — flag di configurazione a livello **website**
2. item del reso in stato `pending`
3. `resolution` dell'item = opzione *Change Size*
4. nessun ordine di cambio taglia già collegato all'item
5. **valore `change_size` presente sull'item**, cioè il `product_id` della taglia di destinazione

Se manca la 1 il metodo esce lasciando un warning (`ChangeSize disabilitato per il website N:
nessun ordine generato per RMA X`, vedi §Log per il file giusto dove cercarlo); se manca una delle
altre esce e **non produce nulla di visibile**. La 1 e la 5 sono quelle che in produzione hanno
causato resi senza ordine.

### Strategia di creazione

`filoblu_disablerma_section/rma_group/rma_change_size_order_creation_strategy`:

- `each` — un ordine per ogni item richiesto (valore usato su antonymorato)
- `total` — un solo ordine con tutti gli item

### Recupero manuale

```bash
bin/magento filoblu:rma:generate:missing -i <rma_increment_id> -s <sku_nuova_taglia>
```

Genera l'ordine per un reso che non l'ha prodotto. **Non passa dal controllo
`isChangeSizeEnabled`**, quindi funziona anche su website dove la feature è disabilitata, e accetta
la SKU esplicitamente (`$rmaItem->getChangeSize() ?: $newProduct->getId()`), quindi funziona anche
quando il valore `change_size` non è mai stato salvato. Vedi le trappole 3, 4 e 5 prima di usarlo.

Altri comandi:

| Comando | Scopo |
|---|---|
| `filoblu:rma:generate:missing` | genera ordini di cambio taglia mancanti |
| `filoblu:easyrma:generate:missing` | genera dati easy return mancanti |
| `filoblu:sync-changesize-order-and-rma` | riallinea ordine di cambio taglia e reso |
| `filoblu:rma:restore` | ripristina un reso su un ordine |

---

## Configurazione

Sezione admin: **Stores → Configuration → FiloBlu → RMA Configuration**
(`filoblu_disablerma_section/rma_group/...`).

| Path | Note |
|---|---|
| `rma_change_size` | abilita il cambio taglia. **Scope website**, default `0`. Un website senza riga esplicita eredita il default, cioè disabilitato |
| `changesize_option_mapping` | `option_id` dell'opzione *Change Size* dell'attributo `resolution` |
| `rma_change_sizeorder_can_use_changesize` | se un ordine di cambio taglia possa a sua volta essere cambiato |
| `rma_change_size_order_creation_strategy` | `each` \| `total` |
| `add_shipping_costs` | se addebitare la spedizione sull'ordine di cambio taglia |
| `allowed_change_size_order_status` | stati verso cui un ordine di cambio taglia può transitare liberamente (vedi trappola 6) |
| `rma_auto_authorized` | autorizzazione automatica del reso |
| `sales/rma_reason/reason` | mappatura `brand_reason` (option_id del motivo) → `standard_reason` (`wrong_size`, `damaged_product`, `changed_my_mind`, …) |
| `payment/changesizepay/*` | metodo di pagamento offline dell'ordine di cambio taglia. Su antonymorato `active=1` a scope default e `allowspecific=0` |

Il cambio taglia viene offerto **solo** se il motivo selezionato mappa su `wrong_size`
(`RmaHelper::STANDARD_REASON_ID_FOR_CHANGESIZE`) — vedi `ChangeSize::isSelectedReasonWrongSize`.

---

## Dati

### Tabelle del modulo

| Tabella | Contenuto |
|---|---|
| `filoblu_rma_items` | `rma_item_id → order_id`: è l'extension attribute `change_size_order_id`, cioè il collegamento fra item del reso e ordine di cambio taglia. Gestita da `Model/Rma/RmaExtensionAttributesRepository` |
| `filoblu_change_size_items` | dettaglio della richiesta: ordine e item padre, reso, SKU vecchia e nuova, ordine di cambio taglia generato |
| `filoblu_easy_rma_items` | dati indirizzo/telefono della richiesta easy return (chiave `rma_entity_id`) |
| `filoblu_rma_items_resolution_history` | storico dei cambi di risoluzione per item |
| `filoblu_change_size_matrixrate` | tariffe di spedizione del cambio taglia. **Su antonymorato è vuota per tutti i website** |

### Tabelle core coinvolte

`magento_rma`, `magento_rma_item_entity` e i suoi EAV
`magento_rma_item_entity_{int,varchar,text,decimal,datetime}`.

> Attenzione: `resolution`, `condition`, `reason` e `change_size` sono **attributi EAV**, non
> colonne di `magento_rma_item_entity`. Una query sull'item deve fare join sulle tabelle EAV: i
> primi tre sono `int` e stanno in `magento_rma_item_entity_int`, `change_size` è `varchar` e sta
> in `magento_rma_item_entity_varchar`.

Gli `attribute_id` e gli `option_id` dipendono invece dall'ordine di installazione, quindi non hanno
alcun valore fuori dall'installazione da cui li si legge: vanno sempre ricavati dal DB su cui si sta
lavorando. Nemmeno gli ambienti di uno stesso progetto coincidono — su morato l'opzione
*Change Size* dell'attributo `resolution` è `460` in produzione e `459` in stage. Nel codice
l'`option_id` di *Change Size* si legge da `changesize_option_mapping`, mai hardcodato.

---

## Endpoint REST

`etc/webapi.xml` — 7 route, **tutte dichiarate `anonymous`**.

| Metodo | URL | Servizio |
|---|---|---|
| POST | `/V1/filoblu-rma/changesize` | `getSizes` — taglie alternative disponibili |
| POST | `/V1/filoblu-rma/getChangeSizeEnabledProducts` | `getChangeSizeEnabledProducts` |
| GET | `/V1/filoblu-rma/returnsAttributeWithAlternativesMetadata/itemId/:itemId/qty/:qty` | `getAllAttributesWithAlternativesMetadata` — motivi e risoluzioni ammessi |
| POST | `/V1/filoblu-rma/getlabel` | `getLabel` |
| POST | `/V1/filoblu-rma/estimatereturnshippingcost` | `estimateReturnShippingCost` |
| GET | `/V1/filoblu-rma/getReturnableItems/:orderId` | `getReturnableItems` |
| POST | `/V1/filoblu-rma/getreturntypes` | `getReturnTypes` |

Essendo route anonime, **i parametri in ingresso non sono attendibili**: vedi trappola 1.

### Chi le consuma (verificato su antonymorato, 04.08.2026)

Lo storefront headless non chiama Magento direttamente: passa da **honey**
(`beehive-api/honeyv2`), che nei suoi datasource usa **solo 4** delle 8 route —
`returnsAttributeWithAlternativesMetadata`, `getReturnableItems`, `estimatereturnshippingcost`,
`getreturntypes`. `getSizes` e `getChangeSizeEnabledProducts` **non sono chiamate da honey**: le
taglie arrivano già annidate nelle opzioni della risoluzione (`ChangeSize.php`,
`$option->setOptions($sizes)` dentro `getAllowedResolutions`), quindi una sola chiamata restituisce
risoluzioni e taglie insieme.

Lo store code è propagato correttamente su tutta la catena: lo storefront imposta l'header `store`
(`refilo-storefront/src/client/system/api.js:55-58`, da `window.INITIAL_STATE.store.store`), honey lo riceve come
`@param.header.string('store')` e lo mette nell'URL `/rest/{store}/V1/filoblu-rma/...`. Sul percorso
del cliente **non esiste** quindi una chiamata "senza store code" che cada sul website di default:
la divergenza della trappola 1 nasce dallo store view di navigazione, non da un contesto mancante.

Lo storefront però non è l'unico consumer. Il JS della creazione RMA di admin
(`view/adminhtml/web/js/form/changesize.js`) chiama `/rest/V1/filoblu-rma/changesize` e
`/rest/V1/filoblu-rma/returnsAttributeWithAlternativesMetadata/...` **senza store code**, quindi
quelle richieste si risolvono sullo store di default: è un percorso in cui il contesto è davvero
assente, e il motivo per cui il website va derivato dall'ordine anche negli endpoint che sembrano
serviti solo allo storefront.

## Observer

| Evento | Observer |
|---|---|
| `sales_order_save_before` | `Observer/UpdateStatusVerify` |
| `checkout_submit_all_after` | `Observer/Sales/Order/FreeReturnShipping` |
| `filoblu_override_theoric_shipping_amount` | `Observer/OverrideTheoricShippingAmount` |

## Log

Logger dedicato `FiloBlu\Rma\Logger\Logger` (estende Monolog) →
**`var/log/filoblu_extrma.log`**, canale `filoblu_extrma_logger`. È il primo posto dove guardare.

Non tutto il modulo lo usa, però: le classi che si fanno iniettare `Psr\Log\LoggerInterface` invece
di `FiloBlu\Rma\Logger\Logger` scrivono nei log standard di Magento. `CreateChangeSizeOrderHandler`
è una di queste, quindi il warning sul cambio taglia disabilitato finisce in **`var/log/system.log`**
sul canale `report`, non in `filoblu_extrma.log`.

Su `system.log` serve **`grep -a`**: il file contiene byte che grep considera binari e senza `-a`
risponde solo `binary file matches`, senza stampare la riga trovata.

---

## Trappole note

### 1. Il website va risolto dall'ordine, non dal contesto

`rma_change_size` è a scope **website**. Storicamente lettura e scrittura lo risolvevano in modo
diverso: le opzioni mostrate al cliente lo prendevano dal contesto della richiesta
(`storeManager->getWebsite()`), la creazione dell'ordine dallo store dell'ordine.

Il contesto della richiesta è lo store view che il cliente sta **navigando**, che non è
necessariamente quello in cui ha ordinato. Un reso aperto navigando uno store view diverso da quello
dell'ordine — ad esempio ordine su `gb`, dove il cambio taglia è spento, e navigazione su `eu`,
`int` o `roe`, dove è attivo — otteneva la lista delle risoluzioni dal website di navigazione:
*Change Size* veniva offerto, il reso salvato con quella risoluzione, e nessun ordine creato perché
la creazione valutava il website dell'ordine. Tutto in silenzio.

Corretto in **MNTFOS-1337** su `ChangeSize::getAllowedResolutions`, `ChangeSize::getSizes`,
`ChangeSize::getChangeSizeEnabledProducts` e `Plugin/Block/Form/Renderer/Select`. Il website si
deriva con `RmaHelper::getStoreByOrderItemId($orderItemId)`. **Non reintrodurre
`storeManager->getWebsite()` in codice di frontend o API.**

Due corollari sui parametri in ingresso:

- `getSizes` e `getChangeSizeEnabledProducts` ricevono ancora un `$websiteId` in firma (mantenuto
  per compatibilità dell'interfaccia) che **non viene usato**. Non fidarsene.
- `getChangeSizeEnabledProducts` risolve il website dal **primo** `order_item_id` della lista e
  applica l'esito a tutta la lista. Finché gli item arrivano da un solo ordine è corretto; con item
  di ordini diversi decide il primo, quindi o passa anche un item di un website disabilitato, o —
  se il primo item è di un website disabilitato — la risposta torna vuota scartando anche gli item
  legittimi.

Lo stesso vale per il **form RMA di admin**, che ha una sorgente di opzioni tutta sua. Le tre select
`resolution` del backend (`NewRma\Tab\Items`, `NewRma\Tab\Items\Grid`, `Edit\Tab\Items\Grid`)
prendono le opzioni da `Magento\Rma\Helper\Eav::getAttributeOptionValues('resolution')`, che
restituisce tutte le opzioni dell'attributo senza guardare l'ordine: senza filtro il backend espone
*Change Size* anche su un website dove è spenta, con lo stesso esito silenzioso del frontend — reso
salvato con quella risoluzione e nessun ordine creato.

Il filtro è `Plugin/Rma/Helper/Eav`, registrato in `etc/adminhtml/di.xml` e quindi attivo solo in
area admin: risolve il website dall'ordine in request (`order_id`, che sopravvive anche alla
chiamata ajax `addProductGrid` perché usa `_current => true`) e toglie l'opzione quando la feature è
disabilitata. Vale **solo in creazione**: sulla edit di un reso già salvato rimuovere l'opzione
svuoterebbe la select del valore corrente, quindi lì non interviene.

Attenzione al perimetro: il filtro nasconde l'opzione, non la rifiuta. Una POST costruita a mano può
ancora salvare quella risoluzione, e in quel caso l'unica traccia è il warning di
`CreateChangeSizeOrderHandler`.

### 2. `change_size` viene rimosso e reinserito in memoria

`RmaModelRmaProcessor::beforeSaveRma` **rimuove** `change_size` dai dati degli item prima del
salvataggio (M2.4.7 rifiuta attributi select senza opzioni), tenendolo in una proprietà
dell'istanza, e `setChangeSizeAttributeValuesBeforeCreateChangeSizeOrder` lo reinserisce sulle
extension attributes prima della creazione ordine. È un passaggio di stato in memoria fra due
metodi dello stesso plugin: se l'abbinamento per `order_item_id` non torna, il valore si perde e
l'ordine non viene creato.

### 3. `filoblu:rma:generate:missing` fallisce in silenzio

`Console/GenerateMissingChangeSizeOrders` incapsula la generazione in un
`catch (\Exception $e) { $x = null; }`: se qualcosa va storto il comando **esce con codice 0, senza
output e senza log**. Con questo comando *nessun output non significa riuscito*: verificare sempre
sui dati (`filoblu_rma_items`, `change_size_order_id`, esistenza dell'ordine).

Inoltre l'abbinamento item ↔ SKU è **posizionale** (`$newSku = $skus[$count]`): su resi con più
item l'ordine dei valori passati a `-s` conta.

### 4. `generateMissingOrder` scrive l'ordine sbagliato, poi si corregge

`CreateChangeSizeOrderHandler::generateMissingOrder` scrive in `filoblu_rma_items` l'id
dell'**ordine padre** invece di quello dell'ordine di cambio taglia. Il valore viene corretto
subito dopo da `ChangeSizeItemsHelper::generateOrder` → `updateFiloBluRmaItem`, quindi il risultato
netto è giusto — ma se quel secondo write fallisse il collegamento resterebbe sbagliato. Da
verificare dopo ogni recupero manuale.

### 5. Rilanciare il comando con una SKU diversa crea duplicati

`findOrCreateChangeSizeItem` cerca la riga per la terna `rma_item_id` + `parent_product_sku` +
`new_product_sku`. Se il primo tentativo è andato con una SKU sbagliata, un secondo tentativo con
la SKU corretta **non riusa** quella riga: ne crea una seconda e lascia un orfano con
`change_size_order_id = 0`. Correggere o cancellare la riga prima di rilanciare.

### 6. Gli ordini di cambio taglia non cambiano stato liberamente

`Observer/UpdateStatusVerify` solleva `LocalizedException` se si prova a cambiare lo stato di un
ordine di cambio taglia prima che il reso collegato sia processato, a meno che lo stato di
destinazione sia in `allowed_change_size_order_status` o `waiting_for_rma_receiving`. Un ordine
fermo in `waiting_for_rma_receiving` sta aspettando la lavorazione del reso: è di processo, non un
bug.

### 7. La generazione manda la mail al cliente

Sia il flusso automatico sia `generateMissingOrder` chiamano `orderSender->send($order)`. Non
esiste modalità di prova: appena il comando riesce, il cliente riceve la conferma d'ordine.

### 8. Esistono due `isChangeSizeEnabled()`, con scope diversi

| Metodo | Argomento | Scope letto |
|---|---|---|
| `ChangeSizeAttributeHelper::isChangeSizeEnabled($websiteId)` | website id | `SCOPE_WEBSITE` |
| `ChangeSizeHelper::isChangeSizeEnabled($storeId)` | store id | `SCOPE_STORE` |

Stesso nome, stessa configurazione letta, argomento diverso. Entrambi funzionano — uno store
risolve comunque sul suo website — ma passare un **website id** al secondo, o uno **store id** al
primo, legge lo scope sbagliato e **non dà nessun errore**: restituisce semplicemente il valore di
un altro website, o il default. Prima di chiamarlo, guardare quale dei due helper è iniettato.

### 9. `changesize.js`: il blocco `ready()` deve stare in fondo

In `view/adminhtml/web/js/form/changesize.js` tutte le funzioni sono assegnate a `var`
(`var f = function() {...}`), quindi esistono solo dopo che il corpo del modulo è stato eseguito.
Magento 2.4 usa **jQuery 1.12**, dove un callback registrato con `$(document).ready()` a DOM già
pronto — sempre il caso sotto RequireJS, che carica dopo `DOMContentLoaded` — viene eseguito in modo
**sincrono**. Un `ready()` in testa al file gira quindi con le `var` ancora `undefined`.

Il danno non si ferma al modulo: la `TypeError` interrompe l'esecuzione del resto del file (niente
`ajaxComplete`, quindi niente `addFormCell`) **e** abortisce il ciclo RequireJS in corso, per cui
`window.rma = new AdminRma()` — dichiarata in `Magento_Rma/view/adminhtml/templates/new/general.phtml`
— non viene mai eseguita. I sintomi visibili sono la griglia degli item dell'ordine che resta vuota
in creazione RMA e `rma is not defined` al click su *Add Products*: entrambi puntano altrove, ma la
causa è qui.

Chi tocca il file: le invocazioni vanno in fondo, dopo tutte le assegnazioni, oppure le funzioni
vanno dichiarate come `function f() {}`.

---

## Cosa questo documento non copre

- **Easy return**: `Model/EasyReturn`, `Handlers/EasyReturnHandler`, `filoblu_easy_rma_items`,
  comando `filoblu:easyrma:generate:missing`
- **Policy di reso**: `Handlers/RmaPoliciesHandler`, `Helper/RmaPoliciesHandler`
- **Etichette di spedizione e corrieri**: `magento_rma_shipping_label`,
  `Handlers/CustomCarrierHandler`, le griglie matrixrate
- **Email**: `Plugin/EmailTemplate`, `Model/RmaEmailManagment`, i template sotto
  `view/frontend/email/`
- **Blocchi e griglie admin** (42 file in `Block/`) e i 32 plugin in `Plugin/`, di cui qui sono
  citati solo quelli sul percorso del cambio taglia
- **Rimborsi e credit memo**: `Helper/RefundParentOrderHelper`,
  `Plugin/Helper/RmaHelperCreditmemoProcessor`
- **Integrazione pagamenti**: `Plugin/Model/Payment/Kp.php`, `Helper/OfflinePaymentMethodsHelper`
- **La coda storica**: su antonymorato risultano ~130 item con risoluzione *Change Size* e nessun
  ordine collegato, distribuiti su più store negli anni. Mai passati in rassegna; è probabilmente
  il motivo per cui `GenerateMissingChangeSizeOrders` esiste
