diff --git a/app/Console/Commands/SuperAdminProgramSettingsRefreshCommand.php b/app/Console/Commands/SuperAdminProgramSettingsRefreshCommand.php index 37fd494..772c262 100644 --- a/app/Console/Commands/SuperAdminProgramSettingsRefreshCommand.php +++ b/app/Console/Commands/SuperAdminProgramSettingsRefreshCommand.php @@ -10,7 +10,7 @@ class SuperAdminProgramSettingsRefreshCommand extends Command protected $signature = 'superadmin:refresh-program-settings {--clear-only : فقط کش last-good را پاک کن}'; - protected $description = 'کش overlay تنظیمات سوپرادمین را پاک/تازه می‌کند (مثلاً بعد از تغییر enable_automation)'; + protected $description = 'کش overlay تنظیمات سوپرادمین را پاک/تازه می‌کند'; public function handle(): int { @@ -30,11 +30,10 @@ class SuperAdminProgramSettingsRefreshCommand extends Command return self::FAILURE; } + $items = is_array($result['items'] ?? null) ? $result['items'] : []; $this->info('fetched '.(int) ($result['count'] ?? 0).' setting(s) from SuperAdmin'); - if (($result['enable_automation'] ?? null) !== null) { - $this->line('enable_automation='.$result['enable_automation']); - } else { - $this->warn('enable_automation missing in SuperAdmin response'); + foreach ($items as $key => $value) { + $this->line(" {$key}={$value}"); } return self::SUCCESS; diff --git a/app/Http/Controllers/Admin/AutomationController.php b/app/Http/Controllers/Admin/AutomationController.php index 3274197..4dd50f7 100644 --- a/app/Http/Controllers/Admin/AutomationController.php +++ b/app/Http/Controllers/Admin/AutomationController.php @@ -5,6 +5,7 @@ namespace App\Http\Controllers\Admin; use App\DB\Te\TableEngine; use App\Http\Controllers\Controller; use App\Models\Automation; +use App\Models\AutomationSend; use App\Services\Automation\AutomationCatalog; use App\Services\Automation\AutomationEngine; use App\Services\Automation\AutomationFingerprint; @@ -89,6 +90,49 @@ class AutomationController extends Controller return response()->json(true); } + /** اجرای فوری برای تست — بدون انتظار ساعت cron */ + public function runNow(Request $request, $id) + { + $this->assertFeatureEnabled(); + $row = Automation::query()->findOrFail($id); + $force = $request->boolean('force', true); + $stats = app(AutomationEngine::class)->runNow($row, $force); + + return response()->json([ + 'message' => sprintf( + 'مخاطب %d — ارسال موفق %d، ناموفق %d، ردشده %d', + $stats['audience'], + $stats['sent'], + $stats['failed'], + $stats['skipped'] + ), + ...$stats, + ]); + } + + public function sendsTable(Request $request, $id) + { + $this->assertFeatureEnabled(); + Automation::query()->findOrFail($id); + + $query = AutomationSend::query() + ->where('automation_id', (int) $id) + ->leftJoin('customers', 'customers.id', '=', 'automation_sends.customer_id') + ->select([ + 'automation_sends.*', + 'customers.name as customer_name', + 'customers.mobile as customer_mobile', + ]) + ->orderByDesc('automation_sends.id'); + + TableEngine::create($query, $request->all()) + ->initSearch(['customers.name', 'customers.mobile', 'automation_sends.status', 'automation_sends.error']) + ->select(['automation_sends.*', 'customers.name as customer_name', 'customers.mobile as customer_mobile']) + ->export($query); + + return $query; + } + private function assertFeatureEnabled(): void { if (! app(ConfigService::class)->isAutomationEnabled()) { diff --git a/app/Http/Controllers/Admin/SettingsController.php b/app/Http/Controllers/Admin/SettingsController.php index 007dfcc..4e2e119 100644 --- a/app/Http/Controllers/Admin/SettingsController.php +++ b/app/Http/Controllers/Admin/SettingsController.php @@ -80,7 +80,7 @@ class SettingsController extends Controller /** * پاک کردن کش overlay و خواندن دوباره از سوپرادمین - * (سوییچ‌هایی مثل enable_automation بعد از تغییر در پنل سوپرادمین). + * (همهٔ کلیدهای can_edit=0 مالک سوپرادمین — دسترسی مشتری به قابلیت‌ها). */ public function refreshSuperAdminOverlay() { @@ -91,14 +91,17 @@ class SettingsController extends Controller 'message' => 'ارتباط با سوپرادمین برقرار نشد؛ کش پاک شد و در درخواست بعدی دوباره تلاش می‌شود', 'ok' => false, 'count' => 0, + 'items' => [], ], 502); } + $count = (int) ($result['count'] ?? 0); + return response()->json([ - 'message' => 'تنظیمات پیشرفته از سوپرادمین همگام شد', + 'message' => "همگام شد ({$count} کلید دسترسی از سوپرادمین)", 'ok' => true, - 'count' => (int) ($result['count'] ?? 0), - 'enable_automation' => $result['enable_automation'] ?? null, + 'count' => $count, + 'items' => $result['items'] ?? [], 'source' => $result['source'] ?? 'superadmin', ]); } diff --git a/app/Services/Automation/AutomationEngine.php b/app/Services/Automation/AutomationEngine.php index 16ae220..f709783 100644 --- a/app/Services/Automation/AutomationEngine.php +++ b/app/Services/Automation/AutomationEngine.php @@ -9,6 +9,7 @@ use App\Models\Product; use App\Services\ConfigService; use Carbon\Carbon; use Illuminate\Support\Facades\Log; +use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException; class AutomationEngine { @@ -87,6 +88,53 @@ class AutomationEngine return $this->conditions->apply($this->conditions->baseQuery(), $conditions)->count(); } + /** + * اجرای فوری برای تست ادمین — بدون محدودیت ساعت، بدون touch کردن last_sms_run_at. + * + * @return array{audience: int, sent: int, failed: int, skipped: int} + */ + public function runNow(Automation $auto, bool $force = true): array + { + if (! app(ConfigService::class)->isAutomationEnabled()) { + throw new UnprocessableEntityHttpException('قابلیت اتوماسیون خاموش است.'); + } + if (! $auto->is_active) { + throw new UnprocessableEntityHttpException('اتوماسیون غیرفعال است.'); + } + if (! $this->inWindow($auto)) { + throw new UnprocessableEntityHttpException('اتوماسیون خارج از بازهٔ شروع/پایان است.'); + } + + $stats = ['audience' => 0, 'sent' => 0, 'failed' => 0, 'skipped' => 0]; + $audienceQuery = $this->conditions->apply( + $this->conditions->baseQuery(), + $auto->conditions ?? [] + ); + $stats['audience'] = (int) $audienceQuery->count(); + + foreach ($this->channels($auto) as $channel) { + $this->conditions + ->apply($this->conditions->baseQuery(), $auto->conditions ?? []) + ->select(['id', 'name', 'fname', 'lname', 'mobile', 'type', 'is_archive']) + ->chunkById(80, function ($customers) use ($auto, $channel, $force, &$stats) { + foreach ($customers as $customer) { + if (! $force && ! $this->sender->maySend($auto, $customer, $channel)) { + $stats['skipped']++; + + continue; + } + if ($this->sender->send($auto, $customer, $channel, $force)) { + $stats['sent']++; + } else { + $stats['failed']++; + } + } + }); + } + + return $stats; + } + private function tryPriceFire(Automation $auto, array $payload): void { if (! $this->inWindow($auto)) { diff --git a/app/Services/Automation/AutomationSender.php b/app/Services/Automation/AutomationSender.php index 79048a8..2b7f450 100644 --- a/app/Services/Automation/AutomationSender.php +++ b/app/Services/Automation/AutomationSender.php @@ -21,9 +21,9 @@ class AutomationSender ) { } - public function send(Automation $auto, Customer $customer, string $channel): bool + public function send(Automation $auto, Customer $customer, string $channel, bool $force = false): bool { - if (! $this->maySend($auto, $customer, $channel)) { + if (! $this->maySend($auto, $customer, $channel, $force)) { return false; } @@ -71,12 +71,16 @@ class AutomationSender } } - public function maySend(Automation $auto, Customer $customer, string $channel): bool + public function maySend(Automation $auto, Customer $customer, string $channel, bool $force = false): bool { if (! $auto->usesChannel($channel)) { return false; } + if ($force) { + return true; + } + $max = $auto->max_sends; if ($max !== null && (int) $max > 0) { $count = AutomationSend::query() diff --git a/app/Services/SuperAdminProgramSettingsOverlay.php b/app/Services/SuperAdminProgramSettingsOverlay.php index ffebdcd..32062ad 100644 --- a/app/Services/SuperAdminProgramSettingsOverlay.php +++ b/app/Services/SuperAdminProgramSettingsOverlay.php @@ -86,7 +86,7 @@ class SuperAdminProgramSettingsOverlay /** * کش را پاک می‌کند و دوباره از سوپرادمین می‌خواند. * - * @return array{ok: bool, count: int, enable_automation: string|null, source: string} + * @return array{ok: bool, count: int, items: array, source: string} */ public function refreshFromSuperAdmin(): array { @@ -96,17 +96,27 @@ class SuperAdminProgramSettingsOverlay return [ 'ok' => false, 'count' => 0, - 'enable_automation' => null, + 'items' => [], 'source' => 'unavailable', ]; } - $auto = $map['enable_automation']['value'] ?? null; + $items = []; + foreach ($map as $key => $row) { + if (! is_array($row)) { + continue; + } + $value = $row['value'] ?? null; + if ($value === null || $value === '') { + continue; + } + $items[(string) $key] = (string) $value; + } return [ 'ok' => true, - 'count' => count($map), - 'enable_automation' => $auto === null || $auto === '' ? null : (string) $auto, + 'count' => count($items), + 'items' => $items, 'source' => 'superadmin', ]; } diff --git a/routes/admin.php b/routes/admin.php index e3a38c1..bcb758a 100644 --- a/routes/admin.php +++ b/routes/admin.php @@ -71,6 +71,8 @@ Route::middleware('auth:user')->group(function (){ Route::post('automations', [AdminAutomationController::class, 'store']); Route::put('automations/{id}', [AdminAutomationController::class, 'update']); Route::put('automations/{id}/toggle', [AdminAutomationController::class, 'toggle']); + Route::post('automations/{id}/run-now', [AdminAutomationController::class, 'runNow']); + Route::post('automations/{id}/sends/table', [AdminAutomationController::class, 'sendsTable']); Route::delete('automations/{id}', [AdminAutomationController::class, 'destroy']); //------------ConditionalOrder Route::post('conditional_orders/table', [\App\Http\Controllers\Admin\ConditionalOrderController::class, 'table']);