346 lines
15 KiB
PHP
346 lines
15 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
namespace Esun\CustomerMessage\Model;
|
|
|
|
use Magento\Framework\App\ResourceConnection;
|
|
use Magento\Framework\Exception\LocalizedException;
|
|
use Magento\Sales\Api\OrderRepositoryInterface;
|
|
|
|
class ConversationService
|
|
{
|
|
public function __construct(
|
|
private readonly ResourceConnection $resource,
|
|
private readonly OrderRepositoryInterface $orders,
|
|
private readonly AttachmentStorage $attachments
|
|
) {
|
|
}
|
|
|
|
public function getThreadsForCustomer(int $customerId, int $limit = 100): array
|
|
{
|
|
$connection = $this->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<int, int>
|
|
*/
|
|
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<int, int>
|
|
*/
|
|
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);
|
|
}
|
|
}
|