Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -288,3 +288,6 @@ PAYMENTS_SERVICE_OAUTH2_SCOPES=payment-profile/read
L5_SWAGGER_OPEN_API_SPEC_VERSION=3.1.0

SCHEDULER_OUTPUT_TO_STDOUT=false

# registration stats response cache lifetime in seconds (0 disables the cache)
CACHE_API_RESPONSE_REGISTRATION_STATS_LIFETIME=600
41 changes: 41 additions & 0 deletions app/ModelSerializers/Summit/SummitRegistrationStatsSerializer.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
**/
use App\Http\Exceptions\HTTP403ForbiddenException;
use App\Utils\FilterUtils;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Log;
use libs\utils\JsonUtils;
use models\summit\Summit;
Expand All @@ -36,6 +38,45 @@ public function serialize($expand = null, array $fields = [], array $relations =
{
$summit = $this->object;
if (!$summit instanceof Summit) return [];

$ttl = intval(Config::get('cache_api_response.registration_stats_response_lifetime', 600));
// the endpoints are admin only, so the key does not depend on the caller
if ($ttl <= 0)
return $this->buildValues($summit, $expand, $fields, $relations, $params);

$filter = $params['filter'] ?? null;
$start_date = null;
$end_date = null;
if ($filter instanceof Filter) list($start_date, $end_date) = FilterUtils::parseDateRangeUTC($filter);

$key = sprintf
(
'registration_stats_%s_%s_%s_%s',
$summit->getId(),
$start_date?->getTimestamp() ?? 'null',
$end_date?->getTimestamp() ?? 'null',
md5(json_encode([$expand, $fields, $relations]))
);

$values = Cache::get($key);
if (!is_array($values)) {
$values = $this->buildValues($summit, $expand, $fields, $relations, $params);
Cache::put($key, $values, $ttl);
}
return $values;
}

/**
* @param Summit $summit
* @param null $expand
* @param array $fields
* @param array $relations
* @param array $params
* @return array
* @throws \Exception
*/
private function buildValues(Summit $summit, $expand, array $fields, array $relations, array $params): array
{
$values = parent::serialize($expand, $fields, $relations, $params);

$filter = $params['filter'] ?? null;
Expand Down
59 changes: 23 additions & 36 deletions app/Models/Foundation/Summit/SummitRegistrationStats.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ private static function getDefaultTimeZoneOffset(?DateTime $now = null){
return $defaultSSTimeZone->getOffset($now) / 3600;
}

/**
* Known limitation: a single fixed offset is used for the whole summit, the one in effect on its begin date.
* Tickets bought under a different offset (i.e. across a DST change) can fall in the neighbouring
* hour/day bucket of the purchased tickets histogram. Kept as is on purpose, see ClickUp task 86bcjg0u6.
* @return float|int
*/
private function getTimeZoneOffset(){
try {
$tz = $this->getTimeZone();
Expand Down Expand Up @@ -105,8 +111,7 @@ public function getActiveTicketsCount(?DateTime $startDate = null, ?DateTime $e
try {
$sql = <<<SQL
select COUNT(SummitAttendeeTicket.ID) FROM SummitAttendeeTicket
INNER JOIN SummitOrder ON SummitOrder.ID = SummitAttendeeTicket.OrderID
WHERE SummitOrder.SummitID = :summit_id AND
WHERE SummitAttendeeTicket.SummitID = :summit_id AND
SummitAttendeeTicket.IsActive = 1 AND
SummitAttendeeTicket.Status = 'Paid'
SQL;
Expand Down Expand Up @@ -134,8 +139,7 @@ public function getInactiveTicketsCount(?DateTime $startDate = null, ?DateTime
try {
$sql = <<<SQL
select COUNT(SummitAttendeeTicket.ID) FROM SummitAttendeeTicket
INNER JOIN SummitOrder ON SummitOrder.ID = SummitAttendeeTicket.OrderID
WHERE SummitOrder.SummitID = :summit_id AND
WHERE SummitAttendeeTicket.SummitID = :summit_id AND
SummitAttendeeTicket.IsActive = 0 AND
SummitAttendeeTicket.Status = 'Paid'
SQL;
Expand Down Expand Up @@ -163,9 +167,8 @@ public function getActiveAssignedTicketsCount(?DateTime $startDate = null, ?Dat
try {
$sql = <<<SQL
SELECT COUNT(SummitAttendeeTicket.ID) FROM SummitAttendeeTicket
INNER JOIN SummitOrder ON SummitOrder.ID = SummitAttendeeTicket.OrderID
INNER JOIN SummitAttendee ON SummitAttendee.ID = SummitAttendeeTicket.OwnerID
WHERE SummitOrder.SummitID = :summit_id AND
WHERE SummitAttendeeTicket.SummitID = :summit_id AND
SummitAttendeeTicket.IsActive = 1 AND
SummitAttendeeTicket.Status = 'Paid'
SQL;
Expand Down Expand Up @@ -220,8 +223,7 @@ public function getTotalPaymentAmountCollected(?DateTime $startDate = null, ?Da
try {
$sql = <<<SQL
SELECT SUM(SummitAttendeeTicket.RawCost - SummitAttendeeTicket.Discount) FROM SummitAttendeeTicket
INNER JOIN SummitOrder ON SummitOrder.ID = SummitAttendeeTicket.OrderID
WHERE SummitOrder.SummitID = :summit_id AND
WHERE SummitAttendeeTicket.SummitID = :summit_id AND
SummitAttendeeTicket.Status = 'Paid'
SQL;

Expand Down Expand Up @@ -252,10 +254,9 @@ public function getTotalRefundAmountEmitted(?DateTime $startDate = null, ?DateT
SELECT SUM(SummitRefundRequest.RefundedAmount) FROM `SummitRefundRequest`
INNER JOIN SummitAttendeeTicketRefundRequest on SummitAttendeeTicketRefundRequest.ID = SummitRefundRequest.ID
INNER JOIN SummitAttendeeTicket on SummitAttendeeTicket.ID = SummitAttendeeTicketRefundRequest.TicketID
INNER JOIN SummitOrder on SummitOrder.ID = SummitAttendeeTicket.OrderID
WHERE
SummitRefundRequest.Status='Approved' AND
SummitOrder.SummitID = :summit_id
SummitAttendeeTicket.SummitID = :summit_id
SQL;
$sql = self::addDatesFilteringWithTimeZone($sql, "SummitRefundRequest", "Created", $startDate, $endDate);

Expand All @@ -280,9 +281,8 @@ public function getActiveTicketsCountPerTicketType(?DateTime $startDate = null,
try {
$sql = <<<SQL
select SummitTicketType.Name AS type, COUNT(SummitAttendeeTicket.ID) as qty FROM SummitAttendeeTicket
INNER JOIN SummitOrder ON SummitOrder.ID = SummitAttendeeTicket.OrderID
INNER JOIN SummitTicketType ON SummitAttendeeTicket.TicketTypeID = SummitTicketType.ID
WHERE SummitOrder.SummitID = :summit_id AND
WHERE SummitAttendeeTicket.SummitID = :summit_id AND
SummitAttendeeTicket.IsActive = 1 AND
SummitAttendeeTicket.Status = 'Paid'
SQL;
Expand All @@ -309,15 +309,14 @@ public function getCheckedInActiveTicketsCountPerTicketType(?DateTime $startDate
try {
$sql = <<<SQL
SELECT SummitTicketType.Name AS type, COUNT(SummitAttendeeTicket.ID) as qty FROM SummitAttendeeTicket
INNER JOIN SummitOrder ON SummitOrder.ID = SummitAttendeeTicket.OrderID
INNER JOIN SummitTicketType ON SummitAttendeeTicket.TicketTypeID = SummitTicketType.ID
INNER JOIN SummitAttendee ON SummitAttendee.ID = SummitAttendeeTicket.OwnerID
INNER JOIN SummitAttendeeBadge ON SummitAttendeeBadge.TicketID = SummitAttendeeTicket.ID
INNER JOIN SummitBadgeType ON SummitBadgeType.ID = SummitAttendeeBadge.BadgeTypeID
INNER JOIN SummitBadgeType_AccessLevels ON SummitBadgeType_AccessLevels.SummitBadgeTypeID = SummitBadgeType.ID
INNER JOIN SummitAccessLevelType ON SummitAccessLevelType.ID = SummitBadgeType_AccessLevels.SummitAccessLevelTypeID
WHERE
SummitOrder.SummitID = :summit_id AND
SummitAttendeeTicket.SummitID = :summit_id AND
SummitAttendeeTicket.IsActive = 1 AND
SummitAttendeeTicket.Status = 'Paid' AND
SummitAttendee.SummitHallCheckedIn = 1 AND
Expand Down Expand Up @@ -348,9 +347,8 @@ public function getActiveBadgesCountPerBadgeType(?DateTime $startDate = null, ?
SELECT SummitBadgeType.Name as type, COUNT(DISTINCT(SummitAttendeeBadge.ID)) as qty FROM SummitAttendeeBadge
INNER JOIN SummitBadgeType ON SummitAttendeeBadge.BadgeTypeID = SummitBadgeType.ID
INNER JOIN SummitAttendeeTicket ON SummitAttendeeTicket.ID = SummitAttendeeBadge.TicketID
INNER JOIN SummitOrder ON SummitOrder.ID = SummitAttendeeTicket.OrderID
INNER JOIN SummitTicketType ON SummitAttendeeTicket.TicketTypeID = SummitTicketType.ID
WHERE SummitOrder.SummitID = :summit_id AND
WHERE SummitAttendeeTicket.SummitID = :summit_id AND
SummitAttendeeTicket.IsActive = 1 AND
SummitAttendeeTicket.Status = 'Paid'
SQL;
Expand Down Expand Up @@ -379,12 +377,11 @@ public function getActiveCheckedInBadgesCountPerBadgeType(?DateTime $startDate
SELECT SummitBadgeType.Name as type, COUNT(DISTINCT(SummitAttendeeBadge.ID)) as qty FROM SummitAttendeeBadge
INNER JOIN SummitBadgeType ON SummitAttendeeBadge.BadgeTypeID = SummitBadgeType.ID
INNER JOIN SummitAttendeeTicket ON SummitAttendeeTicket.ID = SummitAttendeeBadge.TicketID
INNER JOIN SummitOrder ON SummitOrder.ID = SummitAttendeeTicket.OrderID
INNER JOIN SummitTicketType ON SummitAttendeeTicket.TicketTypeID = SummitTicketType.ID
INNER JOIN SummitAttendee ON SummitAttendee.ID = SummitAttendeeTicket.OwnerID
INNER JOIN SummitBadgeType_AccessLevels ON SummitBadgeType_AccessLevels.SummitBadgeTypeID = SummitBadgeType.ID
INNER JOIN SummitAccessLevelType ON SummitAccessLevelType.ID = SummitBadgeType_AccessLevels.SummitAccessLevelTypeID
WHERE SummitOrder.SummitID = :summit_id AND
WHERE SummitAttendeeTicket.SummitID = :summit_id AND
SummitAttendeeTicket.IsActive = 1 AND
SummitAttendeeTicket.Status = 'Paid' AND
SummitAttendee.SummitHallCheckedIn = 1 AND
Expand Down Expand Up @@ -608,7 +605,6 @@ public function getActiveTicketsPerBadgeFeatureType(?DateTime $startDate = null
try {
$sql = <<<SQL
SELECT SummitBadgeFeatureType.Name as type, COUNT(DISTINCT(SummitAttendeeTicket.ID)) as qty FROM SummitAttendeeTicket
INNER JOIN SummitOrder ON SummitOrder.ID = SummitAttendeeTicket.OrderID
INNER JOIN SummitAttendeeBadge ON SummitAttendeeBadge.TicketID = SummitAttendeeTicket.ID
INNER JOIN SummitBadgeType ON SummitBadgeType.ID = SummitAttendeeBadge.BadgeTypeID
LEFT JOIN SummitBadgeType_BadgeFeatures ON SummitBadgeType_BadgeFeatures.SummitBadgeTypeID = SummitBadgeType.ID
Expand All @@ -618,7 +614,7 @@ public function getActiveTicketsPerBadgeFeatureType(?DateTime $startDate = null
WHERE
SummitAttendeeTicket.IsActive = 1 AND
SummitAttendeeTicket.Status = 'Paid' AND
SummitOrder.SummitID = :summit_id
SummitAttendeeTicket.SummitID = :summit_id
SQL;

$sql = self::addDatesFilteringWithTimeZone($sql, "SummitAttendeeTicket", "Created", $startDate, $endDate);
Expand All @@ -644,7 +640,6 @@ public function getAttendeesCheckinPerBadgeFeatureType(?DateTime $startDate = n
try {
$sql = <<<SQL
SELECT SummitBadgeFeatureType.Name as type, COUNT(DISTINCT(SummitAttendeeTicket.ID)) as qty FROM SummitAttendeeTicket
INNER JOIN SummitOrder ON SummitOrder.ID = SummitAttendeeTicket.OrderID
INNER JOIN SummitAttendeeBadge ON SummitAttendeeBadge.TicketID = SummitAttendeeTicket.ID
INNER JOIN SummitBadgeType ON SummitBadgeType.ID = SummitAttendeeBadge.BadgeTypeID
LEFT JOIN SummitBadgeType_BadgeFeatures ON SummitBadgeType_BadgeFeatures.SummitBadgeTypeID = SummitBadgeType.ID
Expand All @@ -655,7 +650,7 @@ public function getAttendeesCheckinPerBadgeFeatureType(?DateTime $startDate = n
WHERE
SummitAttendeeTicket.IsActive = 1 AND
SummitAttendeeTicket.Status = 'Paid' AND
SummitOrder.SummitID = :summit_id AND
SummitAttendeeTicket.SummitID = :summit_id AND
SummitAttendee.SummitHallCheckedIn = 1
SQL;

Expand Down Expand Up @@ -806,9 +801,8 @@ public function getPurchasedTicketsGroupedBy
SELECT COUNT(SummitAttendeeTicket.ID) as qty,
DATE_FORMAT(CONVERT_TZ(SummitAttendeeTicket.TicketBoughtDate,'+00:00','{$offset}'), '{$date_format}') AS label
FROM SummitAttendeeTicket
INNER JOIN SummitOrder ON SummitOrder.ID = SummitAttendeeTicket.OrderID
WHERE
SummitOrder.SummitID = :summit_id AND
SummitAttendeeTicket.SummitID = :summit_id AND
SummitAttendeeTicket.IsActive = 1 AND
SummitAttendeeTicket.Status = 'Paid'
SQL;
Expand All @@ -833,22 +827,15 @@ public function getPurchasedTicketsGroupedBy
$sql .= " GROUP BY DATE_FORMAT(CONVERT_TZ(SummitAttendeeTicket.TicketBoughtDate,'+00:00','{$offset}'), '{$date_format}')";
$sql .= " ORDER BY SummitAttendeeTicket.TicketBoughtDate ASC";

$count_sql = <<<SQL
SELECT COUNT(*) FROM ({$sql}) T1
SQL;
$stmt = $this->prepareRawSQL($count_sql, ['summit_id' => $this->id]);
$res = $stmt->executeQuery();
$res = $res->fetchFirstColumn();

$total = count($res) > 0 ? intval($res[0]) : 0;

$sql .= " LIMIT {$pagingInfo->getPerPage()} OFFSET {$pagingInfo->getOffset()} ";
Log::debug(sprintf("Summit::getTicketsPurchasedGroupedBy sql %s", $sql));

// the histogram is computed once; the total is the number of buckets and the page is sliced here
$stmt = $this->prepareRawSQL($sql, ['summit_id' => $this->id]);
$res = $stmt->executeQuery();
$res = $res->fetchAllAssociative();
$res = count($res) > 0 ? $res : [];
$buckets = $res->fetchAllAssociative();

$total = count($buckets);
$res = array_slice($buckets, $pagingInfo->getOffset(), $pagingInfo->getPerPage());

return new PagingResponse
(
Expand Down
1 change: 1 addition & 0 deletions config/cache_api_response.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
'get_event_feedback_response_lifetime' => env('CACHE_API_RESPONSE_GET_EVENT_FEEDBACK_LIFETIME', 600),
'get_published_event_response_lifetime' => env('CACHE_API_RESPONSE_GET_PUBLISHED_EVENT_LIFETIME', 600),
'get_summits_response_lifetime' => env('CACHE_API_RESPONSE_GET_SUMMITS_LIFETIME', 600),
'registration_stats_response_lifetime' => env('CACHE_API_RESPONSE_REGISTRATION_STATS_LIFETIME', 600),
'file_url_lifetime' => env('CACHE_API_RESPONSE_FILE_URL_LIFETIME', 3600),
'event_has_access_lifetime' => env('CACHE_API_RESPONSE_EVENT_HAS_ACCESS_LIFETIME', 600),
];
36 changes: 36 additions & 0 deletions database/migrations/model/Version20261021120000.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

declare(strict_types=1);

namespace Database\Migrations\Model;

use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;

/**
* Backfills SummitAttendeeTicket.SummitID from the order for tickets that have an order but no summit,
* so the registration stats queries can filter on the ticket's own (indexed) SummitID.
*/
final class Version20261021120000 extends AbstractMigration
{
public function getDescription(): string
{
return 'Back Fill Ticket table with summit id from orders (remaining rows)';
}

public function up(Schema $schema): void
{
$sql = <<<SQL
UPDATE SummitAttendeeTicket t
JOIN SummitOrder o ON o.ID = t.OrderID
SET t.SummitID = o.SummitID
WHERE t.SummitID IS NULL AND t.OrderID IS NOT NULL;
SQL;

$this->addSql($sql);
}

public function down(Schema $schema): void
{
}
}
Loading
Loading