Skip to content

feat: prevent ticket reassignment based on promo code#139

Open
gcutrini wants to merge 1 commit intomainfrom
feature/prevent-ticket-reassignment
Open

feat: prevent ticket reassignment based on promo code#139
gcutrini wants to merge 1 commit intomainfrom
feature/prevent-ticket-reassignment

Conversation

@gcutrini
Copy link
Member

@gcutrini gcutrini commented Feb 26, 2026

  • Use the /apply endpoint's allows_to_reassign response to control ticket reassignment UI
  • Add VALIDATE_PROMO_CODE_ERROR action to reset reassignment flag when validation fails (e.g. switching to an unsupported ticket type)

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

Summary by CodeRabbit

Release Notes

  • New Features

    • Promo codes can now restrict ticket reassignment with clear non-transferable warnings displayed
    • Promo code validation automatically triggers when selecting different tickets
  • Improvements

    • Enhanced promo code validation flow with improved error handling
    • Better loading state management during promo code operations
    • Improved UI layout and spacing for promo code input controls

Use the /apply endpoint's allows_to_reassign response to control
whether tickets can be reassigned. When a promo code disallows
reassignment, the widget hides the assignee radio options and shows
a non-transferable notice. Reset the flag on validation errors so
switching to an unsupported ticket type clears the notice.
@coderabbitai
Copy link

coderabbitai bot commented Feb 26, 2026

📝 Walkthrough

Walkthrough

This PR refactors promo code validation to remove the onError callback pattern and adds ticket reassignment controls. A new VALIDATE_PROMO_CODE_ERROR action type is introduced, while validatePromoCode simplifies to return API responses directly. The system now tracks reassignment permissions via promoCodeAllowsReassign state, with ticket and promo code constraints enforced across components. UI styling is adjusted for the promo code input layout, and Sentry error tracking is temporarily disabled.

Changes

Cohort / File(s) Summary
Promo Code Validation
src/actions.js, src/reducer.js
Refactored validatePromoCode to remove onError callback; added VALIDATE_PROMO_CODE_ERROR action. applyPromoCode converted to async thunk. Reducer now handles validation actions to update promoCodeAllowsReassign state based on API response. Removed debug logging.
Component Integration & Props Threading
src/components/registration-lite.js, src/components/ticket-type/index.js, src/components/personal-information/index.js
Introduced promoCodeAllowsReassign prop across components to control ticket reassignment. RegistrationLite wires loading actions and passes props to children. TicketTypeComponent validates promo code on ticket selection and displays non-transferable warnings. PersonalInfoComponent auto-assigns attendee data for non-transferable tickets or shows manual selection for transferable ones.
Styling Updates
src/components/promocode-input/index.module.scss, src/components/ticket-type/index.module.scss
Promo code input layout changed from horizontal to vertical stack with tighter spacing. Added .nonTransferable style class for UI warnings. Updated .moreInfo styling with reduced margin and font sizing across both files.
Utility Changes
src/utils/utils.js
Commented out Sentry integration in handleSentryException; error handling now logs to console only with TODO annotation for Sentry fix.

Sequence Diagram

