Files
portal/app/app/Models/Tenant.php
T
Дмитрий 8696b5e27f
Accessibility (Pa11y live) / a11y (push) Has been cancelled
SAST — Semgrep / Semgrep SAST scan (push) Has been cancelled
feat/billing: F/J — единый расчёт замков + пополнение/пересчёт снимают оба замка
- 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>
2026-06-23 14:58:44 +03:00

108 lines
3.7 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_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');
}
}