[#203] Réceptions — Parcours de pesée multi-étapes #3

Merged
tristan merged 10 commits from feat/203-reception-parcours-pesee-multi-etapas into develop 2026-01-14 07:17:34 +00:00
4 changed files with 241 additions and 24 deletions
Showing only changes of commit a8bbae8716 - Show all commits

57
.idea/workspace.xml generated
View File

@@ -6,8 +6,8 @@
<component name="ChangeListManager"> <component name="ChangeListManager">
<list default="true" id="7c107abe-5995-4428-8429-b146aaca8386" name="Changes" comment="feat : update du fichier README.md et CHANGELOG.md"> <list default="true" id="7c107abe-5995-4428-8429-b146aaca8386" name="Changes" comment="feat : update du fichier README.md et CHANGELOG.md">
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" /> <change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/CHANGELOG.md" beforeDir="false" afterPath="$PROJECT_DIR$/CHANGELOG.md" afterDir="false" /> <change beforePath="$PROJECT_DIR$/config/reference.php" beforeDir="false" afterPath="$PROJECT_DIR$/config/reference.php" afterDir="false" />
<change beforePath="$PROJECT_DIR$/README.md" beforeDir="false" afterPath="$PROJECT_DIR$/README.md" afterDir="false" /> <change beforePath="$PROJECT_DIR$/frontend/composables/useApi.ts" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/composables/useApi.ts" afterDir="false" />
</list> </list>
<option name="SHOW_DIALOG" value="false" /> <option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" /> <option name="HIGHLIGHT_CONFLICTS" value="true" />
@@ -198,28 +198,28 @@
<option name="hideEmptyMiddlePackages" value="true" /> <option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" /> <option name="showLibraryContents" value="true" />
</component> </component>
<component name="PropertiesComponent"><![CDATA[{ <component name="PropertiesComponent">{
"keyToString": { &quot;keyToString&quot;: {
"RunOnceActivity.MCP Project settings loaded": "true", &quot;RunOnceActivity.MCP Project settings loaded&quot;: &quot;true&quot;,
"RunOnceActivity.ShowReadmeOnStart": "true", &quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;,
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true", &quot;RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252&quot;: &quot;true&quot;,
"RunOnceActivity.git.unshallow": "true", &quot;RunOnceActivity.git.unshallow&quot;: &quot;true&quot;,
"RunOnceActivity.typescript.service.memoryLimit.init": "true", &quot;RunOnceActivity.typescript.service.memoryLimit.init&quot;: &quot;true&quot;,
"git-widget-placeholder": "feat/203-reception-parcours-pesee-multi-etapas", &quot;git-widget-placeholder&quot;: &quot;feat/203-reception-parcours-pesee-multi-etapas&quot;,
"node.js.detected.package.eslint": "true", &quot;node.js.detected.package.eslint&quot;: &quot;true&quot;,
"node.js.detected.package.tslint": "true", &quot;node.js.detected.package.tslint&quot;: &quot;true&quot;,
"node.js.selected.package.eslint": "(autodetect)", &quot;node.js.selected.package.eslint&quot;: &quot;(autodetect)&quot;,
"node.js.selected.package.tslint": "(autodetect)", &quot;node.js.selected.package.tslint&quot;: &quot;(autodetect)&quot;,
"nodejs_package_manager_path": "npm", &quot;nodejs_package_manager_path&quot;: &quot;npm&quot;,
"settings.editor.selected.configurable": "reference.webide.settings.project.settings.php.debug", &quot;settings.editor.selected.configurable&quot;: &quot;reference.webide.settings.project.settings.php.debug&quot;,
"vue.rearranger.settings.migration": "true" &quot;vue.rearranger.settings.migration&quot;: &quot;true&quot;
}, },
"keyToStringList": { &quot;keyToStringList&quot;: {
"vue.recent.templates": [ &quot;vue.recent.templates&quot;: [
"Vue Composition API Component" &quot;Vue Composition API Component&quot;
] ]
} }
}]]></component> }</component>
<component name="RecentsManager"> <component name="RecentsManager">
<key name="MoveFile.RECENT_KEYS"> <key name="MoveFile.RECENT_KEYS">
<recent name="\\wsl.localhost\Ubuntu-24.04\home\tristan\workspace\ferme\frontend\pages\reception" /> <recent name="\\wsl.localhost\Ubuntu-24.04\home\tristan\workspace\ferme\frontend\pages\reception" />
@@ -242,7 +242,7 @@
<updated>1767956826164</updated> <updated>1767956826164</updated>
<workItem from="1767956827666" duration="7866000" /> <workItem from="1767956827666" duration="7866000" />
<workItem from="1768201706520" duration="13383000" /> <workItem from="1768201706520" duration="13383000" />
<workItem from="1768287908317" duration="22144000" /> <workItem from="1768287908317" duration="23185000" />
</task> </task>
<task id="LOCAL-00001" summary="feat : Ajout de pinia, création de la table weight et reception mise en place du système de step pour les receptions (WIP)"> <task id="LOCAL-00001" summary="feat : Ajout de pinia, création de la table weight et reception mise en place du système de step pour les receptions (WIP)">
<option name="closed" value="true" /> <option name="closed" value="true" />
@@ -276,7 +276,15 @@
<option name="project" value="LOCAL" /> <option name="project" value="LOCAL" />
<updated>1768316965511</updated> <updated>1768316965511</updated>
</task> </task>
<option name="localTasksCounter" value="5" /> <task id="LOCAL-00005" summary="feat : update du fichier README.md et CHANGELOG.md">
<option name="closed" value="true" />
<created>1768317786187</created>
<option name="number" value="00005" />
<option name="presentableId" value="LOCAL-00005" />
<option name="project" value="LOCAL" />
<updated>1768317786187</updated>
</task>
<option name="localTasksCounter" value="6" />
<servers /> <servers />
</component> </component>
<component name="TypeScriptGeneratedFilesManager"> <component name="TypeScriptGeneratedFilesManager">
@@ -300,7 +308,8 @@
<MESSAGE value="feat : Ajout de zod, création d'un composant de chargement loading-dots.vue et finalisation du flow d'une reception" /> <MESSAGE value="feat : Ajout de zod, création d'un composant de chargement loading-dots.vue et finalisation du flow d'une reception" />
<MESSAGE value="feat : Ajout d'un composable pour la pesée qui sera réutilisable pour l'expédition, ajout de contrainte sur les entity de reception et weight pour plus de robustesse et correction de la class active des liens dans la nav" /> <MESSAGE value="feat : Ajout d'un composable pour la pesée qui sera réutilisable pour l'expédition, ajout de contrainte sur les entity de reception et weight pour plus de robustesse et correction de la class active des liens dans la nav" />
<MESSAGE value="feat : update du fichier AGENTS.md" /> <MESSAGE value="feat : update du fichier AGENTS.md" />
<option name="LAST_COMMIT_MESSAGE" value="feat : update du fichier AGENTS.md" /> <MESSAGE value="feat : update du fichier README.md et CHANGELOG.md" />
<option name="LAST_COMMIT_MESSAGE" value="feat : update du fichier README.md et CHANGELOG.md" />
</component> </component>
<component name="XDebuggerManager"> <component name="XDebuggerManager">
<breakpoint-manager> <breakpoint-manager>

View File

@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
namespace App\Tests\Service;
use App\Exception\PontBasculeException;
use App\Service\PontBasculePayloadDecoder;
use PHPUnit\Framework\TestCase;
/**
* @internal
*/
final class PontBasculePayloadDecoderTest extends TestCase
{
public function testDecodeValidPayload(): void
{
$decoder = new PontBasculePayloadDecoder();
$payload = json_encode([
'response_ascii' => "\u{0001}\u{0002}040200\u{0002}01001420.kg \u{0002}02000000.kg \u{0002}03001420.kg \u{0002}9900121",
], JSON_THROW_ON_ERROR);
$result = $decoder->decode($payload);
self::assertSame(121, $result->getDsd());
self::assertSame(1420.0, $result->getWeight());
}
public function testDecodeInvalidPayloadThrows(): void
{
$decoder = new PontBasculePayloadDecoder();
$this->expectException(PontBasculeException::class);
$this->expectExceptionMessage('Réponse invalide du pont bascule.');
$decoder->decode('not-json');
}
public function testDecodeMissingFieldThrows(): void
{
$decoder = new PontBasculePayloadDecoder();
$payload = json_encode(['ok' => true], JSON_THROW_ON_ERROR);
$this->expectException(PontBasculeException::class);
$this->expectExceptionMessage('Réponse incomplète du pont bascule: champ "response_ascii" manquant.');
$decoder->decode($payload);
}
public function testDecodeUnreadableValuesThrows(): void
{
$decoder = new PontBasculePayloadDecoder();
$payload = json_encode(['response_ascii' => 'no-data'], JSON_THROW_ON_ERROR);
$this->expectException(PontBasculeException::class);
$this->expectExceptionMessage('Impossible de lire les valeurs de pesée du pont bascule.');
$decoder->decode($payload);
}
}

View File

@@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
namespace App\Tests\Service;
use App\Exception\PontBasculeException;
use App\Service\PontBasculePayloadDecoder;
use App\Service\PontBasculeService;
use DateTimeImmutable;
use PHPUnit\Framework\TestCase;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
/**
* @internal
*/
final class PontBasculeServiceTest extends TestCase
{
public function testFetchBypassUsesDecoder(): void
{
$decoder = new PontBasculePayloadDecoder();
$httpClient = $this->createMock(HttpClientInterface::class);
$httpClient->expects(self::never())->method('request');
$service = new PontBasculeService($httpClient, $decoder, 'http://example.test', true);
$result = $service->fetch();
self::assertSame(121, $result->getDsd());
self::assertSame(1420.0, $result->getWeight());
self::assertInstanceOf(DateTimeImmutable::class, $result->getWeighedAt());
}
public function testFetchUsesHttpClientWhenNotBypass(): void
{
$payload = json_encode([
'response_ascii' => "\u{0001}\u{0002}040200\u{0002}03000123.kg \u{0002}9900042",
], JSON_THROW_ON_ERROR);
$response = $this->createMock(ResponseInterface::class);
$response->expects(self::once())->method('getContent')->with(false)->willReturn($payload);
$httpClient = $this->createMock(HttpClientInterface::class);
$httpClient
->expects(self::once())
->method('request')
->with('POST', 'http://example.test')
->willReturn($response)
;
$decoder = new PontBasculePayloadDecoder();
$service = new PontBasculeService($httpClient, $decoder, 'http://example.test', false);
$result = $service->fetch();
self::assertSame(42, $result->getDsd());
self::assertSame(123.0, $result->getWeight());
self::assertInstanceOf(DateTimeImmutable::class, $result->getWeighedAt());
}
public function testFetchThrowsOnTransportFailure(): void
{
$exception = $this->createStub(TransportExceptionInterface::class);
$httpClient = $this->createMock(HttpClientInterface::class);
$httpClient
->expects(self::once())
->method('request')
->willThrowException($exception)
;
$decoder = new PontBasculePayloadDecoder();
$service = new PontBasculeService($httpClient, $decoder, 'http://example.test', false);
$this->expectException(PontBasculeException::class);
$this->expectExceptionMessage('Erreur lors de la communication avec le pont bascule:');
$service->fetch();
}
}

View File

@@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
namespace App\Tests\State;
use ApiPlatform\Metadata\Get;
use App\Dto\PontBasculeReading;
use App\Service\PontBasculePayloadDecoder;
use App\Service\PontBasculeService;
use App\State\ReceptionWeighingProvider;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
/**
* @internal
*/
final class ReceptionWeighingProviderTest extends TestCase
{
public function testProvideReturnsReading(): void
{
$decoder = new PontBasculePayloadDecoder();
$httpClient = $this->createMock(HttpClientInterface::class);
$httpClient->expects(self::never())->method('request');
$service = new PontBasculeService($httpClient, $decoder, 'http://example.test', true);
$provider = new ReceptionWeighingProvider($service);
$result = $provider->provide(new Get());
self::assertInstanceOf(PontBasculeReading::class, $result);
self::assertSame(121, $result->getDsd());
self::assertSame(1420.0, $result->getWeight());
}
public function testProvideThrowsHttpException(): void
{
$exception = $this->createStub(TransportExceptionInterface::class);
$httpClient = $this->createMock(HttpClientInterface::class);
$httpClient
->expects(self::once())
->method('request')
->willThrowException($exception)
;
$decoder = new PontBasculePayloadDecoder();
$service = new PontBasculeService($httpClient, $decoder, 'http://example.test', false);
$provider = new ReceptionWeighingProvider($service);
$this->expectException(HttpException::class);
$this->expectExceptionMessage('Erreur lors de la communication avec le pont bascule:');
$provider->provide(new Get());
}
}