$name, 'guard_name' => 'web']); } private function userWithRole(string $role, ?Service $service = null): User { $service ??= Service::factory()->create(); $user = User::factory()->create(['service_id' => $service->id, 'email_verified_at' => now()]); $user->assignRole($this->createRole($role)); return $user; } private function makeBudget(Service $service, string $statut = 'preparation'): Budget { return Budget::create([ 'service_id' => $service->id, 'annee' => now()->year, 'type_budget' => 'agglo', 'statut' => $statut, ]); } private function makeLigne(Budget $budget, float $propose = 5000, ?float $arbitre = null): LigneBudget { return LigneBudget::create([ 'budget_id' => $budget->id, 'nom' => 'Ligne test', 'type_depense' => 'investissement', 'montant_propose' => $propose, 'montant_arbitre' => $arbitre, ]); } private function makeCommande(array $overrides = []): \App\Models\Commande { static $counter = 1; $service = $overrides['service_id'] ?? Service::factory()->create()->id; $user = $overrides['user_id'] ?? User::factory()->create(['service_id' => $service, 'email_verified_at' => now()])->id; return \App\Models\Commande::create(array_merge([ 'service_id' => $service, 'user_id' => $user, 'numero_commande' => 'CMD-METIER-' . $counter++, 'objet' => 'Commande test', 'statut' => 'brouillon', 'montant_ht' => 1000, 'montant_ttc' => 1200, 'date_demande' => now()->toDateString(), ], $overrides)); } // ------------------------------------------------------------------------- // Historique des arbitrages // ------------------------------------------------------------------------- public function test_arbitrage_creates_historique_entry(): void { $service = Service::factory()->create(); $raf = $this->userWithRole('raf', $service); $budget = $this->makeBudget($service); $ligne = $this->makeLigne($budget, 1000); $this->actingAs($raf)->patch(route('lignes-budget.arbitrer', $ligne), [ 'statut_arbitrage' => 'accepte_dsi', 'montant_arbitre' => 800, 'commentaire' => 'Réduit suite révision priorités', ]); $this->assertDatabaseHas('historique_budgets', [ 'ligne_budget_id' => $ligne->id, 'user_id' => $raf->id, 'ancien_statut' => 'brouillon', 'nouveau_statut' => 'accepte_dsi', 'ancien_montant_arbitre' => null, 'nouveau_montant_arbitre' => 800, 'commentaire' => 'Réduit suite révision priorités', ]); } public function test_multiple_arbitrages_build_full_history(): void { $service = Service::factory()->create(); $raf = $this->userWithRole('raf', $service); $budget = $this->makeBudget($service); $ligne = $this->makeLigne($budget, 1000); $this->actingAs($raf)->patch(route('lignes-budget.arbitrer', $ligne), [ 'statut_arbitrage' => 'accepte_dsi', 'montant_arbitre' => 800, ]); $this->actingAs($raf)->patch(route('lignes-budget.arbitrer', $ligne), [ 'statut_arbitrage' => 'valide_definitif', 'montant_arbitre' => 750, ]); $this->assertSame(2, HistoriqueBudget::where('ligne_budget_id', $ligne->id)->count()); // Le deuxième arbitrage (le plus récent) doit tracer le passage de accepte_dsi → valide_definitif $last = HistoriqueBudget::where('ligne_budget_id', $ligne->id)->oldest()->skip(1)->first(); $this->assertEquals('accepte_dsi', $last->ancien_statut); $this->assertEquals('valide_definitif', $last->nouveau_statut); $this->assertEquals(800, (float) $last->ancien_montant_arbitre); $this->assertEquals(750, (float) $last->nouveau_montant_arbitre); } public function test_historique_is_included_in_budget_show(): void { $service = Service::factory()->create(); $raf = $this->userWithRole('raf', $service); $budget = $this->makeBudget($service); $ligne = $this->makeLigne($budget, 1000); $this->actingAs($raf)->patch(route('lignes-budget.arbitrer', $ligne), [ 'statut_arbitrage' => 'accepte_dsi', 'montant_arbitre' => 800, 'commentaire' => 'Premier arbitrage', ]); $response = $this->actingAs($raf)->get(route('budgets.show', $budget)); $response->assertOk(); $response->assertInertia(fn ($page) => $page->has('lignes.0.historique', 1) ->where('lignes.0.historique.0.commentaire', 'Premier arbitrage') ->where('lignes.0.historique.0.user', $raf->name) ); } public function test_historique_deleted_when_ligne_deleted(): void { $service = Service::factory()->create(); $raf = $this->userWithRole('raf', $service); $budget = $this->makeBudget($service); $ligne = $this->makeLigne($budget, 1000); $this->actingAs($raf)->patch(route('lignes-budget.arbitrer', $ligne), [ 'statut_arbitrage' => 'accepte_dsi', 'montant_arbitre' => 800, ]); $ligneId = $ligne->id; $this->actingAs($this->userWithRole('admin'))->delete(route('lignes-budget.destroy', $ligne)); $this->assertDatabaseMissing('historique_budgets', ['ligne_budget_id' => $ligneId]); } // ------------------------------------------------------------------------- // Blocage des commandes si budget insuffisant // ------------------------------------------------------------------------- public function test_commande_creation_blocked_when_budget_exceeded(): void { $service = Service::factory()->create(); $acheteur = $this->userWithRole('acheteur', $service); $budget = $this->makeBudget($service); $ligne = $this->makeLigne($budget, propose: 5000, arbitre: 1000); // Commande existante qui consomme 800 € TTC $this->makeCommande([ 'ligne_budget_id' => $ligne->id, 'service_id' => $service->id, 'user_id' => $acheteur->id, 'montant_ttc' => 800, 'statut' => 'validee', ]); // Tentative de créer une commande avec une ligne à 500 € HT + 20% TVA = 600 € TTC // Reste dispo : 1000 - 800 = 200 € → 600 > 200 → bloqué $response = $this->actingAs($acheteur)->post(route('commandes.store'), [ 'service_id' => $service->id, 'objet' => 'Dépassement test', 'priorite' => 'normale', 'date_demande' => now()->toDateString(), 'ligne_budget_id' => $ligne->id, 'lignes' => [ ['designation' => 'Article test', 'quantite' => 1, 'prix_unitaire_ht' => 500, 'taux_tva' => 20], ], ]); $response->assertSessionHasErrors('ligne_budget_id'); } public function test_commande_creation_allowed_when_budget_sufficient(): void { $service = Service::factory()->create(); $acheteur = $this->userWithRole('acheteur', $service); $budget = $this->makeBudget($service); $ligne = $this->makeLigne($budget, propose: 5000, arbitre: 2000); // Commande existante de 800 € TTC, reste 1200 € $this->makeCommande([ 'ligne_budget_id' => $ligne->id, 'service_id' => $service->id, 'user_id' => $acheteur->id, 'montant_ttc' => 800, 'statut' => 'validee', ]); // Nouvelle ligne à 100 € HT + 20% = 120 € TTC → OK (800+120=920 < 2000) $response = $this->actingAs($acheteur)->post(route('commandes.store'), [ 'service_id' => $service->id, 'objet' => 'Commande dans le budget', 'priorite' => 'normale', 'date_demande' => now()->toDateString(), 'ligne_budget_id' => $ligne->id, 'lignes' => [ ['designation' => 'Article test', 'quantite' => 1, 'prix_unitaire_ht' => 100, 'taux_tva' => 20], ], ]); $response->assertSessionDoesntHaveErrors('ligne_budget_id'); } public function test_commande_creation_not_blocked_when_ligne_not_yet_arbitree(): void { $service = Service::factory()->create(); $acheteur = $this->userWithRole('acheteur', $service); $budget = $this->makeBudget($service); // Ligne sans montant_arbitre $ligne = $this->makeLigne($budget, propose: 1000, arbitre: null); // Ligne à 9000 € HT → normalement dépasserait, mais pas arbitrée = pas de blocage $response = $this->actingAs($acheteur)->post(route('commandes.store'), [ 'service_id' => $service->id, 'objet' => 'Commande avant arbitrage', 'priorite' => 'normale', 'date_demande' => now()->toDateString(), 'ligne_budget_id' => $ligne->id, 'lignes' => [ ['designation' => 'Article test', 'quantite' => 1, 'prix_unitaire_ht' => 9000, 'taux_tva' => 20], ], ]); $response->assertSessionDoesntHaveErrors('ligne_budget_id'); } }