feat(rental): Sprint M — refonds + annulations Stripe #87

Merged
tarzzan merged 1 commit from feat/rental-sprint-m into main 2026-06-03 02:18:34 +00:00
Owner

Sprint M — Refonds + annulations rental Stripe

Complète le flow rental : actuellement l'annulation se fait juste en flippant status=CANCELLED sans refund/email/release. Sprint M ajoute la vraie logique métier.

Politique de remboursement v1

  • FULL : annulation > 7 jours du début → location + caution remboursées
  • PARTIAL_50 : entre 1 et 7 jours → 50 % location + caution intégrale
  • DEPOSIT_ONLY : < 24h ou passé → caution seulement (la location reste due)

src/lib/rental-refund.ts (NEW)

computeRentalRefund({startDate, itemsTotal, depositTotal, now?}){ itemsRefund, depositRefund, totalRefund, policy, policyLabel }. Arrondi au centime, support Prisma.Decimal.

POST /api/rentals/[id]/cancel

  • Auth multi-rôle : tenant de la booking, RENTAL_PROVIDER nominal, CE_MANAGER de l'org du provider, ADMIN. Détecte cancelledBy pour adapter l'email
  • Refuse si status ∉ {PENDING, CONFIRMED} (HANDED_OVER ne peut plus s'annuler)
  • Stripe refund best-effort : si paymentStatus=SUCCEEDED + stripeSessionId existante + isStripeConfigured + totalRefund > 0 → retrieve session → payment_intent → refunds.create. Échec Stripe = audit-logged mais le flip status continue (l'asso pourra rembourser manuellement)
  • Transaction : update RentalBooking (CANCELLED + paymentStatus REFUNDED si SUCCEEDED sinon FAILED) + deleteMany RentalItemAvailability (libère le stock)
  • Audit log rental.cancel avec montants, policy, cancelledBy, stripeRefundId, stripeRefundError
  • Email best-effort : sendRentalCancelled à tenant + provider (sauf si provider est le canceller)

<CancelRentalButton />

Composant client confirm dialog inline avec textarea motif (max 500 chars). Branché sur :

  • /mes-locations — « Annuler ma location » sur résa PENDING/CONFIRMED
  • BookingDecision (utilisé par /espace-prestataire/reservations ET /espace-ce/materiel/reservations) — remplace l'ancienne mini-confirm qui flippait juste le status, désormais via la vraie API refund

Email

sendRentalCancelled(to, firstName, rbId, providerName, refundAmount, currency, policyLabel, cancelledBy) — texte adapté selon cancelledBy ("Vous avez annulé" / " a annulé" / "L'équipe Karbé a annulé"). Mention TTL Stripe 3-5j.

Tests

tests/lib/rental-refund.test.ts (8 cas) : FULL @ 10+ jours et @ 7j exact, PARTIAL_50, DEPOSIT_ONLY < 24h + passé, arrondi centime, zéro caution, policyLabel content checks.

70/70 tests ✓ (62 précédents + 8 nouveaux). Lint + typecheck + build ✓.

Test plan

  • Vitest 70/70 ✓
  • Tenant /mes-locations → bouton « Annuler ma location » sur résa CONFIRMED future → confirm dialog → API → DB CANCELLED + REFUNDED + availability supprimée + emails dry-run
  • Provider /espace-prestataire/reservations → bouton Annuler → idem
  • CE_MANAGER /espace-ce/materiel/reservations → bouton Annuler → idem
  • Admin via la même API → cancelledBy=admin
  • Cancel sur HANDED_OVER → 409
  • User non autorisé → 403
  • Sans Stripe configuré → stripeRefundId=null, paymentStatus reste FAILED si pas SUCCEEDED initialement

🤖 Generated with Claude Code

## Sprint M — Refonds + annulations rental Stripe Complète le flow rental : actuellement l'annulation se fait juste en flippant `status=CANCELLED` sans refund/email/release. Sprint M ajoute la vraie logique métier. ### Politique de remboursement v1 - **FULL** : annulation > 7 jours du début → location + caution remboursées - **PARTIAL_50** : entre 1 et 7 jours → 50 % location + caution intégrale - **DEPOSIT_ONLY** : < 24h ou passé → caution seulement (la location reste due) ### `src/lib/rental-refund.ts` (NEW) `computeRentalRefund({startDate, itemsTotal, depositTotal, now?})` → `{ itemsRefund, depositRefund, totalRefund, policy, policyLabel }`. Arrondi au centime, support Prisma.Decimal. ### `POST /api/rentals/[id]/cancel` - **Auth multi-rôle** : tenant de la booking, RENTAL_PROVIDER nominal, CE_MANAGER de l'org du provider, ADMIN. Détecte `cancelledBy` pour adapter l'email - Refuse si status ∉ {PENDING, CONFIRMED} (HANDED_OVER ne peut plus s'annuler) - **Stripe refund best-effort** : si paymentStatus=SUCCEEDED + stripeSessionId existante + isStripeConfigured + totalRefund > 0 → retrieve session → payment_intent → `refunds.create`. Échec Stripe = audit-logged mais le flip status continue (l'asso pourra rembourser manuellement) - **Transaction** : update RentalBooking (CANCELLED + paymentStatus REFUNDED si SUCCEEDED sinon FAILED) + `deleteMany` RentalItemAvailability (libère le stock) - Audit log `rental.cancel` avec montants, policy, cancelledBy, stripeRefundId, stripeRefundError - Email best-effort : `sendRentalCancelled` à tenant + provider (sauf si provider est le canceller) ### `<CancelRentalButton />` Composant client confirm dialog inline avec textarea motif (max 500 chars). Branché sur : - `/mes-locations` — « Annuler ma location » sur résa PENDING/CONFIRMED - `BookingDecision` (utilisé par `/espace-prestataire/reservations` ET `/espace-ce/materiel/reservations`) — remplace l'ancienne mini-confirm qui flippait juste le status, désormais via la vraie API refund ### Email `sendRentalCancelled(to, firstName, rbId, providerName, refundAmount, currency, policyLabel, cancelledBy)` — texte adapté selon cancelledBy ("Vous avez annulé" / "<Provider> a annulé" / "L'équipe Karbé a annulé"). Mention TTL Stripe 3-5j. ### Tests `tests/lib/rental-refund.test.ts` (8 cas) : FULL @ 10+ jours et @ 7j exact, PARTIAL_50, DEPOSIT_ONLY < 24h + passé, arrondi centime, zéro caution, policyLabel content checks. **70/70 tests ✓** (62 précédents + 8 nouveaux). Lint + typecheck + build ✓. ### Test plan - [x] Vitest 70/70 ✓ - [ ] Tenant `/mes-locations` → bouton « Annuler ma location » sur résa CONFIRMED future → confirm dialog → API → DB CANCELLED + REFUNDED + availability supprimée + emails dry-run - [ ] Provider `/espace-prestataire/reservations` → bouton Annuler → idem - [ ] CE_MANAGER `/espace-ce/materiel/reservations` → bouton Annuler → idem - [ ] Admin via la même API → cancelledBy=admin - [ ] Cancel sur HANDED_OVER → 409 - [ ] User non autorisé → 403 - [ ] Sans Stripe configuré → stripeRefundId=null, paymentStatus reste FAILED si pas SUCCEEDED initialement 🤖 Generated with [Claude Code](https://claude.com/claude-code)
tarzzan added 1 commit 2026-06-03 02:18:32 +00:00
feat(rental): Sprint M — refonds + annulations Stripe
All checks were successful
CI / test (pull_request) Successful in 2m37s
c564028ca9
Politique de remboursement v1 :
- > 7 jours du début → FULL (location + caution)
- 1 à 7 jours → PARTIAL_50 (50% location + caution intégrale)
- < 24h ou passé → DEPOSIT_ONLY (caution seulement, pas de remboursement
  sur la location)

src/lib/rental-refund.ts (NEW) : computeRentalRefund({startDate,
itemsTotal, depositTotal}) → { itemsRefund, depositRefund, totalRefund,
policy, policyLabel }. Arrondi au centime, support de Decimal.

POST /api/rentals/[id]/cancel :
- Auth multi-rôle : tenant de la booking, RENTAL_PROVIDER nominal ou
  CE_MANAGER de l'org du provider, ADMIN. Détecte `cancelledBy` pour
  adapter l'email.
- Refuse si status ∉ {PENDING, CONFIRMED} (HANDED_OVER → non
  annulable, contacter Karbé).
- Calcule le refund selon la politique.
- Stripe refund best-effort si paymentStatus=SUCCEEDED + stripeSessionId
  existante + isStripeConfigured + totalRefund > 0. Retrieve session →
  payment_intent → refunds.create. Échec Stripe = audit-logged mais
  le flip status continue (l'asso pourra rembourser manuellement).
- Transaction : update RentalBooking (CANCELLED + paymentStatus
  REFUNDED si SUCCEEDED sinon FAILED) + delete RentalItemAvailability
  (libère stock).
- Audit log rental.cancel avec montants, policy, cancelledBy,
  stripeRefundId, stripeRefundError.
- Email best-effort : sendRentalCancelled à tenant + provider (sauf si
  provider est le canceller).

src/components/CancelRentalButton.tsx : composant client confirm dialog
inline avec textarea motif (max 500 chars). Branché sur :
- /mes-locations : « Annuler ma location » sur résa PENDING/CONFIRMED
- BookingDecision (utilisé par /espace-prestataire/reservations ET
  /espace-ce/materiel/reservations) : remplace l'ancienne mini-confirm
  qui flippait juste le status, désormais via la vraie API refund

sendRentalCancelled email : adapté selon cancelledBy ("Vous avez annulé"
/ "<Provider> a annulé" / "L'équipe Karbé a annulé").

tests/lib/rental-refund.test.ts : 8 cas (FULL @ 10+ et 7j, PARTIAL_50,
DEPOSIT_ONLY < 24h et passé, arrondi centime, zéro caution, policyLabel).
Total projet : 70/70 ✓.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
tarzzan merged commit 73d24b70f7 into main 2026-06-03 02:18:34 +00:00
tarzzan deleted branch feat/rental-sprint-m 2026-06-03 02:18:34 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: tarzzan/karbe#87
No description provided.