create(['status' => 'published']); $settings = (array) ($event->tenant->settings ?? []); data_set($settings, 'ai_editing.budget.soft_cap_usd', 10.0); data_set($settings, 'ai_editing.budget.hard_cap_usd', 20.0); $event->tenant->update(['settings' => $settings]); AiUsageLedger::query()->create([ 'tenant_id' => $event->tenant_id, 'event_id' => $event->id, 'entry_type' => AiUsageLedger::TYPE_DEBIT, 'quantity' => 1, 'unit_cost_usd' => 3.0, 'amount_usd' => 3.0, 'currency' => 'USD', 'recorded_at' => now(), ]); $decision = app(AiBudgetGuardService::class)->evaluateForEvent($event->fresh('tenant')); $this->assertTrue($decision['allowed']); $this->assertFalse($decision['budget']['soft_reached']); $this->assertFalse($decision['budget']['hard_reached']); $this->assertSame(3.0, $decision['budget']['current_spend_usd']); } public function test_it_blocks_requests_when_hard_cap_is_reached_without_override(): void { $event = Event::factory()->create(['status' => 'published']); $owner = User::factory()->create(); $event->tenant->update(['user_id' => $owner->id]); $settings = (array) ($event->tenant->settings ?? []); data_set($settings, 'ai_editing.budget.hard_cap_usd', 5.0); $event->tenant->update(['settings' => $settings]); AiUsageLedger::query()->create([ 'tenant_id' => $event->tenant_id, 'event_id' => $event->id, 'entry_type' => AiUsageLedger::TYPE_DEBIT, 'quantity' => 1, 'unit_cost_usd' => 5.0, 'amount_usd' => 5.0, 'currency' => 'USD', 'recorded_at' => now(), ]); $decision = app(AiBudgetGuardService::class)->evaluateForEvent($event->fresh('tenant')); $this->assertFalse($decision['allowed']); $this->assertSame('budget_hard_cap_reached', $decision['reason_code']); $this->assertTrue($decision['budget']['hard_reached']); $this->assertFalse($decision['budget']['override_active']); $this->assertDatabaseHas('tenant_notification_logs', [ 'tenant_id' => $event->tenant_id, 'type' => 'ai_budget_hard_cap', 'channel' => 'system', 'status' => 'sent', ]); $this->assertDatabaseHas('tenant_notification_receipts', [ 'tenant_id' => $event->tenant_id, 'user_id' => $owner->id, 'status' => 'delivered', ]); } public function test_it_throttles_soft_cap_notifications_with_cooldown(): void { $event = Event::factory()->create(['status' => 'published']); $settings = (array) ($event->tenant->settings ?? []); data_set($settings, 'ai_editing.budget.soft_cap_usd', 2.0); data_set($settings, 'ai_editing.budget.hard_cap_usd', 100.0); $event->tenant->update(['settings' => $settings]); AiUsageLedger::query()->create([ 'tenant_id' => $event->tenant_id, 'event_id' => $event->id, 'entry_type' => AiUsageLedger::TYPE_DEBIT, 'quantity' => 1, 'unit_cost_usd' => 3.0, 'amount_usd' => 3.0, 'currency' => 'USD', 'recorded_at' => now(), ]); $service = app(AiBudgetGuardService::class); $service->evaluateForEvent($event->fresh('tenant')); $service->evaluateForEvent($event->fresh('tenant')); $this->assertSame( 1, \App\Models\TenantNotificationLog::query() ->where('tenant_id', $event->tenant_id) ->where('type', 'ai_budget_soft_cap') ->count() ); } }