Files
portal/app/app/Models/Tenant.php
T
Дмитрий 7ac9af7c79
Accessibility (Pa11y live) / a11y (push) Has been cancelled
SAST — Semgrep / Semgrep SAST scan (push) Has been cancelled
feat: убрать лимит по числу проектов — ограничение только по балансу/лидам
Правило продукта: ограничений по количеству проектов нет, лимит только
по балансу и заказанным лидам. Убран гейт tenants.limits.max_projects
в ProjectService::create и показ лимита проектов на дашборде. Поле limits
оставлено как резерв; max_users и api_rps в коде не используются.

Заодно фикс типа в EditProjectDialog.spec: sampleProject типизирован
настоящим Project, source_locked больше не краснит vue-tsc.

Тесты: ProjectsStore 13/13, DashboardSummary 11/11, DashboardView 8/8,
EditProjectDialog 7/7; vue-tsc чисто; pint чисто; vite build ок.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 12:47:49 +03:00

110 lines
4.0 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
namespace App\Models;
use Database\Factories\TenantFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
* Тенант — клиент SaaS-портала Лидерра.
*
* Saas-уровневая модель: НЕ имеет RLS-политик (тенанты создаются и
* управляются админкой SaaS). Используется как родительский ресурс
* для всех tenant-aware моделей (User, Project, Deal и др.).
*
* Источник: db/schema.sql v8.6 §3, table `tenants`.
*
* @mixin IdeHelperTenant
*/
class Tenant extends Model
{
/** @use HasFactory<TenantFactory> */
use HasFactory, SoftDeletes;
protected $fillable = [
'subdomain',
'organization_name',
'contact_email',
'timezone',
'locale',
'current_tariff_id',
'balance_rub',
'balance_leads',
'is_trial',
'trial_leads_used',
'last_activity_at',
'last_webhook_at',
'desired_daily_numbers',
'delivered_in_month',
'api_key_limit',
'limits',
'frozen_by_balance_at',
];
protected function casts(): array
{
return [
'is_trial' => 'boolean',
'balance_rub' => 'decimal:2',
'chargeback_unrecovered_rub' => 'decimal:2',
'balance_leads' => 'integer',
'trial_leads_used' => 'integer',
'desired_daily_numbers' => 'integer',
'delivered_in_month' => 'integer',
'api_key_limit' => 'integer',
// JSONB-резерв тарифных ограничений. Ключ max_projects убран —
// лимита по числу проектов нет (ограничение только по балансу/лидам).
// max_users / api_rps в коде не используются (зарезервированы).
'limits' => 'array',
'last_activity_at' => 'datetime',
'last_webhook_at' => 'datetime',
// Billing v2 Spec C: флаг заморозки по балансу (NULL = не заморожен).
'frozen_by_balance_at' => 'datetime',
'created_at' => 'datetime',
'updated_at' => 'datetime',
'deleted_at' => 'datetime',
];
}
/** @return HasMany<User, $this> */
public function users(): HasMany
{
return $this->hasMany(User::class);
}
/** @return HasMany<Project, $this> */
public function projects(): HasMany
{
return $this->hasMany(Project::class);
}
/**
* Сумма daily_limit_target активных проектов — «сколько лидов клиент хочет в день».
* Используется преfflight'ом (Billing v2 Spec C §3.3) как requiredLeads.
*
* NB: фильтр по `is_active` (boolean), не `status` — у projects нет колонки status.
*/
public function requiredLeadsForTomorrow(): int
{
// D2 (23.06.2026, спека balance-lock-unify-FJ): единый расчёт по ПОЛНОМУ
// лимиту активных проектов. Откат share-aware (R-19) — решение владельца:
// один расчёт во всех точках (заморозка/разморозка, снятие проектного блока),
// чтобы замки вели себя предсказуемо.
return (int) $this->projects()
->where('is_active', true)
->sum('daily_limit_target');
}
/** @return BelongsTo<TariffPlan, $this> */
public function tariff(): BelongsTo
{
return $this->belongsTo(TariffPlan::class, 'current_tariff_id');
}
}