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
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,9 @@ public function getPercentageApplicationFee(): float
{
return $this->getApplicationFees()['percentage'] ?? config('app.default_application_fee_percentage');
}

public function getApplicationFeeCurrency(): string
{
return $this->getApplicationFees()['currency'] ?? 'USD';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ public function __invoke(Request $request): JsonResponse
'application_fees' => 'required|array',
'application_fees.fixed' => 'required|numeric|min:0',
'application_fees.percentage' => 'required|numeric|min:0|max:100',
'application_fees.currency' => 'sometimes|string|size:3|alpha|uppercase',
'bypass_application_fees' => 'sometimes|boolean',
]);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public function __invoke(Request $request, int $configurationId): JsonResponse
'application_fees' => 'required|array',
'application_fees.fixed' => 'required|numeric|min:0',
'application_fees.percentage' => 'required|numeric|min:0|max:100',
'application_fees.currency' => 'sometimes|string|size:3|alpha|uppercase',
'bypass_application_fees' => 'sometimes|boolean',
]);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

namespace HiEvents\Http\Actions\EventSettings;

use HiEvents\DomainObjects\EventDomainObject;
use HiEvents\Http\Actions\BaseAction;
use HiEvents\Resources\Event\PlatformFeePreviewResource;
use HiEvents\Services\Application\Handlers\EventSettings\DTO\GetPlatformFeePreviewDTO;
use HiEvents\Services\Application\Handlers\EventSettings\GetPlatformFeePreviewHandler;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

