E-commerce
WooCommerce-audit: voorraad, btw-drift, webhook-races
Een Nederlands beautymerk vroeg ons een WhatsApp-agent te bouwen op hun WooCommerce-shop van €1,6M. We stuurden eerst een checklist terug. Hier is waarom.

Een founder van een Nederlands beautymerk mailde ons in maart. Onderwerp: "Kunnen jullie een WhatsApp-agent bouwen voor orderstatus, retouren en 'heb je dit op voorraad'?" Hun WooCommerce-shop doet ongeveer €1,6M GMV, 22k orders per jaar, drie magazijnen, twee ERP's.
We offreerden niet. We stuurden een checklist terug.
De reden: een chat-agent die met een klant praat, is alleen zo eerlijk als de data eronder. Als WooCommerce tegen zichzelf liegt over voorraad, btw of de orderlifecycle, neemt de agent die leugen over en verspreidt 'm op WhatsApp-snelheid. De audit is het deel dat we doen vóór er één regel code geschreven wordt, en de laatste €1,2M+ shop die we herbouwden leerde ons naar welke getallen je het eerst moet kijken.
Waarom eerst auditen
Een chat-agent vervangt je supportteam niet omdat 'ie sneller antwoordt. Hij vervangt ze omdat 'ie vaker correct antwoordt dan een vermoeide mens op vrijdag om 17:50. De lat is niet 'gemiddelde accuratesse'. Het is het slechtste antwoord dat de agent geeft, vermenigvuldigd met het aantal klanten dat het ziet voordat iemand het opmerkt.
Op shops onder €500k GMV slaan we de audit meestal over. De dataset is klein genoeg dat een agent die twee keer per week de voorraad mist vooral irritant is, niet dodelijk. Boven ongeveer €1,2M GMV kom je in een ander regime: genoeg ordervolume dat 0,5% foutmarge echt geld kost, genoeg productcomplexiteit dat 'waar is mijn pakket' naast 'wat is de btw hierop' staat, en bijna altijd minstens één integratie die twee jaar geleden er in haast bijgebouwd is en sindsdien nooit meer is aangeraakt.
Dus vóór we offreren, draaien we een halve dag audit op de live shop. Drie categorieën problemen komen elke keer terug: lag in de voorraad-sync, btw-klasse-drift en een handvol webhook-races. Dit is waar we naar kijken en welke queries we draaien.
Lag in de voorraad-sync
De native voorraad van WooCommerce is prima, totdat je een tweede source of truth toevoegt. Zodra een ERP (Exact, AFAS, SAP Business One, Odoo, Brightpearl) of een 3PL met de shop praat, heb je twee databases die met een delta van elkaar afwijken, en die delta is de lag.
De chat-agent leest voorraad uit WooCommerce. Het magazijn leest uit het ERP. De klant vraagt "is de matte cleanser op voorraad?" De agent kijkt naar _stock in de product-meta, zegt ja, de klant betaalt, drie uur later krijgt 'ie een 'sorry, oververkocht'-mail. Dat is de duurste mail in je stack: hij kost de order, het vertrouwen en het supportticket.
We meten de lag met een probe-script. WP-CLI draait het tegen de live shop vanaf de deploy-host:
wp eval-file scripts/stock-lag-probe.php --user=1 \
| tee logs/stock-lag-$(date +%F).logDe probe trekt 200 willekeurige SKU's uit WooCommerce, hit het ERP-leesendpoint per stuk en schrijft het absolute verschil plus de timestamps van de laatste sync aan beide kanten. Alles waar de lag boven de 10 minuten uitkomt, gaat op de 'vraag, zeg niet'-lijst van de chat-agent. Dat betekent dat de agent 'zou op voorraad moeten zijn, ik bevestig het bij checkout' zegt in plaats van een hard ja.
Wat we meestal vinden bij shops in deze range: mediane sync-lag van 6 tot 12 minuten, p95 rond de 40 minuten. Eén of twee SKU's met permanente drift omdat er ooit handmatig een override in WooCommerce-admin is gezet die nooit naar het ERP is teruggeschreven. En bijna altijd een cron-job die claimt 'elke 5 minuten te syncen' maar stilletjes sneuvelt op een PHP-timeout boven de 800 SKU's.
Als je ERP-sync via admin-ajax.php op een wp_cron-tick draait, heb je geen 5-minuten-sync. Je hebt een sync die alleen vuurt wanneer iemand de site bezoekt. Verhuis 'm naar een echte system-cron voordat je iets aan een chat-agent koppelt.
Btw-klasse-drift
Dit is de stille. Niemand klaagt erover, omdat klanten niet weten wat hun btw zou moeten zijn, en finance vangt het pas één keer per kwartaal op als de btw-aangifte niet sluit.
Drift gebeurt wanneer producten in WooCommerce gezet worden zonder expliciete tax-class, waardoor ze standaard in 'Standard' belanden. Op een Nederlandse shop is dat 21%. Hoort het product in de verlaagde 9%-categorie (boeken, sommige voeding, kappers, bepaalde gezondheidsproducten), dan reken je klanten te veel, en je customer-service-inbox vult zich langzaam met beleefd geformuleerde refund-verzoeken. Erger: hoort het product op 0% (intra-EU B2B met geldig btw-nummer), dan reken je btw over transacties die schoon verlegd hadden moeten worden, en de accountant van de koper is nu boos op jullie allebei.
De query die we draaien op klassieke post-meta-opslag:
SELECT p.ID, p.post_title, pm.meta_value AS tax_class
FROM wp_posts p
LEFT JOIN wp_postmeta pm
ON pm.post_id = p.ID AND pm.meta_key = '_tax_class'
WHERE p.post_type IN ('product', 'product_variation')
AND p.post_status = 'publish'
AND (pm.meta_value IS NULL OR pm.meta_value = '');Elk product waar _tax_class leeg is, valt door naar Standard. Die SKU's checken we vervolgens tegen de echte tax-mapping van de merchant (meestal een spreadsheet die de boekhouder bijhoudt). Bij de laatste audit zaten 312 van de 4.100 producten op de verkeerde klasse. Zes daarvan waren goed voor ongeveer €18k aan verkeerd berekende btw over twaalf maanden.
Voor een chat-agent waarvan verwacht wordt dat 'ie 'wat is mijn totaal inclusief btw' of 'kan ik een zakelijke factuur krijgen' beantwoordt, fix je dit eerst. De agent kan nooit een accuratere bron van waarheid zijn dan de database waar 'ie tegen praat.
Draai je op de nieuwe High-Performance Order Storage (HPOS), dan verandert de join: btw-data zit in de nieuwe wc_orders- en wc_order_addresses-tabellen. Het driftprobleem is hetzelfde. Alleen de SQL verschuift.
De drie webhook-races
Hier verloren we een week op het vorige project, en daarom draaien we nu vóór elke offerte een webhook-trace.
WooCommerce vuurt webhooks op order-events: created, updated, paid, completed, refunded. Het naïeve mentale model is dat ze in die volgorde vuren, precies één keer, met de order in een stabiele staat. Het echte model is geen van die drie dingen.
Race 1: created vs updated, in de verkeerde volgorde
Wanneer een klant betaalt via een redirect-gateway (Mollie, Adyen, Buckaroo), wordt de order eerst aangemaakt in pending-status en daarna naar processing getransitioneerd na de gateway-callback. WooCommerce kan order.updated vuren vóórdat order.created is bezorgd, als je queue-worker ze parallel oppakt, of als de created-webhook een tijdelijke 502 raakte en nog in de retry-queue staat.
De chat-agent ontvangt updated, zoekt de order op, vindt niks (omdat created aan jouw kant nog onderweg is), en antwoordt "we zien die order nog niet, kun je het nummer dubbelchecken?" De klant heeft net betaald. Die ziet paniek. Opent een ticket. Je agent maakte het ticket aan dat 'ie moest voorkomen.
Fix: dedupliceer en sorteer events aan jouw kant. Gebruik id plus een monotone date_modified_gmt, en queue events per order met een kleine settle-delay (2 à 3 seconden) voordat de agent leest.
Race 2: payment_complete vuurt vóór de status-row gecommit is
De woocommerce_payment_complete-action vuurt binnen de gateway-callback, soms vóórdat de order-row gecommit is in bepaalde database-configuraties (met name MySQL met row-level locks onder READ-COMMITTED). Leest je chat-agent-worker de order binnen die hook, dan kun je status = 'pending' terugkrijgen, ook al is de betaling logisch al gelukt.
Dit leerden we op een shop met Mollie iDEAL. Het 'je betaling is bevestigd'-bericht van de agent ging 90 seconden eerder uit dan de order in de admin op processing kwam te staan. Customer service kreeg 14 verwarde mails in één weekend, allemaal van klanten die hun order nog niet op hun accountpagina konden vinden.
Fix: vertrouw het status-veld nooit binnen woocommerce_payment_complete. Wacht op woocommerce_order_status_processing of woocommerce_order_status_completed, die vuren pas nadat de row gepersist is.
Race 3: voorraad-decrement vs cancel-on-timeout
Wanneer een order naar pending gaat, houdt WooCommerce voorraad vast voor hold_stock_minutes (default 60). Als de klant afhaakt bij checkout, geeft een cron die voorraad weer vrij. Leest de chat-agent voorraad binnen het hold-window, dan ziet 'ie een getal dat op het punt staat te veranderen.
Erger: vraagt een klant 'hebben jullie deze in rood?', en de agent zegt 'ja, nog 1 beschikbaar', de klant klikt door naar checkout, en midden in die flow geeft de vastgehouden voorraad van een andere pending-order vrij. Nu zijn het er 2. Maar de agent zei 1, dus de klant gooit de reserve niet in het mandje en je verliest de upsell die gratis had moeten zijn.
Fix: query _stock expliciet minus de pending-order-reserveringen, of behandel alles onder 3 stuks als 'beperkt' in de agent-copy en forceer een harde re-check bij add-to-cart. De WooCommerce stock-management docs leggen het hold-stock-mechanisme uit, maar waarschuwen je niet voor de race. Die moet je zelf zien te engineeren.
De checklist zelf
Dit is het formulier dat we draaien op een €1,2M+ shop vóór we een chat-agent-build offreren. Een senior developer doet er ongeveer vier uur over en het brengt 90% van het werk aan het licht dat de agent-build anders halverwege ontdekt, op de moeilijke manier.
## Pre-build audit, WooCommerce + chat-agent
### Data-integriteit
- [ ] Voorraad-sync-lag-probe over 200 random SKU's, p50 + p95 gelogd
- [ ] Producten met leeg _tax_class geteld, gekruist met boekhouder
- [ ] Wees-order-meta (orders zonder line items) geteld
- [ ] HPOS-migratiestatus; bij halve migratie, audit beide tabellen
### Webhook-hygiëne
- [ ] Alle geregistreerde webhooks gelijst (wp wc webhook list)
- [ ] Delivery log nagelopen op 4xx/5xx van de laatste 7 dagen
- [ ] Retry-policy aan consumer-kant gedocumenteerd
- [ ] Dedupe-key gekozen (order_id + date_modified_gmt)
### Order-lifecycle
- [ ] Lijst van gateways + welke hooks per gateway vuren
- [ ] hold_stock_minutes-waarde bevestigd
- [ ] Cancel-on-timeout-cron geverifieerd als system-cron, niet wp_cron
### Agent-oppervlak
- [ ] Intents die de agent afhandelt, gemapt op read-only of write
- [ ] Per write-intent: idempotency-key + rollback-pad
- [ ] PII-grens: wat de agent mag citeren, wat 'ie moet maskerenDe starter-audit van vijf minuten
Draai je een WooCommerce-shop boven €1,2M GMV en staat een chat-agent-project op je roadmap, dan hoef je niemand in te huren om te beginnen. Draai de _tax_class-SQL-query hierboven en tel de lege rijen. Tijd hoe lang het duurt voordat een voorraadwijziging in je ERP zichtbaar wordt in WooCommerce: pas in het ERP de voorraad van één SKU handmatig aan, en refresh de WP-admin-productpagina elke minuut totdat het getal verandert. Schrijf die twee getallen op een post-it. Het zijn de eerste twee slides van je build-brief, en ze vertellen je meer over de echte vorm van het project dan welke vendor-demo dan ook.
Toen we voor het beautymerk uit de opening de WhatsApp-agent bouwden, was het de derde webhook-race, die met het voorraad-decrement, die onze planning opvrat. We zagen 'm alleen omdat de audit een hold_stock_minutes-waarde van 240 had geflagd, ooit gezet tijdens een A/B-test van twee jaar daarvoor en nooit teruggedraaid. Wil je deze audit laten draaien op je eigen shop voordat je een agent-project scoped, dan is dat werk dat we doen onder onze AI-agents-praktijk, en het kost minder dan de eerste oververkochte order die een ongeauditeerde agent eruit stuurt.
Kern
Een chat-agent is een publieke API voor het slechtst bewaarde geheim van je shop: hoe vaak je data tegen zichzelf liegt. Fix de data voordat je de agent offreert.
FAQ
Wanneer heeft een WooCommerce-shop een pre-build audit nodig?
Boven ongeveer €1,2M GMV maken het ordervolume en de integratiecomplexiteit datakwaliteit tot het echte knelpunt. Daaronder brengt een kleine pilot problemen meestal sneller naar boven dan een formele audit.
Kan de audit draaien zonder de shop offline te halen?
Ja. Elke check is read-only tegen de live database plus een passieve review van de webhook-delivery-log. Geen productieverkeer wordt onderbroken en er worden geen schemawijzigingen gedaan.
Wat is de grootste verrassing in de audit?
Btw-klasse-drift. Bijna elke shop boven €1M GMV heeft minstens 50 SKU's op de verkeerde btw-klasse, meestal stilletjes te veel berekend aan klanten, soms te weinig afgedragen aan de Belastingdienst.
Hoe lang duurt de audit?
Een halve dag voor een senior developer om de queries en probes te draaien, plus een walkthrough van 30 minuten met de finance-lead om de btw-mappings te valideren tegen de spreadsheet van de boekhouder.