Skip to content

fix(promo): SDS-aligned polish for promo-code discovery flow - #148

Merged
smarcet merged 7 commits into
mainfrom
fix/promo-code-discovery-polish
Jun 5, 2026
Merged

fix(promo): SDS-aligned polish for promo-code discovery flow#148
smarcet merged 7 commits into
mainfrom
fix/promo-code-discovery-polish

Conversation

@gcutrini

@gcutrini gcutrini commented Jun 4, 2026

Copy link
Copy Markdown
Member

ref: https://app.clickup.com/t/86ba9qt03

Per-commit summary

1. c60b30e — Re-validate the discovered code when the user switches tickets

  • Before: switching to a ticket the code doesn't apply to silently removed the code. The user never saw why.
  • After: always re-validate against the new ticket. The backend rejection reaches the user as INVALID, so they can choose Remove or pick a different ticket.

2. a8590e2 — Fire early auto-apply per the SDS rule

  • Before: auto-apply on load required the ticket list to be empty (!hasTickets). If public tickets were already visible, auto-apply never fired.
  • After: per SDS, auto-apply fires whenever a single auto_apply code is discovered and none is applied. The hasTickets gate is gone.

3. a718823 — Auto-switch to the qualifying ticket on Apply

  • Before: with the suggestion banner visible, clicking Apply while a non-qualifying ticket was selected returned INVALID against that ticket. Misleading after the UI just said "you qualify".
  • After: three coordinated changes:
    • On Apply of the suggested code, if the current ticket doesn't qualify → deselect it first.
    • Wait for the code-filtered ticket-list refresh, then auto-pick the first qualifying ticket. Falls back to the only-ticket case when nothing qualifies.
    • Keep the suggestion banner visible across non-qualifying ticket picks, so the "click Apply" prompt doesn't disappear.
  • Exposes applyingCode and isCodeValidForTicket from usePromoCode so the ticket-type caller can drive both the gate and the qualifying pick.
  • Manual (non-discovered) codes are unaffected.

4. 8b9b814 — Auto-select only from tickets the user can actually buy (+ new e2e spec)

  • Before: the post-apply auto-select scanned the unfiltered Redux ticket list. A code could pick a ticket outside its sales window. The dropdown only lists in-window tickets, so the user couldn't change the selection.
  • After: scans the date-filtered list (allowedTicketTypes) — same list the dropdown uses.
  • New regression spec covers both the expired-vs-active discriminator and the all-out-of-window case.

5. 98a7ba5 — Make the "you didn't click Apply" warning yield to a real validation error (+ new e2e spec)

  • Background: when the user types a code but skips Apply, the form shows "you typed a code but didn't apply it". Today that warning happens to clear correctly only because applyPromoCode briefly sets Redux promoCode to truthy before clearing it on failure — a race window inside the action.
  • Before: the cleanup depends on that race. A reasonable refactor of applyPromoCode (e.g. "only set state on success") would silently leave the stale warning masking the actual "invalid code" message.
  • After: cleanup keys off the hook's own validationError directly. Same behavior today, robust to future refactors of the apply action.
  • New regression spec guards the precedence between the two messages.

Summary by CodeRabbit

  • New Features

    • Auto-selects the first qualifying ticket from the allowed/date-filtered set when a promo code is applied.
    • Discovered promo codes can auto-apply as soon as ticket data is loaded.
  • Bug Fixes

    • Unapplied-code warnings clear when validation errors occur; backend validation errors are shown without removing promo input.
    • When only expired tickets qualify, no ticket is auto-selected and the placeholder/no-ticket UI displays.
  • Tests

    • Added/expanded end-to-end and hook tests covering discovery, auto-apply, switching, and invalid-code flows.
  • Localization

    • New promo copy for suggesting a code that applies to a different ticket.

