ldc-boutique-cnc-sable/app/Services/EmployeeSyncService.php
Jérémy Munsch e0c4b34ed8
feat(employees): implement deterministic 6-character code generation using SerialCaster
- Add kwaadpepper/serial-int-caster and SerialCasterServiceProvider
- Update CodeGenerationService to encode Matricule deterministically with length 6
- Update Code value object to support lengths between 6 and 10 with 53 non-ambiguous characters
- Add database migration to sanitize legacy ambiguous codes
- Recreate and update unit and integration tests for CodeGenerationService and Code value object
2026-10-27 09:58:46 +01:00

141 lines
4.4 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services;
use App\Contracts\QueryInputEmpty;
use App\Queries\Settings\GetSettingsQuery;
use Carbon\CarbonImmutable;
use Carbon\CarbonInterface;
use Domain\Models\ValueObjects\EmployeeUpsertData;
use Domain\Models\ValueObjects\Matricule;
use Domain\Ports\Services\EmployeeImportStorageService;
use Illuminate\Contracts\Cache\Repository as CacheRepository;
use Illuminate\Support\Carbon;
class EmployeeSyncService
{
private const SYNC_TIMESTAMP_CACHE = 'EMPLOYEE_LAST_SYNC';
public function __construct(
private readonly CacheRepository $cacheRepository,
private readonly ArcadiaEmployeeService $arcadiaEmployeeService,
private readonly CodeGenerationService $codeService,
private readonly EmployeeImportStorageService $employeeImportStorageService,
private readonly GetSettingsQuery $getSettingsQuery
) {
}
/**
* @param array<int, \Domain\Models\ValueObjects\ShopperSiteCode>|null $sites
*/
public function synchronize(
?\DateTimeInterface $since = null,
?array $sites = null
): void {
$sitesToSync = $sites ?? ($this->getSettingsQuery)(new QueryInputEmpty())->getShopperSiteCodes();
$sinceDate = $since ?? Carbon::createFromTimestamp(0);
foreach ($sitesToSync as $siteCode) {
$employees = $this->arcadiaEmployeeService->getEmployeesOfSiteSince(
$siteCode,
$sinceDate
);
if ($employees === false) {
continue;
}
$this->upsertEmployees($employees);
}
$this->markLastSync();
}
public function getLastSync(): ?CarbonInterface
{
$timestamp = $this->cacheRepository->get(self::SYNC_TIMESTAMP_CACHE);
if (! is_string($timestamp)) {
return null;
}
return Carbon::parse($timestamp);
}
private function markLastSync(): void
{
$this->cacheRepository->remember(
self::SYNC_TIMESTAMP_CACHE,
3600 * 36, // Cache for 36 hours
fn () => Carbon::now()->format(Carbon::RFC3339)
);
}
/** @param \App\Services\ArcadiaEmployee[] $employeeList */
private function upsertEmployees(array $employeeList): EmployeeSyncServiceResult
{
$matriculesToInsert = [];
$errors = [];
$upsertData = [];
$importStartTime = CarbonImmutable::now()->subSecond();
foreach ($employeeList as $employeeImport) {
$matricule = $employeeImport->matricule->value;
if (in_array($matricule, $matriculesToInsert, true)) {
$errors[] = "Duplicate detected in the file: The matricule {$matricule} appears multiple times.";
continue;
}
$matriculesToInsert[] = $matricule;
// NOTE: We generate a new code for each row, but only insert it for new records.
$code = $this->codeService->generateFrom(new Matricule($matricule));
$upsertData[] = new EmployeeUpsertData(
matricule: $employeeImport->matricule,
firstname: $employeeImport->firstname,
lastname: $employeeImport->lastname,
etablissement: $employeeImport->etablissement,
service: $employeeImport->service,
site: $employeeImport->site,
startDate: $employeeImport->startDate,
endDate: $employeeImport->endDate,
code: $code
);
}
if (empty($upsertData) && empty($errors)) {
$errors[] = 'The file contains no valid data.';
}
if (! empty($upsertData)) {
$this->employeeImportStorageService->upsertEmployees($upsertData);
$result = $this->employeeImportStorageService
->getImportedAfter(
array_map(
fn ($m) => new Matricule($m),
$matriculesToInsert
),
$importStartTime
);
$insertedCount = $result->insertedCount;
$updatedCount = $result->updatedCount;
} else {
$insertedCount = 0;
$updatedCount = 0;
}
return new EmployeeSyncServiceResult(
insertedCount: $insertedCount,
updatedCount: $updatedCount,
errors: $errors,
);
}
}