resource->getConnection(); return $connection->fetchAll( $connection->select()->from(['thread' => $this->table('esun_customer_message_thread')]) ->joinLeft(['entry' => $this->table('esun_customer_message_entry')], 'entry.entry_id=thread.last_message_id', ['last_content' => 'content', 'last_sender_type' => 'sender_type']) ->joinLeft(['orders' => $this->table('sales_order')], 'orders.entity_id=thread.order_id', ['order_increment_id' => 'increment_id']) ->where('thread.customer_id=?', $customerId) ->order('thread.last_message_at DESC') ->limit(max(1, min(100, $limit))) ); } public function getCustomerUnreadTotal(int $customerId): int { $connection = $this->resource->getConnection(); return (int)$connection->fetchOne( $connection->select()->from($this->table('esun_customer_message_thread'), ['total' => new \Zend_Db_Expr('COALESCE(SUM(customer_unread_count), 0)')]) ->where('customer_id=?', $customerId) ); } /** * Return unread message totals keyed by order entity ID. * * @param int[] $orderIds * @return array */ public function getCustomerUnreadByOrderIds(int $customerId, array $orderIds): array { $orderIds = array_values(array_unique(array_filter(array_map('intval', $orderIds)))); if ($customerId <= 0 || !$orderIds) { return []; } $connection = $this->resource->getConnection(); $rows = $connection->fetchPairs( $connection->select() ->from( $this->table('esun_customer_message_thread'), ['order_id', 'customer_unread_count'] ) ->where('customer_id=?', $customerId) ->where('order_id IN (?)', $orderIds) ->where('customer_unread_count > ?', 0) ); $counts = []; foreach ($rows as $orderId => $count) { $counts[(int)$orderId] = (int)$count; } return $counts; } /** * Return administrator unread message totals keyed by order entity ID. * * @param int[] $orderIds * @return array */ public function getAdminUnreadByOrderIds(array $orderIds): array { $orderIds = array_values(array_unique(array_filter(array_map('intval', $orderIds)))); if (!$orderIds) { return []; } $connection = $this->resource->getConnection(); $rows = $connection->fetchPairs( $connection->select() ->from( $this->table('esun_customer_message_thread'), ['order_id', 'admin_unread_count'] ) ->where('order_id IN (?)', $orderIds) ->where('admin_unread_count > ?', 0) ); $counts = []; foreach ($rows as $orderId => $count) { $counts[(int)$orderId] = (int)$count; } return $counts; } public function getThreadsForAdmin(int $limit = 200): array { $connection = $this->resource->getConnection(); return $connection->fetchAll( $connection->select()->from(['thread' => $this->table('esun_customer_message_thread')]) ->joinInner(['customer' => $this->table('customer_entity')], 'customer.entity_id=thread.customer_id', ['customer_email' => 'email', 'customer_firstname' => 'firstname', 'customer_lastname' => 'lastname']) ->joinLeft(['entry' => $this->table('esun_customer_message_entry')], 'entry.entry_id=thread.last_message_id', ['last_content' => 'content']) ->joinLeft(['orders' => $this->table('sales_order')], 'orders.entity_id=thread.order_id', ['order_increment_id' => 'increment_id']) ->order('thread.admin_unread_count DESC') ->order('thread.last_message_at DESC') ->limit(max(1, min(500, $limit))) ); } public function getThreadForCustomer(int $threadId, int $customerId): ?array { return $this->fetchThread(['thread.thread_id=?' => $threadId, 'thread.customer_id=?' => $customerId]); } public function getThreadForAdmin(int $threadId): ?array { return $this->fetchThread(['thread.thread_id=?' => $threadId]); } public function getOrderThreadForCustomer(int $orderId, int $customerId): ?array { return $this->fetchThread(['thread.order_id=?' => $orderId, 'thread.customer_id=?' => $customerId]); } public function getOrderThreadForAdmin(int $orderId): ?array { return $this->fetchThread(['thread.order_id=?' => $orderId]); } public function getMessages(int $threadId): array { $connection = $this->resource->getConnection(); $messages = $connection->fetchAll( $connection->select()->from($this->table('esun_customer_message_entry')) ->where('thread_id=?', $threadId)->order('entry_id ASC') ); if (!$messages) { return []; } $entryIds = array_column($messages, 'entry_id'); $attachmentRows = $connection->fetchAll( $connection->select()->from($this->table('esun_customer_message_attachment')) ->where('entry_id IN (?)', $entryIds)->order('attachment_id ASC') ); $attachments = []; foreach ($attachmentRows as $attachment) { $attachments[(int)$attachment['entry_id']][] = $attachment; } foreach ($messages as &$message) { $message['attachments'] = $attachments[(int)$message['entry_id']] ?? []; } unset($message); return $messages; } public function sendAsCustomer(int $customerId, int $storeId, string $content, array $files, int $threadId = 0, int $orderId = 0, string $subject = ''): int { $content = mb_substr(trim($content), 0, 10000); if ($content === '' && !$this->attachments->hasUploads($files)) { throw new LocalizedException(__('Please enter a message or upload an image.')); } $createdThread = false; if ($threadId) { $thread = $this->getThreadForCustomer($threadId, $customerId); if (!$thread) { throw new LocalizedException(__('The requested conversation is not available.')); } } elseif ($orderId) { $order = $this->orders->get($orderId); if ((int)$order->getCustomerId() !== $customerId) { throw new LocalizedException(__('The requested order is not available.')); } $thread = $this->getOrderThreadForCustomer($orderId, $customerId); if (!$thread) { $threadId = $this->createThread('order', $customerId, $storeId, (string)__('Order #%1', $order->getIncrementId()), $orderId); $createdThread = true; } else { $threadId = (int)$thread['thread_id']; } } else { $subject = mb_substr(trim($subject), 0, 255); if ($subject === '') { throw new LocalizedException(__('Please enter a subject.')); } $threadId = $this->createThread('general', $customerId, $storeId, $subject, null); $createdThread = true; } try { return $this->append($threadId, 'customer', $customerId, $content, $files); } catch (\Throwable $exception) { if ($createdThread) { $this->resource->getConnection()->delete($this->table('esun_customer_message_thread'), ['thread_id=?' => $threadId]); } throw $exception; } } public function sendAsAdmin(int $adminId, string $content, array $files, int $threadId = 0, int $orderId = 0): int { $content = mb_substr(trim($content), 0, 10000); if ($content === '' && !$this->attachments->hasUploads($files)) { throw new LocalizedException(__('Please enter a message or upload an image.')); } $createdThread = false; if ($threadId) { $thread = $this->getThreadForAdmin($threadId); if (!$thread) { throw new LocalizedException(__('The requested conversation is not available.')); } } elseif ($orderId) { $order = $this->orders->get($orderId); if (!(int)$order->getCustomerId()) { throw new LocalizedException(__('This order is not associated with a customer account.')); } $thread = $this->getOrderThreadForAdmin($orderId); if ($thread) { $threadId = (int)$thread['thread_id']; } else { $threadId = $this->createThread('order', (int)$order->getCustomerId(), (int)$order->getStoreId(), (string)__('Order #%1', $order->getIncrementId()), $orderId); $createdThread = true; } } else { throw new LocalizedException(__('An order or conversation is required.')); } try { return $this->append($threadId, 'admin', $adminId, $content, $files); } catch (\Throwable $exception) { if ($createdThread) { $this->resource->getConnection()->delete($this->table('esun_customer_message_thread'), ['thread_id=?' => $threadId]); } throw $exception; } } public function markCustomerRead(int $threadId, int $customerId): void { $this->resource->getConnection()->update($this->table('esun_customer_message_thread'), ['customer_unread_count' => 0], ['thread_id=?' => $threadId, 'customer_id=?' => $customerId]); } public function markAdminRead(int $threadId): void { $this->resource->getConnection()->update($this->table('esun_customer_message_thread'), ['admin_unread_count' => 0], ['thread_id=?' => $threadId]); } public function getAttachmentForCustomer(int $attachmentId, int $customerId): ?array { return $this->fetchAttachment($attachmentId, $customerId); } public function getAttachmentForAdmin(int $attachmentId): ?array { return $this->fetchAttachment($attachmentId, null); } private function createThread(string $type, int $customerId, int $storeId, string $subject, ?int $orderId): int { $connection = $this->resource->getConnection(); $connection->insert($this->table('esun_customer_message_thread'), [ 'thread_type' => $type, 'customer_id' => $customerId, 'order_id' => $orderId, 'store_id' => $storeId, 'subject' => (string)$subject, ]); return (int)$connection->lastInsertId(); } private function append(int $threadId, string $senderType, int $senderId, string $content, array $files): int { $connection = $this->resource->getConnection(); $savedPaths = []; $connection->beginTransaction(); try { $connection->insert($this->table('esun_customer_message_entry'), [ 'thread_id' => $threadId, 'sender_type' => $senderType, 'sender_id' => $senderId, 'content' => $content, ]); $entryId = (int)$connection->lastInsertId(); $saved = $this->attachments->saveUploads($files, $threadId, $entryId); $savedPaths = array_column($saved, 'storage_path'); foreach ($saved as $attachment) { $connection->insert($this->table('esun_customer_message_attachment'), ['entry_id' => $entryId] + $attachment); } $now = gmdate('Y-m-d H:i:s'); $data = [ 'last_message_id' => $entryId, 'last_message_at' => $now, 'status' => $senderType === 'customer' ? 'awaiting_admin' : 'awaiting_customer', ]; $data[$senderType === 'customer' ? 'admin_unread_count' : 'customer_unread_count'] = new \Zend_Db_Expr(($senderType === 'customer' ? 'admin_unread_count' : 'customer_unread_count') . '+1'); $data[$senderType === 'customer' ? 'customer_unread_count' : 'admin_unread_count'] = 0; $connection->update($this->table('esun_customer_message_thread'), $data, ['thread_id=?' => $threadId]); $connection->commit(); return $threadId; } catch (\Throwable $exception) { $connection->rollBack(); $this->attachments->delete($savedPaths); throw $exception; } } private function fetchThread(array $conditions): ?array { $connection = $this->resource->getConnection(); $select = $connection->select()->from(['thread' => $this->table('esun_customer_message_thread')]) ->joinInner(['customer' => $this->table('customer_entity')], 'customer.entity_id=thread.customer_id', ['customer_email' => 'email', 'customer_firstname' => 'firstname', 'customer_lastname' => 'lastname']) ->joinLeft(['orders' => $this->table('sales_order')], 'orders.entity_id=thread.order_id', ['order_increment_id' => 'increment_id']); foreach ($conditions as $condition => $value) { $select->where($condition, $value); } $row = $connection->fetchRow($select); return $row ?: null; } private function fetchAttachment(int $attachmentId, ?int $customerId): ?array { $connection = $this->resource->getConnection(); $select = $connection->select()->from(['attachment' => $this->table('esun_customer_message_attachment')]) ->joinInner(['entry' => $this->table('esun_customer_message_entry')], 'entry.entry_id=attachment.entry_id', []) ->joinInner(['thread' => $this->table('esun_customer_message_thread')], 'thread.thread_id=entry.thread_id', ['thread_id', 'customer_id']); $select->where('attachment.attachment_id=?', $attachmentId); if ($customerId !== null) { $select->where('thread.customer_id=?', $customerId); } $row = $connection->fetchRow($select); return $row ?: null; } private function table(string $name): string { return $this->resource->getTableName($name); } }