-
Notifications
You must be signed in to change notification settings - Fork 1
Treat quantity 0 as unlimited in ticket and promo caps #154
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| import React from 'react'; | ||
| import { render } from '@testing-library/react'; | ||
| import '@testing-library/jest-dom'; | ||
| import T from 'i18n-react'; | ||
| import TicketTypeComponent from '..'; | ||
|
|
||
| T.setTexts(require('../../../i18n/en.json')); | ||
|
|
||
| const unlimitedStockTicket = { | ||
| id: 1, | ||
| name: 'General Admission', | ||
| currency: 'USD', | ||
| currency_symbol: '$', | ||
| quantity_2_sell: 0, // API semantics: 0 = unlimited stock | ||
| quantity_sold: 10, | ||
| max_quantity_per_order: 4, // real, binding per-order cap | ||
| }; | ||
|
|
||
| it('shows the per-order limit notice when stock is unlimited (quantity_2_sell 0)', () => { | ||
| const { getByText } = render( | ||
| <TicketTypeComponent | ||
| isActive | ||
| allowedTicketTypes={[unlimitedStockTicket]} | ||
| originalTicketTypes={[unlimitedStockTicket]} | ||
| taxTypes={[]} | ||
| changeForm={jest.fn()} | ||
| trackViewItem={jest.fn()} | ||
| allowPromoCodes={false} | ||
| reservation={{ tickets: [{ ticket_type_id: 1 }] }} | ||
| /> | ||
| ); | ||
|
|
||
| // The stepper caps at 4 (getTicketMaxQuantity treats quantity_2_sell 0 as | ||
| // unlimited stock), so the notice explaining that cap must be shown. | ||
| expect(getByText('This ticket type is limited to 4 per order.')).toBeInTheDocument(); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| import { getTicketMaxQuantity } from '../getTicketMaxQuantity'; | ||
| import { TICKET_TYPE_SUBTYPE_PREPAID } from '../../utils/constants'; | ||
|
|
||
| describe('getTicketMaxQuantity', () => { | ||
| it('returns 0 when no ticket is given', () => { | ||
| expect(getTicketMaxQuantity(null)).toBe(0); | ||
| }); | ||
|
|
||
| it('caps at the remaining stock when there is a real per-order limit', () => { | ||
| // 100 to sell, 90 sold, up to 5 per order -> per-order limit wins | ||
| const ticket = { quantity_2_sell: 100, quantity_sold: 90, max_quantity_per_order: 5 }; | ||
| expect(getTicketMaxQuantity(ticket)).toBe(5); | ||
| }); | ||
|
|
||
| it('caps at remaining stock when stock is the tighter limit', () => { | ||
| // 100 to sell, 97 sold, up to 5 per order -> only 3 left | ||
| const ticket = { quantity_2_sell: 100, quantity_sold: 97, max_quantity_per_order: 5 }; | ||
| expect(getTicketMaxQuantity(ticket)).toBe(3); | ||
| }); | ||
|
|
||
| it('treats max_quantity_per_order of 0 as unlimited (the sold-out bug)', () => { | ||
| // ticket 212 from prod: 2900 to sell, 2832 sold, per-order limit 0 (API = no limit) | ||
| // 68 tickets remain, so it must NOT read as sold out | ||
| const ticket = { quantity_2_sell: 2900, quantity_sold: 2832, max_quantity_per_order: 0 }; | ||
| expect(getTicketMaxQuantity(ticket)).toBe(68); | ||
| }); | ||
|
|
||
| it('treats quantity_2_sell of 0 as unlimited stock', () => { | ||
| // 0 to sell = no cap on stock; per-order limit of 4 is the only bound | ||
| const ticket = { quantity_2_sell: 0, quantity_sold: 10, max_quantity_per_order: 4 }; | ||
| expect(getTicketMaxQuantity(ticket)).toBe(4); | ||
| }); | ||
|
|
||
| it('defaults quantity_sold to 0 when the API omits it', () => { | ||
| const ticket = { quantity_2_sell: 100, max_quantity_per_order: 5 }; | ||
| expect(getTicketMaxQuantity(ticket)).toBe(5); | ||
| }); | ||
|
|
||
| it('still returns <= 0 for a genuinely sold-out ticket', () => { | ||
| // real cap reached: 100 to sell, 100 sold | ||
| const ticket = { quantity_2_sell: 100, quantity_sold: 100, max_quantity_per_order: 5 }; | ||
| expect(getTicketMaxQuantity(ticket)).toBeLessThan(1); | ||
| }); | ||
|
|
||
| it('applies the remaining-per-account cap when it is the tightest', () => { | ||
| const ticket = { quantity_2_sell: 100, quantity_sold: 10, max_quantity_per_order: 10 }; | ||
| expect(getTicketMaxQuantity(ticket, 2)).toBe(2); | ||
| }); | ||
|
|
||
| it('always returns 1 for prepaid ticket types', () => { | ||
| const ticket = { sub_type: TICKET_TYPE_SUBTYPE_PREPAID, quantity_2_sell: 0, quantity_sold: 0, max_quantity_per_order: 0 }; | ||
| expect(getTicketMaxQuantity(ticket)).toBe(1); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,7 +3,10 @@ import { isPrePaidTicketType } from '../utils/utils'; | |
| export const getTicketMaxQuantity = (ticket, remainingQuantityPerAccount) => { | ||
| if(!ticket) return 0; | ||
| if(isPrePaidTicketType(ticket)) return 1; | ||
| let max = Math.min((ticket.quantity_2_sell ?? Number.MAX_SAFE_INTEGER) - ticket.quantity_sold, (ticket.max_quantity_per_order ?? Number.MAX_SAFE_INTEGER)); | ||
| // The API treats 0 as "no limit" for both fields; only a positive value is a real cap. | ||
| const quantityToSell = ticket.quantity_2_sell || Number.MAX_SAFE_INTEGER; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @gcutrini The 0-means-unlimited semantics this line establishes are still missing in one sibling consumer of the same fields: the Why it matters: Suggested fix (one line, same semantics as this helper): const inventory = (ticket.quantity_2_sell || Number.MAX_SAFE_INTEGER) - (ticket.quantity_sold ?? 0);Red test that verifies it (fails on this branch with "Unable to find an element with the text: This ticket type is limited to 4 per order.", passes with the one-line fix — verified locally both ways). Suggested location: import React from 'react';
import { render } from '@testing-library/react';
import '@testing-library/jest-dom';
import T from 'i18n-react';
import TicketTypeComponent from '..';
T.setTexts(require('../../../i18n/en.json'));
const unlimitedStockTicket = {
id: 1,
name: 'General Admission',
currency: 'USD',
currency_symbol: '$',
quantity_2_sell: 0, // API semantics: 0 = unlimited stock
quantity_sold: 10,
max_quantity_per_order: 4, // real, binding per-order cap
};
it('shows the per-order limit notice when stock is unlimited (quantity_2_sell 0)', () => {
const { getByText } = render(
<TicketTypeComponent
isActive
allowedTicketTypes={[unlimitedStockTicket]}
originalTicketTypes={[unlimitedStockTicket]}
taxTypes={[]}
changeForm={jest.fn()}
trackViewItem={jest.fn()}
allowPromoCodes={false}
reservation={{ tickets: [{ ticket_type_id: 1 }] }}
/>
);
// The stepper caps at 4 (getTicketMaxQuantity treats quantity_2_sell 0 as
// unlimited stock), so the notice explaining that cap must be shown.
expect(getByText('This ticket type is limited to 4 per order.')).toBeInTheDocument();
});
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch, fixed. ticketPerOrderLimit had the same quantity_2_sell ?? MAX, so with 0 stock the inventory went negative and the notice was suppressed. Changed to || MAX and added your per-order-notice test. Verified red before, green after. |
||
| const maxPerOrder = ticket.max_quantity_per_order || Number.MAX_SAFE_INTEGER; | ||
| let max = Math.min(quantityToSell - (ticket.quantity_sold ?? 0), maxPerOrder); | ||
| if (remainingQuantityPerAccount != null) { | ||
| max = Math.min(max, remainingQuantityPerAccount); | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@gcutrini The
quantity_soldguard the PR description advertises ("guards quantity_sold when absent") has no test pinning it: every case in this file suppliesquantity_sold, so reverting the?? 0in the helper (NaN propagates throughMath.min, makingticketSelectionValidpermanently false and blocking the Next button) fails nothing in the suite.Why it matters: both API serializers currently always emit
quantity_soldas an int, so this is contract-pinning rather than a reachable production bug — but an unpinned guard is the first thing a future refactor silently drops.Suggested test (passes on this branch, fails with
Expected: 5, Received: NaNwhen the?? 0guard is removed — verified locally both ways):There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added your suggested test. Confirmed it fails with NaN when the ?? 0 guard is removed and passes with it.