feat: complete Epic 0 — foundation migration & infrastructure setup

Stories 0.2-0.5: rename folders→declarations (backend+frontend), configure
Redis for cache/queue/sessions, add foundation database migrations
(permissions, archived_at), replace DeclarationStatus enum with architecture
lifecycle values, create DeclarationObserver for status transition validation
and auto-archive, fix controller status transitions to respect observer rules.

93 tests pass (240 assertions).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-12 18:25:32 +00:00
parent d380df4074
commit fd43a6f429
105 changed files with 3899 additions and 1558 deletions

View File

@@ -3,15 +3,15 @@
namespace Database\Seeders;
use App\Enums\ClientStatus;
use App\Enums\FolderPriority;
use App\Enums\FolderStatus;
use App\Enums\FolderType;
use App\Enums\DeclarationPriority;
use App\Enums\DeclarationStatus;
use App\Enums\DeclarationType;
use App\Enums\LegalForm;
use App\Enums\UserGroup;
use App\Enums\WorkspaceUserRole;
use App\Models\Client;
use App\Models\ClientContact;
use App\Models\Folder;
use App\Models\Declaration;
use App\Models\User;
use App\Models\Workspace;
use Illuminate\Database\Seeder;
@@ -99,7 +99,7 @@ class DatabaseSeeder extends Seeder
$client = Client::create($data);
ClientContact::create([
'client_id' => $client->id,
'full_name' => $data['contact_first_name'] . ' ' . $data['contact_last_name'],
'full_name' => $data['contact_first_name'].' '.$data['contact_last_name'],
'job_title' => $data['contact_job_title'],
'email' => $data['contact_email'],
'phone' => $data['contact_phone'],
@@ -125,7 +125,7 @@ class DatabaseSeeder extends Seeder
$client = Client::create($data);
ClientContact::create([
'client_id' => $client->id,
'full_name' => $data['contact_first_name'] . ' ' . $data['contact_last_name'],
'full_name' => $data['contact_first_name'].' '.$data['contact_last_name'],
'job_title' => $data['contact_job_title'],
'email' => $data['contact_email'],
'phone' => $data['contact_phone'],
@@ -133,57 +133,55 @@ class DatabaseSeeder extends Seeder
]);
}
// --- Folders (dossiers) for Casablanca clients ---
$folderTypes = [
['type' => FolderType::VatMonthly, 'label' => 'TVA mensuelle'],
['type' => FolderType::VatQuarterly, 'label' => 'TVA trimestrielle'],
['type' => FolderType::CorporateTax, 'label' => 'IS'],
['type' => FolderType::IncomeTax, 'label' => 'IR'],
['type' => FolderType::CNSS, 'label' => 'CNSS'],
['type' => FolderType::AnnualBalance, 'label' => 'Bilan annuel'],
// --- Declarations for Casablanca clients ---
$declarationTypes = [
['type' => DeclarationType::VatMonthly, 'label' => 'TVA mensuelle'],
['type' => DeclarationType::VatQuarterly, 'label' => 'TVA trimestrielle'],
['type' => DeclarationType::CorporateTax, 'label' => 'IS'],
['type' => DeclarationType::IncomeTax, 'label' => 'IR'],
['type' => DeclarationType::CNSS, 'label' => 'CNSS'],
['type' => DeclarationType::AnnualBalance, 'label' => 'Bilan annuel'],
];
$statuses = [
FolderStatus::Draft,
FolderStatus::WaitingDocuments,
FolderStatus::DocumentsReceived,
FolderStatus::Processing,
FolderStatus::WaitingClientValidation,
FolderStatus::Validated,
FolderStatus::Closed,
DeclarationStatus::Created,
DeclarationStatus::EnCours,
DeclarationStatus::EnAttenteClient,
DeclarationStatus::Termine,
DeclarationStatus::Ferme,
];
$priorities = [FolderPriority::Low, FolderPriority::Medium, FolderPriority::High];
$priorities = [DeclarationPriority::Low, DeclarationPriority::Medium, DeclarationPriority::High];
$folderIndex = 0;
$declarationIndex = 0;
foreach ($createdCasaClients as $client) {
// Each client gets 2-4 folders
$numFolders = fake()->numberBetween(2, 4);
$selectedTypes = fake()->randomElements($folderTypes, $numFolders);
// Each client gets 2-4 declarations
$numDeclarations = fake()->numberBetween(2, 4);
$selectedTypes = fake()->randomElements($declarationTypes, $numDeclarations);
foreach ($selectedTypes as $ft) {
foreach ($selectedTypes as $dt) {
$year = fake()->randomElement([2025, 2026]);
$status = $statuses[$folderIndex % count($statuses)];
$isVatMonthly = $ft['type'] === FolderType::VatMonthly;
$isVatQuarterly = $ft['type'] === FolderType::VatQuarterly;
$status = $statuses[$declarationIndex % count($statuses)];
$isVatMonthly = $dt['type'] === DeclarationType::VatMonthly;
$isVatQuarterly = $dt['type'] === DeclarationType::VatQuarterly;
$month = $isVatMonthly ? fake()->numberBetween(1, 3) : null;
$quarter = $isVatQuarterly ? fake()->numberBetween(1, 4) : null;
$periodSuffix = $isVatMonthly ? " — Mois $month" : ($isVatQuarterly ? " — T$quarter" : '');
Folder::create([
$declaration = Declaration::create([
'workspace_id' => $wsCasa->id,
'client_id' => $client->id,
'created_by' => $responsibles[$folderIndex % count($responsibles)]->id,
'title' => "Déclaration {$ft['label']} $year{$periodSuffix}",
'type' => $ft['type'],
'created_by' => $responsibles[$declarationIndex % count($responsibles)]->id,
'title' => "Déclaration {$dt['label']} $year{$periodSuffix}",
'type' => $dt['type'],
'period_year' => $year,
'period_month' => $month,
'period_quarter' => $quarter,
'due_date' => fake()->dateTimeBetween('2026-01-01', '2026-06-30'),
'status' => $status,
'priority' => $priorities[$folderIndex % count($priorities)],
'assigned_to' => $responsibles[$folderIndex % count($responsibles)]->id,
'priority' => $priorities[$declarationIndex % count($priorities)],
'assigned_to' => $responsibles[$declarationIndex % count($responsibles)]->id,
'notes_internal' => fake()->optional(0.4)->randomElement([
'En attente des relevés bancaires.',
'Client à relancer pour les factures manquantes.',
@@ -193,7 +191,12 @@ class DatabaseSeeder extends Seeder
]),
]);
$folderIndex++;
// Set archived_at for ferme declarations (observer only fires on updating, not creating)
if ($status === DeclarationStatus::Ferme) {
$declaration->forceFill(['archived_at' => now()])->saveQuietly();
}
$declarationIndex++;
}
}
}