feat(api): endpoint public /api/allergens + decision prix (P4 optionnel)
All checks were successful
CI / secret-scan (pull_request) Successful in 10s
CI / static-tests (pull_request) Successful in 46s
CI / js-tests (pull_request) Successful in 30s
CI / php-lint (push) Successful in 25s
CI / php-lint (pull_request) Successful in 21s
CI / secret-scan (push) Successful in 12s
CI / static-tests (push) Successful in 47s
CI / js-tests (push) Successful in 27s
All checks were successful
CI / secret-scan (pull_request) Successful in 10s
CI / static-tests (pull_request) Successful in 46s
CI / js-tests (pull_request) Successful in 30s
CI / php-lint (push) Successful in 25s
CI / php-lint (pull_request) Successful in 21s
CI / secret-scan (push) Successful in 12s
CI / static-tests (push) Successful in 47s
CI / js-tests (push) Successful in 27s
- GET /api/allergens (public anonyme, lecture seule) : les 14 allergenes INCO
(id/code/name) via App\Catalogue\AllergenRepository. CatalogueController::allergens
(meme enveloppe {data,total} + present que les autres lectures catalogue).
- La borne CONSERVE son JSON statique (data/allergens.json) : il porte les
descriptions riches, absentes du schema allergen (code+name seulement). Le swap
reste possible si les descriptions sont ajoutees cote API ; commentaire data.js maj.
- Prix : decision confirmee -- centimes en stockage/API (price_cents), euros a
l'affichage (state.formatPrice). Aucun changement de convention requis.
- Test : AllergenReadDbTest (integration, vraie DB : les 14 INCO). 351 PHP verts,
PHPStan L6 propre, /api/allergens verifie live (200, total 14, same-origin).
This commit is contained in:
parent
1d56d5b574
commit
77bf973860
5 changed files with 121 additions and 2 deletions
32
src/app/Catalogue/AllergenRepository.php
Normal file
32
src/app/Catalogue/AllergenRepository.php
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Catalogue;
|
||||||
|
|
||||||
|
use App\Core\DatabaseInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lecture des allergenes a declaration obligatoire (INCO) : info GENERALE (les 14
|
||||||
|
* categories), pas un calcul par produit (le mapping ingredient_allergen reste
|
||||||
|
* differe). Sert l'endpoint public anonyme /api/allergens. Le schema ne porte que
|
||||||
|
* code + name ; les descriptions riches restent cote borne (data/allergens.json).
|
||||||
|
*
|
||||||
|
* Non `final` : seam de test (sous-classe -> double sans base).
|
||||||
|
*/
|
||||||
|
class AllergenRepository
|
||||||
|
{
|
||||||
|
public function __construct(private readonly DatabaseInterface $db)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Les allergenes references, tries par id (ordre INCO du seed).
|
||||||
|
*
|
||||||
|
* @return list<array<string, mixed>>
|
||||||
|
*/
|
||||||
|
public function all(): array
|
||||||
|
{
|
||||||
|
return $this->db->fetchAll('SELECT id, code, name FROM allergen ORDER BY id');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Controllers;
|
namespace App\Controllers;
|
||||||
|
|
||||||
|
use App\Catalogue\AllergenRepository;
|
||||||
use App\Catalogue\CategoryRepository;
|
use App\Catalogue\CategoryRepository;
|
||||||
use App\Catalogue\MenuRepository;
|
use App\Catalogue\MenuRepository;
|
||||||
use App\Catalogue\ProductRepository;
|
use App\Catalogue\ProductRepository;
|
||||||
|
|
@ -111,6 +112,21 @@ class CatalogueController extends Controller
|
||||||
return $this->json(['data' => $menu]);
|
return $this->json(['data' => $menu]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allergenes INCO (info generale, 14 categories). Public anonyme, lecture seule.
|
||||||
|
*
|
||||||
|
* @param array<string, string> $params
|
||||||
|
*/
|
||||||
|
public function allergens(array $params = []): Response
|
||||||
|
{
|
||||||
|
$rows = array_map(
|
||||||
|
fn (array $row): array => $this->presentAllergen($row),
|
||||||
|
$this->allergensRepo()->all(),
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->json(['data' => $rows, 'total' => count($rows)]);
|
||||||
|
}
|
||||||
|
|
||||||
protected function categoriesRepo(): CategoryRepository
|
protected function categoriesRepo(): CategoryRepository
|
||||||
{
|
{
|
||||||
return new CategoryRepository($this->db());
|
return new CategoryRepository($this->db());
|
||||||
|
|
@ -126,6 +142,11 @@ class CatalogueController extends Controller
|
||||||
return new MenuRepository($this->db());
|
return new MenuRepository($this->db());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function allergensRepo(): AllergenRepository
|
||||||
|
{
|
||||||
|
return new AllergenRepository($this->db());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Acces BDD comme DatabaseInterface (seam de test). Database l'implemente.
|
* Acces BDD comme DatabaseInterface (seam de test). Database l'implemente.
|
||||||
*/
|
*/
|
||||||
|
|
@ -134,6 +155,19 @@ class CatalogueController extends Controller
|
||||||
return $this->database;
|
return $this->database;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $row
|
||||||
|
* @return array{id: int, code: string, name: string}
|
||||||
|
*/
|
||||||
|
private function presentAllergen(array $row): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => (int) ($row['id'] ?? 0),
|
||||||
|
'code' => (string) ($row['code'] ?? ''),
|
||||||
|
'name' => (string) ($row['name'] ?? ''),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<string, mixed> $row
|
* @param array<string, mixed> $row
|
||||||
* @return array{id: int, name: string, slug: string, image_path: ?string, display_order: int}
|
* @return array{id: int, name: string, slug: string, image_path: ?string, display_order: int}
|
||||||
|
|
|
||||||
|
|
@ -99,6 +99,9 @@ try {
|
||||||
// Menus composes : liste legere + detail avec slots (B1 burger impose, B2 Normal/Maxi).
|
// Menus composes : liste legere + detail avec slots (B1 burger impose, B2 Normal/Maxi).
|
||||||
$router->add('GET', '/api/menus', [CatalogueController::class, 'menus']);
|
$router->add('GET', '/api/menus', [CatalogueController::class, 'menus']);
|
||||||
$router->add('GET', '/api/menus/{id}', [CatalogueController::class, 'menu']);
|
$router->add('GET', '/api/menus/{id}', [CatalogueController::class, 'menu']);
|
||||||
|
// Allergenes INCO (info generale, 14 categories). La borne garde son JSON statique
|
||||||
|
// (descriptions riches) ; l'endpoint sert d'autres consommateurs eventuels.
|
||||||
|
$router->add('GET', '/api/allergens', [CatalogueController::class, 'allergens']);
|
||||||
|
|
||||||
// Back-office (P3) : pages rendues serveur sous /admin, gardees par SessionGuard.
|
// Back-office (P3) : pages rendues serveur sous /admin, gardees par SessionGuard.
|
||||||
$router->add('GET', '/admin/dashboard', [DashboardController::class, 'index']);
|
$router->add('GET', '/admin/dashboard', [DashboardController::class, 'index']);
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,10 @@
|
||||||
const CATEGORIES_URL = '/api/categories';
|
const CATEGORIES_URL = '/api/categories';
|
||||||
const PRODUCTS_URL = '/api/products';
|
const PRODUCTS_URL = '/api/products';
|
||||||
const MENUS_URL = '/api/menus';
|
const MENUS_URL = '/api/menus';
|
||||||
/* Liste fixe des 14 allergenes INCO (info generale, modale borne). Repli statique
|
/* Liste fixe des 14 allergenes INCO (info generale, modale borne). L'endpoint
|
||||||
* encore en place : bascule sur '/api/allergens' differee. */
|
* /api/allergens existe desormais (id/code/name), mais la borne garde ce JSON
|
||||||
|
* statique : il porte les DESCRIPTIONS riches, absentes du schema allergen. Bascule
|
||||||
|
* possible si les descriptions sont ajoutees cote API. */
|
||||||
const ALLERGENS_URL = 'data/allergens.json';
|
const ALLERGENS_URL = 'data/allergens.json';
|
||||||
|
|
||||||
/* Memoisation par PROMESSE (pas par resultat) : N appelants concurrents au meme
|
/* Memoisation par PROMESSE (pas par resultat) : N appelants concurrents au meme
|
||||||
|
|
|
||||||
48
tests/Integration/AllergenReadDbTest.php
Normal file
48
tests/Integration/AllergenReadDbTest.php
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Tests\Integration;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Throwable;
|
||||||
|
use App\Catalogue\AllergenRepository;
|
||||||
|
use App\Core\Config;
|
||||||
|
use App\Core\Database;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AllergenRepository contre une vraie MariaDB (schema migre + seed reference).
|
||||||
|
* Auto-skip si WAKDO_DB_TESTS != 1. Lecture seule (donnees de reference) : aucun
|
||||||
|
* fixture/teardown. Verifie que les 14 allergenes INCO sont references avec code+name.
|
||||||
|
*/
|
||||||
|
final class AllergenReadDbTest extends TestCase
|
||||||
|
{
|
||||||
|
private Database $db;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
if (getenv('WAKDO_DB_TESTS') !== '1') {
|
||||||
|
self::markTestSkipped('Tests DB desactives (definir WAKDO_DB_TESTS=1 + DB_*).');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->db = new Database(new Config());
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->db->fetch('SELECT 1');
|
||||||
|
} catch (Throwable $exception) {
|
||||||
|
self::markTestSkipped('Base injoignable: ' . $exception->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testListsIncoReferenceWithCodeAndName(): void
|
||||||
|
{
|
||||||
|
$rows = (new AllergenRepository($this->db))->all();
|
||||||
|
|
||||||
|
self::assertGreaterThanOrEqual(14, count($rows), 'les 14 allergenes INCO doivent etre references');
|
||||||
|
foreach ($rows as $a) {
|
||||||
|
self::assertArrayHasKey('code', $a);
|
||||||
|
self::assertArrayHasKey('name', $a);
|
||||||
|
self::assertNotSame('', (string) ($a['name'] ?? ''));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue