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
36 changes: 36 additions & 0 deletions backend/app/Console/Commands/BootstrapDevDataCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ public function handle(
return self::FAILURE;
}

$this->seedCurrencyDefaultConfigurations();

$email = $this->option('email') ?: 'agent+'.now()->format('YmdHis').'@dev.test';
$password = $this->option('password');

Expand Down Expand Up @@ -208,4 +210,38 @@ private function createProduct(
'price_id' => DB::table('product_prices')->where('product_id', $product->getId())->value('id'),
];
}

private function seedCurrencyDefaultConfigurations(): void
{
$fees = [
'USD' => 0.60,
'EUR' => 0.50,
'GBP' => 0.45,
'AUD' => 0.85,
];

foreach ($fees as $currency => $fixedFee) {
$exists = DB::table('organizer_configurations')
->where('default_for_currency', $currency)
->whereNull('deleted_at')
->exists();

if ($exists) {
continue;
}

DB::table('organizer_configurations')->insert([
'name' => "Standard ($currency)",
'is_system_default' => false,
'application_fees' => json_encode([
'percentage' => 1.25,
'fixed' => $fixedFee,
'currency' => $currency,
], JSON_THROW_ON_ERROR),
'default_for_currency' => $currency,
'created_at' => now(),
'updated_at' => now(),
]);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ abstract class OrganizerConfigurationDomainObjectAbstract extends \HiEvents\Doma
final public const CREATED_AT = 'created_at';
final public const UPDATED_AT = 'updated_at';
final public const DELETED_AT = 'deleted_at';
final public const DEFAULT_FOR_CURRENCY = 'default_for_currency';

protected int $id;
protected string $name;
Expand All @@ -29,6 +30,7 @@ abstract class OrganizerConfigurationDomainObjectAbstract extends \HiEvents\Doma
protected ?string $created_at = null;
protected ?string $updated_at = null;
protected ?string $deleted_at = null;
protected ?string $default_for_currency = null;

public function toArray(): array
{
Expand All @@ -42,6 +44,7 @@ public function toArray(): array
'created_at' => $this->created_at ?? null,
'updated_at' => $this->updated_at ?? null,
'deleted_at' => $this->deleted_at ?? null,
'default_for_currency' => $this->default_for_currency ?? null,
];
}

Expand Down Expand Up @@ -143,4 +146,15 @@ public function getDeletedAt(): ?string
{
return $this->deleted_at;
}

public function setDefaultForCurrency(?string $default_for_currency): self
{
$this->default_for_currency = $default_for_currency;
return $this;
}

public function getDefaultForCurrency(): ?string
{
return $this->default_for_currency;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,9 @@ public function getApplicationFeeCurrency(): string
{
return $this->getApplicationFees()['currency'] ?? 'USD';
}

public function isDefault(): bool
{
return $this->getIsSystemDefault() || $this->getDefaultForCurrency() !== null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use HiEvents\Resources\Organizer\OrganizerConfigurationResource;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;

class UpdateConfigurationAction extends BaseAction
{
Expand All @@ -30,6 +31,18 @@ public function __invoke(Request $request, int $configurationId): JsonResponse
'bypass_application_fees' => 'sometimes|boolean',
]);

$existingConfiguration = $this->repository->findById($configurationId);
$defaultForCurrency = $existingConfiguration->getDefaultForCurrency();
$feeCurrency = $validated['application_fees']['currency'] ?? null;

if ($defaultForCurrency !== null && $feeCurrency !== null && $feeCurrency !== $defaultForCurrency) {
throw ValidationException::withMessages([
'application_fees.currency' => __('The fee currency of the :currency default configuration must remain :currency.', [
'currency' => $defaultForCurrency,
]),
]);
}

$configuration = $this->repository->updateFromArray(
id: $configurationId,
attributes: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ public function toArray(Request $request): array
'id' => $configuration->id,
'name' => $configuration->name,
'is_system_default' => $configuration->is_system_default,
'default_for_currency' => $configuration->default_for_currency,
'application_fees' => $configuration->application_fees ?? [
'percentage' => 0,
'fixed' => 0,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public function toArray($request): array
'id' => $this->getId(),
'name' => $this->getName(),
'is_system_default' => $this->getIsSystemDefault(),
'default_for_currency' => $this->getDefaultForCurrency(),
'application_fees' => [
'percentage' => $this->getPercentageApplicationFee(),
'fixed' => $this->getFixedApplicationFee(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ public function handle(int $configurationId): void
{
$configuration = $this->repository->findById($configurationId);

if ($configuration->getIsSystemDefault()) {
if ($configuration->isDefault()) {
throw new CannotDeleteEntityException(
__('The system default configuration cannot be deleted.')
__('Default configurations cannot be deleted.')
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public function handle(UpdateOrganizerConfigurationDTO $dto): OrganizerConfigura
$currentConfiguration = $organizer->getOrganizerConfiguration();

if ($currentConfiguration !== null
&& ! $currentConfiguration->getIsSystemDefault()
&& ! $currentConfiguration->isDefault()
&& $this->isConfigurationDedicatedTo($currentConfiguration->getId(), $organizer->getId())
) {
return $this->configurationRepository->updateFromArray(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use HiEvents\Repository\Interfaces\OrganizerStripePlatformRepositoryInterface;
use HiEvents\Services\Application\Handlers\Organizer\Payment\Stripe\DTO\CopyStripeConnectAccountDTO;
use HiEvents\Services\Application\Handlers\Organizer\Payment\Stripe\DTO\CreateStripeConnectAccountResponse;
use HiEvents\Services\Domain\Organizer\AssignCurrencyDefaultOrganizerConfigurationService;
use HiEvents\Services\Domain\Payment\Stripe\StripeAccountSyncService;
use Illuminate\Config\Repository;
use Illuminate\Database\DatabaseManager;
Expand All @@ -22,6 +23,7 @@ public function __construct(
private readonly OrganizerRepositoryInterface $organizerRepository,
private readonly OrganizerStripePlatformRepositoryInterface $organizerStripePlatformRepository,
private readonly StripeAccountSyncService $stripeAccountSyncService,
private readonly AssignCurrencyDefaultOrganizerConfigurationService $assignCurrencyDefaultOrganizerConfigurationService,
private readonly DatabaseManager $databaseManager,
private readonly Repository $config,
) {}
Expand Down Expand Up @@ -114,6 +116,11 @@ private function copy(CopyStripeConnectAccountDTO $command): CreateStripeConnect
stripeAccountId: $sourcePlatform->getStripeAccountId(),
);

$this->assignCurrencyDefaultOrganizerConfigurationService->assignForCountry(
organizerId: (int) $target->getId(),
countryCode: $sourceDetails['country'] ?? null,
);

return new CreateStripeConnectAccountResponse(
stripeConnectAccountType: $sourcePlatform->getStripeConnectAccountType() ?? '',
stripeAccountId: $sourcePlatform->getStripeAccountId() ?? '',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
<?php

declare(strict_types=1);

namespace HiEvents\Services\Domain\Organizer;

use HiEvents\DomainObjects\Generated\OrganizerConfigurationDomainObjectAbstract;
use HiEvents\DomainObjects\Generated\OrganizerDomainObjectAbstract;
use HiEvents\DomainObjects\OrganizerConfigurationDomainObject;
use HiEvents\DomainObjects\OrganizerDomainObject;
use HiEvents\Repository\Interfaces\OrganizerConfigurationRepositoryInterface;
use HiEvents\Repository\Interfaces\OrganizerRepositoryInterface;
use Illuminate\Config\Repository;
use Psr\Log\LoggerInterface;
use Throwable;

class AssignCurrencyDefaultOrganizerConfigurationService
{
private const array CURRENCY_BY_COUNTRY = [
'US' => 'USD',
'GB' => 'GBP',
'AU' => 'AUD',
];

private const string FALLBACK_CURRENCY = 'EUR';

public function __construct(
private readonly OrganizerRepositoryInterface $organizerRepository,
private readonly OrganizerConfigurationRepositoryInterface $organizerConfigurationRepository,
private readonly Repository $config,
private readonly LoggerInterface $logger,
) {}

public function assignForCountry(int $organizerId, ?string $countryCode): void
{
if (! $this->config->get('app.saas_mode_enabled')) {
return;
}

try {
$this->assign($organizerId, $countryCode);
} catch (Throwable $exception) {
$this->logger->error('Failed to assign currency default organizer configuration', [
'organizer_id' => $organizerId,
'country_code' => $countryCode,
'error' => $exception->getMessage(),
]);
}
}

private function assign(int $organizerId, ?string $countryCode): void
{
if ($countryCode === null || trim($countryCode) === '') {
$this->logger->info('No Stripe account country available, skipping configuration assignment', [
'organizer_id' => $organizerId,
]);

return;
}

$currency = self::CURRENCY_BY_COUNTRY[strtoupper(trim($countryCode))] ?? self::FALLBACK_CURRENCY;

if (! $this->currencyDefaultConfigurationsExist()) {
return;
}

/** @var OrganizerDomainObject|null $organizer */
$organizer = $this->organizerRepository->findFirstWhere(['id' => $organizerId]);
if ($organizer === null) {
return;
}

if (! $this->isOrganizerOnDefaultConfiguration($organizer)) {
return;
}

/** @var OrganizerConfigurationDomainObject|null $target */
$target = $this->organizerConfigurationRepository->findFirstWhere([
OrganizerConfigurationDomainObjectAbstract::DEFAULT_FOR_CURRENCY => $currency,
]);

if ($target === null) {
$this->logger->warning('No default organizer configuration found for currency', [
'organizer_id' => $organizerId,
'country_code' => $countryCode,
'currency' => $currency,
]);

return;
}

if ($target->getId() === $organizer->getOrganizerConfigurationId()) {
return;
}

$this->organizerRepository->updateWhere(
attributes: [
OrganizerDomainObjectAbstract::ORGANIZER_CONFIGURATION_ID => $target->getId(),
],
where: [
'id' => $organizerId,
],
);

$this->logger->info('Assigned currency default configuration to organizer', [
'organizer_id' => $organizerId,
'country_code' => $countryCode,
'currency' => $currency,
'organizer_configuration_id' => $target->getId(),
]);
}

private function currencyDefaultConfigurationsExist(): bool
{
return $this->organizerConfigurationRepository->countWhere([
[OrganizerConfigurationDomainObjectAbstract::DEFAULT_FOR_CURRENCY, 'not null', null],
]) > 0;
}

private function isOrganizerOnDefaultConfiguration(OrganizerDomainObject $organizer): bool
{
if ($organizer->getOrganizerConfigurationId() === null) {
return true;
}

/** @var OrganizerConfigurationDomainObject|null $currentConfiguration */
$currentConfiguration = $this->organizerConfigurationRepository->findFirstWhere([
'id' => $organizer->getOrganizerConfigurationId(),
]);

if ($currentConfiguration === null) {
return true;
}

return $currentConfiguration->isDefault();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use HiEvents\Repository\Interfaces\OrganizerRepositoryInterface;
use HiEvents\Repository\Interfaces\OrganizerStripePlatformRepositoryInterface;
use HiEvents\Repository\Interfaces\OrganizerVatSettingRepositoryInterface;
use HiEvents\Services\Domain\Organizer\AssignCurrencyDefaultOrganizerConfigurationService;
use Illuminate\Config\Repository;
use Psr\Log\LoggerInterface;
use Stripe\Account;
Expand All @@ -26,6 +27,7 @@ public function __construct(
private readonly OrganizerStripePlatformRepositoryInterface $organizerStripePlatformRepository,
private readonly OrganizerVatSettingRepositoryInterface $vatSettingRepository,
private readonly Repository $config,
private readonly AssignCurrencyDefaultOrganizerConfigurationService $assignCurrencyDefaultOrganizerConfigurationService,
) {}

public function isStripeAccountComplete(Account $stripeAccount): bool
Expand Down Expand Up @@ -99,6 +101,10 @@ public function syncStripeAccountStatusByAccountId(Account $stripeAccount): void
stripeAccountId: $stripeAccount->id,
organizerStripePlatformId: $organizerRow->getId(),
);
$this->assignCurrencyDefaultOrganizerConfigurationService->assignForCountry(
organizerId: $organizerRow->getOrganizerId(),
countryCode: $stripeAccount->country,
);
}
}

Expand Down Expand Up @@ -129,6 +135,10 @@ public function markAccountAsCompleteForOrganizer(
stripeAccountId: $stripeAccount->id,
organizerStripePlatformId: $organizerStripePlatform->getId(),
);
$this->assignCurrencyDefaultOrganizerConfigurationService->assignForCountry(
organizerId: $organizerStripePlatform->getOrganizerId(),
countryCode: $stripeAccount->country,
);
}

public function seedVatSettingForOrganizerIfMissing(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

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

return new class extends Migration
{
public function up(): void
{
Schema::table('organizer_configurations', static function (Blueprint $table) {
$table->string('default_for_currency', 3)->nullable();
});

DB::statement('
CREATE UNIQUE INDEX IF NOT EXISTS organizer_configurations_default_per_currency
ON organizer_configurations (default_for_currency)
WHERE default_for_currency IS NOT NULL AND deleted_at IS NULL
');
}

public function down(): void
{
DB::statement('DROP INDEX IF EXISTS organizer_configurations_default_per_currency');

Schema::table('organizer_configurations', static function (Blueprint $table) {
$table->dropColumn('default_for_currency');
});
}
};
Loading
Loading