Skip to content
Merged
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
@@ -0,0 +1,37 @@
<?php

namespace HiEvents\Http\Actions\Organizers\Stripe;

use HiEvents\DomainObjects\Enums\Role;
use HiEvents\DomainObjects\OrganizerDomainObject;
use HiEvents\Exceptions\ResourceNotFoundException;
use HiEvents\Http\Actions\BaseAction;
use HiEvents\Services\Application\Handlers\Organizer\Payment\Stripe\DisconnectStripeConnectAccountHandler;
use Symfony\Component\HttpFoundation\Response;

class DisconnectStripeConnectAccountAction extends BaseAction
{
public function __construct(
private readonly DisconnectStripeConnectAccountHandler $disconnectStripeConnectAccountHandler,
) {}

public function __invoke(int $organizerId, string $stripeAccountId): Response
{
$this->isActionAuthorized($organizerId, OrganizerDomainObject::class, Role::ADMIN);

try {
$this->disconnectStripeConnectAccountHandler->handle(
organizerId: $organizerId,
accountId: $this->getAuthenticatedAccountId(),
stripeAccountId: $stripeAccountId,
);
} catch (ResourceNotFoundException $e) {
return $this->errorResponse(
message: $e->getMessage(),
statusCode: Response::HTTP_NOT_FOUND,
);
}

return $this->deletedResponse();
}
}
2 changes: 1 addition & 1 deletion backend/app/Http/Request/Image/CreateImageRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public function rules(): array
'image' => [
'required',
'image',
'max:8192', // 8mb
'max:5120', // 5mb
'dimensions:min_width='.$minWidth.',min_height='.$minHeight.',max_width=4000,max_height=4000',
'mimes:jpeg,png,jpg,webp',
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
use HiEvents\DomainObjects\EventLocationDomainObject;
use HiEvents\DomainObjects\EventOccurrenceDomainObject;
use HiEvents\DomainObjects\LocationDomainObject;
use HiEvents\DomainObjects\Status\OrderStatus;
use HiEvents\Events\Dispatcher;
use HiEvents\Events\EventUpdateEvent;
use HiEvents\Exceptions\CannotChangeCurrencyException;
Expand Down Expand Up @@ -66,8 +65,11 @@ private function updateEventAttributes(UpdateEventDTO $eventData): void
);
}

if ($eventData->currency !== null && $eventData->currency !== $existingEvent->getCurrency()) {
$this->checkForCompletedOrders($eventData);
$isCurrencyChanging = $eventData->currency !== null && $eventData->currency !== $existingEvent->getCurrency();

if ($isCurrencyChanging) {
$this->databaseManager->statement('SELECT pg_advisory_xact_lock(?)', [$eventData->id]);
$this->guardCurrencyChange($eventData);
}

$attributes = [
Expand All @@ -86,6 +88,16 @@ private function updateEventAttributes(UpdateEventDTO $eventData): void
],
);

if ($isCurrencyChanging) {
$this->orderRepository->updateWhere(
attributes: ['currency' => $eventData->currency],
where: [
'event_id' => $eventData->id,
['total_gross', '=', 0],
],
);
}

$this->updateSingleOccurrenceDates($eventData, $existingEvent);
}