gcutrini added 5 commits June 4, 2026 10:04
Previously, switching to a non-qualifying ticket silently called
removePromoCode, hiding the rejection from the user. Now always
re-validates against the new ticket so the backend rejection
surfaces as INVALID and the user can decide to Remove or pick
another ticket.
The early-auto-apply effect required no tickets to be visible
(!hasTickets), preventing auto-apply when public tickets were
already in the list. Per the registration-lite SDS, auto-apply
fires whenever a single auto_apply discovered code exists and
no code is currently applied. Drops the hasTickets condition
from the gate and deps; updates the stale concurrency comment.
When the suggestion banner is visible, clicking Apply with a non-
qualifying ticket selected used to fail INVALID against that ticket
— misleading after the UI just promised "you qualify".

Three coordinated changes implement the "Apply switches you" UX:

- On Apply of the discovered code with a non-qualifying ticket,
  deselect first. The qualifying ticket isn't in the dropdown list
  yet; it shows up only after the code-filtered refetch lands inside
  applyPromoCode.

- Once that refresh settles (applyingCode clears), the post-apply
  effect picks the first ticket the code applies to. Falls back to
  the only-ticket case when nothing qualifies.

- The suggestion banner stays visible across non-qualifying ticket
  picks, so the "Apply will switch you" affordance doesn't disappear
  the moment the user picks a different ticket.

Exposes applyingCode and isCodeValidForTicket from usePromoCode so
the ticket-type caller can drive both the gate and the qualifying-
first pick.

Manual (non-discovered) codes are unaffected — they still validate
against the current ticket; backend decides.
The post-apply auto-select scanned originalTicketTypes (unfiltered Redux
list), so a discovered code could pick a ticket outside its sales window.
The dropdown is built from allowedTicketTypes, so the user couldn't see
or change the selection. Scan allowedTicketTypes instead.
Removes an implicit dependency on applyPromoCode's brief optimistic
promoCode=truthy window. Cleanup now expresses the policy directly:
hook errors take precedence over the unapplied warning.
@gcutrini
gcutrini requested a review from smarcet June 4, 2026 13:18
@coderabbitai

coderabbitai Bot commented Jun 4, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fb2ee0b3-5c4b-4ec5-8caf-418241293580

📥 Commits

Reviewing files that changed from the base of the PR and between 553f493 and bd853eb.

📒 Files selected for processing (7)
  • e2e/promo-code-discovery.spec.js
  • src/components/promocode-input/index.js
  • src/components/registration-form/index.js
  • src/components/ticket-type/index.js
  • src/hooks/__tests__/usePromoCode.test.js
  • src/hooks/usePromoCode.js
  • src/i18n/en.json
✅ Files skipped from review due to trivial changes (1)
  • src/i18n/en.json
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/components/ticket-type/index.js
  • src/components/registration-form/index.js
  • src/hooks/tests/usePromoCode.test.js

📝 Walkthrough

Walkthrough

Updated promo code hook and components so discovered codes are revalidated on ticket change, early auto-apply uses date-filtered allowedTicketTypes (selects qualifying tickets after applying), registration form clears unapplied warnings on validation errors, PromoCodeInput shows alternate suggestion copy, and unit + e2e tests were added/updated.

Changes

Promo Code Auto-Apply with Date Filtering

