SerpCheckStatus::class, 'search_engine' => SearchEngine::class, 'device' => KeywordDevice::class, 'checked_at' => 'datetime', 'started_at' => 'datetime', 'completed_at' => 'datetime', 'failed_at' => 'datetime', 'provider_task_posted_at' => 'datetime', 'provider_last_polled_at' => 'datetime', 'provider_next_poll_at' => 'datetime', 'provider_cost' => 'decimal:6']; } public function uniqueIds(): array { return ['ulid']; } public function getRouteKeyName(): string { return 'ulid'; } public function keyword(): BelongsTo { return $this->belongsTo(Keyword::class); } public function results(): HasMany { return $this->hasMany(SerpResult::class); } public function begin(): bool { if ($this->status !== SerpCheckStatus::Pending) { return false; } return $this->forceFill(['status' => SerpCheckStatus::Running, 'active_slot' => 'active', 'started_at' => now()])->save(); } public function complete(array $data): bool { if ($this->status !== SerpCheckStatus::Running) { throw new LogicException('Invalid check transition.'); } return $this->forceFill($data + ['status' => SerpCheckStatus::Completed, 'active_slot' => null, 'checked_at' => now(), 'completed_at' => now(), 'failed_at' => null, 'error_code' => null, 'error_message' => null])->save(); } public function fail(string $code, string $message): bool { if (! in_array($this->status, [SerpCheckStatus::Pending, SerpCheckStatus::Running], true)) { return false; } return $this->forceFill(['status' => SerpCheckStatus::Failed, 'active_slot' => null, 'failed_at' => now(), 'error_code' => $code, 'error_message' => $message])->save(); } }