Softwareontwikkeling

Kiewiet: boekingssysteem, winkel en bagagevervoer

Kiewiet Rijwielverhuur BV

Opdrachtgever
Kiewiet Rijwielverhuur BV
Categorie
Softwareontwikkeling
Jaar
2025
Technologieën
4DPHPJavascriptHTMLCSS
Kiewiet Cyclisme — Een volledig boekingsplatform voor de grootste fietsverhuurder van Ameland

Op het Nederlandse eiland Ameland komt vrijwel iedere toerist met de veerboot aan en heeft meteen een fiets nodig. Kiewiet Cyclisme is de meest gevestigde fietsverhuurder van het eiland. Achter de website staat een op maat gebouwd platform voor alle onderdelen van het bedrijf: online reserveringen, bagagevervoer vanaf de veerhaven, winkelverkoop, samenwerking met reisagenten, betalingen, facturen en een reeks desktopwerkplekken in de winkels. Dit is het verhaal van dat systeem.


De uitdaging: één platform, vier bedrijfsactiviteiten

De meeste boekingsplatforms lossen één vraagstuk op. Kiewiet moest er vier tegelijk oplossen en ze verbinden tot één samenhangende ervaring.

  1. Fietsverhuur — Klanten kiezen datums, bekijken fietscategorieën zoals stadsfietsen, mountainbikes, elektrische fietsen, tandems en kinderfietsen, en reserveren specifieke exemplaren op basis van actuele voorraad. De prijs hangt af van het groepstype, de huurduur en de periode. De beschikbaarheid wordt direct berekend aan de hand van een matrix van alle lopende reserveringen.
  2. Bagagevervoer — Aankomende toeristen kunnen bagagevervoer van de veerhaven naar hun vakantieadres boeken. Orders houden rekening met de dienstregeling, gewichtsafhankelijke prijzen, traceerlabels met QR-code en routeoptimalisatie die elk pakket aan een vervoersrit toewijst.
  3. Winkelverkoop — Helmen, sloten, verlichting, kinderzitjes en accessoires worden via een apart winkelproces verkocht. Dit gebruikt dezelfde winkelwagen als de fietsverhuur, zodat er één gecombineerde order en factuur ontstaat.
  4. Netwerk van reisagenten & partners — Geregistreerde reisbureaus en accommodatiepartners loggen in op een eigen portaal, boeken namens hun gasten en krijgen partnerspecifieke tarieven en provisieregelingen.

Alle vier de processen komen samen in dezelfde winkelwagen, betaalgateway, ordernummering en factuurverwerking.


De architectuur: een eigen framework vanaf de basis

Het platform is gebouwd in PHP 8.4 zonder extern framework, dus zonder Laravel of Symfony. Elke laag, van routing tot ORM, is specifiek voor deze toepassing ontworpen en geschreven.

Verwerking van een verzoek

Elke publieke URL komt binnen via één ingang: index.php. Apache stuurt met .htaccess alle verzoeken die geen bestand opvragen hiernaartoe. Een eigen router (NSSwitchBoard) verwerkt maximaal zes URI-padniveaus, vergelijkt het eerste segment met de routingtabel en laadt de juiste paginacontroller. Onbekende routes krijgen een 400 Bad Request en worden naar de homepage doorgestuurd, zonder algemene foutpagina’s die stacktraces prijsgeven.

Elke paginacontroller is een klasse die voortbouwt op een gedeelde basis (CyclismeWebPage) en als singleton in de PHP-sessie wordt bewaard. Daardoor blijft de volledige paginatoestand tussen verzoeken behouden: winkelwagen, gekozen datums, actieve productfilters en de inlogstatus van reisagenten, zonder dat de database voor interfacetoestand hoeft te worden uitgelezen. Een klant kan tien minuten rondkijken, fietsen uit meerdere categorieën toevoegen en daarna afrekenen. De server reconstrueert bij ieder verzoek de precieze sessietoestand uit het geheugen.

De ORM-laag

