Intégration reservations et mock lavage
This commit is contained in:
@@ -65,6 +65,10 @@ AWS_USE_PATH_STYLE_ENDPOINT=false
|
|||||||
|
|
||||||
VITE_APP_NAME="${APP_NAME}"
|
VITE_APP_NAME="${APP_NAME}"
|
||||||
|
|
||||||
|
# --- CORS (Flutter Web / clients navigateur) ---
|
||||||
|
# * = toutes origines (dev). En prod : http://localhost:5173,https://app.example.com
|
||||||
|
CORS_ALLOWED_ORIGINS=*
|
||||||
|
|
||||||
# --- Configuration Laverie ---
|
# --- Configuration Laverie ---
|
||||||
|
|
||||||
# Clé API pour les webhooks / intégrations machines (header X-Machine-Api-Key)
|
# Clé API pour les webhooks / intégrations machines (header X-Machine-Api-Key)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
```cmd
|
```cmd
|
||||||
/c/wamp64/bin/php/php8.3.28/php.exe artisan serve
|
/c/wamp64/bin/php/php8.3.28/php.exe artisan serve
|
||||||
vite
|
vite
|
||||||
|
/c/wamp64/bin/php/php8.3.28/php.exe artisan queue:work
|
||||||
```
|
```
|
||||||
|
|
||||||
## About Laravel
|
## About Laravel
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ use App\Http\Resources\BookingResource;
|
|||||||
use App\Models\Booking;
|
use App\Models\Booking;
|
||||||
use App\Models\Machine;
|
use App\Models\Machine;
|
||||||
use App\Services\BookingService;
|
use App\Services\BookingService;
|
||||||
use Carbon\Carbon;
|
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
|
||||||
class BookingController extends Controller
|
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\Booking;
|
||||||
use App\Models\Machine;
|
use App\Models\Machine;
|
||||||
use App\Services\PricingService;
|
use App\Services\PricingService;
|
||||||
|
use App\Support\WashCycleProgress;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
@@ -29,6 +30,34 @@ class MachineController extends Controller
|
|||||||
|
|
||||||
return $this->success([
|
return $this->success([
|
||||||
'machine' => new MachineResource($machine),
|
'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
|
public function start(StartWashRequest $request): JsonResponse
|
||||||
{
|
{
|
||||||
$machine = Machine::query()
|
$machine = $request->filled('machine_uuid')
|
||||||
->where('uuid', $request->string('machine_uuid')->toString())
|
? Machine::query()->where('uuid', $request->string('machine_uuid')->toString())->firstOrFail()
|
||||||
->firstOrFail();
|
: Machine::query()->where('qr_code', $request->string('qr_code')->toString())->firstOrFail();
|
||||||
|
|
||||||
$bookingId = null;
|
$bookingId = null;
|
||||||
if ($request->filled('booking_uuid')) {
|
if ($request->filled('booking_uuid')) {
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ class StartWashRequest extends FormRequest
|
|||||||
public function rules(): array
|
public function rules(): array
|
||||||
{
|
{
|
||||||
return [
|
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'],
|
'booking_uuid' => ['nullable', 'uuid', 'exists:bookings,uuid'],
|
||||||
'trigger_method' => ['required', Rule::in(['qr_code', 'booking', 'supervisor', 'system'])],
|
'trigger_method' => ['required', Rule::in(['qr_code', 'booking', 'supervisor', 'system'])],
|
||||||
'program' => ['nullable', 'string', 'max:100'],
|
'program' => ['nullable', 'string', 'max:100'],
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Http\Resources;
|
namespace App\Http\Resources;
|
||||||
|
|
||||||
|
use App\Support\WashCycleProgress;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Http\Resources\Json\JsonResource;
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
@@ -22,6 +23,7 @@ class WashResource extends JsonResource
|
|||||||
'machine' => new MachineResource($this->whenLoaded('machine')),
|
'machine' => new MachineResource($this->whenLoaded('machine')),
|
||||||
'booking' => new BookingResource($this->whenLoaded('booking')),
|
'booking' => new BookingResource($this->whenLoaded('booking')),
|
||||||
'user' => new UserResource($this->whenLoaded('user')),
|
'user' => new UserResource($this->whenLoaded('user')),
|
||||||
|
'progress' => WashCycleProgress::forWash($this->resource),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ class MachineStatusHistory extends Model
|
|||||||
{
|
{
|
||||||
public const UPDATED_AT = null;
|
public const UPDATED_AT = null;
|
||||||
|
|
||||||
|
protected $table = 'machine_status_history';
|
||||||
|
|
||||||
protected function casts(): array
|
protected function casts(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use App\Models\Booking;
|
|||||||
use App\Models\Machine;
|
use App\Models\Machine;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Models\Wash;
|
use App\Models\Wash;
|
||||||
|
use App\Support\WashCycleProgress;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
@@ -71,6 +72,7 @@ class WashService
|
|||||||
'status' => 'pending_start',
|
'status' => 'pending_start',
|
||||||
'program' => $program,
|
'program' => $program,
|
||||||
'cost' => $cost,
|
'cost' => $cost,
|
||||||
|
'duration_minutes' => WashCycleProgress::estimateDurationMinutes($machine->type),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$result = $this->machineIntegrationService->startCycle($machine, [
|
$result = $this->machineIntegrationService->startCycle($machine, [
|
||||||
|
|||||||
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
$allowedOrigins = env('CORS_ALLOWED_ORIGINS');
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Cross-Origin Resource Sharing (CORS) Configuration
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Requis pour Flutter Web (Chrome) et tout client navigateur.
|
||||||
|
| Sans ce fichier, le middleware HandleCors ne s'applique pas.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'paths' => ['api/*', 'sanctum/csrf-cookie'],
|
||||||
|
|
||||||
|
'allowed_methods' => ['*'],
|
||||||
|
|
||||||
|
'allowed_origins' => $allowedOrigins !== null && $allowedOrigins !== ''
|
||||||
|
? array_values(array_filter(array_map('trim', explode(',', $allowedOrigins))))
|
||||||
|
: ['*'],
|
||||||
|
|
||||||
|
'allowed_origins_patterns' => [
|
||||||
|
'#^https?://localhost(:\d+)?$#',
|
||||||
|
'#^https?://127\.0\.0\.1(:\d+)?$#',
|
||||||
|
'#^https?://\[::1\](:\d+)?$#',
|
||||||
|
],
|
||||||
|
|
||||||
|
'allowed_headers' => ['*'],
|
||||||
|
|
||||||
|
'exposed_headers' => [],
|
||||||
|
|
||||||
|
'max_age' => 86400,
|
||||||
|
|
||||||
|
'supports_credentials' => false,
|
||||||
|
|
||||||
|
];
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use App\Http\Controllers\Api\V1\AuthController;
|
use App\Http\Controllers\Api\V1\AuthController;
|
||||||
|
use App\Http\Controllers\Api\V1\HealthController;
|
||||||
use App\Http\Controllers\Api\V1\BookingController;
|
use App\Http\Controllers\Api\V1\BookingController;
|
||||||
use App\Http\Controllers\Api\V1\EstablishmentController;
|
use App\Http\Controllers\Api\V1\EstablishmentController;
|
||||||
use App\Http\Controllers\Api\V1\Integration\MachineEventController;
|
use App\Http\Controllers\Api\V1\Integration\MachineEventController;
|
||||||
@@ -17,6 +18,9 @@ use App\Http\Controllers\Api\V1\WashController;
|
|||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
Route::prefix('v1')->group(function () {
|
Route::prefix('v1')->group(function () {
|
||||||
|
Route::get('health', [HealthController::class, 'index']);
|
||||||
|
Route::get('health/db', [HealthController::class, 'database']);
|
||||||
|
|
||||||
Route::prefix('auth')->group(function () {
|
Route::prefix('auth')->group(function () {
|
||||||
Route::post('register', [AuthController::class, 'register']);
|
Route::post('register', [AuthController::class, 'register']);
|
||||||
Route::post('login', [AuthController::class, 'login']);
|
Route::post('login', [AuthController::class, 'login']);
|
||||||
@@ -49,6 +53,7 @@ Route::prefix('v1')->group(function () {
|
|||||||
|
|
||||||
Route::get('establishments', [EstablishmentController::class, 'index']);
|
Route::get('establishments', [EstablishmentController::class, 'index']);
|
||||||
Route::get('establishments/{uuid}', [EstablishmentController::class, 'show']);
|
Route::get('establishments/{uuid}', [EstablishmentController::class, 'show']);
|
||||||
|
Route::get('machines/lookup', [MachineController::class, 'lookup']);
|
||||||
Route::get('machines/{uuid}', [MachineController::class, 'show']);
|
Route::get('machines/{uuid}', [MachineController::class, 'show']);
|
||||||
Route::get('machines/{uuid}/availability', [MachineController::class, 'availability']);
|
Route::get('machines/{uuid}/availability', [MachineController::class, 'availability']);
|
||||||
Route::get('machines/{uuid}/pricing', [MachineController::class, 'pricing']);
|
Route::get('machines/{uuid}/pricing', [MachineController::class, 'pricing']);
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature\Api;
|
||||||
|
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class CorsTest extends TestCase
|
||||||
|
{
|
||||||
|
public function test_preflight_options_on_api_returns_cors_headers(): void
|
||||||
|
{
|
||||||
|
$response = $this->call(
|
||||||
|
'OPTIONS',
|
||||||
|
'/api/v1/health',
|
||||||
|
server: [
|
||||||
|
'HTTP_ORIGIN' => 'http://localhost:5173',
|
||||||
|
'HTTP_ACCESS_CONTROL_REQUEST_METHOD' => 'GET',
|
||||||
|
'HTTP_ACCESS_CONTROL_REQUEST_HEADERS' => 'Authorization, Content-Type',
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
$response->assertOk();
|
||||||
|
$response->assertHeader('Access-Control-Allow-Origin', '*');
|
||||||
|
$response->assertHeader('Access-Control-Allow-Methods');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_get_request_includes_cors_headers_for_browser_origin(): void
|
||||||
|
{
|
||||||
|
$response = $this->getJson('/api/v1/health', [
|
||||||
|
'Origin' => 'http://localhost:5173',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertOk();
|
||||||
|
$response->assertHeader('Access-Control-Allow-Origin', '*');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature\Api;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class HealthTest extends TestCase
|
||||||
|
{
|
||||||
|
public function test_health_endpoint_returns_api_status(): void
|
||||||
|
{
|
||||||
|
$response = $this->getJson('/api/v1/health');
|
||||||
|
|
||||||
|
$response->assertOk()
|
||||||
|
->assertJson([
|
||||||
|
'success' => true,
|
||||||
|
'data' => [
|
||||||
|
'status' => 'ok',
|
||||||
|
'service' => 'laverie-api',
|
||||||
|
],
|
||||||
|
])
|
||||||
|
->assertJsonStructure([
|
||||||
|
'data' => ['timestamp'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_database_health_endpoint_returns_ok_when_connected(): void
|
||||||
|
{
|
||||||
|
$response = $this->getJson('/api/v1/health/db');
|
||||||
|
|
||||||
|
$response->assertOk()
|
||||||
|
->assertJson([
|
||||||
|
'success' => true,
|
||||||
|
'data' => [
|
||||||
|
'status' => 'ok',
|
||||||
|
'database' => 'sqlite',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_database_health_endpoint_returns_service_unavailable_when_disconnected(): void
|
||||||
|
{
|
||||||
|
DB::shouldReceive('select')
|
||||||
|
->once()
|
||||||
|
->with('SELECT 1')
|
||||||
|
->andThrow(new \RuntimeException('Connection refused'));
|
||||||
|
|
||||||
|
$response = $this->getJson('/api/v1/health/db');
|
||||||
|
|
||||||
|
$response->assertStatus(503)
|
||||||
|
->assertJson([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Database connection failed',
|
||||||
|
'errors' => [
|
||||||
|
'database' => ['Connection unavailable'],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Unit;
|
||||||
|
|
||||||
|
use App\Models\MachineStatusHistory;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class MachineStatusHistoryTest extends TestCase
|
||||||
|
{
|
||||||
|
public function test_uses_singular_history_table_name(): void
|
||||||
|
{
|
||||||
|
$this->assertSame('machine_status_history', (new MachineStatusHistory)->getTable());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user