Skip to content

Commit 5c4d2da

Browse files
authored
Merge pull request #285 from HiEventsDev/feature/add-event-reports
2 parents 0215634 + d27b37d commit 5c4d2da

File tree

30 files changed

+1354
-9
lines changed

30 files changed

+1354
-9
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
namespace HiEvents\DomainObjects\Enums;
4+
5+
enum ReportTypes: string
6+
{
7+
use BaseEnum;
8+
9+
case PRODUCT_SALES = 'product_sales';
10+
case DAILY_SALES_REPORT = 'daily_sales_report';
11+
case PROMO_CODES_REPORT = 'promo_codes_report';
12+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<?php
2+
3+
namespace HiEvents\Http\Actions\Reports;
4+
5+
use HiEvents\DomainObjects\Enums\ReportTypes;
6+
use HiEvents\DomainObjects\EventDomainObject;
7+
use HiEvents\Http\Actions\BaseAction;
8+
use HiEvents\Http\Request\Report\GetReportRequest;
9+
use HiEvents\Services\Handlers\Reports\DTO\GetReportDTO;
10+
use HiEvents\Services\Handlers\Reports\GetReportHandler;
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 GetReportAction extends BaseAction
17+
{
18+
public function __construct(private readonly GetReportHandler $reportHandler)
19+
{
20+
}
21+
22+
/**
23+
* @throws ValidationException
24+
*/
25+
public function __invoke(GetReportRequest $request, int $eventId, string $reportType): JsonResponse
26+
{
27+
$this->isActionAuthorized($eventId, EventDomainObject::class);
28+
29+
$this->validateDateRange($request);
30+
31+
if (!in_array($reportType, ReportTypes::valuesArray(), true)) {
32+
throw new BadRequestHttpException('Invalid report type.');
33+
}
34+
35+
$reportData = $this->reportHandler->handle(
36+
reportData: new GetReportDTO(
37+
eventId: $eventId,
38+
reportType: ReportTypes::from($reportType),
39+
startDate: $request->validated('start_date'),
40+
endDate: $request->validated('end_date'),
41+
),
42+
);
43+
44+
return $this->jsonResponse($reportData);
45+
}
46+
47+
/**
48+
* @throws ValidationException
49+
*/
50+
private function validateDateRange(GetReportRequest $request): void
51+
{
52+
$startDate = $request->validated('start_date');
53+
$endDate = $request->validated('end_date');
54+
55+
$diffInDays = Carbon::parse($startDate)->diffInDays(Carbon::parse($endDate));
56+
57+
if ($diffInDays > 370) {
58+
throw ValidationException::withMessages(['start_date' => 'Date range must be less than 370 days.']);
59+
}
60+
}
61+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
namespace HiEvents\Http\Request\Report;
4+
5+
use HiEvents\Http\Request\BaseRequest;
6+
7+
class GetReportRequest 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+
];
15+
}
16+
}

backend/app/Resources/Product/ProductResourcePublic.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ public function toArray(Request $request): array
1818
'id' => $this->getId(),
1919
'title' => $this->getTitle(),
2020
'type' => $this->getType(),
21+
'product_type' => $this->getProductType(),
2122
'description' => $this->getDescription(),
2223
'max_per_order' => $this->getMaxPerOrder(),
2324
'min_per_order' => $this->getMinPerOrder(),

backend/app/Resources/ProductCategory/ProductCategoryResourcePublic.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
namespace HiEvents\Resources\ProductCategory;
44

55
use HiEvents\DomainObjects\ProductCategoryDomainObject;
6-
use HiEvents\Resources\Product\ProductResource;
6+
use HiEvents\Resources\Product\ProductResourcePublic;
77
use Illuminate\Http\Resources\Json\JsonResource;
88