Databasemodellen volgen een patroon met twee bestanden. Een _base.php-bestand wordt automatisch uit het databaseschema gegenereerd en bevat uitsluitend getypeerde eigenschappen en standaardcode voor getters en setters. Dit wordt nooit met de hand gewijzigd. Een tweede bestand voegt de bedrijfslogica toe. Daardoor kunnen schemawijzigingen opnieuw worden gegenereerd zonder eigen code te overschrijven.

De ORM-basisklasse (NSPersistentObject) houdt wijzigingen op twee manieren bij: een expliciete wijzigingsvlag die de code instelt en een MD5-controlesom van alle geserialiseerde eigenschappen, berekend bij het laden. Als de vlag is gezet of de controlesom is veranderd, slaat een store()-aanroep het object op. Dit voorkomt onbedoelde dubbele schrijfacties en maakt gedeeltelijke updates veilig.

De opslaglogica kiest tijdens uitvoering tussen INSERT en UPDATE door te controleren of de UUID van het record al in de database staat. Er zijn geen afzonderlijke methoden voor aanmaken en bijwerken; de ORM beslist. Gekoppelde modelverzamelingen, zoals artikelregels binnen een reservering, horen bij bovenliggende objecten en worden met één gecoördineerde aanroep opgeslagen.

De querybouwer

Een lichte querybouwer (DataStore) voegt positionele parameters toe aan de ORM. Query’s zien er bijvoorbeeld zo uit: query("Groep_uuid=:1 AND Verhuur>0", $groupUUID). De bouwer bepaalt typen automatisch: gehele getallen worden zonder escaping doorgegeven, tekenreeksen gaan door de escape-functie van de database, arrays worden uitgebreid tot SQL IN (...)-lijsten en eigen datum- en tijdtypen worden naar MySQL-formaat omgezet. Het resultaat is een EntitySelection: een iterator die volledige modelobjecten direct of pas bij gebruik laadt.

EntitySelection ondersteunt ook verzamelingsbewerkingen in het geheugen: doorsnede, vereniging en verschil van resultaten op basis van ID-lijsten. Zo kunnen meerdere queryresultaten worden gecombineerd zonder extra serververzoeken.


De boekingssoftware

Beschikbaarheidsmatrix

Beschikbaarheid voor verhuur is meer dan een voorraadteller. Een fiets die van dinsdag tot vrijdag is gereserveerd, is in elke overlappende periode niet beschikbaar. Een fiets die maandagochtend terugkomt, kan maandagmiddag wel beschikbaar zijn als er genoeg tijd is om deze gereed te maken. Producten::buildAvailabilityMatrix() berekent voor de hele gevraagde periode de beschikbaarheid per product en per dag, rekening houdend met alle bestaande reserveringen. Deze matrix voedt de actuele kalenderweergave op de boekingspagina.

Prijsmatrix

Prijzen hangen af van de productgroep, het aantal huurdagen en de periode van het jaar. Een apart prijsmodel (Prijzen) bewaart regels per groep, duur en datumbereik. Bij het afrekenen bepaalt de software voor ieder winkelwagenartikel de toepasselijke prijs. Reisagentenaccounts hebben een eigen prijsgroep. Dezelfde fiets kan daardoor een ander tarief hebben bij een directe boeking dan bij een boeking via een partner.

Actiecodes

Kortingscodes worden tijdens het boeken direct gecontroleerd op geldigheid, minimale orderwaarde en toepasselijke productcategorieën. De korting wordt in het winkelwagentotaal verwerkt en vastgelegd in de reservering en op de factuur.

Koppeling met de veerdienstregeling

Het bagagevervoer moet aansluiten op de dienstregeling van Wagenborg. Klanten kiezen een overtocht en het systeem controleert of het gewenste vervoerstijdstip daarbij past. Een aparte factoryklasse (FerryScheduleFactory) haalt de dienstregeling op via een externe API, voegt nieuwe gegevens toe of werkt bestaande bij en bewaart deze lokaal voor snelle raadpleging.


Betalingsverwerking

