8696b5e27f
- D2: requiredLeadsForTomorrow переведён на полный лимит, откат share-aware R-19 [решение владельца] - B/D3: пополнение снимает клиентскую заморозку И блоки проектов вместе, политика всё-или-ничего - F/J/D6: вечерний пересчёт 18:00 снимает блоки проектов у незаморожённых; общий ProjectBlockReleaseService; иерархия заморозка > блок - fix: balance_freeze_log INSERT переведён на главное соединение — межсессионный self-lock с FOR UPDATE топапа [найден живым прогоном, pg_blocking подтвердил; в тестах маскировался SharesSupplierPdo] - spec + plan в docs/superpowers 138/138 биллинг-тестов GREEN. Pint чисто. Живьём B+F подтверждены на докалке. На прод НЕ катилось. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
108 lines
3.7 KiB
PHP
108 lines
3.7 KiB
PHP
<?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_users":5,"max_projects":10,"api_rps":60}
|
||
'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');
|
||
}
|
||
}
|