db = Database::connection(); } public function create(array $data): string { do { $ticket = 'GTS-' . date('Ymd') . '-' . strtoupper(bin2hex(random_bytes(3))); $check = $this->db->prepare('SELECT COUNT(*) FROM inquiries WHERE ticket = :ticket'); $check->execute(['ticket' => $ticket]); } while ((int) $check->fetchColumn() > 0); $statement = $this->db->prepare( "INSERT INTO inquiries (ticket, name, phone, email, inquiry_type, title, body, password_hash, status, consent_at) VALUES (:ticket, :name, :phone, :email, :inquiry_type, :title, :body, :password_hash, 'pending', NOW())" ); $statement->execute([ 'ticket' => $ticket, 'name' => $data['name'], 'phone' => $data['phone'], 'email' => $data['email'] ?: null, 'inquiry_type' => $data['type'], 'title' => $data['title'], 'body' => $data['body'], 'password_hash' => password_hash($data['password'], PASSWORD_DEFAULT), ]); return $ticket; } public function findForLookup(string $ticket): ?array { $statement = $this->db->prepare( 'SELECT id, ticket, inquiry_type, title, body, status, reply, replied_at, created_at, password_hash, lookup_failed_attempts, lookup_locked_until FROM inquiries WHERE ticket = :ticket AND deleted_at IS NULL LIMIT 1' ); $statement->execute(['ticket' => strtoupper($ticket)]); $inquiry = $statement->fetch(); return $inquiry ?: null; } public function resetLookupLimit(int $id): void { $statement = $this->db->prepare( 'UPDATE inquiries SET lookup_failed_attempts = 0, lookup_locked_until = NULL WHERE id = :id AND deleted_at IS NULL' ); $statement->execute(['id' => $id]); } public function recordLookupFailure(int $id): bool { $this->db->beginTransaction(); try { $select = $this->db->prepare( 'SELECT lookup_failed_attempts, lookup_locked_until FROM inquiries WHERE id = :id AND deleted_at IS NULL FOR UPDATE' ); $select->execute(['id' => $id]); $limit = $select->fetch(); if (!$limit) { $this->db->rollBack(); return false; } $attempts = (int) $limit['lookup_failed_attempts']; $lockedUntil = $limit['lookup_locked_until']; if (is_string($lockedUntil) && strtotime($lockedUntil) <= time()) { $attempts = 0; } $attempts++; $locked = $attempts >= 5; $update = $this->db->prepare( 'UPDATE inquiries SET lookup_failed_attempts = :attempts, lookup_locked_until = CASE WHEN :locked = 1 THEN DATE_ADD(NOW(), INTERVAL 15 MINUTE) ELSE NULL END WHERE id = :id' ); $update->execute([ 'attempts' => $attempts, 'locked' => $locked ? 1 : 0, 'id' => $id, ]); $this->db->commit(); return $locked; } catch (\Throwable $error) { if ($this->db->inTransaction()) { $this->db->rollBack(); } throw $error; } } }