Skip to content

Commit

Permalink
Ticketing price integration (#659)
Browse files Browse the repository at this point in the history
* Integrate ticket price field into ticket creation/list views, as well into ticket creation frontend.

* Enforce non-negative ticket prices at creation

* Add frontend checks for fractional/negative ticket count and cost.

* Prevent users from entering negative/fractional ticket counts/price for now.

---------

Co-authored-by: aviupadhyayula <aupadhy@gmail.com>
  • Loading branch information
julianweng and aviupadhyayula committed Apr 16, 2024
1 parent 4af8765 commit 5880d7c
Show file tree
Hide file tree
Showing 2 changed files with 81 additions and 21 deletions.
34 changes: 31 additions & 3 deletions backend/clubs/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
DurationField,
ExpressionWrapper,
F,
Max,
Prefetch,
Q,
Sum,
Expand Down Expand Up @@ -2542,6 +2543,8 @@ def tickets(self, request, *args, **kwargs):
type: string
count:
type: integer
price:
type: number
available:
type: array
items:
Expand All @@ -2551,15 +2554,24 @@ def tickets(self, request, *args, **kwargs):
type: string
count:
type: integer
price:
type: number
---
"""
event = self.get_object()
tickets = Ticket.objects.filter(event=event)

Check warning on line 2562 in backend/clubs/views.py

View check run for this annotation

Codecov / codecov/patch

backend/clubs/views.py#L2561-L2562

Added lines #L2561 - L2562 were not covered by tests

totals = tickets.values("type").annotate(count=Count("type")).order_by("type")
# Take price of first ticket of given type for now
totals = (

Check warning on line 2565 in backend/clubs/views.py

View check run for this annotation

Codecov / codecov/patch

backend/clubs/views.py#L2565

Added line #L2565 was not covered by tests
tickets.values("type")
.annotate(price=Max("price"))
.annotate(count=Count("type"))
.order_by("type")
)
available = (

Check warning on line 2571 in backend/clubs/views.py

View check run for this annotation

Codecov / codecov/patch

backend/clubs/views.py#L2571

Added line #L2571 was not covered by tests
tickets.filter(owner__isnull=True, holder__isnull=True)
.values("type")
.annotate(price=Max("price"))
.annotate(count=Count("type"))
.order_by("type")
)
Expand All @@ -2586,6 +2598,8 @@ def create_tickets(self, request, *args, **kwargs):
type: string
count:
type: integer
price:
type: number
order_limit:
type: int
required: false
Expand All @@ -2598,17 +2612,31 @@ def create_tickets(self, request, *args, **kwargs):
properties:
detail:
type: string
"400":
content:
application/json:
schema:
type: object
properties:
detail:
type: string
---
"""
event = self.get_object()

Check warning on line 2625 in backend/clubs/views.py

View check run for this annotation

Codecov / codecov/patch

backend/clubs/views.py#L2625

Added line #L2625 was not covered by tests

quantities = request.data.get("quantities")

Check warning on line 2627 in backend/clubs/views.py

View check run for this annotation

Codecov / codecov/patch

backend/clubs/views.py#L2627

Added line #L2627 was not covered by tests

# Atomicity ensures idempotency
# Ticket prices must be non-negative
if any(item.get("price", 0) < 0 for item in quantities):
return Response(

Check warning on line 2631 in backend/clubs/views.py

View check run for this annotation

Codecov / codecov/patch

backend/clubs/views.py#L2630-L2631

Added lines #L2630 - L2631 were not covered by tests
{"detail": "Ticket price cannot be negative."},
status=status.HTTP_400_BAD_REQUEST,
)

# Atomicity ensures idempotency
Ticket.objects.filter(event=event).delete() # Idempotency
tickets = [

Check warning on line 2638 in backend/clubs/views.py

View check run for this annotation

Codecov / codecov/patch

backend/clubs/views.py#L2637-L2638

Added lines #L2637 - L2638 were not covered by tests
Ticket(event=event, type=item["type"])
Ticket(event=event, type=item["type"], price=item["price"])
for item in quantities
for _ in range(item["count"])
]
Expand Down
68 changes: 50 additions & 18 deletions frontend/components/ClubEditPage/TicketsModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,21 +55,32 @@ const TicketItem = ({
ticket,
changeName,
changeCount,
changePrice,
deleteTicket,
deletable,
index,
}): ReactElement => {
const [name, setName] = useState(ticket.name)
const [count, setCount] = useState(ticket.count)
const [price, setPrice] = useState(ticket.price)

const handleNameChange = (e) => {
setName(e.target.value)
changeName(e.target.value, index)
}

const handleCountChange = (e) => {
setCount(e.target.value)
changeCount(e.target.value, index)
let rounded = Math.round(parseFloat(e.target.value))
rounded = rounded < 0 ? 0 : rounded
setCount(rounded.toString())
changeCount(rounded.toString(), index)
}

const handlePriceChange = (e) => {
let rounded = Math.round(parseFloat(e.target.value) * 100) / 100
rounded = rounded < 0 ? 0 : rounded
setPrice(rounded.toString())
changePrice(rounded.toString(), index)
}

return (
Expand All @@ -95,6 +106,13 @@ const TicketItem = ({
placeholder="Ticket Count"
onChange={handleCountChange}
/>
<Input
type="number"
className="input"
value={price}
placeholder="Ticket Price"
onChange={handlePriceChange}
/>
<button
className="button is-danger"
disabled={!deletable}
Expand All @@ -110,6 +128,7 @@ const TicketItem = ({
type Ticket = {
name: string
count: string | null
price: string | null // Free if null
}

const TicketsModal = (props: { event: ClubEvent }): ReactElement => {
Expand All @@ -119,7 +138,7 @@ const TicketsModal = (props: { event: ClubEvent }): ReactElement => {
const [submitting, setSubmitting] = useState(false)

const [tickets, setTickets] = useState<Ticket[]>([
{ name: 'Regular Ticket', count: null },
{ name: 'Regular Ticket', count: null, price: null },
])

const handleNameChange = (name, i) => {
Expand All @@ -134,6 +153,12 @@ const TicketsModal = (props: { event: ClubEvent }): ReactElement => {
setTickets(ticks)
}

const handlePriceChange = (price, i) => {
const ticks = [...tickets]
ticks[i].price = price
setTickets(ticks)
}

const deleteTicket = (i) => {
const ticks = [...tickets]
ticks.splice(i, 1)
Expand All @@ -142,7 +167,7 @@ const TicketsModal = (props: { event: ClubEvent }): ReactElement => {

const addNewTicket = () => {
const ticks = [...tickets]
ticks.push({ name: '', count: null })
ticks.push({ name: '', count: null, price: null })
setTickets(ticks)
}

Expand All @@ -151,24 +176,38 @@ const TicketsModal = (props: { event: ClubEvent }): ReactElement => {
const quantities = tickets
.filter((ticket) => ticket.count != null)
.map((ticket) => {
return { type: ticket.name, count: parseInt(ticket.count || '') }
return {
type: ticket.name,
count: parseInt(ticket.count || ''),
price: parseFloat(ticket.price || ''),
}
})
doApiRequest(`/events/${id}/tickets/?format=json`, {
method: 'PUT',
body: {
quantities: quantities,
quantities,
},
}).then((res) => {
if (res.ok) {
notify(<>Tickets Created!</>, 'success')
setSubmitting(false)
} else {
notify(<>Error creating tickets</>, 'error')
setSubmitting(false)
}
})
notify(<>Tickets Created!</>, 'success')
setSubmitting(false)
}
}

const disableSubmit = tickets.some(
(ticket) =>
typeof ticket.name !== 'string' ||
ticket.count === null ||
!Number.isInteger(parseInt(ticket.count || '0')),
!Number.isInteger(parseInt(ticket.count || '0')) ||
parseInt(ticket.count || '0') < 0 ||
ticket.price === null ||
!Number.isFinite(parseFloat(ticket.price || '0')) ||
parseFloat(ticket.price || '0') < 0,
)

return (
Expand All @@ -181,15 +220,7 @@ const TicketsModal = (props: { event: ClubEvent }): ReactElement => {
/>
<ModalBody>
<Title>{name}</Title>
<Text>
Create new tickets for this event. To be filled with actual
instructions. Lorem ipsum dolor sit amet, consectetur adipiscing elit,
sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi
ut aliquip ex ea commodo consequat. Duis aute irure dolor in
reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla
pariatur.
</Text>
<Text>Create new tickets for this event.</Text>
<Line />
<SectionContainer>
<h1>Tickets</h1>
Expand All @@ -202,6 +233,7 @@ const TicketsModal = (props: { event: ClubEvent }): ReactElement => {
deletable={tickets.length !== 1}
changeName={handleNameChange}
changeCount={handleCountChange}
changePrice={handlePriceChange}
deleteTicket={deleteTicket}
/>
))}
Expand Down

0 comments on commit 5880d7c

Please sign in to comment.