Loyaliteit (X-Coins)¶
De webshop kent een loyaliteitsprogramma waarbij professionele klanten punten ("X-Coins") sparen op hun bestellingen. Het programma steunt op twee lagen:
- Interne registratie (
src/Atraxion/Loyalty/) — een lokale database met een toestandsmachine die elke verdiende portie punten bewaakt (pending→confirmed→paidofforfeited). - Externe provider Arteel (
src/Arteel/) — de REST-API-client van Arteel, het externe beloningsportaal waar de klant zijn punten uiteindelijk inwisselt.
Het programma is niet gebonden aan een verkoopkanaal: het is een eigenschap van de klant en zijn bestellingen, niet van een channel. In de mini-shop-flow worden geen punten toegekend (de mini-shop-modus blokkeert dat). De toegankelijkheid is per land (BE/NL/LU) en per klant (via tags en regels) bepaald — zie Scoping hieronder.
Toegankelijkheid (eligibility)¶
Context::showXCoins() bepaalt of X-Coins voor de huidige bezoeker actief zijn. Dat is
alleen het geval als aan alle voorwaarden is voldaan:
- de gebruiker is ingelogd en niet in mini-shop-modus;
- het is niet de inwisselperiode (zie hieronder);
Login::hasOptedInToLoyaltyProgram()is waar;- de gebruiker is niet uitgesloten (tag
Geen X-Coinsof een match op een uitsluitingsregel); - de guard-permissie
x-coinsis voldaan.
showXCoinsMenu() gebruikt daarnaast isEligibleForLoyaltyProgram() (land BE/NL/LU +
tag Wel X-Coins), zodat in aanmerking komende klanten die nog niet zijn opgemeld, het
menu-item wel zien en zich kunnen aanmelden.
Inwisselperiode¶
LoyaltyProgram (src/Atraxion/Loyalty/LoyaltyProgram.php) kent een jaarlijkse
inwisselperiode. Tijdens die periode geeft isRedemptionPeriod() waar terug
(LoyaltyProgram.php:37) en stopt het sparen: klanten besteden dan hun saldo in plaats
van bij te verdienen.
Aanmelden (opt-in)¶
De opt-in loopt via LoyaltyProgramController (_core/config/pages.xml:78) op de
pagina loyalty-program, met als URL's /getrouwheidsprogramma (NL),
/programme-de-fidelite (FR) en /loyalty-program (EN). De string x-coins in de
codebase is een Guard-permissiesleutel, geen route:
- een uitgesloten of afgemelde gebruiker wordt teruggestuurd naar
/; ?forceOptOut=truemeldt de gebruiker direct af;- een POST met
loyaltyPointsOptIn=1valideert de gegevens en roeptoptIn()aan; - bij opt-in wordt een PUT naar Arteel gestuurd om het gebruikersprofiel aan te
maken/bij te werken, en wordt de e-mailtemplate
business-owner-x-coinsverstuurd (zie Notificaties & e-mails).
Op de klant (customer_user) leggen de velden loyaltyOptDate en loyaltyOptStatus
(opt-in/opt-out) de aanmeldstatus vast; loyaltyPoints cachet het Arteel-saldo.
Punten verdienen¶
Punten worden bepaald door regels in customer_loyalty_points_rules, gemodelleerd als
LoyaltyPointsRule (src/Atraxion/Loyalty/Entity/LoyaltyPointsRule.php). Een regel
heeft een rule_type (cart, article of brand), een amount_type (percentage
of fixed), een geldigheidsperiode, een minimaal winkelwagenbedrag en koppelingen naar
artikelen, merken, banden-/velgtypes en voertuigcategorieën. De
percentageformule is round((percentage/100) × prijs × 200) — elke euro kortingswaarde
levert 200 punten op.
De berekening gebeurt in LoyaltyPointsCalculator
(src/Atraxion/Loyalty/Service/LoyaltyPointsCalculator.php):
calculateByCartArticles()telt de artikel- en merkregels per orderregel op;calculateByCartTotal()past de eerste passende winkelwagenregel toe op het in aanmerking komende totaal;- daarnaast kan een coupon van het type
loyaltyextra punten toekennen.
In de checkout (CartController rond regel 1616) worden bij een opgemelde gebruiker de
punten berekend, en na het opslaan van de order via attributeLoyaltyPoints() per bron
een LoyaltyPoints-record aangemaakt in status pending.
Bij een gesplitste bestelling (OrderSplitter) krijgt alleen de eerste order de
winkelwagentotaal- en couponpunten; alle deelorders krijgen wel hun artikelpunten.
attributeLoyaltyPoints() keert direct terug zodra showXCoins() onwaar is — dit
voorkomt dat punten worden toegekend aan niet-opgemelde gebruikers.
Punten bevestigen en uitbetalen¶
De toestandsmachine van LoyaltyPoints
(src/Atraxion/Loyalty/Entity/LoyaltyPoints.php) loopt
pending → confirmed → paid (of forfeited vanuit elke niet-eindstatus). Het netto
saldo is max(points − penaltyPoints, 0).
- Bevestigen (
confirmByOrder()) brengt punten vanpendingnaarconfirmed. Dit gebeurt na een geslaagde betaling (doFinishOrder()/CheckPaymentStatus::checkPaymentStatus(), zie Betalingen) en bij het binnenkomen van de eerste Odoo-factuur viaSynchroniseOrders. - Uitbetalen (
payout(),src/Atraxion/Loyalty/Service/LoyaltyPointsService.php:100) brengtconfirmed-punten naarpaiden duwt ze viaArteelCustomerLoyaltyService::addPoints()als delta naar Arteel.
De opt-in-controle bij uitbetaling¶
payout() controleert expliciet of de gebruiker is opgemeld
(LoyaltyPointsService.php:110). Is dat niet zo, dan worden de punten niet
uitbetaald maar verbeurd (forfeit()): een niet-opgemelde gebruiker heeft geen
Arteel-profiel om punten te ontvangen, dus een push zou eindeloos op een 404 blijven
hangen. Dit is de correctie voor de eerder opgetreden situatie waarbij een
gesplitste checkout punten toekende aan niet-opgemelde gebruikers.
Arteel-API¶
De Arteel-client (src/Arteel/Service/ArteelClient.php) gebruikt OAuth2
(client_credentials) met een gecachet token. Belangrijke endpoints:
| Doel | Aanroep |
|---|---|
| Gebruiker upserten | PUT /api/users |
| Punten toevoegen (delta) | POST /api/users/{id}/points (projectId=368, occasionId=382) |
| Saldo opvragen | GET /api/users/{id}/points |
| Alle saldo's | GET /api/users/points |
| Portaal-SSO | JWT-getekende URL (getSingleSignOnUrl()) |
De customerInternalId waaronder een klant bij Arteel bekend staat is doorgaans de
OdooId, tenzij overschreven via het JSON-veld arteelUserOverride op customer_user.
Arteel-gebruikers worden ingedeeld in landengroepen (BE→409, NL→422, LU→423; in
non-prod 123). De ArteelCustomerLoyaltyService werkt het profiel ook bij vanuit de
Odoo-synchronisatie (SynchronisePartners).
CLI-commando's¶
| Commando | Beschrijving |
|---|---|
atx:loyalty:payout-confirmed-points |
Duwt alle confirmed-punten naar Arteel (productie-cron) |
atx:loyalty:void-orphaned-points [--force] |
Verbeurt (dry-run of geforceerd) pending/confirmed-punten van niet-opgemelde gebruikers |
atx:loyalty:add-missing-points [--dry-run] [--force] |
Verzoent het lokale paid-totaal met het Arteel-saldo en vult het verschil aan |
atx:loyalty:debug-user <email> |
Inspecteert/herstelt de punten van één gebruiker |
arteel:bulk-create-users <ids...> |
Pusht specifieke Login-ID's naar Arteel |
arteel:find-user <needle> |
Zoekt een gebruiker in de bulk-punten-export |
arteel:reset-users |
Zet alle Arteel-punten op 0 |
Scoping¶
- Per land: alleen BE, NL en LU komen in aanmerking (
isEligibleForLoyaltyProgram()). - Per klant: regels koppelen many-to-many naar klanten, klant-tags en
klantcategorieën; uitsluiting via de tag
Geen X-Coinsof een uitsluitingsregel (LoyaltyPointsExclusionRule). - Niet per channel: loyaliteit is niet gebonden aan een verkoopkanaal, maar de mini-shop-modus verhindert puntentoekenning in de mini-shop-flow.
Zie ook¶
- Betalingen — waar punten worden bevestigd na betaling.
- Kortingen & coupons — couponbeloning van het type
loyalty. - Crons & queue — de uitbetalingscron.
- Integraties — de Arteel-koppeling technisch.