addSeconds($durationSeconds); $command = MachineCommand::query()->create([ 'uuid' => (string) Str::uuid(), 'machine_id' => $machine->id, 'provider' => $provider, 'command_type' => 'start_cycle', 'payload' => $payload, 'status' => 'acknowledged', 'external_reference' => $externalReference, 'correlation_id' => $correlationId, 'requested_by_user_id' => $payload['requested_by_user_id'] ?? null, 'sent_at' => now(), 'responded_at' => now(), ]); $previousStatus = $machine->status; $machine->update([ 'status' => 'running', 'current_user_id' => $payload['requested_by_user_id'] ?? $machine->current_user_id, 'cycle_started_at' => now(), 'cycle_ends_at' => $cycleEndsAt, 'last_heartbeat_at' => now(), ]); $this->recordStatusChange($machine, $previousStatus, 'running', $provider, 'cycle_started'); $this->recordEvent($machine, $provider, 'cycle_started', [ 'command_uuid' => $command->uuid, 'wash_uuid' => $payload['wash_uuid'] ?? null, ]); SimulateCycleCompletion::dispatch( $machine->id, $command->id, $payload['wash_id'] ?? null, )->delay($cycleEndsAt); return MachineCommandResult::success('acknowledged', $externalReference, $correlationId); } public function stopCycle(Machine $machine, array $payload = []): MachineCommandResult { $provider = config('laverie.simulation.provider_name', 'simulated'); $correlationId = (string) Str::uuid(); MachineCommand::query()->create([ 'uuid' => (string) Str::uuid(), 'machine_id' => $machine->id, 'provider' => $provider, 'command_type' => 'stop_cycle', 'payload' => $payload, 'status' => 'acknowledged', 'correlation_id' => $correlationId, 'sent_at' => now(), 'responded_at' => now(), ]); $previousStatus = $machine->status; $machine->update([ 'status' => 'available', 'current_user_id' => null, 'cycle_started_at' => null, 'cycle_ends_at' => null, 'last_heartbeat_at' => now(), ]); $this->recordStatusChange($machine, $previousStatus, 'available', $provider, 'cycle_stopped'); $this->recordEvent($machine, $provider, 'cycle_completed', ['reason' => 'manual_stop']); return MachineCommandResult::success('acknowledged', null, $correlationId); } public function refreshStatus(Machine $machine): MachineStatusSnapshot { $machine->refresh(); return new MachineStatusSnapshot( status: $machine->status, lastHeartbeat: $machine->last_heartbeat_at, metadata: [ 'cycle_started_at' => $machine->cycle_started_at?->toIso8601String(), 'cycle_ends_at' => $machine->cycle_ends_at?->toIso8601String(), 'current_user_id' => $machine->current_user_id, ], ); } public function handleInboundEvent(array $payload): void { $externalMachineId = $payload['external_machine_id'] ?? $payload['machine_id'] ?? null; if ($externalMachineId === null) { return; } $integration = \App\Models\MachineIntegration::query() ->where('external_machine_id', (string) $externalMachineId) ->where('is_active', true) ->first(); if ($integration === null) { return; } $machine = $integration->machine; $provider = $integration->provider; $eventType = $payload['event_type'] ?? 'status_changed'; if ($eventType === 'heartbeat') { $machine->update(['last_heartbeat_at' => now()]); if ($machine->status === 'offline') { $previousStatus = $machine->status; $machine->update(['status' => 'available']); $this->recordStatusChange($machine, $previousStatus, 'available', $provider, 'heartbeat'); } } if ($eventType === 'machine_offline') { $previousStatus = $machine->status; $machine->update(['status' => 'offline']); $this->recordStatusChange($machine, $previousStatus, 'offline', $provider, 'machine_offline'); } if ($eventType === 'machine_online') { $previousStatus = $machine->status; $machine->update(['status' => 'available', 'last_heartbeat_at' => now()]); $this->recordStatusChange($machine, $previousStatus, 'available', $provider, 'machine_online'); } if ($eventType === 'cycle_completed') { $previousStatus = $machine->status; $machine->update([ 'status' => 'available', 'current_user_id' => null, 'cycle_started_at' => null, 'cycle_ends_at' => null, 'last_heartbeat_at' => now(), ]); $this->recordStatusChange($machine, $previousStatus, 'available', $provider, 'cycle_completed'); } if ($eventType === 'error_reported') { $previousStatus = $machine->status; $machine->update(['status' => 'error']); $this->recordStatusChange($machine, $previousStatus, 'error', $provider, 'error_reported'); } $this->recordEvent( $machine, $provider, $eventType, $payload, $payload['external_event_id'] ?? null, ); } private function recordStatusChange( Machine $machine, ?string $previousStatus, string $newStatus, string $source, string $reason, ): void { MachineStatusHistory::query()->create([ 'machine_id' => $machine->id, 'previous_status' => $previousStatus, 'new_status' => $newStatus, 'source' => $source, 'reason' => $reason, 'created_at' => Carbon::now(), ]); } /** * @param array $payload */ private function recordEvent( Machine $machine, string $provider, string $eventType, array $payload, ?string $externalEventId = null, ): void { MachineEvent::query()->create([ 'machine_id' => $machine->id, 'provider' => $provider, 'external_event_id' => $externalEventId, 'event_type' => $eventType, 'payload' => $payload, 'occurred_at' => now(), 'received_at' => now(), 'processed_at' => now(), 'processing_status' => 'processed', ]); } }