Magento
Magento naar Shopify Plus: 11.000 staffelprijzen redden
Een industriële groothandel uit Eindhoven met 22 mensen ging live op Shopify Plus met 11.000 SKU's, zag de staffelprijzen instorten en had drie weken tot kwartaaleinde om het te repareren.

Het telefoontje kwam om 09:14 op een dinsdag. De B2B-accountmanager van een industriële groothandel met 22 mensen net buiten Eindhoven had hun grootste klant aan de lijn, een elektrotechnisch installateur in Tilburg die M6 RVS-bouten per doos van 5.000 stuks afneemt. De cutover naar Shopify Plus was zondagavond geweest. Op dinsdagochtend logde de installateur in, opende zijn vaste herhaalbestelling en zag retailprijzen op elke regel. Tweeënveertig procent hoger dan het contractblad dat hij in januari had ondertekend.
Tegen lunchtijd hadden drie andere contractklanten gebeld. Op woensdag zette de oprichter alle nieuwe B2B-orderintake op de nieuwe shop op pauze en mailde ons. Ze hadden elf dagen tot kwartaaleinde, een directie die de replatforming al had goedgekeurd, en een Magento 2.4-omgeving die het bureau dat het had gebouwd zes maanden eerder had teruggegeven. We namen de telefoon op, openden de database en gaven drie weken op. Het werd twintig dagen.
De verborgen geometrie van Magento-staffelprijzen
De groothandel levert industriële bevestigingsmaterialen, elektrotechnische componenten en verbruiksartikelen aan zo'n 400 actieve zakelijke accounts in de Benelux. Hun Magento 2.4.6-catalogus telde op het moment van de cutover 11.287 actieve SKU's. De staffelprijzen waren in negen jaar opgebouwd.
Van buitenaf zag het er eenvoudig uit. Open een product. Drie of vier prijsbreuken bij 1 tot 9, 10 tot 49, 50 tot 199, en 200 plus. De werkelijkheid eronder was anders. Magento slaat staffelprijzen op in een tabel genaamd catalog_product_entity_tier_price, met sleutels op entity_id, customer_group_id, website_id en qty. De groothandel had zeven klantgroepen: retail, drie wholesale-tiers, twee distributeur-tiers, en één speciale groep voor een gemeentelijk account. Elke groep kon eigen breuken per SKU dragen, en ongeveer 60 procent van de SKU's had minstens één groep-specifieke override.
Het aantal rijen in die ene tabel was 184.612 prijsrecords. De Shopify-partner die het had overgenomen, had begroot op een platte productimport. De geometrie was een orde van grootte groter.
Waar de eerste migratie brak
Het oorspronkelijke plan, opgesteld voordat wij gebeld werden, gebruikte een CSV-export en het standaard Shopify REST-productendpoint met metafield-writes voor de staffeltabel. Het stortte binnen de eerste dag van de pilot-uploads in. Drie redenen, afzonderlijk benoemd omdat elke reden zijn eigen les is.
1. REST is niet gebouwd voor 100k+ prijs-writes
Shopify rate-limit de REST Admin API op 4 calls per seconde in de standaardbucket en 40 per seconde op Plus, met een leaky-bucket-toelage. Een naïeve loop die 184.612 metafield-rijen schrijft op 11.287 producten kost minimaal twee uur aanhoudende doorvoer, en in de praktijk vijf tot zes uur als je retries meerekent. Het pilotscript draaide om 3 uur 's nachts op vrijdag en was nog bezig toen het team zaterdagochtend binnenkwam. De fix hier is de GraphQL Bulk Operations API, die een JSONL-payload accepteert en de write asynchroon aan Shopify's kant uitvoert.
2. Een metafield is data, geen gedrag
Dit was de structurele fout. Het eerste plan sloeg de staffelbreuken op als JSON-metafield op elke variant, en nam vervolgens aan dat Shopify ze bij checkout zou toepassen. Dat doet Shopify niet. Een metafield is inert. Om regelprijzen aan te passen heb je een B2B-catalog-publicatie nodig, een Shopify Function, of een externe app die beide omvat. Er bestaat geen admin-instelling die zegt: behandel dit metafield als een prijsregel.
3. Magento-klantgroepen hebben geen één-op-één op Shopify
Shopify B2B, beschikbaar op het Plus-plan, modelleert de afnemerskant als Companies, die gekoppeld zijn aan Catalogs, die gekoppeld zijn aan price Publications. Dat is een rijker model dan de customer_group-tabel van Magento, maar het is een ander model. Zeven Magento-groepen mappen op zeven Shopify-catalogs is rechttoe rechtaan. 400 individuele bedrijven in het juiste catalog krijgen, en die mapping correct houden wanneer sales reps een account herindelen, dat is het werk.
Als een B2B-migratieplan staffelprijzen opslaat als metafields zonder de engine te benoemen die ze gaat lezen, is het plan onvolledig. Metafields houden data vast. Functions, catalogs of apps passen het toe. Vraag welke het werk doet voordat je de import start.
De architectuur die werkte
Het herstelplan had vier bewegende delen en één randvoorwaarde: niets mocht de handmatige salesflow breken die de binnendienst parallel draaide terwijl wij herbouwden.
Deel één was structureel. We maakten zeven B2B-catalogs aan, één per Magento-klantgroep, elk met een eigen prijslijst. De prijslijst droeg de basisprijs per variant voor die groep, oftewel de prijs die een bestelling van één stuk zou betalen. Dat alleen al dekte ongeveer 35 procent van het ordervolume van de groothandel, want de meeste contractklanten kopen in verpakkingseenheden die al matchen met een staffelbreuk.
Deel twee was de staffel-engine. We schreven een Shopify Function op de cart-transform-target die een JSON-metafield van elke variant leest en de regelprijs herschrijft op basis van aantal. De Function is kort. Het is het belangrijkste bestand in de herbouw.
// extensions/tier-pricing/src/run.js
// @ts-check
const NO_CHANGES = { operations: [] };
/**
* @param {RunInput} input
* @returns {FunctionRunResult}
*/
export function run(input) {
const ops = [];
for (const line of input.cart.lines) {
const raw = line.merchandise?.tierBreaks?.value;
if (!raw) continue;
/** @type {{min_qty:number, price:number}[]} */
const tiers = JSON.parse(raw);
if (!tiers.length) continue;
const qty = line.quantity;
const hit = tiers
.filter(t => qty >= t.min_qty)
.sort((a, b) => b.min_qty - a.min_qty)[0];
if (!hit) continue;
ops.push({
update: {
cartLineId: line.id,
price: {
adjustment: {
fixedPricePerUnit: { amount: hit.price.toFixed(2) }
}
}
}
});
}
return ops.length ? { operations: ops } : NO_CHANGES;
}
De Function leest tierBreaks uit het variant-metafield, vindt het hoogste min_qty waaraan het regelaantal voldoet, en schrijft de vaste prijs per stuk. Hij draait op elke cart-mutatie, ruim onder het instructiebudget dat Shopify Functions opleggen, omdat de staffel-array kort is. De meeste SKU's hadden drie tot vijf entries.
Deel drie was de importer. We vervingen de REST-loop door twee GraphQL Bulk Operations: één voor producten en varianten, één voor metafields. De gecombineerde run was klaar in 38 minuten tegen de productiewinkel, inclusief een verificatieronde.
Deel vier was de company-to-catalog mapping. We exporteerden de klant-naar-groep-join uit Magento, lieten die door een reconciliatiescript lopen dat 23 accounts markeerde waarvan de groepsindeling niet matchte met het contractblad in het dossier, en maakten daarna in bulk Shopify B2B Companies aan met de juiste catalog-toewijzing via de GraphQL Admin API. Die 23 mismatches waren geen migratiebugs. Het was negen jaar drift tussen wat de database zei en wat het salesteam in persoon had afgesproken.
Twintig dagen op de kalender
Zo zag de tijdlijn er echt uit, want de abstracte 'drie weken herstel' verbergt het deel dat ertoe doet.
Dag 1 tot 3. We trokken drie tabellen uit Magento: catalog_product_entity_tier_price, customer_group, en customer_entity. We bouwden een reconciliatiesheet die per actieve klant vergeleek wat hij op zijn tien meest bestelde SKU's zou moeten betalen, tegen wat de nieuwe Shopify Plus-winkel hem die week in rekening bracht. De afwijking was gemiddeld 31 procent. Drie accounts zaten meer dan 50 procent ernaast. De groothandel had het verschil al twee dagen handmatig zitten terugstorten.
Dag 4 tot 7. We prototypeden de cart-transform-Function op een dev store met vijf echte SKU's en drie echte klanten erin geladen. We schreven 47 testcases afgeleid uit echte contractbladen. De Function haalde 47 van 47 op dag zes. Daarna bouwden we het metafield-schema, kwamen met de lead binnendienst tot een tier_breaks-definitie, en tekenden af.
Dag 8 tot 14. Bouwweek. De bulk-importer, de catalog-maker, de company-mapper en het verificatieharnas. We draaiden het harnas elke nacht parallel tegen de live Magento-winkel, en vergeleken wat 500 willekeurig getrokken SKU-klant-combinaties op elk platform zouden kosten. De drift zakte van 31 procent op dag één naar 0,4 procent op dag twaalf. Die resterende 0,4 procent was te herleiden naar twee Magento-overrides die als vaste prijzen waren ingevoerd in plaats van als staffelbreuken. Die migreerden we als variant-niveau-overrides op het Shopify-catalog.
Dag 15 tot 18. Staging en generale repetitie. Drie klanten werden vrijdagmiddag uitgenodigd om in te loggen, een echte mand op te bouwen, en bij checkout te stoppen. Alle drie de manden prijsden correct. Eén klant merkte een UI-lag van 600 ms op de cart drawer, die we terugleidden naar een externe loyalty-app die we uiteindelijk hebben verwijderd.
Dag 19 tot 20. Cutover. De B2B-orderintake ging woensdag om 08:00 weer open. Tegen vrijdagavond hadden 71 contractklanten een order geplaatst. Nul prijstickets.
Het getal om bij te houden bij een B2B-replatforming is niet het aantal gemigreerde producten. Het is de prijsgetrouwheid per klant per SKU. Als je vóór de cutover niet kunt beantwoorden 'wat zou precies deze klant op precies dit verpakkingsformaat nu op de nieuwe winkel betalen', dan ben je nog niet klaar.
Wat we de volgende keer anders zouden doen
Drie dingen, en ze zitten allemaal stroomopwaarts van de techniek.
Eén. Draai het prijsgetrouwheidsscript als gate, niet als post-mortem. Het eerste bureau van de groothandel had een go-live-checklist met 142 punten. Geen enkele was: 'kloppen de contractprijzen met de brondata op een representatieve steekproef'. Bouw dat script in week één, draai het 's nachts tegen staging, en laat het de cutover blokkeren als het rood staat.
Twee. Vertrouw niet op de documentatie van het vorige bureau over wat Magento doet. De overdrachtsnotities beschreven staffelprijzen als 'standaard customer-group-tiers'. Ze noemden niet de override voor het gemeentelijke account, niet de vier legacy-distributeur-tiers die nooit waren uitgezet, en niet de 23 accounts waarvan de groepsindeling was afgedreven van hun ondertekende contract. De database is de bron van waarheid. De wiki is een hypothese.
Drie. Benoem de engine vóór de import. Als je migratieplan staffelprijzen als metafields opslaat, moet de allereerstvolgende alinea in dat plan zeggen welke Function, welk catalog of welke app ze gaat lezen. Het eerste plan dat wij overnamen sloeg die alinea over. Dat was de hele bug.
Het kleinste wat je deze week kunt doen
Zit je midden in een Magento-naar-Shopify Plus-replatforming met B2B-prijzen in de mix, dan hoef je je architectuur vanmiddag niet te herbouwen. Je hebt één query nodig. Open je Magento-database, draai de row count op catalog_product_entity_tier_price, en deel die door je aantal actieve SKU's. Als het antwoord groter is dan drie, dan zit het zware werk van je migratie niet in de producten. Het zit in de prijsgeometrie eromheen.
Toen we de catalogus van de Eindhovense groothandel herbouwden, behandelden we het als een legacy-migratie in plaats van als nieuwbouw, omdat de institutionele kennis in negen jaar overrides zat in plaats van in de catalogus zelf. De architectuur die uit dat project kwam, pakken we nu standaard erbij zodra een B2B-Magento-omgeving moet verhuizen en de staffeltabel meer rijen heeft dan de producttabel.
Kern
Het zware werk van een B2B-Magento-verhuizing zit niet in de producten. Het zit in de prijsgeometrie: aantal klantgroepen maal aantal staffelbreuken maal aantal SKU's.
FAQ
Kan Shopify Plus B2B-staffelprijzen aan zonder een externe app?
Ja. B2B Catalogs dekken de basisprijs per bedrijf, en een cart-transform Shopify Function kan staffelbreuken uit een variant-metafield lezen en regelprijzen bij checkout herschrijven.
Hoe lang moet een Magento-naar-Shopify Plus B2B-migratie eigenlijk duren?
Zes tot twaalf weken voor een mid-size catalogus met echte staffelprijzen. Drie weken is hersteltijd op een halfgebouwde migratie, geen schone start. Reken op twee maanden bouw plus een parallel-run-venster.
Waarom werkte het opslaan van staffelprijzen als metafields niet?
Metafields zijn inerte data. Zonder een Function, B2B-catalog of app die ze leest en de prijs bij checkout toepast, hebben ze geen effect op wat de klant betaalt.
Wat is een prijsgetrouwheidsscript en wanneer moet het draaien?
Een nachtelijke job die echte klanten en SKU's steekproefsgewijs vergelijkt op wat elk paar op Magento versus de nieuwe winkel zou betalen. Behandel een drift tegen nul als de gate voor cutover, niet als check achteraf.
Moet je de customer-group-structuur één-op-één migreren?
Nee. Leid hem opnieuw af uit de actuele contracten voordat je migreert. Langlopende Magento-winkels dragen bijna altijd drift tussen de customer_group-toewijzing en wat er echt met de klant is afgesproken.