Skip to content

feat(promotions): promo codes / discounts with redemption + booking apply#40

Merged
kashkoool merged 2 commits into
mainfrom
feat/promo-codes
Jun 3, 2026
Merged

feat(promotions): promo codes / discounts with redemption + booking apply#40
kashkoool merged 2 commits into
mainfrom
feat/promo-codes

Conversation

@kashkoool

@kashkoool kashkoool commented Jun 3, 2026

Copy link
Copy Markdown
Owner

What

  • PromoCode aggregate (percent or fixed, optional expiry + max-redemptions) + promo_code table (unique per company,code); booking gains 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: CreateBooking takes an optional promoCode; 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).
  • Shared PromoEvaluation used 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; migration AddPromoCodes applies 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

  • New Features
    • Customers can now apply promo codes at checkout to receive discounts on bookings.
    • Vendors can create, manage, and deactivate promotional codes with customizable discount options.
    • Customers can preview estimated discounts before completing their booking.

…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.
@coderabbitai

coderabbitai Bot commented Jun 3, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@kashkoool, we couldn't start this review because you've reached your PR review rate limit.

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 @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0e88c10f-6d79-49d4-ac73-63d7b965a4b2

📥 Commits

Reviewing files that changed from the base of the PR and between 00c83d0 and d84420c.

📒 Files selected for processing (2)
  • src/TransportPlatform.Application/Booking/CreateBooking.cs
  • src/TransportPlatform.Infrastructure/Persistence/Configurations/PromoCodeConfiguration.cs
📝 Walkthrough

Walkthrough

This 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.

Changes

Promotional Code System

Layer / File(s) Summary
PromoCode domain aggregate
src/TransportPlatform.Domain/Promotions/PromoCode.cs
PromoCode aggregate root with DiscountType enum (Percent/Fixed), constructor validation, and business methods: CanRedeem() checks time and redemption limits, ComputeDiscount() calculates discount capped at base amount, Redeem() increments counter, Deactivate() marks inactive.
Booking domain with discount support
src/TransportPlatform.Domain/Booking/Booking.cs
Booking properties PromoCode and DiscountAmount added; Create() factory accepts optional discount and promo code, clamping discount and computing net total after discount.
Shared promo evaluation utility
src/TransportPlatform.Application/Promotions/PromoEvaluation.cs
Internal PromoEvaluation.EvaluateAsync() validates promo codes against company, checks redemption eligibility via domain CanRedeem(), and returns promo object with computed discount; used by both booking and preview flows.
Vendor promo code management
src/TransportPlatform.Application/Promotions/ManagePromoCodes.cs
CQRS handlers for creating promo codes with validation and duplicate conflict detection, listing paginated codes filtered by vendor's company, and deactivating codes; includes PromoCodeDto for API serialization.
Customer promo preview
src/TransportPlatform.Application/Promotions/PreviewPromo.cs
PreviewPromoHandler query handler allows customers to preview discount before booking by clamping seat count, calculating base total, and evaluating promo via shared PromoEvaluation.
Booking flow with promo integration
src/TransportPlatform.Application/Booking/CreateBooking.cs, src/TransportPlatform.Api/Endpoints/BookingEndpoints.cs
CreateBookingCommand and request now accept optional PromoCode; handler evaluates code via PromoEvaluation, redeems it, and passes discount and code to Booking.Create.
Persistence layer and configurations
src/TransportPlatform.Application/Common/IApplicationDbContext.cs, src/TransportPlatform.Infrastructure/Persistence/ApplicationDbContext.cs, src/TransportPlatform.Infrastructure/Persistence/Configurations/BookingConfiguration.cs, src/TransportPlatform.Infrastructure/Persistence/Configurations/PromoCodeConfiguration.cs
DbContext updated to expose PromoCode DbSet; BookingConfiguration adds DiscountAmount numeric(12,2) and PromoCode varchar(40) columns; PromoCodeConfiguration maps promo_code table with unique (CompanyId, Code) index and company cascade delete.
Database migration
src/TransportPlatform.Infrastructure/Persistence/Migrations/20260603130129_AddPromoCodes.cs
Migration adds DiscountAmount and PromoCode columns to booking table, creates promo_code table with identity, company FK, code, discount config, redemption tracking, timestamps, and unique index; Down reverses both changes.
API endpoints
src/TransportPlatform.Api/Endpoints/BookingEndpoints.cs, src/TransportPlatform.Api/Endpoints/VendorEndpoints.cs
Booking endpoint: CreateBookingRequest includes optional PromoCode passed to command; new /promo-preview endpoint. Vendor endpoints: POST /promo-codes creates code, GET /promo-codes lists with pagination, POST /promo-codes/{id}/deactivate deactivates.
Dependency injection
src/TransportPlatform.Application/DependencyInjection.cs
Registers four scoped handlers: CreatePromoCodeHandler, ListPromoCodesHandler, DeactivatePromoCodeHandler, PreviewPromoHandler.
Integration tests
tests/TransportPlatform.IntegrationTests/PromoCodeTests.cs
End-to-end tests for creating vendor promo code, previewing 20% discount, booking with promo code, verifying discounted total, and confirming redemption count increments.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • kashkoool/Transport#22: Domain event payload updates for BookingConfirmed/BookingCancelled may be consumed by notification dispatching related to promo-applied bookings.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 22.22% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main feature addition: promo code management with discount redemption integrated into the booking flow, which aligns with all the major changes across domain, application, and API layers.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/promo-codes

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
src/TransportPlatform.Api/Endpoints/BookingEndpoints.cs (1)

