all()) ->initSearch(['name', 'code', 'title', 'body']) ->select(['*']) ->export($query); return $query; } public function catalog() { return response()->json(app(AutomationCatalog::class)->toArray()); } public function preview(Request $request) { $conditions = array_values((array) $request->input('conditions', [])); $count = app(AutomationEngine::class)->previewCount($conditions); return response()->json(['count' => $count]); } public function store(Request $request) { $data = $this->validated($request); $data['created_by'] = auth()->id(); $row = Automation::create($data); return response()->json($row); } public function update(Request $request, $id) { $row = Automation::query()->findOrFail($id); $data = $this->validated($request, (int) $row->id); $row->fill($data)->save(); return response()->json($row); } public function toggle($id) { $row = Automation::query()->findOrFail($id); if (! $row->is_active) { $dup = $this->duplicateOf($row->logic_fingerprint, (int) $row->id, true); if ($dup) { throw new UnprocessableEntityHttpException( 'اتوماسیون مشابه فعال است: '.$dup->name ); } } $row->is_active = ! $row->is_active; $row->save(); return response()->json(['is_active' => $row->is_active]); } public function destroy($id) { Automation::query()->findOrFail($id)->delete(); return response()->json(true); } private function validated(Request $request, ?int $ignoreId = null): array { $name = trim((string) $request->input('name', '')); $title = trim((string) $request->input('title', '')); $body = trim((string) $request->input('body', '')); if ($name === '' || $title === '' || $body === '') { throw new UnprocessableEntityHttpException('نام، عنوان و متن الزامی است.'); } $triggerType = (string) $request->input('trigger_type', AutomationCatalog::TRIGGER_SCHEDULE); if (! in_array($triggerType, [ AutomationCatalog::TRIGGER_SCHEDULE, AutomationCatalog::TRIGGER_EVENT, AutomationCatalog::TRIGGER_PRICE, ], true)) { throw new UnprocessableEntityHttpException('نوع تریگر نامعتبر است.'); } $channels = array_values(array_intersect( (array) $request->input('channels', []), [AutomationCatalog::CHANNEL_SMS, AutomationCatalog::CHANNEL_IN_APP] )); if ($channels === []) { throw new UnprocessableEntityHttpException('حداقل یک کانال انتخاب کنید.'); } $smsHour = $request->input('sms_send_hour'); $inAppHour = $request->input('in_app_send_hour'); if (in_array(AutomationCatalog::CHANNEL_SMS, $channels, true) && $smsHour === null) { throw new UnprocessableEntityHttpException('ساعت ارسال پیامک را مشخص کنید.'); } if (in_array(AutomationCatalog::CHANNEL_IN_APP, $channels, true) && $inAppHour === null) { throw new UnprocessableEntityHttpException('ساعت ارسال اعلان را مشخص کنید.'); } $conditions = array_values(array_filter( (array) $request->input('conditions', []), fn ($row) => is_array($row) && filled($row['field'] ?? null) )); $catalog = app(AutomationCatalog::class); $allowed = $catalog->fieldKeys(); foreach ($conditions as $row) { if (! in_array($row['field'] ?? '', $allowed, true)) { throw new UnprocessableEntityHttpException('شرط نامعتبر است.'); } } $triggerConfig = (array) $request->input('trigger_config', []); if ($triggerType === AutomationCatalog::TRIGGER_SCHEDULE) { $triggerConfig = []; } elseif ($triggerType === AutomationCatalog::TRIGGER_EVENT) { $eventKeys = array_column(AutomationCatalog::EVENTS, 'key'); if (! in_array((string) ($triggerConfig['event'] ?? ''), $eventKeys, true)) { throw new UnprocessableEntityHttpException('رویداد را انتخاب کنید.'); } $triggerConfig = [ 'event' => (string) $triggerConfig['event'], 'delay_hours' => max(0, (int) ($triggerConfig['delay_hours'] ?? 0)), ]; } elseif ($triggerType === AutomationCatalog::TRIGGER_PRICE) { $step = (float) str_replace(',', '', (string) ($triggerConfig['step_toman'] ?? 0)); if ($step <= 0) { throw new UnprocessableEntityHttpException('رقم تغییر قیمت باید بزرگ‌تر از صفر باشد.'); } $dir = (string) ($triggerConfig['direction'] ?? 'both'); if (! in_array($dir, ['up', 'down', 'both'], true)) { $dir = 'both'; } $triggerConfig = [ 'step_toman' => $step, 'direction' => $dir, 'product_id' => (int) ($triggerConfig['product_id'] ?? 0) ?: null, ]; } $fingerprint = app(AutomationFingerprint::class)->make($triggerType, $triggerConfig, $conditions); $dup = $this->duplicateOf($fingerprint, $ignoreId, false); if ($dup) { throw new UnprocessableEntityHttpException( 'اتوماسیون شبیه وجود دارد: '.$dup->name.' — همان را ویرایش کنید.' ); } $starts = $request->input('starts_at') ?: null; $ends = $request->input('ends_at') ?: null; return [ 'name' => $name, 'code' => $this->uniqueCode($name, $ignoreId), 'is_active' => (bool) $request->input('is_active', true), 'starts_at' => $starts, 'ends_at' => $ends, 'sms_send_hour' => in_array(AutomationCatalog::CHANNEL_SMS, $channels, true) ? (int) $smsHour : null, 'in_app_send_hour' => in_array(AutomationCatalog::CHANNEL_IN_APP, $channels, true) ? (int) $inAppHour : null, 'trigger_type' => $triggerType, 'trigger_config' => $triggerConfig, 'conditions' => $conditions, 'channels' => $channels, 'title' => mb_substr($title, 0, 191), 'body' => $body, 'cooldown_hours' => max(0, (int) $request->input('cooldown_hours', 24)), 'max_sends' => $request->filled('max_sends') ? max(1, (int) $request->input('max_sends')) : null, 'logic_fingerprint' => $fingerprint, ]; } private function duplicateOf(string $fingerprint, ?int $ignoreId, bool $activeOnly): ?Automation { $q = Automation::query()->where('logic_fingerprint', $fingerprint); if ($ignoreId) { $q->where('id', '!=', $ignoreId); } if ($activeOnly) { $q->where('is_active', true); } return $q->first(); } private function uniqueCode(string $name, ?int $ignoreId): string { $base = Str::slug($name); if ($base === '') { $base = 'auto'; } $code = $base; $i = 2; while ( Automation::query() ->where('code', $code) ->when($ignoreId, fn ($q) => $q->where('id', '!=', $ignoreId)) ->exists() ) { $code = $base.'-'.$i; $i++; } return mb_substr($code, 0, 64); } }