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
Expand Up @@ -22,6 +22,9 @@ abstract class AccountVatSettingDomainObjectAbstract extends \HiEvents\DomainObj
final public const CREATED_AT = 'created_at';
final public const UPDATED_AT = 'updated_at';
final public const DELETED_AT = 'deleted_at';
final public const VAT_VALIDATION_STATUS = 'vat_validation_status';
final public const VAT_VALIDATION_ERROR = 'vat_validation_error';
final public const VAT_VALIDATION_ATTEMPTS = 'vat_validation_attempts';

protected int $id;
protected int $account_id;
Expand All @@ -35,6 +38,9 @@ abstract class AccountVatSettingDomainObjectAbstract extends \HiEvents\DomainObj
protected ?string $created_at = null;
protected ?string $updated_at = null;
protected ?string $deleted_at = null;
protected string $vat_validation_status = 'PENDING';
protected ?string $vat_validation_error = null;
protected int $vat_validation_attempts = 0;

public function toArray(): array
{
Expand All @@ -51,6 +57,9 @@ public function toArray(): array
'created_at' => $this->created_at ?? null,
'updated_at' => $this->updated_at ?? null,
'deleted_at' => $this->deleted_at ?? null,
'vat_validation_status' => $this->vat_validation_status ?? null,
'vat_validation_error' => $this->vat_validation_error ?? null,
'vat_validation_attempts' => $this->vat_validation_attempts ?? null,
];
}

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

public function setVatValidationStatus(string $vat_validation_status): self
{
$this->vat_validation_status = $vat_validation_status;
return $this;
}

public function getVatValidationStatus(): string
{
return $this->vat_validation_status;
}

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

public function getVatValidationError(): ?string
{
return $this->vat_validation_error;
}

public function setVatValidationAttempts(int $vat_validation_attempts): self
{
$this->vat_validation_attempts = $vat_validation_attempts;
return $this;
}

public function getVatValidationAttempts(): int
{
return $this->vat_validation_attempts;
}
}
18 changes: 18 additions & 0 deletions backend/app/DomainObjects/Status/VatValidationStatus.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

namespace HiEvents\DomainObjects\Status;

use HiEvents\DomainObjects\Enums\BaseEnum;

enum VatValidationStatus: string
{
use BaseEnum;

case PENDING = 'PENDING';
case VALIDATING = 'VALIDATING';
case VALID = 'VALID';
case INVALID = 'INVALID';
case FAILED = 'FAILED';
}
180 changes: 180 additions & 0 deletions backend/app/Jobs/Vat/ValidateVatNumberJob.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
<?php

declare(strict_types=1);

namespace HiEvents\Jobs\Vat;

use DateTimeInterface;
use HiEvents\DomainObjects\Status\VatValidationStatus;
use HiEvents\Repository\Interfaces\AccountVatSettingRepositoryInterface;
use HiEvents\Services\Infrastructure\Vat\ViesValidationService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Psr\Log\LoggerInterface;
use Throwable;

class ValidateVatNumberJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

public int $tries = 15;

public int $maxExceptions = 15;

public int $timeout = 15;

public function __construct(
private readonly int $accountVatSettingId,
private readonly string $vatNumber,
) {}

public function handle(
ViesValidationService $viesService,
AccountVatSettingRepositoryInterface $repository,
LoggerInterface $logger,
): void {
$logger->info('VAT validation job started', [
'account_vat_setting_id' => $this->accountVatSettingId,
'vat_number' => $this->maskVatNumber($this->vatNumber),
'attempt' => $this->attempts(),
]);

$repository->updateFromArray($this->accountVatSettingId, [
'vat_validation_status' => VatValidationStatus::VALIDATING->value,
'vat_validation_attempts' => $this->attempts(),
]);

$result = $viesService->validateVatNumber($this->vatNumber);

if ($result->valid) {
$logger->info('VAT validation successful', [
'account_vat_setting_id' => $this->accountVatSettingId,
'vat_number' => $this->maskVatNumber($this->vatNumber),
'business_name' => $result->businessName,
'attempt' => $this->attempts(),
]);

$repository->updateFromArray($this->accountVatSettingId, [
'vat_validated' => true,
'vat_validation_status' => VatValidationStatus::VALID->value,
'vat_validation_date' => now(),
'business_name' => $result->businessName,
'business_address' => $result->businessAddress,
'vat_country_code' => $result->countryCode,
'vat_validation_error' => null,
'vat_validation_attempts' => $this->attempts(),
]);

return;
}

if ($result->isTransientError) {
$logger->warning('VAT validation transient error - will retry', [
'account_vat_setting_id' => $this->accountVatSettingId,
'vat_number' => $this->maskVatNumber($this->vatNumber),
'error' => $result->errorMessage,
'attempt' => $this->attempts(),
]);

$repository->updateFromArray($this->accountVatSettingId, [
'vat_validation_status' => VatValidationStatus::PENDING->value,
'vat_validation_error' => $result->errorMessage,
'vat_validation_attempts' => $this->attempts(),
]);

$this->release($this->calculateBackoff());

return;
}

$logger->info('VAT validation failed - invalid VAT number', [
'account_vat_setting_id' => $this->accountVatSettingId,
'vat_number' => $this->maskVatNumber($this->vatNumber),
'error' => $result->errorMessage,
'attempt' => $this->attempts(),
]);

$repository->updateFromArray($this->accountVatSettingId, [
'vat_validated' => false,
'vat_validation_status' => VatValidationStatus::INVALID->value,
'vat_validation_error' => $result->errorMessage,
'vat_validation_attempts' => $this->attempts(),
]);
}

