feat(promotions): promo codes / discounts with redemption + booking apply#40
Conversation
…pply
- PromoCode aggregate (percent or fixed, optional expiry + max-redemptions) + table
(unique per company,code) + booking columns PromoCode + DiscountAmount.
- Vendor: POST/GET /api/vendor/promo-codes and /{id}/deactivate (company-scoped).
- Customer: GET /api/bookings/promo-preview?tripId&code&seats (validate + preview
the discount without redeeming).
- Booking: CreateBookingCommand takes an optional PromoCode; it's validated for the
trip's company, the discount is applied to the total, and the redemption is counted
in the same transaction (no double-count on a failed/idempotent retry).
- Shared PromoEvaluation used by both the booking apply and the preview.
Migration AddPromoCodes. 53 unit + 43 integration green (preview + redeem discounts
the total + counts redemption; unknown code 409); 0 warnings.
Note: counter (desk) booking promo application is a fast-follow; this covers online.
|
Warning Review limit reached
More reviews will be available in 40 minutes and 15 seconds. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughThis PR introduces a complete promotional code system to the TransportPlatform. It adds a PromoCode domain aggregate with discount logic, updates Booking to support discounts, implements vendor endpoints for promo management and customer-facing preview functionality, integrates promo evaluation into the booking flow, and provides complete database persistence with migrations. ChangesPromotional Code System
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (2)
src/TransportPlatform.Api/Endpoints/BookingEndpoints.cs (1)
72-76: ⚡ Quick winConsider adding input validation for query parameters.
The
codeparameter is passed directly to the handler without explicit validation. While the handler will throw an exception for null/empty codes, adding an early guard would provide clearer error messages and fail faster.🛡️ Suggested validation guard
group.MapGet("/promo-preview", async ( Guid tripId, string code, int? seats, PreviewPromoHandler handler, CancellationToken ct) => +{ + if (string.IsNullOrWhiteSpace(code)) + return Results.BadRequest(new { code = "promo.required", message = "Promo code is required." }); Results.Ok(await handler.HandleAsync(new PreviewPromoQuery(tripId, code, seats ?? 1), ct))) +}) .WithName("PreviewPromo") .WithSummary("Preview a promo code's discount on a trip before booking.");🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/TransportPlatform.Api/Endpoints/BookingEndpoints.cs` around lines 72 - 76, The endpoint mapped in BookingEndpoints (MapGet "/promo-preview", named "PreviewPromo") should validate incoming query parameters before calling PreviewPromoHandler.HandleAsync: add guards that ensure the string code is not null/empty/whitespace (return Results.BadRequest with a clear message), ensure seats (int? seats) is >= 1 when provided (or default to 1), and optionally check tripId != Guid.Empty; if any validation fails return a BadRequest result instead of invoking PreviewPromoQuery/PreviewPromoHandler.HandleAsync.src/TransportPlatform.Infrastructure/Persistence/Configurations/BookingConfiguration.cs (1)
19-20: ⚡ Quick winReference
PromoCode.MaxCodeLengthfor the snapshot column length.
Booking.PromoCodestores a snapshot of the promo code string, but the max length here is hardcoded to40, whilePromoCodeConfigurationderives itsCodelength fromPromoCode.MaxCodeLength. If that constant ever changes, these two columns silently diverge and a valid code could be truncated when persisted on a booking.♻️ Keep the snapshot length in sync with the source constant
- builder.Property(b => b.PromoCode).HasMaxLength(40); + builder.Property(b => b.PromoCode).HasMaxLength(PromoCode.MaxCodeLength);Add the namespace import if not already present:
using TransportPlatform.Domain.Bookings; +using TransportPlatform.Domain.Promotions; using TransportPlatform.Domain.Trips;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/TransportPlatform.Infrastructure/Persistence/Configurations/BookingConfiguration.cs` around lines 19 - 20, Replace the hardcoded 40 for the Booking.PromoCode snapshot length with the canonical constant used by PromoCode (use PromoCode.MaxCodeLength) so the BookingConfiguration builder.Property(b => b.PromoCode).HasMaxLength(...) stays in sync with PromoCodeConfiguration; also add the necessary namespace import for the PromoCode type if missing. Ensure you update BookingConfiguration to reference PromoCode.MaxCodeLength rather than the literal and run migrations/tests to confirm no truncation issues.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/TransportPlatform.Application/Booking/CreateBooking.cs`:
- Around line 85-94: The promo redemption can race because CreateBooking calls
PromoEvaluation.EvaluateAsync and then promo.Redeem() in-memory without any EF
concurrency protection; to fix, make redemption atomic by either (A) adding a
concurrency token/rowversion to the PromoCode entity and configuring it in the
EF model so SaveChanges will detect lost updates when Promo.Redeem() increments
RedemptionCount and then handle DbUpdateConcurrencyException in CreateBooking,
or (B) change the redemption to a single DB-side conditional update (e.g., an
Update where RedemptionCount < MaxRedemptions returning affected rows) inside
the same transaction as the booking save and throw/abort if the update affects 0
rows; apply this to the code paths around PromoEvaluation.EvaluateAsync,
promo.Redeem(), and the booking save so concurrent requests cannot exceed
MaxRedemptions.
In `@src/TransportPlatform.Application/Promotions/PromoEvaluation.cs`:
- Around line 14-22: The current check in EvaluateAsync (and subsequent
PromoCode.Redeem call in CreateBooking) can race because RedemptionCount isn't
protected by EF concurrency; add an EF optimistic concurrency token to the
PromoCode entity (e.g., a byte[] RowVersion property with [Timestamp] or fluent
IsRowVersion/IsConcurrencyToken mapping on
TransportPlatform.Domain.Promotions.PromoCode) and update the DB migration; then
modify the CreateBooking save path to catch DbUpdateConcurrencyException around
SaveChangesAsync when redeeming (or implement an atomic conditional update/SQL
update that increments RedemptionCount only if below the cap) and surface a
conflict/ retry behavior so concurrent redemptions cannot over-cap
RedemptionCount. Ensure references: PromoCode (class), RedemptionCount
(property), PromoCode.Redeem (method), CreateBooking (where SaveChangesAsync is
called), and handle DbUpdateConcurrencyException.
---
Nitpick comments:
In `@src/TransportPlatform.Api/Endpoints/BookingEndpoints.cs`:
- Around line 72-76: The endpoint mapped in BookingEndpoints (MapGet
"/promo-preview", named "PreviewPromo") should validate incoming query
parameters before calling PreviewPromoHandler.HandleAsync: add guards that
ensure the string code is not null/empty/whitespace (return Results.BadRequest
with a clear message), ensure seats (int? seats) is >= 1 when provided (or
default to 1), and optionally check tripId != Guid.Empty; if any validation
fails return a BadRequest result instead of invoking
PreviewPromoQuery/PreviewPromoHandler.HandleAsync.
In
`@src/TransportPlatform.Infrastructure/Persistence/Configurations/BookingConfiguration.cs`:
- Around line 19-20: Replace the hardcoded 40 for the Booking.PromoCode snapshot
length with the canonical constant used by PromoCode (use
PromoCode.MaxCodeLength) so the BookingConfiguration builder.Property(b =>
b.PromoCode).HasMaxLength(...) stays in sync with PromoCodeConfiguration; also
add the necessary namespace import for the PromoCode type if missing. Ensure you
update BookingConfiguration to reference PromoCode.MaxCodeLength rather than the
literal and run migrations/tests to confirm no truncation issues.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: c7bdc8f3-99e6-4bc7-8064-2af80ebc3c35
⛔ Files ignored due to path filters (2)
src/TransportPlatform.Infrastructure/Persistence/Migrations/20260603130129_AddPromoCodes.Designer.csis excluded by!**/Migrations/*.Designer.cssrc/TransportPlatform.Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.csis excluded by!**/Migrations/*ModelSnapshot.cs
📒 Files selected for processing (15)
src/TransportPlatform.Api/Endpoints/BookingEndpoints.cssrc/TransportPlatform.Api/Endpoints/VendorEndpoints.cssrc/TransportPlatform.Application/Booking/CreateBooking.cssrc/TransportPlatform.Application/Common/IApplicationDbContext.cssrc/TransportPlatform.Application/DependencyInjection.cssrc/TransportPlatform.Application/Promotions/ManagePromoCodes.cssrc/TransportPlatform.Application/Promotions/PreviewPromo.cssrc/TransportPlatform.Application/Promotions/PromoEvaluation.cssrc/TransportPlatform.Domain/Booking/Booking.cssrc/TransportPlatform.Domain/Promotions/PromoCode.cssrc/TransportPlatform.Infrastructure/Persistence/ApplicationDbContext.cssrc/TransportPlatform.Infrastructure/Persistence/Configurations/BookingConfiguration.cssrc/TransportPlatform.Infrastructure/Persistence/Configurations/PromoCodeConfiguration.cssrc/TransportPlatform.Infrastructure/Persistence/Migrations/20260603130129_AddPromoCodes.cstests/TransportPlatform.IntegrationTests/PromoCodeTests.cs
…eview) CodeRabbit flagged a redemption race: concurrent bookings could push RedemptionCount past MaxRedemptions. Replace the read-modify-write (tracked Redeem()) with an ATOMIC conditional UPDATE — increment only WHERE the code is still active/unexpired/under its cap; 0 rows affected => reject with promo.exhausted. No concurrency-token column or extra migration needed. 53 unit + 43 integration green; 0 warnings.
|
Addressed the redemption race in commit d84420c: replaced the read-modify-write |
…#45) Completes the dashboards against the analytics + sales backend (#23/#37/#40) and the admin notify/reports endpoints: - Vendor Reports: date-range financial/occupancy summary cards, per-trip table, CSV/XLSX/PDF download, and a demand-forecast widget. - Vendor Promo: list + create (percent/fixed, caps, expiry) + deactivate. - Vendor Desk: counter cash booking (trip + customer + passengers) and a company-bookings list with cancel/refund — reachable by managers AND staff (the vendor nav now shows only the Desk to staff). - Admin: per-company in-app Notify composer, plus a platform overview page (companies/trips/bookings/revenue) behind a new admin nav. Frontend only. ng build + ng lint clean.
What
PromoCodeaggregate (percent or fixed, optional expiry + max-redemptions) +promo_codetable (unique percompany,code);bookinggainsPromoCode+DiscountAmount.POST/GET /api/vendor/promo-codesand/{id}/deactivate(company-scoped).GET /api/bookings/promo-preview?tripId&code&seats— validate + preview the discount without redeeming.CreateBookingtakes an optionalpromoCode; it's validated for the trip's company, the discount applied to the total, and the redemption counted in the same transaction (no double-count on a failed/idempotent retry).PromoEvaluationused by both the booking-apply and preview paths.Tests
PromoCodeTests(Testcontainers): preview shows 20% off a 100k fare = 80k; booking with the code has the discounted total and increments the redemption count; an unknown code → 409. 53 unit + 43 integration green; migrationAddPromoCodesapplies cleanly; 0 warnings.Note
Counter (desk) booking promo application is a small fast-follow; this PR covers the online flow.
Continues the plan (… reviews ✅ → promo codes → seat maps/documents/waypoints → frontend dashboards → email-verified login).
Summary by CodeRabbit
Release Notes