class GetPlatformFeePreviewAction extends BaseAction
{
public function __construct(
private readonly GetPlatformFeePreviewHandler $handler,
)
{
}

public function __invoke(Request $request, int $eventId): JsonResponse
{
$this->isActionAuthorized($eventId, EventDomainObject::class);

$this->validate($request, [
'price' => 'required|numeric|min:0',
]);

$dto = new GetPlatformFeePreviewDTO(
eventId: $eventId,
price: (float)$request->input('price'),
);

$result = $this->handler->handle($dto);

return $this->resourceResponse(PlatformFeePreviewResource::class, $result);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public function toArray($request): array
'application_fees' => [
'percentage' => $this->getPercentageApplicationFee(),
'fixed' => $this->getFixedApplicationFee(),
'currency' => $this->getApplicationFeeCurrency(),
],
'bypass_application_fees' => $this->getBypassApplicationFees(),
];
Expand Down
26 changes: 26 additions & 0 deletions backend/app/Resources/Event/PlatformFeePreviewResource.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

namespace HiEvents\Resources\Event;

use HiEvents\Resources\BaseResource;
use HiEvents\Services\Application\Handlers\EventSettings\DTO\PlatformFeePreviewResponseDTO;

/**
* @mixin PlatformFeePreviewResponseDTO
*/
class PlatformFeePreviewResource extends BaseResource
{
public function toArray($request): array
{
return [
'event_currency' => $this->eventCurrency,
'fee_currency' => $this->feeCurrency,
'fixed_fee_original' => $this->fixedFeeOriginal,
'fixed_fee_converted' => $this->fixedFeeConverted,
'percentage_fee' => $this->percentageFee,
'sample_price' => $this->samplePrice,
'platform_fee' => $this->platformFee,
'total' => $this->total,
];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

namespace HiEvents\Services\Application\Handlers\EventSettings\DTO;

use HiEvents\DataTransferObjects\BaseDataObject;

class GetPlatformFeePreviewDTO extends BaseDataObject
{
public function __construct(
public readonly int $eventId,
public readonly float $price,
) {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

namespace HiEvents\Services\Application\Handlers\EventSettings\DTO;

use HiEvents\DataTransferObjects\BaseDataObject;

class PlatformFeePreviewResponseDTO extends BaseDataObject
{
public function __construct(
public readonly string $eventCurrency,
public readonly ?string $feeCurrency,
public readonly float $fixedFeeOriginal,
public readonly float $fixedFeeConverted,
public readonly float $percentageFee,
public readonly float $samplePrice,
public readonly float $platformFee,
public readonly float $total,
) {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<?php

namespace HiEvents\Services\Application\Handlers\EventSettings;

use Brick\Money\Currency;
use HiEvents\DomainObjects\AccountConfigurationDomainObject;
use HiEvents\Repository\Eloquent\Value\Relationship;
use HiEvents\Repository\Interfaces\AccountRepositoryInterface;
use HiEvents\Repository\Interfaces\EventRepositoryInterface;
use HiEvents\Services\Application\Handlers\EventSettings\DTO\GetPlatformFeePreviewDTO;
use HiEvents\Services\Application\Handlers\EventSettings\DTO\PlatformFeePreviewResponseDTO;
use HiEvents\Services\Infrastructure\CurrencyConversion\CurrencyConversionClientInterface;

class GetPlatformFeePreviewHandler
{
public function __construct(
private readonly AccountRepositoryInterface $accountRepository,
private readonly EventRepositoryInterface $eventRepository,
private readonly CurrencyConversionClientInterface $currencyConversionClient,
)
{
}

public function handle(GetPlatformFeePreviewDTO $dto): PlatformFeePreviewResponseDTO
{
$event = $this->eventRepository->findById($dto->eventId);
$eventCurrency = $event->getCurrency();

$account = $this->accountRepository
->loadRelation(new Relationship(
domainObject: AccountConfigurationDomainObject::class,
name: 'configuration',
))
->findByEventId($dto->eventId);

$configuration = $account->getConfiguration();

if ($configuration === null) {
return new PlatformFeePreviewResponseDTO(
eventCurrency: $eventCurrency,
feeCurrency: null,
fixedFeeOriginal: 0,
fixedFeeConverted: 0,
percentageFee: 0,
samplePrice: $dto->price,
platformFee: 0,
total: $dto->price,
);
}

$feeCurrency = $configuration->getApplicationFeeCurrency();
$fixedFeeOriginal = $configuration->getFixedApplicationFee();
$percentageFee = $configuration->getPercentageApplicationFee();

$fixedFeeConverted = $this->convertFixedFee($fixedFeeOriginal, $feeCurrency, $eventCurrency);

$platformFee = $this->calculatePlatformFee($fixedFeeConverted, $percentageFee, $dto->price);

return new PlatformFeePreviewResponseDTO(
eventCurrency: $eventCurrency,
feeCurrency: $feeCurrency,
fixedFeeOriginal: $fixedFeeOriginal,
fixedFeeConverted: round($fixedFeeConverted, 2),
percentageFee: $percentageFee,
samplePrice: $dto->price,
platformFee: $platformFee,
total: round($dto->price + $platformFee, 2),
);
}

private function convertFixedFee(float $amount, string $fromCurrency, string $toCurrency): float
{
if ($fromCurrency === $toCurrency) {
return $amount;
}

return $this->currencyConversionClient->convert(
fromCurrency: Currency::of($fromCurrency),
toCurrency: Currency::of($toCurrency),
amount: $amount
)->toFloat();
}

private function calculatePlatformFee(float $fixedFee, float $percentageFee, float $price): float
{
$percentageRate = $percentageFee / 100;

$platformFee = $percentageRate >= 1
? $fixedFee + ($price * $percentageRate)
: ($fixedFee + ($price * $percentageRate)) / (1 - $percentageRate);

return round($platformFee, 2);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@

class OrderApplicationFeeCalculationService
{
private const BASE_CURRENCY = 'USD';

public function __construct(
private readonly Repository $config,
private readonly CurrencyConversionClientInterface $currencyConversionClient,
Expand Down Expand Up @@ -64,12 +62,14 @@ private function getConvertedFixedFee(
string $currency
): MoneyValue
{
if ($currency === self::BASE_CURRENCY) {
$baseCurrency = $accountConfiguration->getApplicationFeeCurrency();

if ($currency === $baseCurrency) {
return MoneyValue::fromFloat($accountConfiguration->getFixedApplicationFee(), $currency);
}

return $this->currencyConversionClient->convert(
fromCurrency: Currency::of(self::BASE_CURRENCY),
fromCurrency: Currency::of($baseCurrency),
toCurrency: Currency::of($currency),
amount: $accountConfiguration->getFixedApplicationFee()
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@

class OrderPlatformFeePassThroughService
{
private const BASE_CURRENCY = 'USD';

public const PLATFORM_FEE_ID = 0;

public static function getPlatformFeeName(): string
Expand All @@ -37,12 +35,14 @@ public function isEnabled(EventSettingDomainObject $eventSettings): bool
}

/**
* Calculate platform fee that exactly covers Stripe's application fee.
* Calculate the platform fee line item to show on the buyer's invoice.
*
* Formula: P = (fixed + total * r) / (1 - r)
* Uses gross-up formula: P = (fixed + total * r) / (1 - r)
* Where r = percentage rate, P = platform fee
*
* This ensures: Stripe fee on (total + P) = P
* This ensures that when the platform fee (P) is added to the order total,
* the resulting Stripe application fee calculated on the new total equals P.
* In other words: application_fee(total + P) = P
*/
public function calculatePlatformFee(
AccountConfigurationDomainObject $accountConfiguration,
Expand Down Expand Up @@ -75,13 +75,14 @@ private function getConvertedFixedFee(
): float
{
$baseFee = $accountConfiguration->getFixedApplicationFee();
$baseCurrency = $accountConfiguration->getApplicationFeeCurrency();

if ($currency === self::BASE_CURRENCY) {
if ($currency === $baseCurrency) {
return $baseFee;
}

return $this->currencyConversionClient->convert(
fromCurrency: BrickCurrency::of(self::BASE_CURRENCY),
fromCurrency: BrickCurrency::of($baseCurrency),
toCurrency: BrickCurrency::of($currency),
amount: $baseFee
)->toFloat();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;

return new class extends Migration
{
public function up(): void
{
DB::table('account_configuration')
->whereNotNull('application_fees')
->get()
->each(function ($row) {
$fees = json_decode($row->application_fees, true);
if ($fees && !isset($fees['currency'])) {
$fees['currency'] = 'USD';
DB::table('account_configuration')
->where('id', $row->id)
->update(['application_fees' => json_encode($fees)]);
}
});
}

public function down(): void
{
DB::table('account_configuration')
->whereNotNull('application_fees')
->get()
->each(function ($row) {
$fees = json_decode($row->application_fees, true);
if ($fees && isset($fees['currency'])) {
unset($fees['currency']);
DB::table('account_configuration')
->where('id', $row->id)
->update(['application_fees' => json_encode($fees)]);
}
});
}
};
2 changes: 2 additions & 0 deletions backend/routes/api.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
use HiEvents\Http\Actions\Events\UpdateEventStatusAction;
use HiEvents\Http\Actions\EventSettings\EditEventSettingsAction;
use HiEvents\Http\Actions\EventSettings\GetEventSettingsAction;
use HiEvents\Http\Actions\EventSettings\GetPlatformFeePreviewAction;
use HiEvents\Http\Actions\EmailTemplates\CreateOrganizerEmailTemplateAction;
use HiEvents\Http\Actions\EmailTemplates\CreateEventEmailTemplateAction;
use HiEvents\Http\Actions\EmailTemplates\UpdateOrganizerEmailTemplateAction;
Expand Down Expand Up @@ -376,6 +377,7 @@ function (Router $router): void {
$router->get('/events/{event_id}/settings', GetEventSettingsAction::class);
$router->put('/events/{event_id}/settings', EditEventSettingsAction::class);
$router->patch('/events/{event_id}/settings', PartialEditEventSettingsAction::class);
$router->get('/events/{event_id}/settings/platform-fee-preview', GetPlatformFeePreviewAction::class);

// Capacity Assignments
$router->post('/events/{event_id}/capacity-assignments', CreateCapacityAssignmentAction::class);
Expand Down
Loading
Loading