feat(23-shipment-presets-backend): tabela DB, repository CRUD i JSON API dla presetów przesyłek

Phase 23 complete:
- Migracja shipment_presets (16 kolumn: name, color, carrier, wymiary, waga, itp.)
- ShipmentPresetRepository z findAll/findById/create/update/delete
- ShipmentPresetController z 4 endpointami JSON API
- Routing w routes/web.php z auth middleware

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-22 23:27:14 +01:00
parent d6375cc61d
commit 03a237e7d2
8 changed files with 635 additions and 13 deletions

View File

@@ -0,0 +1,100 @@
<?php
declare(strict_types=1);
namespace App\Modules\Shipments;
use App\Core\Http\Request;
use App\Core\Http\Response;
final class ShipmentPresetController
{
public function __construct(
private readonly ShipmentPresetRepository $repository
) {
}
public function list(Request $request): Response
{
return Response::json(['presets' => $this->repository->findAll()]);
}
public function store(Request $request): Response
{
$name = trim((string) $request->input('name', ''));
if ($name === '') {
return Response::json(['error' => 'Name is required'], 422);
}
$data = $this->extractData($request);
$id = $this->repository->create($data);
$preset = $this->repository->findById($id);
return Response::json(['preset' => $preset], 201);
}
public function update(Request $request): Response
{
$id = (int) $request->input('id', '0');
if ($id <= 0) {
return Response::json(['error' => 'Invalid preset ID'], 400);
}
$existing = $this->repository->findById($id);
if ($existing === null) {
return Response::json(['error' => 'Preset not found'], 404);
}
$data = $this->extractData($request);
if (trim($data['name']) === '') {
return Response::json(['error' => 'Name is required'], 422);
}
$this->repository->update($id, $data);
$preset = $this->repository->findById($id);
return Response::json(['preset' => $preset]);
}
public function destroy(Request $request): Response
{
$id = (int) $request->input('id', '0');
if ($id <= 0) {
return Response::json(['error' => 'Invalid preset ID'], 400);
}
$existing = $this->repository->findById($id);
if ($existing === null) {
return Response::json(['error' => 'Preset not found'], 404);
}
$this->repository->delete($id);
return Response::json(['deleted' => true]);
}
/**
* @return array<string, mixed>
*/
private function extractData(Request $request): array
{
return [
'name' => (string) $request->input('name', ''),
'color' => (string) $request->input('color', '#3b82f6'),
'carrier' => (string) $request->input('carrier', ''),
'provider_code' => (string) $request->input('provider_code', ''),
'delivery_method_id' => (string) $request->input('delivery_method_id', ''),
'credentials_id' => (string) $request->input('credentials_id', ''),
'carrier_id' => (string) $request->input('carrier_id', ''),
'package_type' => (string) $request->input('package_type', 'PACKAGE'),
'length_cm' => (string) $request->input('length_cm', '25'),
'width_cm' => (string) $request->input('width_cm', '20'),
'height_cm' => (string) $request->input('height_cm', '8'),
'weight_kg' => (string) $request->input('weight_kg', '1'),
'sender_point_id' => (string) $request->input('sender_point_id', ''),
'label_format' => (string) $request->input('label_format', 'PDF'),
'sort_order' => (string) $request->input('sort_order', '0'),
];
}
}

View File

@@ -0,0 +1,117 @@
<?php
declare(strict_types=1);
namespace App\Modules\Shipments;
use PDO;
final class ShipmentPresetRepository
{
public function __construct(
private readonly PDO $pdo
) {
}
/**
* @return array<int, array<string, mixed>>
*/
public function findAll(): array
{
$statement = $this->pdo->prepare(
'SELECT * FROM shipment_presets ORDER BY sort_order ASC, id ASC'
);
$statement->execute();
return $statement->fetchAll(PDO::FETCH_ASSOC) ?: [];
}
/**
* @return array<string, mixed>|null
*/
public function findById(int $id): ?array
{
$statement = $this->pdo->prepare('SELECT * FROM shipment_presets WHERE id = :id LIMIT 1');
$statement->execute(['id' => $id]);
$row = $statement->fetch(PDO::FETCH_ASSOC);
return is_array($row) ? $row : null;
}
/**
* @param array<string, mixed> $data
*/
public function create(array $data): int
{
$statement = $this->pdo->prepare(
'INSERT INTO shipment_presets (
name, color, carrier, provider_code, delivery_method_id,
credentials_id, carrier_id, package_type,
length_cm, width_cm, height_cm, weight_kg,
sender_point_id, label_format, sort_order
) VALUES (
:name, :color, :carrier, :provider_code, :delivery_method_id,
:credentials_id, :carrier_id, :package_type,
:length_cm, :width_cm, :height_cm, :weight_kg,
:sender_point_id, :label_format, :sort_order
)'
);
$statement->execute($this->mapParams($data));
return (int) $this->pdo->lastInsertId();
}
/**
* @param array<string, mixed> $data
*/
public function update(int $id, array $data): void
{
$statement = $this->pdo->prepare(
'UPDATE shipment_presets SET
name = :name, color = :color, carrier = :carrier,
provider_code = :provider_code, delivery_method_id = :delivery_method_id,
credentials_id = :credentials_id, carrier_id = :carrier_id,
package_type = :package_type,
length_cm = :length_cm, width_cm = :width_cm,
height_cm = :height_cm, weight_kg = :weight_kg,
sender_point_id = :sender_point_id, label_format = :label_format,
sort_order = :sort_order
WHERE id = :id'
);
$params = $this->mapParams($data);
$params['id'] = $id;
$statement->execute($params);
}
public function delete(int $id): void
{
$statement = $this->pdo->prepare('DELETE FROM shipment_presets WHERE id = :id');
$statement->execute(['id' => $id]);
}
/**
* @param array<string, mixed> $data
* @return array<string, mixed>
*/
private function mapParams(array $data): array
{
return [
'name' => trim((string) ($data['name'] ?? '')),
'color' => trim((string) ($data['color'] ?? '#3b82f6')),
'carrier' => trim((string) ($data['carrier'] ?? '')),
'provider_code' => trim((string) ($data['provider_code'] ?? '')),
'delivery_method_id' => trim((string) ($data['delivery_method_id'] ?? '')),
'credentials_id' => trim((string) ($data['credentials_id'] ?? '')),
'carrier_id' => trim((string) ($data['carrier_id'] ?? '')),
'package_type' => trim((string) ($data['package_type'] ?? 'PACKAGE')),
'length_cm' => max(0.1, (float) ($data['length_cm'] ?? 25.0)),
'width_cm' => max(0.1, (float) ($data['width_cm'] ?? 20.0)),
'height_cm' => max(0.1, (float) ($data['height_cm'] ?? 8.0)),
'weight_kg' => max(0.001, (float) ($data['weight_kg'] ?? 1.0)),
'sender_point_id' => trim((string) ($data['sender_point_id'] ?? '')),
'label_format' => in_array(strtoupper(trim((string) ($data['label_format'] ?? ''))), ['PDF', 'ZPL'], true)
? strtoupper(trim((string) $data['label_format']))
: 'PDF',
'sort_order' => max(0, (int) ($data['sort_order'] ?? 0)),
];
}
}