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)
| Sectie | URL |
|---|---|
| Dashboard | localhost:5000/admin/dashboard/?tenant=demo-premium |
| Gerechten | localhost:5000/admin/gerechten/?tenant=demo-premium |
| Pagina's | localhost:5000/admin/paginas/?tenant=demo-premium |
| Instellingen | localhost: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/
Marketing Site
cocinia-staging-marketing.web.appDemo 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:
- Ga naar Actions → Build and Deploy Tenant (Staging)
- Klik Run workflow en vul
tenant_idin (bv.de-notelaar) - Workflow bouwt tenant-site + tenant-admin met die
TENANT_IDen deployt naarcocinia-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.appServeert /admin/ (login shell) + /platform-admin/
Marketing
cocinia-marketing.web.appTenant: demo-premium
cocinia-3679c-demo-premium.web.appNaam afwijkend van standaard cocinia-{id} wegens collision met staging-demo sites — quick fix toegepast, structurele fix voor deriveSiteId volgt.
Custom domeinen (in verificatie)
| Domein | Doel |
|---|---|
| cocinia.be | Marketing site (cocinia-marketing target) |
| admin.cocinia.be | Platform admin (cocinia-3679c main target, /platform-admin/) |
Firebase Auth
- Email/Password provider ingeschakeld
- Platform admin:
webandallwebdesigns@gmail.com—platformAdmin: trueclaim gesynced viaonUserWriteFirestore trigger - Web app SDK config: app ID
1:891036335605:web:bb9938bd2b9e73c7c474bc, API keyAIzaSyBS3p7arM9KWKF_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
| Naam | Status |
|---|---|
| GA4_SERVICE_ACCOUNT | Aanwezig |
| RESEND_API_KEY | Aanwezig |
| REPLY_HMAC_SECRET | Aanwezig |
| RESEND_WEBHOOK_SECRET | Aanwezig |
| ANTHROPIC_API_KEY | Nog via .env (#114) |
| GOOGLE_AI_API_KEY | Nog via .env (#114) |
| GITHUB_TOKEN | Nog via .env (#114) |
| MOLLIE_TEST_KEY | Nog via .env (#114) |
Firestore
- Database: propere lei sinds 2026-04-26 — backup op
gs://cocinia-3679c.firebasestorage.app/backups/ - Indexes:
firestore.indexes.jsonis single source of truth (deploy viafirebase deploy --only firestore:indexes) - Tenants:
demo-premiumaanwezig 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
-
Wizard in platform admin (
/platform-admin/tenants/new/) — vult basisgegevens, tier en owner email in. -
createTenantcallable schrijft het tenant document, stuurt een invite en roeptprovisionHostingSiteaan. -
provisionHostingSitemaakt automatisch de Firebase Hosting site aan:cocinia-{id}op productie,cocinia-staging-{id}op staging. -
buildReadyFirestore trigger detecteert de nieuwe tenant en dispatched de GitHub Actions build-tenant workflow (repository_dispatch, typebuild-tenant). -
GitHub Actions bouwt
apps/tenant-site+apps/tenant-adminmet de juisteTENANT_IDenFIREBASE_PROJECT_ID, en deployt naar de hosting site van de tenant. -
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
| Service | Poort |
|---|---|
| Emulator UI | 4000 |
| Hosting (admin + platform) | 5000 |
| Functions | 5001 |
| Dev Build Server | 3099 |
| Site basic (tests) | 5010 |
| Site pro (tests) | 5011 |
| Site premium (tests) | 5012 |
| Firestore | 8181 |
| Firestore (migratiebron, ad-hoc) | 8282 |
| Auth | 9099 |
| Storage | 9199 |
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
GitHub
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.senderEmailof 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: returnsDEV_EMAIL(fallbackdev@cocinia.be). Prod: returns origineel adres.getDevSafeFrom(tenant)— staging/lokaal: returns${tenant.fromName} <noreply@${REPLY_DOMAIN}>(defaultreplies-staging.cocinia.be). Prod: returns${tenant.fromName} <${tenant.fromEmail}>uit Firestore — call sites geven hardcodednoreply@replies.cocinia.bemee 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 |
|---|---|---|---|
| sendContactEmails | onDocumentCreated contacts/ | comms/contact-email.js | RESEND_API_KEY |
| sendVoucherEmail | onDocumentCreated vouchers/ | vouchers/email.js | RESEND_API_KEY |
| onLeadCreated | onDocumentCreated leads/ | marketing/lead-notification.js | RESEND_API_KEY |
| sendNewsletter | callable | comms/sendNewsletter.js | RESEND_API_KEY |
| checkScheduledNewsletters | onSchedule 0 * * * * | comms/scheduledNewsletters.js | RESEND_API_KEY |
| sendNewsletterConfirmation | callable | comms/newsletter-email.js | RESEND_API_KEY |
| confirmNewsletterSubscription | onRequest (HTTP) | comms/newsletter-email.js | RESEND_API_KEY |
| resetMonthlyCredits | onSchedule 0 2 * * * | credits/index.js | RESEND_API_KEY |
| createTenantAdmin | callable | tenants/createTenantAdmin.js | RESEND_API_KEY |
| onInboundEmail | onRequest (Resend webhook) | messaging/inbound-email.js | RESEND_API_KEY + REPLY_HMAC_SECRET |
| sendReply | callable | messaging/send-reply.js | RESEND_API_KEY + REPLY_HMAC_SECRET |
| createReservation | callable | reservations/createReservation.js | RESEND_API_KEY |
| confirmReservation | callable | reservations/confirmReservation.js | RESEND_API_KEY |
| rejectReservation | callable | reservations/rejectReservation.js | RESEND_API_KEY |
| cancelByToken | onRequest (HTTP) | reservations/cancelByToken.js | RESEND_API_KEY |
| sendDailyReminders | onSchedule every day 10:00 | reservations/sendDailyReminders.js | RESEND_API_KEY |
| scheduledBillingCycle | onSchedule 0 4 * * * | invoicing/scheduled-billing-cycle.js | RESEND_API_KEY |
| triggerBillingCycle | callable | invoicing/scheduled-billing-cycle.js | RESEND_API_KEY |
| scheduledOverdueReminders | onSchedule 0 8 * * * | invoicing/scheduled-overdue-reminders.js | RESEND_API_KEY |
| triggerOverdueReminders | callable | invoicing/scheduled-overdue-reminders.js | RESEND_API_KEY |
| generateInvoice | callable | invoicing/generate-invoice.js | RESEND_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:
- Een notificatie email in hun gewone mailbox (op
tenants/{id}/settings/general.email) met preview + link naar admin berichten-tab - 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)
| From | Naar | |
|---|---|---|
| Welkomstmail nieuwe tenant | noreply@replies.cocinia.be | tenant admin email |
| Factuur | facturen@replies.cocinia.be | tenant billing email |
| Aanmaning / overdue | facturen@replies.cocinia.be | tenant billing email |
| Credit-waarschuwing | noreply@replies.cocinia.be | tenant admin email |
| Lead-notificatie (marketing) | noreply@replies.cocinia.be | platform/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 |
|---|---|---|---|
| TXT | resend._domainkey.replies | DKIM public key (uit Resend) | — |
| MX | send.replies | feedback-smtp.eu-west-1.amazonses.com | 10 |
| TXT | send.replies | v=spf1 include:amazonses.com include:_custspf.one.com ~all | — |
| MX | replies | inbound-smtp.eu-west-1.amazonaws.com | 10 |
| TXT | _dmarc | v=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.
- Resend → Domains → Add domain — voer het restaurantdomein in. Regio: Ireland (eu-west-1).
- 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 ~allBij one.com: gebruik
v=spf1 include:amazonses.com include:_custspf.one.com ~all - Wachten op verificatie in Resend (5-30 minuten, soms tot 24u)
- Resend API key uitbreiden — productie key moet ook
{restaurantdomein}als toegestaan domein hebben (of nieuwe restricted key per tenant — veiliger maar meer werk) - Firestore instellen:
tenants/{tenantId}/settings/general.senderEmail = "noreply@restaurant-denotelaar.be" - 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.besubdomein - [ ] Domein toevoegen in Resend en verifiëren
- [ ] DNS records toevoegen bij DNS-provider
- [ ]
senderEmailinstellen in Firestore (tenants/{tid}/settings/general) - [ ]
emailinstellen intenants/{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.be | Alle staging mails (DEV_EMAIL) | ✅ one.com |
| contact@cocinia.be | Fallback + zichtbaar op marketing site | ⏳ aan te maken |
| webandallwebdesigns@gmail.com | Platform 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
| File | Verantwoordelijkheid |
|---|---|
| maventa/token-helper.js | OAuth2 token cache (scope=eui, TTL 55min) |
| maventa/submit-invoice.js | POST /v1/invoices (multipart, PEPPOLBIS30) + derivePeppolEia() |
| maventa/get-invoice.js | GET /v1/invoices/{id} voor re-fetch in webhook |
| maventa/webhook.js | onRequest handler — URL-token check + re-fetch + Firestore update |
| generate-invoice.js | Auto-submit na PDF email (alleen auto-flow, niet manueel) |
| send-via-peppol.js | onCall — manuele retry vanuit platform-admin |
Firebase Secrets (cocinia-staging)
| Secret | Rol |
|---|---|
| MAVENTA_VENDOR_API_KEY | OAuth2 vendor_api_key |
| MAVENTA_USER_API_KEY | OAuth2 client_secret |
| MAVENTA_COMPANY_UUID | OAuth2 client_id (= Cocinia's Maventa company) |
| MAVENTA_BILLING_UUID | Maventa customer-id voor reconciliatie (niet runtime) |
| MAVENTA_WEBHOOK_SECRET | Random token in webhook-URL ?token=... (vervangt HMAC, want Maventa signeert niet) |
Toggles & vlaggen
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.
Recipient EIA wordt afgeleid: peppolId (override) wint, anders
0208:<kboNumber> (Belgisch CBE/KBO scheme). Beide leeg → geen Peppol-submit voor die factuur.
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
node scripts/maventa-token-smoke.js
node scripts/maventa-submit-smoke.js
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.
GCLOUD_PROJECT=cocinia-staging node scripts/set-platform-config.js peppolEnabled=false
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
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.
Zie #137 — UBL TaxCategory voor art. 56bis tegen Maventa staging testen.
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.