public function failed(Throwable $exception): void
{
$logger = app(LoggerInterface::class);
$repository = app(AccountVatSettingRepositoryInterface::class);

$logger->error('VAT validation job failed permanently', [
'account_vat_setting_id' => $this->accountVatSettingId,
'vat_number' => $this->maskVatNumber($this->vatNumber),
'error' => $exception->getMessage(),
'attempt' => $this->attempts(),
]);

try {
$repository->updateFromArray($this->accountVatSettingId, [
'vat_validated' => false,
'vat_validation_status' => VatValidationStatus::FAILED->value,
'vat_validation_error' => __('Validation failed after multiple attempts: :error', [
'error' => $exception->getMessage(),
]),
'vat_validation_attempts' => $this->attempts(),
]);
} catch (Throwable $e) {
$logger->error('Failed to update VAT setting after job failure', [
'account_vat_setting_id' => $this->accountVatSettingId,
'error' => $e->getMessage(),
]);
}
}

public function backoff(): array
{
return [
10, // 10s
10, // 10s
10, // 10s
10, // 10s
20, // 20s
30, // 30s
60, // 1m
120, // 2m
180, // 3m
300, // 5m
420, // 7m
600, // 10m
900, // 15m
1200, // 20m
1800, // 30m
];
}

public function retryUntil(): DateTimeInterface
{
return now()->addHours(4);
}

private function calculateBackoff(): int
{
$backoffs = $this->backoff();
$attempt = $this->attempts() - 1;

return $backoffs[$attempt] ?? end($backoffs);
}

private function maskVatNumber(string $vatNumber): string
{
$length = strlen($vatNumber);
if ($length <= 4) {
return $vatNumber;
}

return substr($vatNumber, 0, 2) . str_repeat('*', $length - 4) . substr($vatNumber, -2);
}
}
7 changes: 7 additions & 0 deletions backend/app/Models/AccountVatSetting.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
* @property bool $vat_registered
* @property string|null $vat_number
* @property bool $vat_validated
* @property string $vat_validation_status
* @property string|null $vat_validation_error
* @property int $vat_validation_attempts
* @property Carbon|null $vat_validation_date
* @property string|null $business_name
* @property string|null $business_address
Expand All @@ -31,6 +34,9 @@ protected function getFillableFields(): array
'vat_registered',
'vat_number',
'vat_validated',
'vat_validation_status',
'vat_validation_error',
'vat_validation_attempts',
'vat_validation_date',
'business_name',
'business_address',
Expand All @@ -43,6 +49,7 @@ protected function getCastMap(): array
return [
'vat_registered' => 'boolean',
'vat_validated' => 'boolean',
'vat_validation_attempts' => 'integer',
'vat_validation_date' => 'datetime',
];
}
Expand Down
3 changes: 3 additions & 0 deletions backend/app/Resources/Account/AccountVatSettingResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ public function toArray($request): array
'vat_registered' => $this->getVatRegistered(),
'vat_number' => $this->getVatNumber(),
'vat_validated' => $this->getVatValidated(),
'vat_validation_status' => $this->getVatValidationStatus(),
'vat_validation_error' => $this->getVatValidationError(),
'vat_validation_attempts' => $this->getVatValidationAttempts(),
'vat_validation_date' => $this->getVatValidationDate(),
'business_name' => $this->getBusinessName(),
'business_address' => $this->getBusinessAddress(),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<?php

declare(strict_types=1);

namespace HiEvents\Services\Application\Handlers\Account\Vat\DTO;

use HiEvents\DataTransferObjects\BaseDataObject;
Expand All @@ -12,6 +14,8 @@ public function __construct(
public readonly ?string $businessAddress = null,
public readonly string $countryCode = '',
public readonly string $vatNumber = '',
public readonly bool $isTransientError = false,
public readonly ?string $errorMessage = null,
) {
}
}
Loading
Loading