Het platform koppelt met twee betaalgateways: OGone (Ingenico) en EMS. Klanten kiezen hun betaalwijze tijdens het afrekenen. Na de terugmelding van de betaalgateway bevestigt Reservering::ValidateReservation() de betaalstatus, genereert een ordernummer, slaat de reservering op, maakt een pdf-bevestiging en verstuurt een bevestigingsmail. Dit gebeurt als één ondeelbare reeks handelingen na de betaling.

Een factory genereert ordernummers met tellers per ordertype: reserveringen, winkelverkopen, bagageorders en facturen. De tellers worden automatisch bij jaar- en maandwisselingen opnieuw gestart. Nummers krijgen voorloopnullen tot een vaste lengte, zodat alle documenten dezelfde opmaak hanteren.


Documenten genereren

Het platform maakt zelf drie soorten pdf-documenten:

  • Reserveringsbevestigingen — Een boekingsoverzicht met datums, fietsen, prijsopbouw en ordernummer.
  • Facturen — Btw-facturen met artikelregels voor alle ordertypen.
  • Bagagelabels — Afgedrukte labels met QR-code die op pakketten worden aangebracht en bij elk controlepunt tijdens het vervoer worden gescand.

Een PostOffice-klasse gebruikt PHPMailer om deze documenten als e-mailbijlagen te versturen, samen met HTML-bevestigingsberichten op basis van templates.


De API voor bagagetracering

Een aparte REST-API-ingang (ApiEntrypoint.php) bedient de mobiele scanapp van transportmedewerkers op het eiland. Na authenticatie via een X-Auth-Token-header biedt deze endpoints om pakketgegevens op te halen, afleveringsscans in te dienen en pakketlocaties bij te werken. Elke scangebeurtenis past de bagagestatus direct aan. Klanten zien die status wanneer zij hun order bekijken.


De desktopapplicatie: 4D op twintig werkplekken

Het webplatform werkt niet op zichzelf. Op ongeveer twintig macOS-werkplekken in de winkels van Kiewiet op Ameland draait daarnaast een desktopapplicatie in 4D v20R8: een volledig relationeel applicatieplatform met een eigen interface, gegevenslaag en bedrijfslogica.

De desktopapp beheert de volledige dagelijkse bedrijfsvoering: medewerkers melden klanten aan en geven fietsen mee aan de hand van verhuurbewijzen (VerhuurBonnen), verwerken reparaties zonder afspraak, beheren de onderdelenvoorraad van de werkplaats, bedienen de kassa en plannen bagageroutes.

Synchronisatie in twee richtingen

Beide systemen delen een MySQL-database, maar gebruiken niet simpelweg dezelfde verbinding. Wijzigingen lopen via een speciale synchronisatielaag boven op een RecordSync-tabel die als wijzigingslogboek fungeert.

Als de PHP-webwinkel een record aanmaakt of wijzigt, bijvoorbeeld een reservering, betaalbevestiging of klant, schrijft deze een regel in RecordSync met een volgnummer en de applicatieaanduiding "Online". Een achtergrondproces in 4D vraagt deze wijzigingen op via bridge.php, haalt de gewijzigde records op en voegt ze met ORDA (Object Relational Data Access) toe aan de 4D-datastore of werkt ze bij. Het volgnummer gaat pas verder na succesvolle verwerking, zodat geen wijziging wordt overgeslagen.

De omgekeerde route gebruikt het triggersysteem van 4D. Alle 62 databasetabellen hebben triggers die bij iedere opslag worden uitgevoerd en de verwerking aan een centrale DataBridge-coördinator doorgeven. Die zet de wijziging als RecordSync-regel in een wachtrij. Het achtergrondproces stuurt deze naar bridge.php, dat de gegevens in MySQL toevoegt of bijwerkt. Na HTTP 200 worden de synchronisatieregels verwijderd.

De triggerarchitectuur gebruikt SharedStorage voor communicatie tussen processen binnen de 4D-runtime. Ontdubbeling voorkomt dat dezelfde recordwijziging bij snel opeenvolgende opslagacties meerdere keren in de wachtrij komt.

Routeoptimalisatie

