Migration
Van Magento 1.9 naar Medusa.js: cutover in tien weken
Een modegroothandel met 26 mensen in Gent. Magento 1.9 op zijn laatste benen. 38 EDI-partners die geen feed mochten missen. Zo kregen we ze er in tien weken vanaf.

Het was een dinsdag in Gent en de magazijnchef las factuurnummers voor van een geprinte lijst. De Magento-admin liep sinds de lunch om de haverklap in een timeout. Drie EDI-partners hadden hun stockfeeds gestopt, waaronder de Duitse keten die er elke negen minuten een trok. De modegroothandel met 26 mensen waar we mee aan tafel zaten, had de afgelopen tien jaar op dit platform gebouwd, en kreeg er voor de komende veertig minuten geen bruikbaar antwoord uit.
Dat was het gesprek waarmee het project begon. Wat hieronder volgt is het playbook dat we daadwerkelijk gedraaid hebben, in de volgorde waarin we het deden.
De opdracht die op ons bureau belandde
De klant verkocht premium menswear aan zo'n 800 zelfstandige boutiques in de Benelux, Frankrijk, Duitsland en Noord-Italië. Hun stack was een Magento 1.9.4.5-installatie die negen jaar aan losse maatwerkstukken had opgegeten, een PHP-cronbrug naar een Navision-ERP, en een zelfgebouwde commission engine. Die laatste was in 2013 geschreven door een ontwikkelaar die inmiddels naar Lissabon was verhuisd en daar een koffiezaak runde.
Drie randvoorwaarden maakten het werk interessant. Magento 1 was vier jaar over de officiële end-of-support, en de PCI-auditor had eindelijk besloten geen verlengingen meer te geven. 38 EDI-partners trokken stock- en prijsfeeds op schema's die, alles bij elkaar, de server elke negen minuten raakten. En de commission engine voedde elke vrijdag de salarissen van veertien mensen, een kwart van het bedrijf.
De cutover moest schoon. Niet omdat het management dat zei, maar omdat een groothandel terugdraaien midden in het najaars-orderboek simpelweg niet kan. Zodra je inkopers zich voor een seizoen vastleggen, is het systeem dat hun commitments heeft geboekt het systeem waar je tot februari mee leeft.
De regels in kaart brengen die niemand meer beheerde
De commission engine was het meest risicovolle stuk, dus daar zijn we begonnen. De PHP-code was 4.200 regels verdeeld over negen bestanden, met methodenamen in een mix van Nederlands en Engels. Commentaar was schaars. Tests waren er niet. De huidige finance-lead had het ding in 2019 geërfd en behandelde de calculator als een black box die elke vrijdagochtend een CSV uitspuugde.
We hebben de code niet eerst proberen te lezen. We hebben er meters op gezet.
// app/code/local/Abn/CommissionAudit/Model/Observer.php
public function logCalculation(Varien_Event_Observer $observer)
{
$invoice = $observer->getEvent()->getInvoice();
$rep = $invoice->getOrder()->getSalesRepId();
$payload = [
'invoice_id' => $invoice->getId(),
'order_id' => $invoice->getOrder()->getId(),
'rep_id' => $rep,
'subtotal' => $invoice->getSubtotal(),
'discount' => $invoice->getDiscountAmount(),
'commission' => $this->_legacy->calculate($invoice),
'rule_path' => $this->_legacy->getLastRulePath(),
'inputs_hash' => sha1(json_encode($invoice->getData())),
];
Mage::getModel('abn_audit/run')->setData($payload)->save();
}
Die observer logde acht weken lang elke commissieberekening die in productie liep. In week drie zaten we op 47.000 rijen. We konden nu de database vragen wat de oude code daadwerkelijk deed, in plaats van uit de code te raden wat hij misschien deed.
Deze stap is saai en levert veel op. Als je één ding uit dit stuk meeneemt, neem dan dit: als de business logic ouder is dan de helft van je personeel, begin dan niet met lezen. Begin met opnemen.
Het vervangingsdoel
Voor de nieuwe stack kozen we Medusa.js als commerce-core en Temporal voor alles wat uren of dagen betrouwbaar moest blijven draaien. Magento gaf de klant een flexibele storefront en een redelijke admin. Medusa gaf dezelfde flexibiliteit zonder de onderhoudsschuld van een PHP-monoliet die al verouderd was toen de iPhone 5 uitkwam. Temporal regelde het werk waar Magento nooit goed in was: de EDI-cronstorm, de commissie-rollups, de Navision-sync.
De splitsing was belangrijk, want Magento 1 had elke langlopende job in één crontabel gepropt. Als de feed van de Duitse EDI-partner traag werd, liep de prijssync naar het magazijn achter, en op slechte dagen miste de commissierun de vrijdag helemaal. Het scheiden van duurzame workflows en request-time commerce was de architecturele verschuiving die de rest mogelijk maakte.
Een afgeslankte versie van de commissieworkflow ziet er zo uit:
// workflows/commission-run.ts
import { proxyActivities } from '@temporalio/workflow';
import type * as activities from '../activities';
const { fetchInvoices, applyRule, postToNavision } =
proxyActivities<typeof activities>({
startToCloseTimeout: '5 minutes',
retry: { maximumAttempts: 8 },
});
export async function commissionRun(weekIso: string) {
const invoices = await fetchInvoices(weekIso);
const lines = [];
for (const inv of invoices) {
// applyRule mirrors the audited legacy path exactly.
// Any divergence raises a non-retryable error.
lines.push(await applyRule(inv));
}
await postToNavision(weekIso, lines);
return { week: weekIso, count: lines.length };
}
De workflow is bewust saai. Het interessante zit in wat applyRule doet, en het interessante aan applyRule is dat hij leest uit een platte rules.csv die we genereerden uit de 47.000 audit-rijen. De oude codebase kromp tot zo'n 180 regels TypeScript en één spreadsheet.
EDI als zijn eigen cutover-baan
Bij de 38 EDI-partners werd de klant zenuwachtig. De meeste gebruikten EDIFACT D96A over SFTP. Een handvol draaide X12. Twee zetten nog steeds CSV op een FTP-folder die de cronjob elk uur leegtrok. Eén stuurde ons elke maandagochtend een fax, die het magazijn met de hand opnieuw intikte. De fax hebben we niet opgelost.
We hebben de partners niet in één keer overgezet. We bouwden een kleine EDI-gateway als Medusa-subscriber plus een Temporal-workflow per partner, en zetten partners in batches van vijf over.
// subscribers/edi-dispatch.ts
import { SubscriberArgs, SubscriberConfig } from '@medusajs/framework';
export default async function ediDispatch(
{ event: { data }, container }: SubscriberArgs<{ partnerId: string }>
) {
const temporal = container.resolve('temporalClient');
await temporal.workflow.start('partnerSync', {
workflowId: `edi-${data.partnerId}-${Date.now()}`,
taskQueue: 'edi',
args: [data.partnerId],
});
}
export const config: SubscriberConfig = {
event: ['inventory.updated', 'price.updated'],
};
Elke partner-workflow kende het schema, het formaat, de idempotentieregels en de eigenaardigheden van die ene partner. De Duitse keten die regels met meer dan twee decimalen weigerde. De Italiaanse distributeur die ISO-8859-1 vereiste en UTF-8 BOMs stilletjes weggooide. De twee retailers in Rijsel die elke woensdagochtend handmatig reconcilieerden en de feed een uur op pauze nodig hadden.
Niets daarvan zat in gedeelde code. Het leefde in de workflow van die specifieke partner. We zijn twee keer in de verleiding gekomen om dingen tussen partners te normaliseren. Beide keren hebben we het niet gedaan.
De parallel-run van tien weken
We hebben beide stacks tien weken naast elkaar gedraaid. Magento 1.9 bleef leidend. Medusa was de schaduw. Elke order, factuur en commissieberekening liep in beide systemen, en een vergelijker draaide elke nacht om elk veld te markeren waar de twee meer dan een cent uit elkaar liepen.
In week één zat de afwijking op 11 procent. In week vier was die 0,4 procent, en elk overgebleven gat was óf een bekende legacy-bug (die de klant wilde houden) óf een rounding-keuze in floating-point die we hebben verplaatst naar hele centen als integer.
Waar de afwijkingen vandaan kwamen
Drie bronnen waren goed voor het grootste deel van de gaten. Btw-afronding op facturen met meerdere regels, waar Magento per regel afrondde en onze eerste Medusa-versie het totaal afrondde. Vrije-tekst kortingscodes die in Magento de rule engine omzeilden en met de hand op de factuur werden toegepast. En de commissieregel uit 2017 voor de Belgische vertegenwoordiger die, om redenen die niemand kon uitleggen, 0,6 procent extra verdiende op schoenen, maar alleen tussen maat 41 en 44.
Die regel hebben we niet gerefactord. We hebben hem als regel in rules.csv gezet met als commentaar # niet vragen, en zijn doorgegaan. Het instinct om ongedocumenteerd gedrag tijdens een migratie op te schonen is het instinct dat migraties de das om doet.
Parallel run is geen luxe. Bij een verouderde commerce-migratie met EDI-partners en payroll-logica van tien jaar oud is het het verschil tussen een rustig weekend en zes weken incident review.
Het cutover-weekend
De cutover ging op vrijdagavond in. Om 18:00 CEST hebben we writes naar Magento bevroren. De laatste reconciliatie liep schoon. Om 19:30 zetten we de DNS om voor de admin en de storefront. Om 20:00 raakte de eerste EDI-pull Medusa en kreeg de partner de verwachte stockfeed terug. Om 21:30 zat het team thuis.
Zaterdagochtend meldden twee partners lege feeds. Beide hadden het oude VPS-IP gecachet. Daarom hadden we de oude VPS aan laten staan, met een klein Node-servicetje dat een redirect-header teruggaf en een mail naar ops stuurde. Tegen het middaguur waren beide partners weer groen. Niemand van de klant belde ons tot maandag, en dat telefoontje ging over een Navision-rapport dat niets met de migratie te maken had.
Wat we anders zouden doen
Twee dingen, vooral.
Eén: we hadden onderschat hoeveel oude commissielogica in de factuur-PDF-templates zat in plaats van in de calculation engine. De PDF-generator herrekende stilletjes drie velden uit ruwe orderdata, met andere afronding dan de calculator. We hadden het in week zes te pakken. Het had week één moeten zijn. Als je money paths audit, audit dan ook de renderlaag, niet alleen de rekenkant.
Twee: we hebben de vergelijker te laat gebouwd. Hij ging in week drie van de parallel run live, wat betekende dat de data-backfill zonder hem liep. Hadden we hem op dag één geleverd, ook al was het met een dun schema voor alleen totalen, dan hadden we het btw-afrondingsgat al tijdens de backfill gezien en niet pas in live shadow-verkeer.
De kleinste versie hiervan voor je eigen stack
Zit je op een Magento 1, een Drupal 7 commerce-build of een PHP-platform waar de oorspronkelijke ontwikkelaar geen mails meer beantwoordt: de goedkoopste vijf uur die je deze maand kunt steken, is meters zetten op je money paths. Schrijf een logger zoals hierboven. Vang twee weken lang elke commissie-, btw- en kortingsberekening op. Bewaar de inputs-hash naast de output.
Aan het einde van die twee weken weet je meer over je eigen bedrijf dan de oorspronkelijke ontwikkelaar wist toen hij het schreef. Of je daarna naar Medusa, Shopify of commercetools migreert, of blijft waar je zit, wordt een veel kleinere beslissing dan het nu voelt.
Toen we het Gent-project draaiden, was wat ons verraste hoeveel van die elfjarige commission engine bediscussieerbaar bleek zodra we konden zien wat hij werkelijk berekende. De oude regels bleven bestaan, in een CSV. De 4.200 regels code niet. Wil je de langere versie van deze aanpak, inclusief de Temporal-workflowschema's en de vergelijker, dan lopen onze aantekeningen over legacy migratie de rest door.
Kern
Een verouderde commerce-migratie struikelt alleen op de stukken die niemand beheert. Zet eerst meters op de money paths; het platform omruilen is de makkelijke helft.
FAQ
Waarom Medusa.js en niet Shopify of commercetools?
We hadden volledige controle over het datamodel nodig en de mogelijkheid om per partner custom EDI-logica te draaien, zonder dat een SaaS-ratelimit ons in de weg zat. Medusa leverde dat, zonder de platformteam-overhead die commercetools met zich meebrengt.
Had je dit zonder Temporal kunnen doen?
Ja, met een slechter resultaat. We hadden retries, signals en duurzame state nodig voor de EDI-partner-workflows. Dat op cron en een queue oplossen kan. Het goed oplossen kost ongeveer evenveel code als gewoon Temporal pakken.
Waarom tien weken parallel run? Had je niet eerder kunnen cutoveren?
We hadden in week zes kunnen cutoveren. De laatste vier weken kochten ons een schone vrijdagse commissierun binnen het parallel-venster, waardoor finance kon tekenen zonder fallback-plan. Die handtekening was vier weken waard.
Wat is er met de oorspronkelijke PHP-commissiecode gebeurd?
Die bleef de volledige parallel run in productie staan, en is daarna gearchiveerd. De auditlog van 47.000 rijen leeft in cold storage en is de feitelijke specificatie van de business logic, voor als iemand er ooit nog over wil discussiëren.