Skip to content

Commit cb5af69

Browse files
authored
Feature: Add platform fees report (#974)
1 parent 26d9362 commit cb5af69

Some content is hidden

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

51 files changed

+3392
-2011
lines changed

backend/app/DomainObjects/Enums/OrganizerReportTypes.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ enum OrganizerReportTypes: string
1010
case EVENTS_PERFORMANCE = 'events_performance';
1111
case TAX_SUMMARY = 'tax_summary';
1212
case CHECK_IN_SUMMARY = 'check_in_summary';
13+
case PLATFORM_FEES = 'platform_fees';
1314
}

backend/app/DomainObjects/Generated/OrderItemDomainObjectAbstract.php

Lines changed: 0 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,6 @@ abstract class OrderItemDomainObjectAbstract extends \HiEvents\DomainObjects\Abs
2525
final public const TOTAL_SERVICE_FEE = 'total_service_fee';
2626
final public const TAXES_AND_FEES_ROLLUP = 'taxes_and_fees_rollup';
2727
final public const PRODUCT_TYPE = 'product_type';
28-
final public const BUNDLE_GROUP_ID = 'bundle_group_id';
29-
final public const IS_BUNDLE_PRIMARY = 'is_bundle_primary';
3028

3129
protected int $id;
3230
protected int $order_id;
@@ -43,8 +41,6 @@ abstract class OrderItemDomainObjectAbstract extends \HiEvents\DomainObjects\Abs
4341
protected ?float $total_service_fee = 0.0;
4442
protected array|string|null $taxes_and_fees_rollup = null;
4543
protected string $product_type = 'TICKET';
46-
protected ?string $bundle_group_id = null;
47-
protected bool $is_bundle_primary = false;
4844

4945
public function toArray(): array
5046
{
@@ -64,8 +60,6 @@ public function toArray(): array
6460
'total_service_fee' => $this->total_service_fee ?? null,
6561
'taxes_and_fees_rollup' => $this->taxes_and_fees_rollup ?? null,
6662
'product_type' => $this->product_type ?? null,
67-
'bundle_group_id' => $this->bundle_group_id ?? null,
68-
'is_bundle_primary' => $this->is_bundle_primary ?? null,
6963
];
7064
}
7165

