File: /home/xedaptot/ai.naniguide.com/app/Model/WarmupStrategy.php
<?php
namespace App\Model;
use App\Dto\WarmupStrategyData;
use App\Services\Warmup\WarmupStrategyFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use App\Library\Traits\HasUid;
class WarmupStrategy extends Model
{
use HasFactory;
use HasUid;
// statuses
public const STATUS_INACTIVE = 'inactive';
public const STATUS_ACTIVE = 'active';
// Limit type
public const LIMIT_TYPE_PER_DAY_CAP = 'per_day_cap';
public const LIMIT_TYPE_TARGET_VOLUME = 'target_volume';
public const LIMIT_TYPE_STOP_AFTER_DAYS = 'stop_after_days';
// Strategy type
public const GROWTH_STRATEGY_LINEAR = 'linear';
public const GROWTH_STRATEGY_EXPONENTIAL = 'exponential';
public const PRESET_BALANCED = 'balanced';
public const PRESET_CAUTIOUS = 'cautious';
public const PRESET_AGGRESSIVE = 'aggressive';
public const QUOTA_TYPE_HOURLY = 'hourly';
public const QUOTA_TYPE_DAILY = 'daily';
public const QUOTA_TYPE_MONTHLY = 'monthly';
public const DEFAULT_MAX_BOUNCE_RATE = 4.0;
public const DEFAULT_MAX_COMPLAINT_RATE = 0.3;
public const DEFAULT_EXPONENTIAL_FACTOR = 1.15;
protected $fillable = [
'name',
'description',
'preset',
'growth_strategy',
'starting_volume',
'quota_type',
'daily_increment',
'exponential_factor',
'limit_type',
'limit_per_day_cap',
'limit_target_volume',
'limit_stop_after_days',
'send_on_weekends',
'enable_safety_checks',
'pause_on_negative_signals',
'max_bounce_rate',
'max_complaint_rate',
'status',
'sending_limit',
'sending_count',
'increment_percentage',
];
protected $casts = [
'starting_volume' => 'integer',
'daily_increment' => 'integer',
'exponential_factor' => 'float',
'limit_per_day_cap' => 'integer',
'limit_target_volume' => 'integer',
'limit_stop_after_days' => 'integer',
'send_on_weekends' => 'boolean',
'enable_safety_checks' => 'boolean',
'pause_on_negative_signals' => 'boolean',
'max_bounce_rate' => 'float',
'max_complaint_rate' => 'float',
'sending_limit' => 'integer',
'sending_count' => 'integer',
'increment_percentage' => 'integer',
];
public static function scopeSearch($query, $keyword)
{
// Keyword
if (!empty(trim($keyword))) {
foreach (explode(' ', trim($keyword)) as $keyword) {
$query = $query->where(function ($q) use ($keyword) {
$q->orwhere('warmup_strategies.name', 'like', '%'.$keyword.'%')
->orwhere('warmup_strategies.description', 'like', '%'.$keyword.'%')
->orwhere('warmup_strategies.preset', 'like', '%'.$keyword.'%');
});
}
}
return $query;
}
public static function newDefault()
{
$strategy = new self();
$strategy->status = self::STATUS_ACTIVE;
$strategy->preset = self::PRESET_BALANCED;
$strategy->growth_strategy = self::GROWTH_STRATEGY_LINEAR;
$strategy->quota_type = self::QUOTA_TYPE_DAILY;
$strategy->limit_type = self::LIMIT_TYPE_PER_DAY_CAP;
$strategy->starting_volume = 20;
$strategy->daily_increment = 15;
$strategy->exponential_factor = self::DEFAULT_EXPONENTIAL_FACTOR;
$strategy->limit_per_day_cap = 1000;
$strategy->limit_target_volume = 10000;
$strategy->limit_stop_after_days = 30;
$strategy->send_on_weekends = false;
$strategy->enable_safety_checks = true;
$strategy->pause_on_negative_signals = true;
$strategy->max_bounce_rate = self::DEFAULT_MAX_BOUNCE_RATE;
$strategy->max_complaint_rate = self::DEFAULT_MAX_COMPLAINT_RATE;
$strategy->increment_percentage = 15;
$strategy->sending_count = 30;
$strategy->sending_limit = 1000;
return $strategy;
}
public function disable()
{
$this->status = self::STATUS_INACTIVE;
$this->save();
}
public function enable()
{
$this->status = self::STATUS_ACTIVE;
$this->save();
}
public static function scopeActive($query)
{
return $query->where('warmup_strategies.status', '=', self::STATUS_ACTIVE);
}
public static function limitTypeOptions()
{
return [
['text' => trans('warmup.limit_types.per_day_cap'), 'value' => self::LIMIT_TYPE_PER_DAY_CAP],
['text' => trans('warmup.limit_types.target_volume'), 'value' => self::LIMIT_TYPE_TARGET_VOLUME],
['text' => trans('warmup.limit_types.stop_after_days'), 'value' => self::LIMIT_TYPE_STOP_AFTER_DAYS],
];
}
public static function strategyOptions()
{
return [
['text' => trans('warmup.growth_strategies.linear'), 'value' => self::GROWTH_STRATEGY_LINEAR],
['text' => trans('warmup.growth_strategies.exponential'), 'value' => self::GROWTH_STRATEGY_EXPONENTIAL],
];
}
public static function quotaTypeOptions()
{
return [
['text' => trans('warmup.quota_types.hourly'), 'value' => self::QUOTA_TYPE_HOURLY],
['text' => trans('warmup.quota_types.daily'), 'value' => self::QUOTA_TYPE_DAILY],
['text' => trans('warmup.quota_types.monthly'), 'value' => self::QUOTA_TYPE_MONTHLY],
];
}
public static function presetOptions()
{
return [
['text' => trans('warmup.presets.balanced'), 'value' => self::PRESET_BALANCED],
['text' => trans('warmup.presets.cautious'), 'value' => self::PRESET_CAUTIOUS],
['text' => trans('warmup.presets.aggressive'), 'value' => self::PRESET_AGGRESSIVE],
];
}
public static function presetValues(): array
{
return array_column(self::presetOptions(), 'value');
}
public static function growthStrategyValues(): array
{
return array_column(self::strategyOptions(), 'value');
}
public static function quotaTypeValues(): array
{
return array_column(self::quotaTypeOptions(), 'value');
}
public static function limitTypeValues(): array
{
return array_column(self::limitTypeOptions(), 'value');
}
public static function incrementFromExponentialFactor(float $factor): int
{
$normalizedFactor = max(1.01, $factor);
return max(1, (int) round(($normalizedFactor - 1) * 100));
}
public static function exponentialFactorFromIncrement(int|float $increment): float
{
return round(1 + (max(0, (float) $increment) / 100), 2);
}
public function getPlanDetails(): array
{
$calculator = app(WarmupStrategyFactory::class)->make($this);
$limitType = $this->limit_type ?: self::LIMIT_TYPE_PER_DAY_CAP;
$limitValue = $this->activeLimitValue();
$result = [];
$currentQuota = 0;
for ($step = 1; $step <= 365; $step++) {
$sendingOfStep = $calculator->calculateDailyQuota($step);
if ($limitType === self::LIMIT_TYPE_PER_DAY_CAP) {
$sendingOfStep = min($limitValue, $sendingOfStep);
}
if ($limitType === self::LIMIT_TYPE_TARGET_VOLUME) {
$remaining = max(0, $limitValue - $currentQuota);
if ($remaining < 1) {
break;
}
$sendingOfStep = min($sendingOfStep, $remaining);
}
$currentQuota += $sendingOfStep;
$result[] = [
'step' => $step,
'sendingOfStep' => (int) $sendingOfStep,
'quota' => (int) $currentQuota,
];
if ($limitType === self::LIMIT_TYPE_PER_DAY_CAP && $sendingOfStep >= $limitValue) {
break;
}
if ($limitType === self::LIMIT_TYPE_TARGET_VOLUME && $currentQuota >= $limitValue) {
break;
}
if ($limitType === self::LIMIT_TYPE_STOP_AFTER_DAYS && $step >= $limitValue) {
break;
}
}
return $result;
}
public function estimateWarmupStepsFromData(WarmupStrategyData $data): int
{
$previewStrategy = self::newDefault();
$previewStrategy->fill($data->toArray());
$previewStrategy->growth_strategy = $data->growthStrategy;
$previewStrategy->daily_increment = $data->growthStrategy === self::GROWTH_STRATEGY_EXPONENTIAL
? self::incrementFromExponentialFactor((float) ($data->exponentialFactor ?? self::DEFAULT_EXPONENTIAL_FACTOR))
: $data->dailyIncrement;
$previewStrategy->exponential_factor = $data->exponentialFactor ?? self::DEFAULT_EXPONENTIAL_FACTOR;
return count($previewStrategy->getPlanDetails());
}
public function estimatedFullWarmupDay(): int
{
$steps = max(1, count($this->getPlanDetails()));
if ($this->send_on_weekends) {
return $steps;
}
$calendarDays = 0;
$activeDays = 0;
while ($activeDays < $steps) {
$calendarDays++;
$dayOfWeek = (($calendarDays - 1) % 7) + 1;
if ($dayOfWeek <= 5) {
$activeDays++;
}
}
return $calendarDays;
}
public function riskLevel(): string
{
$score = 0;
$peakVolume = $this->projectedPeakVolume();
$activeLimitValue = $this->activeLimitValue();
if ($this->preset === self::PRESET_AGGRESSIVE) {
$score += 2;
}
if ($this->growth_strategy === self::GROWTH_STRATEGY_EXPONENTIAL) {
$score += 1;
}
if ((int) $this->daily_increment >= 20) {
$score += 1;
}
if ($peakVolume >= 2000) {
$score += 1;
}
if (($this->limit_type ?: self::LIMIT_TYPE_PER_DAY_CAP) === self::LIMIT_TYPE_TARGET_VOLUME && $activeLimitValue >= 10000) {
$score += 1;
}
if (($this->limit_type ?: self::LIMIT_TYPE_PER_DAY_CAP) === self::LIMIT_TYPE_STOP_AFTER_DAYS && $activeLimitValue >= 45) {
$score += 1;
}
if ($this->send_on_weekends) {
$score += 1;
}
return match (true) {
$score >= 4 => 'high',
$score >= 2 => 'medium',
default => 'low',
};
}
public function volumeForDisplayDay(int $day): int
{
$details = $this->getPlanDetails();
$index = max(0, min(count($details) - 1, $day - 1));
return (int) ($details[$index]['sendingOfStep'] ?? 0);
}
public function activeLimitValue(): int
{
$startingVolume = max(1, (int) ($this->starting_volume ?: 1));
$limitType = $this->limit_type ?: self::LIMIT_TYPE_PER_DAY_CAP;
return match ($limitType) {
self::LIMIT_TYPE_TARGET_VOLUME => max($startingVolume, (int) ($this->limit_target_volume ?: $this->sending_limit ?: $startingVolume)),
self::LIMIT_TYPE_STOP_AFTER_DAYS => max(1, (int) ($this->limit_stop_after_days ?: $this->sending_count ?: 1)),
default => max($startingVolume, (int) ($this->limit_per_day_cap ?: $this->sending_limit ?: $startingVolume)),
};
}
public function projectedPeakVolume(): int
{
return max(1, (int) collect($this->getPlanDetails())->max('sendingOfStep'));
}
public function projectedTotalVolume(): int
{
return max(1, (int) collect($this->getPlanDetails())->max('quota'));
}
}