E-commerce
WooCommerce-audit playbook: 45 minuten, vijf geldlekken
Het bureau reageert niet meer. Je krijgt om 22:00 SSH-toegang. Dit is de audit van 45 minuten die we draaien op elke overgenomen WooCommerce-store: queries, diff, fixes.

Een oprichter stuurt je op een dinsdag om 22:00 een bericht. "De checkout is sinds zondag stuk. Het bureau reageert sinds maart niet meer. We hebben €180k aan openstaande mandjes. Kun je kijken?" Je vraagt om SSH, database-credentials en de wp-admin login. 45 minuten later heb je een lijst van wat bloedt, wat gevaarlijk is, en wat je vanavond moet fixen. Dit is de playbook die we gebruiken, in de volgorde waarin we 'm draaien.
Wat je nodig hebt voor de klok begint te lopen
Vijf dingen. SSH-toegang tot de server, database-credentials (read-only is genoeg), een wp-admin-account met administrator-rol, het huidige payment-provider dashboard (Stripe, Mollie, Adyen), en Google Analytics of een equivalent. Heb je alle vijf, dan ben je binnen 45 minuten klaar. Mis je er één, dan wordt de audit een dag, want je bent het grootste deel van die tijd kwijt aan het achternazitten van de vorige developer.
Open drie terminal-tabs en een notitiebestand. Eén tab voor SSH, één voor een MySQL-client, één voor git. In het notitiebestand leg je elke vreemde bevinding vast met een query-resultaat, een regelnummer of een pad. Geen screenshots zonder context. De output van deze audit is één A4'tje dat de oprichter in drie minuten leest, dus structureer je notities meteen op die manier.
De database vertelt als eerste de waarheid
WordPress-admins liegen. Plugins melden zichzelf als actief terwijl ze bij boot crashten. Themes melden de verkeerde versie. De database is waar het echte verhaal leeft. Begin daar voordat je iets klikt in wp-admin.
Eerste query: hoe groot is de orders-tabel, en wat is de spreiding van de statussen?
SELECT post_status, COUNT(*) AS orders,
MIN(post_date) AS first_order,
MAX(post_date) AS last_order
FROM wp_posts
WHERE post_type = 'shop_order'
GROUP BY post_status
ORDER BY orders DESC;
Dat geeft je drie dingen in één keer. Het totale aantal orders, dat je voor de zekerheid checkt tegen het totaal in het dashboard. Het aandeel dat vastzit in wc-pending of wc-failed (alles boven 8% is een probleem met de payment-integratie, geen user error). En de datum van de laatste succesvolle bestelling. Als last_order op wc-completed drie dagen geleden is en de store krijgt dagelijks verkeer, dan is de checkout stuk en heb je je eerste bevinding voor minuut tien.
Vervolgens tel je de stapel mislukte orders op in euro's. De sessietabel is interessant maar bevat veel ruis. Failed orders met regelitems zijn het echte signaal.
SELECT SUM(CAST(pm.meta_value AS DECIMAL(10,2))) AS lost_revenue,
COUNT(DISTINCT p.ID) AS failed_orders
FROM wp_posts p
JOIN wp_postmeta pm ON pm.post_id = p.ID
WHERE p.post_type = 'shop_order'
AND p.post_status = 'wc-failed'
AND pm.meta_key = '_order_total'
AND p.post_date > DATE_SUB(NOW(), INTERVAL 30 DAY);
Komt het totaal van failed orders over dertig dagen boven 5% van de omzet uit completed orders uit, dan is de payment gateway verkeerd geconfigureerd of gooit een plugin een fatal op het verkeerde moment in de checkout-flow. We zien dit op ongeveer de helft van de stores die we overnemen. De fix is meestal een verlopen API-key bij de gateway, of een custom theme-override die een verwijderde action hook aanroept.
Derde query, degene die de meeste bureaus overslaan: hoeveel producten hebben fysiek geen voorraad, maar zijn nog steeds koopbaar?
SELECT p.ID, p.post_title,
MAX(CASE WHEN pm.meta_key = '_stock' THEN pm.meta_value END) AS stock,
MAX(CASE WHEN pm.meta_key = '_stock_status' THEN pm.meta_value END) AS status,
MAX(CASE WHEN pm.meta_key = '_backorders' THEN pm.meta_value END) AS backorders
FROM wp_posts p
JOIN wp_postmeta pm ON pm.post_id = p.ID
WHERE p.post_type = 'product'
AND p.post_status = 'publish'
GROUP BY p.ID
HAVING stock = '0' AND status = 'instock' AND backorders = 'no';
Elke rij in dat resultaat is een bestelling die het magazijn niet kan verzenden. De klant heeft betaald. Je bent een terugbetaling en een excuus verschuldigd. Sommige staan al een jaar live omdat de stock-sync vanuit het ERP stilletjes faalt zodra de SKU een slash bevat.
De plugin-diff die niemand draait
Schakel over naar de SSH-tab. Ga naar wp-content/plugins. Je hebt WP-CLI nodig; staat het er niet op, installeer het voor je begint. Draai dan deze one-liner:
for d in */; do
plugin="${d%/}"
active=$(wp plugin is-active "$plugin" --quiet && echo "ON" || echo "off")
version=$(wp plugin get "$plugin" --field=version 2>/dev/null)
updated=$(stat -c %y "$d" 2>/dev/null | cut -d' ' -f1)
printf "%-3s %-40s %-12s %s\n" "$active" "$plugin" "$version" "$updated"
done | sort -k4
De output sorteert op last-modified datum. Plugins die al drie jaar niet zijn aangeraakt, zijn kandidaten voor kwetsbaarheden. Plugins die actief zijn op disk maar ontbreken in de admin-lijst zijn meestal mu-plugins of dropins die het update-proces omzeilen. Beide zijn interessant, om verschillende redenen.
Cross-check elke plugin tegen de Wordfence vulnerability database. We kijken specifiek naar alles in de payment-, shipping-, tax- of coupon-paths van WooCommerce. Oudere woocommerce-gateway-stripe-releases hadden eigenaardigheden bij het verwerken van 3DS-callbacks; oudere Mollie-plugins verliezen af en toe de order ID bij een redirect als een CDN query strings stript. Beide komen naar boven als failed orders die in het dashboard mysterieus lijken en in wp-content/uploads/wc-logs/ overduidelijk zijn.
Draai vervolgens de dependency-diff. Het manifest van de plugin vertelt je welke WooCommerce-core-versie hij verwacht. Is het verschil meer dan twee minor-versies, dan heeft iemand iets hot-gepatcht. Kijk in mu-plugins naar bestanden met namen als fix-checkout.php, custom.php of tmp.php. We hebben meer dan eens 800-regel hotfixes in één bestand overgenomen. Ze zijn altijd ongedocumenteerd en altijd dragend.
Vind je een bestand in mu-plugins dat woocommerce_payment_complete of woocommerce_order_status_changed raakt, verwijder het dan niet voor je het hebt gelezen. Waarschijnlijk houdt het de boekhoudexport, de magazijn-webhook of de loyalty-plugin in leven.
De vijf instellingen die stilletjes geld lekken
Hier hebben we een checklist van gemaakt. Elke overgenomen WooCommerce-store lekt op minstens twee van deze vijf plekken. De helft lekt op vier. Geen ervan duikt op in een dashboard, omdat het configuratie is, geen fout.
1. BTW-afronding per regel vs subtotaal
WooCommerce, Settings, Tax, Rounding. Staat "Round tax at subtotal level" uit, dan wordt BTW per regelitem afgerond. Op een B2B-store met facturen van 40 regels loopt de cumulatieve afrondingsfout op tot ongeveer €0,30 per bestelling. Op 3.000 orders per maand is dat €900 die de boekhouder elke maand mag reconciliëren. Zet 'm aan. De juiste setting hangt af van de factuurwet per land, maar in de EU sluit afronding op subtotaal-niveau aan bij hoe accountants reconciliëren tegen het bankafschrift.
2. Stapelen van kortingscodes
"Individual use only" is een flag per coupon, niet globaal. Query de coupons-tabel:
SELECT p.post_title AS code,
MAX(CASE WHEN pm.meta_key = 'individual_use' THEN pm.meta_value END) AS individual,
MAX(CASE WHEN pm.meta_key = 'usage_count' THEN pm.meta_value END) AS used,
MAX(CASE WHEN pm.meta_key = 'coupon_amount' THEN pm.meta_value END) AS amount
FROM wp_posts p
JOIN wp_postmeta pm ON pm.post_id = p.ID
WHERE p.post_type = 'shop_coupon'
AND p.post_status = 'publish'
GROUP BY p.ID
ORDER BY used DESC;
Elke coupon met individual = no en een hoge used-count wordt gestapeld met de welcome-korting en de seizoenssale. Op een fashion-store die we in april auditten, hadden drie stapelbare codes samen 47% korting opgeleverd op een derde van de bestellingen, zes weken lang. Niemand had het door, want elke code op zich leek redelijk.
3. Hold-stock minuten
Standaard staat 'ie op 60 minuten. Op een snelle store met beperkte voorraad houden verlaten mandjes echte stock een uur vast. Klanten komen aan, zien "niet op voorraad" en springen naar een concurrent. Zet de hold op 15 minuten voor SKU's met lage voorraad en laat 60 staan voor high-volume artikelen. De instelling staat onder Settings, Products, Inventory.
4. E-mailontvangers van de new-order notificatie
Klinkt onbenullig. Is het niet. We hebben stores overgenomen waar de new-order mail naar info@oudbureau.nl ging en het magazijn al maanden werkte vanuit een CSV die de oprichter elke ochtend exporteerde. Bestellingen gingen twee dagen later de deur uit en de mailbox van het oude bureau bouncete stilletjes omdat het domein verlopen was. Check Settings, Emails, New order, Recipient. Staat er iets anders dan een actieve mailbox op het domein van de klant (of een gedeelde magazijn-mailbox), fix het voor je de laptop dichtklapt.
5. Webhooks die naar dode endpoints wijzen
Settings, Advanced, Webhooks. Elke dode webhook is een Klaviyo-flow, een ERP-sync of een Slack-notificatie die al maanden faalt. De delivery-log tabel vertelt je welke:
SELECT webhook_id, response_code, COUNT(*) AS deliveries
FROM wp_wc_webhook_delivery_log
WHERE timestamp > DATE_SUB(NOW(), INTERVAL 7 DAY)
GROUP BY webhook_id, response_code
ORDER BY webhook_id, deliveries DESC;
Alles dat boven 4xx blijft hangen, betekent dat een downstream-systeem al minstens een week blind is. Schakel de dode webhooks uit voor je iets anders doet. De delivery-log tabel groeit snel en vertraagt de hele order-pipeline zodra hij voorbij een paar miljoen rijen komt.
Wat die 45 minuten je opleveren
Tegen minuut 45 heb je: een samenvatting van orderstatussen, een totaal aan failed orders in euro's, een lijst van overselling SKU's met aantallen, een telling van kwetsbaarheidsflags, een lijst van ongedocumenteerde hotfixes in mu-plugins, en de bevindingen op de vijf instellingen met huidige en aanbevolen waarden. Dat past op één A4'tje. Stuur het naar de oprichter voor je over scope onderhandelt. Je weet dan of dit een opruimklus van een dag is of een redding van drie maanden, en zij weten het ook.
Het patroon dat we het vaakst zien: een store die vorig jaar €2M omzette, raakte ongeveer €40k kwijt aan verkeerd geconfigureerde payment retries, €15k aan overselling dat refund-tickets werd, en een onbekend bedrag aan klantvertrouwen door een checkout-flow die voor zo'n 6% van de mandjes breekt. Niets daarvan staat in een dashboard. Het staat allemaal in de database, en het meeste is in de eerste week op te lossen.
Toen we deze audit vorige maand draaiden voor een Nederlands woon-merk, sloot alleen de BTW-afrondingsinstelling al een maandelijks boekhoudgat van €600 dat de boekhouder afdeed als "ruis". Dat is één setting. We doen dit soort legacy-audits en reddingen als een week met vaste scope, zodat de operations lead de rekening kent voor we beginnen en de oprichter op vrijdag het A4'tje krijgt.
Het kleinste wat je vandaag kunt doen: draai de eerste query van deze pagina tegen je eigen store. Liggen de wc-failed-orders boven 3% van wc-completed over de afgelopen dertig dagen, dan lekt je checkout en heb je tot morgenochtend om uit te vinden waar.
Kern
WordPress liegt, plugins liegen, dashboards ronden af. De database, het filesystem en de webhook log zijn de enige drie plekken die de waarheid vertellen over een WooCommerce-store.
FAQ
Kan ik deze audit draaien op een live productie-database?
Ja. Elke query in de playbook is read-only en draait op geïndexeerde kolommen. Beperk de datumrange tot 30 dagen als de orders-tabel meer dan een miljoen rijen heeft, zodat je de order-queue niet blokkeert.
Wat als ik geen SSH-toegang heb, alleen wp-admin?
Installeer een database-console plugin zoals Query Monitor, of gebruik de Site Health-tool om PHP-info te exporteren. Je mist dan de plugin-diff en de mu-plugins-inspectie, en juist daar zit de meeste gevaarlijke code verstopt.
Hoe vaak moeten we deze audit opnieuw draaien op een gezonde store?
Per kwartaal voor de vijf instellingen, maandelijks voor de failed-orders query, en na elke plugin- of theme-update voor de webhook delivery log. De hele sweep duurt 20 minuten zodra je 'm twee keer gedaan hebt.
Waarom niet gewoon een betaalde WooCommerce health-check plugin gebruiken?
Die plugins lezen wat WordPress over zichzelf rapporteert. Het hele punt van deze audit is dat WordPress liegt. De database, het filesystem en de delivery log vertellen je wat er écht gebeurt.