diff --git a/backend/app/Services/Domain/Order/OrderItemProcessingService.php b/backend/app/Services/Domain/Order/OrderItemProcessingService.php index d047f78d1e..f34ccd50ff 100644 --- a/backend/app/Services/Domain/Order/OrderItemProcessingService.php +++ b/backend/app/Services/Domain/Order/OrderItemProcessingService.php @@ -2,7 +2,10 @@ namespace HiEvents\Services\Domain\Order; +use HiEvents\DomainObjects\AccountConfigurationDomainObject; +use HiEvents\DomainObjects\Enums\TaxCalculationType; use HiEvents\DomainObjects\EventDomainObject; +use HiEvents\DomainObjects\EventSettingDomainObject; use HiEvents\DomainObjects\Generated\ProductDomainObjectAbstract; use HiEvents\DomainObjects\OrderDomainObject; use HiEvents\DomainObjects\ProductDomainObject; @@ -10,6 +13,9 @@ use HiEvents\DomainObjects\PromoCodeDomainObject; use HiEvents\DomainObjects\TaxAndFeesDomainObject; use HiEvents\Helper\Currency; +use HiEvents\Repository\Eloquent\Value\Relationship; +use HiEvents\Repository\Interfaces\AccountRepositoryInterface; +use HiEvents\Repository\Interfaces\EventRepositoryInterface; use HiEvents\Repository\Interfaces\OrderRepositoryInterface; use HiEvents\Repository\Interfaces\ProductRepositoryInterface; use HiEvents\Services\Application\Handlers\Order\DTO\ProductOrderDetailsDTO; @@ -21,11 +27,17 @@ class OrderItemProcessingService { + private ?AccountConfigurationDomainObject $accountConfiguration = null; + private ?EventSettingDomainObject $eventSettings = null; + public function __construct( - private readonly OrderRepositoryInterface $orderRepository, - private readonly ProductRepositoryInterface $productRepository, - private readonly TaxAndFeeCalculationService $taxCalculationService, - private readonly ProductPriceService $productPriceService, + private readonly OrderRepositoryInterface $orderRepository, + private readonly ProductRepositoryInterface $productRepository, + private readonly TaxAndFeeCalculationService $taxCalculationService, + private readonly ProductPriceService $productPriceService, + private readonly OrderPlatformFeePassThroughService $platformFeeService, + private readonly AccountRepositoryInterface $accountRepository, + private readonly EventRepositoryInterface $eventRepository, ) { } @@ -44,6 +56,8 @@ public function process( ?PromoCodeDomainObject $promoCode ): Collection { + $this->loadPlatformFeeConfiguration($event->getId()); + $orderItems = collect(); foreach ($productsOrderDetails as $productOrderDetail) { @@ -61,11 +75,11 @@ public function process( ); } - $productOrderDetail->quantities->each(function (OrderProductPriceDTO $productPrice) use ($promoCode, $order, $orderItems, $product) { + $productOrderDetail->quantities->each(function (OrderProductPriceDTO $productPrice) use ($promoCode, $order, $orderItems, $product, $event) { if ($productPrice->quantity === 0) { return; } - $orderItemData = $this->calculateOrderItemData($product, $productPrice, $order, $promoCode); + $orderItemData = $this->calculateOrderItemData($product, $productPrice, $order, $promoCode, $event->getCurrency()); $orderItems->push($this->orderRepository->addOrderItem($orderItemData)); }); } @@ -73,11 +87,30 @@ public function process( return $orderItems; } + private function loadPlatformFeeConfiguration(int $eventId): void + { + $account = $this->accountRepository + ->loadRelation(new Relationship( + domainObject: AccountConfigurationDomainObject::class, + name: 'configuration', + )) + ->findByEventId($eventId); + + $this->accountConfiguration = $account->getConfiguration(); + + $event = $this->eventRepository + ->loadRelation(EventSettingDomainObject::class) + ->findById($eventId); + + $this->eventSettings = $event->getEventSettings(); + } + private function calculateOrderItemData( ProductDomainObject $product, OrderProductPriceDTO $productPriceDetails, OrderDomainObject $order, - ?PromoCodeDomainObject $promoCode + ?PromoCodeDomainObject $promoCode, + string $currency ): array { $prices = $this->productPriceService->getPrice($product, $productPriceDetails, $promoCode); @@ -92,6 +125,23 @@ private function calculateOrderItemData( quantity: $productPriceDetails->quantity ); + $totalTax = $taxesAndFees->taxTotal; + $totalFee = $taxesAndFees->feeTotal; + $rollUp = $taxesAndFees->rollUp; + + $platformFee = $this->calculatePlatformFee( + $itemTotalWithDiscount + $taxesAndFees->feeTotal + $taxesAndFees->taxTotal, + $productPriceDetails->quantity, + $currency + ); + + if ($platformFee > 0) { + $totalFee += $platformFee; + $rollUp = $this->addPlatformFeeToRollup($rollUp, $platformFee); + } + + $totalGross = Currency::round($itemTotalWithDiscount + $totalTax + $totalFee); + return [ 'product_type' => $product->getProductType(), 'product_id' => $product->getId(), @@ -102,13 +152,41 @@ private function calculateOrderItemData( 'price' => $priceWithDiscount, 'order_id' => $order->getId(), 'item_name' => $this->getOrderItemLabel($product, $productPriceDetails->price_id), - 'total_tax' => $taxesAndFees->taxTotal, - 'total_service_fee' => $taxesAndFees->feeTotal, - 'total_gross' => Currency::round($itemTotalWithDiscount + $taxesAndFees->taxTotal + $taxesAndFees->feeTotal), - 'taxes_and_fees_rollup' => $taxesAndFees->rollUp, + 'total_tax' => $totalTax, + 'total_service_fee' => $totalFee, + 'total_gross' => $totalGross, + 'taxes_and_fees_rollup' => $rollUp, ]; } + private function calculatePlatformFee(float $total, int $quantity, string $currency): float + { + if ($this->accountConfiguration === null || $this->eventSettings === null) { + return 0.0; + } + + return $this->platformFeeService->calculatePlatformFee( + $this->accountConfiguration, + $this->eventSettings, + $total, + $quantity, + $currency, + ); + } + + private function addPlatformFeeToRollup(array $rollUp, float $platformFee): array + { + $rollUp['fees'] ??= []; + $rollUp['fees'][] = [ + 'name' => OrderPlatformFeePassThroughService::getPlatformFeeName(), + 'rate' => $platformFee, + 'type' => TaxCalculationType::FIXED->name, + 'value' => $platformFee, + ]; + + return $rollUp; + } + private function getOrderItemLabel(ProductDomainObject $product, int $priceId): string { if ($product->isTieredType()) { diff --git a/backend/app/Services/Domain/Order/OrderManagementService.php b/backend/app/Services/Domain/Order/OrderManagementService.php index fd50e521c3..f788f2646c 100644 --- a/backend/app/Services/Domain/Order/OrderManagementService.php +++ b/backend/app/Services/Domain/Order/OrderManagementService.php @@ -3,20 +3,14 @@ namespace HiEvents\Services\Domain\Order; use Carbon\Carbon; -use HiEvents\DomainObjects\AccountConfigurationDomainObject; use HiEvents\DomainObjects\AffiliateDomainObject; -use HiEvents\DomainObjects\Enums\TaxCalculationType; use HiEvents\DomainObjects\EventDomainObject; -use HiEvents\DomainObjects\EventSettingDomainObject; use HiEvents\DomainObjects\Generated\OrderDomainObjectAbstract; use HiEvents\DomainObjects\OrderDomainObject; use HiEvents\DomainObjects\OrderItemDomainObject; use HiEvents\DomainObjects\PromoCodeDomainObject; use HiEvents\DomainObjects\Status\OrderStatus; use HiEvents\Helper\IdHelper; -use HiEvents\Repository\Eloquent\Value\Relationship; -use HiEvents\Repository\Interfaces\AccountRepositoryInterface; -use HiEvents\Repository\Interfaces\EventRepositoryInterface; use HiEvents\Repository\Interfaces\OrderRepositoryInterface; use HiEvents\Services\Domain\Tax\TaxAndFeeOrderRollupService; use Illuminate\Support\Collection; @@ -24,11 +18,8 @@ class OrderManagementService { public function __construct( - readonly private OrderRepositoryInterface $orderRepository, - readonly private TaxAndFeeOrderRollupService $taxAndFeeOrderRollupService, - readonly private OrderPlatformFeePassThroughService $platformFeeService, - readonly private AccountRepositoryInterface $accountRepository, - readonly private EventRepositoryInterface $eventRepository, + readonly private OrderRepositoryInterface $orderRepository, + readonly private TaxAndFeeOrderRollupService $taxAndFeeOrderRollupService, ) { } @@ -70,6 +61,9 @@ public function createNewOrder( } /** + * Update order totals by summing up all order items. + * Platform fee and its tax are included at the item level. + * * @param OrderDomainObject $order * @param Collection $orderItems * @return OrderDomainObject @@ -90,21 +84,6 @@ public function updateOrderTotals(OrderDomainObject $order, Collection $orderIte $rollup = $this->taxAndFeeOrderRollupService->rollup($orderItems); - $platformFee = $order->getIsManuallyCreated() - ? 0.0 - : $this->calculatePlatformFee($order->getEventId(), $orderItems, $order->getCurrency()); - - if ($platformFee > 0) { - $rollup['fees'][] = [ - 'name' => OrderPlatformFeePassThroughService::getPlatformFeeName(), - 'rate' => $platformFee, - 'type' => TaxCalculationType::FIXED->name, - 'value' => $platformFee, - ]; - $totalFee += $platformFee; - $totalGross += $platformFee; - } - $this->orderRepository->updateFromArray($order->getId(), [ 'total_before_additions' => $totalBeforeAdditions, 'total_tax' => $totalTax, @@ -117,38 +96,4 @@ public function updateOrderTotals(OrderDomainObject $order, Collection $orderIte ->loadRelation(OrderItemDomainObject::class) ->findById($order->getId()); } - - /** - * @param Collection $orderItems - */ - private function calculatePlatformFee(int $eventId, Collection $orderItems, string $currency): float - { - $account = $this->accountRepository - ->loadRelation(new Relationship( - domainObject: AccountConfigurationDomainObject::class, - name: 'configuration', - )) - ->findByEventId($eventId); - - $accountConfiguration = $account->getConfiguration(); - if ($accountConfiguration === null) { - return 0.0; - } - - $event = $this->eventRepository - ->loadRelation(EventSettingDomainObject::class) - ->findById($eventId); - - $eventSettings = $event->getEventSettings(); - if ($eventSettings === null) { - return 0.0; - } - - return $this->platformFeeService->calculateForOrder( - accountConfiguration: $accountConfiguration, - eventSettings: $eventSettings, - orderItems: $orderItems, - currency: $currency, - ); - } } diff --git a/backend/app/Services/Domain/Order/OrderPlatformFeePassThroughService.php b/backend/app/Services/Domain/Order/OrderPlatformFeePassThroughService.php index 558028f29b..89ccf95060 100644 --- a/backend/app/Services/Domain/Order/OrderPlatformFeePassThroughService.php +++ b/backend/app/Services/Domain/Order/OrderPlatformFeePassThroughService.php @@ -2,16 +2,17 @@ namespace HiEvents\Services\Domain\Order; +use Brick\Money\Currency as BrickCurrency; use HiEvents\DomainObjects\AccountConfigurationDomainObject; use HiEvents\DomainObjects\EventSettingDomainObject; -use HiEvents\DomainObjects\OrderDomainObject; -use HiEvents\DomainObjects\OrderItemDomainObject; use HiEvents\Helper\Currency; +use HiEvents\Services\Infrastructure\CurrencyConversion\CurrencyConversionClientInterface; use Illuminate\Config\Repository; -use Illuminate\Support\Collection; class OrderPlatformFeePassThroughService { + private const BASE_CURRENCY = 'USD'; + public const PLATFORM_FEE_ID = 0; public static function getPlatformFeeName(): string @@ -20,8 +21,8 @@ public static function getPlatformFeeName(): string } public function __construct( - private readonly Repository $config, - private readonly OrderApplicationFeeCalculationService $applicationFeeCalculationService, + private readonly Repository $config, + private readonly CurrencyConversionClientInterface $currencyConversionClient, ) { } @@ -36,66 +37,53 @@ public function isEnabled(EventSettingDomainObject $eventSettings): bool } /** - * Calculate the platform fee for a single product price (quantity = 1). + * Calculate platform fee that exactly covers Stripe's application fee. + * + * Formula: P = (fixed + total * r) / (1 - r) + * Where r = percentage rate, P = platform fee + * + * This ensures: Stripe fee on (total + P) = P */ - public function calculateForProductPrice( + public function calculatePlatformFee( AccountConfigurationDomainObject $accountConfiguration, EventSettingDomainObject $eventSettings, - float $priceWithFeesAndTaxes, + float $total, + int $quantity, string $currency, ): float { - if (!$this->isEnabled($eventSettings) || $priceWithFeesAndTaxes <= 0) { + if (!$this->isEnabled($eventSettings) || $total <= 0) { return 0.0; } - $order = (new OrderDomainObject()) - ->setCurrency($currency) - ->setTotalGross($priceWithFeesAndTaxes); - - $orderItem = (new OrderItemDomainObject()) - ->setPrice($priceWithFeesAndTaxes) - ->setQuantity(1) - ->setTotalGross($priceWithFeesAndTaxes); + $fixedFee = $this->getConvertedFixedFee($accountConfiguration, $currency); + $percentageRate = $accountConfiguration->getPercentageApplicationFee() / 100; - $order->setOrderItems(collect([$orderItem])); + if ($percentageRate >= 1) { + return Currency::round(($fixedFee * $quantity) + ($total * $percentageRate)); + } - $result = $this->applicationFeeCalculationService->calculateApplicationFee( - $accountConfiguration, - $order, - ); + $totalFixedFee = $fixedFee * $quantity; + $platformFee = ($totalFixedFee + ($total * $percentageRate)) / (1 - $percentageRate); - return $result ? Currency::round($result->netApplicationFee->toFloat()) : 0.0; + return Currency::round($platformFee); } - /** - * Calculate the platform fee for an order. - * - * @param Collection $orderItems - */ - public function calculateForOrder( + private function getConvertedFixedFee( AccountConfigurationDomainObject $accountConfiguration, - EventSettingDomainObject $eventSettings, - Collection $orderItems, - string $currency, + string $currency ): float { - if (!$this->isEnabled($eventSettings)) { - return 0.0; - } + $baseFee = $accountConfiguration->getFixedApplicationFee(); - $totalGross = $orderItems->sum(fn(OrderItemDomainObject $item) => $item->getTotalGross()); - - $order = (new OrderDomainObject()) - ->setCurrency($currency) - ->setTotalGross($totalGross) - ->setOrderItems($orderItems); - - $result = $this->applicationFeeCalculationService->calculateApplicationFee( - $accountConfiguration, - $order, - ); + if ($currency === self::BASE_CURRENCY) { + return $baseFee; + } - return $result ? Currency::round($result->netApplicationFee->toFloat()) : 0.0; + return $this->currencyConversionClient->convert( + fromCurrency: BrickCurrency::of(self::BASE_CURRENCY), + toCurrency: BrickCurrency::of($currency), + amount: $baseFee + )->toFloat(); } } diff --git a/backend/app/Services/Domain/Organizer/CreateDefaultOrganizerSettingsService.php b/backend/app/Services/Domain/Organizer/CreateDefaultOrganizerSettingsService.php index 42d18a658b..51dc943f00 100644 --- a/backend/app/Services/Domain/Organizer/CreateDefaultOrganizerSettingsService.php +++ b/backend/app/Services/Domain/Organizer/CreateDefaultOrganizerSettingsService.php @@ -26,6 +26,9 @@ public function createOrganizerSettings(OrganizerDomainObject $organizer): void // Use the "Modern" theme as default 'homepage_theme_settings' => $defaultTheme->getThemeData(), + + // Platform fee pass-through default from config + 'default_pass_platform_fee_to_buyer' => config('app.saas_default_pass_platform_fee_to_buyer', false), ]); } } diff --git a/backend/app/Services/Domain/Product/ProductFilterService.php b/backend/app/Services/Domain/Product/ProductFilterService.php index 92445d1462..a3b5ec0869 100644 --- a/backend/app/Services/Domain/Product/ProductFilterService.php +++ b/backend/app/Services/Domain/Product/ProductFilterService.php @@ -92,13 +92,13 @@ private function loadAccountConfiguration(int $eventId): void ->findByEventId($eventId); $this->accountConfiguration = $account->getConfiguration(); - $this->eventCurrency = $account->getCurrencyCode(); $event = $this->eventRepository ->loadRelation(EventSettingDomainObject::class) ->findById($eventId); $this->eventSettings = $event->getEventSettings(); + $this->eventCurrency = $event->getCurrency(); } private function isHiddenByPromoCode(ProductDomainObject $product, ?PromoCodeDomainObject $promoCode): bool @@ -147,8 +147,6 @@ private function processProduct( ); }); - // If there is a capacity assigned to the product, we set the capacity to capacity available qty, or the sum of all - // product prices qty, whichever is lower $productQuantities->each(function (AvailableProductQuantitiesDTO $quantity) use ($product) { if ($quantity->capacities !== null && $quantity->capacities->isNotEmpty() && $quantity->product_id === $product->getId()) { $product->setQuantityAvailable( @@ -198,7 +196,6 @@ private function filterProduct( private function processProductPrice(ProductDomainObject $product, ProductPriceDomainObject $price): void { - // If the product is free of charge, we don't charge service fees or taxes if (!$price->isFree()) { $taxAndFees = $this->taxCalculationService ->calculateTaxAndFeesForProductPrice($product, $price); @@ -206,10 +203,10 @@ private function processProductPrice(ProductDomainObject $product, ProductPriceD $feeTotal = $taxAndFees->feeTotal; $taxTotal = $taxAndFees->taxTotal; - $platformFee = $this->calculatePlatformFeeForPrice($price->getPrice(), $feeTotal, $taxTotal); - $feeTotal += $platformFee; + $platformFee = $this->calculatePlatformFee($price->getPrice() + $feeTotal + $taxTotal); if ($platformFee > 0) { + $feeTotal += $platformFee; $this->addPlatformFeeToProduct($product); } @@ -221,19 +218,18 @@ private function processProductPrice(ProductDomainObject $product, ProductPriceD $price->setIsAvailable($this->getPriceAvailability($price, $product)); } - private function calculatePlatformFeeForPrice(float $basePrice, float $feeTotal, float $taxTotal): float + private function calculatePlatformFee(float $total): float { if ($this->accountConfiguration === null || $this->eventSettings === null) { return 0.0; } - $priceWithFeesAndTaxes = $basePrice + $feeTotal + $taxTotal; - - return $this->platformFeeService->calculateForProductPrice( - $this->accountConfiguration, - $this->eventSettings, - $priceWithFeesAndTaxes, - $this->eventCurrency, + return $this->platformFeeService->calculatePlatformFee( + accountConfiguration: $this->accountConfiguration, + eventSettings: $this->eventSettings, + total: $total, + quantity: 1, + currency: $this->eventCurrency, ); } @@ -302,13 +298,6 @@ private function processProductPrices(ProductDomainObject $product, bool $hideSo ); } - /** - * For non-tiered products, we can inherit the availability of the product. - * - * @param ProductPriceDomainObject $price - * @param ProductDomainObject $product - * @return bool - */ private function getPriceAvailability(ProductPriceDomainObject $price, ProductDomainObject $product): bool { if ($product->isTieredType()) { diff --git a/backend/config/app.php b/backend/config/app.php index c354b3b83e..99344b4d8b 100644 --- a/backend/config/app.php +++ b/backend/config/app.php @@ -16,6 +16,7 @@ 'saas_mode_enabled' => env('APP_SAAS_MODE_ENABLED', false), 'saas_stripe_application_fee_percent' => env('APP_SAAS_STRIPE_APPLICATION_FEE_PERCENT', 1.5), 'saas_stripe_application_fee_fixed' => env('APP_SAAS_STRIPE_APPLICATION_FEE_FIXED', 0), + 'saas_default_pass_platform_fee_to_buyer' => env('APP_SAAS_DEFAULT_PASS_PLATFORM_FEE_TO_BUYER', false), 'disable_registration' => env('APP_DISABLE_REGISTRATION', false), 'api_rate_limit_per_minute' => env('APP_API_RATE_LIMIT_PER_MINUTE', 180), 'stripe_connect_account_type' => env('APP_STRIPE_CONNECT_ACCOUNT_TYPE', 'express'), diff --git a/backend/tests/Unit/Services/Domain/Order/OrderPlatformFeePassThroughServiceTest.php b/backend/tests/Unit/Services/Domain/Order/OrderPlatformFeePassThroughServiceTest.php index 2cfdfe662a..a11e0d7b31 100644 --- a/backend/tests/Unit/Services/Domain/Order/OrderPlatformFeePassThroughServiceTest.php +++ b/backend/tests/Unit/Services/Domain/Order/OrderPlatformFeePassThroughServiceTest.php @@ -2,12 +2,11 @@ namespace Tests\Unit\Services\Domain\Order; +use Brick\Money\Currency; use HiEvents\DomainObjects\AccountConfigurationDomainObject; use HiEvents\DomainObjects\EventSettingDomainObject; -use HiEvents\DomainObjects\OrderItemDomainObject; -use HiEvents\Services\Domain\Order\DTO\ApplicationFeeValuesDTO; -use HiEvents\Services\Domain\Order\OrderApplicationFeeCalculationService; use HiEvents\Services\Domain\Order\OrderPlatformFeePassThroughService; +use HiEvents\Services\Infrastructure\CurrencyConversion\CurrencyConversionClientInterface; use HiEvents\Values\MoneyValue; use Illuminate\Config\Repository; use PHPUnit\Framework\TestCase; @@ -15,31 +14,30 @@ class OrderPlatformFeePassThroughServiceTest extends TestCase { private Repository $config; - private OrderApplicationFeeCalculationService $applicationFeeCalculationService; + private CurrencyConversionClientInterface $currencyConversionClient; private OrderPlatformFeePassThroughService $service; protected function setUp(): void { $this->config = $this->createMock(Repository::class); - $this->applicationFeeCalculationService = $this->createMock(OrderApplicationFeeCalculationService::class); + $this->currencyConversionClient = $this->createMock(CurrencyConversionClientInterface::class); + + $this->currencyConversionClient->method('convert')->willReturnCallback( + fn(Currency $from, Currency $to, float $amount) => MoneyValue::fromFloat($amount, $to->getCurrencyCode()) + ); + $this->service = new OrderPlatformFeePassThroughService( $this->config, - $this->applicationFeeCalculationService, + $this->currencyConversionClient ); } - private function createOrderItem(float $price, int $quantity, float $totalGross): OrderItemDomainObject + private function createAccountConfig(float $fixedFee = 0.30, float $percentageFee = 2.9): AccountConfigurationDomainObject { - $item = $this->createMock(OrderItemDomainObject::class); - $item->method('getPrice')->willReturn($price); - $item->method('getQuantity')->willReturn($quantity); - $item->method('getTotalGross')->willReturn($totalGross); - return $item; - } - - private function createAccountConfig(): AccountConfigurationDomainObject - { - return $this->createMock(AccountConfigurationDomainObject::class); + $mock = $this->createMock(AccountConfigurationDomainObject::class); + $mock->method('getFixedApplicationFee')->willReturn($fixedFee); + $mock->method('getPercentageApplicationFee')->willReturn($percentageFee); + return $mock; } private function createEventSettings(bool $passPlatformFeeToBuyer = true): EventSettingDomainObject @@ -49,14 +47,6 @@ private function createEventSettings(bool $passPlatformFeeToBuyer = true): Event return $settings; } - private function createApplicationFeeDTO(float $amount, string $currency = 'USD'): ApplicationFeeValuesDTO - { - return new ApplicationFeeValuesDTO( - grossApplicationFee: MoneyValue::fromFloat($amount, $currency), - netApplicationFee: MoneyValue::fromFloat($amount, $currency), - ); - } - public function testIsEnabledReturnsFalseWhenSaasModeDisabled(): void { $this->config->method('get')->with('app.saas_mode_enabled')->willReturn(false); @@ -84,96 +74,222 @@ public function testIsEnabledReturnsTrueWhenBothEnabled(): void $this->assertTrue($this->service->isEnabled($eventSettings)); } - public function testCalculateForProductPriceReturnsZeroWhenDisabled(): void + public function testCalculatePlatformFeeReturnsZeroWhenDisabled(): void { $this->config->method('get')->with('app.saas_mode_enabled')->willReturn(true); $account = $this->createAccountConfig(); $eventSettings = $this->createEventSettings(false); - $fee = $this->service->calculateForProductPrice($account, $eventSettings, 100.00, 'USD'); + $result = $this->service->calculatePlatformFee($account, $eventSettings, 100.00, 1, 'USD'); - $this->assertEquals(0.0, $fee); + $this->assertEquals(0.0, $result); } - public function testCalculateForProductPriceReturnsZeroForZeroPrice(): void + public function testCalculatePlatformFeeReturnsZeroForZeroTotal(): void { $this->config->method('get')->willReturn(true); $account = $this->createAccountConfig(); $eventSettings = $this->createEventSettings(true); - $fee = $this->service->calculateForProductPrice($account, $eventSettings, 0.0, 'USD'); + $result = $this->service->calculatePlatformFee($account, $eventSettings, 0.0, 1, 'USD'); - $this->assertEquals(0.0, $fee); + $this->assertEquals(0.0, $result); } - public function testCalculateForProductPriceDelegatesToApplicationFeeService(): void + public function testCalculatePlatformFeeBasicCalculation(): void { $this->config->method('get')->willReturn(true); - $account = $this->createAccountConfig(); + // 2.9% + $0.30 fixed fee + $account = $this->createAccountConfig(0.30, 2.9); $eventSettings = $this->createEventSettings(true); - $this->applicationFeeCalculationService - ->expects($this->once()) - ->method('calculateApplicationFee') - ->willReturn($this->createApplicationFeeDTO(5.50)); + // Total: $100 + // Formula: P = (0.30 + 100 * 0.029) / (1 - 0.029) + // P = (0.30 + 2.90) / 0.971 = 3.20 / 0.971 = 3.30 + $result = $this->service->calculatePlatformFee($account, $eventSettings, 100.00, 1, 'USD'); + + $this->assertEqualsWithDelta(3.30, $result, 0.01); + } + + public function testCalculatePlatformFeeWithMultipleQuantity(): void + { + $this->config->method('get')->willReturn(true); + + // 2.9% + $0.30 fixed fee per item + $account = $this->createAccountConfig(0.30, 2.9); + $eventSettings = $this->createEventSettings(true); - $fee = $this->service->calculateForProductPrice($account, $eventSettings, 100.00, 'USD'); + // 2 items, total: $200 + // Fixed fee = 0.30 * 2 = 0.60 + // Formula: P = (0.60 + 200 * 0.029) / (1 - 0.029) + // P = (0.60 + 5.80) / 0.971 = 6.40 / 0.971 = 6.59 + $result = $this->service->calculatePlatformFee($account, $eventSettings, 200.00, 2, 'USD'); - $this->assertEquals(5.50, $fee); + $this->assertEqualsWithDelta(6.59, $result, 0.01); } - public function testCalculateForOrderReturnsZeroWhenDisabled(): void + public function testPlatformFeeExactlyCoversStripeApplicationFee(): void { - $this->config->method('get')->with('app.saas_mode_enabled')->willReturn(true); + $this->config->method('get')->willReturn(true); - $orderItems = collect([$this->createOrderItem(10, 2, 20)]); - $account = $this->createAccountConfig(); - $eventSettings = $this->createEventSettings(false); + $fixedFee = 0.30; + $percentageRate = 2.9; + $account = $this->createAccountConfig($fixedFee, $percentageRate); + $eventSettings = $this->createEventSettings(true); + + $totalBeforePlatformFee = 129.15; + + $platformFee = $this->service->calculatePlatformFee( + $account, + $eventSettings, + $totalBeforePlatformFee, + 1, + 'USD' + ); - $fee = $this->service->calculateForOrder($account, $eventSettings, $orderItems, 'USD'); + // The new total that Stripe sees + $newTotal = $totalBeforePlatformFee + $platformFee; - $this->assertEquals(0.0, $fee); + // What Stripe would calculate as application fee + $stripeAppFee = $fixedFee + ($newTotal * $percentageRate / 100); + + // Platform fee should equal Stripe app fee + $this->assertEqualsWithDelta( + $platformFee, + $stripeAppFee, + 0.01, + "Platform fee ({$platformFee}) should equal Stripe app fee ({$stripeAppFee})" + ); } - public function testCalculateForOrderDelegatesToApplicationFeeService(): void + public function testPlatformFeeWithDifferentTotals(): void { $this->config->method('get')->willReturn(true); - $orderItems = collect([ - $this->createOrderItem(10, 1, 10), - $this->createOrderItem(20, 2, 40), - ]); + $fixedFee = 0.30; + $percentageRate = 2.9; + $account = $this->createAccountConfig($fixedFee, $percentageRate); + $eventSettings = $this->createEventSettings(true); - $account = $this->createAccountConfig(); + $testCases = [ + ['total' => 50.00, 'desc' => '$50 order'], + ['total' => 100.00, 'desc' => '$100 order'], + ['total' => 250.00, 'desc' => '$250 order'], + ['total' => 500.00, 'desc' => '$500 order'], + ['total' => 1000.00, 'desc' => '$1000 order'], + ]; + + foreach ($testCases as $testCase) { + $platformFee = $this->service->calculatePlatformFee( + $account, + $eventSettings, + $testCase['total'], + 1, + 'USD' + ); + + $newTotal = $testCase['total'] + $platformFee; + $stripeAppFee = $fixedFee + ($newTotal * $percentageRate / 100); + + $this->assertEqualsWithDelta( + $platformFee, + $stripeAppFee, + 0.01, + "Failed for {$testCase['desc']}: Platform fee ({$platformFee}) != Stripe fee ({$stripeAppFee})" + ); + } + } + + public function testCurrencyConversionCalledForNonUsdCurrency(): void + { + $this->config->method('get')->willReturn(true); + + $currencyConversionClient = $this->createMock(CurrencyConversionClientInterface::class); + + $currencyConversionClient->expects($this->once()) + ->method('convert') + ->with( + $this->callback(fn(Currency $c) => $c->getCurrencyCode() === 'USD'), + $this->callback(fn(Currency $c) => $c->getCurrencyCode() === 'EUR'), + 0.30 + ) + ->willReturn(MoneyValue::fromFloat(0.27, 'EUR')); + + $service = new OrderPlatformFeePassThroughService( + $this->config, + $currencyConversionClient + ); + + $account = $this->createAccountConfig(0.30, 2.9); + $eventSettings = $this->createEventSettings(true); + + $result = $service->calculatePlatformFee($account, $eventSettings, 100.00, 1, 'EUR'); + + $this->assertGreaterThan(0, $result); + } + + public function testNoCurrencyConversionForUsd(): void + { + $this->config->method('get')->willReturn(true); + + $currencyConversionClient = $this->createMock(CurrencyConversionClientInterface::class); + $currencyConversionClient->expects($this->never())->method('convert'); + + $service = new OrderPlatformFeePassThroughService( + $this->config, + $currencyConversionClient + ); + + $account = $this->createAccountConfig(0.30, 2.9); + $eventSettings = $this->createEventSettings(true); + + $result = $service->calculatePlatformFee($account, $eventSettings, 100.00, 1, 'USD'); + + $this->assertGreaterThan(0, $result); + } + + public function testZeroFixedFeeOnlyPercentage(): void + { + $this->config->method('get')->willReturn(true); + + $account = $this->createAccountConfig(0.0, 2.9); $eventSettings = $this->createEventSettings(true); - $this->applicationFeeCalculationService - ->expects($this->once()) - ->method('calculateApplicationFee') - ->willReturn($this->createApplicationFeeDTO(6.50)); + $platformFee = $this->service->calculatePlatformFee($account, $eventSettings, 100.00, 1, 'USD'); - $fee = $this->service->calculateForOrder($account, $eventSettings, $orderItems, 'USD'); + $this->assertGreaterThan(0, $platformFee); - $this->assertEquals(6.50, $fee); + // Verify Stripe coverage + $newTotal = 100.00 + $platformFee; + $stripeAppFee = 0.0 + ($newTotal * 2.9 / 100); + $this->assertEqualsWithDelta($platformFee, $stripeAppFee, 0.01); } - public function testCalculateForOrderReturnsZeroWhenApplicationFeeServiceReturnsNull(): void + public function testZeroPercentageOnlyFixedFee(): void { $this->config->method('get')->willReturn(true); - $orderItems = collect([$this->createOrderItem(10, 1, 10)]); - $account = $this->createAccountConfig(); + $account = $this->createAccountConfig(0.50, 0.0); $eventSettings = $this->createEventSettings(true); - $this->applicationFeeCalculationService - ->method('calculateApplicationFee') - ->willReturn(null); + $platformFee = $this->service->calculatePlatformFee($account, $eventSettings, 100.00, 1, 'USD'); + + // With 0% percentage, formula simplifies to just the fixed fee + $this->assertEquals(0.50, $platformFee); + } + + public function testBothFeesZero(): void + { + $this->config->method('get')->willReturn(true); + + $account = $this->createAccountConfig(0.0, 0.0); + $eventSettings = $this->createEventSettings(true); - $fee = $this->service->calculateForOrder($account, $eventSettings, $orderItems, 'USD'); + $platformFee = $this->service->calculatePlatformFee($account, $eventSettings, 100.00, 1, 'USD'); - $this->assertEquals(0.0, $fee); + $this->assertEquals(0.0, $platformFee); } } diff --git a/frontend/src/components/common/EventCard/EventCard.module.scss b/frontend/src/components/common/EventCard/EventCard.module.scss index aa6ce4c69b..e1b4bfad6e 100644 --- a/frontend/src/components/common/EventCard/EventCard.module.scss +++ b/frontend/src/components/common/EventCard/EventCard.module.scss @@ -2,7 +2,7 @@ .eventCard { overflow: hidden; - padding: 0; + padding: 0 !important; transition: border-color 0.2s ease, box-shadow 0.2s ease; container-type: inline-size; min-width: 280px; diff --git a/frontend/src/components/common/InlineOrderSummary/InlineOrderSummary.module.scss b/frontend/src/components/common/InlineOrderSummary/InlineOrderSummary.module.scss index 0d9b625391..465b6dcddc 100644 --- a/frontend/src/components/common/InlineOrderSummary/InlineOrderSummary.module.scss +++ b/frontend/src/components/common/InlineOrderSummary/InlineOrderSummary.module.scss @@ -228,6 +228,56 @@ color: var(--checkout-text-secondary, #6B7280); } +.totalsLabelWithInfo { + font-size: 14px; + color: var(--checkout-text-secondary, #6B7280); + display: inline-flex; + align-items: center; + gap: 4px; +} + +.infoIcon { + display: inline-flex; + align-items: center; + justify-content: center; + color: var(--checkout-text-tertiary, #9CA3AF); + cursor: pointer; + opacity: 0.7; + transition: opacity 0.15s ease, color 0.15s ease; + + &:hover { + opacity: 1; + color: var(--checkout-text-secondary, #6B7280); + } +} + +.breakdownList { + display: flex; + flex-direction: column; + gap: 8px; +} + +.breakdownItem { + display: flex; + justify-content: space-between; + align-items: center; + gap: 12px; +} + +.breakdownName { + font-size: 13px; + color: var(--mantine-color-dimmed, #6B7280); + flex: 1; + min-width: 0; +} + +.breakdownValue { + font-size: 13px; + font-weight: 500; + color: var(--mantine-color-text, #111827); + flex-shrink: 0; +} + .totalsValue { font-size: 14px; color: var(--checkout-text-primary, #111827); diff --git a/frontend/src/components/common/InlineOrderSummary/index.tsx b/frontend/src/components/common/InlineOrderSummary/index.tsx index 1c7ada2ac0..fb7926c6bd 100644 --- a/frontend/src/components/common/InlineOrderSummary/index.tsx +++ b/frontend/src/components/common/InlineOrderSummary/index.tsx @@ -1,6 +1,6 @@ import {useState} from "react"; -import {Collapse} from "@mantine/core"; -import {IconCalendarEvent, IconChevronDown, IconShieldCheck, IconTag} from "@tabler/icons-react"; +import {Collapse, Popover} from "@mantine/core"; +import {IconCalendarEvent, IconChevronDown, IconInfoCircle, IconShieldCheck, IconTag} from "@tabler/icons-react"; import {t} from "@lingui/macro"; import classNames from "classnames"; import {Event, Order} from "../../../types.ts"; @@ -135,7 +135,30 @@ export const InlineOrderSummary = ({
- {t`Fees`} + + {t`Fees`} + {order.taxes_and_fees_rollup?.fees && order.taxes_and_fees_rollup.fees.length > 0 && ( + + + + + + + +
+ {order.taxes_and_fees_rollup.fees.map((fee, index) => ( +
+ {fee.name} + + {formatCurrency(fee.value, order.currency)} + +
+ ))} +
+
+
+ )} +
@@ -145,7 +168,30 @@ export const InlineOrderSummary = ({ {totalTax > 0 && (
- {t`Taxes`} + + {t`Taxes`} + {order.taxes_and_fees_rollup?.taxes && order.taxes_and_fees_rollup.taxes.length > 0 && ( + + + + + + + +
+ {order.taxes_and_fees_rollup.taxes.map((tax, index) => ( +
+ {tax.name} + + {formatCurrency(tax.value, order.currency)} + +
+ ))} +
+
+
+ )} +
{formatCurrency(totalTax, order.currency)} diff --git a/frontend/src/components/common/PlatformFeesSettings/PlatformFeesSettings.module.scss b/frontend/src/components/common/PlatformFeesSettings/PlatformFeesSettings.module.scss new file mode 100644 index 0000000000..5ac0f57b70 --- /dev/null +++ b/frontend/src/components/common/PlatformFeesSettings/PlatformFeesSettings.module.scss @@ -0,0 +1,48 @@ +.breakdown { + padding: 12px 0; +} + +.breakdownRow { + display: flex; + justify-content: space-between; + align-items: center; + padding: 6px 0; +} + +.breakdownLabel { + font-size: 14px; + color: var(--mantine-color-gray-6); +} + +.breakdownValue { + font-size: 14px; + color: var(--mantine-color-gray-7); +} + +.breakdownLabelBold { + font-size: 14px; + font-weight: 600; + color: var(--mantine-color-dark-7); +} + +.breakdownValueBold { + font-size: 14px; + font-weight: 600; + color: var(--mantine-color-dark-7); +} + +.breakdownDivider { + height: 1px; + background: var(--mantine-color-gray-3); + margin: 8px 0; +} + +.breakdownNote { + margin-top: 8px; + padding-top: 8px; + border-top: 1px dashed var(--mantine-color-gray-3); +} + +.activeCard { + border: 2px solid var(--mantine-color-teal-5); +} diff --git a/frontend/src/components/common/PlatformFeesSettings/index.tsx b/frontend/src/components/common/PlatformFeesSettings/index.tsx new file mode 100644 index 0000000000..0ff38b2462 --- /dev/null +++ b/frontend/src/components/common/PlatformFeesSettings/index.tsx @@ -0,0 +1,247 @@ +import {t} from "@lingui/macro"; +import {Anchor, Button, Grid, Group, NumberInput, SegmentedControl, Stack, Text, Title} from "@mantine/core"; +import {useEffect, useRef, useState} from "react"; +import {Card} from "../Card"; +import {HeadingWithDescription} from "../Card/CardHeading"; +import {formatCurrency} from "../../../utilites/currency.ts"; +import {IconArrowRight} from "@tabler/icons-react"; +import classes from "./PlatformFeesSettings.module.scss"; +import {AccountConfiguration} from "../../../types.ts"; + +const formatPercentage = (value: number) => { + return new Intl.NumberFormat('en-US', { + style: 'percent', + minimumFractionDigits: 2, + maximumFractionDigits: 2 + }).format(value / 100); +}; + +interface FeeBreakdownProps { + ticketPrice: number; + feePercentage: number; + fixedFee: number; + currency: string; + passToBuyer: boolean; +} + +const FeeBreakdown = ({ticketPrice, feePercentage, fixedFee, currency, passToBuyer}: FeeBreakdownProps) => { + const percentageRate = feePercentage / 100; + // Formula: P = (fixed + total * r) / (1 - r) + // This ensures the platform fee exactly covers what Stripe will charge + const platformFee = percentageRate >= 1 + ? fixedFee + (ticketPrice * percentageRate) + : (fixedFee + (ticketPrice * percentageRate)) / (1 - percentageRate); + const roundedPlatformFee = Math.round(platformFee * 100) / 100; + + const buyerPays = passToBuyer ? ticketPrice + roundedPlatformFee : ticketPrice; + const organizerReceives = passToBuyer ? ticketPrice : ticketPrice - roundedPlatformFee; + + return ( +
+
+ {t`Ticket price`} + {formatCurrency(ticketPrice, currency)} +
+ {passToBuyer && ( +
+ {t`Platform fee`} + +{formatCurrency(roundedPlatformFee, currency)} +
+ )} +
+
+ {t`Buyer pays`} + {formatCurrency(buyerPays, currency)} +
+
+ {t`You receive`} + {formatCurrency(organizerReceives, currency)} +
+ {!passToBuyer && roundedPlatformFee > 0 && ( +
+ + {t`Platform fee of ${formatCurrency(roundedPlatformFee, currency)} deducted from your payout`} + +
+ )} +
+ ); +}; + +export interface PlatformFeesSettingsProps { + configuration?: AccountConfiguration; + currentValue: boolean; + onSave: (passToBuyer: boolean) => void; + isLoading: boolean; + isSaving: boolean; + heading: string; + description: string; + feeHandlingLabel: string; + feeHandlingDescription: string; +} + +export const PlatformFeesSettings = ({ + configuration, + currentValue, + onSave, + isLoading, + isSaving, + heading, + description, + feeHandlingLabel, + feeHandlingDescription, +}: PlatformFeesSettingsProps) => { + const [samplePrice, setSamplePrice] = useState(50); + const [selectedOption, setSelectedOption] = useState<'pass' | 'absorb'>('absorb'); + const initializedRef = useRef(false); + + const feePercentage = configuration?.application_fees?.percentage || 0; + const fixedFee = configuration?.application_fees?.fixed || 0; + const feeCurrency = 'USD'; // Platform fees are always in USD + + const numericPrice = typeof samplePrice === 'number' ? samplePrice : parseFloat(samplePrice) || 0; + + const handleSave = () => { + onSave(selectedOption === 'pass'); + }; + + // Initialize selected option from currentValue only once after data loads + useEffect(() => { + if (!isLoading && !initializedRef.current) { + setSelectedOption(currentValue ? 'pass' : 'absorb'); + initializedRef.current = true; + } + }, [isLoading, currentValue]); + + return ( + + + + + {configuration && ( + + +
+ {t`Your Plan`} + {configuration.name} +
+
+ {t`Platform fee`} + + {formatPercentage(feePercentage)} + {fixedFee > 0 && ` + ${formatCurrency(fixedFee, feeCurrency)}`} + +
+
+
+ )} + +
+ {feeHandlingLabel} + + {feeHandlingDescription} + + + + +
+ setSelectedOption(value as 'pass' | 'absorb')} + data={[ + {label: t`Pass fee to buyer`, value: 'pass'}, + {label: t`Absorb fee`, value: 'absorb'}, + ]} + mb="md" + fullWidth + size="md" + /> + + + + + + {t`Pass to Buyer`} + {selectedOption === 'pass' && ( + {t`Selected`} + )} + + + {t`The platform fee is added to the ticket price. Buyers pay more, but you receive the full ticket price.`} + + + + + + + + {t`Absorb Fee`} + {selectedOption === 'absorb' && ( + {t`Selected`} + )} + + + {t`Buyers see a clean price. The platform fee is deducted from your payout.`} + + + + + + + +
+
+ + + +
+ {t`Additional Fees`} + + {t`You can configure additional service fees and taxes in your account settings.`} + +
+ + + {t`Configure Taxes & Fees`} + + + +
+
+
+
+ ); +}; diff --git a/frontend/src/components/routes/event/Settings/Sections/HomepageAndCheckoutSettings/index.tsx b/frontend/src/components/routes/event/Settings/Sections/HomepageAndCheckoutSettings/index.tsx index 640cb202e3..76fba0c3c2 100644 --- a/frontend/src/components/routes/event/Settings/Sections/HomepageAndCheckoutSettings/index.tsx +++ b/frontend/src/components/routes/event/Settings/Sections/HomepageAndCheckoutSettings/index.tsx @@ -26,7 +26,6 @@ export const HomepageAndCheckoutSettings = () => { order_timeout_in_minutes: 15, attendee_details_collection_method: 'PER_TICKET' as 'PER_TICKET' | 'PER_ORDER', show_marketing_opt_in: true, - pass_platform_fee_to_buyer: false, }, transformValues: (values) => ({ ...values, @@ -59,7 +58,6 @@ export const HomepageAndCheckoutSettings = () => { order_timeout_in_minutes: eventSettingsQuery.data.order_timeout_in_minutes, attendee_details_collection_method: eventSettingsQuery.data.attendee_details_collection_method || 'PER_TICKET', show_marketing_opt_in: eventSettingsQuery.data.show_marketing_opt_in ?? true, - pass_platform_fee_to_buyer: eventSettingsQuery.data.pass_platform_fee_to_buyer ?? false, }); } }, [eventSettingsQuery.isFetched]); @@ -130,13 +128,6 @@ export const HomepageAndCheckoutSettings = () => { {...form.getInputProps('show_marketing_opt_in', {type: 'checkbox'})} /> - - diff --git a/frontend/src/components/routes/event/Settings/Sections/PlatformFeesSettings/index.tsx b/frontend/src/components/routes/event/Settings/Sections/PlatformFeesSettings/index.tsx new file mode 100644 index 0000000000..c3b9628883 --- /dev/null +++ b/frontend/src/components/routes/event/Settings/Sections/PlatformFeesSettings/index.tsx @@ -0,0 +1,48 @@ +import {t} from "@lingui/macro"; +import {useParams} from "react-router"; +import {useEffect, useState} from "react"; +import {showSuccess} from "../../../../../../utilites/notifications.tsx"; +import {useUpdateEventSettings} from "../../../../../../mutations/useUpdateEventSettings.ts"; +import {useGetEventSettings} from "../../../../../../queries/useGetEventSettings.ts"; +import {useGetAccount} from "../../../../../../queries/useGetAccount.ts"; +import {PlatformFeesSettings as PlatformFeesSettingsBase} from "../../../../../common/PlatformFeesSettings"; + +export const PlatformFeesSettings = () => { + const {eventId} = useParams(); + const eventSettingsQuery = useGetEventSettings(eventId); + const accountQuery = useGetAccount(); + const updateMutation = useUpdateEventSettings(); + const [currentValue, setCurrentValue] = useState(false); + + useEffect(() => { + if (eventSettingsQuery?.isFetched && eventSettingsQuery?.data) { + setCurrentValue(eventSettingsQuery.data.pass_platform_fee_to_buyer ?? false); + } + }, [eventSettingsQuery.isFetched, eventSettingsQuery.data]); + + const handleSave = (passToBuyer: boolean) => { + updateMutation.mutate({ + eventSettings: { pass_platform_fee_to_buyer: passToBuyer }, + eventId: eventId, + }, { + onSuccess: () => { + showSuccess(t`Successfully Updated Platform Fee Settings`); + setCurrentValue(passToBuyer); + }, + }); + }; + + return ( + + ); +}; diff --git a/frontend/src/components/routes/event/Settings/index.tsx b/frontend/src/components/routes/event/Settings/index.tsx index b5e1ddd793..9b658bdd61 100644 --- a/frontend/src/components/routes/event/Settings/index.tsx +++ b/frontend/src/components/routes/event/Settings/index.tsx @@ -16,60 +16,79 @@ import { IconCreditCard, IconHome, IconMapPin, + IconPercentage, } from "@tabler/icons-react"; import {useMediaQuery} from "@mantine/hooks"; -import {useState} from "react"; +import {useMemo, useState} from "react"; import {Card} from "../../../common/Card"; import {PaymentAndInvoicingSettings} from "./Sections/PaymentSettings"; +import {PlatformFeesSettings} from "./Sections/PlatformFeesSettings"; +import {useGetAccount} from "../../../../queries/useGetAccount.ts"; export const Settings = () => { - const SECTIONS = [ - { - id: 'event-details', - label: t`Event Details`, - icon: IconBuildingStore, - component: EventDetailsForm - }, - { - id: 'location-settings', - label: t`Location`, - icon: IconMapPin, - component: LocationSettings - }, - { - id: 'homepage-settings', - label: t`Checkout`, - icon: IconHome, - component: HomepageAndCheckoutSettings - }, - { - id: 'seo-settings', - label: t`SEO`, - icon: IconBrandGoogleAnalytics, - component: SeoSettings - }, - { - id: 'email-settings', - label: t`Email & Templates`, - icon: IconAt, - component: EmailSettings - }, - { - id: 'misc-settings', - label: t`Miscellaneous`, - icon: IconAdjustments, - component: MiscSettings - }, - { - id: 'payment-settings', - label: t`Payment & Invoicing`, - icon: IconCreditCard, - component: PaymentAndInvoicingSettings, + const {data: account} = useGetAccount(); + const isSaasMode = account?.is_saas_mode_enabled; + + const SECTIONS = useMemo(() => { + const baseSections = [ + { + id: 'event-details', + label: t`Event Details`, + icon: IconBuildingStore, + component: EventDetailsForm + }, + { + id: 'location-settings', + label: t`Location`, + icon: IconMapPin, + component: LocationSettings + }, + { + id: 'homepage-settings', + label: t`Checkout`, + icon: IconHome, + component: HomepageAndCheckoutSettings + }, + { + id: 'seo-settings', + label: t`SEO`, + icon: IconBrandGoogleAnalytics, + component: SeoSettings + }, + { + id: 'email-settings', + label: t`Email & Templates`, + icon: IconAt, + component: EmailSettings + }, + { + id: 'misc-settings', + label: t`Miscellaneous`, + icon: IconAdjustments, + component: MiscSettings + }, + { + id: 'payment-settings', + label: t`Payment & Invoicing`, + icon: IconCreditCard, + component: PaymentAndInvoicingSettings, + } + ]; + + if (isSaasMode) { + baseSections.splice(baseSections.length - 1, 0, { + id: 'platform-fees', + label: t`Platform Fees`, + icon: IconPercentage, + component: PlatformFeesSettings, + }); } - ]; + + return baseSections; + }, [isSaasMode]); const isLargeScreen = useMediaQuery('(min-width: 1200px)', true); - const [activeSection, setActiveSection] = useState(SECTIONS[0].id); + const [activeSection, setActiveSection] = useState('event-details'); const handleClick = (sectionId: string) => { setActiveSection(sectionId); diff --git a/frontend/src/components/routes/organizer/Settings/Sections/EventDefaults/index.tsx b/frontend/src/components/routes/organizer/Settings/Sections/EventDefaults/index.tsx index b0a23fd7dc..2c346abaae 100644 --- a/frontend/src/components/routes/organizer/Settings/Sections/EventDefaults/index.tsx +++ b/frontend/src/components/routes/organizer/Settings/Sections/EventDefaults/index.tsx @@ -21,7 +21,6 @@ export const EventDefaults = () => { initialValues: { default_attendee_details_collection_method: 'PER_TICKET' as 'PER_TICKET' | 'PER_ORDER', default_show_marketing_opt_in: true, - default_pass_platform_fee_to_buyer: false, } }); @@ -47,12 +46,11 @@ export const EventDefaults = () => { form.setValues({ default_attendee_details_collection_method: organizerSettingsQuery.data.default_attendee_details_collection_method || 'PER_TICKET', default_show_marketing_opt_in: organizerSettingsQuery.data.default_show_marketing_opt_in ?? true, - default_pass_platform_fee_to_buyer: organizerSettingsQuery.data.default_pass_platform_fee_to_buyer ?? false, }); } }, [organizerSettingsQuery.isFetched]); - const handleSubmit = (values: { default_attendee_details_collection_method: 'PER_TICKET' | 'PER_ORDER'; default_show_marketing_opt_in: boolean; default_pass_platform_fee_to_buyer: boolean }) => { + const handleSubmit = (values: { default_attendee_details_collection_method: 'PER_TICKET' | 'PER_ORDER'; default_show_marketing_opt_in: boolean }) => { updateMutation.mutate({ organizerSettings: values, organizerId: organizerId, @@ -89,13 +87,6 @@ export const EventDefaults = () => { {...form.getInputProps('default_show_marketing_opt_in', {type: 'checkbox'})} /> - -