diff --git a/app/api/admin/epos/route.ts b/app/api/admin/epos/route.ts new file mode 100644 index 00000000..1e245db7 --- /dev/null +++ b/app/api/admin/epos/route.ts @@ -0,0 +1,50 @@ +import { createClerkClient } from '@clerk/backend'; +import { auth } from '@clerk/nextjs/server'; +import { NextResponse } from 'next/server'; +// import Pusher from 'pusher' + +// const pusher = new Pusher({ +// appId: process.env.PUSHER_APP_ID, +// key: process.env.NEXT_PUBLIC_PUSHER_APP_KEY, +// secret: process.env.PUSHER_APP_SECRET, +// cluster: "eu", +// useTLS: true +// }); + + +export async function POST(request: Request) { + const body = await request.json() + console.log(body) + + const clerkClient = createClerkClient({ secretKey: process.env.CLERK_SECRET_KEY }); + const {userId} = await auth(); + + if(!userId){ + return Response.json({error: "User is not signed in."}, { status: 401 }); + } + + const requestingUser = await clerkClient.users.getUser(userId); + if(!requestingUser.publicMetadata.admin){ + return Response.json({error: "User is does not have permissions."}, { status: 401 }); + } + + const checkoutUrl = process.env.LAMBDA_CHECKOUT_COMPLETE_INPERSON + + const checkoutResponse = await fetch(checkoutUrl, { + method: 'POSt', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(body) + }) + if(!checkoutResponse.ok) { + const checkoutData = await checkoutResponse.json() + return NextResponse.json({...checkoutData, generated_at: new Date().toISOString() }, {status: checkoutResponse.status}) + } + const checkoutData = await checkoutResponse.json() + const ticket_number = checkoutData.ticket_number + // const ticket_number = 2212652493 //! This should be returned + // console.log("checkoutData",checkoutData) + return NextResponse.json({ ticket_number: ticket_number, generated_at: new Date().toISOString() }) + // } +} \ No newline at end of file diff --git a/app/api/izettle/route.ts b/app/api/izettle/route.ts index 2cc60e30..6b68764e 100644 --- a/app/api/izettle/route.ts +++ b/app/api/izettle/route.ts @@ -1,6 +1,47 @@ import { NextResponse} from 'next/server' +import Pusher from 'pusher' + +const pusher = new Pusher({ + appId: process.env.PUSHER_APP_ID, + key: process.env.NEXT_PUBLIC_PUSHER_APP_KEY, + secret: process.env.PUSHER_APP_SECRET, + cluster: "eu", + useTLS: true +}); + export async function POST(request: Request) { - console.log(request) - return NextResponse.json({ generated_at: new Date().toISOString() }) + const body = await request.json() + const eventName = body.eventName + const payload = JSON.parse(body.payload) + console.log(eventName) + console.log(payload) + // console.log(JSON.stringify(JSON.stringify(payload))) + + if(eventName == 'TestMessage') { + return NextResponse.json({ generated_at: new Date().toISOString() }) + } else { + // console.log("request:", request) + // console.log("body:",body) + // console.log("payload:",payload, payload.products.map((product) => product.category)) + const channel = "card-payments" + + const tills = payload.products.map((product) => product?.sku) as string[] + const notification = { + amount: payload.amount, + created: payload.created, + timestamp: payload.timestamp, + payment_ref: payload.payments.map((payment) => payment.attributes.referenceNumber).join(' , '), + purchaseUuid: payload.purchaseUuid, + tills: tills + // products: payload.products.map((product) => { return { name: product.sku , till: product?.category?.name?.toLowerCase()?.replaceAll(" ", ""), Uuid: product['productUuid'] }} ) as string[], + } + console.log("notification:",notification) + + tills.forEach(till => { + pusher.trigger(channel, till, notification); + }) + pusher.trigger(channel, "all", notification); + return NextResponse.json({ generated_at: new Date().toISOString() }) + } } \ No newline at end of file diff --git a/components/admin/lists/filterable.tsx b/components/admin/lists/filterable.tsx index 345dc1ab..9e42b2e1 100644 --- a/components/admin/lists/filterable.tsx +++ b/components/admin/lists/filterable.tsx @@ -51,8 +51,15 @@ export const filterItems = (items, filters) => { } else if (filter.field == 'passes') { const matches = person[filter.field].map((pass) => pass.toLowerCase()).join(', ').includes(filterValue.toLowerCase()) excludePerson = excludePerson || (invertFilter ? matches : !matches) - } else if (filter.field == 'signed_in') { - excludePerson = excludePerson || (filterValue == true && !person[filter.field]) || (person[filter.field] && format(new Date(person[filter.field]), 'eee') != filterValue) + } else if (filter.field == 'checkin_at') { + excludePerson = excludePerson + || (true == filterValue && false == person[filter.field]) // Exclude anyone who hasn't checked in + || (false == filterValue && false != person[filter.field]) // Exclude anyone who has checked in + || (!invertFilter && ![true,false].includes(filterValue) && (person[filter.field] == false || format(new Date(person[filter.field]), 'eee').toLowerCase() != filterValue.toLowerCase())) // Exlude anyone if we're not search for true or false and the day matches + || (invertFilter && ![true,false].includes(filterValue) && (person[filter.field] == false || format(new Date(person[filter.field]), 'eee').toLowerCase() == filterValue.toLowerCase())) // Exlude anyonee if we're not search for true or false and the day DOESNT matche and we've inverted the filter + if(['2212652493','5060423521'].includes(person.ticket_number)) { + console.log("FILTER",person.name,filter.field,filterValue,person[filter.field],excludePerson,format(new Date(person[filter.field]), 'eee')) + } } else if (filter.field == 'active') { excludePerson = excludePerson || person[filter.field] != filterValue } else { diff --git a/components/admin/lists/ticketRow.tsx b/components/admin/lists/ticketRow.tsx index 54ea2d91..75eb0594 100644 --- a/components/admin/lists/ticketRow.tsx +++ b/components/admin/lists/ticketRow.tsx @@ -3,8 +3,8 @@ import { BiCreditCard, BiLogoSketch, BiLeftArrowCircle, BiSolidRightArrowSquare, import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/react' import { EllipsisVerticalIcon, CurrencyPoundIcon, ClipboardIcon, ExclamationTriangleIcon } from '@heroicons/react/24/solid' import { format,fromUnixTime } from 'date-fns'; -import { scanIn } from '@lib/fetchers'; -import { mutate } from 'swr'; +// import { scanIn } from '@lib/fetchers'; +// import { mutate } from 'swr'; const TicketStatusIcon = ({attendee}) => { const PaymentIcon = attendee.status === 'paid_stripe' ? @@ -43,8 +43,9 @@ export const TicketRow = ({attendee,setActiveTicket, setNameChangeModalActive, s {passString} {attendee.checkin_at - ? - : } + // ? + ? + : } diff --git a/components/admin/scan/ScanSuccessDialog.tsx b/components/admin/scan/ScanSuccessDialog.tsx index 167e1e82..c318d89a 100644 --- a/components/admin/scan/ScanSuccessDialog.tsx +++ b/components/admin/scan/ScanSuccessDialog.tsx @@ -1,30 +1,46 @@ import useSWR from "swr"; +import { BiSolidBadgeCheck, BiIdCard} from "react-icons/bi"; // import { itemsFromPassCombination} from '@components/ticketing/pricingUtilities' +import WristBandIcon from '@public/wristband.svg'; +import TicketIcon from '@public/ticket.svg'; + import { format, fromUnixTime } from "date-fns"; import { fetcher, scanIn } from "@lib/fetchers"; export type line_item = { description: string, } +// fri party, sat class, sat din, sat party, sun class, sun party +// 0 1 2 3 4 5 const accessToWristbands = (accesCode: number[], line_items: line_item[]) => { let wristBands = [] const meal = accesCode[2] > 0 const friday = accesCode[0] > 0 + const artist = line_items.filter((li) => { return /Artist/.test(li.description)}).length > 0 const staffPass = line_items.filter((li) => { return /Staff/.test(li.description) || /Volunteer/.test(li.description)}).length > 0 const fullPass = line_items.filter((li) => { return /Full/.test(li.description)}).length > 0 - const partyPass = line_items.filter((li) => { return /Party\sPass/.test(li.description)}).length > 0 - const classPass = line_items.filter((li) => { return /Class\sPass/.test(li.description)}).length > 0 - const saturdayPass = line_items.filter((li) => { return /Saturday\sPass/.test(li.description) }).length > 0 - const sundayPass = line_items.filter((li) => { return /Sunday\sPass/.test(li.description) }).length > 0 - const saturdayClass = line_items.filter((li) => { return /Saturday\s-\sClass/.test(li.description) }).length > 0 - const saturdayParty = line_items.filter((li) => { return /Saturday\s-\sParty/.test(li.description) || /Dine\sand\sDance\sPass/.test(li.description) }).length > 0 - const sundayClass = line_items.filter((li) => { return /Sunday\s-\sClass/.test(li.description) }).length > 0 - const sundayParty = line_items.filter((li) => { return /Sunday\s-\sParty/.test(li.description) }).length > 0 + const fullPassLike = artist || staffPass || fullPass + const partyPass = line_items.filter((li) => { return /Party\sPass/.test(li.description)}).length > 0 || (accesCode[0] > 0 && accesCode[3] > 0 && accesCode[5] > 0) && !fullPassLike + const classPass = line_items.filter((li) => { return /Class\sPass/.test(li.description)}).length > 0 || (accesCode[1] > 0 && accesCode[4] > 0) && !fullPassLike + const saturdayPass = line_items.filter((li) => { return /Saturday\sPass/.test(li.description) }).length > 0 || (accesCode[1] > 0 && accesCode[3] > 0) && !fullPassLike + const sundayPass = line_items.filter((li) => { return /Sunday\sPass/.test(li.description) }).length > 0 || (accesCode[4] > 0 && accesCode[5] > 0) && !fullPassLike + const saturdayClass = line_items.filter((li) => { return /Saturday\s-\sClass/.test(li.description) }).length > 0 || (accesCode[1] > 0 && !saturdayPass && !classPass) && !fullPassLike + const saturdayParty = line_items.filter((li) => { return /Saturday\s-\sParty/.test(li.description) || /Dine\sand\sDance\sPass/.test(li.description) }).length > 0 || (accesCode[3] > 0 && !saturdayPass && !partyPass) && !fullPassLike + const sundayClass = line_items.filter((li) => { return /Sunday\s-\sClass/.test(li.description) }).length > 0 || (accesCode[4] > 0 && !sundayPass && !classPass) && !fullPassLike + const sundayParty = line_items.filter((li) => { return /Sunday\s-\sParty/.test(li.description) }).length > 0 || (accesCode[5] > 0 && !sundayPass && !partyPass) && !fullPassLike - if(meal) { wristBands.push({ colour: "bg-white text-black", name: "Dinner - Ticket"}) } - if(friday && !artist && !staffPass && !fullPass && !partyPass) { wristBands.push({ colour: "bg-black", name: "Friday - Stamp"}) } + if(meal) { wristBands.push({ + colour: "bg-white text-black", + name: "Dinner - Ticket", + icon: + }) } + if(friday && !artist && !staffPass && !fullPass && !partyPass) { wristBands.push({ + colour: "bg-black border border-gray-600", + name: "Friday - Stamp", + icon: + }) } if(artist) { wristBands.push({ colour: "bg-blue-600", name: "Artist Pass - Plastic"}) } if(staffPass) { wristBands.push({ colour: "bg-blue-600", name: "Staff/Volunteer Pass - Plastic"}) } if(fullPass) { wristBands.push({ colour: "bg-gray-300 text-black", name: "Full Pass - Plastic"}) } @@ -41,41 +57,53 @@ const accessToWristbands = (accesCode: number[], line_items: line_item[]) => { const ScanSuccessDialog = ({scan,onClick}) => { const {data, error, isLoading, isValidating} = useSWR(`/api/admin/scan/${scan}`, fetcher, { keepPreviousData: false }); - + // const wristBandIcon = + // + // + const defaultWristBandIcon = if(scan) { - if(isLoading) return
Loading
+ if(isLoading) return if(error || data.error) return
Error: {error} {data?.error}
if(!data.attendee ) return
No Attendee Data?
- if(isValidating) return
Validating...
+ if(isValidating) return const attendee = data.attendee const goodResult = attendee.active && !attendee.ticket_used const student = attendee.student_ticket const wristBands = accessToWristbands(attendee.access,attendee.line_items) // const access = itemsFromPassCombination(attendee.line_items.map(item => item.description)) - const checked_in = attendee.ticket_used ? format(fromUnixTime(attendee.ticket_used), 'HH:mm:ss do MMM') : attendee.ticket_used - const cardColor = isLoading ? "bg-gray-500" : goodResult ? student ? "bg-green-600" : "bg-green-500" : "bg-chillired-600" - const cancelButton = checked_in ? "border-red-900 text-red-900" : "border-green-900 text-green-900" - const checkinButton = checked_in ? "bg-red-950" : "bg-green-950" + const checked_in: string = attendee.ticket_used ? format(fromUnixTime(attendee.ticket_used), 'HH:mm:ss do MMM') : attendee.ticket_used + const cardColor = isLoading ? "bg-gray-900" : goodResult ? student ? "bg-green-900" : "bg-green-900" : "bg-chillired-800" + const cancelButton = checked_in ? "border-red-900 text-red-900" : "border-green-200 text-green-100 hover:text-white" + const checkinButton = checked_in ? "bg-red-600" : "bg-green-600 hover:bg-green-500" - return (
-
+ return (
+
-

{attendee.full_name}

+

{attendee.full_name}

{attendee.ticket_number}

- { checked_in ?
Checked in already: {checked_in}
:
- {/*
    - {access.map(item =>
  • {item}
  • )} -
*/} +
+
+ { checked_in ?
+

ALREADY CHECKED IN:

+

{checked_in.toUpperCase()}

+
+ :
{ wristBands.map((wristBand) => { - return (
{wristBand.name}
) + return (
+ {wristBand.icon? wristBand.icon : defaultWristBandIcon} + {wristBand.name} +
) })}
} -
-
- {student ?
Student Ticket Check ID
: null} +
+ {student && goodResult ?
+ Check Student ID
: null} +
+
+
@@ -83,10 +111,19 @@ const ScanSuccessDialog = ({scan,onClick}) => {
) } else { - return
Nothing
+ return } } +const LoadingDialog = ({onClick}) => { + return
+ + + + +
+} + export default ScanSuccessDialog diff --git a/components/admin/statBlock.tsx b/components/admin/statBlock.tsx index 87c3b52c..c45f71dd 100644 --- a/components/admin/statBlock.tsx +++ b/components/admin/statBlock.tsx @@ -7,7 +7,7 @@ export type StatLine = { unit?: string; }; export default function StatBlock({stats}: {stats: StatLine[]}) { - const [hidden,setHidden] = useState(false); + const [hidden,setHidden] = useState(true); const toggelButton =
const statBlock = (
diff --git a/components/admin/ticketList.tsx b/components/admin/ticketList.tsx index 9b5c5b02..c2af80de 100644 --- a/components/admin/ticketList.tsx +++ b/components/admin/ticketList.tsx @@ -9,6 +9,7 @@ import TicketTransferModal from './modals/ticketTransferModal'; import { filterItems, filter, FilterSelector, FilterLabel } from './lists/filterable'; import { TicketRow } from './lists/ticketRow'; import { fetcher } from "@lib/fetchers"; +import ScanSuccessDialog from '@components/admin/scan/ScanSuccessDialog' export default function TicketList() { const searchParams = useSearchParams() @@ -98,6 +99,9 @@ export default function TicketList() {
{ nameChangeModalActive ? { setNameChangeModalActive(value)}} refreshFunction={()=> mutate("/api/admin/attendees")} ticket={activeTicket}/> : null } { ticketTransferModalActive ? { setTicketTransferModalActive(value);}} refreshFunction={()=> mutate("/api/admin/attendees")} ticket={activeTicket}/> : null } + { activeTicket ?
+ {setActiveTicket(false); setTimeout(() => mutate('/api/admin/attendees'),350)}}/>
: null } +
@@ -146,10 +150,10 @@ export default function TicketList() { - + Check-in? - { sortFieldToggler('signed_in') } + { sortFieldToggler('checkin_at') } diff --git a/components/admin/till.tsx b/components/admin/till.tsx index b5c0b044..46fd088d 100644 --- a/components/admin/till.tsx +++ b/components/admin/till.tsx @@ -1,19 +1,23 @@ 'use client' import React, { useState, useEffect } from 'react'; import { useFormStatus } from "react-dom" +import { BiAlarmAdd } from 'react-icons/bi'; import Cell from '../ticketing/Cell'; import { initialSelectedOptions, fullPassName, passes, individualTickets } from '../ticketing/pricingDefaults' -import { calculateTotalCost, passOrTicket, getBestCombination, itemsFromPassCombination, itemListToOptions, addToOptions, thingsToAccess} from '../ticketing/pricingUtilities' +import { calculateTotalCost, passOrTicket, getBestCombination, itemsFromPassCombination, itemListToOptions, addToOptions, mapItemsToAccessArray} from '../ticketing/pricingUtilities' import PassCards from '../ticketing/passes' import { OptionsTable } from '../ticketing/OptionsTable'; -import { useRouter } from 'next/navigation' +import ScanSuccessDialog from '@components/admin/scan/ScanSuccessDialog' +// import { useRouter } from 'next/navigation' import { deepCopy } from '@lib/useful' -import { getUnixTime } from 'date-fns'; +import { format, getUnixTime } from 'date-fns'; +import Pusher from 'pusher-js'; import symmetricDifference from 'set.prototype.symmetricdifference' import difference from 'set.prototype.difference' symmetricDifference.shim(); difference.shim(); +const pusher = new Pusher(process.env.NEXT_PUBLIC_PUSHER_APP_KEY, { cluster: 'eu', }); const Till = ({fullPassFunction,scrollToElement}:{fullPassFunction?:Function,scrollToElement?:Function}) => { const [selectedOptions, setSelectedOptions] = useState(deepCopy(initialSelectedOptions)); @@ -22,7 +26,21 @@ const Till = ({fullPassFunction,scrollToElement}:{fullPassFunction?:Function,scr const [totalCost, setTotalCost] = useState(0); const [packages,setPackages] = useState([]) const [packageCost, setPackageCost] = useState(0) - const router = useRouter() + const [till, setTill] = useState(null) + const [locked, setLocked] = useState(false) + const [channel, setChannel] = useState(null) + const [cardPayment, setCardPayment] = useState(null) + const [payments,setPayments] = useState([] as any[]) + const [ticket,setTicket] = useState(false as any) + const [selectedAccessArray, setSelectedAccessArray] = useState([]) + + const tillColours = { + TILL1: "bg-chillired-500 text-white", + TILL2: "bg-blue-400 text-white", + TILL3: "bg-gold-400 text-black", + } + + // const router = useRouter() const togglePriceModel = () => { setPriceModel(priceModel === "cost"? "studentCost" : "cost") @@ -40,6 +58,21 @@ const Till = ({fullPassFunction,scrollToElement}:{fullPassFunction?:Function,scr console.log(`Suggested packages: ${suggestedPackages.join(', ')} - £${suggestedCost}`) setPackageCost(suggestedCost) setPackages(suggestedPackages) + + //! Connor's suggestion + Adam's ammends + const combinedPackages = suggestedPackages.flatMap((pass)=>{ + return passes[pass]?.combination ? passes[pass].combination : [] + }) + const selectedStrings = Object.keys(selectedOptions).flatMap((day) => { + return Object.keys(selectedOptions[day]).flatMap((option) => { + return selectedOptions[day][option] ? `${day} ${option}` : [] + }) + }) + // append the access array from combinedPackages and selectedStrings, ignoring duplicates for now it is handled in function + const accessArray = mapItemsToAccessArray([...combinedPackages, ...selectedStrings]) + setSelectedAccessArray(accessArray) + console.log([...combinedPackages, ...selectedStrings]) + console.log(selectedAccessArray) } const setIndividualOption = (day,passType) => { @@ -52,6 +85,8 @@ const Till = ({fullPassFunction,scrollToElement}:{fullPassFunction?:Function,scr console.log("Options reset to: ", initialSelectedOptions) localStorage.removeItem("selectedOptions") setSelectedOptions(deepCopy(initialSelectedOptions)) + setLocked(false) + setCardPayment(false) } useEffect(() => { @@ -61,20 +96,60 @@ const Till = ({fullPassFunction,scrollToElement}:{fullPassFunction?:Function,scr useEffect(() => { if(fullPassFunction) { fullPassFunction(() => selectFullPass) } + setChannel(pusher.subscribe('card-payments')) + return () => { + pusher.unsubscribe('card-payments') + } },[]) - function CheckoutButton() { - const { pending } = useFormStatus(); - return ( - + useEffect(() => { + if(till) { + console.log("Connected to ",till) + channel.bind(till, function(data) { + console.log("pusher",data) + // const products = data.products.filter((product)=> product.till == till ).map((product) => product.name) + // const itemsInPassName = itemsFromPassCombination(products) as string[] + // console.log("products",products) + // console.log("itemsInPassName",itemsInPassName) + // setSelectedOptions(itemListToOptions(itemsInPassName,true)) + // setLocked(true) + setCardPayment(data) + }); + channel.bind("all", (payment) => { + setPayments((prevPayments) => [payment,...prevPayments]) + }) + } + },[till]) + + // const addPayment = + const changeTill = (till) => { + if(till) { channel.unbind(till) } + setTill(till) + } + + function CheckoutButtons({allgood}:{allgood?:boolean}) { + const positiveState = allgood ? true : false + const { pending } = useFormStatus(); + const activeButtonClass = `disabled:bg-gray-500 disabled:cursor-not-allowed text-white rounded-lg py-3 px-8 \ + ${ positiveState ? "bg-green-700 hover:bg-green-800" : "bg-chillired-600 hover: bg-chillired-400" } text-xl text-nowrap w-full max-w-72 md:w-auto` + return( +
+ + +
+ ); } async function checkout(formObject) { + setSelectedOptions(deepCopy(initialSelectedOptions)) setStudentDiscount(false) @@ -92,33 +167,43 @@ const Till = ({fullPassFunction,scrollToElement}:{fullPassFunction?:Function,scr } }) // Record the sale + console.log("selectedOptions",selectedOptions) const purchaseObj = { - 'email': formObject.get("inperson-name"), - 'full_name': formObject.get("inperson-email"), + 'email': formObject.get("inperson-email"), + 'full_name': formObject.get("inperson-name"), 'purchase_date': getUnixTime(new Date()) , 'line_items': line_items, - 'access': thingsToAccess(selectedOptions), - 'status': "paid_cash", + 'access': selectedAccessArray, //! use selectedAccessArray instead + 'status': `paid_${formObject.get('checkout-button')}`, 'student_ticket': studentDiscount, // // 'promo_code': None|{ // // 'code': "MLF", // // 'value': 500 // // }, // // 'meal_preferences': None|{}, - // // 'checkout_session': None|"cs_xxxxxx", - // // 'schedule': {}, + 'checkout_session': formObject.get("inperson-payment-ref") || formObject.get('checkout-button'), + 'checkout_amount': formObject.get("inperson-payment-amount") || packageCost*100, 'heading_message':"THANK YOU FOR YOUR PURCHASE", 'send_standard_ticket': true, - } - // const apiResponse = fetch(process.env.LAMBDA_CREATE_TICKET, { - // method: 'POST', - // headers: { - // 'Content-Type': 'application/json', - // }, - // body: JSON.stringify(purchaseObj), - // }) + } console.log("Purchase object",purchaseObj) - router.push("/admin/epos") //TODO This 100% needs a check for errors + const apiResponse = await fetch('/api/admin/epos', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(purchaseObj), + }) + const apiData = await apiResponse.json() + const apiAmmendedData = apiResponse.ok ? apiData : {...apiData, ticket_number: false } + console.log("apiData",apiAmmendedData) + setTicket(apiData.ticket_number) + if(!apiResponse.ok) { + alert(`PROBLEM, ${JSON.stringify(apiData)} ${apiResponse.status}`) + } + // router.push("/admin/epos") //TODO This 100% needs a check for errors + // Should reset the thing and unlock the form + setLocked(false) } const cellClasses = 'border border-gray-600 text-center py-2 px-3 md:py-2 md:px-4 '; @@ -127,8 +212,16 @@ const Till = ({fullPassFunction,scrollToElement}:{fullPassFunction?:Function,scr const inputClasses = "w-full mb-3 rounded-md border border-gray-600 p-2 bg-gray-50 text-gray-900 w-full\ focus:border-chillired-400 focus:ring-chillired-400" - return ( -
+ const displayAmount = cardPayment ? (cardPayment.amount/100).toFixed(2) : 0 + const correctAmount = cardPayment ? displayAmount == packageCost.toFixed(2) ? true : false : true + + return till ?( +
+ { ticket ?
{setTicket(false); console.log("Checkin")}}/>
: null } +