@@ -233,26 +227,4 @@ public function getProductType(): string
233227
{
234228
return $this->product_type;
235229
}
236-
237-
public function setBundleGroupId(?string $bundle_group_id): self
238-
{
239-
$this->bundle_group_id = $bundle_group_id;
240-
return $this;
241-
}
242-
243-
public function getBundleGroupId(): ?string
244-
{
245-
return $this->bundle_group_id;
246-
}
247-
248-
public function setIsBundlePrimary(bool $is_bundle_primary): self
249-
{
250-
$this->is_bundle_primary = $is_bundle_primary;
251-
return $this;
252-
}
253-
254-
public function getIsBundlePrimary(): bool
255-
{
256-
return $this->is_bundle_primary;
257-
}
258230
}
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
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 HiEvents\Services\Domain\Report\DTO\PaginatedReportDTO;
12+
use Illuminate\Support\Carbon;
13+
use Illuminate\Validation\ValidationException;
14+
use Symfony\Component\HttpFoundation\StreamedResponse;
15+
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
16+
17+
class ExportOrganizerReportAction extends BaseAction
18+
{
19+
private const MAX_EXPORT_ROWS = 15000;
20+
21+
public function __construct(private readonly GetOrganizerReportHandler $reportHandler)
22+
{
23+
}
24+
25+
/**
26+
* @throws ValidationException
27+
*/
28+
public function __invoke(GetOrganizerReportRequest $request, int $organizerId, string $reportType): StreamedResponse
29+
{
30+
$this->isActionAuthorized($organizerId, OrganizerDomainObject::class);
31+
32+
$this->validateDateRange($request);
33+
34+
if (!in_array($reportType, OrganizerReportTypes::valuesArray(), true)) {
35+
throw new BadRequestHttpException(__('Invalid report type.'));
36+
}
37+
38+
$reportData = $this->reportHandler->handle(
39+
reportData: new GetOrganizerReportDTO(
40+
organizerId: $organizerId,
41+
reportType: OrganizerReportTypes::from($reportType),
42+
startDate: $request->validated('start_date'),
43+
endDate: $request->validated('end_date'),
44+
currency: $request->validated('currency'),
45+
eventId: $request->validated('event_id'),
46+
page: 1,
47+
perPage: self::MAX_EXPORT_ROWS,
48+
),
49+
);
50+
51+
$data = $reportData instanceof PaginatedReportDTO
52+
? $reportData->data
53+
: $reportData;
54+
55+
$filename = $reportType . '_' . date('Y-m-d_H-i-s') . '.csv';
56+
57+
return new StreamedResponse(function () use ($data, $reportType) {
58+
$handle = fopen('php://output', 'w');
59+
60+
$headers = $this->getHeadersForReportType($reportType);
61+
fputcsv($handle, $headers);
62+
63+
foreach ($data as $row) {
64+
$csvRow = $this->formatRowForReportType($row, $reportType);
65+
fputcsv($handle, $csvRow);
66+
}
67+
68+
fclose($handle);
69+
}, 200, [
70+
'Content-Type' => 'text/csv',
71+
'Content-Disposition' => "attachment; filename=\"$filename\"",
72+
'Cache-Control' => 'no-cache, no-store, must-revalidate',
73+
'Pragma' => 'no-cache',
74+
'Expires' => '0',
75+
]);
76+
}
77+
78+
private function getHeadersForReportType(string $reportType): array
79+
{
80+
return match ($reportType) {
81+
OrganizerReportTypes::PLATFORM_FEES->value => [
82+
'Event',
83+
'Payment Date',
84+
'Order Reference',
85+
'Amount Paid',
86+
'Hi.Events Fee',
87+
'VAT Rate',
88+
'VAT on Fee',
89+
'Total Fee',
90+
'Currency',
91+
'Stripe Payment ID',
92+
],
93+
OrganizerReportTypes::REVENUE_SUMMARY->value => [
94+
'Date',
95+
'Gross Sales',
96+
'Net Revenue',
97+
'Total Refunded',
98+
'Total Tax',
99+
'Total Fee',
100+
'Order Count',
101+
],
102+
OrganizerReportTypes::EVENTS_PERFORMANCE->value => [
103+
'Event ID',
104+
'Event Name',
105+
'Currency',
106+
'Start Date',
107+
'End Date',
108+
'Status',
109+
'Event State',
110+
'Products Sold',
111+
'Gross Revenue',
112+
'Total Refunded',
113+
'Net Revenue',
114+
'Total Tax',
115+
'Total Fee',
116+
'Total Orders',
117+
'Unique Customers',
118+
'Page Views',
119+
],
120+
OrganizerReportTypes::TAX_SUMMARY->value => [
121+
'Event ID',
122+
'Event Name',
123+
'Currency',
124+
'Tax Name',
125+
'Tax Rate',
126+
'Total Collected',
127+
'Order Count',
128+
],
129+
OrganizerReportTypes::CHECK_IN_SUMMARY->value => [
130+
'Event ID',
131+
'Event Name',
132+
'Start Date',
133+
'Total Attendees',
134+
'Total Checked In',
135+
'Check-in Rate (%)',
136+
'Check-in Lists Count',
137+
],
138+
default => [],
139+
};
140+
}
141+
142+
private function formatRowForReportType(object $row, string $reportType): array
143+
{
144+
return match ($reportType) {
145+
OrganizerReportTypes::PLATFORM_FEES->value => [
146+
$row->event_name ?? '',
147+
$row->payment_date ? date('Y-m-d H:i:s', strtotime($row->payment_date)) : '',
148+
$row->order_reference ?? '',
149+
$row->amount_paid ?? 0,
150+
$row->fee_amount ?? 0,
151+
$row->vat_rate !== null ? ($row->vat_rate * 100) . '%' : '',
152+
$row->vat_amount ?? 0,
153+
$row->total_fee ?? 0,
154+
$row->currency ?? '',
155+
$row->payment_intent_id ?? '',
156+
],
157+
OrganizerReportTypes::REVENUE_SUMMARY->value => [
158+
$row->date ?? '',
159+
$row->gross_sales ?? 0,
160+
$row->net_revenue ?? 0,
161+
$row->total_refunded ?? 0,
162+
$row->total_tax ?? 0,
163+
$row->total_fee ?? 0,
164+
$row->order_count ?? 0,
165+
],
166+
OrganizerReportTypes::EVENTS_PERFORMANCE->value => [
167+
$row->event_id ?? '',
168+
$row->event_name ?? '',
169+
$row->event_currency ?? '',
170+
$row->start_date ?? '',
171+
$row->end_date ?? '',
172+
$row->status ?? '',
173+
$row->event_state ?? '',
174+
$row->products_sold ?? 0,
175+
$row->gross_revenue ?? 0,
176+
$row->total_refunded ?? 0,
177+
$row->net_revenue ?? 0,
178+
$row->total_tax ?? 0,
179+
$row->total_fee ?? 0,
180+
$row->total_orders ?? 0,
181+
$row->unique_customers ?? 0,
182+
$row->page_views ?? 0,
183+
],
184+
OrganizerReportTypes::TAX_SUMMARY->value => [
185+
$row->event_id ?? '',
186+
$row->event_name ?? '',
187+
$row->event_currency ?? '',
188+
$row->tax_name ?? '',
189+
$row->tax_rate ? ($row->tax_rate * 100) . '%' : '',
190+
$row->total_collected ?? 0,
191+
$row->order_count ?? 0,
192+
],
193+
OrganizerReportTypes::CHECK_IN_SUMMARY->value => [
194+
$row->event_id ?? '',
195+
$row->event_name ?? '',
196+
$row->start_date ?? '',
197+
$row->total_attendees ?? 0,
198+
$row->total_checked_in ?? 0,
199+
$row->check_in_rate ?? 0,
200+
$row->check_in_lists_count ?? 0,
201+
],
202+
default => [],
203+
};
204+
}
205+
206+
/**
207+
* @throws ValidationException
208+
*/
209+
private function validateDateRange(GetOrganizerReportRequest $request): void
210+
{
211+
$startDate = $request->validated('start_date');
212+
$endDate = $request->validated('end_date');
213+
214+
if (!$startDate || !$endDate) {
215+
return;
216+
}
217+
218+
$diffInDays = Carbon::parse($startDate)->diffInDays(Carbon::parse($endDate));
219+
220+
if ($diffInDays > 370) {
221+
throw ValidationException::withMessages(['start_date' => __('Date range must be less than 370 days.')]);
222+
}
223+
}
224+
}

backend/app/Http/Actions/Reports/GetOrganizerReportAction.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use HiEvents\Http\Request\Report\GetOrganizerReportRequest;
99
use HiEvents\Services\Application\Handlers\Reports\DTO\GetOrganizerReportDTO;
1010
use HiEvents\Services\Application\Handlers\Reports\GetOrganizerReportHandler;
11+
use HiEvents\Services\Domain\Report\DTO\PaginatedReportDTO;
1112
use Illuminate\Http\JsonResponse;
1213
use Illuminate\Support\Carbon;
1314
use Illuminate\Validation\ValidationException;
@@ -39,9 +40,18 @@ public function __invoke(GetOrganizerReportRequest $request, int $organizerId, s
3940
startDate: $request->validated('start_date'),
4041
endDate: $request->validated('end_date'),
4142
currency: $request->validated('currency'),
43+
eventId: $request->validated('event_id'),
44+
page: (int) $request->validated('page', 1),
45+
perPage: (int) $request->validated('per_page', 1000),
4246
),
4347
);
4448

49+
if ($reportData instanceof PaginatedReportDTO) {
50+
return $this->jsonResponse(
51+
data: $reportData->toArray(),
52+
);
53+
}
54+
4555
return $this->jsonResponse(
4656
data: $reportData,
4757
wrapInData: true,

backend/app/Http/Request/Report/GetOrganizerReportRequest.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ public function rules(): array
1212
'start_date' => 'date|before:end_date|required_with:end_date|nullable',
1313
'end_date' => 'date|after:start_date|required_with:start_date|nullable',
1414
'currency' => 'string|size:3|nullable',
15+
'event_id' => 'integer|nullable',
16+
'page' => 'integer|min:1|nullable',
17+
'per_page' => 'integer|min:1|max:1000|nullable',
1518
];
1619
}
1720
}

