Kernprobleem
Code bestaat, maar mag niet
Een kortingscode die technisch bestaat maar toch geweigerd wordt — zonder dat de gast begrijpt waarom. Verwarring voor de klant, rommeligheid voor de developer.
Case study · kortingscodes · boekingsplatform
Kernprobleem
Code bestaat, maar mag niet
Een kortingscode die technisch bestaat maar toch geweigerd wordt — zonder dat de gast begrijpt waarom. Verwarring voor de klant, rommeligheid voor de developer.
Klantervaring
Duidelijke reden
De gast ziet exact waarom een code wordt geweigerd: verlopen, buiten de actieperiode of het minimumaantal nachten niet behaald.
Resultaat
Controleerbaar & uitlegbaar
Elke afwijzing heeft een concrete reden. Zichtbaar voor de gast, traceerbaar voor de developer, uitbreidbaar voor marketing.
Probleem
Een gast vult een kortingscode in. De code bestaat. Toch wordt hij geweigerd. De gast ziet alleen: "de code werkt niet."
Misschien is hij verlopen. Misschien al te vaak gebruikt. Misschien geldig voor een andere periode, of pas bij zes nachten of meer. Al die gevallen zijn legitiem — maar ze vragen elk een andere melding.
Voorbeelden uit de praktijk
Analyse
Kortingscodes zaten verspreid over de prijsberekening én de boekingsvalidatie — twee plekken voor dezelfde logica.
Algemene regels (verlopen, max uses) en boekingsspecifieke regels (datumvensters, minimum nachten) werden door elkaar behandeld.
Een foutmelding was altijd "de code werkt niet", zonder concrete reden voor de gast of de developer.
Elke nieuwe campagne vroeg kleine aanpassingen op meerdere plekken — met risico op regressions in bestaande acties.
Kern van het probleem
Kortingscodes hebben twee soorten regels: algemene geldigheid (actief, niet verlopen, uses onder max) en boekingsspecifieke regels (datumvenster, minimum nachten). Zolang die gemengd zijn, is de logica moeilijk te testen en moeilijk uit te leggen.
Oplossing
De logica is opgesplitst in twee expliciete stappen. Eerst: mag de code überhaupt bestaan? Dan: past hij bij deze specifieke boeking? De controller kent geen kortingsregels meer.
Stap 1 — Algemene geldigheid (DiscountCode)
public bool CanBeUsed()
{
return Uses < MaxUses
&& IsActive
&& !IsExpired();
}
private bool IsExpired()
=> ExpiresAt.HasValue && ExpiresAt.Value < DateTime.UtcNow;Stap 2 — Boekingsregels (campagnelogica)
public bool PassesBookingRules(DateTime checkIn, DateTime checkOut)
{
var nights = (checkOut - checkIn).Days;
return Code switch
{
"LASTMIN30" =>
checkIn >= new DateTime(2025, 7, 1) &&
checkIn <= new DateTime(2025, 8, 31),
"ZONNEBRILACTIE2025" =>
nights >= 6 &&
checkIn >= new DateTime(2025, 6, 15) &&
checkIn <= new DateTime(2025, 9, 15),
_ => true
};
}Controller — orkestreert alleen de flow
var discountCode = await _context.DiscountCodes
.FirstOrDefaultAsync(
d => d.Code == request.DiscountCode,
cancellationToken);
if (discountCode is null)
return Errors.DiscountCode.NotFound;
if (!discountCode.CanBeUsed())
return Errors.DiscountCode.NotUsable;
if (!discountCode.PassesBookingRules(request.CheckIn, request.CheckOut))
return Errors.DiscountCode.InvalidForBooking;
// Pas korting toe als laatste stap
total -= discountCode.Amount;
discountCode.Uses++;Validatiepijplijn
CanBeUsed()
Actief · niet verlopen · uses < maxUses
PassesBookingRules(checkIn, checkOut)
Campagneperiode · minimum nachten · andere boekingseisen
total -= discountCode.Amount
Korting alleen toegepast als beide poorten passeren.
Trade-off
Pro
Logica zit dicht bij het domein en is herbruikbaar. De controller weet niets van campagnedetails — die hoeft ook niets te weten.
Con
Een switch per campagne is maatwerk. Zodra marketing elk kwartaal nieuwe uitzonderingen toevoegt, wordt dit te duur om te onderhouden. Dan is een los regelsysteem de volgende stap.
Architectuurprincipe
Zodra een code meer heeft dan een percentage en een einddatum, heb je te maken met bedrijfsregels. Die horen in het domeinobject — niet verspreid over controllers, services en database queries.
Ontwerpstandpunt
De middenweg die we kozen: basisvalidatie in het model, campagneregels expliciet naast het model. Niet alles in de database (te abstract), niet alles in de controller (te verspreid). De code is nu leesbaar op de plek waar ze wordt gebruikt.
Resultaat
Foutmeldingen zijn specifieker: de gast ziet exacte reden van de weigering.
DiscountCode bevat eigen validatielogica en is herbruikbaar buiten de boekingsflow.
De controller kent geen kortingsregels meer — die orkestreert alleen de stappen.
Nieuwe campagnes toevoegen vraagt één uitbreiding, geen aanpassing aan de bestaande flow.
Lessons learned
Kortingscodes lijken klein, maar ze worden snel businesskritisch zodra er campagnes, seizoenen en uitzonderingen bijkomen. De structuur die je kiest, betaal je terug bij elke nieuwe actie.
Onderscheid altijd "code bestaat" van "code mag nu gebruikt worden".
Houd algemene geldigheidsregels in het model — niet verspreid over controllers.
Maak campagneregels expliciet: een switch per actie is prima voor een paar campagnes.
Geef altijd een concrete reden terug waarom een code faalt — voor gast én developer.
Test elke afwijzingsreden apart: verlopen, max uses, verkeerde periode, te weinig nachten.
Zodra marketing elk kwartaal nieuwe uitzonderingen toevoegt, vervang de switch door een los regelsysteem.
Duidelijke mening
Ik zou bij elk project met kortingscodes direct beginnen met de scheiding tussen algemene geldigheid en campagneregels. Niet omdat het ingewikkeld is, maar omdat je het later nodig hebt.
Klein en snel
Een switch in PassesBookingRules() is prima voor een handvol campagnes.
Als het groeit
Vervang de switch door losse regels per campagne — één klasse, één verantwoordelijkheid.
Altijd
Geef een concrete reden terug. "Niet geldig voor deze periode" is beter dan "ongeldige code".
Takeaway
Voor een paar acties werkt een switch prima. Voor veel campagnes is een los regelsysteem de enige schaalbare keuze. Het moment waarop je overstapt? Zodra marketing sneller nieuwe uitzonderingen toevoegt dan je ze nog begrijpt.

Plan een vrijblijvende digitale kennismaking met Mitch en ontdek wat wij voor jouw organisatie kunnen betekenen.