Skip to content

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-only LoyaltyTransactions : on ne modifie jamais une ligne, sauf PointsRemaining (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 (null pour 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 mois pour les crédits ; null pour les débits).
    • ValidFrom : date à partir de laquelle le crédit devient utilisable (null pour 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ébits BookingPointsSpent (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).
  • TravelLoyaltyPointsWinningType (enum, sur Travel.LoyaltyPointsWinningType + valeur Travel.LoyaltyPointsWinningValue) : None (défaut — calcul standard sur le montant), FixedAmount, Multiplier. ⚠ FixedAmount et Multiplier sont prêts mais commentés dans LoyaltyService.CreditBookingPurchaseAsync (Buchard n'en veut pas pour l'instant) — seul None est actif.

Règles métier spécifiques

  • Gain (CreditBookingPurchaseAsync) : points = ceil(Booking.FinalAmount × 0,1), arrondi au supérieur (demande client). ×2 si Booking.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édit BookingPurchase complet 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, ou aujourd'hui + 6 mois si déjà expirée).
  • Effet sur le montant / la facture :
    • BookingService (calcul d'amount) pose Booking.FinalAmountWithoutLoyalityPoints puis soustrait points / 10Booking.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 flag BookingDto.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é dans BookingService.ReverseMap depuis les transactions BookingPointsSpent/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 ») — expose LoyaltyPoints, PendingLoyaltyPoints (crédits pas encore valides : ValidFrom futur), 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) et InternalUpdateAsync (: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:2954 et au rework de facture InvoiceService.cs:483 (RefundBookingPointsAsync).
  • Bonus install mobile : LoginController.cs:340 (CreditFirstMobileAppInstall).
  • Expiration : job Worker quotidien BackgroundWorker.DoLoyaltyPointsExpirationJobExpireAllPointsAsync.

Points d'attention / pièges

  • Débit/crédit à la FACTURE, pas au booking : envoyer ConsumeLoyaltyPoints = true au POST /api/bookings ne 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).
  • ConsumedLoyaltyPoints est 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 automatiquement PointsRemaining des crédits — il faut aussi ajuster manuellement un crédit pour réellement retirer des points disponibles.
  • FinalAmountWithoutLoyalityPoints ([NotMapped] sur Booking) : 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.
  • AccountCreation dé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 PointsRemaining des crédits bouge. Tout débit crée une nouvelle ligne avec LinkedTransactionId vers le crédit ponctionné.
  • Consommation et expiration toujours FIFO par ExpiresAt.

Dépendances

  • booking (flag ConsumeLoyaltyPoints, calcul du montant, annulation).
  • customer-membership (solde rattaché au Customer, bonus adhésion Club).
  • billing-payment (ligne de facture « Points fidélité », comptes NAV 3916/3917, débit/crédit déclenchés par InvoiceService).

Contributors

No contributors

Changelog

No recent changes