172 lines
5.0 KiB
PHP
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() ?: [];
|
|
}
|
|
}
|