e88bb059e6
- WeighbridgeReaderInterface (contrat) + RandomWeighbridgeReader (stub,
poids aléatoire ∈ [10000,50000] kg, RG-5.06) + WeighbridgeUnavailableException
- DsdAllocator : compteur DSD par site (weighbridge_dsd_counter) incrémenté
sous verrou ligne SELECT ... FOR UPDATE (RG-5.04, § 2.7)
- endpoint POST /api/weighbridge_readings : ressource virtuelle
WeighbridgeReadingResource + WeighbridgeReadingProcessor (pas de controller)
- AUTO -> {weight, dsd, mode} ; MANUAL -> {weight, dsd, manualNumber, mode}
- WeighbridgeUnavailableException -> HTTP 503 explicite (RG-5.06)
- site courant via CurrentSiteProviderInterface (contrat Sites)
- is_granted('logistique.weighing_tickets.manage')
- dsd renvoyé prévisionnel : attribution autoritaire refaite à la création
du ticket (ERP-185)
- tests : WeighbridgeReaderStubTest, DsdAllocatorTest, processor (503/400),
WeighbridgeReadingApiTest (RBAC + AUTO/MANUAL + 422)
66 lines
2.6 KiB
PHP
66 lines
2.6 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Module\Logistique\Infrastructure\Service;
|
|
|
|
use App\Module\Logistique\Application\Service\DsdAllocatorInterface;
|
|
use App\Shared\Domain\Contract\SiteInterface;
|
|
use Doctrine\DBAL\Connection;
|
|
use LogicException;
|
|
|
|
/**
|
|
* Implementation DBAL de l'allocateur DSD (RG-5.04, § 2.7).
|
|
*
|
|
* Le compteur vit dans la table `weighbridge_dsd_counter (site_id PK,
|
|
* last_value)` — jamais mappee en ORM (DBAL brut, exclue du schema_filter).
|
|
* L'increment est realise dans une transaction avec verrou ligne
|
|
* `SELECT ... FOR UPDATE` : deux postes pesant en parallele sur le meme site
|
|
* sont serialises, ce qui garantit des DSD distincts (pas de collision).
|
|
*
|
|
* AUTO comme MANUAL passent par le meme increment (« dernier DSD du site + 1 ») :
|
|
* la seule difference fonctionnelle est l'origine du poids (lu par le pont en
|
|
* AUTO, saisi en MANUAL), pas la sequence DSD.
|
|
*
|
|
* La ligne compteur n'est pas seedee a la creation du site : on la cree a la
|
|
* volee (INSERT ... ON CONFLICT DO NOTHING) avant de prendre le verrou.
|
|
*/
|
|
final class DsdAllocator implements DsdAllocatorInterface
|
|
{
|
|
public function __construct(private readonly Connection $connection) {}
|
|
|
|
public function next(SiteInterface $site): int
|
|
{
|
|
$siteId = $site->getId();
|
|
if (null === $siteId) {
|
|
// Garde defensive : un site non persiste n'a pas de compteur (et la FK
|
|
// weighbridge_dsd_counter.site_id -> site(id) rejetterait l'INSERT).
|
|
throw new LogicException('Impossible d\'allouer un DSD pour un site non persiste (id null).');
|
|
}
|
|
|
|
return $this->connection->transactional(function (Connection $conn) use ($siteId): int {
|
|
// Garantit l'existence de la ligne compteur du site sans ecraser une
|
|
// valeur deja presente (idempotent, concurrence-safe).
|
|
$conn->executeStatement(
|
|
'INSERT INTO weighbridge_dsd_counter (site_id, last_value) VALUES (:site, 0) ON CONFLICT (site_id) DO NOTHING',
|
|
['site' => $siteId],
|
|
);
|
|
|
|
// Verrou ligne : serialise les pesees concurrentes du meme site.
|
|
$current = (int) $conn->fetchOne(
|
|
'SELECT last_value FROM weighbridge_dsd_counter WHERE site_id = :site FOR UPDATE',
|
|
['site' => $siteId],
|
|
);
|
|
|
|
$next = $current + 1;
|
|
|
|
$conn->executeStatement(
|
|
'UPDATE weighbridge_dsd_counter SET last_value = :value WHERE site_id = :site',
|
|
['value' => $next, 'site' => $siteId],
|
|
);
|
|
|
|
return $next;
|
|
});
|
|
}
|
|
}
|