- 1. Regel-prijs (basis-formule)
- 2. Default-aantal per record (
suggestAantal) - 3. Afgeleid aantal (
bronVoorAantal+ pakLengte + snijfactor) - 4.
snijFactor()— bron-volgorde voor snijverlies - 5. Multi-keuze limiet (profielen)
- 6. Stuks-display voor plinten/profielen
- 7. Producten-tabel (snijverlies + pak-rounding)
- 8. Order-overzicht totalen
- 9. Uitgewerkte voorbeelden
- 10. Zoho-veld-mapping
1. Regel-prijs (basis-formule)
Voor elke regel in de selectie (uitgevoerd door regelPrijs(sel)):
| Veld | Bron | Default |
|---|---|---|
aantal | sel.aantal (door klant of suggestAantal ingevuld) | — |
aantalMin | art.aantalMin uit TSV/Zoho | 0 of 1 |
prijsPerUnit | art.prijsPerUnit uit Zoho Products | verplicht |
snijFactor(art) | Helper met bron-volgorde: omrekenfactor → snijverlies-veld → default (zie §4) | 1.10 voor Vloeren, 1.0 voor services |
berekening | art.berekening: Variabel of Vast | Variabel |
aantalMin, wordt voor de prijs-berekening alsnog aantalMin gebruikt. Voorbeeld: leg-egaliseren heeft aantalMin=35 m² — een ruimte van 20 m² wordt op 35 m² gefactureerd.2. Default-aantal per record (suggestAantal)
Wanneer een record nieuw aan de selectie wordt toegevoegd (Verplicht-auto-add of klant klikt Toevoegen) berekent suggestAantal(art, r) het default-aantal in deze volgorde (early-return op de eerste match):
| # | Conditie | Resultaat |
|---|---|---|
| 1 | productCode === 'leg-houtvisgraat' | r.m2 — hout-visgraat-leg-arbeid; Zoho-eenheid is m¹ maar berekening volgt ruimte-m² |
| 2 | Jumpax-pair-codes (861202, Xcx170123) | ceil(r.m2 / rolM2) × rolM2 — pair-products verkocht per volle rol (861202 = 50 m²/rol, Xcx170123 = 15 m²/rol) |
| 3a | productCode === 'leg-dichtzet-vlrvrwa' + r.aanleggenVerwarming === 'LAB21' | r.vloerverwarmingM2 (fallback r.m2) — dichtzet volgt VV-oppervlak (alleen freesgangen worden dichtgezet) |
| 3b | productCode === 'leg-vloerverwarm-ega' + r.aanleggenVerwarming === 'LAB21' | r.m2 — egaliseren wordt altijd over de volledige basisvloer gedaan, ook waar geen VV ligt (je kunt geen halve egalisatielaag aanbrengen) |
| 3c | productCode matcht /^Leg-Vloerverw-gr\d+$/i | 1 — Vast bedrag per gr-tier; m2Range selecteert welk record fired |
| 3d | productCode === 'VV-Afvoerspullen' | N (aantal VV-groepen uit Leg-Vloerverw-grN), fallback 1 — 1× afvoerspullen per groep |
| 3e | productCode === 'VV-brandstof' | 1 — Vast bedrag; dedupe naar 1× per order in syncVVBrandstof |
| 4 | art.aantalMin === 0 | 0 — klant past zelf aan (Leg-massieve-plint, leg-platteplint Verplicht-records) |
| 5 | maten ∈ {Stuk, St, Koker, Bus} (case-insensitive) | art.aantalMin || 1 (Leg-kast, Leg-toilet, Leg-techruimte, KC25 bij directe selectie) |
| 6 | art.bronVoorAantal gezet | Afgeleid uit bron — zie §3 |
| 7 | maten === 'm1' | r.m1Rand (default 0 als niet ingevuld in ruimte-info) |
| 8 | fallback | r.m2 (ruimte-oppervlakte) |
Dynamische rebind (syncVerplichteSelecties §1b)
Voor cases 1, 2, 3a-e herrekent de §1b-loop het aantal bij elke render — niet alleen bij toevoeg-moment. Dit zorgt dat:
- Leg-houtvisgraat automatisch meegroeit met
r.m2als de klant de oppervlakte aanpast. - Jumpax-pair-rollen meebewegen wanneer een andere Jumpax-keuze wordt gemaakt (via de auto-chain in
pickArtikel). - VV-dichtzet meezakt zodra
r.vloerverwarmingM2wordt verlaagd (centraal inVV_VOLG_VVM2_PCODES); VV-egaliseren volgt juist altijdr.m2(volledige basisvloer). - VV-Afvoerspullen volgt het aantal VV-groepen — bv. zodra de klant
r.vloerverwarmingM2aanpast en daardoor een andere Leg-Vloerverw-grN fired (gr3 → gr4), schaalt het aantal afvoerspullen mee (3 → 4). - VV-brandstof wordt door
syncVVBrandstofgededupliceerd tot 1× per order in de hoogste-verdieping-ruimte.
Prompt bij €0-regel
Bij pickArtikel voor een Optioneel/Keuze record met aantal=0 én maten m¹/m²/m²: verschijnt een prompt "Vul aantal m¹ in voor [naam]". Klant kan annuleren (geen toevoeging) of bedrag invullen. Voorkomt €0-regels in de selectie.
3. Afgeleid aantal (bronVoorAantal + pakLengte + snijfactor)
Records met een bronVoorAantal-veld krijgen hun aantal afgeleid van een ander record in de selectie. Drie velden bepalen de formule:
| Veld | Betekenis |
|---|---|
bronVoorAantal | productCode van het parent-record waar het aantal vandaan komt |
pakLengte | lengte per stuk/pak: 2.4m voor plinten, 1m voor profielen, 20m voor kit-bus, 75.55m voor PVC voegstrip-koker |
snijfactor | snijverlies-factor: 1.10 (=10% extra) voor plinten/deklijsten, 1.0 voor kit/profielen |
Formule per maten-type
Praktische toepassingen
| Parent | Vervolg | pakLengte | snijfactor |
|---|---|---|---|
| leg-platteplint (m¹) | 162 platte-plint productcodes (m¹) | 2.4 | 1.10 |
| Leg-massieve-plint (m¹) | 45 deklijsten (m¹) | 2.4 | 1.10 |
| leg-hogeplint (m¹) | 28 hoge plinten (m¹) | 2.4 | 1.10 |
| Leg-profiel (Stuk) | 62 profielen (St) | 1 | 1.0 |
| Leg-afkithogeplint (m¹) | KC25 290261 (Stuk) | 20 | 1.0 |
| Leg-afkitvloer (m¹) | KC25 100325 (Stuk) | 20 | 1.0 |
| Leg-pvcbies (m¹) | 419210 voegstrip (Koker) | 75.55 | 1.0 |
| Leg-pvcbies (m¹) | 419206 voegstrip (Koker) | 72.80 | 1.0 |
Voorbeeld 1 — plint bij leg-hogeplint = 25 m¹
aantal_plint = ceil(25 × 1.10 / 2.4) × 2.4 = ceil(11.458) × 2.4 = 12 × 2.4 = 28.8 m¹Display: 12 stuks à 2,4m (28,8 m¹)
Prijs (à €7,95/m¹): 28.8 × 7.95 = €228,96
Voorbeeld 2 — KC25 290261 bus bij Leg-afkithogeplint = 50 m¹
aantal_bus = max(1, ceil(50 × 1.0 / 20)) = max(1, 3) = 3 bussenPrijs (à €14,95/stuk): 3 × 14.95 = €44,85
Voorbeeld 3 — PVC voegstrip 419210 bij Leg-pvcbies = 30 m¹
aantal_koker = max(1, ceil(30 × 1.0 / 75.55)) = max(1, 1) = 1 kokerPrijs (à €150/koker): €150
Bij 100 m¹: ceil(100/75.55) = 2 kokers = €300.
4. snijFactor() — bron-volgorde voor snijverlies
De multiplier voor snijverlies / pak-conversie wordt centraal bepaald in snijFactor(art). Bron-volgorde:
Gebruikt door regelPrijs(sel) én door de Producten-tabel-loop (§7). Voor Xcx-MV88001-7-03 (Sensation PVC met snijverlies=5) levert dit factor 1.05 — niet langer het verkeerde 1.10-default uit oude code.
| Record-soort | Velden | Resultaat |
|---|---|---|
| Vloeren met snijverlies-veld | snijverlies=5, omrekenfactor=null | 1.05 (5% snij) |
| Vloeren zonder snij-data | beide null | 1.10 (10% default voor Vloeren) |
| Accessoires-plinten | omrekenfactor=2.4, snijverlies=10 | 2.4 (pak-conversie wint) |
| Services (Diensten) | beide null | 1.0 (geen impliciete snij) |
Override: 18% snij bij PVC-Lijm-visgraat met Leg-pvcband
In de Producten-tabel-loop (§7) wordt de snij-factor overgeschreven naar 1.18 wanneer:
- De vloer-subcategorie bevat
PVC - De vloer-installatie bevat
Gelijmd - De vloer-patroon bevat één van
{Visgraat, Walvisgraat, Hongaarse punt, Weense punt} - In dezelfde ruimte staat een
Leg-pvcband-sel met aantal > 0
Reden: bies-werk in combinatie met een grafisch punt-patroon geeft fors meer afval dan het standaard 10%. De 18%-bump zit alleen in renderOrderSummary's Vloeren-loop — snijFactor() zelf is contextloos en blijft de bron-volgorde-helper.
5. Multi-keuze limiet (profielen)
Parents in MULTI_KEUZE_PARENTS (momenteel {Leg-profiel}) staan meerdere kind-keuzes tegelijk toe, met een gesommeerde limiet op het parent-aantal. De Keuze-modal toont per rij een aantal-input + ± knoppen en een live usage-banner.
Voorbeeld — Leg-profiel parent met aantal=5:
- Klant kiest profiel A in modal → default 5 (remaining = 5 − 0). Stelt in op 2.
- Klant kiest profiel B → default 3 (remaining = 5 − 2). Stelt in op 3.
- Banner toont "5 van 5 profielen toegewezen · limiet bereikt" (groen).
- + op profiel B → invoer wil 4 maar clamp blokt → blijft 3.
- Klant verhoogt parent naar 6 → banner toont "5 van 6 · nog 1 beschikbaar".
6. Stuks-display voor plinten/profielen
Achter de schermen wordt sel.aantal voor plinten opgeslagen in m¹ (12 m¹ × €7,95 = €95,40). Voor klant-display zet formatAantalStuks(art, aantal) dit om naar stuks:
Geldt voor:
- Gekozen-vloer-strip · keuze-modal · keuze-groep-card
- Order-overzicht Service-tabel
Klant ziet "5 stuks à 2,4m (12 m¹)" en betaalt voor 12 m¹ — geen confusie tussen stuks-prijs en m¹-prijs.
7. Producten-tabel (snijverlies + pak-rounding)
De Producten-tabel toont fysieke SKU's. Welke regels horen daar thuis? isFysiekProduct(art) matcht:
hoofdcategorie === 'Vloeren'(PVC/Hout/Laminaat-vloeren)conditioneelOp ∈ FYSIEK_PRODUCT_PARENTS= {leg-hogeplint, leg-platteplint, leg-massieve-plint, Leg-profiel, Leg-afkithogeplint, Leg-afkitvloer, leg-sysondervloer, leg-spaanplaten} — dus alle MDF-plinten, profielen, kit-bussen (KC25), Jumpax-systeem-ondervloeren met pair-products en spaanplaten, ook al hebben ze in Zohohoofdcategorie=Diensten.- Plus deurmat-mat (XCX-*) via aparte auto-chain. Een ruimte mag meerdere deurmat-instances bevatten — elk met eigen
deurmatInstanceId, eigen kleur/maat-config en eigen mat-chain (zie werking.html §8).
Voor Vloeren-records komt daar pak-rounding bovenop:
| Kolom in tabel | Veld | Opmerking |
|---|---|---|
| Artikel | art.naam + art.productCode | volledige Zoho-naam |
| Prijs/u | €${prijsPerUnit} / m² | per eenheid |
| Aantal incl. snij | ${teLeveren} m² incl. ${snijverliesPct}% snij | wat klant betaalt en geleverd krijgt |
| Totaal | €${regelTotaal} | = prijsPerUnit × teLeveren |
8. Order-overzicht totalen
Het rechter-paneel toont per ruimte een Producten- en Service-totaal, plus globale Project-aggregaties.
Per ruimte
Service-subkoppen
| Subkop | Filter |
|---|---|
| Artikel | hoofdcategorie === 'Accessoires' (ondervloeren, hulpmaterialen — niet plinten/kit, die staan onder Producten) |
| (geen sub-header) | hoofdcategorie === 'Diensten' && onderdeel !== 'Verwijderen' && !isFysiekProduct(art) — flat als leg-arbeid-bucket |
| Verwijderen | hoofdcategorie === 'Diensten' && onderdeel === 'Verwijderen' |
De extra !isFysiekProduct-filter verplaatst plinten, profielen en kit-bussen uit de Service-bucket naar Producten (waar ze conceptueel thuishoren — fysieke SKU's die LAB21 inkoopt).
Project-aggregaties
- Ondergrond: m² per basisvloer-type, gesommeerd over alle ruimtes (uit
r.basisvloer+r.basisvloerM2) - Totaal vloeroppervlakte: Σ r.m2 over alle ruimtes
- Containers nodig: rule-of-thumb 1 container per 30 m² verwijderd materiaal (sels met onderdeel=Verwijderen + hoofdcategorie=Diensten)
Vloer-m² overschrijdt ruimte-oppervlakte
Veiligheids-check per ruimte: Σ vloer-product.aantal > r.m2 + 0.01 m² → banner-waarschuwing (zie werking.html §6). Fysiek onmogelijk om meer vloer te leggen dan de ruimte groot is.
9. Uitgewerkte voorbeelden
Voorbeeld A — Hout-verlijmd-LAB21, 60 m² woonkamer
Klant kiest een Albero Parket-product (€99,95/m², omrekenfactor 1.0) + chip Lijm + LAB21 legt. Auto-add:
- leg-egaliseren — niet auto (Hout-verlijmd geen egal-warning sinds banner-fix)
- Leg-verlijmdhout Verplicht (€49,95/m²) → 60 m² × €49,95 = €2.997
- Leg-massieve-plint Optioneel met aantalMin=0 — klant ziet maar staat op 0
- Vloer-product Producten-sectie: 60 m² × 1.0 factor = 60 m² te leveren × €99,95 = €5.997
Ruimte-totaal voorlopig: €2.997 + €5.997 = €8.994. Klant kan plint-aantal verhogen voor kozijnafwerking.
Voorbeeld B — PVC zwevend Klik (klant-zelf legt), 130 m²
Klant kiest een Chablis XL Klik PVC-product met geïntegreerde ondervloer (€56,95/m², omrekenfactor 1.10).
- Geen leg-arbeid auto-add (uitvoeringLeggen=Klant — leg-PVCklik filter faalt)
- Sectie 5: klant ziet Optioneel 4944250419 (Trans Klik PVC ondervloer 7mm, €7,95/m²) wegens
geintegreerdeOndervloer=Ja - Producten-sectie: 130 × 1.10 = 143 m² te leveren × €56,95 = €8.143,85
- Containers: 0 (geen verwijderen-records)
Voorbeeld D — PVC Sensation Xcx-MV88001-7-03, 21 m² (snijverlies 5%)
Klant kiest Xcx-MV88001-7-03 · Sensation PVC Reisa Oak (€29,95/m²). Productdata: snijverlies=5, omrekenfactor=null, pakgrootte=4,46.
snijFactor(art)→ omrekenfactor null → snijverlies=5 → 1.05inclSnij = 21 × 1.05 = 22.05 m²pakken = ceil(22.05 / 4.46) = 5teLeveren = 5 × 4.46 = 22.30 m²regelTotaal = 29.95 × 22.30 = €667,89- Display: "22,30 m² · incl. 5% snij" — niet langer "10%" zoals voor de snijFactor-fix.
Met de oude code (factor 1.10): inclSnij=23.1, pakken=ceil(5.18)=6, teLeveren=26.76 m², totaal=€801,32. Het verschil van €133,43 ontstond doordat de pak-rounding op de 6e in plaats van 5e pak uitkwam.
Voorbeeld E — PVC-lijm-LAB21 op Houten planken (instabiel), 25 m², 2e verdieping
Klant kiest een PVC-lijm-product + chip Lijm + LAB21 legt + basisvloer = Houten planken (instabiel) + Type VV = Geen + VVE = Nee + verdieping = 2e. Cascade:
syncDroogbouwAutoTriggerscenario 2 detecteerthoutBasis && pvcLijmTrigger. m²-tier-lookup vindtDroogbouw20-29(m2Range 20–29) → Aanbevolen auto-add met aantal = r.m2 = 25, prijs €164,95/m² → €4.123,75.VOORBEREIDEN-11407(leg-fermacell-ega) matcht basisvloer = Houten planken (instabiel) + 6 user-criteria → Verplicht auto-add, aantal = 25, prijs €15,95/m² → €398,75.VOORBEREIDEN-11001(leg-egaliseren) Verplicht voor PVC-Lijm-LAB21, aantal = 25, prijs €16,95 → €423,75.DROOGBOUW-10006(Droogbouw-kraan) heeft conditioneelOp matchingDroogbouw20-29én verdieping ∈ {1e, 2e, 3e, Hogere} → Verplicht auto-add, 1× €799,95 → €799,95.leg-PVClijmVerplicht voor PVC-Lijm-LAB21, 25 m² × €16,95 → €423,75.
Bij m² = 60 zou Droogbouw50-99 in plaats van 20-29 worden gekozen (60 × €99,95 = €5.997). Op begane grond (verdieping = "Begane grond") vervalt de Droogbouw-kraan (€799,95).
Voorbeeld F — PVC-lijm-LAB21 op Noppen platen-basisvloer (laagdikte-garantie), 40 m²
Klant kiest een PVC-lijm-product + chip Lijm + LAB21 legt + basisvloer = Noppen platen + Type VV = Geen + VVE = Nee. De basisvloer is een gipsachtige plaat met noppen waar normaal VV-buizen in komen; ook zonder VV is een dikke egalisatielaag nodig om de noppen volledig te verzwijgen. Cascade:
VOORBEREIDEN-11001(leg-egaliseren) Verplicht voor PVC-Lijm-LAB21 → aantal = 40, prijs €16,95 → €678,00VOORBEREIDEN-11023(Leg-gipsegaline) Verplicht — gipsachtige hechtlaag op Noppen platen-basisvloer → 40 × €2,95 → €118,00VOORBEREIDEN-11409(leg-vloerverwarm-ega, nieuw) Verplicht — laagdikte-garantie analoog aan gefreesde VV om de noppen af te dekken → 40 × €15,95 → €638,00leg-PVClijmVerplicht voor PVC-Lijm-LAB21 → 40 × €16,95 → €678,00
Subtotaal voorbewerking + leg-arbeid: €2.112,00 (excl. vloerproduct + transport). De 11020-rule (leg-vloerverwarm-ega via typeVerwarming) firet hier niet — die vereist typeVerwarming ∈ Gefreesd/Noppen platen, en het user-scenario heeft typeVerwarming = Geen.
Voorbeeld C — Laminaat-LAB21, 45 m², met visgraat-patroon
Klant kiest een Laminaat-product met patroon Visgraat + chip Zwevend + LAB21.
- leg-laminaat Verplicht — basis leg-arbeid auto-add (€16,95/m² × 45 = €762,75)
- Leg-laminaatvisgraat Verplicht — visgraat-toeslag auto-add (€8,95/m² × 45 = €402,75)
- Auto-chain naar 91632 (Parketman D3 nadenlijm) via chainProduct → 1× €8,95 = €8,95
- Klant kiest uit Set A keuze-records (861442 / 861202 / etc.) via "Kies laminaat ondervloer · 7 opties"-knop
- Banner-check: als basisvloer = Houten planken → waarschuwing "LAB21 legt geen laminaatvisgraat op houten basisvloer"
10. Zoho-veld-mapping
Zoho Products → product-record fields (geverifieerd via getFields(Products) op 2026-05-17). Mapping in tools/zoho-pull-catalog.mjs → normalizeProduct().
| Zoho-API-naam | JS-veld | Gebruik in berekening |
|---|---|---|
Unit_Price | prijsPerUnit | basis-prijs voor regelPrijs |
Usage_Unit | maten | m¹/m²/Stuk/Koker/Bus — bepaalt aantal-bron + display |
Omrekenfactor | omrekenfactor | snijverlies-factor voor vloer-producten |
Snijverlies | snijverlies | snijverlies-percentage (display) — 5 / 10 / 15 |
Pakgrootte | pakgrootte | info-veld (m² per pak — display in vloerdetails) |
Stroken | strokenPerPak | info-veld (formula in Zoho) |
Manufacturer | merk | filter vereistMerk + display |
Categorie_1 | subcategorie | filter — vloertype Hout/Laminaat/PVC |
Product_Category | hoofdcategorie | filter — Vloeren/Accessoires/Diensten |
Laying_Methode | installatie | filter — Zwevend/Gelijmd/Keuze |
Legpatroon_2 | patroon | filter patroon + uitsluitPatroon |
Ge_ntegreerde_ondervloer | geintegreerdeOndervloer | filter Ja/Nee voor klik-PVC |
Type_verbinding | typeVerbinding | info-veld |
Lengte_in_mm / Breedte_in_mm / Dikte_in_mm | lengtemm / breedtemm / diktemm | afmetingen-display + hoge-plint filter-funnel |
Rd_waarde_m_K_W | rdWaarde | info-veld |
Geschikt_voor_vloerverwarming | geschiktVloerverwarming | info-veld |
Kleur (multi) | kleurAlgemeen | display Uiterlijk/Afwerking |
Trends | trends | display Uiterlijk/Afwerking |
Afwerkingen / Sortering / Gebruiksklasse / Gebruiksklasse_laminaat / Behandeling | diverse hout/PVC/laminaat-specifieke velden | display Vloerdetails |
V_groeven / FSC_keurmerk / Waterbestendigheid / Antislip / Garantie_Jaar / dB_norm / Toplaag_in_mm / Geschikt_voor_trap / Kit | info-velden | display Vloerdetails |
Verwerkingslogica | verwerkingslogica | display Afmetingen-sectie |
Sub-flow records (gegenereerd uit TSV)
Voorbereiden-records komen uit tools/voorbereiden_user_spec.tsv via generate-voorbereiden.py. 28 TSV-kolommen, mapping:
productCode→ linkt naar Zoho-Products (prijs + maten + productRef)verplicht→ Verplicht/Aanbevolen/Optioneel/Keuzeberekening→ Vast / VariabelaantalMin→ minimum-aantal (0 = klant start op 0)conditioneelOp→ parent-productCode (vervolg-keuze trigger)chainProduct→ vervolg-product-code (auto-add via keuzeproductRef)bronVoorAantal+pakLengte+snijfactor→ afgeleide-aantal-formule (§3)- Filter-attributen: subcategorie, installatie, uitvoeringLeggen, basisvloer, uitsluitBasisvloer, etc. (zie werking.html §3)
11. Order-niveau verzendkosten
Sinds 20260531n wordt de transport-keuze order-breed gemaakt in sectie 5 (niet meer per ruimte; state.orderInfo.levering als veld in Relatie-/Ordergegevens is geschrapt). State: state.orderInfo.verzendkosten. Engine: syncVerzendkostenOrder().
| Keuze in card | Record op eerste ruimte | Bedrag | Afgeleide orderInfo.levering |
|---|---|---|---|
| Afhaal | AfhAfoort (marker, geen prijs) | €0 | Afhalen Amersfoort |
| Standaard verzendkosten | TP | €100 | Bezorging |
| Binnenbrengservice | Bgbezorging / Etage1/2/3-levering (zie tier-tabel) | €400 / €500 / €600 / €700 | Bezorging |
Binnenbrengservice — tier-keuze door pickBinnenbrengenEtage()
Tarief volgt twee bron-signalen: state.orderInfo.lift en state.orderInfo.aantalVerdiepingen. Bij ≥ 5 verdiepingen zonder lift is er geen standaardservice; de UI toont een melding "wij hebben geen standaardservice voor deze situatie".
| Lift | Aantal verdiepingen | Record | Bedrag |
|---|---|---|---|
| Ja | (alle) | Bgbezorging | €400 |
| Nee | 1 | Bgbezorging | €400 |
| Nee | 2 | Etage1-levering | €500 |
| Nee | 3 | Etage2-levering | €600 |
| Nee | 4 | Etage3-levering | €700 |
| Nee | ≥ 5 | — (melding speciale-service) | — |
Default-keuze + gating
De card verschijnt pas wanneer allRuimtesIngevuld() waar is (alle ruimtes hebben m²). Default-keuze = binnenbrengservice; auto-gezet bij eerste render. Klant kan ook handmatig naar Afhaal of Standaard verzendkosten wisselen — bij elke wisseling wordt het eerder geplaatste record netjes opgeruimd (stripAutoVerzendkosten).
Migratie van per-ruimte naar order-niveau
Bestaande state met per-ruimte r.verzendkosten wordt eenmalig bij init gemigreerd (migrateVerzendkostenToOrderLevel()): meest-voorkomende keuze over alle ruimtes wint, oude per-ruimte velden worden leeggemaakt. Idempotent — draait alleen wanneer state.orderInfo.verzendkosten nog leeg is.