Overview
The CompanyAiUsage model records every AI diagnostic request made by a company. It tracks token consumption, associates usage with specific orders, and enables monthly quota enforcement based on subscription plans.
Model Definition
namespace App\Models;
class CompanyAiUsage extends Model
{
protected $fillable = [
'company_id',
'order_id',
'year_month',
'plan_snapshot',
'prompt_chars',
'response_chars',
'prompt_tokens_estimated',
'response_tokens_estimated',
'total_tokens_estimated',
'status',
'error_message',
];
}
Attributes
Foreign key to the company that made the AI request
Foreign key to the service order being diagnosed
Month of usage in YYYY-MM format (e.g., 2026-03) for quota tracking
Subscription plan at time of request (e.g., enterprise, developer_test)
Character count of the prompt sent to AI
Character count of the AI response
Estimated tokens used in the prompt (~chars / 4)
response_tokens_estimated
Estimated tokens used in the response (~chars / 4)
Total estimated tokens (prompt + response)
Request status: success, failed, quota_exceeded
Error details if status is failed
Relationships
Company
public function company(): BelongsTo
{
return $this->belongsTo(Company::class);
}
Access the company that made the request:
$usage = CompanyAiUsage::with('company')->find(1);
$companyName = $usage->company->name;
$plan = $usage->company->subscription?->plan;
Order
public function order(): BelongsTo
{
return $this->belongsTo(Order::class);
}
Link usage to the specific service order:
$usage = CompanyAiUsage::with('order')->find(1);
$symptoms = $usage->order->symptoms;
$diagnosis = $usage->order->ai_potential_causes;
Usage Examples
Recording AI Usage
use App\Models\CompanyAiUsage;
$usage = CompanyAiUsage::create([
'company_id' => $company->id,
'order_id' => $order->id,
'year_month' => now()->format('Y-m'),
'plan_snapshot' => $company->subscription?->plan ?? 'starter',
'prompt_chars' => strlen($prompt),
'response_chars' => strlen($response),
'prompt_tokens_estimated' => (int) ceil(strlen($prompt) / 4),
'response_tokens_estimated' => (int) ceil(strlen($response) / 4),
'total_tokens_estimated' => $totalTokens,
'status' => 'success',
]);
Checking Monthly Usage
$currentMonth = now()->format('Y-m');
$monthlyUsage = CompanyAiUsage::where('company_id', $companyId)
->where('year_month', $currentMonth)
->where('status', 'success')
->selectRaw('
COUNT(*) as queries_used,
SUM(total_tokens_estimated) as tokens_used
')
->first();
// Check against plan limits
$queriesUsed = $monthlyUsage->queries_used ?? 0;
$tokensUsed = $monthlyUsage->tokens_used ?? 0;
$plan = $company->subscription?->plan ?? 'starter';
$queryLimit = $aiPlanPolicyService->queryLimit($plan);
$tokenLimit = $aiPlanPolicyService->tokenLimit($plan);
$canUseAi = $queriesUsed < $queryLimit && $tokensUsed < $tokenLimit;
Recording Failed Requests
CompanyAiUsage::create([
'company_id' => $company->id,
'order_id' => $order->id,
'year_month' => now()->format('Y-m'),
'plan_snapshot' => $plan,
'prompt_chars' => strlen($prompt),
'response_chars' => 0,
'prompt_tokens_estimated' => $estimatedTokens,
'response_tokens_estimated' => 0,
'total_tokens_estimated' => $estimatedTokens,
'status' => 'failed',
'error_message' => $exception->getMessage(),
]);
Recording Quota Exceeded
CompanyAiUsage::create([
'company_id' => $company->id,
'order_id' => $order->id,
'year_month' => now()->format('Y-m'),
'plan_snapshot' => $plan,
'prompt_chars' => strlen($prompt),
'response_chars' => 0,
'prompt_tokens_estimated' => $estimatedTokens,
'response_tokens_estimated' => 0,
'total_tokens_estimated' => 0,
'status' => 'quota_exceeded',
'error_message' => 'Monthly query limit reached',
]);
Quota Enforcement
The AiUsageService uses this model to enforce limits:
// app/Services/AiUsageService.php
public function monthlyUsage(Company $company): array
{
$currentMonth = now()->format('Y-m');
$aggregate = CompanyAiUsage::query()
->where('company_id', $company->id)
->where('year_month', $currentMonth)
->where('status', 'success')
->selectRaw('
COUNT(*) as queries_used,
COALESCE(SUM(total_tokens_estimated), 0) as tokens_used
')
->first();
return [
'queries_used' => $aggregate?->queries_used ?? 0,
'tokens_used' => (int) ($aggregate?->tokens_used ?? 0),
];
}
public function canUseAi(Company $company, int $estimatedTokens): bool
{
$plan = $company->subscription?->plan ?? 'starter';
if (!$this->aiPlanPolicyService->supportsAi($plan)) {
return false;
}
$usage = $this->monthlyUsage($company);
$queryLimit = $this->aiPlanPolicyService->queryLimit($plan);
$tokenLimit = $this->aiPlanPolicyService->tokenLimit($plan);
return $usage['queries_used'] < $queryLimit
&& ($usage['tokens_used'] + $estimatedTokens) <= $tokenLimit;
}
Reporting & Analytics
Usage by Month
$usageByMonth = CompanyAiUsage::where('company_id', $companyId)
->where('status', 'success')
->selectRaw('
year_month,
COUNT(*) as total_queries,
SUM(total_tokens_estimated) as total_tokens
')
->groupBy('year_month')
->orderBy('year_month', 'desc')
->get();
Success Rate
$stats = CompanyAiUsage::where('company_id', $companyId)
->whereBetween('created_at', [$startDate, $endDate])
->selectRaw('
COUNT(*) as total_requests,
SUM(CASE WHEN status = "success" THEN 1 ELSE 0 END) as successful,
SUM(CASE WHEN status = "failed" THEN 1 ELSE 0 END) as failed,
SUM(CASE WHEN status = "quota_exceeded" THEN 1 ELSE 0 END) as blocked
')
->first();
$successRate = $stats->total_requests > 0
? ($stats->successful / $stats->total_requests) * 100
: 0;
Top Users by Token Consumption
$topCompanies = CompanyAiUsage::selectRaw('
company_id,
COUNT(*) as query_count,
SUM(total_tokens_estimated) as total_tokens
')
->where('year_month', now()->format('Y-m'))
->where('status', 'success')
->groupBy('company_id')
->orderBy('total_tokens', 'desc')
->limit(10)
->with('company')
->get();
Token Estimation
Token estimation uses a simple heuristic:
// app/Services/AiTokenEstimator.php
public function estimateFromChars(int $chars): int
{
if ($chars <= 0) {
return 0;
}
return (int) ceil($chars / 4);
}
This assumes ~4 characters per token (typical for English text).
Status Values
| Status | Description | Counted Against Quota? |
|---|
success | AI request completed successfully | Yes |
failed | AI request failed due to error | No |
quota_exceeded | Request blocked due to limits | No |
Best Practices
1. Always Snapshot Plan
Store the plan at time of request:
'plan_snapshot' => $company->subscription?->plan ?? 'starter',
This preserves historical accuracy even if the company changes plans.
2. Estimate Tokens Before Request
$estimatedTokens = $aiTokenEstimator->estimateFromChars(strlen($symptoms));
if (!$aiUsageService->canUseAi($company, $estimatedTokens)) {
return response()->json(['error' => 'Quota exceeded'], 403);
}
3. Record All Attempts
Even failed requests should be logged:
try {
$result = $aiService->diagnose($symptoms);
$status = 'success';
} catch (\Exception $e) {
$status = 'failed';
$errorMessage = $e->getMessage();
}
CompanyAiUsage::create([
'status' => $status,
'error_message' => $errorMessage ?? null,
// ...
]);
4. Use Transactions
When creating usage records and updating orders:
DB::transaction(function () use ($company, $order, $aiResult) {
CompanyAiUsage::create([...]);
$order->update([
'ai_diagnosed_at' => now(),
'ai_potential_causes' => $aiResult['causes'],
// ...
]);
});
Database Schema
CREATE TABLE company_ai_usages (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
company_id BIGINT UNSIGNED NOT NULL,
order_id BIGINT UNSIGNED NOT NULL,
year_month VARCHAR(7) NOT NULL,
plan_snapshot VARCHAR(255) NOT NULL,
prompt_chars INT NOT NULL,
response_chars INT NOT NULL,
prompt_tokens_estimated INT NOT NULL,
response_tokens_estimated INT NOT NULL,
total_tokens_estimated INT NOT NULL,
status VARCHAR(255) NOT NULL,
error_message TEXT NULL,
created_at TIMESTAMP NULL,
updated_at TIMESTAMP NULL,
FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE,
FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE,
INDEX idx_company_month_status (company_id, year_month, status)
);