Skip to content

Commit d553fb1

Browse files
authored
Merge pull request #910 from HiEventsDev/develop
2 parents 35b4d06 + 714a654 commit d553fb1

File tree

58 files changed

+5913
-1883
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

58 files changed

+5913
-1883
lines changed
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace HiEvents\DomainObjects\Enums;
4+
5+
enum OrganizerReportTypes: string
6+
{
7+
use BaseEnum;
8+
9+
case REVENUE_SUMMARY = 'revenue_summary';
10+
case EVENTS_PERFORMANCE = 'events_performance';
11+
case TAX_SUMMARY = 'tax_summary';
12+
case CHECK_IN_SUMMARY = 'check_in_summary';
13+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php
2+
3+
namespace HiEvents\Http\Actions\Reports;
4+
5+
use HiEvents\DomainObjects\Enums\OrganizerReportTypes;
6+
use HiEvents\DomainObjects\OrganizerDomainObject;
7+
use HiEvents\Http\Actions\BaseAction;
8+
use HiEvents\Http\Request\Report\GetOrganizerReportRequest;
9+
use HiEvents\Services\Application\Handlers\Reports\DTO\GetOrganizerReportDTO;
10+
use HiEvents\Services\Application\Handlers\Reports\GetOrganizerReportHandler;
11+
use Illuminate\Http\JsonResponse;
12+
use Illuminate\Support\Carbon;
13+
use Illuminate\Validation\ValidationException;
14+
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
15+
16+
class GetOrganizerReportAction extends BaseAction
17+
{
18+
public function __construct(private readonly GetOrganizerReportHandler $reportHandler)
19+
{
20+
}
21+
22+
/**
23+
* @throws ValidationException
24+
*/
25+
public function __invoke(GetOrganizerReportRequest $request, int $organizerId, string $reportType): JsonResponse
26+
{
27+
$this->isActionAuthorized($organizerId, OrganizerDomainObject::class);
28+
29+
$this->validateDateRange($request);
30+
31+
if (!in_array($reportType, OrganizerReportTypes::valuesArray(), true)) {
32+
throw new BadRequestHttpException(__('Invalid report type.'));
33+
}
34+
35+
$reportData = $this->reportHandler->handle(
36+
reportData: new GetOrganizerReportDTO(
37+
organizerId: $organizerId,
38+
reportType: OrganizerReportTypes::from($reportType),
39+
startDate: $request->validated('start_date'),
40+
endDate: $request->validated('end_date'),
41+
currency: $request->validated('currency'),
42+
),
43+
);
44+
45+
return $this->jsonResponse(
46+
data: $reportData,
47+
wrapInData: true,
48+
);
49+
}
50+
51+
/**
52+
* @throws ValidationException
53+
*/
54+
private function validateDateRange(GetOrganizerReportRequest $request): void
55+
{
56+
$startDate = $request->validated('start_date');
57+
$endDate = $request->validated('end_date');
58+
59+
if (!$startDate || !$endDate) {
60+
return;
61+
}
62+
63+
$diffInDays = Carbon::parse($startDate)->diffInDays(Carbon::parse($endDate));
64+
65+
if ($diffInDays > 370) {
66+
throw ValidationException::withMessages(['start_date' => __('Date range must be less than 370 days.')]);
67+
}
68+
}
69+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
namespace HiEvents\Http\Request\Report;
4+
5+
use HiEvents\Http\Request\BaseRequest;
6+
7+
class GetOrganizerReportRequest extends BaseRequest
8+
{
9+
public function rules(): array
10+
{
11+
return [
12+
'start_date' => 'date|before:end_date|required_with:end_date|nullable',
13+
'end_date' => 'date|after:start_date|required_with:start_date|nullable',
14+
'currency' => 'string|size:3|nullable',
15+
];
16+
}
17+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
namespace HiEvents\Services\Application\Handlers\Reports\DTO;
4+
5+
use HiEvents\DataTransferObjects\BaseDataObject;
6+
use HiEvents\DomainObjects\Enums\OrganizerReportTypes;
7+
8+
class GetOrganizerReportDTO extends BaseDataObject
9+
{
10+
public function __construct(
11+
public readonly int $organizerId,
12+
public readonly OrganizerReportTypes $reportType,
13+
public readonly ?string $startDate,
14+
public readonly ?string $endDate,
15+
public readonly ?string $currency,
16+
)
17+
{
18+
}
19+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
namespace HiEvents\Services\Application\Handlers\Reports;
4+
5+
use HiEvents\Services\Application\Handlers\Reports\DTO\GetOrganizerReportDTO;
6+
use HiEvents\Services\Domain\Report\Factory\OrganizerReportServiceFactory;
7+
use Illuminate\Support\Carbon;
8+
use Illuminate\Support\Collection;
9+
10+
class GetOrganizerReportHandler
11+
{
12+
public function __construct(
13+
private readonly OrganizerReportServiceFactory $reportServiceFactory,
14+
)
15+
{
16+
}
17+
18+
public function handle(GetOrganizerReportDTO $reportData): Collection
19+
{
20+
return $this->reportServiceFactory
21+
->create($reportData->reportType)
22+
->generateReport(
23+
organizerId: $reportData->organizerId,
24+
currency: $reportData->currency,
25+
startDate: $reportData->startDate ? Carbon::parse($reportData->startDate) : null,
26+
endDate: $reportData->endDate ? Carbon::parse($reportData->endDate) : null,
27+
);
28+
}
29+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php
2+
3+
namespace HiEvents\Services\Domain\Report;
4+
5+
use HiEvents\Repository\Interfaces\OrganizerRepositoryInterface;
6+
use Illuminate\Cache\Repository;
7+
use Illuminate\Database\DatabaseManager;
8+
use Illuminate\Support\Carbon;
9+
use Illuminate\Support\Collection;
10+
11+
abstract class AbstractOrganizerReportService
12+
{
13+
private const CACHE_TTL_SECONDS = 30;
14+
15+
public function __construct(
16+
private readonly Repository $cache,
17+
private readonly DatabaseManager $queryBuilder,
18+
private readonly OrganizerRepositoryInterface $organizerRepository,
19+
)
20+
{
21+
}
22+
23+
public function generateReport(
24+
int $organizerId,
25+
?string $currency = null,
26+
?Carbon $startDate = null,
27+
?Carbon $endDate = null
28+
): Collection
29+
{
30+
$organizer = $this->organizerRepository->findById($organizerId);
31+
$timezone = $organizer->getTimezone();
32+
33+
$endDate = $endDate
34+
? $endDate->copy()->setTimezone($timezone)->startOfDay()
35+
: now($timezone)->startOfDay();
36+
$startDate = $startDate
37+
? $startDate->copy()->setTimezone($timezone)->startOfDay()
38+
: $endDate->copy()->subDays(30)->startOfDay();
39+
40+
$reportResults = $this->cache->remember(
41+
key: $this->getCacheKey($organizerId, $currency, $startDate, $endDate),
42+
ttl: Carbon::now()->addSeconds(self::CACHE_TTL_SECONDS),
43+
callback: fn() => $this->queryBuilder->select(
44+
$this->getSqlQuery($startDate, $endDate, $currency),
45+
[
46+
'organizer_id' => $organizerId,
47+
]
48+
)
49+
);
50+
51+
return collect($reportResults);
52+
}
53+
54+
abstract protected function getSqlQuery(Carbon $startDate, Carbon $endDate, ?string $currency = null): string;
55+
56+
protected function buildCurrencyFilter(string $column, ?string $currency): string
57+
{
58+
if ($currency === null) {
59+
return '';
60+
}
61+
$escapedCurrency = addslashes($currency);
62+
return "AND $column = '$escapedCurrency'";
63+
}
64+
65+
protected function getCacheKey(int $organizerId, ?string $currency, ?Carbon $startDate, ?Carbon $endDate): string
66+
{
67+
return static::class . "$organizerId.$currency.{$startDate?->toDateString()}.{$endDate?->toDateString()}";
68+
}
69+
}

backend/app/Services/Domain/Report/AbstractReportService.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,12 @@ public function generateReport(int $eventId, ?Carbon $startDate = null, ?Carbon
2323
$event = $this->eventRepository->findById($eventId);
2424
$timezone = $event->getTimezone();
2525

26-
$endDate = Carbon::parse($endDate ?? now(), $timezone);
27-
$startDate = Carbon::parse($startDate ?? $endDate->copy()->subDays(30), $timezone);
26+
$endDate = $endDate
27+
? $endDate->copy()->setTimezone($timezone)->startOfDay()
28+
: now($timezone)->startOfDay();
29+
$startDate = $startDate
30+
? $startDate->copy()->setTimezone($timezone)->startOfDay()
31+
: $endDate->copy()->subDays(30)->startOfDay();
2832

2933
$reportResults = $this->cache->remember(
3034
key: $this->getCacheKey($eventId, $startDate, $endDate),
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
namespace HiEvents\Services\Domain\Report\Factory;
4+
5+
use HiEvents\DomainObjects\Enums\OrganizerReportTypes;
6+
use HiEvents\Services\Domain\Report\AbstractOrganizerReportService;
7+
use HiEvents\Services\Domain\Report\OrganizerReports\CheckInSummaryReport;
8+
use HiEvents\Services\Domain\Report\OrganizerReports\EventsPerformanceReport;
9+
use HiEvents\Services\Domain\Report\OrganizerReports\RevenueSummaryReport;
10+
use HiEvents\Services\Domain\Report\OrganizerReports\TaxSummaryReport;
11+
use Illuminate\Support\Facades\App;
12+
13+
class OrganizerReportServiceFactory
14+
{
15+
public function create(OrganizerReportTypes $reportType): AbstractOrganizerReportService
16+
{
17+
return match ($reportType) {
18+
OrganizerReportTypes::REVENUE_SUMMARY => App::make(RevenueSummaryReport::class),
19+
OrganizerReportTypes::EVENTS_PERFORMANCE => App::make(EventsPerformanceReport::class),
20+
OrganizerReportTypes::TAX_SUMMARY => App::make(TaxSummaryReport::class),
21+
OrganizerReportTypes::CHECK_IN_SUMMARY => App::make(CheckInSummaryReport::class),
22+
};
23+
}
24+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<?php
2+
3+
namespace HiEvents\Services\Domain\Report\OrganizerReports;
4+
5+
use HiEvents\DomainObjects\Status\AttendeeStatus;
6+
use HiEvents\Services\Domain\Report\AbstractOrganizerReportService;
7+
use Illuminate\Support\Carbon;
8+
9+
class CheckInSummaryReport extends AbstractOrganizerReportService
10+
{
11+
protected function getSqlQuery(Carbon $startDate, Carbon $endDate, ?string $currency = null): string
12+
{
13+
$activeStatus = AttendeeStatus::ACTIVE->name;
14+
15+
return <<<SQL
16+
WITH organizer_events AS (
17+
SELECT id
18+
FROM events
19+
WHERE organizer_id = :organizer_id
20+
AND deleted_at IS NULL
21+
)
22+
SELECT
23+
e.id AS event_id,
24+
e.title AS event_name,
25+
e.start_date,
26+
COALESCE(attendee_counts.total_attendees, 0) AS total_attendees,
27+
COALESCE(checkin_counts.total_checked_in, 0) AS total_checked_in,
28+
CASE
29+
WHEN COALESCE(attendee_counts.total_attendees, 0) = 0 THEN 0
30+
ELSE ROUND((COALESCE(checkin_counts.total_checked_in, 0)::numeric / attendee_counts.total_attendees) * 100, 1)
31+
END AS check_in_rate,
32+
COALESCE(list_counts.check_in_lists_count, 0) AS check_in_lists_count
33+
FROM events e
34+
LEFT JOIN (
35+
SELECT
36+
event_id,
37+
COUNT(*) AS total_attendees
38+
FROM attendees
39+
WHERE event_id IN (SELECT id FROM organizer_events)
40+
AND status = '$activeStatus'
41+
AND deleted_at IS NULL
42+
GROUP BY event_id
43+
) attendee_counts ON e.id = attendee_counts.event_id
44+
LEFT JOIN (
45+
SELECT
46+
event_id,
47+
COUNT(DISTINCT attendee_id) AS total_checked_in
48+
FROM attendee_check_ins
49+
WHERE event_id IN (SELECT id FROM organizer_events)
50+
AND deleted_at IS NULL
51+
GROUP BY event_id
52+
) checkin_counts ON e.id = checkin_counts.event_id
53+
LEFT JOIN (
54+
SELECT
55+
event_id,
56+
COUNT(*) AS check_in_lists_count
57+
FROM check_in_lists
58+
WHERE event_id IN (SELECT id FROM organizer_events)
59+
AND deleted_at IS NULL
60+
GROUP BY event_id
61+
) list_counts ON e.id = list_counts.event_id
62+
WHERE e.organizer_id = :organizer_id
63+
AND e.deleted_at IS NULL
64+
ORDER BY e.start_date DESC NULLS LAST
65+
SQL;
66+
}
67+
}

0 commit comments

Comments
 (0)