backend/app/Models/StripePayment.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ class StripePayment extends BaseModel
1111

1212
protected function getTimestampsEnabled(): bool
1313
{
14-
return false;
14+
return true;
1515
}
1616

1717
protected function getCastMap(): array

backend/app/Services/Application/Handlers/Reports/DTO/GetOrganizerReportDTO.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ public function __construct(
1313
public readonly ?string $startDate,
1414
public readonly ?string $endDate,
1515
public readonly ?string $currency,
16+
public readonly ?int $eventId = null,
17+
public readonly int $page = 1,
18+
public readonly int $perPage = 1000,
1619
)
1720
{
1821
}

backend/app/Services/Application/Handlers/Reports/GetOrganizerReportHandler.php

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
namespace HiEvents\Services\Application\Handlers\Reports;
44

55
use HiEvents\Services\Application\Handlers\Reports\DTO\GetOrganizerReportDTO;
6+
use HiEvents\Services\Domain\Report\DTO\PaginatedReportDTO;
67
use HiEvents\Services\Domain\Report\Factory\OrganizerReportServiceFactory;
8+
use HiEvents\Services\Domain\Report\OrganizerReports\PlatformFeesReport;
79
use Illuminate\Support\Carbon;
810
use Illuminate\Support\Collection;
911

@@ -15,15 +17,27 @@ public function __construct(
1517
{
1618
}
1719

18-
public function handle(GetOrganizerReportDTO $reportData): Collection
20+
public function handle(GetOrganizerReportDTO $reportData): Collection|PaginatedReportDTO
1921
{
20-
return $this->reportServiceFactory
21-
->create($reportData->reportType)
22-
->generateReport(
22+
$reportService = $this->reportServiceFactory->create($reportData->reportType);
23+
24+
if ($reportService instanceof PlatformFeesReport) {
25+
return $reportService->generateReport(
2326
organizerId: $reportData->organizerId,
2427
currency: $reportData->currency,
2528
startDate: $reportData->startDate ? Carbon::parse($reportData->startDate) : null,
2629
endDate: $reportData->endDate ? Carbon::parse($reportData->endDate) : null,
30+
eventId: $reportData->eventId,
31+
page: $reportData->page,
32+
perPage: $reportData->perPage,
2733
);
34+
}
35+
36+
return $reportService->generateReport(
37+
organizerId: $reportData->organizerId,
38+
currency: $reportData->currency,
39+
startDate: $reportData->startDate ? Carbon::parse($reportData->startDate) : null,
40+
endDate: $reportData->endDate ? Carbon::parse($reportData->endDate) : null,
41+
);
2842
}
2943
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public function generateReport(
2424
int $organizerId,
2525
?string $currency = null,
2626
?Carbon $startDate = null,
27-
?Carbon $endDate = null
27+
?Carbon $endDate = null,
2828
): Collection
2929
{
3030
$organizer = $this->organizerRepository->findById($organizerId);

0 commit comments

Comments
 (0)