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:
Binary file not shown.
@@ -2,17 +2,17 @@
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
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\Models\Client;
|
||||
use App\Models\Workspace;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Folder>
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Declaration>
|
||||
*/
|
||||
class FolderFactory extends Factory
|
||||
class DeclarationFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* Define the model's default state.
|
||||
@@ -25,7 +25,7 @@ class FolderFactory extends Factory
|
||||
$client = Client::factory()->create(['workspace_id' => $workspace->id]);
|
||||
|
||||
$year = fake()->numberBetween(2024, 2026);
|
||||
$excludeOldVat = array_filter(FolderType::getValues(), fn ($v) => $v !== 'vat');
|
||||
$excludeOldVat = array_filter(DeclarationType::getValues(), fn ($v) => $v !== 'vat');
|
||||
$type = fake()->randomElement(array_values($excludeOldVat));
|
||||
|
||||
$isVatMonthly = $type === 'vat_monthly';
|
||||
@@ -41,8 +41,8 @@ class FolderFactory extends Factory
|
||||
'period_month' => $isVatMonthly ? fake()->numberBetween(1, 12) : null,
|
||||
'period_quarter' => $isVatQuarterly ? fake()->numberBetween(1, 4) : null,
|
||||
'due_date' => fake()->dateTimeBetween('now', '+3 months'),
|
||||
'status' => fake()->randomElement(FolderStatus::getValues()),
|
||||
'priority' => fake()->randomElement(FolderPriority::getValues()),
|
||||
'status' => DeclarationStatus::Created,
|
||||
'priority' => fake()->randomElement(DeclarationPriority::getValues()),
|
||||
'assigned_to' => null,
|
||||
'validated_at' => null,
|
||||
'closed_at' => null,
|
||||
@@ -1,8 +1,8 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateActivityLogTable extends Migration
|
||||
{
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class AddEventColumnToActivityLogTable extends Migration
|
||||
{
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class AddBatchUuidColumnToActivityLogTable extends Migration
|
||||
{
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
DB::table('media')
|
||||
->where('model_type', 'App\\Models\\Folder')
|
||||
->update(['model_type' => 'App\\Models\\Declaration']);
|
||||
|
||||
DB::table('activity_log')
|
||||
->where('subject_type', 'App\\Models\\Folder')
|
||||
->update(['subject_type' => 'App\\Models\\Declaration']);
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
DB::table('media')
|
||||
->where('model_type', 'App\\Models\\Declaration')
|
||||
->update(['model_type' => 'App\\Models\\Folder']);
|
||||
|
||||
DB::table('activity_log')
|
||||
->where('subject_type', 'App\\Models\\Declaration')
|
||||
->update(['subject_type' => 'App\\Models\\Folder']);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
<?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::table('workspace_user', function (Blueprint $table) {
|
||||
$table->json('permissions')->nullable()->default(null)->after('role');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('workspace_user', function (Blueprint $table) {
|
||||
$table->dropColumn('permissions');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
<?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::table('declarations', function (Blueprint $table) {
|
||||
$table->timestamp('archived_at')->nullable()->after('deleted_at');
|
||||
$table->index('archived_at');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('declarations', function (Blueprint $table) {
|
||||
$table->dropIndex(['archived_at']);
|
||||
$table->dropColumn('archived_at');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
$mapping = [
|
||||
'draft' => 'created',
|
||||
'waiting_documents' => 'en_cours',
|
||||
'documents_received' => 'en_cours',
|
||||
'processing' => 'en_cours',
|
||||
'additional_documents_requested' => 'en_attente_client',
|
||||
'waiting_client_validation' => 'en_attente_client',
|
||||
'validated' => 'termine',
|
||||
'closed' => 'ferme',
|
||||
'cancelled' => 'ferme',
|
||||
];
|
||||
|
||||
foreach ($mapping as $old => $new) {
|
||||
DB::table('declarations')
|
||||
->where('status', $old)
|
||||
->update(['status' => $new]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
// Data migration cannot be reversed — old status distinctions are lost
|
||||
}
|
||||
};
|
||||
@@ -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++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user