72-76: ⚡ Quick win

Consider adding input validation for query parameters.

The code parameter 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 win

Reference PromoCode.MaxCodeLength for the snapshot column length.

Booking.PromoCode stores a snapshot of the promo code string, but the max length here is hardcoded to 40, while PromoCodeConfiguration derives its Code length from PromoCode.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

📥 Commits

Reviewing files that changed from the base of the PR and between 03dc3a1 and 00c83d0.

⛔ Files ignored due to path filters (2)
  • src/TransportPlatform.Infrastructure/Persistence/Migrations/20260603130129_AddPromoCodes.Designer.cs is excluded by !**/Migrations/*.Designer.cs
  • src/TransportPlatform.Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs is excluded by !**/Migrations/*ModelSnapshot.cs
📒 Files selected for processing (15)
  • src/TransportPlatform.Api/Endpoints/BookingEndpoints.cs
  • src/TransportPlatform.Api/Endpoints/VendorEndpoints.cs
  • src/TransportPlatform.Application/Booking/CreateBooking.cs
  • src/TransportPlatform.Application/Common/IApplicationDbContext.cs
  • src/TransportPlatform.Application/DependencyInjection.cs
  • src/TransportPlatform.Application/Promotions/ManagePromoCodes.cs
  • src/TransportPlatform.Application/Promotions/PreviewPromo.cs
  • src/TransportPlatform.Application/Promotions/PromoEvaluation.cs
  • src/TransportPlatform.Domain/Booking/Booking.cs
  • src/TransportPlatform.Domain/Promotions/PromoCode.cs
  • src/TransportPlatform.Infrastructure/Persistence/ApplicationDbContext.cs
  • src/TransportPlatform.Infrastructure/Persistence/Configurations/BookingConfiguration.cs
  • src/TransportPlatform.Infrastructure/Persistence/Configurations/PromoCodeConfiguration.cs
  • src/TransportPlatform.Infrastructure/Persistence/Migrations/20260603130129_AddPromoCodes.cs
  • tests/TransportPlatform.IntegrationTests/PromoCodeTests.cs

Comment thread src/TransportPlatform.Application/Booking/CreateBooking.cs Outdated
Comment thread src/TransportPlatform.Application/Promotions/PromoEvaluation.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.
@kashkoool

Copy link
Copy Markdown
Owner Author

Addressed the redemption race in commit d84420c: replaced the read-modify-write Redeem() with an atomic conditional UPDATE (ExecuteUpdateAsync) that increments RedemptionCount only WHERE the code is still active/unexpired/under its cap — 0 rows affected ⇒ promo.exhausted. So concurrent bookings can never exceed MaxRedemptions, with no concurrency-token column or extra migration.

@kashkoool kashkoool merged commit e2c073f into main Jun 3, 2026
9 checks passed
@kashkoool kashkoool deleted the feat/promo-codes branch June 3, 2026 13:28
kashkoool added a commit that referenced this pull request Jun 3, 2026
…#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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant