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}
>
) : "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"
| |