Files
MokoSuiteEvent/source/packages/plg_system_mokosuiteevent/src/Helper/TicketHelper.php
T

172 lines
5.0 KiB
PHP

<?php
namespace Moko\Plugin\System\MokoSuiteEvent\Helper;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\Database\DatabaseInterface;
/**
* Ticket management helper — types, purchases, QR codes, availability.
*/
class TicketHelper
{
/**
* Create a ticket type for an event.
*/
public static function createType(int $eventId, string $name, float $price, int $quantity): object
{
$db = Factory::getContainer()->get(DatabaseInterface::class);
// Get next ordering
$db->setQuery($db->getQuery(true)
->select('COALESCE(MAX(' . $db->quoteName('ordering') . '), 0) + 1')
->from($db->quoteName('#__mokosuiteevent_ticket_types'))
->where($db->quoteName('event_id') . ' = ' . (int) $eventId));
$ordering = (int) $db->loadResult();
$type = (object) [
'event_id' => (int) $eventId,
'name' => $name,
'price' => $price,
'quantity' => (int) $quantity,
'sold' => 0,
'published' => 1,
'ordering' => $ordering,
];
$db->insertObject('#__mokosuiteevent_ticket_types', $type, 'id');
return $type;
}
/**
* Purchase tickets with row-level locking to prevent oversell.
*/
public static function purchase(int $ticketTypeId, int $contactId, int $qty, string $attendeeName = ''): object
{
$db = Factory::getContainer()->get(DatabaseInterface::class);
// Lock the ticket type row to prevent oversell
$db->setQuery(
'SELECT * FROM ' . $db->quoteName('#__mokosuiteevent_ticket_types')
. ' WHERE ' . $db->quoteName('id') . ' = ' . (int) $ticketTypeId
. ' FOR UPDATE'
);
$type = $db->loadObject();
if (!$type) {
throw new \RuntimeException('Ticket type not found');
}
$remaining = (int) $type->quantity - (int) $type->sold;
if ($qty > $remaining) {
throw new \RuntimeException('Insufficient tickets: ' . $remaining . ' remaining');
}
if ($qty > (int) $type->max_per_order) {
throw new \RuntimeException('Maximum ' . $type->max_per_order . ' tickets per order');
}
$now = Factory::getDate()->toSql();
$totalPrice = $type->price * $qty;
// Create registration
$registration = (object) [
'event_id' => (int) $type->event_id,
'contact_id' => (int) $contactId,
'name' => $attendeeName,
'email' => '',
'status' => 'confirmed',
'total_amount' => $totalPrice,
'payment_status' => $totalPrice > 0 ? 'unpaid' : 'paid',
'registered_at' => $now,
];
$db->insertObject('#__mokosuiteevent_registrations', $registration, 'id');
// Create individual tickets
$tickets = [];
for ($i = 0; $i < $qty; $i++) {
$ticket = (object) [
'registration_id' => (int) $registration->id,
'ticket_type_id' => (int) $ticketTypeId,
'qr_code' => self::generateQR(0),
'attendee_name' => $attendeeName,
'checked_in' => 0,
];
$db->insertObject('#__mokosuiteevent_tickets', $ticket, 'id');
// Re-generate QR with actual ticket ID
$ticket->qr_code = self::generateQR((int) $ticket->id);
$db->setQuery($db->getQuery(true)
->update($db->quoteName('#__mokosuiteevent_tickets'))
->set($db->quoteName('qr_code') . ' = ' . $db->quote($ticket->qr_code))
->where($db->quoteName('id') . ' = ' . (int) $ticket->id));
$db->execute();
$tickets[] = $ticket;
}
// Update sold count
$db->setQuery($db->getQuery(true)
->update($db->quoteName('#__mokosuiteevent_ticket_types'))
->set($db->quoteName('sold') . ' = ' . $db->quoteName('sold') . ' + ' . (int) $qty)
->where($db->quoteName('id') . ' = ' . (int) $ticketTypeId));
$db->execute();
// Update event registered_count
$db->setQuery($db->getQuery(true)
->update($db->quoteName('#__mokosuiteevent_events'))
->set($db->quoteName('registered_count') . ' = ' . $db->quoteName('registered_count') . ' + ' . (int) $qty)
->where($db->quoteName('id') . ' = ' . (int) $type->event_id));
$db->execute();
return (object) [
'registration' => $registration,
'tickets' => $tickets,
'total' => $totalPrice,
];
}
/**
* Generate a unique QR code string for check-in.
*/
public static function generateQR(int $ticketId): string
{
return 'MOKO-EVT-' . strtoupper(bin2hex(random_bytes(4)))
. '-' . str_pad((string) $ticketId, 6, '0', STR_PAD_LEFT)
. '-' . strtoupper(bin2hex(random_bytes(4)));
}
/**
* Get remaining ticket availability by type for an event.
*/
public static function getAvailability(int $eventId): array
{
$db = Factory::getContainer()->get(DatabaseInterface::class);
$query = $db->getQuery(true)
->select([
$db->quoteName('id'),
$db->quoteName('name'),
$db->quoteName('price'),
$db->quoteName('quantity'),
$db->quoteName('sold'),
'(' . $db->quoteName('quantity') . ' - ' . $db->quoteName('sold') . ') AS ' . $db->quoteName('remaining'),
])
->from($db->quoteName('#__mokosuiteevent_ticket_types'))
->where($db->quoteName('event_id') . ' = ' . (int) $eventId)
->where($db->quoteName('published') . ' = 1')
->order($db->quoteName('ordering') . ' ASC');
$db->setQuery($query);
return $db->loadObjectList() ?: [];
}
}