initial commit
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Services\BookingService;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Queue\Queueable;
|
||||
|
||||
class CheckNoShowBookings implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
public function handle(BookingService $bookingService): void
|
||||
{
|
||||
$bookingService->checkNoShows();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\Booking;
|
||||
use App\Models\DailyEstablishmentStat;
|
||||
use App\Models\Establishment;
|
||||
use App\Models\PaymentTransaction;
|
||||
use App\Models\Wash;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Queue\Queueable;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class GenerateDailyStats implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
public function __construct(
|
||||
public ?string $statDate = null,
|
||||
) {}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
$date = $this->statDate !== null
|
||||
? Carbon::parse($this->statDate)->startOfDay()
|
||||
: now()->subDay()->startOfDay();
|
||||
|
||||
$dayStart = $date->copy();
|
||||
$dayEnd = $date->copy()->endOfDay();
|
||||
|
||||
Establishment::query()->where('is_active', true)->each(function (Establishment $establishment) use ($dayStart, $dayEnd, $date) {
|
||||
DB::transaction(function () use ($establishment, $dayStart, $dayEnd, $date) {
|
||||
$machineIds = $establishment->machines()->pluck('id');
|
||||
|
||||
if ($machineIds->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$totalWashes = Wash::query()
|
||||
->whereIn('machine_id', $machineIds)
|
||||
->where('status', 'completed')
|
||||
->whereBetween('ended_at', [$dayStart, $dayEnd])
|
||||
->count();
|
||||
|
||||
$totalRevenue = (float) Wash::query()
|
||||
->whereIn('machine_id', $machineIds)
|
||||
->where('status', 'completed')
|
||||
->whereBetween('ended_at', [$dayStart, $dayEnd])
|
||||
->sum('cost');
|
||||
|
||||
$bookingsCount = Booking::query()
|
||||
->whereIn('machine_id', $machineIds)
|
||||
->whereBetween('created_at', [$dayStart, $dayEnd])
|
||||
->whereNotIn('status', ['cancelled'])
|
||||
->count();
|
||||
|
||||
$noShowCount = Booking::query()
|
||||
->whereIn('machine_id', $machineIds)
|
||||
->where('status', 'no_show')
|
||||
->whereBetween('slot_start', [$dayStart, $dayEnd])
|
||||
->count();
|
||||
|
||||
$userIds = Wash::query()
|
||||
->whereIn('machine_id', $machineIds)
|
||||
->whereBetween('started_at', [$dayStart, $dayEnd])
|
||||
->pluck('user_id')
|
||||
->unique();
|
||||
|
||||
$topUpAmount = $userIds->isEmpty()
|
||||
? 0.0
|
||||
: (float) PaymentTransaction::query()
|
||||
->whereIn('user_id', $userIds)
|
||||
->where('status', 'succeeded')
|
||||
->whereBetween('updated_at', [$dayStart, $dayEnd])
|
||||
->sum('amount');
|
||||
|
||||
$machineCount = max(1, $machineIds->count());
|
||||
$minutesInDay = 24 * 60;
|
||||
$runningMinutes = Wash::query()
|
||||
->whereIn('machine_id', $machineIds)
|
||||
->where('status', 'completed')
|
||||
->whereBetween('ended_at', [$dayStart, $dayEnd])
|
||||
->get(['duration_minutes'])
|
||||
->sum(fn (Wash $wash) => $wash->duration_minutes ?? 0);
|
||||
|
||||
$occupancyRate = round(min(100, ($runningMinutes / ($machineCount * $minutesInDay)) * 100), 2);
|
||||
|
||||
DailyEstablishmentStat::query()->updateOrCreate(
|
||||
[
|
||||
'establishment_id' => $establishment->id,
|
||||
'stat_date' => $date->toDateString(),
|
||||
],
|
||||
[
|
||||
'total_washes' => $totalWashes,
|
||||
'total_revenue' => $totalRevenue,
|
||||
'bookings_count' => $bookingsCount,
|
||||
'no_show_count' => $noShowCount,
|
||||
'top_up_amount' => $topUpAmount,
|
||||
'occupancy_rate' => $occupancyRate,
|
||||
],
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\Machine;
|
||||
use App\Models\MachineEvent;
|
||||
use App\Models\MachineStatusHistory;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Queue\Queueable;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class RefreshOfflineMachines implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
$thresholdMinutes = (int) config('laverie.machine.offline_threshold_minutes', 10);
|
||||
$cutoff = now()->subMinutes($thresholdMinutes);
|
||||
|
||||
$machines = Machine::query()
|
||||
->whereNotIn('status', ['offline', 'maintenance'])
|
||||
->where(function ($query) use ($cutoff) {
|
||||
$query->whereNull('last_heartbeat_at')
|
||||
->orWhere('last_heartbeat_at', '<', $cutoff);
|
||||
})
|
||||
->get();
|
||||
|
||||
foreach ($machines as $machine) {
|
||||
DB::transaction(function () use ($machine) {
|
||||
$locked = Machine::query()->whereKey($machine->id)->lockForUpdate()->firstOrFail();
|
||||
|
||||
if (in_array($locked->status, ['offline', 'maintenance', 'running'], true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$thresholdMinutes = (int) config('laverie.machine.offline_threshold_minutes', 10);
|
||||
$cutoff = now()->subMinutes($thresholdMinutes);
|
||||
|
||||
if ($locked->last_heartbeat_at !== null && $locked->last_heartbeat_at->gte($cutoff)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$previousStatus = $locked->status;
|
||||
$provider = config('laverie.simulation.provider_name', 'simulated');
|
||||
|
||||
$locked->update(['status' => 'offline']);
|
||||
|
||||
MachineStatusHistory::query()->create([
|
||||
'machine_id' => $locked->id,
|
||||
'previous_status' => $previousStatus,
|
||||
'new_status' => 'offline',
|
||||
'source' => 'RefreshOfflineMachines',
|
||||
'reason' => 'heartbeat_timeout',
|
||||
'created_at' => now(),
|
||||
]);
|
||||
|
||||
MachineEvent::query()->create([
|
||||
'machine_id' => $locked->id,
|
||||
'provider' => $provider,
|
||||
'event_type' => 'machine_offline',
|
||||
'payload' => ['reason' => 'heartbeat_timeout'],
|
||||
'occurred_at' => now(),
|
||||
'received_at' => now(),
|
||||
'processed_at' => now(),
|
||||
'processing_status' => 'processed',
|
||||
]);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\Machine;
|
||||
use App\Models\MachineEvent;
|
||||
use App\Models\MachineStatusHistory;
|
||||
use App\Models\Wash;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Queue\Queueable;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class SimulateCycleCompletion implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
public function __construct(
|
||||
public int $machineId,
|
||||
public int $machineCommandId,
|
||||
public ?int $washId = null,
|
||||
) {}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
DB::transaction(function () {
|
||||
$machine = Machine::query()->whereKey($this->machineId)->lockForUpdate()->first();
|
||||
|
||||
if ($machine === null || $machine->status !== 'running') {
|
||||
return;
|
||||
}
|
||||
|
||||
$provider = config('laverie.simulation.provider_name', 'simulated');
|
||||
$previousStatus = $machine->status;
|
||||
|
||||
$machine->update([
|
||||
'status' => 'available',
|
||||
'current_user_id' => null,
|
||||
'cycle_started_at' => null,
|
||||
'cycle_ends_at' => null,
|
||||
'last_heartbeat_at' => now(),
|
||||
]);
|
||||
|
||||
MachineStatusHistory::query()->create([
|
||||
'machine_id' => $machine->id,
|
||||
'previous_status' => $previousStatus,
|
||||
'new_status' => 'available',
|
||||
'source' => $provider,
|
||||
'reason' => 'simulated_cycle_completion',
|
||||
'created_at' => now(),
|
||||
]);
|
||||
|
||||
MachineEvent::query()->create([
|
||||
'machine_id' => $machine->id,
|
||||
'provider' => $provider,
|
||||
'event_type' => 'cycle_completed',
|
||||
'payload' => [
|
||||
'machine_command_id' => $this->machineCommandId,
|
||||
'wash_id' => $this->washId,
|
||||
'simulated' => true,
|
||||
],
|
||||
'occurred_at' => now(),
|
||||
'received_at' => now(),
|
||||
'processed_at' => now(),
|
||||
'processing_status' => 'processed',
|
||||
]);
|
||||
|
||||
if ($this->washId !== null) {
|
||||
$wash = Wash::query()->whereKey($this->washId)->lockForUpdate()->first();
|
||||
|
||||
if ($wash !== null && $wash->status === 'running') {
|
||||
$startedAt = $wash->started_at ?? now();
|
||||
$endedAt = now();
|
||||
|
||||
$wash->update([
|
||||
'status' => 'completed',
|
||||
'ended_at' => $endedAt,
|
||||
'duration_minutes' => max(1, (int) $startedAt->diffInMinutes($endedAt)),
|
||||
]);
|
||||
|
||||
if ($wash->booking_id !== null) {
|
||||
\App\Models\Booking::query()
|
||||
->whereKey($wash->booking_id)
|
||||
->update(['status' => 'completed']);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user