De bagageplanning in de desktopapp bevat routeoptimalisatie. Een LuggageRouting-klasse wijst pakketten aan vervoersritten toe. Een koppeling met OpenRouteService vraagt via de ORS API wegafstanden op. Een Routing-klasse gebruikt een 2-opt-verbeteralgoritme om de totale routelengte langs alle tussenliggende stops (tussenstops) te verkleinen.


De frontend

Het ontwerpsysteem bestaat uit 32 SCSS-deelbestanden, opgebouwd van ontwerpvariabelen via componentlagen naar paginaspecifieke lay-outs. CSS custom properties bevatten alle kleuren, afstanden en typografiewaarden, zodat ontwerpwijzigingen consequent doorwerken. De gecompileerde stylesheet is ongeveer 49 KB.

Interactie wordt verzorgd door 26 JavaScript-modules voor de boekingskalender, het winkelwagenpaneel, productfilters, tijdkeuze voor bagage, partnerprocessen en betalingen. De kalender koppelt FullCalendar.js aan een eigen beschikbaarheidsweergave met gegevens uit de PHP-beschikbaarheidsmatrix. Moment.js verzorgt de datumberekeningen in de browser, afgestemd op de datumcontrole op de server.

AJAX-communicatie tussen frontend en backend gebruikt een vast antwoordformaat. Elk antwoord bevat een status, eventueel een doorverwijs-URL, een reeks HTML-fragmenten met CSS-selectoren voor invoeging in de DOM via jQuery en veldgegevens voor het invullen van formulieren. Daardoor blijft de clientcode uniform: deze verwerkt één antwoordstructuur, ongeacht de aangeroepen serveractie.


De beheeromgeving

Een aparte beheerbackend (office.php) biedt de volledige beheerinterface: gegevensmodellen bekijken en bewerken, prijzen en voorraad beheren, reserveringen en betalingen bekijken en bedrijfsrapporten genereren. De beheeromgeving gebruikt meer dan 65 JSON-schemabestanden, één per tabel. Deze definiëren veldnamen, gegevenstypen, sorteergedrag en zichtbaarheid in lijsten. Ze sturen een dynamische weergave van lijsten en formulieren aan. Een nieuw databaseveld verschijnt daardoor automatisch in de beheerinterface, zonder templatewijzigingen.


In cijfers

  • 233 PHP-bronbestanden
  • Meer dan 70 gegevensmodellen voor alle bedrijfsactiviteiten
  • Meer dan 65 databasetabellen
  • 50 basisklassen in het centrale framework
  • 11 publieke paginacontrollers
  • 32 SCSS-deelbestanden, gecompileerd tot één samenhangend ontwerpsysteem
  • 26 JavaScript-functiemodules
  • 96 4D-klassebestanden en 1.099 oudere methoden in de desktopapplicatie
  • 62 databasetriggers voor de synchronisatie in twee richtingen
  • Ongeveer 20 desktopwerkplekken met de 4D-applicatie, verdeeld over meerdere locaties op Ameland
  • Twee betaalgateways, drie typen pdf-documenten en één REST-API voor tracering

Wat dit project laat zien

Kiewiet heeft geen website met een boekingswidget, maar een volledig bedrijfsplatform dat op specificatie is gebouwd voor een bedrijf met sterke seizoenspieken en weinig ruimte voor fouten. Een dubbel geboekte fiets of gemiste bagagelevering raakt direct aan iemands vakantie. Elke laag, van de routingtabel en synchronisatiesoftware tot de routeoptimalisatie, is voor die praktijk ontworpen en gebouwd.

De technische keuzes, zoals een eigen ORM met controlesommen voor wijzigingsdetectie, een netwerk van paginaobjecten dat in de sessie wordt bewaard en een tweerichtingswijzigingslogboek tussen verschillende runtimes, zijn niet gemaakt om elegant te lijken. Ze zijn gemaakt omdat het bedrijf precies dit gedrag nodig had en geen standaardoplossing dat bood.

Projectafbeeldingen

Meer werk

Gerelateerde projecten

Start een project met ons

Heeft u een project in gedachten? Bespreek met ons hoe we kunnen helpen.