Expand Down Expand Up @@ -151,16 +163,16 @@ private function getUpdateEvent(UpdateEventDTO $eventData): EventDomainObject
/**
* @throws CannotChangeCurrencyException
*/
private function checkForCompletedOrders(UpdateEventDTO $eventData): void
private function guardCurrencyChange(UpdateEventDTO $eventData): void
{
$orders = $this->orderRepository->findWhere([
$paidOrder = $this->orderRepository->findFirstWhere([
'event_id' => $eventData->id,
'status' => OrderStatus::COMPLETED->name,
['total_gross', '>', 0],
]);

if ($orders->isNotEmpty()) {
if ($paidOrder !== null) {
throw new CannotChangeCurrencyException(
__('You cannot change the currency of an event that has completed orders'),
__('You cannot change the currency of an event that has paid orders. To use a different currency, duplicate the event and change the currency on the new event.'),
);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

namespace HiEvents\Services\Application\Handlers\Organizer\Payment\Stripe;

use HiEvents\Exceptions\ResourceNotFoundException;
use HiEvents\Repository\Interfaces\OrganizerRepositoryInterface;
use HiEvents\Repository\Interfaces\OrganizerStripePlatformRepositoryInterface;
use Psr\Log\LoggerInterface;

class DisconnectStripeConnectAccountHandler
{
public function __construct(
private readonly OrganizerRepositoryInterface $organizerRepository,
private readonly OrganizerStripePlatformRepositoryInterface $organizerStripePlatformRepository,
private readonly LoggerInterface $logger,
) {}

/**
* @throws ResourceNotFoundException
*/
public function handle(int $organizerId, int $accountId, string $stripeAccountId): void
{
$organizer = $this->organizerRepository->findFirstWhere([
'id' => $organizerId,
'account_id' => $accountId,
]);

if ($organizer === null) {
throw new ResourceNotFoundException(__('Organizer not found.'));
}

$deletedCount = $this->organizerStripePlatformRepository->deleteWhere([
'organizer_id' => $organizerId,
'stripe_account_id' => $stripeAccountId,
]);

if ($deletedCount === 0) {
throw new ResourceNotFoundException(__('Stripe connection not found.'));
}

$this->logger->info('Stripe connect account disconnected from organizer', [
'organizer_id' => $organizerId,
'account_id' => $accountId,
'stripe_account_id' => $stripeAccountId,
]);
}
}
2 changes: 1 addition & 1 deletion backend/app/Validators/Rules/RulesHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class RulesHelper
public const IMAGE_RULES = [
'required',
'image',
'max:8192', // 8mb
'max:5120', // 5mb
'dimensions:min_width=600,min_height=50,max_width=4000,max_height=4000',
'mimes:jpeg,png,jpg,webp',
];
Expand Down
3 changes: 3 additions & 0 deletions backend/routes/api.php
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@
use HiEvents\Http\Actions\Organizers\Stats\GetOrganizerStatsAction;
use HiEvents\Http\Actions\Organizers\Stripe\CopyStripeConnectAccountAction;
use HiEvents\Http\Actions\Organizers\Stripe\CreateStripeConnectAccountAction;
use HiEvents\Http\Actions\Organizers\Stripe\DisconnectStripeConnectAccountAction;
use HiEvents\Http\Actions\Organizers\Stripe\GetStripeConnectAccountsAction;
use HiEvents\Http\Actions\Organizers\UpdateOrganizerLocationAction;
use HiEvents\Http\Actions\Organizers\UpdateOrganizerStatusAction;
Expand Down Expand Up @@ -355,6 +356,8 @@ function (Router $router): void {
$router->get('/organizers/{organizerId}/stripe/connect_accounts', GetStripeConnectAccountsAction::class);
$router->post('/organizers/{organizerId}/stripe/connect', CreateStripeConnectAccountAction::class);
$router->post('/organizers/{organizerId}/stripe/copy_from/{sourceOrganizerId}', CopyStripeConnectAccountAction::class);
$router->delete('/organizers/{organizerId}/stripe/connect_accounts/{stripeAccountId}', DisconnectStripeConnectAccountAction::class)
->where('stripeAccountId', '[A-Za-z0-9_]+');

// VAT Settings - Organizer level
$router->get('/organizers/{organizerId}/vat-settings', GetOrganizerVatSettingAction::class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

use HiEvents\DomainObjects\EventDomainObject;
use HiEvents\DomainObjects\OrderDomainObject;
use HiEvents\DomainObjects\Status\OrderStatus;
use HiEvents\Events\Dispatcher;
use HiEvents\Exceptions\CannotChangeCurrencyException;
use HiEvents\Repository\Interfaces\EventOccurrenceRepositoryInterface;
Expand All @@ -16,7 +15,6 @@
use HiEvents\Services\Application\Handlers\Event\UpdateEventHandler;
use HiEvents\Services\Infrastructure\HtmlPurifier\HtmlPurifierService;
use Illuminate\Database\DatabaseManager;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Bus;
use Mockery;
use Mockery\MockInterface;
Expand All @@ -34,6 +32,8 @@ class UpdateEventHandlerTest extends TestCase

private Dispatcher|MockInterface $dispatcher;

private DatabaseManager|MockInterface $databaseManager;

private UpdateEventHandler $handler;

protected function setUp(): void
Expand All @@ -49,13 +49,13 @@ protected function setUp(): void
$this->purifier = Mockery::mock(HtmlPurifierService::class);
$this->dispatcher = Mockery::mock(Dispatcher::class);

$databaseManager = Mockery::mock(DatabaseManager::class);
$databaseManager->shouldReceive('transaction')->andReturnUsing(fn ($cb) => $cb());
$this->databaseManager = Mockery::mock(DatabaseManager::class);
$this->databaseManager->shouldReceive('transaction')->andReturnUsing(fn ($cb) => $cb());

$this->handler = new UpdateEventHandler(
$this->eventRepository,
$this->dispatcher,
$databaseManager,
$this->databaseManager,
$this->orderRepository,
$this->purifier,
$this->occurrenceRepository,
Expand All @@ -68,7 +68,7 @@ protected function tearDown(): void
parent::tearDown();
}

public function test_throws_when_changing_currency_with_completed_orders(): void
public function test_throws_when_changing_currency_with_paid_orders(): void
{
$existing = Mockery::mock(EventDomainObject::class);
$existing->shouldReceive('getCurrency')->andReturn('USD');
Expand All @@ -77,14 +77,17 @@ public function test_throws_when_changing_currency_with_completed_orders(): void
->shouldReceive('findFirstWhere')
->andReturn($existing);

$completedOrder = Mockery::mock(OrderDomainObject::class);
$this->databaseManager
->shouldReceive('statement')
->once()
->with('SELECT pg_advisory_xact_lock(?)', [1]);

$this->orderRepository
->shouldReceive('findWhere')
->with([
'event_id' => 1,
'status' => OrderStatus::COMPLETED->name,
])
->andReturn(new Collection([$completedOrder]));
->shouldReceive('findFirstWhere')
->with(['event_id' => 1, ['total_gross', '>', 0]])
->andReturn(Mockery::mock(OrderDomainObject::class));

$this->orderRepository->shouldNotReceive('updateWhere');

$this->expectException(CannotChangeCurrencyException::class);

Expand All @@ -97,7 +100,7 @@ public function test_throws_when_changing_currency_with_completed_orders(): void
));
}

public function test_allows_currency_change_when_no_completed_orders(): void
public function test_allows_currency_change_and_updates_free_orders_when_no_paid_orders(): void
{
$existing = Mockery::mock(EventDomainObject::class);
$existing->shouldReceive('getCurrency')->andReturn('USD');
Expand All @@ -112,13 +115,15 @@ public function test_allows_currency_change_when_no_completed_orders(): void
->shouldReceive('findFirstWhere')
->andReturn($existing, $reloaded);

$this->databaseManager
->shouldReceive('statement')
->once()
->with('SELECT pg_advisory_xact_lock(?)', [1]);

$this->orderRepository
->shouldReceive('findWhere')
->with([
'event_id' => 1,
'status' => OrderStatus::COMPLETED->name,
])
->andReturn(new Collection);
->shouldReceive('findFirstWhere')
->with(['event_id' => 1, ['total_gross', '>', 0]])
->andReturnNull();

$this->purifier->shouldReceive('purify')->andReturn(null);

Expand All @@ -130,6 +135,14 @@ public function test_allows_currency_change_when_no_completed_orders(): void
['id' => 1, 'account_id' => 5],
);

$this->orderRepository
->shouldReceive('updateWhere')
->once()
->with(
['currency' => 'EUR'],
['event_id' => 1, ['total_gross', '=', 0]],
);

$this->dispatcher->shouldReceive('dispatchEvent')->once();

$result = $this->handler->handle(new UpdateEventDTO(
Expand All @@ -143,7 +156,7 @@ public function test_allows_currency_change_when_no_completed_orders(): void
$this->assertSame($reloaded, $result);
}

public function test_skips_completed_orders_check_when_currency_unchanged(): void
public function test_skips_orders_check_when_currency_unchanged(): void
{
$existing = Mockery::mock(EventDomainObject::class);
$existing->shouldReceive('getCurrency')->andReturn('USD');
Expand All @@ -158,7 +171,9 @@ public function test_skips_completed_orders_check_when_currency_unchanged(): voi
->shouldReceive('findFirstWhere')
->andReturn($existing, $reloaded);

$this->orderRepository->shouldNotReceive('findWhere');
$this->databaseManager->shouldNotReceive('statement');
$this->orderRepository->shouldNotReceive('findFirstWhere');
$this->orderRepository->shouldNotReceive('updateWhere');
$this->purifier->shouldReceive('purify')->andReturn(null);
$this->eventRepository->shouldReceive('updateWhere')->once();
$this->dispatcher->shouldReceive('dispatchEvent')->once();
Expand Down
Loading
Loading