84 lines
3.6 KiB
PHP
84 lines
3.6 KiB
PHP
<?php
|
|
|
|
namespace App\Services\MailFlow;
|
|
|
|
use App\Models\Setting;
|
|
use App\Models\User;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
class AutomaticMailPolicy
|
|
{
|
|
// One atomic record; read directly so long-running workers never use a stale tier.
|
|
public static function state(): ?array
|
|
{
|
|
try {
|
|
$state = Setting::where('name', 'automatic_mail_policy')->first()?->value;
|
|
if ($state === null) { return ['level' => 1, 'since' => null]; }
|
|
if (!is_array($state) || !in_array($state['level'] ?? null, [1, 2, 3], true)) { return null; }
|
|
return $state;
|
|
} catch (\Throwable $e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public static function save(int $level): void
|
|
{
|
|
if (!in_array($level, [1, 2, 3], true)) { throw new \InvalidArgumentException('Invalid automatic mail level'); }
|
|
Cache::lock('automatic-mail-policy:save', 10)->block(5, function () use ($level) {
|
|
DB::transaction(function () use ($level) {
|
|
$previous = self::state();
|
|
if ($previous === null) { throw new \RuntimeException('automatic_mail_policy_unavailable'); }
|
|
Setting::createOrUpdate('automatic_mail_policy', [
|
|
'level' => $level,
|
|
'since' => $level === 2 ? (($previous['level'] === 2 ? $previous['since'] : null) ?? now()->timestamp) : null,
|
|
]);
|
|
});
|
|
});
|
|
Cache::store('redis')->forget(\App\Support\Setting::CACHE_KEY);
|
|
}
|
|
|
|
public static function purpose(array $params): string
|
|
{
|
|
// Compatibility for older queued jobs. Never infer notify: it also serves replies/tests.
|
|
return $params['purpose'] ?? match ($params['template_name'] ?? '') {
|
|
'verify' => 'verification', 'mailLogin' => 'login_link',
|
|
'remindTraffic' => 'traffic_reminder', 'remindExpire' => 'expiry_reminder',
|
|
default => 'other',
|
|
};
|
|
}
|
|
|
|
public static function decision(string $purpose, array $context = []): ?array
|
|
{
|
|
if (!in_array($purpose, ['traffic_reminder', 'expiry_reminder', 'automatic_broadcast', 'service_expired'], true)) {
|
|
return null; // Includes manual broadcast, all responses and explicitly classified admin tests.
|
|
}
|
|
$state = self::state();
|
|
if ($state === null) {
|
|
return ['status' => 'pending', 'error' => 'automatic_mail_policy_unavailable', 'retryable' => true];
|
|
}
|
|
$allowed = $purpose === 'service_expired' ? $state['level'] === 2 : $state['level'] === 1;
|
|
if (!$allowed) { return self::blocked('automatic_mail_level_blocked'); }
|
|
if ($purpose === 'service_expired') {
|
|
$due = (int) ($context['subscription_expired_at'] ?? 0);
|
|
try {
|
|
$user = User::find($context['user_id'] ?? 0);
|
|
$reminders = Setting::where('name', 'remind_mail_enable')->first()?->value;
|
|
} catch (\Throwable $e) {
|
|
return ['status' => 'pending', 'error' => 'subscription_expiry_check_unavailable', 'retryable' => true];
|
|
}
|
|
if (!$state['since'] || $due <= $state['since'] || $due > now()->timestamp
|
|
|| !$reminders || !$user || !$user->remind_expire || $user->banned || !$user->plan_id
|
|
|| (int) $user->expired_at !== $due) {
|
|
return self::blocked('subscription_expiry_no_longer_valid');
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private static function blocked(string $error): array
|
|
{
|
|
return ['status' => 'policy_blocked', 'error' => $error, 'retryable' => false];
|
|
}
|
|
}
|