Developer Guide

Lokale ontwikkeling, staging, tests en nuttige links

K

1. Quick Start

Emulators starten

Start Auth, Functions, Firestore, Storage en Hosting. Laat deze terminal open.

npm run dev

Seed data laden

Maakt 3 demo tenants (basic/pro/premium) en test users aan.

node tests/seed.mjs

Login: test_admin@cocinia.be / test123456

2. Lokale URLs

Platform Admin

localhost:5000/platform-admin/

Tenant Admin

localhost:5000/admin/?tenant=demo-premium

Op staging/prod wordt de tenant via hostname herkend. Lokaal blijft ?tenant= de fallback.

Emulator UI

localhost:4000

Dev Build Server

localhost:3099

Marketing Site

localhost:8082

Tenant Admin — directe links (lokaal)

SectieURL
Dashboardlocalhost:5000/admin/dashboard/?tenant=demo-premium
Gerechtenlocalhost:5000/admin/gerechten/?tenant=demo-premium
Pagina'slocalhost:5000/admin/paginas/?tenant=demo-premium
Instellingenlocalhost:5000/admin/instellingen/?tenant=demo-premium

Vervang demo-premium door een andere tenant ID. Eén /admin/ path voor alle tiers — tier komt uit het tenant document.

3. Tenant Sites bouwen

Alle test tenants bouwen

npm run test:build

Eén site met live-reload

cd apps/tenant-site && TENANT_ID=demo-premium npx @11ty/eleventy --serve --port=8083

Tailwind CSS rebuilden

cd apps/tenant-site && npx tailwindcss -i src/css/input.css -o src/css/tailwind.css --minify

Dev Build Server — per-tenant build triggeren

Draait Eleventy voor apps/tenant-site + apps/tenant-admin met de juiste TENANT_ID. Schrijft deployment-status naar de emulator Firestore.

curl -X POST http://127.0.0.1:3099/build \ -H "Content-Type: application/json" \ -d '{"tenantId": "de-notelaar"}'

Start de server via node dev-build-server.js of start-dev.bat.

4. Staging Omgeving

Tenant Admin (staging)

Per tenant eigen hosting site:

cocinia-staging-{tenantId}.web.app/admin/

Voorbeeld: cocinia-staging-demo-premium.web.app/admin/

Demo Sites (staging)

Productie hosting: cocinia-{tenantId}.web.app · Staging hosting: cocinia-staging-{tenantId}.web.app

Platform + admin deployen (hosting main)

NOOIT hosting en functions in één commando — als functions faalt wordt hosting niet gereleased zonder foutmelding (silent failure). Altijd apart:

firebase deploy --only functions --project cocinia-staging firebase deploy --only hosting:main --project cocinia-staging firebase deploy --only firestore --project cocinia-staging

Als een deploy klaar meldt maar je ziet oude content: check curl -sI https://cocinia-staging.web.app/admin/ | grep last-modified. Als de datum niet vers is, re-run met --force.

Per-tenant hosting site deployen

Elke tenant heeft een eigen hosting site (cocinia-staging-{id}.web.app). Die wordt niet meegenomen door firebase deploy tenzij expliciet als target opgegeven — gebruik daarom de GitHub Actions workflow:

  1. Ga naar Actions → Build and Deploy Tenant (Staging)
  2. Klik Run workflow en vul tenant_id in (bv. de-notelaar)
  3. Workflow bouwt tenant-site + tenant-admin met die TENANT_ID en deployt naar cocinia-staging-{id}.web.app

CLI-alternatief (vereist dat de site als target in firebase.json + .firebaserc staat):

firebase deploy --only hosting:cocinia-staging-{id} --project cocinia-staging

ADC credentials (voor staging builds)

gcloud auth application-default login

Staging seeden

node tests/seed-staging.mjs