sequenceDiagram
    participant User
    participant TicketTypeComp as TicketTypeComponent
    participant RegLite as RegistrationLite
    participant Actions as validatePromoCode Action
    participant API as Backend API
    participant Reducer as Redux Reducer
    participant PersonalComp as PersonalInfoComponent

    User->>TicketTypeComp: Select ticket
    TicketTypeComp->>RegLite: handleTicketChange (if promo code active)
    RegLite->>Actions: validatePromoCode(ticketData)
    Actions->>API: getRequest(validate endpoint)
    API-->>Actions: response {allows_to_reassign}
    alt Validation Success
        Actions->>Reducer: dispatch VALIDATE_PROMO_CODE
        Reducer->>Reducer: update promoCodeAllowsReassign
        Reducer-->>RegLite: state updated
        RegLite->>TicketTypeComp: Pass reassignment permissions
        TicketTypeComp->>User: Show/hide reassignment UI
    else Validation Error
        Actions->>Reducer: dispatch VALIDATE_PROMO_CODE_ERROR
        Reducer->>Reducer: reset promoCodeAllowsReassign to true
        Reducer-->>RegLite: state updated
        RegLite->>User: Display error message
    end
    RegLite->>PersonalComp: Pass promoCodeAllowsReassign
    User->>PersonalComp: Submit registration
    alt Non-Transferable Ticket
        PersonalComp->>PersonalComp: Auto-assign purchaser as attendee
    else Transferable Ticket
        PersonalComp->>PersonalComp: Allow manual ticket owner selection
    end
    PersonalComp->>User: Confirm submission
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐰 A hop through promo codes, so grand!
Where tickets may transfer or stand,
The validation now flows with such grace,
No callbacks to clog up the space,
Redux holds the truth of our plan! ✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main feature: preventing ticket reassignment based on promo code validation, which aligns with the core change across all modified files.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/prevent-ticket-reassignment

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


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.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (5)
src/utils/utils.js (1)

146-146: isSentryInitialized is now dead code.

This function is no longer called anywhere since handleSentryException was changed to only log to console. Either remove it along with the Sentry changes, or keep it if you plan to restore Sentry functionality soon.

🧹 Proposed cleanup if Sentry won't be restored soon
-const isSentryInitialized = () => !!window.SENTRY_DSN;
-
 // TODO: Fix Sentry dependency issue
 export const handleSentryException = (err) =>
     console.log("Error on registration: ", err);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/utils.js` at line 146, The isSentryInitialized helper is dead code
since handleSentryException no longer uses Sentry; remove the unused function
isSentryInitialized from utils.js and any related imports/usages, or if you
intend to restore Sentry telemetry, rewire handleSentryException to check
isSentryInitialized (and call Sentry.captureException) instead of only logging
to console; refer to the isSentryInitialized function name and
handleSentryException to locate the change points and make the corresponding
cleanup or restoration.
src/components/registration-lite.js (1)

287-310: Review the error handling condition.

The error handler only calls onError when e?.res?.body?.errors exists. If the API returns a different error structure (e.g., network error, timeout, or error without body.errors), the UI won't display feedback to the user—only Sentry logging occurs.

Consider whether all error scenarios should surface to the user:

♻️ Proposed fix to handle broader error cases
             .catch((e) => {
-                if (e?.res?.body?.errors) {
-                    onError(e, e.res);
-                }
+                if (e?.res?.body?.errors) {
+                    onError(e, e.res);
+                } else if (e?.res) {
+                    // Handle errors with different structure
+                    const fallbackErrors = [e?.res?.body?.message || 'An error occurred validating the promo code.'];
+                    onError(e, { body: { errors: fallbackErrors } });
+                }
                 handleSentryException(e);
             })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/registration-lite.js` around lines 287 - 310,
handleValidatePromocode currently only calls onError when e?.res?.body?.errors
exists, so other failures (network, timeout, different error shapes) never
surface to the UI; update the catch in handleValidatePromocode to always call
onError with a user-friendly error payload for any rejected promise from
validatePromoCode (pass the original error as first arg and a normalized
response object as second), preserving the existing branch that uses e.res when
available (i.e., prefer e.res for detailed errors but fall back to a default {
body: { errors: [<friendly message or e.message>] } }); keep calling
handleSentryException(e) and ensure stopWidgetLoading() still runs in finally.
src/components/ticket-type/index.js (1)

102-105: Consider adding minimal error logging in the catch block.

The empty .catch(() => {}) silently swallows errors. While validatePromoCode handles the Redux state update via VALIDATE_PROMO_CODE_ERROR, having no logging here can make debugging difficult if issues arise.

