VGBC Ticket Systeem - Definitieve Specificatie¶
Datum: 2025-11-25
Status: In ontwikkeling
Doel: Complete beschrijving van ticketsoorten, aankoopflow, en QR-scan proces voor VGBC2026
1. Ticketsoorten (Definitief)¶
1.1 Standaard Ticket¶
Prijs: Instelbaar via Commerce UI
Betaling: Mollie
Checkout: Normale flow
Kenmerken:
- Basisticket voor alle bezoekers
- Prijs handmatig aan te passen via Commerce UI (bijv. Early Bird periode)
- Geen speciale voorwaarden
1.2 Student Ticket¶
Prijs: Korting instelbaar via Commerce UI
Betaling: Mollie
Checkout: Normale flow met automatische korting
Kenmerken:
- NIET een apart ticket type
- Gewoon Standaard Ticket met automatische korting
- Korting wordt toegepast als field_beroep = "student" in customer profile
- Implementatie via Commerce Price Rule of Event Subscriber
- Kortingspercentage instelbaar via UI
- Controle aan de deur: studentenpas vereist
Technisch:
Product Variation: Standaard Ticket
↓
Checkout check: User profile → field_beroep = "student"?
↓
Ja: Apply discount (via Price Rule)
Nee: Normale prijs
1.3 Standhouder Ticket¶
Prijs: €0 (gratis)
Betaling: GEEN Mollie (direct completed)
Checkout: Normale flow met quotum check
Kenmerken:
- Gratis tickets voor Partners (standhouders)
- Gekoppeld aan Partner ECK entity
- Quotum per Partner: field_vgbc2026_tickets (integer)
- Admin/Winkel rol vult quotum handmatig in
- Bij checkout: check of Partner nog quota heeft
- Blokkeer checkout als quotum vol
Technisch:
Partner entity fields:
- field_vgbc2026_tickets (integer) - bijv. "3" voor grote standhouder
- field_vgbc_editie (reference) - link naar VGBC2026
Checkout validatie:
1. User selecteert Standhouder ticket
2. System checkt: Hoeveel tickets al verkocht voor deze Partner?
3. Als (verkocht < quotum): Allow checkout
4. Als (verkocht >= quotum): Block + error message
1.4 Custom/Admin Ticket¶
Prijs: Variabel (instelbaar per order)
Betaling: GEEN Mollie (handmatig markeren als betaald)
Checkout: Normale flow maar direct completed
Kenmerken:
- Voor uitzonderingen (VIPs, cash ter plekke, gratis crew)
- Aangemaakt door rol "Winkel" of "Administrator"
- Geen Mollie redirect
- Order status direct: "completed" of "custom_created"
- Invoice + QR gegenereerd zoals normaal
- Email via Symfony Mailer
Use cases: - Cash betaling aan de balie - VIP uitnodigingen - Crew/vrijwilligers - Compensatie tickets
Technisch:
Order field: field_payment_method
Opties: "mollie" | "custom"
Custom ticket flow:
1. Winkel medewerker maakt order aan (namens klant)
2. Selecteert payment method: "custom"
3. Order state: direct "completed" (skip Mollie)
4. Invoice gegenereerd
5. QR-code gegenereerd
6. Email naar klant
1.5 VIP/Gratis (Gastenlijst)¶
Prijs: N.v.t.
Betaling: N.v.t.
Checkout: Gaat BUITEN Commerce systeem om
Kenmerken:
- Handmatige gastenlijst (Excel/spreadsheet)
- GEEN Drupal accounts
- GEEN QR-codes
- Check aan de deur: naam op lijst
2. Meerdere Tickets Kopen¶
2.1 Probleem¶
Jan wil 2 tickets kopen: 1 voor zichzelf, 1 voor Marie.
Elk ticket moet unieke QR-code hebben gekoppeld aan juiste persoon.
2.2 Oplossing: Ticket Toewijzing tijdens Checkout¶
Checkout flow:
1. Jan logt in
2. Jan voegt 2× Standaard Ticket toe aan winkelwagen
3. Checkout stap "Deelnemers toewijzen" (custom pane):
┌─────────────────────────────────────┐
│ Ticket 1 │
│ ○ Jezelf (Jan Jansen) │
│ ○ Iemand anders │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ Ticket 2 │
│ ○ Jezelf │
│ ● Iemand anders │
│ │
│ Naam: [Marie de Vries_______] │
│ Email: [marie@example.com____] │
│ Beroep: [▼ Student ] │
│ │
│ ℹ️ Alleen beroep nodig voor │
│ studentenkorting. Marie kan │
│ rest later zelf invullen. │
└─────────────────────────────────────┘
4. Prijs herberekening:
- Ticket 1 (Jan): Standaard prijs
- Ticket 2 (Marie, Student): Met korting
- Totaal: berekend
5. Betaal via Mollie
6. Order completed → 2 aparte handelingen:
a. License 1 → Jan (uid=45)
- QR: VGBC2026-USER45-LICENSE123
- Email naar jan@example.com
b. License 2 → Marie (uid=789, nieuw account!)
- User aangemaakt met naam + email + beroep
- Customer profile aangemaakt (field_beroep="student")
- QR: VGBC2026-USER789-LICENSE124
- Email naar marie@example.com
- Wachtwoord reset link
7. Marie krijgt email:
"Jan Jansen heeft een ticket voor je gekocht!
Je QR-code vind je in de bijlage.
Log in om je profiel aan te vullen: [reset link]"
2.3 Technische Implementatie¶
Custom Checkout Pane:
/web/modules/custom/vg_commerce/src/Plugin/Commerce/CheckoutPane/TicketAssignmentPane.php
Key functies:
buildPaneForm():
- Loop door order items (tickets)
- Toon radio: "Jezelf" of "Iemand anders"
- Als "Iemand anders": toon naam + email + beroep veld
- Haal beroep opties op uit field_storage (profile.field_beroep)
submitPaneForm():
- Voor elk ticket:
- Als "Jezelf": attendee_uid = current_user
- Als "Iemand anders":
- Check of email al bestaat → gebruik bestaande user
- Zo niet: maak nieuwe user + customer profile aan
- Sla ALLEEN field_beroep op (rest blijft leeg!)
- Stuur wachtwoord reset email
- Sla attendee_uid op in order_item->setData('attendee_uid', $uid)
License Generatie:
Event Subscriber luistert naar commerce_order.place.post_transition:
foreach ($order->getItems() as $order_item) {
$attendee_uid = $order_item->getData('attendee_uid');
// Genereer License voor JUISTE user
$license = License::create([
'type' => 'vgbc_ticket',
'uid' => $attendee_uid, // ← Jan of Marie
'product_variation' => $order_item->getPurchasedEntity(),
]);
// Genereer QR-code
$qr_code = generateQR("VGBC2026-USER{$attendee_uid}-LICENSE{$license->id()}");
// Attach aan license
$license->set('field_qr_code', $qr_code);
$license->save();
// Stuur email naar attendee (niet naar koper!)
sendTicketEmail($attendee_uid, $license);
}
3. QR-code Scan Workflow (Aan de Deur)¶
3.1 Complete Flow¶
┌─────────────────────────────────────────────────────────┐
│ BEZOEKER ARRIVEERT │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Bezoeker toont QR-code │
│ - Via email op telefoon │
│ - Of geprint op papier │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ DEUR MEDEWERKER (tablet/phone) │
│ │
│ 1. Open browser → /scan │
│ (permission: "scan vgbc tickets") │
│ │
│ 2. Camera interface opent automatisch │
│ - Toestemming camera wordt gevraagd (eerste keer) │
│ - Live video stream zichtbaar │
│ - Overlay met scan area │
│ │
│ 3. Bezoeker houdt QR voor camera │
│ - jsQR library scant automatisch (elke 100ms) │
│ - Geen knop klikken nodig │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ DRUPAL VALIDATIE (server-side) │
│ │
│ API: /api/ticket/validate?code=VGBC2026-USER45-LIC123 │
│ │
│ Controller doet: │
│ 1. Zoek License entity met code │
│ 2. Check status: │
│ - Bestaat de license? │
│ - Is het voor VGBC2026? │
│ - Is het al gescand? │
│ - Is de user valid? │
│ │
│ 3. Update License: │
│ - status: "scanned" │
│ - field_scan_timestamp: NOW() │
│ - field_scan_location: "Hoofdingang" │
│ │
│ 4. Return JSON: │
│ { │
│ "valid": true, │
│ "name": "Jan Jansen", │
│ "ticket_type": "Standaard", │
│ "message": "Welkom!" │
│ } │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ SCAN INTERFACE RESULTAAT │
│ │
│ ✓ GROEN SCHERM (geldig) │
│ ┌─────────────────────────────────────┐ │
│ │ ✓ WELKOM! │ │
│ │ │ │
│ │ Jan Jansen │ │
│ │ Standaard Ticket │ │
│ │ │ │
│ │ [Volgende scan] → │ │
│ └─────────────────────────────────────┘ │
│ │
│ ✗ ROOD SCHERM (probleem) │
│ ┌─────────────────────────────────────┐ │
│ │ ✗ PROBLEEM! │ │
│ │ │ │
│ │ Ticket al gescand om 14:23 │ │
│ │ (Hoofdingang) │ │
│ │ │ │
│ │ Neem contact op met balie │ │
│ │ │ │
│ │ [Volgende scan] → │ │
│ └─────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ ACCREDITATIEBEWIJS GENEREREN │
│ │
│ Trigger: License status = "scanned" │
│ Event Subscriber: LicenseScannedSubscriber │
│ │
│ 1. Maak Certificate entity/node: │
│ - Type: "Deelname Certificaat" │
│ - User: Jan Jansen (uid=45) │
│ - Event: VGBC2026 │
│ - Datum: 15 maart 2026 │
│ - Tijd scan: 14:23:15 │
│ │
│ 2. Genereer PDF: │
│ - Via Entity Print + wkhtmltopdf │
│ - Template: accreditatiebewijs.html.twig │
│ - Bevat: Logo, naam, event, datum + tijd │
│ - QR-code voor verificatie │
│ │
│ 3. Koppel aan user account: │
│ - Downloadbaar op /user/45/certificates │
│ - Link zichtbaar in user dashboard │
│ │
│ 4. Verstuur email (Symfony Mailer): │
│ Subject: "Bedankt voor je deelname aan VGBC2026" │
│ Body: "Je accreditatiebewijs vind je in bijlage" │
│ Attachment: accreditatiebewijs-jan-jansen.pdf │
│ Template: zoals invoice emails (consistency) │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ END RESULT │
│ │
│ ✓ Bezoeker is binnen │
│ ✓ Scan is gelogd in systeem │
│ ✓ Accreditatiebewijs is gegenereerd │
│ ✓ Email is verstuurd │
│ ✓ User kan later downloaden vanaf account │
└─────────────────────────────────────────────────────────┘
3.2 Error Scenarios¶
Scenario 1: Ticket al gescand
Response:
{
"valid": false,
"error": "already_scanned",
"message": "Ticket al gescand om 14:23 (Hoofdingang)",
"scan_time": "2026-03-15T14:23:15",
"name": "Jan Jansen"
}
UI: ROOD scherm met melding
Actie: Neem contact op met balie
Scenario 2: Ongeldig ticket
Response:
{
"valid": false,
"error": "not_found",
"message": "Ticket niet gevonden in systeem"
}
UI: ROOD scherm
Actie: Check of QR-code correct is, of neem contact op met balie
Scenario 3: Verkeerd event
Response:
{
"valid": false,
"error": "wrong_event",
"message": "Dit ticket is voor VGBC2025, niet VGBC2026"
}
UI: ROOD scherm
Actie: Verwijzen naar balie
3.3 Technische Specificaties¶
Frontend (JavaScript):
// Library: jsQR (open source, geen dependencies)
// Camera API: navigator.mediaDevices.getUserMedia()
function scanQR() {
// 1. Start camera
navigator.mediaDevices.getUserMedia({
video: { facingMode: "environment" } // Rear camera op mobiel
});
// 2. Elke 100ms: screenshot van video
setInterval(() => {
const imageData = canvas.getContext('2d').getImageData(0, 0, width, height);
const code = jsQR(imageData.data, width, height);
if (code) {
// 3. QR gevonden → stuur naar Drupal
validateTicket(code.data);
}
}, 100);
}
function validateTicket(qrCode) {
fetch('/api/ticket/validate?code=' + qrCode)
.then(response => response.json())
.then(data => {
if (data.valid) {
showGreenScreen(data.name, data.ticket_type);
} else {
showRedScreen(data.message);
}
});
}
Backend (Drupal Controller):
// Route: /api/ticket/validate
// Permission: "scan vgbc tickets"
public function validate(Request $request) {
$code = $request->query->get('code');
// Parse: VGBC2026-USER45-LICENSE123
preg_match('/VGBC2026-USER(\d+)-LICENSE(\d+)/', $code, $matches);
$user_id = $matches[1];
$license_id = $matches[2];
// Load License
$license = License::load($license_id);
if (!$license) {
return new JsonResponse([
'valid' => FALSE,
'error' => 'not_found',
'message' => 'Ticket niet gevonden',
]);
}
// Check al gescand?
if ($license->get('field_status')->value === 'scanned') {
$scan_time = $license->get('field_scan_timestamp')->value;
return new JsonResponse([
'valid' => FALSE,
'error' => 'already_scanned',
'message' => "Ticket al gescand om " . date('H:i', $scan_time),
'scan_time' => $scan_time,
]);
}
// Valid! Update license
$license->set('field_status', 'scanned');
$license->set('field_scan_timestamp', time());
$license->save();
// Trigger accreditatiebewijs generatie (via event)
\Drupal::service('event_dispatcher')->dispatch(
new LicenseScannedEvent($license),
'vg_ticket.license_scanned'
);
return new JsonResponse([
'valid' => TRUE,
'name' => $license->getOwner()->getDisplayName(),
'ticket_type' => 'Standaard',
'message' => 'Welkom!',
]);
}
4. Implementatie Tijdslijn¶
Fase 1: Basis Commerce Setup ✅ (voltooid)¶
- Commerce modules installed
- Mollie payment gateway
- Invoice generation
- Email via Symfony Mailer
- BTW automatisering (vg_commerce module)
Fase 2: Ticket Product Setup (deze sessie)¶
- Rol "Winkel" aanmaken
- field_payment_method op order
- Partner ECK: field_vgbc2026_tickets quotum
- Standaard Ticket product variation
- Studentenkorting implementeren (Price Rule, percentage via UI)
- Standhouder ticket + quotum validatie
Fase 3: Multi-ticket Checkout (volgende sessie)¶
- Custom Checkout Pane: TicketAssignmentPane
- User + Profile creatie voor "iemand anders"
- Prijs herberekening met kortingen
- Testen: 2 tickets kopen (Jan + Marie)
Fase 4: Commerce License Setup¶
- Commerce License module configureren
- License type: "VGBC Ticket"
- Event Subscriber: Order Paid → License aanmaken
- QR-code generatie (endroid/qr-code)
- QR koppelen aan License entity
- QR toevoegen aan invoice PDF
Fase 5: Scan Interface¶
- Custom module: vg_ticket_scanner
- Route: /scan (permission: "scan vgbc tickets")
- Frontend: jsQR library + camera interface
- Backend: /api/ticket/validate endpoint
- UI: groen/rood scherm feedback
- Logging: wie, wanneer, waar
Fase 6: Accreditatiebewijs¶
- Certificate entity/node type
- Event Subscriber: License Scanned → Certificate
- PDF template (wkhtmltopdf)
- User dashboard: download link
- Email via Symfony Mailer met PDF attachment
Fase 7: Testing & Launch¶
- E2E test: koop → betaal → email → scan → bewijs
- Test studentenkorting
- Test standhouder quotum
- Test multi-ticket (Jan + Marie)
- Test custom/admin tickets
- Scan interface UX test met tablet
- Training deur medewerkers
- Live deployment checklist
5. Open Vragen / Beslissingen Nodig¶
5.1 Partner quotum beheer¶
- Welke rol kan quotum invullen? (Admin + Winkel?)
- UI locatie:
/admin/content/partner/{id}/edit? - Notificatie als quotum vol is?
5.2 Accreditatiebewijs design¶
- Template design beschikbaar?
- Welke content/logo moet erop?
- PDF styling: wie doet dit?
5.3 Scan interface hardware¶
- Welke tablets worden gebruikt?
- WiFi op locatie geregeld?
- Backup plan bij internetuitval?
5.4 Early Bird timing¶
- Handmatig prijs aanpassen of automatisch?
- Als automatisch: welke datum?
6. Modules & Dependencies¶
Contrib modules: - drupal/commerce (^3.0@beta) - drupal/commerce_invoice (^2.2) - drupal/commerce_license (^3.0) - drupal/commerce_mollie (1.x-dev) - drupal/entity_print (PDF generatie) - drupal/symfony_mailer (email) - drupal/eck (Partner entity) - drupal/profile (customer profiles)
JavaScript libraries: - jsQR (QR-code scanning) - endroid/qr-code (QR-code generatie, al geïnstalleerd)
Custom modules: - vg_commerce (BTW, pricing logic) - vg_ticket_scanner (scan interface + API) - NIEUW - vg_mailchimp_subscribe (MailChimp integratie)
System dependencies: - wkhtmltopdf (PDF generatie) - PHP GD of Imagick (QR image handling)
7. Contacten / Verantwoordelijkheden¶
Development: - Robin (technisch)
Redactie/Content: - Partner quotum beheer - Ticket prijzen instellen via Commerce UI - Studentenkorting percentage instellen via Commerce UI - Early bird timing
Event Organisatie: - Deur medewerkers training - Hardware (tablets) organiseren - Studentenpas controle procedure - WiFi op locatie regelen
Collega's (ervaring deur flow): - Review van scan workflow - Feedback op UI/UX scan interface - Training nieuwe medewerkers
8. Notities & Changelog¶
2025-11-25: Sessie 1 - Planning - Ticketsoorten definitief vastgesteld (4 types) - Student ticket = korting op Standaard (niet apart type) - Kortingspercentage en prijzen worden via Commerce UI ingesteld - Multi-ticket flow bedacht (checkout pane met toewijzing) - QR-scan workflow compleet beschreven - Alleen field_beroep nodig tijdens toewijzing (rest vult user later in) - VIP tickets gaan buiten Commerce (handmatige gastenlijst) - Accreditatiebewijs via Symfony Mailer (niet MailChimp) - Tijdslijn 7 fases opgesteld
Volgende sessie: - Start met Fase 2: Rol Winkel + product variations - field_payment_method implementeren - Partner quotum fields toevoegen
Document eigenaar: Robin
Laatste update: 2025-11-25 15:18
Versie: 1.1
9. AI Context & Technical Details¶
9.1 Project Structuur¶
Drupal root: /var/www/sites/dev.voedingsgeneeskunde/
Web root: /var/www/sites/dev.voedingsgeneeskunde/web/
Custom modules: /var/www/sites/dev.voedingsgeneeskunde/web/modules/custom/
Theme: /var/www/sites/dev.voedingsgeneeskunde/web/themes/custom/vg25/
Notities map: /var/www/sites/dev.voedingsgeneeskunde/Notities/
9.2 Bestaande Custom Modules¶
- vg_commerce — BTW automatisering, pricing logic
- vg_invoice_styling — Invoice PDF styling
- vg_mailchimp_subscribe — MailChimp user sync
9.3 Commerce Setup (Huidig)¶
- Order types: default
- Payment gateway: Mollie (id: betaling_via_mollie)
- Invoice numbering: VG-[jaar]-[nummer]
- PDF engine: wkhtmltopdf (
/usr/bin/wkhtmltopdf) - Email: Symfony Mailer
- Product variation types: los_nummer, uitgave, ticket, merchandise, abonnement, e-book
9.4 User Profile Setup¶
- Profile type: customer (via Profile module)
- Belangrijk veld:
field_beroep(list_string) - Beroep opties (o.a.): therapeut, dietist, leefstijlcoach, arts, onderzoeker, student, anders
- Locatie: entity_type=profile, bundle=customer (niet op user!)
9.5 Partner ECK Entity¶
- Entity type: partner (via ECK)
- Nog toe te voegen:
field_vgbc2026_tickets(integer) enfield_vgbc_editie(entity_reference)
9.6 Implementatie Status (Real-time)¶
Voltooid:
- Commerce basis, Mollie, invoices, BTW, Symfony Mailer, profiles met field_beroep
TODO volgende sessies (samenvatting):
- Rol "Winkel"; field_payment_method op order; Partner quotum velden
- Standaard Ticket variation; Price Rule student (percentage via UI); Standhouder + quotum validatie
- Checkout pane TicketAssignmentPane; user/profile creatie; attendee_uid opslaan
- License + QR; scan interface; certificaat PDF en email
9.7 Key Design Decisions¶
1) Student is korting op Standaard (geen apart product)
2) field_beroep zit op Profile (customer), niet op User
3) Multi-ticket toewijzing tijdens checkout (naam, email, beroep)
4) VIP/Crew buiten Commerce (gastenlijst)
5) Standhouder tickets gratis via Commerce met quotum
6) Custom tickets direct completed (invoice flow blijft werken)
9.8 Start volgende sessie¶
1) Open dit document
2) Check TODO in sectie 9.6
3) Begin met: Rol "Winkel" aanmaken en permissions
4) Update deze file (Changelog sectie 8) na elk onderdeel
Voor AI: Dit document is de single source of truth voor het VGBC ticket systeem. Bij sessiestart: lees dit document eerst voor context.
Changelog - Fase 2 Update¶
2025-12-15: Fase 2 Voltooid
Geimplementeerd: - [x] ECK entity vgbc_partner_quota met velden (partner, vgbc_editie, quota) - [x] VGBC2026 editie aangemaakt (ID: 33) - [x] StudentProfessionCondition plugin (vg_student_profession) - [x] StandhouderQuotaValidator event subscriber - [x] Rol Winkel met permissies - [x] field_payment_method op commerce_order - [x] Product variation type vgbc_ticket - [x] Product "VGBC 2026 - Standaard Ticket" (SKU: vgbc2026-ticket, EUR 49.95) - [x] Product "VGBC 2026 - Standhouder Ticket" (SKU: vgbc2026-standhouder, EUR 0.00) - [x] Promotion Studentenkorting 40% - [x] Winkel Views per product type (tickets, uitgaven, merchandise, abonnementen) - [x] Config export
Code locaties: - StudentProfessionCondition: /web/modules/custom/vg_commerce/src/Plugin/Commerce/Condition/StudentProfessionCondition.php - StandhouderQuotaValidator: /web/modules/custom/vg_commerce/src/EventSubscriber/StandhouderQuotaValidator.php - Service registratie: /web/modules/custom/vg_commerce/vg_commerce.services.yml
Bekende limitaties: - VGBC editie ID 33 hardcoded in StandhouderQuotaValidator.php:104 - field_partner op customer profile niet aangemaakt (optioneel, handmatig toe te voegen) - Winkel Views aangemaakt maar nog niet geplaatst in Layout Builder
Testing: - Nog niet uitgevoerd - Testplan gedocumenteerd in 20251215-fase2-ticket-setup-sessie.md
Volgende fase: - Fase 3: Multi-ticket checkout pane en attendee assignment - Zie: 20251215-fase2-ticket-setup-sessie.md sectie "Volgende Sessie"
Versie: 2.0 (Fase 2 voltooid, Fase 3 pending) Laatste update: 2025-12-15