Known issue: App Check is tijdelijk uitgeschakeld op staging (#38).

5. Productie Omgeving (cocinia-3679c)

Hosting sites

Main (platform + admin shell)

cocinia-3679c.web.app

Serveert /admin/ (login shell) + /platform-admin/

Tenant: demo-premium

cocinia-3679c-demo-premium.web.app

Naam afwijkend van standaard cocinia-{id} wegens collision met staging-demo sites — quick fix toegepast, structurele fix voor deriveSiteId volgt.

Custom domeinen (in verificatie)

DomeinDoel
cocinia.beMarketing site (cocinia-marketing target)
admin.cocinia.bePlatform admin (cocinia-3679c main target, /platform-admin/)

Firebase Auth

  • Email/Password provider ingeschakeld
  • Platform admin: webandallwebdesigns@gmail.complatformAdmin: true claim gesynced via onUserWrite Firestore trigger
  • Web app SDK config: app ID 1:891036335605:web:bb9938bd2b9e73c7c474bc, API key AIzaSyBS3p7arM9KWKF_DFKDbwFM8eTuqEbxZwY

Email domeinen

  • Resend domein geverifieerd: replies.cocinia.be
  • API key: restricted tot replies.cocinia.be (Sending access only)
  • Inbound webhook URL: https://oninboundemail-eegvljpyaq-ew.a.run.app
  • Platform-mails from: noreply@replies.cocinia.be
  • Tenant-mails from: tijdelijk ook noreply@replies.cocinia.be — zie #117 voor per-tenant domein (launch-blocker)
  • Facturen from: facturen@replies.cocinia.be

Secret Manager

NaamStatus
GA4_SERVICE_ACCOUNTAanwezig
RESEND_API_KEYAanwezig
REPLY_HMAC_SECRETAanwezig
RESEND_WEBHOOK_SECRETAanwezig
ANTHROPIC_API_KEYNog via .env (#114)
GOOGLE_AI_API_KEYNog via .env (#114)
GITHUB_TOKENNog via .env (#114)
MOLLIE_TEST_KEYNog via .env (#114)

Firestore

  • Database: propere lei sinds 2026-04-26 — backup op gs://cocinia-3679c.firebasestorage.app/backups/
  • Indexes: firestore.indexes.json is single source of truth (deploy via firebase deploy --only firestore:indexes)
  • Tenants: demo-premium aanwezig als test tenant

Hosting deploy volgorde

Marketing + platform admin

firebase deploy --only hosting:marketing,hosting:main --project cocinia-3679c

Tenant sites — via GitHub Actions

gh workflow run build-tenant.yml --ref master \ -f tenant_id=<id> -f project_id=cocinia-3679c

Workflow bouwt tenant-site + tenant-admin met de juiste TENANT_ID en deployt naar de target uit .firebaserc.

Demo-premium admin

gh workflow run build-tenant.yml --ref master \ -f tenant_id=demo-premium -f project_id=cocinia-3679c

Ignore lijst hosting:main bevat demo-builds (admin-basic/pro/premium, site-basic/pro/premium) — die zitten lokaal in public/ maar worden bewust niet meegestuurd in main deploys.

6. Tenant aanmaken flow

  1. Wizard in platform admin (/platform-admin/tenants/new/) — vult basisgegevens, tier en owner email in.
  2. createTenant callable schrijft het tenant document, stuurt een invite en roept provisionHostingSite aan.
  3. provisionHostingSite maakt automatisch de Firebase Hosting site aan: cocinia-{id} op productie, cocinia-staging-{id} op staging.
  4. buildReady Firestore trigger detecteert de nieuwe tenant en dispatched de GitHub Actions build-tenant workflow (repository_dispatch, type build-tenant).
  5. GitHub Actions bouwt apps/tenant-site + apps/tenant-admin met de juiste TENANT_ID en FIREBASE_PROJECT_ID, en deployt naar de hosting site van de tenant.
  6. Deployment status wordt teruggeschreven in tenants/{id}/deployments/latest.

Handmatig rebuilden kan via de Build and Deploy Tenant workflow (Actions tab) met tenant_id en optioneel project_id.

7. E2E Tests

Lokaal (emulators moeten draaien)

npm run test:e2e npm run test:e2e:ui npm run test:e2e:headed

Staging

npm run test:e2e:staging npm run test:e2e:staging:cleanup

Zie TESTING_GUIDE.md voor fixtures, tier-testing en data-test attributen.

8. Poorten Overzicht

ServicePoort
Emulator UI4000
Hosting (admin + platform)5000
Functions5001
Dev Build Server3099
Site basic (tests)5010
Site pro (tests)5011
Site premium (tests)5012
Firestore8181
Firestore (migratiebron, ad-hoc)8282
Auth9099
Storage9199

Migratiebron emulator (port 8282) wordt ad-hoc gestart voor data-import van een legacy project (bv. denotelaar-7a8d2): firebase emulators:start --config .denotelaar-emulator.json --project denotelaar-7a8d2 --only firestore --import=./database_export.

9. Troubleshooting

"Port XXXX is not open"

Vorige emulators draaien nog:

netstat -ano | findstr "4000 4400 5001 8181 9099 9199"

Kan niet inloggen na emulator restart

Auth emulator start leeg. Seed opnieuw:

node tests/seed.mjs

Gitleaks hook na clone

winget install gitleaks

Maak .git/hooks/pre-commit aan — zie bestaande hook als voorbeeld.

ADC credentials verlopen

gcloud auth application-default login

Storage upload faalt (unauthorized)

Custom claims niet gesynchroniseerd. Uitloggen en opnieuw inloggen.

10. Nuttige Links

Documentatie

TESTING_GUIDE.md · AUDIT_RAPPORT.md · TODO_MASTER.md — in de project root.

11. Email functies

Alle uitgaande mail loopt via Resend. Op staging worden recipients automatisch geredirected naar DEV_EMAIL zodat geen echte klanten gemaild worden.

Architectuur — twee lagen

  • Platform-mails — van COCINIA naar tenants (facturen, credit-waarschuwingen, welkomstmail bij aanmaak, lead-notificaties). From-adres altijd op replies.cocinia.be.
  • Tenant-mails — van het restaurant naar hun klanten (reservaties, cadeaubonnen, nieuwsbrief, contactbevestiging). From-adres uit tenants/{id}/settings/general.senderEmail of fallback (zie issue #117).
  • Inbound replies van klanten worden via een Resend webhook (onInboundEmail) verwerkt en verschijnen in de berichten-tab van de tenant-admin.

Veiligheidsnet — non-prod redirect

functions/src/shared/tenant-helper.js levert twee helpers die elke Resend-call moet gebruiken. Detectie via isProductionProject() = vergelijkt process.env.GCLOUD_PROJECT met cocinia-3679c.

  • getDevSafeEmail(email) — staging/lokaal: returns DEV_EMAIL (fallback dev@cocinia.be). Prod: returns origineel adres.
  • getDevSafeFrom(tenant) — staging/lokaal: returns ${tenant.fromName} <noreply@${REPLY_DOMAIN}> (default replies-staging.cocinia.be). Prod: returns ${tenant.fromName} <${tenant.fromEmail}> uit Firestore — call sites geven hardcoded noreply@replies.cocinia.be mee voor platform-mails.

Regel: elke resend.emails.send({ from, to }) call MOET deze wrappers gebruiken. Anders lekt staging-test naar echte klant-mailboxes.

Configuratie per omgeving

Variabele Lokaal / emulator cocinia-staging cocinia-3679c (prod)
RESEND_API_KEY functions/.env Secret Manager Secret Manager (issue #114)
REPLY_HMAC_SECRET functions/.env Secret Manager Secret Manager (issue #114)
DEV_EMAIL eigen email staging@cocinia.be — (niet actief, returns origineel)
REPLY_DOMAIN replies-staging.cocinia.be replies-staging.cocinia.be (geverifieerd) replies.cocinia.be (geverifieerd, API key restricted)
From-adres (platform) noreply@replies-staging.cocinia.be noreply@replies-staging.cocinia.be noreply@replies.cocinia.be · facturen: facturen@replies.cocinia.be
From-adres (tenant) noreply@replies-staging.cocinia.be noreply@replies-staging.cocinia.be Tijdelijk noreply@replies.cocinia.be — per-tenant domein in #117

Secret Manager: instellen met echo "VALUE" | firebase functions:secrets:set NAME --project=cocinia-staging --data-file=-. Elke functie die de secret gebruikt moet secrets: ['NAME'] in de function options hebben staan, anders injecteert Firebase v2 de waarde niet in process.env op runtime.

21 Cloud Functions die Resend gebruiken

Functie Trigger Bestand Secrets
sendContactEmailsonDocumentCreated contacts/comms/contact-email.jsRESEND_API_KEY
sendVoucherEmailonDocumentCreated vouchers/vouchers/email.jsRESEND_API_KEY
onLeadCreatedonDocumentCreated leads/marketing/lead-notification.jsRESEND_API_KEY
sendNewslettercallablecomms/sendNewsletter.jsRESEND_API_KEY
checkScheduledNewslettersonSchedule 0 * * * *comms/scheduledNewsletters.jsRESEND_API_KEY
sendNewsletterConfirmationcallablecomms/newsletter-email.jsRESEND_API_KEY
confirmNewsletterSubscriptiononRequest (HTTP)comms/newsletter-email.jsRESEND_API_KEY
resetMonthlyCreditsonSchedule 0 2 * * *credits/index.jsRESEND_API_KEY
createTenantAdmincallabletenants/createTenantAdmin.jsRESEND_API_KEY
onInboundEmailonRequest (Resend webhook)messaging/inbound-email.jsRESEND_API_KEY + REPLY_HMAC_SECRET
sendReplycallablemessaging/send-reply.jsRESEND_API_KEY + REPLY_HMAC_SECRET
createReservationcallablereservations/createReservation.jsRESEND_API_KEY
confirmReservationcallablereservations/confirmReservation.jsRESEND_API_KEY
rejectReservationcallablereservations/rejectReservation.jsRESEND_API_KEY
cancelByTokenonRequest (HTTP)reservations/cancelByToken.jsRESEND_API_KEY
sendDailyRemindersonSchedule every day 10:00reservations/sendDailyReminders.jsRESEND_API_KEY
scheduledBillingCycleonSchedule 0 4 * * *invoicing/scheduled-billing-cycle.jsRESEND_API_KEY
triggerBillingCyclecallableinvoicing/scheduled-billing-cycle.jsRESEND_API_KEY
scheduledOverdueRemindersonSchedule 0 8 * * *invoicing/scheduled-overdue-reminders.jsRESEND_API_KEY
triggerOverdueReminderscallableinvoicing/scheduled-overdue-reminders.jsRESEND_API_KEY
generateInvoicecallableinvoicing/generate-invoice.jsRESEND_API_KEY

Twee functions hebben óók REPLY_HMAC_SECRET nodig (geel) — ze genereren of parsen reply-addresses via messaging/reply-address.js.

Inbound email — wanneer een klant replyt

Wanneer een klant antwoordt op een email van het restaurant, vangt COCINIA die reply op via Resend en toont hem in de berichten-tab van de tenant-admin. Het restaurant ontvangt twee dingen:

  1. Een notificatie email in hun gewone mailbox (op tenants/{id}/settings/general.email) met preview + link naar admin berichten-tab
  2. De reply staat zichtbaar in de berichten-tab van de admin als nieuwe inbound entry op de thread

Volledige flow

Klant replyt op email
  → Reply gaat naar: replies+ct_{tenantId}_{contactId}_{hmac}@replies.cocinia.be
  → Resend ontvangt op MX record voor replies.cocinia.be
  → Resend stuurt webhook POST naar onInboundEmail Cloud Function
  → Function verifieert Svix signature + parseert HMAC
  → Body opgehaald via resend.emails.get(emailId)
  → Quoted text gestript (cleanEmailBody)
  → Reply opgeslagen in Firestore (thread arrayUnion)
  → Status 'new' + readAt = null
  → Notificatie email verstuurd naar settings/general.email
     met preview van de reply + link naar admin berichten-tab

⚠️ Stille fail bij ontbrekend tenant-email

Als settings/general.email leeg is in Firestore, wordt de Firestore-update wel uitgevoerd maar verstuurt COCINIA geen notificatie. Het restaurant ziet de reply dan alleen als ze inloggen in de admin. Er is geen log of alert voor deze situatie — moet gecheckt worden bij tenant-aanmaak.

Reply-To mechanisme

Wanneer het restaurant een bericht stuurt via de admin, krijgt de klant een email met:

  • From: noreply@replies.cocinia.be (of het tenant from-adres)
  • Reply-To: replies+ct_{tenantId}_{contactId}_{hmac}@replies.cocinia.be

Het restaurant-email adres blijft privé — de klant ziet alleen het replies.cocinia.be adres. De HMAC in het reply-to adres zorgt voor veilige thread-attributie zodat een reply altijd bij het juiste gesprek terechtkomt.

Item Productie Staging
Webhook URL https://oninboundemail-eegvljpyaq-ew.a.run.app https://oninboundemail-pznzvb44sa-ew.a.run.app
Reply-domein replies.cocinia.be replies-staging.cocinia.be
Beveiliging Svix signature verificatie (svix-id, svix-timestamp, svix-signature) — invalide requests → HTTP 401. Signing secret in RESEND_WEBHOOK_SECRET.

From-adressen per mail-type (productie)

Platform-mails (van COCINIA naar tenant)

Mail From Naar
Welkomstmail nieuwe tenantnoreply@replies.cocinia.betenant admin email
Factuurfacturen@replies.cocinia.betenant billing email
Aanmaning / overduefacturen@replies.cocinia.betenant billing email
Credit-waarschuwingnoreply@replies.cocinia.betenant admin email
Lead-notificatie (marketing)noreply@replies.cocinia.beplatform/config.adminEmail

Tenant-mails (van restaurant naar klant)

Alle tenant-mails gebruiken dezelfde senderEmail fallback-logica (in functions/src/shared/tenant-helper.js):

tenant.senderEmail
  || tenant.email.senderEmail
  || `noreply@${tenantId}.cocinia.be`  // ← tijdelijk, zie issue #117

Type-specifiek: contactformulier bevestiging, cadeaubon (met PDF), reservatie ontvangst/bevestiging/weigering/herinnering, annulering, nieuwsbrief welkom, nieuwsbrief versturen — alle deze gebruiken senderEmail of fallback richting de klant.

⚠️ Issue #117: de fallback noreply@{tenantId}.cocinia.be is momenteel niet geverifieerd in Resend op productie. Tenant-mails werken alleen correct als er een senderEmail ingesteld is op een geverifieerd domein. Zie sectie "Per-tenant email setup" hieronder.

DNS-configuratie in one.com

Voor replies.cocinia.be (productie)

Type Naam Waarde Prio
TXTresend._domainkey.repliesDKIM public key (uit Resend)
MXsend.repliesfeedback-smtp.eu-west-1.amazonses.com10
TXTsend.repliesv=spf1 include:amazonses.com include:_custspf.one.com ~all
MXrepliesinbound-smtp.eu-west-1.amazonaws.com10
TXT_dmarcv=DMARC1; p=none;

one.com specifiek: voeg altijd include:_custspf.one.com toe aan het SPF record, anders kunnen one.com mailservices geblokkeerd worden.

Voor replies-staging.cocinia.be (staging): zelfde structuur maar met replies-staging als subdomein prefix.

Per-tenant email setup (issue #117)

Launch-blocker voor elke tenant. Zonder eigen geverifieerd domein gaan tenant-mails via noreply@replies.cocinia.be, wat niet professioneel is — klant ziet COCINIA als afzender, niet het restaurant.

Optie A — Eigen restaurantdomein (aanbevolen)

Gebruik het bestaande domein van het restaurant, bv. noreply@restaurant-denotelaar.be. Voordeel: professioneel, klant herkent het restaurant. Vereiste: toegang tot DNS van het restaurantdomein.

  1. Resend → Domains → Add domain — voer het restaurantdomein in. Regio: Ireland (eu-west-1).
  2. DNS records toevoegen bij de DNS-provider van het restaurant:
    TXT  resend._domainkey.{domein}    [DKIM waarde uit Resend]
    MX   send.{domein}                 feedback-smtp.eu-west-1.amazonses.com  (prio 10)
    TXT  send.{domein}                 v=spf1 include:amazonses.com ~all

    Bij one.com: gebruik v=spf1 include:amazonses.com include:_custspf.one.com ~all

  3. Wachten op verificatie in Resend (5-30 minuten, soms tot 24u)
  4. Resend API key uitbreiden — productie key moet ook {restaurantdomein} als toegestaan domein hebben (of nieuwe restricted key per tenant — veiliger maar meer werk)
  5. Firestore instellen:
    tenants/{tenantId}/settings/general.senderEmail = "noreply@restaurant-denotelaar.be"
  6. Rebuild tenant-admin zodat de nieuwe instelling actief is

Optie B — COCINIA subdomein

Gebruik noreply@{tenantId}.cocinia.be, bv. noreply@de-notelaar.cocinia.be. Voordeel: geen toegang tot extern domein nodig. Nadeel: klant ziet een cocinia.be subdomein in plaats van het restaurantdomein. Stappen identiek aan Optie A maar met het subdomein op one.com voor cocinia.be.

Wat als restaurant geen eigen domein heeft

Tijdelijke fallback (huidig gedrag): zolang er geen senderEmail ingesteld is, gaan alle tenant-mails via noreply@replies.cocinia.be. Werkt technisch maar niet ideaal voor branding — klant ziet COCINIA, niet het restaurant. Optie B is dan de beste route — geef het restaurant een COCINIA subdomein.

Checklist bij nieuwe tenant aanmaken

  • [ ] Tenant aangemaakt via platform-admin wizard
  • [ ] Email-domein kiezen: eigen domein of {tenantId}.cocinia.be subdomein
  • [ ] Domein toevoegen in Resend en verifiëren
  • [ ] DNS records toevoegen bij DNS-provider
  • [ ] senderEmail instellen in Firestore (tenants/{tid}/settings/general)
  • [ ] email instellen in tenants/{tid}/settings/general — vereist voor inbound-reply notificaties (zie stille-fail waarschuwing)
  • [ ] Testen: stuur testmail vanuit berichten-tab in admin
  • [ ] Verifieer dat mail aankomt en from-adres correct is

Speciale mailboxen

Adres Doel Status
staging@cocinia.beAlle staging mails (DEV_EMAIL)✅ one.com
contact@cocinia.beFallback + zichtbaar op marketing site⏳ aan te maken
webandallwebdesigns@gmail.comPlatform admin notificaties (prod)✅ Gmail

Probleemoplossing

Mail wordt niet verstuurd (403 in Resend logs)

Oorzaak: API key is niet geautoriseerd voor het from-domein.
Fix: controleer of het from-adres een geverifieerd domein gebruikt (replies.cocinia.be voor platform-mails).

Mail wordt verstuurd maar komt niet aan

Oorzaak 1: DEV_EMAIL redirect actief — check of isProductionProject() correct werkt.
Oorzaak 2: spam/bounce — check Resend dashboard → Logs voor deliverability status.

Inbound webhook geeft 401

Oorzaak: Svix signature verificatie faalt — signing secret klopt niet.
Fix: controleer RESEND_WEBHOOK_SECRET in Secret Manager en herlaad de function.

Dubbele berichten in berichten-tab

Oorzaak: webhook wordt tweemaal afgeleverd door Resend (retry na timeout).
Fix: onInboundEmail moet idempotent zijn — check of er al een bericht bestaat voor hetzelfde messageId.

Restaurant ontvangt geen notificatie bij klant-reply

Oorzaak: settings/general.email is leeg in Firestore — stille fail in inbound-email.js (geen log, geen alert).
Fix: controleer bij tenant-aanmaak dat settings/general.email ingesteld is. Een waarschuwing tonen in de platform-admin als dit veld leeg is zou wenselijk zijn (open TODO in inbound-email.js regel 200-201: if (!tenantEmail) return; zonder logging).

12. Peppol / Maventa e-facturatie

Cocinia stuurt zijn eigen abonnementsfacturen door Peppol via Maventa als access point. UBL Peppol BIS Billing 3.0 wordt al gegenereerd door de bestaande invoice-pipeline; deze laag verzorgt het transport. Tenants zonder Peppol-ID krijgen alleen de PDF email (huidig gedrag — geen breaking change).

Architectuur

FileVerantwoordelijkheid
maventa/token-helper.jsOAuth2 token cache (scope=eui, TTL 55min)
maventa/submit-invoice.jsPOST /v1/invoices (multipart, PEPPOLBIS30) + derivePeppolEia()
maventa/get-invoice.jsGET /v1/invoices/{id} voor re-fetch in webhook
maventa/webhook.jsonRequest handler — URL-token check + re-fetch + Firestore update
generate-invoice.jsAuto-submit na PDF email (alleen auto-flow, niet manueel)
send-via-peppol.jsonCall — manuele retry vanuit platform-admin

Firebase Secrets (cocinia-staging)

SecretRol
MAVENTA_VENDOR_API_KEYOAuth2 vendor_api_key
MAVENTA_USER_API_KEYOAuth2 client_secret
MAVENTA_COMPANY_UUIDOAuth2 client_id (= Cocinia's Maventa company)
MAVENTA_BILLING_UUIDMaventa customer-id voor reconciliatie (niet runtime)
MAVENTA_WEBHOOK_SECRETRandom token in webhook-URL ?token=... (vervangt HMAC, want Maventa signeert niet)

Toggles & vlaggen

platform/config.peppolEnabled (Firestore)

Kill-switch in Settings UI. false = alle Peppol-submits worden geskipt (auto-flow én manuele retry); alleen PDF email. Cache TTL: ~15 min per Cloud Function instance — flip op enige minuten vóór release.

customer.peppolId vs customer.kboNumber

Recipient EIA wordt afgeleid: peppolId (override) wint, anders 0208:<kboNumber> (Belgisch CBE/KBO scheme). Beide leeg → geen Peppol-submit voor die factuur.

MAVENTA_PREVENT_ROUTING env var (optioneel)

Op true zet alle submits in dry-run modus — Maventa accepteert en zet status SENT, maar levert NIET af aan Peppol. Voor staging-only veiligheid. Voorlopig niet aan secrets-array toegevoegd; activeren betekent een deploy.

Lokale scripts

Token health check
node scripts/maventa-token-smoke.js
End-to-end submit met test UBL (prevent_routing=true)
node scripts/maventa-submit-smoke.js
Laatste invoice van een tenant submitten (prevent_routing default)
node scripts/maventa-submit-last-invoice.js --tenant de-notelaar

Voeg --live toe voor echte Peppol-delivery. Aborts als peppolSubmittedAt al gezet is — verwijder dat veld uit het invoice-doc om opnieuw te submitten.

platform/config velden zetten via admin SDK
GCLOUD_PROJECT=cocinia-staging node scripts/set-platform-config.js peppolEnabled=false
Webhook (re-)registreren bij Maventa
node scripts/maventa-register-webhook.js https://europe-west1-cocinia-staging.cloudfunctions.net/maventaWebhook

Script checkt of er al een notification met dezelfde base URL bestaat en aborts om duplicaat-registraties te voorkomen.

Status lifecycle (peppolStatus)

PENDING → DELIVERED (= SENT) → DELIVERY_CONFIRMED FAILED / ERROR

PENDING komt direct uit de submit-respons. Statuswijzigingen daarna komen via webhook. Met prevent_routing=true blijft het meestal bij PENDING — Maventa skipt de routing-pipeline.

Bekende beperkingen

Manuele factuur in platform-admin doet GEEN auto-Peppol

generateManualInvoice heeft de Peppol-block niet geïntegreerd (drift-vermijding). Gebruik de sendViaPeppol callable of het maventa-submit-last-invoice.js script om handmatig aan te trekken.

BTW-vrijgestelde facturen × Peppol nog niet gevalideerd

Zie #137 — UBL TaxCategory voor art. 56bis tegen Maventa staging testen.

Productie credentials nog niet ingesteld

Alle secrets bestaan alleen in cocinia-staging. Voor productie: aparte Maventa company-registratie, eigen credentials, en MAVENTA_BASE_URL=https://ax.maventa.com env override.

Troubleshooting

OAuth2 token-call geeft 401

Oorzaak: verkeerd geplakte credentials, of scope fout. Fix: run maventa-token-smoke.js — succesresponse bewijst dat alle 3 OAuth-secrets correct zijn. Scope is hardcoded eui in token-helper.

POST /v1/invoices geeft 401 ondanks geldig token

Oorzaak: scope=eui ontbreekt — gebeurt bij oudere token-helper builds. Fix: controleer dat token-helper.js DEFAULT_SCOPE = 'eui' heeft.

Webhook fired maar peppolStatus niet bijgewerkt

Oorzaak: peppolMessageId in Firestore matcht niet met de Maventa-id in webhook payload. Fix: check Cloud Function logs voor "geen Cocinia invoice met peppolMessageId=..."; mogelijk werd de invoice via een ander script gesubmit zonder Firestore update.

Factuur staat in Maventa maar peppolMessageId in Firestore is leeg

Oorzaak: submit gelukt, maar Firestore update faalde of werd niet gedaan (bv. door manuele test buiten de helper). Fix: handmatig peppolSubmittedAt + peppolMessageId + peppolStatus velden zetten zodat de webhook ze kan koppelen.