♻️ Optional: Add console logging for debugging
         // Validate promo code if already applied (ticket selected after promo applied)
         if (promoCode) {
-            validatePromoCode({ id: t.id, ticketQuantity: minQuantity, sub_type: t.sub_type }).catch(() => {});
+            validatePromoCode({ id: t.id, ticketQuantity: minQuantity, sub_type: t.sub_type }).catch((e) => {
+                console.log('Promo code validation failed for ticket change:', e);
+            });
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ticket-type/index.js` around lines 102 - 105, The empty catch
on the validatePromoCode call swallows errors; update the promise error handler
so it logs a minimal, contextual message and the error (for example include
promoCode, ticket id t.id, minQuantity and t.sub_type) instead of doing nothing
— locate the validatePromoCode invocation in the ticket-type component and
replace .catch(() => {}) with a catch that logs a short message plus the caught
error to console or the existing logger.
src/components/personal-information/index.js (1)

54-65: Consider using constants or useMemo instead of functions for computed values.

isSingleTicketOrder and shouldDisplayTicketAssignment are defined as functions but don't take any parameters - they read from component scope. This is inconsistent with ticketTypeAllowsReassign, canReassign, and isNonTransferable which are computed as constants.

♻️ Proposed refactor for consistency
-  // if there's only one ticket on the order and there is no invitation available, handle ticket assignment
-  const isSingleTicketOrder = () => formValues.ticketQuantity === 1 && !invitation && !isPrePaidTicketType(formValues.ticketType);
-
-  // check if reassignment is allowed by both promo code AND ticket type
-  const ticketTypeAllowsReassign = formValues.ticketType?.allows_to_reassign !== false;
-  const canReassign = promoCodeAllowsReassign && ticketTypeAllowsReassign;
-
-  // show radio options only if reassignment is allowed by both sources
-  const shouldDisplayTicketAssignment = () => isSingleTicketOrder() && canReassign;
-
-  // show notice when ticket is non-transferable (either promo code or ticket type disallows)
-  const isNonTransferable = isSingleTicketOrder() && !canReassign;
+  // if there's only one ticket on the order and there is no invitation available, handle ticket assignment
+  const isSingleTicketOrder = formValues.ticketQuantity === 1 && !invitation && !isPrePaidTicketType(formValues.ticketType);
+
+  // check if reassignment is allowed by both promo code AND ticket type
+  const ticketTypeAllowsReassign = formValues.ticketType?.allows_to_reassign !== false;
+  const canReassign = promoCodeAllowsReassign && ticketTypeAllowsReassign;
+
+  // show radio options only if reassignment is allowed by both sources
+  const shouldDisplayTicketAssignment = isSingleTicketOrder && canReassign;
+
+  // show notice when ticket is non-transferable (either promo code or ticket type disallows)
+  const isNonTransferable = isSingleTicketOrder && !canReassign;

Then update usages from isSingleTicketOrder() and shouldDisplayTicketAssignment() to isSingleTicketOrder and shouldDisplayTicketAssignment.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/personal-information/index.js` around lines 54 - 65, Replace
the zero-arg functions isSingleTicketOrder and shouldDisplayTicketAssignment
with computed constants (or wrap them in useMemo if you want memoization) that
read from formValues, invitation, promoCodeAllowsReassign and
isPrePaidTicketType (i.e., compute const isSingleTicketOrder =
formValues.ticketQuantity === 1 && !invitation &&
!isPrePaidTicketType(formValues.ticketType) and const
shouldDisplayTicketAssignment = isSingleTicketOrder && canReassign), keep
ticketTypeAllowsReassign, canReassign and isNonTransferable as-is, and update
all usages to refer to these identifiers without calling them as functions
(remove the trailing () where used).
src/reducer.js (1)

169-172: Consider resetting promoCodeAllowsReassign when setting a new promo code.

When SET_CURRENT_PROMO_CODE is dispatched (e.g., user applies a different promo code), the promoCodeAllowsReassign value from the previous promo code persists until validation completes. This could cause a brief UI inconsistency where the reassignment state reflects the old promo code.

♻️ Proposed fix to reset the flag when applying a new code
 case SET_CURRENT_PROMO_CODE:{
     const { currentPromoCode } = payload;
-    return { ...state, promoCode: currentPromoCode }
+    return { ...state, promoCode: currentPromoCode, promoCodeAllowsReassign: true }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/reducer.js` around lines 169 - 172, In the reducer case handling
SET_CURRENT_PROMO_CODE (in src/reducer.js) you currently set promoCode from
payload.currentPromoCode but leave promoCodeAllowsReassign from the previous
state; update that case to also reset promoCodeAllowsReassign (e.g., to false or
null/undefined) when applying a new promo code so the old flag doesn’t briefly
persist in the UI; ensure you modify the object returned in the
SET_CURRENT_PROMO_CODE branch (the case block that references currentPromoCode)
to include promoCodeAllowsReassign: <desired-reset-value>.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/components/personal-information/index.js`:
- Around line 54-65: Replace the zero-arg functions isSingleTicketOrder and
shouldDisplayTicketAssignment with computed constants (or wrap them in useMemo
if you want memoization) that read from formValues, invitation,
promoCodeAllowsReassign and isPrePaidTicketType (i.e., compute const
isSingleTicketOrder = formValues.ticketQuantity === 1 && !invitation &&
!isPrePaidTicketType(formValues.ticketType) and const
shouldDisplayTicketAssignment = isSingleTicketOrder && canReassign), keep
ticketTypeAllowsReassign, canReassign and isNonTransferable as-is, and update
all usages to refer to these identifiers without calling them as functions
(remove the trailing () where used).

In `@src/components/registration-lite.js`:
- Around line 287-310: handleValidatePromocode currently only calls onError when
e?.res?.body?.errors exists, so other failures (network, timeout, different
error shapes) never surface to the UI; update the catch in
handleValidatePromocode to always call onError with a user-friendly error
payload for any rejected promise from validatePromoCode (pass the original error
as first arg and a normalized response object as second), preserving the
existing branch that uses e.res when available (i.e., prefer e.res for detailed
errors but fall back to a default { body: { errors: [<friendly message or
e.message>] } }); keep calling handleSentryException(e) and ensure
stopWidgetLoading() still runs in finally.

In `@src/components/ticket-type/index.js`:
- Around line 102-105: The empty catch on the validatePromoCode call swallows
errors; update the promise error handler so it logs a minimal, contextual
message and the error (for example include promoCode, ticket id t.id,
minQuantity and t.sub_type) instead of doing nothing — locate the
validatePromoCode invocation in the ticket-type component and replace .catch(()
=> {}) with a catch that logs a short message plus the caught error to console
or the existing logger.

In `@src/reducer.js`:
- Around line 169-172: In the reducer case handling SET_CURRENT_PROMO_CODE (in
src/reducer.js) you currently set promoCode from payload.currentPromoCode but
leave promoCodeAllowsReassign from the previous state; update that case to also
reset promoCodeAllowsReassign (e.g., to false or null/undefined) when applying a
new promo code so the old flag doesn’t briefly persist in the UI; ensure you
modify the object returned in the SET_CURRENT_PROMO_CODE branch (the case block
that references currentPromoCode) to include promoCodeAllowsReassign:
<desired-reset-value>.

In `@src/utils/utils.js`:
- Line 146: The isSentryInitialized helper is dead code since
handleSentryException no longer uses Sentry; remove the unused function
isSentryInitialized from utils.js and any related imports/usages, or if you
intend to restore Sentry telemetry, rewire handleSentryException to check
isSentryInitialized (and call Sentry.captureException) instead of only logging
to console; refer to the isSentryInitialized function name and
handleSentryException to locate the change points and make the corresponding
cleanup or restoration.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2e9f95b and 27da930.

📒 Files selected for processing (8)
  • src/actions.js
  • src/components/personal-information/index.js
  • src/components/promocode-input/index.module.scss
  • src/components/registration-lite.js
  • src/components/ticket-type/index.js
  • src/components/ticket-type/index.module.scss
  • src/reducer.js
  • src/utils/utils.js

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