'Guest list (free)', 'cash' => 'Cash at the door', 'door_card' => 'Card at the door']; add_filter('gatecraft_admin_routes', fn(array $r) => array_merge([ ['GET', '/admin/orders/new', 'a_manual_new', 'orders'], ['POST', '/admin/orders/new', 'a_manual_create', 'orders'], ], $r)); /** The tier a staff booking uses for a category: what is on sale now, else the last price that was on sale, else any. */ function manual_tier(array $cat): ?array { $n = now(); $tiers = array_values(array_filter($cat['tiers'], fn($t) => (int)$t['is_active'] === 1)) ?: $cat['tiers']; foreach ($tiers as $t) if (($t['sale_start'] === null || $t['sale_start'] <= $n) && ($t['sale_end'] === null || $t['sale_end'] >= $n)) return $t; $past = array_filter($tiers, fn($t) => $t['sale_end'] !== null && $t['sale_end'] < $n); if ($past) { usort($past, fn($a, $b) => strcmp((string)$b['sale_end'], (string)$a['sale_end'])); return $past[0]; } return $tiers[0] ?? null; } /** What is left per category, ignoring sale windows: [category id => left] plus 'event' => left or null. */ function manual_left(array $ev): array { $held = held_counts((int)$ev['id']); $out = []; foreach (categories_with_tiers((int)$ev['id']) as $c) $out[(int)$c['id']] = max(0, (int)$c['capacity'] - ($held['cat'][$c['id']] ?? 0)); $cap = event_cap($ev); $out['event'] = $cap === null ? null : max(0, $cap - $held['total']); return $out; } /** * Create a confirmed booking for staff. $in: event_id, qty [category => n], kind, name, email, phone, note, * send_email, checkin_now, over_capacity. Returns the order id. */ function create_manual_order(array $in, int $actorId): int { $kind = (string)($in['kind'] ?? ''); if (!isset(MANUAL_KINDS[$kind])) fail('Choose the type of booking.'); $name = mb_substr(trim((string)($in['name'] ?? '')), 0, 120); if ($name === '' || preg_match('/[\x00-\x1f<>]/', $name)) fail('Enter the guest\'s name.'); $email = strtolower(trim((string)($in['email'] ?? ''))); if ($email !== '' && (strlen($email) > 190 || !filter_var($email, FILTER_VALIDATE_EMAIL))) fail('Enter a valid email address, or leave it empty.'); $phone = trim((string)($in['phone'] ?? '')); if ($phone !== '' && !preg_match('/^[0-9+()\-\s]{6,40}$/', $phone)) fail('Enter a valid phone number, or leave it empty.'); $note = mb_substr(trim((string)($in['note'] ?? '')), 0, 500); $qty = array_filter(array_map('intval', (array)($in['qty'] ?? [])), fn($q) => $q > 0); if (!$qty) fail('Add at least one ticket.'); if (array_sum($qty) > 500) fail('A single booking can have up to 500 tickets.'); $sendEmail = !empty($in['send_email']) && $email !== ''; $checkin = !empty($in['checkin_now']); $over = !empty($in['over_capacity']); $eventId = (int)($in['event_id'] ?? 0); $orderId = tx(function () use ($eventId, $qty, $kind, $name, $email, $phone, $note, $over, $actorId) { $ev = q1('SELECT * FROM {events} WHERE id = ? FOR UPDATE', [$eventId]); if (!$ev) fail('Choose an event.'); if (!in_array($ev['status'], ['draft', 'published'], true)) fail('This event is ' . status_label($ev['status']) . '. Bookings cannot be added.'); $cats = []; foreach (categories_with_tiers($eventId) as $c) $cats[(int)$c['id']] = $c; $left = manual_left($ev); $lines = []; $count = 0; $problems = []; foreach ($qty as $catId => $q) { $c = $cats[(int)$catId] ?? null; if (!$c) fail('One of the ticket categories does not belong to this event.'); $t = manual_tier($c); if (!$t) fail($c['name'] . ' has no price yet. Set one on the Tickets and prices tab.'); if ($q > $left[(int)$catId]) $problems[] = $c['name'] . ': ' . plural($left[(int)$catId], 'ticket') . ' left'; $lines[] = ['category_id' => (int)$catId, 'tier_id' => (int)$t['id'], 'qty' => $q, 'unit_price' => $kind === 'comp' ? 0 : (int)$t['price']]; $count += $q; } if ($left['event'] !== null && $count > $left['event']) $problems[] = 'the event has ' . plural($left['event'], 'place') . ' left'; if ($problems && !$over) fail('Not enough capacity (' . implode('; ', $problems) . '). Tick "Allow over capacity" to add the booking anyway.'); $subtotal = array_sum(array_map(fn($l) => $l['qty'] * $l['unit_price'], $lines)); $tax = tax_compute($subtotal); q('INSERT INTO {orders} (reference, public_token, event_id, buyer_name, buyer_email, buyer_phone, subtotal, discount_amount, total, currency, status, payment_method, pay_option, decision_note, created_by, tax_mode, tax_rate, tax_label, tax_amount, language, paid_at, created_at) VALUES (?,?,?,?,?,?,?,0,?,?,\'paid\',?,?,?,?,?,?,?,?,?,?,?)', [ new_reference(), bin2hex(random_bytes(32)), $eventId, $name, $email, $phone ?: null, $subtotal, $tax['total'], cfg('currency'), $kind, MANUAL_KINDS[$kind], $note ?: null, $actorId, $tax['mode'] === 'none' ? null : $tax['mode'], $tax['rate'], $tax['label'], $tax['amount'], $ev['language'] ?: current_language(), now(), now(), ]); $id = (int)db_insert_id(); foreach ($lines as $l) q('INSERT INTO {order_items} (order_id, category_id, tier_id, qty, unit_price) VALUES (?,?,?,?,?)', [$id, $l['category_id'], $l['tier_id'], $l['qty'], $l['unit_price']]); issue_tickets($id); if ($kind !== 'comp' && $tax['total'] > 0) do_action('gatecraft_order_paid_tx', $id); return ['id' => $id, 'over' => (bool)$problems]; }); $id = $orderId['id']; if ($checkin) { q('UPDATE {tickets} SET checked_in_at = ?, checked_in_by = ? WHERE order_id = ? AND checked_in_at IS NULL', [now(), $actorId, $id]); foreach (q('SELECT id FROM {tickets} WHERE order_id = ?', [$id])->fetchAll() as $t) q('INSERT INTO {checkin_scans} (event_id, ticket_id, user_id, result, created_at) VALUES (?,?,?,?,?)', [$eventId, $t['id'], $actorId, 'ok', now()]); } audit('order.manual', 'order', $id, ['kind' => $kind, 'tickets' => array_sum($qty), 'over_capacity' => $orderId['over'], 'checked_in' => $checkin]); if ($email !== '') contact_refresh($email); do_action('gatecraft_order_paid', $id); if ($sendEmail) email_order($id, 'tickets'); if ($orderId['over']) notify_staff('payment', 'Booking over capacity: ' . qv('SELECT reference FROM {orders} WHERE id = ?', [$id]), 'A staff booking was added beyond the capacity of this event.', '/admin/orders/' . $id); check_capacity_alert($eventId); return $id; } function a_manual_new(): void { $eventId = get_int('event'); $events = q("SELECT id, title, starts_at FROM {events} WHERE status IN ('draft','published') AND (ends_at IS NULL OR ends_at > ?) AND starts_at > ? ORDER BY starts_at LIMIT 200", [ldate('Y-m-d H:i:s', time() - 86400), ldate('Y-m-d H:i:s', time() - 3 * 86400)])->fetchAll(); $ev = $eventId ? q1("SELECT * FROM {events} WHERE id = ? AND status IN ('draft','published')", [$eventId]) : null; if (!$ev && count($events) === 1) $ev = q1('SELECT * FROM {events} WHERE id = ?', [$events[0]['id']]); $cats = []; if ($ev) { $left = manual_left($ev); foreach (categories_with_tiers((int)$ev['id']) as $c) { $t = manual_tier($c); if (!$t) continue; $cats[] = ['id' => (int)$c['id'], 'name' => $c['name'], 'price' => (int)$t['price'], 'left' => $left[(int)$c['id']], 'active' => (int)$c['is_active'] === 1]; } $eventLeft = $left['event']; } render('admin/manual_booking', ['title' => 'New booking', 'nav' => 'orders', 'events' => $events, 'ev' => $ev, 'cats' => $cats, 'eventLeft' => $eventLeft ?? null, 'kind' => isset(MANUAL_KINDS[get_str('kind')]) ? get_str('kind') : 'comp']); } function a_manual_create(): void { $eventId = post_int('event_id'); set_back('/admin/orders/new?event=' . $eventId . '&kind=' . rawurlencode((string)post('kind'))); gatecraft_remember_input('/admin/orders/new'); $qty = []; foreach (post_list('qty') as $k => $v) if (ctype_digit((string)$k) && is_string($v) && ctype_digit($v)) $qty[(int)$k] = (int)$v; $id = create_manual_order([ 'event_id' => $eventId, 'qty' => $qty, 'kind' => (string)post('kind'), 'name' => post_text('name'), 'email' => post_text('email'), 'phone' => post_text('phone'), 'note' => post_text('note'), 'send_email' => post_bool('send_email'), 'checkin_now' => post_bool('checkin_now'), 'over_capacity' => post_bool('over_capacity'), ], (int)current_user()['id']); $o = get_order($id); $n = (int)qv('SELECT COUNT(*) FROM {tickets} WHERE order_id = ?', [$id]); $msg = MANUAL_KINDS[$o['payment_method']] . ': ' . plural($n, 'ticket') . ' for ' . $o['buyer_name'] . '.'; if (post_bool('checkin_now')) $msg .= ' Checked in.'; elseif (post_bool('send_email') && $o['buyer_email'] !== '') $msg .= ' Tickets emailed to ' . $o['buyer_email'] . '.'; flash('success', $msg); redirect(post_bool('another') ? '/admin/orders/new?event=' . $eventId . '&kind=' . rawurlencode($o['payment_method']) : '/admin/orders/' . $id); }