296 lines
8.6 KiB
JavaScript
296 lines
8.6 KiB
JavaScript
const client = require("../graphql-client/graphql-client").client;
|
|
const {
|
|
UPDATE_MESSAGE_STATUS,
|
|
MARK_MESSAGES_AS_READ,
|
|
INSERT_PHONE_NUMBER_OPT_OUT,
|
|
FIND_BODYSHOP_BY_MESSAGING_SERVICE_SID
|
|
} = require("../graphql-client/queries");
|
|
const logger = require("../utils/logger");
|
|
const { phone } = require("phone");
|
|
|
|
// Local GraphQL for “mark unread” (kept here to avoid requiring edits to graphql-client/queries.js)
|
|
const GET_LAST_INBOUND_NON_SYSTEM_MESSAGE = `
|
|
query GetLastInboundNonSystemMessage($conversationId: uuid!) {
|
|
messages(
|
|
where: {
|
|
conversationid: { _eq: $conversationId }
|
|
isoutbound: { _eq: false }
|
|
is_system: { _eq: false }
|
|
}
|
|
order_by: { created_at: desc }
|
|
limit: 1
|
|
) {
|
|
id
|
|
created_at
|
|
read
|
|
}
|
|
}
|
|
`;
|
|
|
|
const MARK_LAST_INBOUND_MESSAGE_UNREAD_MAX_ONE = `
|
|
mutation MarkLastInboundMessageUnreadMaxOne($conversationId: uuid!, $lastId: uuid!) {
|
|
markOthersRead: update_messages(
|
|
_set: { read: true }
|
|
where: {
|
|
conversationid: { _eq: $conversationId }
|
|
isoutbound: { _eq: false }
|
|
is_system: { _eq: false }
|
|
id: { _neq: $lastId }
|
|
}
|
|
) {
|
|
affected_rows
|
|
returning { id }
|
|
}
|
|
|
|
markLastUnread: update_messages_by_pk(pk_columns: { id: $lastId }, _set: { read: false }) {
|
|
id
|
|
}
|
|
}
|
|
`;
|
|
|
|
/**
|
|
* Handle the status of an SMS message
|
|
* @param req
|
|
* @param res
|
|
* @returns {Promise<*>}
|
|
*/
|
|
const status = async (req, res) => {
|
|
const { SmsSid, SmsStatus, ErrorCode, To, MessagingServiceSid } = req.body;
|
|
const {
|
|
ioRedis,
|
|
ioHelpers: { getBodyshopRoom, getBodyshopConversationRoom }
|
|
} = req;
|
|
|
|
try {
|
|
// Ignore status 'queued'
|
|
if (SmsStatus === "queued") {
|
|
return res.status(200).json({ message: "Status 'queued' disregarded." });
|
|
}
|
|
|
|
// Handle ErrorCode 21610 (Attempt to send to unsubscribed recipient) first
|
|
if (ErrorCode === "21610" && To && MessagingServiceSid) {
|
|
try {
|
|
// Step 1: Find the bodyshop by MessagingServiceSid
|
|
const bodyshopResponse = await client.request(FIND_BODYSHOP_BY_MESSAGING_SERVICE_SID, {
|
|
mssid: MessagingServiceSid,
|
|
phone: phone(To).phoneNumber // Pass the normalized phone number as required
|
|
});
|
|
|
|
const bodyshop = bodyshopResponse.bodyshops[0];
|
|
if (!bodyshop) {
|
|
logger.log("sms-opt-out-error", "ERROR", "api", null, {
|
|
msid: SmsSid,
|
|
messagingServiceSid: MessagingServiceSid,
|
|
to: To,
|
|
error: "No matching bodyshop found"
|
|
});
|
|
} else {
|
|
// Step 2: Insert into phone_number_opt_out table
|
|
const now = new Date().toISOString();
|
|
const optOutInput = {
|
|
bodyshopid: bodyshop.id,
|
|
phone_number: phone(To).phoneNumber.replace(/^\+1/, ""), // Normalize phone number (remove +1 for CA numbers)
|
|
created_at: now,
|
|
updated_at: now
|
|
};
|
|
|
|
const optOutResponse = await client.request(INSERT_PHONE_NUMBER_OPT_OUT, {
|
|
optOutInput: [optOutInput]
|
|
});
|
|
|
|
logger.log("sms-opt-out-success", "INFO", null, null, {
|
|
msid: SmsSid,
|
|
bodyshopid: bodyshop.id,
|
|
phone_number: optOutInput.phone_number,
|
|
affected_rows: optOutResponse.insert_phone_number_opt_out.affected_rows
|
|
});
|
|
|
|
// Store bodyshopid for potential use in WebSocket notification
|
|
const broadcastRoom = getBodyshopRoom(bodyshop.id);
|
|
ioRedis.to(broadcastRoom).emit("phone-number-opted-out", {
|
|
bodyshopid: bodyshop.id,
|
|
phone_number: optOutInput.phone_number
|
|
// Note: conversationId is not included yet; will be set after message lookup
|
|
});
|
|
}
|
|
} catch (error) {
|
|
logger.log("sms-opt-out-error", "ERROR", "api", null, {
|
|
msid: SmsSid,
|
|
messagingServiceSid: MessagingServiceSid,
|
|
to: To,
|
|
error: error.message,
|
|
stack: error.stack
|
|
});
|
|
// Continue processing to update message status
|
|
}
|
|
}
|
|
|
|
// Update message status in the database
|
|
const response = await client.request(UPDATE_MESSAGE_STATUS, {
|
|
msid: SmsSid,
|
|
fields: { status: SmsStatus }
|
|
});
|
|
|
|
const message = response.update_messages?.returning?.[0];
|
|
|
|
if (message) {
|
|
logger.log("sms-status-update", "DEBUG", "api", null, {
|
|
msid: SmsSid,
|
|
status: SmsStatus
|
|
});
|
|
|
|
// Emit WebSocket event to notify the change in message status
|
|
const conversationRoom = getBodyshopConversationRoom({
|
|
bodyshopId: message.conversation.bodyshopid,
|
|
conversationId: message.conversationid
|
|
});
|
|
|
|
ioRedis.to(conversationRoom).emit("message-changed", {
|
|
...message,
|
|
status: SmsStatus,
|
|
type: "status-changed"
|
|
});
|
|
} else {
|
|
logger.log("sms-status-update-warning", "WARN", null, null, {
|
|
msid: SmsSid,
|
|
status: SmsStatus,
|
|
warning: "No message found in database for update"
|
|
});
|
|
}
|
|
|
|
res.sendStatus(200);
|
|
} catch (err) {
|
|
logger.log("sms-status-update-error", "ERROR", "api", null, {
|
|
msid: SmsSid,
|
|
status: SmsStatus,
|
|
error: err.message,
|
|
stack: err.stack
|
|
});
|
|
res.status(500).json({ error: "Failed to update message status." });
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Mark a conversation as read
|
|
* @param req
|
|
* @param res
|
|
* @returns {Promise<*>}
|
|
*/
|
|
const markConversationRead = async (req, res) => {
|
|
const {
|
|
ioRedis,
|
|
ioHelpers: { getBodyshopRoom }
|
|
} = req;
|
|
const { conversation, imexshopid, bodyshopid } = req.body;
|
|
|
|
// Alternatively, support both payload formats
|
|
const conversationId = conversation?.id || req.body.conversationId;
|
|
|
|
if (!conversationId || !imexshopid || !bodyshopid) {
|
|
return res.status(400).json({ error: "Invalid conversation data provided." });
|
|
}
|
|
|
|
try {
|
|
const response = await client.request(MARK_MESSAGES_AS_READ, {
|
|
conversationId
|
|
});
|
|
|
|
const updatedMessageIds = response.update_messages.returning.map((message) => message.id);
|
|
|
|
const broadcastRoom = getBodyshopRoom(bodyshopid);
|
|
|
|
ioRedis.to(broadcastRoom).emit("conversation-changed", {
|
|
type: "conversation-marked-read",
|
|
conversationId,
|
|
affectedMessages: response.update_messages.affected_rows,
|
|
messageIds: updatedMessageIds
|
|
});
|
|
|
|
res.status(200).json({
|
|
success: true,
|
|
message: "Conversation marked as read."
|
|
});
|
|
} catch (error) {
|
|
console.error("Error marking conversation as read:", error);
|
|
res.status(500).json({ error: "Failed to mark conversation as read." });
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Mark last inbound (customer) non-system message as unread.
|
|
* Enforces: max unread inbound non-system messages per thread = 1
|
|
*
|
|
* Body: { conversationId, imexshopid, bodyshopid }
|
|
*/
|
|
const markLastMessageUnread = async (req, res) => {
|
|
const {
|
|
ioRedis,
|
|
ioHelpers: { getBodyshopRoom }
|
|
} = req;
|
|
|
|
const { conversationId, imexshopid, bodyshopid } = req.body;
|
|
|
|
if (!conversationId || !imexshopid || !bodyshopid) {
|
|
return res.status(400).json({ error: "Invalid conversation data provided." });
|
|
}
|
|
|
|
try {
|
|
const lastResp = await client.request(GET_LAST_INBOUND_NON_SYSTEM_MESSAGE, { conversationId });
|
|
const last = lastResp?.messages?.[0];
|
|
|
|
if (!last?.id) {
|
|
// No inbound message to mark unread
|
|
const broadcastRoom = getBodyshopRoom(bodyshopid);
|
|
ioRedis.to(broadcastRoom).emit("conversation-changed", {
|
|
type: "conversation-marked-unread",
|
|
conversationId,
|
|
lastUnreadMessageId: null,
|
|
messageIdsMarkedRead: [],
|
|
unreadCount: 0
|
|
});
|
|
|
|
return res.status(200).json({
|
|
success: true,
|
|
conversationId,
|
|
lastUnreadMessageId: null,
|
|
messageIdsMarkedRead: [],
|
|
unreadCount: 0
|
|
});
|
|
}
|
|
|
|
const mutResp = await client.request(MARK_LAST_INBOUND_MESSAGE_UNREAD_MAX_ONE, {
|
|
conversationId,
|
|
lastId: last.id
|
|
});
|
|
|
|
const messageIdsMarkedRead = mutResp?.markOthersRead?.returning?.map((m) => m.id) || [];
|
|
|
|
const broadcastRoom = getBodyshopRoom(bodyshopid);
|
|
|
|
ioRedis.to(broadcastRoom).emit("conversation-changed", {
|
|
type: "conversation-marked-unread",
|
|
conversationId,
|
|
lastUnreadMessageId: last.id,
|
|
messageIdsMarkedRead,
|
|
unreadCount: 1
|
|
});
|
|
|
|
return res.status(200).json({
|
|
success: true,
|
|
conversationId,
|
|
lastUnreadMessageId: last.id,
|
|
messageIdsMarkedRead,
|
|
unreadCount: 1
|
|
});
|
|
} catch (error) {
|
|
console.error("Error marking last message unread:", error);
|
|
return res.status(500).json({ error: "Failed to mark last message unread." });
|
|
}
|
|
};
|
|
|
|
module.exports = {
|
|
status,
|
|
markConversationRead,
|
|
markLastMessageUnread
|
|
};
|