99
/**
@@ -21,7 +21,7 @@ public function toArray($request): array
2121
'order' => $this->getOrder(),
2222
'no_products_message' => $this->getNoProductsMessage(),
2323
$this->mergeWhen((bool)$this->getProducts(), fn() => [
24-
'products' => ProductResource::collection($this->getProducts()),
24+
'products' => ProductResourcePublic::collection($this->getProducts()),
2525
]),
2626
];
2727
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php
2+
3+
namespace HiEvents\Services\Domain\Report;
4+
5+
use HiEvents\Repository\Interfaces\EventRepositoryInterface;
6+
use Illuminate\Cache\Repository;
7+
use Illuminate\Database\DatabaseManager;
8+
use Illuminate\Support\Carbon;
9+
use Illuminate\Support\Collection;
10+
11+
abstract class AbstractReportService
12+
{
13+
public function __construct(
14+
private readonly Repository $cache,
15+
private readonly DatabaseManager $queryBuilder,
16+
private readonly EventRepositoryInterface $eventRepository,
17+
)
18+
{
19+
}
20+
21+
public function generateReport(int $eventId, ?Carbon $startDate = null, ?Carbon $endDate = null): Collection
22+
{
23+
$event = $this->eventRepository->findById($eventId);
24+
$timezone = $event->getTimezone();
25+
26+
$endDate = Carbon::parse($endDate ?? now(), $timezone);
27+
$startDate = Carbon::parse($startDate ?? $endDate->copy()->subDays(30), $timezone);
28+
29+
$reportResults = $this->cache->remember(
30+
key: $this->getCacheKey($eventId, $startDate, $endDate),
31+
ttl: Carbon::now()->addSeconds(20),
32+
callback: fn() => $this->queryBuilder->select(
33+
$this->getSqlQuery($startDate, $endDate),
34+
[
35+
'event_id' => $eventId,
36+
]
37+
)
38+
);
39+
40+
return collect($reportResults);
41+
}
42+
43+
abstract protected function getSqlQuery(Carbon $startDate, Carbon $endDate): string;
44+
45+
protected function getCacheKey(int $eventId, ?Carbon $startDate, ?Carbon $endDate): string
46+
{
47+
return static::class . "$eventId.{$startDate?->toDateString()}.{$endDate?->toDateString()}";
48+
}
49+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
namespace HiEvents\Services\Domain\Report\Exception;
4+
5+
use Exception;
6+
7+
class InvalidDateRange extends Exception
8+
{
9+
10+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
namespace HiEvents\Services\Domain\Report\Factory;
4+
5+
use HiEvents\DomainObjects\Enums\ReportTypes;
6+
use HiEvents\Services\Domain\Report\AbstractReportService;
7+
use HiEvents\Services\Domain\Report\Reports\DailySalesReport;
8+
use HiEvents\Services\Domain\Report\Reports\ProductSalesReport;
9+
use HiEvents\Services\Domain\Report\Reports\PromoCodesReport;
10+
use Illuminate\Support\Facades\App;
11+
12+
class ReportServiceFactory
13+
{
14+
public function create(ReportTypes $reportType): AbstractReportService
15+
{
16+
return match ($reportType) {
17+
ReportTypes::PRODUCT_SALES => App::make(ProductSalesReport::class),
18+
ReportTypes::DAILY_SALES_REPORT => App::make(DailySalesReport::class),
19+
ReportTypes::PROMO_CODES_REPORT => App::make(PromoCodesReport::class),
20+
};
21+
}
22+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
namespace HiEvents\Services\Domain\Report\Reports;
4+
5+
use HiEvents\Services\Domain\Report\AbstractReportService;
6+
use Illuminate\Support\Carbon;
7+
8+
class DailySalesReport extends AbstractReportService
9+
{
10+
public function getSqlQuery(Carbon $startDate, Carbon $endDate): string
11+
{
12+
$startDateStr = $startDate->toDateString();
13+
$endDateStr = $endDate->toDateString();
14+
15+
return <<<SQL
16+
WITH date_range AS (
17+
SELECT generate_series('$startDateStr'::date, '$endDateStr'::date, '1 day'::interval) AS date
18+
)
19+
SELECT
20+
d.date,
21+
COALESCE(eds.sales_total_gross, 0.00) AS sales_total_gross,
22+
COALESCE(eds.total_tax, 0.00) AS total_tax,
23+
COALESCE(eds.sales_total_before_additions, 0.00) AS sales_total_before_additions,
24+
COALESCE(eds.products_sold, 0) AS products_sold,
25+
COALESCE(eds.orders_created, 0) AS orders_created,
26+
COALESCE(eds.total_fee, 0.00) AS total_fee,
27+
COALESCE(eds.total_refunded, 0.00) AS total_refunded,
28+
COALESCE(eds.total_views, 0) AS total_views
29+
FROM
30+
date_range d
31+
LEFT JOIN event_daily_statistics eds
32+
ON d.date = eds.date
33+
AND eds.event_id = :event_id
34+
ORDER BY d.date desc;
35+
SQL;
36+
}
37+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
namespace HiEvents\Services\Domain\Report\Reports;
4+
5+
use HiEvents\DomainObjects\Status\OrderStatus;
6+
use HiEvents\Services\Domain\Report\AbstractReportService;
7+
use Illuminate\Support\Carbon;
8+
9+
class ProductSalesReport extends AbstractReportService
10+
{
11+
protected function getSqlQuery(Carbon $startDate, Carbon $endDate): string
12+
{
13+
$startDateString = $startDate->format('Y-m-d H:i:s');
14+
$endDateString = $endDate->format('Y-m-d H:i:s');
15+
$completedStatus = OrderStatus::COMPLETED->name;
16+
17+
return <<<SQL
18+
WITH filtered_orders AS (
19+
SELECT
20+
oi.product_id,
21+
oi.total_tax,
22+
oi.total_gross,
23+
oi.total_service_fee,
24+
oi.id AS order_item_id
25+
FROM order_items oi
26+
JOIN orders o ON oi.order_id = o.id
27+
WHERE o.status = '$completedStatus'
28+
AND o.event_id = :event_id
29+
AND o.created_at BETWEEN '$startDateString' AND '$endDateString'
30+
AND oi.deleted_at IS NULL
31+
)
32+
SELECT
33+
p.id AS product_id,
34+
p.title AS product_title,
35+
p.type AS product_type,
36+
COALESCE(SUM(fo.total_tax), 0) AS total_tax,
37+
COALESCE(SUM(fo.total_gross), 0) AS total_gross,
38+
COALESCE(SUM(fo.total_service_fee), 0) AS total_service_fees,
39+
COUNT(fo.order_item_id) AS number_sold
40+
FROM products p
41+
LEFT JOIN filtered_orders fo ON fo.product_id = p.id
42+
WHERE p.event_id = :event_id
43+
AND p.deleted_at IS NULL
44+
GROUP BY p.id, p.title, p.type
45+
ORDER BY p."order"
46+
SQL;
47+
}
48+
}

0 commit comments

Comments
 (0)