Intégration reservations et mock lavage

This commit is contained in:
bastien
2026-07-03 19:01:49 +02:00
parent b2443b654c
commit 55a558b536
16 changed files with 346 additions and 5 deletions
@@ -12,9 +12,9 @@ use App\Http\Resources\BookingResource;
use App\Models\Booking;
use App\Models\Machine;
use App\Services\BookingService;
use Carbon\Carbon;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
class BookingController extends Controller
{
@@ -0,0 +1,40 @@
<?php
namespace App\Http\Controllers\Api\V1;
use App\Http\Controllers\Concerns\RespondsWithJson;
use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\DB;
use Throwable;
class HealthController extends Controller
{
use RespondsWithJson;
public function index(): JsonResponse
{
return $this->success([
'status' => 'ok',
'service' => 'laverie-api',
'timestamp' => now()->toIso8601String(),
]);
}
public function database(): JsonResponse
{
try {
DB::select('SELECT 1');
return $this->success([
'status' => 'ok',
'database' => config('database.default'),
'timestamp' => now()->toIso8601String(),
]);
} catch (Throwable $e) {
return $this->error('Database connection failed', 503, [
'database' => ['Connection unavailable'],
]);
}
}
}
@@ -8,6 +8,7 @@ use App\Http\Resources\MachineResource;
use App\Models\Booking;
use App\Models\Machine;
use App\Services\PricingService;
use App\Support\WashCycleProgress;
use Carbon\Carbon;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
@@ -29,6 +30,34 @@ class MachineController extends Controller
return $this->success([
'machine' => new MachineResource($machine),
'pricing' => [
'price' => $this->pricingService->getActivePrice($machine),
'currency' => 'EUR',
],
'estimated_duration_minutes' => WashCycleProgress::estimateDurationMinutes($machine->type),
]);
}
public function lookup(Request $request): JsonResponse
{
$request->validate([
'qr_code' => ['required_without:machine_uuid', 'nullable', 'string', 'max:255'],
'machine_uuid' => ['required_without:qr_code', 'nullable', 'uuid'],
]);
$machine = $request->filled('machine_uuid')
? Machine::query()->where('uuid', $request->string('machine_uuid')->toString())->firstOrFail()
: Machine::query()->where('qr_code', $request->string('qr_code')->toString())->firstOrFail();
$machine->load('establishment');
return $this->success([
'machine' => new MachineResource($machine),
'pricing' => [
'price' => $this->pricingService->getActivePrice($machine),
'currency' => 'EUR',
],
'estimated_duration_minutes' => WashCycleProgress::estimateDurationMinutes($machine->type),
]);
}
@@ -25,9 +25,9 @@ class WashController extends Controller
public function start(StartWashRequest $request): JsonResponse
{
$machine = Machine::query()
->where('uuid', $request->string('machine_uuid')->toString())
->firstOrFail();
$machine = $request->filled('machine_uuid')
? Machine::query()->where('uuid', $request->string('machine_uuid')->toString())->firstOrFail()
: Machine::query()->where('qr_code', $request->string('qr_code')->toString())->firstOrFail();
$bookingId = null;
if ($request->filled('booking_uuid')) {
@@ -15,7 +15,8 @@ class StartWashRequest extends FormRequest
public function rules(): array
{
return [
'machine_uuid' => ['required', 'uuid', 'exists:machines,uuid'],
'machine_uuid' => ['required_without:qr_code', 'nullable', 'uuid', 'exists:machines,uuid'],
'qr_code' => ['required_without:machine_uuid', 'nullable', 'string', 'max:255', 'exists:machines,qr_code'],
'booking_uuid' => ['nullable', 'uuid', 'exists:bookings,uuid'],
'trigger_method' => ['required', Rule::in(['qr_code', 'booking', 'supervisor', 'system'])],
'program' => ['nullable', 'string', 'max:100'],
+2
View File
@@ -2,6 +2,7 @@
namespace App\Http\Resources;
use App\Support\WashCycleProgress;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
@@ -22,6 +23,7 @@ class WashResource extends JsonResource
'machine' => new MachineResource($this->whenLoaded('machine')),
'booking' => new BookingResource($this->whenLoaded('booking')),
'user' => new UserResource($this->whenLoaded('user')),
'progress' => WashCycleProgress::forWash($this->resource),
];
}
}
+2
View File
@@ -11,6 +11,8 @@ class MachineStatusHistory extends Model
{
public const UPDATED_AT = null;
protected $table = 'machine_status_history';
protected function casts(): array
{
return [
+2
View File
@@ -7,6 +7,7 @@ use App\Models\Booking;
use App\Models\Machine;
use App\Models\User;
use App\Models\Wash;
use App\Support\WashCycleProgress;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
@@ -71,6 +72,7 @@ class WashService
'status' => 'pending_start',
'program' => $program,
'cost' => $cost,
'duration_minutes' => WashCycleProgress::estimateDurationMinutes($machine->type),
]);
$result = $this->machineIntegrationService->startCycle($machine, [
+108
View File
@@ -0,0 +1,108 @@
<?php
namespace App\Support;
use App\Models\Wash;
class WashCycleProgress
{
/**
* @return array<string, mixed>|null
*/
public static function forWash(Wash $wash): ?array
{
if (! in_array($wash->status, ['pending_start', 'running', 'active'], true)) {
return null;
}
$wash->loadMissing('machine');
$machine = $wash->machine;
$startedAt = $wash->started_at ?? $wash->created_at;
$endsAt = $machine?->cycle_ends_at;
if ($startedAt === null) {
return [
'percent' => 0,
'phase' => 'pending',
'phase_label' => 'En attente de démarrage',
'estimated_end_at' => $endsAt?->toIso8601String(),
'remaining_seconds' => null,
];
}
$totalSeconds = $endsAt !== null
? max(1, (int) $startedAt->diffInSeconds($endsAt))
: max(1, ($wash->duration_minutes ?? 45) * 60);
$elapsed = max(0, (int) $startedAt->diffInSeconds(now()));
$percent = min(99, (int) round(($elapsed / $totalSeconds) * 100));
[$phase, $phaseLabel] = self::phaseForPercent($percent, $machine?->type);
$remainingSeconds = $endsAt !== null
? max(0, (int) now()->diffInSeconds($endsAt, false))
: max(0, $totalSeconds - $elapsed);
return [
'percent' => $percent,
'phase' => $phase,
'phase_label' => $phaseLabel,
'estimated_end_at' => $endsAt?->toIso8601String(),
'remaining_seconds' => $remainingSeconds,
];
}
/**
* @return array{0: string, 1: string}
*/
private static function phaseForPercent(int $percent, ?string $type): array
{
$isDryer = str_starts_with($type ?? '', 'dryer');
if ($percent < 5) {
return ['lock', 'Verrouillage'];
}
if ($isDryer) {
if ($percent < 15) {
return ['heat', 'Préchauffage'];
}
if ($percent < 85) {
return ['dry', 'Séchage'];
}
return ['finish', 'Refroidissement'];
}
if ($percent < 15) {
return ['fill', 'Remplissage'];
}
if ($percent < 55) {
return ['wash', 'Lavage'];
}
if ($percent < 75) {
return ['rinse', 'Rinçage'];
}
if ($percent < 90) {
return ['spin', 'Essorage'];
}
return ['finish', 'Finition'];
}
public static function estimateDurationMinutes(?string $machineType): int
{
$simSeconds = (int) config('laverie.simulation.cycle_duration_seconds', 30);
if ($simSeconds >= 120) {
return max(1, (int) ceil($simSeconds / 60));
}
return match (true) {
str_starts_with($machineType ?? '', 'dryer_large') => 55,
str_starts_with($machineType ?? '', 'dryer') => 45,
str_starts_with($machineType ?? '', 'washer_large') => 50,
default => 40,
};
}
}