Layer / File(s) Summary
Hook auto-apply and revalidation behavior
src/hooks/usePromoCode.js
Stable EMPTY_TICKET_TYPES default; suggestionActive set only when selected ticket qualifies; discovered-code revalidation clears isAutoApplied (does not remove code); early auto-apply skips when allowedTicketTypes contain no qualifying tickets; returned state adds applyingCode and isCodeValidForTicket.
Ticket type auto-select from allowed list & apply handling
src/components/ticket-type/index.js
When no selection, wait for promoState.applyingCode to finish then auto-select the first qualifying ticket from allowedTicketTypes (fallback to sole allowed); applying a suggested code clears invalid selection and resets quantity before promoActions.onApply; passes isCurrentTicketQualifying into PromoCodeInput.
PromoCodeInput copy and i18n
src/components/promocode-input/index.js, src/i18n/en.json
PromoCodeInput receives isCurrentTicketQualifying and chooses between standard suggestion and suggestion_label_switch when showing SUGGESTED copy; new translation key added.
Registration form warning on validation error
src/components/registration-form/index.js
Passes allowedTicketTypes into usePromoCode; unapplied-code warning is cleared when promoState.validationError is present and effect deps updated.
E2E test coverage for filtering, discovery, and warning flows
e2e/promo-code-date-filter.spec.js, e2e/promo-code-discovery.spec.js, e2e/promo-code-invalid-clears-warning.spec.js
Playwright specs validate date-filtered auto-select (active vs expired), discovery auto-apply and backend INVALID rendering on ticket switch, and that INVALID apply clears the unapplied-code warning.
Hook unit tests updated
src/hooks/__tests__/usePromoCode.test.js
Tests updated to expect validatePromoCode is called (and removePromoCode not called) when switching tickets; early auto-apply tests added/adjusted for ticketDataLoaded and allowedTicketTypes cases.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • smarcet
  • santipalenque

Poem

🐰 I hopped through hooks and tests with glee,
Picked the active ticket the code set free.
I rechecked promos when tickets took flight,
Cleared warnings that blocked the night.
✨ — A rabbit's tidy promo fix

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely describes the main change: a polish/fix for the promo-code discovery flow with emphasis on SDS alignment.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/promo-code-discovery-polish

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.

…uto-apply contract

c60b30e changed onTicketSelected to re-validate the discovered code
on a ticket switch rather than removePromoCode'ing it; a8590e2
dropped the hasTickets gate from the early-auto-apply effect to
align with the SDS. The three tests that asserted the old contract
were still in the suite and failed. Updates them to the new contract.
Comment thread src/hooks/usePromoCode.js

@smarcet smarcet left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM

…qualify

The "You qualify for the following promo code:" banner stayed visible
across non-qualifying ticket picks so the "Apply will switch you"
affordance persists, but it then claimed the user qualified while a
non-qualifying ticket was selected. Keeps the personal phrasing on
qualifying picks and swaps to "Following promo code applies to a
different ticket. Apply to switch." when not.
@smarcet

smarcet commented Jun 5, 2026

Copy link
Copy Markdown
Collaborator

@gcutrini

Early auto-apply can blank the ticket catalog when the code-filtered list comes back empty

With the hasTickets gate removed (a8590e2), every load where a single auto_apply code is discovered fires applyPromoCodegetTicketTypes with filter=promo_code==X (src/actions.js:162-164), replacing the public ticket list with the code-filtered one.

If the code's allowed tickets are all outside their sales window — the exact config 8b9b814 fixes the auto-select for — the refetch returns an empty allowed list and:

  1. tryAutoApply(null) resolves successfully (empty-but-200 response), so the code stays applied — userRemovedAutoApply is never set and nothing self-heals (src/hooks/usePromoCode.js:208-217).
  2. Status derives to INVALID (isApplied && ticketDataLoaded && !hasTickets), so the user sees "promo code invalid" plus the "no tickets available" notice.
  3. Public tickets they could buy are invisible until they click Remove — most users won't realize that's the escape hatch and will assume the summit is sold out.

Blast radius: every authenticated visitor who qualifies for that code, on every load, for as long as the misconfiguration lasts — and it's silent (the widget thinks the apply worked, nothing reaches Sentry). Pre-PR, the !hasTickets gate meant auto-apply only fired when the public list was already empty, so there was nothing to lose.

Suggested guard: after auto-apply settles, if the refreshed (date-filtered) ticket list is empty, auto-remove the code and fall back to the public list — optionally keeping the rejection message visible so the user knows why.

Repro sketch (e2e): single discovered auto_apply code whose allowed_ticket_types all have a past sales_end_date, plus active public tickets → post-load, the dropdown should still offer the public tickets.

@smarcet smarcet left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@gcutrini please review comments

@gcutrini

gcutrini commented Jun 5, 2026

Copy link
Copy Markdown
Member Author

Good catch, mechanism checks out: applyPromoCode → getTicketTypes with filter=promo_code==X replaces the ticketTypes slice, so an empty code-filtered response does blank the public catalog. Confirmed in actions.js + reducer.js.

Want to go with a pre-check rather than the post-apply rollback you sketched. The hook receives the date-filtered ticket list from the form already; the early-auto-apply effect can bail when no ticket in that list satisfies isCodeValidForTicket. That way we never enter the broken state, no catalog flash, nothing applied, nothing to surface or roll back. The user just sees the public list.

Post-apply rollback (silent or with a notice) either flashes "no tickets" briefly or makes a state we deliberately created disappear back, which is its own UX problem. Pre-check sidesteps both. It does add a precondition the SDS does not spell out, that the code is applicable to a currently-purchasable ticket. Want to make sure you're OK with that read before I push.

If you want the operator visibility piece (silent misconfig in prod), I can add a Sentry breadcrumb when the gate skips. Separate concern from the user-facing fix.

@gcutrini

gcutrini commented Jun 5, 2026

Copy link
Copy Markdown
Member Author

Pre-check shipped in bd853eb 🎉

Gate sits in the early-auto-apply effect:

  • Skip when allowedTicketTypes.length > 0 && !allowedTicketTypes.some(isCodeValidForTicket)
  • Read: "the public list has tickets but none qualify for this code, so applying would just blank the catalog."

Carve-out: when allowedTicketTypes is empty (code-only events), we still fire. The code may be the gateway to private/code-only tickets that only surface after the code-filtered refetch, and that's the case the pre-a8590e2 !hasTickets gate was originally optimizing for.

Coverage:

  • New unit regression in src/hooks/tests/usePromoCode.test.js: single auto_apply code + public catalog with no qualifying ticket. applyPromoCode is never called.
  • Rewrote the existing "auto-applies then surfaces INVALID against the lone non-qualifying ticket" e2e to assert the new skip behavior instead (dropdown stays visible, promo input stays empty).

Still leaves the case where discovery returns a code that unlocks nothing for anyone (allowed_ticket_types empty in both directions). That is strictly a backend hygiene problem and falls through to the existing INVALID empty-list path. Happy to add a separate handler if you want it covered explicitly, but I'd argue discovery should not surface those at all.

Thanks for the catch on this one, would have been a nasty silent prod regression.

@smarcet
smarcet self-requested a review June 5, 2026 16:31
@gcutrini
gcutrini force-pushed the fix/promo-code-discovery-polish branch from bd853eb to b6835cd Compare June 5, 2026 16:54
@gcutrini

gcutrini commented Jun 5, 2026

Copy link
Copy Markdown
Member Author

You're right, I shouldn't have assumed the API contract without verifying it. Confirmed in RegularPromoCodeTicketTypesStrategy::getTicketTypes() that filter=promo_code==X augments the catalog rather than replacing it, so the "catalog blanking" couldn't happen, and the pre-check blocks the WithPromoCode gateway case exactly as you flagged (hidden tickets only revealed by the filtered call would never match the visible-tickets predicate).

Reset the branch back to b6835cd and force-pushed. The unit regression and the e2e from bd853eb went with it since they were mocking the wrong API semantics.

Thanks for the double-check.

@smarcet smarcet left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM

@smarcet
smarcet merged commit 9e67d36 into main Jun 5, 2026
5 checks passed
@gcutrini
gcutrini deleted the fix/promo-code-discovery-polish branch July 14, 2026 18:40
@gcutrini
gcutrini restored the fix/promo-code-discovery-polish branch July 14, 2026 18:41
@gcutrini
gcutrini deleted the fix/promo-code-discovery-polish branch July 14, 2026 18:43
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.

2 participants