Module — Points de fidélité (Loyalty)
Synchronisé avec le code. Mettre à jour à chaque changement structurel.
Rôle
Programme de fidélité client : le client gagne des points (achat de voyage, bonus uniques) et les consomme sur une réservation pour réduire le montant à payer. Les points ne sont pas un moyen de paiement : ils se matérialisent en réduction (ligne de facture négative). Le solde est consultable côté site web / app mobile via l'API. Les points expirent après 24 mois (job Worker quotidien).
Barème : 1 CHF dépensé = 0,1 pt gagné ; 10 pts = 1 CHF de réduction.
Emplacement
- Service :
src/Application/Services/Entities/LoyaltyService.cs+Interfaces/ILoyaltyService.cs - Entité / enums :
src/Domain/Entities/LoyaltyTransaction.cs,Domain/Entities/LoyaltyTransactionType.cs,Domain/Entities/LoyaltyPrettyAttribute.cs,Domain/Extensions/LoyaltyTransactionTypeExtensions.cs,Domain/Enum/TravelLoyaltyPointsWinningType.cs - DTOs :
Application/Dtos/LoyaltyPointsResult.cs,Application/Dtos/Api/LoyaltyTransactionDto.cs,Application/Dtos/Api/LoyaltyTransactionPrettyDto.cs - API :
src/Web/Areas/Api/LoyaltyController.cs(calculateur public),LoyaltyDevController.cs(dev/staging),BookingController.cs(flag de consommation),CustomerController.cs(solde + historique),LoginController.cs(bonus install app) - Consommateurs :
InvoiceService.cs(débit/crédit à la facture),BookingService.cs(calcul du montant + remboursement à l'annulation),Worker/BackgroundWorker.cs(expiration quotidienne)
Entités principales
LoyaltyTransaction— ligne du grand livre des points. Table append-onlyLoyaltyTransactions: on ne modifie jamais une ligne, saufPointsRemaining(décrémenté à la consommation). Champs clés :Points: signé —> 0= crédit (gain),< 0= débit (utilisation / expiration / annulation).PointsRemaining: points encore disponibles sur ce crédit (nullpour les débits) ; décrémenté en FIFO à chaque consommation partielle.TransactionType(cf. enum ci-dessous).BookingId: résa source (pour les transactions liées à un achat).LinkedTransactionId: pour un débit/remboursement, lien vers le crédit source ponctionné — permet de retracer exactement quels crédits ont servi.ExpiresAt: date d'expiration (=CreatedAt + 24 moispour les crédits ;nullpour les débits).ValidFrom: date à partir de laquelle le crédit devient utilisable (nullpour les débits). Pour un achat de voyage (BookingPurchase),ValidFrom= date de fin du voyage (les points ne sont acquis qu'au retour, car la résa peut encore être annulée).IsRefunded: garde anti-double-remboursement sur les débitsBookingPointsSpent(allers-retours annulation / dé-annulation).Note,CreatedAt.
LoyaltyTransactionType(enum) :- Crédits :
AccountCreation(⚠ désactivé — code commenté, mail Melissa 10.04.2026),BookingPurchase(achat de voyage),MobileAppDownload(bonus 1ʳᵉ install),ClubMembershipJoin(bonus adhésion Club),BookingRefund(re-crédit des points utilisés suite à annulation),ManualCreditAdjustment(ajustement admin). - Débits :
BookingPointsSpent(utilisation sur une résa),Expiration(auto, job quotidien),BookingCancellation(retrait des points gagnés sur une résa annulée),ManualDebitAdjustment(ajustement admin).
- Crédits :
TravelLoyaltyPointsWinningType(enum, surTravel.LoyaltyPointsWinningType+ valeurTravel.LoyaltyPointsWinningValue) :None(défaut — calcul standard sur le montant),FixedAmount,Multiplier. ⚠FixedAmountetMultipliersont prêts mais commentés dansLoyaltyService.CreditBookingPurchaseAsync(Buchard n'en veut pas pour l'instant) — seulNoneest actif.
Règles métier spécifiques
- Gain (
CreditBookingPurchaseAsync) :points = ceil(Booking.FinalAmount × 0,1), arrondi au supérieur (demande client). ×2 siBooking.Origin == Mobile. Crédit valable à partir de la fin du voyage (ValidFrom), expire 24 mois après création. Idempotent : ne re-crédite pas si un créditBookingPurchasecomplet existe déjà pour la résa (évite le double-crédit acompte + solde). - Consommation (
DebitBookingPointsAsync) : tout-ou-rien — le client ne choisit pas combien de points. On consomme tous les points disponibles, plafonné au nombre nécessaire pour couvrir la résa :pointsToSpend = min(disponibles, ceil(montant × 10)). Consommation FIFO par date d'expiration la plus proche. - Bonus uniques :
MobileAppDownload= 250 pts (1ʳᵉ install,LoginController),ClubMembershipJoin= 100 pts (adhésion / renouvellement Club,InvoiceService). Chacun vérifie l'unicité avant de créditer. - Validité différée + expiration : crédits valides 24 mois ; un crédit d'achat n'est utilisable qu'à partir du retour du voyage. Expiration auto via le job Worker quotidien.
- Remboursement à l'annulation (
RefundBookingPointsAsync) : (1) retire les points gagnés pour ce voyage (BookingCancellation), (2) re-crédite les points dépensés (BookingRefund) avec réajustement de la date d'expiration (origine, ouaujourd'hui + 6 moissi déjà expirée). - Effet sur le montant / la facture :
BookingService(calcul d'amount) poseBooking.FinalAmountWithoutLoyalityPointspuis soustraitpoints / 10→Booking.FinalAmount(clampé à 0).- Ligne de facture négative « Points fidélité (N pts) » sur le compte NAV 3916 (TVA suisse) / 3917 (hors-Suisse), répartie acompte/solde via
Travel.DepositPercentage.
Surface API
POST /api/bookings— création de résa. Le flagBookingDto.General.ConsumeLoyaltyPoints(bool) déclare « j'utilise mes points ». C'est l'unique levier (tout-ou-rien : pas de champ pour un nombre de points).BookingDto.General.ConsumedLoyaltyPoints(int) est en sortie seulement (recalculé dansBookingService.ReverseMapdepuis les transactionsBookingPointsSpent/BookingRefund).GET /api/loyalty/winning-points?amount=— calculateur public (AllowAnonymous) : renvoie les points gagnables ({ WinningPoints, MobileWinningPoints }) pour un montant CHF.GET /api/loyalty/dev/reset?email=&/api/loyalty/dev/credit?email=&points=—LoyaltyDevController, dev/staging uniquement.GET /api/customers(« me ») — exposeLoyaltyPoints,PendingLoyaltyPoints(crédits pas encore valides :ValidFromfutur),LoyaltyTransactions,LoyaltyTransactionsPretty.
Cycle de vie (où ça se déclenche)
- Débit + crédit d'achat → à la création de facture, PAS au POST booking :
InvoiceService.CreateAsync(:407-415) etInternalUpdateAsync(:530-538). Raison : la résa peut générer 2 factures (acompte confirmé + solde facturé) ; on évite de créditer deux fois et on le fait au plus tôt dans le process. - Bonus adhésion Club :
InvoiceService.cs:422(CreditClubMembership). - Remboursement : à l'annulation de résa
BookingService.cs:2954et au rework de factureInvoiceService.cs:483(RefundBookingPointsAsync). - Bonus install mobile :
LoginController.cs:340(CreditFirstMobileAppInstall). - Expiration : job Worker quotidien
BackgroundWorker.DoLoyaltyPointsExpirationJob→ExpireAllPointsAsync.
Points d'attention / pièges
- Débit/crédit à la FACTURE, pas au booking : envoyer
ConsumeLoyaltyPoints = trueau POST/api/bookingsne ponctionne rien tant que la facture n'est pas générée. Ne pas chercher de mouvement de points juste après la création de la résa. - Tout-ou-rien : il n'existe aucun champ « utiliser N points » dans l'API. Le backend décide (tous les dispo, plafonnés au total).
ConsumedLoyaltyPointsest en lecture seule : valeur d'entrée ignorée, recalculée côté serveur (BookingService.cs:1274).ManualDebitAdjustment: visible dans l'historique mais ne décrémente PAS automatiquementPointsRemainingdes crédits — il faut aussi ajuster manuellement un crédit pour réellement retirer des points disponibles.FinalAmountWithoutLoyalityPoints([NotMapped]surBooking) : faute de frappe figée dans le code (« Loyality »). Sert de base au calcul de la réduction et au débit des points.- Comptes NAV 3916 / 3917 : ne pas confondre — 3916 = TVA suisse, 3917 = hors-Suisse.
AccountCreationdésactivé : le bonus de 100 pts à la création de compte est commenté (toujours dans le code, réactivable).
Conventions locales
- Grand livre append-only ; seul
PointsRemainingdes crédits bouge. Tout débit crée une nouvelle ligne avecLinkedTransactionIdvers le crédit ponctionné. - Consommation et expiration toujours FIFO par
ExpiresAt.
Dépendances
- ←
booking(flagConsumeLoyaltyPoints, calcul du montant, annulation). - ←
customer-membership(solde rattaché auCustomer, bonus adhésion Club). - →
billing-payment(ligne de facture « Points fidélité », comptes NAV 3916/3917, débit/crédit déclenchés parInvoiceService).

