initial commit

This commit is contained in:
bastien
2026-06-28 12:29:53 +02:00
parent 03e85d2f9d
commit b2443b654c
220 changed files with 24969 additions and 1 deletions
+1
View File
@@ -0,0 +1 @@
*.sqlite*
+45
View File
@@ -0,0 +1,45 @@
<?php
namespace Database\Factories;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
/**
* @extends Factory<User>
*/
class UserFactory extends Factory
{
/**
* The current password being used by the factory.
*/
protected static ?string $password;
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'name' => fake()->name(),
'email' => fake()->unique()->safeEmail(),
'email_verified_at' => now(),
'password' => static::$password ??= Hash::make('password'),
'remember_token' => Str::random(10),
];
}
/**
* Indicate that the model's email address should be unverified.
*/
public function unverified(): static
{
return $this->state(fn (array $attributes) => [
'email_verified_at' => null,
]);
}
}
@@ -0,0 +1,51 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->uuid('uuid')->unique();
$table->string('first_name', 100);
$table->string('last_name', 100);
$table->string('email')->unique();
$table->string('phone', 20)->unique()->nullable();
$table->date('birthdate')->nullable();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->string('locale', 10)->default('fr');
$table->boolean('is_active')->default(true);
$table->timestamp('anonymized_at')->nullable();
$table->rememberToken();
$table->timestamps();
$table->softDeletes();
});
Schema::create('password_reset_tokens', function (Blueprint $table) {
$table->string('email')->primary();
$table->string('token');
$table->timestamp('created_at')->nullable();
});
Schema::create('sessions', function (Blueprint $table) {
$table->string('id')->primary();
$table->foreignId('user_id')->nullable()->index();
$table->string('ip_address', 45)->nullable();
$table->text('user_agent')->nullable();
$table->longText('payload');
$table->integer('last_activity')->index();
});
}
public function down(): void
{
Schema::dropIfExists('users');
Schema::dropIfExists('password_reset_tokens');
Schema::dropIfExists('sessions');
}
};
@@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('cache', function (Blueprint $table) {
$table->string('key')->primary();
$table->mediumText('value');
$table->bigInteger('expiration')->index();
});
Schema::create('cache_locks', function (Blueprint $table) {
$table->string('key')->primary();
$table->string('owner');
$table->bigInteger('expiration')->index();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('cache');
Schema::dropIfExists('cache_locks');
}
};
@@ -0,0 +1,57 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('jobs', function (Blueprint $table) {
$table->id();
$table->string('queue')->index();
$table->longText('payload');
$table->unsignedSmallInteger('attempts');
$table->unsignedInteger('reserved_at')->nullable();
$table->unsignedInteger('available_at');
$table->unsignedInteger('created_at');
});
Schema::create('job_batches', function (Blueprint $table) {
$table->string('id')->primary();
$table->string('name');
$table->integer('total_jobs');
$table->integer('pending_jobs');
$table->integer('failed_jobs');
$table->longText('failed_job_ids');
$table->mediumText('options')->nullable();
$table->integer('cancelled_at')->nullable();
$table->integer('created_at');
$table->integer('finished_at')->nullable();
});
Schema::create('failed_jobs', function (Blueprint $table) {
$table->id();
$table->string('uuid')->unique();
$table->string('connection', 100);
$table->string('queue', 100);
$table->longText('payload');
$table->longText('exception');
$table->timestamp('failed_at')->useCurrent()->index();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('jobs');
Schema::dropIfExists('job_batches');
Schema::dropIfExists('failed_jobs');
}
};
@@ -0,0 +1,328 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('organizations', function (Blueprint $table) {
$table->id();
$table->uuid('uuid')->unique();
$table->string('name', 200);
$table->string('code', 50)->unique()->nullable();
$table->boolean('is_active')->default(true);
$table->timestamps();
});
Schema::create('establishments', function (Blueprint $table) {
$table->id();
$table->foreignId('organization_id')->constrained();
$table->uuid('uuid')->unique();
$table->string('name', 200);
$table->text('address');
$table->string('city', 100)->nullable();
$table->string('zip_code', 10)->nullable();
$table->decimal('latitude', 10, 8)->nullable();
$table->decimal('longitude', 11, 8)->nullable();
$table->string('timezone', 50)->default('Europe/Paris');
$table->boolean('is_active')->default(true);
$table->timestamps();
});
Schema::create('supervisors', function (Blueprint $table) {
$table->id();
$table->foreignId('organization_id')->constrained();
$table->foreignId('establishment_id')->nullable()->constrained();
$table->uuid('uuid')->unique();
$table->string('first_name', 100);
$table->string('last_name', 100);
$table->string('email')->unique();
$table->string('password');
$table->enum('role', ['platform_admin', 'owner', 'manager', 'viewer'])->default('manager');
$table->boolean('is_active')->default(true);
$table->timestamp('last_login_at')->nullable();
$table->rememberToken();
$table->timestamps();
});
Schema::create('gdpr_consents', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained();
$table->enum('type', ['data_processing', 'marketing', 'analytics']);
$table->boolean('accepted');
$table->string('policy_version', 50);
$table->string('policy_text_hash');
$table->enum('source', ['mobile', 'web', 'backoffice']);
$table->string('consent_language', 10)->default('fr');
$table->string('ip_address', 45)->nullable();
$table->text('user_agent')->nullable();
$table->timestamp('accepted_at');
$table->timestamp('revoked_at')->nullable();
});
Schema::create('user_devices', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained();
$table->enum('platform', ['android', 'ios', 'web']);
$table->string('push_token')->unique();
$table->string('app_version', 50)->nullable();
$table->string('device_name', 100)->nullable();
$table->timestamp('last_seen_at')->nullable();
$table->timestamp('revoked_at')->nullable();
$table->timestamps();
});
Schema::create('notification_preferences', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->unique()->constrained();
$table->boolean('transaction_enabled')->default(true);
$table->boolean('reminder_enabled')->default(true);
$table->boolean('marketing_enabled')->default(false);
$table->boolean('system_enabled')->default(true);
$table->timestamps();
});
Schema::create('machines', function (Blueprint $table) {
$table->id();
$table->foreignId('establishment_id')->constrained();
$table->uuid('uuid')->unique();
$table->string('name', 100);
$table->enum('type', ['washer_small', 'washer_large', 'dryer_small', 'dryer_large']);
$table->string('qr_code')->unique();
$table->enum('status', ['available', 'reserved', 'running', 'maintenance', 'offline', 'error'])->default('available');
$table->foreignId('current_user_id')->nullable()->constrained('users');
$table->timestamp('cycle_started_at')->nullable();
$table->timestamp('cycle_ends_at')->nullable();
$table->timestamp('last_heartbeat_at')->nullable();
$table->timestamps();
});
Schema::create('machine_integrations', function (Blueprint $table) {
$table->id();
$table->foreignId('machine_id')->constrained();
$table->string('provider', 100);
$table->string('external_machine_id', 100);
$table->string('external_site_id', 100)->nullable();
$table->enum('mode', ['pull', 'push', 'hybrid', 'simulated'])->default('simulated');
$table->json('config')->nullable();
$table->boolean('is_active')->default(true);
$table->timestamps();
$table->unique(['provider', 'external_machine_id']);
});
Schema::create('machine_commands', function (Blueprint $table) {
$table->id();
$table->uuid('uuid')->unique();
$table->foreignId('machine_id')->constrained();
$table->string('provider', 100);
$table->enum('command_type', ['start_cycle', 'stop_cycle', 'refresh_status']);
$table->json('payload')->nullable();
$table->enum('status', ['pending', 'sent', 'acknowledged', 'failed', 'timeout', 'cancelled'])->default('pending');
$table->string('external_reference')->nullable();
$table->string('correlation_id')->nullable();
$table->foreignId('requested_by_user_id')->nullable()->constrained('users');
$table->foreignId('requested_by_supervisor_id')->nullable()->constrained('supervisors');
$table->timestamp('sent_at')->nullable();
$table->timestamp('responded_at')->nullable();
$table->timestamps();
});
Schema::create('machine_events', function (Blueprint $table) {
$table->id();
$table->foreignId('machine_id')->constrained();
$table->string('provider', 100);
$table->string('external_event_id')->nullable();
$table->enum('event_type', [
'heartbeat', 'machine_online', 'machine_offline',
'cycle_started', 'cycle_completed', 'cycle_failed',
'error_reported', 'status_changed',
]);
$table->json('payload')->nullable();
$table->timestamp('occurred_at');
$table->timestamp('received_at');
$table->timestamp('processed_at')->nullable();
$table->enum('processing_status', ['pending', 'processed', 'failed', 'ignored'])->default('pending');
$table->timestamps();
});
Schema::create('machine_status_history', function (Blueprint $table) {
$table->id();
$table->foreignId('machine_id')->constrained();
$table->string('previous_status', 50)->nullable();
$table->string('new_status', 50);
$table->string('source', 100);
$table->string('reason')->nullable();
$table->timestamp('created_at')->nullable();
});
Schema::create('pricing_rules', function (Blueprint $table) {
$table->id();
$table->foreignId('establishment_id')->constrained();
$table->foreignId('machine_id')->nullable()->constrained();
$table->enum('machine_type', ['washer_small', 'washer_large', 'dryer_small', 'dryer_large'])->nullable();
$table->enum('day_type', ['weekday', 'weekend', 'holiday', 'all'])->default('all');
$table->time('slot_start');
$table->time('slot_end');
$table->decimal('price', 8, 2);
$table->string('label', 100)->nullable();
$table->boolean('requires_app')->default(false);
$table->smallInteger('priority')->default(100);
$table->boolean('is_active')->default(true);
$table->timestamps();
});
Schema::create('promotions', function (Blueprint $table) {
$table->id();
$table->foreignId('establishment_id')->constrained();
$table->enum('machine_type', ['washer_small', 'washer_large', 'dryer_small', 'dryer_large', 'all'])->default('all');
$table->enum('discount_type', ['percent', 'fixed']);
$table->decimal('discount_value', 8, 2);
$table->timestamp('starts_at');
$table->timestamp('ends_at');
$table->text('description')->nullable();
$table->boolean('is_active')->default(true);
$table->timestamps();
});
Schema::create('wallets', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->unique()->constrained();
$table->char('currency', 3)->default('EUR');
$table->decimal('current_balance', 10, 2)->default(0);
$table->enum('status', ['active', 'blocked', 'closed'])->default('active');
$table->timestamps();
});
Schema::create('payment_transactions', function (Blueprint $table) {
$table->id();
$table->uuid('uuid')->unique();
$table->foreignId('user_id')->constrained();
$table->string('provider', 50);
$table->string('provider_payment_id')->nullable();
$table->decimal('amount', 10, 2);
$table->char('currency', 3)->default('EUR');
$table->enum('status', ['initiated', 'pending', 'succeeded', 'failed', 'cancelled', 'refunded'])->default('initiated');
$table->string('idempotency_key', 100)->unique();
$table->string('return_url')->nullable();
$table->json('raw_payload')->nullable();
$table->timestamps();
});
Schema::create('wallet_transactions', function (Blueprint $table) {
$table->id();
$table->uuid('uuid')->unique();
$table->foreignId('wallet_id')->constrained();
$table->enum('type', ['credit', 'debit', 'hold', 'release', 'refund', 'adjustment']);
$table->decimal('amount', 10, 2);
$table->decimal('balance_before', 10, 2);
$table->decimal('balance_after', 10, 2);
$table->string('source_type', 100);
$table->unsignedBigInteger('source_id')->nullable();
$table->string('idempotency_key', 100)->nullable();
$table->json('metadata')->nullable();
$table->timestamp('created_at')->nullable();
});
Schema::create('bookings', function (Blueprint $table) {
$table->id();
$table->uuid('uuid')->unique();
$table->foreignId('user_id')->constrained();
$table->foreignId('machine_id')->constrained();
$table->timestamp('slot_start');
$table->timestamp('slot_end');
$table->decimal('booking_fee', 8, 2)->default(0);
$table->decimal('reserved_amount', 8, 2)->default(0);
$table->decimal('penalty_amount', 8, 2)->default(0);
$table->enum('status', ['pending', 'confirmed', 'cancelled', 'expired', 'active', 'completed', 'no_show'])->default('pending');
$table->timestamp('cancelled_at')->nullable();
$table->timestamp('penalty_applied_at')->nullable();
$table->timestamps();
});
Schema::create('washes', function (Blueprint $table) {
$table->id();
$table->uuid('uuid')->unique();
$table->foreignId('user_id')->constrained();
$table->foreignId('machine_id')->constrained();
$table->foreignId('booking_id')->nullable()->constrained();
$table->foreignId('machine_command_id')->nullable()->constrained();
$table->enum('trigger_method', ['qr_code', 'booking', 'supervisor', 'system']);
$table->enum('status', ['pending_start', 'running', 'completed', 'failed', 'cancelled'])->default('pending_start');
$table->string('program', 100)->nullable();
$table->timestamp('started_at')->nullable();
$table->timestamp('ended_at')->nullable();
$table->unsignedInteger('duration_minutes')->nullable();
$table->decimal('cost', 8, 2);
$table->timestamps();
});
Schema::create('push_notifications', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->nullable()->constrained();
$table->string('type', 100);
$table->string('title');
$table->text('body');
$table->json('data')->nullable();
$table->enum('status', ['pending', 'sent', 'failed'])->default('pending');
$table->timestamp('sent_at')->nullable();
$table->timestamps();
});
Schema::create('audit_logs', function (Blueprint $table) {
$table->id();
$table->string('actor_type', 50);
$table->unsignedBigInteger('actor_id');
$table->foreignId('organization_id')->nullable()->constrained();
$table->foreignId('establishment_id')->nullable()->constrained();
$table->string('action', 100);
$table->string('target_type', 100);
$table->unsignedBigInteger('target_id')->nullable();
$table->json('before_data')->nullable();
$table->json('after_data')->nullable();
$table->string('ip_address', 45)->nullable();
$table->timestamp('created_at')->nullable();
});
Schema::create('daily_establishment_stats', function (Blueprint $table) {
$table->id();
$table->foreignId('establishment_id')->constrained();
$table->date('stat_date');
$table->unsignedInteger('total_washes')->default(0);
$table->decimal('total_revenue', 10, 2)->default(0);
$table->unsignedInteger('bookings_count')->default(0);
$table->unsignedInteger('no_show_count')->default(0);
$table->decimal('top_up_amount', 10, 2)->default(0);
$table->decimal('occupancy_rate', 5, 2)->default(0);
$table->timestamps();
$table->unique(['establishment_id', 'stat_date']);
});
}
public function down(): void
{
Schema::dropIfExists('daily_establishment_stats');
Schema::dropIfExists('audit_logs');
Schema::dropIfExists('push_notifications');
Schema::dropIfExists('washes');
Schema::dropIfExists('bookings');
Schema::dropIfExists('wallet_transactions');
Schema::dropIfExists('payment_transactions');
Schema::dropIfExists('wallets');
Schema::dropIfExists('promotions');
Schema::dropIfExists('pricing_rules');
Schema::dropIfExists('machine_status_history');
Schema::dropIfExists('machine_events');
Schema::dropIfExists('machine_commands');
Schema::dropIfExists('machine_integrations');
Schema::dropIfExists('machines');
Schema::dropIfExists('notification_preferences');
Schema::dropIfExists('user_devices');
Schema::dropIfExists('gdpr_consents');
Schema::dropIfExists('supervisors');
Schema::dropIfExists('establishments');
Schema::dropIfExists('organizations');
}
};
@@ -0,0 +1,33 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('personal_access_tokens', function (Blueprint $table) {
$table->id();
$table->morphs('tokenable');
$table->text('name');
$table->string('token', 64)->unique();
$table->text('abilities')->nullable();
$table->timestamp('last_used_at')->nullable();
$table->timestamp('expires_at')->nullable()->index();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('personal_access_tokens');
}
};
+15
View File
@@ -0,0 +1,15 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
{
public function run(): void
{
$this->call([
DemoSeeder::class,
]);
}
}
+296
View File
@@ -0,0 +1,296 @@
<?php
namespace Database\Seeders;
use App\Models\Establishment;
use App\Models\Machine;
use App\Models\MachineIntegration;
use App\Models\Organization;
use App\Models\PricingRule;
use App\Models\Promotion;
use App\Models\Supervisor;
use App\Models\User;
use App\Models\Wallet;
use Illuminate\Database\Seeder;
/**
* Jeu de données de démonstration pour Laverie Connectée.
*
* Crée organisations, établissements, superviseurs, utilisateurs,
* machines simulées, tarifs et promotions actives.
*/
class DemoSeeder extends Seeder
{
/** Mot de passe commun pour tous les comptes de démo. */
private const DEMO_PASSWORD = 'password';
public function run(): void
{
$simulatedProvider = config('laverie.simulation.provider_name', 'simulated');
// --- Organisations ---
$orgCentre = Organization::create([
'name' => 'Laverie du Centre',
'code' => 'LDC',
'is_active' => true,
]);
$orgExpress = Organization::create([
'name' => 'Laverie Express',
'code' => 'LEX',
'is_active' => true,
]);
// --- Établissements (2 pour org1, 1 pour org2) ---
$estCentreVille = Establishment::create([
'organization_id' => $orgCentre->id,
'name' => 'Laverie Centre-Ville',
'address' => '12 rue de la République',
'city' => 'Lyon',
'zip_code' => '69002',
'latitude' => 45.7578137,
'longitude' => 4.8320114,
'timezone' => 'Europe/Paris',
'is_active' => true,
]);
$estPartDieu = Establishment::create([
'organization_id' => $orgCentre->id,
'name' => 'Laverie Part-Dieu',
'address' => '45 avenue du Général Leclerc',
'city' => 'Lyon',
'zip_code' => '69003',
'latitude' => 45.7601061,
'longitude' => 4.8566930,
'timezone' => 'Europe/Paris',
'is_active' => true,
]);
$estExpressNord = Establishment::create([
'organization_id' => $orgExpress->id,
'name' => 'Laverie Express Nord',
'address' => '8 boulevard de la Croix-Rousse',
'city' => 'Lyon',
'zip_code' => '69004',
'latitude' => 45.7740000,
'longitude' => 4.8325000,
'timezone' => 'Europe/Paris',
'is_active' => true,
]);
// --- Superviseurs ---
$this->createSupervisor(
organizationId: $orgCentre->id,
establishmentId: null,
firstName: 'Admin',
lastName: 'Plateforme',
email: 'platform_admin@laverie.local',
role: 'platform_admin',
);
$this->createSupervisor(
organizationId: $orgCentre->id,
establishmentId: null,
firstName: 'Pierre',
lastName: 'Moreau',
email: 'owner1@laverie.local',
role: 'owner',
);
$this->createSupervisor(
organizationId: $orgCentre->id,
establishmentId: $estCentreVille->id,
firstName: 'Sophie',
lastName: 'Bernard',
email: 'manager1@laverie.local',
role: 'manager',
);
$this->createSupervisor(
organizationId: $orgExpress->id,
establishmentId: null,
firstName: 'Marc',
lastName: 'Lefèvre',
email: 'owner2@laverie.local',
role: 'owner',
);
// --- Utilisateurs finaux avec portefeuille (20 € chacun) ---
$demoUsers = [
['first_name' => 'Marie', 'last_name' => 'Dupont', 'email' => 'marie.dupont@demo.local'],
['first_name' => 'Jean', 'last_name' => 'Martin', 'email' => 'jean.martin@demo.local'],
['first_name' => 'Lucie', 'last_name' => 'Petit', 'email' => 'lucie.petit@demo.local'],
['first_name' => 'Thomas', 'last_name' => 'Roux', 'email' => 'thomas.roux@demo.local'],
['first_name' => 'Emma', 'last_name' => 'Girard', 'email' => 'emma.girard@demo.local'],
];
$users = collect($demoUsers)->map(function (array $data) {
$user = User::create([
'first_name' => $data['first_name'],
'last_name' => $data['last_name'],
'email' => $data['email'],
'password' => self::DEMO_PASSWORD,
'locale' => 'fr',
'is_active' => true,
'email_verified_at' => now(),
]);
Wallet::create([
'user_id' => $user->id,
'currency' => 'EUR',
'current_balance' => 20.00,
'status' => 'active',
]);
return $user;
});
// --- Machines (10 au total, statuts variés) ---
$machineDefinitions = [
// Laverie Centre-Ville (4 machines)
['establishment' => $estCentreVille, 'name' => 'Lave-linge 1', 'type' => 'washer_small', 'status' => 'available'],
['establishment' => $estCentreVille, 'name' => 'Lave-linge 2', 'type' => 'washer_large', 'status' => 'running', 'user' => $users[0]],
['establishment' => $estCentreVille, 'name' => 'Sèche-linge 1', 'type' => 'dryer_small', 'status' => 'reserved'],
['establishment' => $estCentreVille, 'name' => 'Sèche-linge 2', 'type' => 'dryer_large', 'status' => 'maintenance'],
// Laverie Part-Dieu (3 machines)
['establishment' => $estPartDieu, 'name' => 'Lave-linge A', 'type' => 'washer_small', 'status' => 'available'],
['establishment' => $estPartDieu, 'name' => 'Lave-linge B', 'type' => 'washer_large', 'status' => 'offline'],
['establishment' => $estPartDieu, 'name' => 'Sèche-linge A', 'type' => 'dryer_small', 'status' => 'error'],
// Laverie Express Nord (3 machines)
['establishment' => $estExpressNord, 'name' => 'Express Lave 1', 'type' => 'washer_small', 'status' => 'available'],
['establishment' => $estExpressNord, 'name' => 'Express Lave 2', 'type' => 'washer_large', 'status' => 'running', 'user' => $users[1]],
['establishment' => $estExpressNord, 'name' => 'Express Sèche 1', 'type' => 'dryer_large', 'status' => 'available'],
];
$machineIndex = 1;
foreach ($machineDefinitions as $definition) {
$machine = Machine::create([
'establishment_id' => $definition['establishment']->id,
'name' => $definition['name'],
'type' => $definition['type'],
'qr_code' => 'LAVERIE-DEMO-'.str_pad((string) $machineIndex, 3, '0', STR_PAD_LEFT),
'status' => $definition['status'],
'current_user_id' => isset($definition['user']) ? $definition['user']->id : null,
'cycle_started_at' => $definition['status'] === 'running' ? now()->subMinutes(15) : null,
'cycle_ends_at' => $definition['status'] === 'running' ? now()->addMinutes(27) : null,
'last_heartbeat_at' => $definition['status'] === 'offline' ? now()->subHours(2) : now()->subMinutes(2),
]);
MachineIntegration::create([
'machine_id' => $machine->id,
'provider' => $simulatedProvider,
'external_machine_id' => 'SIM-'.$machine->uuid,
'external_site_id' => 'SITE-'.$definition['establishment']->uuid,
'mode' => 'simulated',
'config' => [
'cycle_duration_seconds' => config('laverie.simulation.cycle_duration_seconds', 30),
],
'is_active' => true,
]);
$machineIndex++;
}
// --- Règles tarifaires (4 € lavage, 3 € séchage) par établissement ---
foreach ([$estCentreVille, $estPartDieu, $estExpressNord] as $establishment) {
PricingRule::create([
'establishment_id' => $establishment->id,
'machine_type' => 'washer_small',
'day_type' => 'all',
'slot_start' => '00:00:00',
'slot_end' => '23:59:59',
'price' => 4.00,
'label' => 'Lavage standard',
'requires_app' => false,
'priority' => 100,
'is_active' => true,
]);
PricingRule::create([
'establishment_id' => $establishment->id,
'machine_type' => 'washer_large',
'day_type' => 'all',
'slot_start' => '00:00:00',
'slot_end' => '23:59:59',
'price' => 4.00,
'label' => 'Lavage grand tambour',
'requires_app' => false,
'priority' => 100,
'is_active' => true,
]);
PricingRule::create([
'establishment_id' => $establishment->id,
'machine_type' => 'dryer_small',
'day_type' => 'all',
'slot_start' => '00:00:00',
'slot_end' => '23:59:59',
'price' => 3.00,
'label' => 'Séchage standard',
'requires_app' => false,
'priority' => 100,
'is_active' => true,
]);
PricingRule::create([
'establishment_id' => $establishment->id,
'machine_type' => 'dryer_large',
'day_type' => 'all',
'slot_start' => '00:00:00',
'slot_end' => '23:59:59',
'price' => 3.00,
'label' => 'Sèche-linge grand tambour',
'requires_app' => false,
'priority' => 100,
'is_active' => true,
]);
}
// --- Promotions actives (2) ---
Promotion::create([
'establishment_id' => $estCentreVille->id,
'machine_type' => 'washer_small',
'discount_type' => 'percent',
'discount_value' => 10.00,
'starts_at' => now()->subDays(7),
'ends_at' => now()->addDays(30),
'description' => 'Offre de bienvenue : -10 % sur les lavages petit tambour',
'is_active' => true,
]);
Promotion::create([
'establishment_id' => $estExpressNord->id,
'machine_type' => 'all',
'discount_type' => 'fixed',
'discount_value' => 0.50,
'starts_at' => now()->subDays(3),
'ends_at' => now()->addDays(14),
'description' => 'Happy hour Express : 0,50 € de réduction sur tous les cycles',
'is_active' => true,
]);
}
/**
* Crée un superviseur avec mot de passe de démonstration.
*/
private function createSupervisor(
int $organizationId,
?int $establishmentId,
string $firstName,
string $lastName,
string $email,
string $role,
): Supervisor {
return Supervisor::create([
'organization_id' => $organizationId,
'establishment_id' => $establishmentId,
'first_name' => $firstName,
'last_name' => $lastName,
'email' => $email,
'password' => self::DEMO_PASSWORD,
'role' => $role,
'is_active' => true,
]);
}
}