Case study · kortingscodes · boekingsplatform

Kortingscodes die kloppen én uitlegbaar zijn

Een code die bestaat maar niet mag worden gebruikt is niet hetzelfde als een ongeldige code. Die twee gevallen vragen een andere behandeling — voor de gast, en in de code. Dit is hoe we die scheiding aanbrachten.

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

De code werkt niet — maar waarom niet?

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

  • LASTMIN30 — geldig maar alleen in een specifieke zomerperiode.
  • ZONNEBRILACTIE2025 — vereist minimaal 6 nachten én een bepaalde verblijfsperiode.
  • Een gast die een geldige code invult buiten die conditie krijgt geen uitleg — alleen een generieke weigering.

Analyse

Twee soorten regels — en ze werden door elkaar behandeld

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

Twee methoden, elk met één vraag

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

Twee poorten, elk met één vraag

1

CanBeUsed()

Actief · niet verlopen · uses < maxUses

IsActive?
IsExpired?
Uses < Max?
2

PassesBookingRules(checkIn, checkOut)

Campagneperiode · minimum nachten · andere boekingseisen

Geldig datumvenster?
Min. nachten bereikt?

total -= discountCode.Amount

Korting alleen toegepast als beide poorten passeren.

Trade-off

Een switch is prima — tot het niet meer is

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

Een kortingscode is geen veldje — het is domeinlogica

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

Controle over elke stap in de kortingsvalidatie

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

Wat ik meeneem bij elk systeem met kortingscodes

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

Stop niet alle kortingsregels in de controller

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

Kortingscodes lijken klein, maar ze worden snel businesskritisch. Houd de basisvalidatie in de kortingscode zelf, en maak campagneregels expliciet en leesbaar.

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.

Veelgestelde vragen over kortingscodevalidatie

Waarom mag de controller de kortingsregels niet kennen?
Omdat een controller verantwoordelijk is voor de flow, niet voor de domeinlogica. Zodra een controller beslist of een LASTMIN30-code geldig is voor een zomerboeking, is die kennis op de verkeerde plek. Als de regel verandert, moet je de controller aanpassen — terwijl die niks van zomers of kortingen zou moeten weten.
Wanneer kies je voor een los regelsysteem in plaats van een switch?
Zodra het aantal campagnes groeit en campagnes hun eigen uitzonderingen krijgen. Een switch is leesbaar voor drie acties. Bij tien acties elk met meerdere condities wordt het onhoudbaar. Dan loon het om campagneregels als losse objecten of configuratie te modelleren — één klasse per actie, elk met een eigen IsValid(booking)-methode.
Hoe geef je een duidelijke reden zonder beveiligingsrisico?
Je geeft de reden in algemene termen: 'code is verlopen', 'maximum gebruik bereikt', 'niet geldig voor deze periode'. Wat je niet doet: de exacte grens blootgeven ('verlopen op 31 oktober') of interne identifiers tonen. Zo is de boodschap informatief voor de gast maar nutteloos voor iemand die probeert de validatie te omzeilen.
Kan je deze validatielogica ook voor andere soorten acties gebruiken?
Ja. Het patroon — algemene geldigheidscheck gevolgd door contextspecifieke regels — werkt overal waar een actie afhankelijk is van meerdere condities: loyaliteitspunten, vroegboekvoordelen, bundelacties. Het domeinobject verandert, de structuur niet.
Wat als een kortingscode meerdere beperkingen tegelijk heeft?
Dan moeten alle beperkingen expliciet zijn en allemaal in PassesBookingRules() staan. Geen verborgen onderlinge afhankelijkheden. Elke beperking kan dan apart worden getest, en een foutmelding kan de specifieke reden bevatten: "niet geldig voor minder dan 6 nachten" in plaats van een generieke weigering.
Mitch

Heb jij ook kortingscodes die werken maar waarbij niemand meer zeker is van de regels?

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