{till}

+ isSelected={studentDiscount} onSelect={togglePriceModel} studentDiscount={studentDiscount} locked={locked}/>
@@ -153,31 +247,58 @@ const Till = ({fullPassFunction,scrollToElement}:{fullPassFunction?:Function,scr selectedOptions={selectedOptions} clearOptions={clearOptions} setIndividualOption={setIndividualOption} - priceModel={priceModel} /> + priceModel={priceModel} + locked={locked} + />
+ { priceModel === 'studentCost' && totalCost && totalCost > 0 ? (
This is a student only ticket deal!
) : null } -
- { totalCost && totalCost > 0 ? ( +
+ {cardPayment ?
+

+ {correctAmount ? "PAYMENT SUCCESSFUL add details" : `ERROR cost is £${totalCost}`} - + Card Payment £{displayAmount} +

+
: null} + { cardPayment || (totalCost && totalCost > 0) ? ( <>

{packages.map((packageName) => `${packageName} ${passOrTicket(packageName)}`).join(', ').replace('Saturday Dinner Ticket','Dinner Ticket')}

-

{ totalCost - packageCost > 0 ? (£{totalCost}) : null } £{packageCost}

+

{ totalCost - packageCost > 0 ? (£{totalCost}) : null } £{packageCost}

+ { cardPayment ? : null } + { cardPayment ? : null } - +
) : "Select options in the table above to see the suggested packages" }
+ +
+
+

Recent Payments

+ {payments.slice(0,10).map((payment,index) => { return ( +
{ + cardPayment?.payment_ref && cardPayment.payment_ref == payment.payment_ref ? setCardPayment(false) : setCardPayment(payment) + }}> + + £{(payment.amount/100).toFixed(2)} : {payment.tills.join(',')} : {format(payment.created,'HH:mm:ss')} {payment.payment_ref} +
+ )})} + {/* {JSON.stringify(payments,null,2)} */}
+
@@ -194,7 +315,16 @@ const Till = ({fullPassFunction,scrollToElement}:{fullPassFunction?:Function,scr
: null }
- ) + ) :
+

Select Till

+
+ + + + {/* */} +
+ +
}; export default Till; diff --git a/components/ticketing/Cell.tsx b/components/ticketing/Cell.tsx index 5777cb3a..0bba5326 100644 --- a/components/ticketing/Cell.tsx +++ b/components/ticketing/Cell.tsx @@ -15,12 +15,13 @@ export interface ICellProps { studentDiscount: boolean, day?: string passType?: string + locked?: boolean } const Cell: React.FC = (props: ICellProps) => { const { option,isSelected,onSelect,studentDiscount, day,passType} = props; const { cost, studentCost, isAvailable} = option || { cost: 0, studentCost: 0, isAvailable: true }; - const checkBoxCss = isSelected? 'bg-chillired-600' : 'bg-gray-200'; + const checkBoxCss = props.locked ? "bg-gray-700" : isSelected? 'bg-chillired-600' : 'bg-gray-200'; const setValue = option && option.cost == 0 && option.studentCost == 0 ? {} : day && passType @@ -30,16 +31,17 @@ const Cell: React.FC = (props: ICellProps) => { : {one:passType, two: !isSelected} ; return ( <>{isAvailable ? ( - + onSelect(setValue.one,setValue.two)} // onChange={onSelect(day,passType)} - className={`${checkBoxCss} group relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-chillired-600 focus:ring-offset-2 order-2 sm:order-1`} + className={`${checkBoxCss} ${props.locked ? 'cursor-not-allowed': 'cursor-pointer'} group relative inline-flex h-6 w-11 flex-shrink-0 rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-chillired-600 focus:ring-offset-2 order-2 sm:order-1`} > diff --git a/components/ticketing/OptionsTable.tsx b/components/ticketing/OptionsTable.tsx index 6c73d744..ee349ee8 100644 --- a/components/ticketing/OptionsTable.tsx +++ b/components/ticketing/OptionsTable.tsx @@ -2,7 +2,7 @@ import { individualTickets, days, passTypes } from './pricingDefaults' import Cell from './Cell'; import { ICellProps } from './Cell'; -export const OptionsTable = ({headerClasses, toggleCellClasses, cellClasses, selectedOptions, clearOptions, setIndividualOption, priceModel}) => { +export const OptionsTable = ({headerClasses, toggleCellClasses, cellClasses, selectedOptions, clearOptions, setIndividualOption, priceModel, locked}) => { return ( @@ -31,6 +31,7 @@ export const OptionsTable = ({headerClasses, toggleCellClasses, cellClasses, sel studentDiscount: priceModel === "studentCost", day: day, passType: passType, + locked: locked, } as ICellProps return ( diff --git a/components/ticketing/PassCard.tsx b/components/ticketing/PassCard.tsx index cde3a63d..6aa0a1d5 100644 --- a/components/ticketing/PassCard.tsx +++ b/components/ticketing/PassCard.tsx @@ -1,17 +1,19 @@ import React from 'react'; import { fullPassName } from './pricingDefaults'; -export const PassCard = ({passName, clickFunction, pass, priceModel, hasASaving, selected, included, basic}: - {passName:string, clickFunction:any, pass:any, priceModel:string, hasASaving:boolean, selected:boolean, included?:boolean, basic?:boolean} +export const PassCard = ({passName, clickFunction, pass, priceModel, hasASaving, selected, included, basic, locked}: + {passName:string, clickFunction:any, pass:any, priceModel:string, hasASaving:boolean, selected:boolean, included?:boolean, basic?:boolean, locked?:boolean} ) => { const cardWidthClasses = passName === fullPassName ? 'col-span-full' : basic ? 'flex-col': 'md:flex-col' const passPadding = basic ? 'p-4 md:p-4' : 'p-6 md:p-10' const baseTextSize = basic ? 'text-sm md:text-sm' : 'text-xl md:text-base' const priceTextSize = basic ? 'text-sm md:text-sm leading-7' : 'text-4xl md:text-4xl' - const hoverClasses = selected ? "border-white cursor-pointer" : included ? 'hover:border-richblack-500 cursor-not-allowed' : 'hover:border-white cursor-pointer' + const hoverClasses = locked ? 'hover:border-richblack-500 cursor-not-allowed' : + selected ? "border-white cursor-pointer" : + included ? 'hover:border-richblack-500 cursor-not-allowed' : 'hover:border-white cursor-pointer' return (
{console.log('locked')} : clickFunction} key={passName} title={passName} className={`relative flex flex-col justify-between rounded-3xl bg-richblack-600 ${passPadding} shadow-xl diff --git a/components/ticketing/PricingTable.tsx b/components/ticketing/PricingTable.tsx index 43dd5cd8..802646ee 100644 --- a/components/ticketing/PricingTable.tsx +++ b/components/ticketing/PricingTable.tsx @@ -117,6 +117,7 @@ const PricingTable = ({fullPassFunction,scrollToElement}:{fullPassFunction?:Func clearOptions={clearOptions} setIndividualOption={setIndividualOption} priceModel={priceModel} + locked={false} />
diff --git a/components/ticketing/passes.tsx b/components/ticketing/passes.tsx index 4a75574e..b692916e 100644 --- a/components/ticketing/passes.tsx +++ b/components/ticketing/passes.tsx @@ -5,14 +5,16 @@ import { itemsFromPassCombination, itemListToOptions, addToOptions, passInCombin import type { PartialSelectedOptions } from './pricingTypes' // export default function PassCards({setDayPass,setTypePass,setDinnerPass,priceModel,scrollToElement,selectFullPass,selected,shouldScroll}) { -export default function PassCards({currentSelectedOptions, setSelectedOptions, priceModel,scrollToElement,selected,shouldScroll, basic} : +export default function PassCards({currentSelectedOptions, setSelectedOptions, priceModel,scrollToElement,selected,shouldScroll, basic, locked} : { currentSelectedOptions:PartialSelectedOptions, setSelectedOptions:any, priceModel: string, scrollToElement:any, selected:any, shouldScroll:boolean, - basic?:boolean} + basic?:boolean + locked?:boolean + } ) { const clickFunctionFromPassName = (passName:string,setTo:boolean) => { @@ -38,7 +40,7 @@ export default function PassCards({currentSelectedOptions, setSelectedOptions, p
- { + { clickFunctionFromPassName(fullPassName,!selected.includes(fullPassName)) }} pass={passes[fullPassName]} priceModel={priceModel} hasASaving={true} selected={selected.includes(fullPassName)}> @@ -57,6 +59,7 @@ export default function PassCards({currentSelectedOptions, setSelectedOptions, p clickFunction={clickFunction} priceModel={priceModel} hasASaving={hasSaving} selected={selected.includes(passName)} included={included} + locked={locked} />) })} diff --git a/components/ticketing/pricingUtilities.tsx b/components/ticketing/pricingUtilities.tsx index 6c84ee6d..4c87f2bd 100644 --- a/components/ticketing/pricingUtilities.tsx +++ b/components/ticketing/pricingUtilities.tsx @@ -166,11 +166,19 @@ const priceIds = (student = false) => { } const thingsToAccess = (selectedOptions:any) => { - return Object.keys(selectedOptions).flatMap((day) => { - return Object.keys(selectedOptions[day]).flatMap((pass) => { - return selectedOptions[day][pass] ? 1 : 0 - }) - }) + console.log("Selected Options",selectedOptions) + const access_order = [["Friday","Party"],["Saturday","Classes"],["Saturday","Dinner"],["Saturday","Party"],["Sunday","Classes"],["Sunday","Party"]] + const access = access_order.map((access) => { return selectedOptions[access[0]][access[1]] ? 1 : 0 }) + console.log(access) + return access +} + +const mapItemsToAccessArray = (itemsList: string[]) => { + const accessOrder = ["Friday Party", "Saturday Classes", "Saturday Dinner", "Saturday Party", "Sunday Classes", "Sunday Party"]; + // create set which will remove duplicates + const uniqueItems = new Set(itemsList); + //for each item in the access if uniqueItems has it then put 1, if not but 0 + return accessOrder.map(item => uniqueItems.has(item) ? 1 : 0); } const passInCombination = (pass:Pass, combinations: string[]) => { @@ -179,4 +187,4 @@ const passInCombination = (pass:Pass, combinations: string[]) => { return subSet.isSubsetOf(superSet) } -export { calculateTotalCost, passOrTicket, optionsToPassArray, availableOptionsForDay, isAllDayOptions, isAllPassOptions, priceForPassCombination, itemsFromPassCombination, priceForIndividualItems, itemsNotCovered, getBestCombination, priceIds, thingsToAccess, passInCombination} +export { calculateTotalCost, passOrTicket, optionsToPassArray, availableOptionsForDay, isAllDayOptions, isAllPassOptions, priceForPassCombination, itemsFromPassCombination, priceForIndividualItems, itemsNotCovered, getBestCombination, priceIds, thingsToAccess, mapItemsToAccessArray, passInCombination} diff --git a/docs/Ticket Thinking.md b/docs/Ticket Thinking.md new file mode 100644 index 00000000..0f43190e --- /dev/null +++ b/docs/Ticket Thinking.md @@ -0,0 +1,29 @@ +A. Staff Pass +B. Artist Pass + +1. Full [1,1,1,1,1,1] +2. Saturday [0,1,1,0,0,0] +3. Sunday [0,0,0,0,1,1] +4. Parties [1,1,0,0,1,0] +5. Classes [0,0,1,0,0,1] +6. Friday Ticket [1,0,0,0,0,0] +7. Weird Combos with 4 boxes, cross out not entitled [0,X,X,0,X,X] + +Friday X on the hand if allowed in and doesnt have PP or FP ? +Dinner Ticket (Digital?) + +Friday Night +FP PP FT + +Saturday Classes +FP SAP SACT, CP + +Saturday Party +FP, PP, SAP, SAPT + +Sunday Classes +FP, CP SUP, SCT + +Sunday Party +FP, PP, SUP, SUPT + diff --git a/docs/generic-payment.json b/docs/generic-payment.json new file mode 100644 index 00000000..b9aa3232 --- /dev/null +++ b/docs/generic-payment.json @@ -0,0 +1,22 @@ +Generic Purchase + +{ + "organizationUuid": "1e707f64-dfa6-11ec-935d-b1ee1980c58a", + "messageUuid": "25957000-a65b-11ef-aa5b-2c67274746b4", + "eventName": "PurchaseCreated", + "messageId": "3e1a73d1-47f4-58ec-aa5b-2c67274746b4", + "payload": "{\"purchaseUuid\":\"2f7ca0f2-a65b-11ef-8fc9-a2dddcd0327f\",\"source\":\"POS\",\"userUuid\":\"1e7586ee-dfa6-11ec-8e4e-fdd8dffdd367\",\"currency\":\"GBP\",\"country\":\"GB\",\"amount\":150,\"vatAmount\":0,\"timestamp\":1732009587434,\"created\":\"2024-11-19T09:46:27.434+0000\",\"gpsCoordinates\":{\"longitude\":-2.9563565203085593,\"latitude\":53.393768310546875,\"accuracyMeters\":7.6295230152477505},\"purchaseNumber\":4153,\"userDisplayName\":\"Karen Graham-Dosanjh\",\"udid\":\"80fe0e78ef016fce55a0edc5779e41f90fb10e36\", \"organizationUuid\":\"1e707f64-dfa6-11ec-935d-b1ee1980c58a\",\"products\":[{\"id\":\"0\",\"productUuid\":\"3f90d7b0-a656-11ef-a258-b373a07aa755\",\"variantUuid\":\"3f90d7b1-a656-11ef-a258-b373a07aa755\",\"name\":\"MLF Door Sale\",\"variantName\":\"\",\"unitPrice\":150,\"costPrice\":0,\"quantity\":\"1\",\"vatPercentage\":0.0,\"taxRates\":[{\"percentage\":0}],\"taxExempt\":false,\"fromLocationUuid\": \"9334df91-dfa7-11ec-9b1e-0d236056669f\",\"toLocationUuid\":\"9334df96-dfa7-11ec-8376-b982eef3c300\",\"autoGenerated\":false,\"type\":\"PRODUCT\",\"details\":{}}],\"discounts\":[],\"payments\":[{\"uuid\":\"24c05c2c-a65b-11ef-879d-80254a53cd4d\",\"amount\":150,\"type\":\"IZETTLE_CARD\",\"createdAt\":\"2024-11-19T09:46:26.036+0000\",\"attributes\":{\"cardHolderVerificationMethod\":\"None\",\"maskedPan\":\"537410**********\",\"acquirerMID\":\"61905188\",\"cardPaymentEntryMode\":\"CONTACTLESS_EMV\",\"referenceNumber\":\"I2WYAPFYRI\",\"authorizationCode\":\"876066\",\"cardType\":\"MASTERCARD\",\"terminalVerificationResults\":\"0000008001\",\"applicationIdentifier\":\"A0000000041010\",\"applicationName\":\"Debit Mastercard\",\"mxPaymentMethodCode\":28}}],\"references\":{\"shoppingCartUuid\":\"377EC540-9C84-11EF-AE43-A52DCB276B85\",\"checkoutUUID\":\"2f7ca0f2-a65b-11ef-8ec8-a3dcddd1337e\"},\"taxationMode\":\"INCLUSIVE\",\"taxationType\":\"VAT\",\"customAmountSale\":false}", + "timestamp": "2024-11-19T09:46:27.456Z" +} + +Till 1 Puchase + +{ + "organizationUuid": "1e707f64-dfa6-11ec-935d-b1ee1980c58a", + "messageUuid": "25957000-a65b-11ef-aa5b-2c67274746b4", + "eventName": "PurchaseCreated", + "messageId": "3e1a73d1-47f4-58ec-aa5b-2c67274746b4", + "payload": "{\"purchaseUuid\":\"231c59be-a666-11ef-8fc9-a2dddcd0327f\",\"source\":\"POS\",\"userUuid\":\"1e7586ee-dfa6-11ec-8e4e-fdd8dffdd367\",\"currency\":\"GBP\",\"country\":\"GB\",\"amount\":150,\"vatAmount\":0,\"timestamp\":1732014288742,\"created\":\"2024-11-19T11:04:48.742+0000\",\"gpsCoordinates\":{\"longitude\":-2.9563565203085593,\"latitude\":53.393768310546875,\"accuracyMeters\":7.6295230152477505},\"purchaseNumber\":4155,\"userDisplayName\":\"Karen Graham-Dosanjh\",\"udid\":\"80fe0e78ef016fce55a0edc5779e41f90fb10e36\",\"organizationUuid\":\"1e707f64-dfa6-11ec-935d-b1ee1980c58a\",\"products\":[{\"id\":\"0\",\"productUuid\":\"3f90d7b0-a656-11ef-a258-b373a07aa755\",\"variantUuid\":\"3f90d7b1-a656-11ef-a258-b373a07aa755\",\"name\":\"MLF Till 1\",\"variantName\":\"\",\"sku\":\"TILL1\",\"unitPrice\":150,\"costPrice\":0,\"quantity\":\"1\",\"vatPercentage\":0,\"taxRates\":[{\"percentage\":0}],\"taxExempt\":false,\"fromLocationUuid\":\"9334df91-dfa7-11ec-9b1e-0d236056669f\",\"toLocationUuid\":\"9334df96-dfa7-11ec-8376-b982eef3c300\",\"autoGenerated\":false,\"type\":\"PRODUCT\",\"details\":{}}],\"discounts\":[],\"payments\":[{\"uuid\":\"17025ad1-a666-11ef-ba41-c0684df91b15\",\"amount\":150,\"type\":\"IZETTLE_CARD\",\"createdAt\":\"2024-11-19T11:04:47.454+0000\",\"attributes\":{\"cardHolderVerificationMethod\":\"None\",\"maskedPan\":\"537410**********\",\"acquirerMID\":\"61905188\",\"cardPaymentEntryMode\":\"CONTACTLESS_EMV\",\"referenceNumber\":\"GSIK7RB5EY\",\"authorizationCode\":\"309802\",\"cardType\":\"MASTERCARD\",\"terminalVerificationResults\":\"0000008001\",\"applicationIdentifier\":\"A0000000041010\",\"applicationName\":\"Debit Mastercard\",\"mxPaymentMethodCode\":28}}],\"references\":{\"shoppingCartUuid\":\"35878F8E-A65B-11EF-8EC8-A3DCDDD1337E\",\"checkoutUUID\":\"231c59be-a666-11ef-8ec8-a3dcddd1337e\"},\"taxationMode\":\"INCLUSIVE\",\"taxationType\":\"VAT\",\"customAmountSale\":false}", + "timestamp": "2024-11-19T09:46:27.456Z" +} + diff --git a/functions/checkout_complete_inperson/lambda_function.py b/functions/checkout_complete_inperson/lambda_function.py new file mode 100644 index 00000000..154a58c0 --- /dev/null +++ b/functions/checkout_complete_inperson/lambda_function.py @@ -0,0 +1,148 @@ +import os +import logging +import time +import json +from random import randint + +import boto3 +from boto3.dynamodb.conditions import Key +from shared.DecimalEncoder import DecimalEncoder +from shared.parser import parse_event, validate_event, validate_line_items + +#ENV +attendees_table_name = os.environ.get("ATTENDEES_TABLE_NAME") +event_table_name = os.environ.get("EVENT_TABLE_NAME") +send_email_lambda = os.environ.get("SEND_EMAIL_LAMBDA") + +logger = logging.getLogger() +logger.setLevel("INFO") + +db = boto3.resource('dynamodb') +attendees_table = db.Table(attendees_table_name) +event_table = db.Table(event_table_name) + +lambda_client = boto3.client('lambda') + +def record_fail(event, reason): + ''' + Records details of a fail + ''' + logger.info("Recording failed upgrade attempt.") + current_time = int(time.time()) + pk = f"CHECKOUTFAIL#{event.get('checkout_session', 'unknown')}" + sk = f"ERROR#{current_time}" + + backup_entry = { + 'PK': pk, + 'SK': sk, + 'timestamp': current_time, + 'email': event.get('email', 'unknown'), + 'full_name': event.get('full_name', 'unknown'), + 'reason': reason, + 'event_details': json.dumps(event, cls=DecimalEncoder) + } + + try: + event_table.put_item(Item=backup_entry) + logger.info("Failure recorded successfully.") + except boto3.exceptions.Boto3Error as e: + logger.error("Error recording failed attempt: %s", str(e)) + +def process_line_items(line_items): + return ''.join([", "+i['description'] for i in line_items])[2:], sum([i['amount_total'] for i in line_items]) + +# Generate a random ticket number +#! this bit could be done quicker +def get_ticket_number(email, student_ticket): + search = True + # generate a new ticket number if it is already used in the table + while search: + ticketnumber = str(randint(1000000000, 9999999999)) if student_ticket == False else str(55)+str(randint(1000000000, 9999999999))[:-2] + response = attendees_table.query(KeyConditionExpression=Key('ticket_number').eq(ticketnumber) & Key('email').eq(email)) + if response['Count'] == 0: + search = False + logger.info("ticket number generated: "+str(ticketnumber)) + return ticketnumber + +def lambda_handler(event, context): + try: + event = parse_event(event) + event = validate_event(event, ['email', 'status', 'purchase_date']) + if event.get('line_items', None): + validate_line_items(event['line_items']) + except (ValueError, TypeError, KeyError) as e: + logger.error("Event validation failed: %s", str(e)) + logger.error(event) + return { + 'statusCode': 400, + 'body': f'Invalid input: {str(e)}' + } + + logger.info("#### CREATING TICKET ####") + logger.info("Event received: %s", json.dumps(event, indent=2, cls=DecimalEncoder)) + + full_name = event.get('full_name', 'unknown') + email = event.get('email', 'unknown') + phone = event.get('phone', None) + line_items = event.get('line_items') + access = event.get('access', [0,0,0,0,0,0]) + status = event.get('status', 'unknown') + purchase_date = event.get('purchase_date', int(time.time())) + student_ticket = event.get('student_ticket', False) + checkout_session = event.get('checkout_session', 'unknown') + + logger.info("Getting ticket number") + ticket_number = get_ticket_number(email, student_ticket) + + item = { + 'ticket_number': str(ticket_number), + 'email': email, + 'full_name': full_name, + 'phone': phone, + 'active': True, + 'purchase_date':purchase_date, + 'line_items': line_items, + 'access': access, + 'ticket_used': False, + 'status': status, + 'student_ticket': student_ticket, + 'checkout_session': checkout_session, + } + + optional = ['schedule', 'meal_preferences', 'promo_code', 'history'] + + for key in optional: + if key in event: + item[key] = event.get(key) + + attendees_table.put_item(Item=item) + + try: + if event.get('send_standard_ticket', True): + # send the email with these details + logger.info("Invoking send_email lambda") + response = lambda_client.invoke( + FunctionName=send_email_lambda, + InvocationType='Event', + Payload=json.dumps({ + 'email_type':"standard_ticket", + 'name':full_name, + 'email':email, + 'ticket_number':ticket_number, + 'line_items':line_items, + 'heading_message': event['heading_message'] if 'heading_message' in event else "THANK YOU FOR YOUR PURCHASE!" + }, cls=DecimalEncoder), + ) + logger.info(response) + except boto3.exceptions.Boto3Error as e: + logger.error("Failed to invoke send_email lambda: %s", str(e)) + record_fail(event, f"Failed to send confirmation email: {str(e)}") + + return { + 'statusCode': 200, + 'body': json.dumps({ + 'message': "Ticket created successfully", + 'ticket_number': ticket_number, + 'email': email + }) + } \ No newline at end of file diff --git a/functions/serverless.yml b/functions/serverless.yml index 2b1f5ac8..4c99efd7 100644 --- a/functions/serverless.yml +++ b/functions/serverless.yml @@ -621,11 +621,33 @@ functions: STAGE_NAME: ${sls:stage} EVENT_TABLE_NAME: ${param:eventTableName} ATTENDEES_TABLE_NAME: ${param:attendeesTableName} - SEND_EMAIL_LAMBDA: "${sls:stage}-meal_seating" + SEND_EMAIL_LAMBDA: "${sls:stage}-meal_seating" # delete this, not used and wrong lambda name silly events: - httpApi: path: /meal/seating method: get - httpApi: path: /meal/seating - method: post \ No newline at end of file + method: post + +#-------------------- + + CheckoutCompleteInperson: + runtime: python3.11 + handler: checkout_complete_inperson/lambda_function.lambda_handler + name: "${sls:stage}-checkout_complete_inperson" + package: + patterns: + - '!**/**' + - "checkout_complete_inperson/**" + - "shared/**" + environment: + STAGE_NAME: ${sls:stage} + EVENT_TABLE_NAME: ${param:eventTableName} + ATTENDEES_TABLE_NAME: ${param:attendeesTableName} + SEND_EMAIL_LAMBDA: "${sls:stage}-send_email" + events: + + - httpApi: + path: /checkout_complete_inperson + method: post \ No newline at end of file diff --git a/package.json b/package.json index 08217896..62819b33 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,8 @@ "next-themes": "^0.3.0", "npm": "^10.8.2", "power-set": "^1.1.8", + "pusher": "^5.2.0", + "pusher-js": "^8.4.0-rc2", "qr-scanner": "^1.4.2", "react": "^18.3.1", "react-countdown": "^2.3.5", diff --git a/public/test.html b/public/test.html new file mode 100644 index 00000000..eee383d5 --- /dev/null +++ b/public/test.html @@ -0,0 +1,29 @@ + + + Pusher Test + + + + +

Pusher Test

+

+ Try publishing an event to channel my-channel + with event name my-event. +

+

+

+ \ No newline at end of file diff --git a/public/ticket.svg b/public/ticket.svg new file mode 100644 index 00000000..acae75b6 --- /dev/null +++ b/public/ticket.svg @@ -0,0 +1,14 @@ + + + + + + + + + + \ No newline at end of file diff --git a/public/wristband.svg b/public/wristband.svg new file mode 100644 index 00000000..45f6ec76 --- /dev/null +++ b/public/wristband.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index f832d6a6..97918434 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6749,6 +6749,11 @@ is-async-function@^2.0.0: dependencies: has-tostringtag "^1.0.0" +is-base64@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-base64/-/is-base64-1.1.0.tgz#8ce1d719895030a457c59a7dcaf39b66d99d56b4" + integrity sha512-Nlhg7Z2dVC4/PTvIFkgVVNvPHSO2eR/Yd0XzhGiXCXEvWnptXlXa/clQ8aePPiMuxEGcWfzWbGw2Fe3d+Y3v1g== + is-bigint@^1.0.1: version "1.0.4" resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3" @@ -9649,6 +9654,25 @@ punycode@^2.1.0: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== +pusher-js@^8.4.0-rc2: + version "8.4.0-rc2" + resolved "https://registry.yarnpkg.com/pusher-js/-/pusher-js-8.4.0-rc2.tgz#44f33bfe5bc89f741d82601125d6095bde28261b" + integrity sha512-d87GjOEEl9QgO5BWmViSqW0LOzPvybvX6WA9zLUstNdB57jVJuR27zHkRnrav2a3+zAMlHbP2Og8wug+rG8T+g== + dependencies: + tweetnacl "^1.0.3" + +pusher@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/pusher/-/pusher-5.2.0.tgz#cc208d15000f8d4d8b485acb844d7557adb2cf20" + integrity sha512-F6LNiZyJsIkoHLz+YurjKZ1HH8V1/cMggn4k97kihjP3uTvm0P4mZzSFeHOWIy+PlJ2VInJBhUFJBYLsFR5cjg== + dependencies: + "@types/node-fetch" "^2.5.7" + abort-controller "^3.0.0" + is-base64 "^1.1.0" + node-fetch "^2.6.1" + tweetnacl "^1.0.0" + tweetnacl-util "^0.15.0" + qr-scanner@^1.4.2: version "1.4.2" resolved "https://registry.yarnpkg.com/qr-scanner/-/qr-scanner-1.4.2.tgz#bc4fb88022a8c9be95c49527a1c8fb8724b47dc4" @@ -11308,6 +11332,16 @@ tunnel-agent@^0.6.0: dependencies: safe-buffer "^5.0.1" +tweetnacl-util@^0.15.0: + version "0.15.1" + resolved "https://registry.yarnpkg.com/tweetnacl-util/-/tweetnacl-util-0.15.1.tgz#b80fcdb5c97bcc508be18c44a4be50f022eea00b" + integrity sha512-RKJBIj8lySrShN4w6i/BonWp2Z/uxwC3h4y7xsRrpP59ZboCd0GpEVsOnMDYLMmKBpYhb5TgHzZXy7wTfYFBRw== + +tweetnacl@^1.0.0, tweetnacl@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-1.0.3.tgz#ac0af71680458d8a6378d0d0d050ab1407d35596" + integrity sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw== + tunnel@^0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.6.tgz#72f1314b34a5b192db012324df2cc587ca47f92c" @@ -11443,6 +11477,11 @@ undici-types@~6.19.2: resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02" integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== +undici-types@~6.19.2: + version "6.19.6" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.6.tgz#e218c3df0987f4c0e0008ca00d6b6472d9b89b36" + integrity sha512-e/vggGopEfTKSvj4ihnOLTsqhrKRN3LeO6qSN/GxohhuRv8qH9bNQ4B8W7e/vFL+0XTnmHPB4/kegunZGA4Org== + unicode-canonical-property-names-ecmascript@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc"