-
+
Upgrade now
diff --git a/config/locales/am.yml b/config/locales/am.yml
index 3c756c85f..c773e3081 100644
--- a/config/locales/am.yml
+++ b/config/locales/am.yml
@@ -172,6 +172,8 @@ am:
sla:
added: '%{user_name} added SLA policy %{sla_name}'
removed: '%{user_name} removed SLA policy %{sla_name}'
+ csat:
+ not_sent_due_to_messaging_window: 'CSAT survey not sent due to outgoing message restrictions'
muted: '%{user_name} has muted the conversation'
unmuted: '%{user_name} has unmuted the conversation'
auto_resolution_message: 'Resolving the conversation as it has been inactive for a while. Please start a new conversation if you need further assistance.'
@@ -272,6 +274,7 @@ am:
made_with: Made with
header:
go_to_homepage: Website
+ visit_website: Visit website
appearance:
system: System
light: Light
diff --git a/config/locales/ar.yml b/config/locales/ar.yml
index 7c587b0a3..5a6463359 100644
--- a/config/locales/ar.yml
+++ b/config/locales/ar.yml
@@ -172,6 +172,8 @@ ar:
sla:
added: '%{user_name} أضاف سياسة مستوى الخدمة %{sla_name}'
removed: '%{user_name} أزال سياسة مستوى الخدمة %{sla_name}'
+ csat:
+ not_sent_due_to_messaging_window: 'CSAT survey not sent due to outgoing message restrictions'
muted: '%{user_name} كتم صوت المحادثة'
unmuted: '%{user_name} قام بإلغاء كتم المحادثة'
auto_resolution_message: 'Resolving the conversation as it has been inactive for a while. Please start a new conversation if you need further assistance.'
@@ -272,6 +274,7 @@ ar:
made_with: صنع بـ
header:
go_to_homepage: الموقع الإلكتروني
+ visit_website: Visit website
appearance:
system: النظام
light: فاتح
diff --git a/config/locales/az.yml b/config/locales/az.yml
index cc80814e1..c9d2c1ac9 100644
--- a/config/locales/az.yml
+++ b/config/locales/az.yml
@@ -172,6 +172,8 @@ az:
sla:
added: '%{user_name} added SLA policy %{sla_name}'
removed: '%{user_name} removed SLA policy %{sla_name}'
+ csat:
+ not_sent_due_to_messaging_window: 'CSAT survey not sent due to outgoing message restrictions'
muted: '%{user_name} has muted the conversation'
unmuted: '%{user_name} has unmuted the conversation'
auto_resolution_message: 'Resolving the conversation as it has been inactive for a while. Please start a new conversation if you need further assistance.'
@@ -272,6 +274,7 @@ az:
made_with: Made with
header:
go_to_homepage: Website
+ visit_website: Visit website
appearance:
system: System
light: Light
diff --git a/config/locales/bg.yml b/config/locales/bg.yml
index 88dd447b2..2ab74952f 100644
--- a/config/locales/bg.yml
+++ b/config/locales/bg.yml
@@ -172,6 +172,8 @@ bg:
sla:
added: '%{user_name} added SLA policy %{sla_name}'
removed: '%{user_name} removed SLA policy %{sla_name}'
+ csat:
+ not_sent_due_to_messaging_window: 'CSAT survey not sent due to outgoing message restrictions'
muted: '%{user_name} has muted the conversation'
unmuted: '%{user_name} has unmuted the conversation'
auto_resolution_message: 'Resolving the conversation as it has been inactive for a while. Please start a new conversation if you need further assistance.'
@@ -272,6 +274,7 @@ bg:
made_with: Made with
header:
go_to_homepage: Website
+ visit_website: Visit website
appearance:
system: System
light: Light
diff --git a/config/locales/ca.yml b/config/locales/ca.yml
index eb2f76d89..7daabbd99 100644
--- a/config/locales/ca.yml
+++ b/config/locales/ca.yml
@@ -172,6 +172,8 @@ ca:
sla:
added: '%{user_name} ha afegit la política de SLA %{sla_name}'
removed: '%{user_name} ha eliminat la política de SLA %{sla_name}'
+ csat:
+ not_sent_due_to_messaging_window: 'CSAT survey not sent due to outgoing message restrictions'
muted: '%{user_name} ha silenciat la conversa'
unmuted: '%{user_name} ha desactivat el silenci de la conversa'
auto_resolution_message: 'Resolving the conversation as it has been inactive for a while. Please start a new conversation if you need further assistance.'
@@ -272,6 +274,7 @@ ca:
made_with: Fet amb
header:
go_to_homepage: Lloc web
+ visit_website: Visit website
appearance:
system: Sistema
light: Clar
diff --git a/config/locales/cs.yml b/config/locales/cs.yml
index caceea531..75a840336 100644
--- a/config/locales/cs.yml
+++ b/config/locales/cs.yml
@@ -172,6 +172,8 @@ cs:
sla:
added: '%{user_name} added SLA policy %{sla_name}'
removed: '%{user_name} removed SLA policy %{sla_name}'
+ csat:
+ not_sent_due_to_messaging_window: 'CSAT survey not sent due to outgoing message restrictions'
muted: '%{user_name} ztlumil/a konverzaci'
unmuted: '%{user_name} has unmuted the conversation'
auto_resolution_message: 'Resolving the conversation as it has been inactive for a while. Please start a new conversation if you need further assistance.'
@@ -272,6 +274,7 @@ cs:
made_with: Made with
header:
go_to_homepage: Website
+ visit_website: Visit website
appearance:
system: System
light: Light
diff --git a/config/locales/da.yml b/config/locales/da.yml
index d05f4e485..5f4a816da 100644
--- a/config/locales/da.yml
+++ b/config/locales/da.yml
@@ -172,6 +172,8 @@ da:
sla:
added: '%{user_name} added SLA policy %{sla_name}'
removed: '%{user_name} removed SLA policy %{sla_name}'
+ csat:
+ not_sent_due_to_messaging_window: 'CSAT survey not sent due to outgoing message restrictions'
muted: '%{user_name} har slukket for samtalen'
unmuted: '%{user_name} har genaktiveret samtalen'
auto_resolution_message: 'Resolving the conversation as it has been inactive for a while. Please start a new conversation if you need further assistance.'
@@ -272,6 +274,7 @@ da:
made_with: Made with
header:
go_to_homepage: Website
+ visit_website: Visit website
appearance:
system: System
light: Light
diff --git a/config/locales/de.yml b/config/locales/de.yml
index 8a53d4284..29ca84b45 100644
--- a/config/locales/de.yml
+++ b/config/locales/de.yml
@@ -172,6 +172,8 @@ de:
sla:
added: '%{user_name} hat SLA-Richtlinie %{sla_name} hinzugefügt'
removed: '%{user_name} hat SLA-Richtlinie %{sla_name} entfernt'
+ csat:
+ not_sent_due_to_messaging_window: 'CSAT survey not sent due to outgoing message restrictions'
muted: '%{user_name} hat das Gespräch stumm geschaltet'
unmuted: '%{user_name} hat das Gespräch laut gestellt'
auto_resolution_message: 'Resolving the conversation as it has been inactive for a while. Please start a new conversation if you need further assistance.'
@@ -272,6 +274,7 @@ de:
made_with: Erstellt mit
header:
go_to_homepage: Webseite
+ visit_website: Visit website
appearance:
system: System
light: Hell
diff --git a/config/locales/el.yml b/config/locales/el.yml
index 74b13bdd9..8e15701ae 100644
--- a/config/locales/el.yml
+++ b/config/locales/el.yml
@@ -172,6 +172,8 @@ el:
sla:
added: '%{user_name} added SLA policy %{sla_name}'
removed: '%{user_name} removed SLA policy %{sla_name}'
+ csat:
+ not_sent_due_to_messaging_window: 'CSAT survey not sent due to outgoing message restrictions'
muted: 'Ο χρήστης %{user_name} σίγασε την συνομιλία'
unmuted: 'Ο χρήστης %{user_name} επανάφερε από την σίγαση την συνομιλία'
auto_resolution_message: 'Resolving the conversation as it has been inactive for a while. Please start a new conversation if you need further assistance.'
@@ -272,6 +274,7 @@ el:
made_with: Made with
header:
go_to_homepage: Website
+ visit_website: Visit website
appearance:
system: System
light: Light
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 74177f7dd..8b392f47f 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -105,6 +105,8 @@ en:
conversations_count: No. of conversations
avg_first_response_time: Avg first response time
avg_resolution_time: Avg resolution time
+ avg_reply_time: Avg reply time
+ resolution_count: Resolution Count
team_csv:
team_name: Team name
conversations_count: Conversations count
@@ -152,6 +154,8 @@ en:
instagram_story_content: '%{story_sender} mentioned you in the story: '
instagram_deleted_story_content: This story is no longer available.
deleted: This message was deleted
+ whatsapp:
+ list_button_label: 'Choose an item'
delivery_status:
error_code: 'Error code: %{error_code}'
activity:
@@ -186,6 +190,12 @@ en:
sla:
added: '%{user_name} added SLA policy %{sla_name}'
removed: '%{user_name} removed SLA policy %{sla_name}'
+ linear:
+ issue_created: 'Linear issue %{issue_id} was created by %{user_name}'
+ issue_linked: 'Linear issue %{issue_id} was linked by %{user_name}'
+ issue_unlinked: 'Linear issue %{issue_id} was unlinked by %{user_name}'
+ csat:
+ not_sent_due_to_messaging_window: 'CSAT survey not sent due to outgoing message restrictions'
muted: '%{user_name} has muted the conversation'
unmuted: '%{user_name} has unmuted the conversation'
auto_resolution_message: 'Resolving the conversation as it has been inactive for a while. Please start a new conversation if you need further assistance.'
@@ -286,6 +296,7 @@ en:
made_with: Made with
header:
go_to_homepage: Website
+ visit_website: Visit website
appearance:
system: System
light: Light
diff --git a/config/locales/es.yml b/config/locales/es.yml
index 85197fc27..82b604ddb 100644
--- a/config/locales/es.yml
+++ b/config/locales/es.yml
@@ -172,6 +172,8 @@ es:
sla:
added: '%{user_name} agregó la política de SLA %{sla_name}'
removed: '%{user_name} eliminó la política de SLA %{sla_name}'
+ csat:
+ not_sent_due_to_messaging_window: 'CSAT survey not sent due to outgoing message restrictions'
muted: '%{user_name} ha silenciado la conversación'
unmuted: '%{user_name} ha anulado el silenciado de la conversación'
auto_resolution_message: 'Resolviendo la conversación, ya que ha estado inactiva durante un tiempo. Por favor, inicia una nueva conversación si necesitas más ayuda.'
@@ -272,6 +274,7 @@ es:
made_with: Hecho con
header:
go_to_homepage: Sitio web
+ visit_website: Visit website
appearance:
system: Sistema
light: Claro
diff --git a/config/locales/fa.yml b/config/locales/fa.yml
index 1297f75d0..6cdc532a7 100644
--- a/config/locales/fa.yml
+++ b/config/locales/fa.yml
@@ -172,6 +172,8 @@ fa:
sla:
added: '%{user_name} سیاست SLA %{sla_name} را اضافه کرد'
removed: '%{user_name} سیاست SLA %{sla_name} را حذف کرد'
+ csat:
+ not_sent_due_to_messaging_window: 'CSAT survey not sent due to outgoing message restrictions'
muted: '%{user_name} مکالمه را بی صدا کرد'
unmuted: '%{user_name} مکالمه را از حالت بی صدا خراج کرد'
auto_resolution_message: 'Resolving the conversation as it has been inactive for a while. Please start a new conversation if you need further assistance.'
@@ -272,6 +274,7 @@ fa:
made_with: ساخته شده با
header:
go_to_homepage: وب سایت
+ visit_website: Visit website
appearance:
system: سیستم
light: روشن
diff --git a/config/locales/fi.yml b/config/locales/fi.yml
index 919570bb7..94ff6cb9c 100644
--- a/config/locales/fi.yml
+++ b/config/locales/fi.yml
@@ -172,6 +172,8 @@ fi:
sla:
added: '%{user_name} added SLA policy %{sla_name}'
removed: '%{user_name} removed SLA policy %{sla_name}'
+ csat:
+ not_sent_due_to_messaging_window: 'CSAT survey not sent due to outgoing message restrictions'
muted: '%{user_name} mykisti keskustelun'
unmuted: '%{user_name} poisti keskustelun mykistyksen'
auto_resolution_message: 'Resolving the conversation as it has been inactive for a while. Please start a new conversation if you need further assistance.'
@@ -272,6 +274,7 @@ fi:
made_with: Made with
header:
go_to_homepage: Website
+ visit_website: Visit website
appearance:
system: System
light: Light
diff --git a/config/locales/fr.yml b/config/locales/fr.yml
index 1cf7220d0..406d69b55 100644
--- a/config/locales/fr.yml
+++ b/config/locales/fr.yml
@@ -172,6 +172,8 @@ fr:
sla:
added: '%{user_name} added SLA policy %{sla_name}'
removed: '%{user_name} removed SLA policy %{sla_name}'
+ csat:
+ not_sent_due_to_messaging_window: 'CSAT survey not sent due to outgoing message restrictions'
muted: '%{user_name} a mis la conversation en sourdine'
unmuted: '%{user_name} a rétabli le son de la conversation'
auto_resolution_message: 'Resolving the conversation as it has been inactive for a while. Please start a new conversation if you need further assistance.'
@@ -272,6 +274,7 @@ fr:
made_with: Réalisé avec
header:
go_to_homepage: Site internet
+ visit_website: Visit website
appearance:
system: Système
light: Clair
diff --git a/config/locales/he.yml b/config/locales/he.yml
index d8dd32e10..0945e2f50 100644
--- a/config/locales/he.yml
+++ b/config/locales/he.yml
@@ -172,6 +172,8 @@ he:
sla:
added: '%{user_name} added SLA policy %{sla_name}'
removed: '%{user_name} removed SLA policy %{sla_name}'
+ csat:
+ not_sent_due_to_messaging_window: 'CSAT survey not sent due to outgoing message restrictions'
muted: '%{user_name} has muted the conversation'
unmuted: '%{user_name} has unmuted the conversation'
auto_resolution_message: 'Resolving the conversation as it has been inactive for a while. Please start a new conversation if you need further assistance.'
@@ -272,6 +274,7 @@ he:
made_with: Made with
header:
go_to_homepage: Website
+ visit_website: Visit website
appearance:
system: System
light: Light
diff --git a/config/locales/hi.yml b/config/locales/hi.yml
index 1523a74c0..76055318c 100644
--- a/config/locales/hi.yml
+++ b/config/locales/hi.yml
@@ -172,6 +172,8 @@ hi:
sla:
added: '%{user_name} added SLA policy %{sla_name}'
removed: '%{user_name} removed SLA policy %{sla_name}'
+ csat:
+ not_sent_due_to_messaging_window: 'CSAT survey not sent due to outgoing message restrictions'
muted: '%{user_name} has muted the conversation'
unmuted: '%{user_name} has unmuted the conversation'
auto_resolution_message: 'Resolving the conversation as it has been inactive for a while. Please start a new conversation if you need further assistance.'
@@ -272,6 +274,7 @@ hi:
made_with: Made with
header:
go_to_homepage: Website
+ visit_website: Visit website
appearance:
system: System
light: Light
diff --git a/config/locales/hr.yml b/config/locales/hr.yml
index 3b28e8fe0..23aa9555e 100644
--- a/config/locales/hr.yml
+++ b/config/locales/hr.yml
@@ -172,6 +172,8 @@ hr:
sla:
added: '%{user_name} added SLA policy %{sla_name}'
removed: '%{user_name} removed SLA policy %{sla_name}'
+ csat:
+ not_sent_due_to_messaging_window: 'CSAT survey not sent due to outgoing message restrictions'
muted: '%{user_name} has muted the conversation'
unmuted: '%{user_name} has unmuted the conversation'
auto_resolution_message: 'Resolving the conversation as it has been inactive for a while. Please start a new conversation if you need further assistance.'
@@ -272,6 +274,7 @@ hr:
made_with: Made with
header:
go_to_homepage: Website
+ visit_website: Visit website
appearance:
system: System
light: Light
diff --git a/config/locales/hu.yml b/config/locales/hu.yml
index f7b484d63..b9bb386a5 100644
--- a/config/locales/hu.yml
+++ b/config/locales/hu.yml
@@ -172,6 +172,8 @@ hu:
sla:
added: '%{user_name} added SLA policy %{sla_name}'
removed: '%{user_name} removed SLA policy %{sla_name}'
+ csat:
+ not_sent_due_to_messaging_window: 'CSAT survey not sent due to outgoing message restrictions'
muted: '%{user_name} elnémította a beszélgetést'
unmuted: '%{user_name} kikapcsolta a beszélgetés elnémítását'
auto_resolution_message: 'Resolving the conversation as it has been inactive for a while. Please start a new conversation if you need further assistance.'
@@ -272,6 +274,7 @@ hu:
made_with: 'Ezzel készítve: '
header:
go_to_homepage: Honlap
+ visit_website: Visit website
appearance:
system: Rendszer
light: Világos mód
diff --git a/config/locales/hy.yml b/config/locales/hy.yml
index 26505f10b..acf78ed4d 100644
--- a/config/locales/hy.yml
+++ b/config/locales/hy.yml
@@ -172,6 +172,8 @@ hy:
sla:
added: '%{user_name} added SLA policy %{sla_name}'
removed: '%{user_name} removed SLA policy %{sla_name}'
+ csat:
+ not_sent_due_to_messaging_window: 'CSAT survey not sent due to outgoing message restrictions'
muted: '%{user_name} has muted the conversation'
unmuted: '%{user_name} has unmuted the conversation'
auto_resolution_message: 'Resolving the conversation as it has been inactive for a while. Please start a new conversation if you need further assistance.'
@@ -272,6 +274,7 @@ hy:
made_with: Made with
header:
go_to_homepage: Website
+ visit_website: Visit website
appearance:
system: System
light: Light
diff --git a/config/locales/id.yml b/config/locales/id.yml
index 944262d17..4c039011c 100644
--- a/config/locales/id.yml
+++ b/config/locales/id.yml
@@ -172,6 +172,8 @@ id:
sla:
added: '%{user_name} added SLA policy %{sla_name}'
removed: '%{user_name} removed SLA policy %{sla_name}'
+ csat:
+ not_sent_due_to_messaging_window: 'CSAT survey not sent due to outgoing message restrictions'
muted: '%{user_name} me-mute percakapan'
unmuted: '%{user_name} telah un-mute percakapan'
auto_resolution_message: 'Resolving the conversation as it has been inactive for a while. Please start a new conversation if you need further assistance.'
@@ -272,6 +274,7 @@ id:
made_with: Dibuat oleh
header:
go_to_homepage: Website
+ visit_website: Visit website
appearance:
system: Sistem
light: Light
diff --git a/config/locales/is.yml b/config/locales/is.yml
index 3cfe3fbff..cf97814e2 100644
--- a/config/locales/is.yml
+++ b/config/locales/is.yml
@@ -172,6 +172,8 @@ is:
sla:
added: '%{user_name} added SLA policy %{sla_name}'
removed: '%{user_name} removed SLA policy %{sla_name}'
+ csat:
+ not_sent_due_to_messaging_window: 'CSAT survey not sent due to outgoing message restrictions'
muted: '%{user_name} has muted the conversation'
unmuted: '%{user_name} has unmuted the conversation'
auto_resolution_message: 'Resolving the conversation as it has been inactive for a while. Please start a new conversation if you need further assistance.'
@@ -272,6 +274,7 @@ is:
made_with: Made with
header:
go_to_homepage: Website
+ visit_website: Visit website
appearance:
system: System
light: Light
diff --git a/config/locales/it.yml b/config/locales/it.yml
index 047fca00d..e6ab55115 100644
--- a/config/locales/it.yml
+++ b/config/locales/it.yml
@@ -172,6 +172,8 @@ it:
sla:
added: '%{user_name} added SLA policy %{sla_name}'
removed: '%{user_name} removed SLA policy %{sla_name}'
+ csat:
+ not_sent_due_to_messaging_window: 'CSAT survey not sent due to outgoing message restrictions'
muted: '%{user_name} ha silenziato la conversazione'
unmuted: '%{user_name} ha riattivato l''audio della conversazione'
auto_resolution_message: 'Resolving the conversation as it has been inactive for a while. Please start a new conversation if you need further assistance.'
@@ -272,6 +274,7 @@ it:
made_with: Made with
header:
go_to_homepage: Website
+ visit_website: Visit website
appearance:
system: System
light: Light
diff --git a/config/locales/ja.yml b/config/locales/ja.yml
index f1d571634..928ac3e71 100644
--- a/config/locales/ja.yml
+++ b/config/locales/ja.yml
@@ -172,6 +172,8 @@ ja:
sla:
added: '%{user_name} がSLAポリシー "%{sla_name}" を追加しました'
removed: '%{user_name} がSLAポリシー "%{sla_name}" を削除しました'
+ csat:
+ not_sent_due_to_messaging_window: 'CSAT survey not sent due to outgoing message restrictions'
muted: '%{user_name} が会話をミュートしました'
unmuted: '%{user_name} が会話のミュートを解除しました'
auto_resolution_message: '会話がしばらく非アクティブのため、解決済みとして処理します。引き続きサポートが必要な場合は、新しい会話を開始してください。'
@@ -272,6 +274,7 @@ ja:
made_with: 作成
header:
go_to_homepage: ウェブサイト
+ visit_website: Visit website
appearance:
system: システム
light: ライト
diff --git a/config/locales/ka.yml b/config/locales/ka.yml
index 638963262..4154ddf14 100644
--- a/config/locales/ka.yml
+++ b/config/locales/ka.yml
@@ -172,6 +172,8 @@ ka:
sla:
added: '%{user_name} added SLA policy %{sla_name}'
removed: '%{user_name} removed SLA policy %{sla_name}'
+ csat:
+ not_sent_due_to_messaging_window: 'CSAT survey not sent due to outgoing message restrictions'
muted: '%{user_name} has muted the conversation'
unmuted: '%{user_name} has unmuted the conversation'
auto_resolution_message: 'Resolving the conversation as it has been inactive for a while. Please start a new conversation if you need further assistance.'
@@ -272,6 +274,7 @@ ka:
made_with: Made with
header:
go_to_homepage: Website
+ visit_website: Visit website
appearance:
system: System
light: Light
diff --git a/config/locales/ko.yml b/config/locales/ko.yml
index 6e249a23c..e76883f44 100644
--- a/config/locales/ko.yml
+++ b/config/locales/ko.yml
@@ -172,6 +172,8 @@ ko:
sla:
added: '%{user_name} added SLA policy %{sla_name}'
removed: '%{user_name} removed SLA policy %{sla_name}'
+ csat:
+ not_sent_due_to_messaging_window: 'CSAT survey not sent due to outgoing message restrictions'
muted: '%{user_name} has muted the conversation'
unmuted: '%{user_name} has unmuted the conversation'
auto_resolution_message: 'Resolving the conversation as it has been inactive for a while. Please start a new conversation if you need further assistance.'
@@ -272,6 +274,7 @@ ko:
made_with: Made with
header:
go_to_homepage: 홈페이지
+ visit_website: Visit website
appearance:
system: 시스템
light: 밝게
diff --git a/config/locales/lt.yml b/config/locales/lt.yml
index 1f9e894e8..dc093901f 100644
--- a/config/locales/lt.yml
+++ b/config/locales/lt.yml
@@ -172,6 +172,8 @@ lt:
sla:
added: '%{user_name} added SLA policy %{sla_name}'
removed: '%{user_name} removed SLA policy %{sla_name}'
+ csat:
+ not_sent_due_to_messaging_window: 'CSAT survey not sent due to outgoing message restrictions'
muted: '%{user_name} nutildė pokalbį'
unmuted: '%{user_name} įjungė pokalbio garsą'
auto_resolution_message: 'Resolving the conversation as it has been inactive for a while. Please start a new conversation if you need further assistance.'
@@ -272,6 +274,7 @@ lt:
made_with: Padaryta su
header:
go_to_homepage: Internetinis puslapis
+ visit_website: Visit website
appearance:
system: Sistema
light: Šviesus
diff --git a/config/locales/lv.yml b/config/locales/lv.yml
index 362bf889b..dab14d39f 100644
--- a/config/locales/lv.yml
+++ b/config/locales/lv.yml
@@ -172,6 +172,8 @@ lv:
sla:
added: '%{user_name} pievienoja SLA politiku %{sla_name}'
removed: '%{user_name} noņēma SLA politiku %{sla_name}'
+ csat:
+ not_sent_due_to_messaging_window: 'CSAT survey not sent due to outgoing message restrictions'
muted: '%{user_name} izslēdza sarunu'
unmuted: '%{user_name} ieslēdza sarunu'
auto_resolution_message: 'Saruna tiek pabeigta, jo tā kādu laiku ir bijusi neaktīva. Ja nepieciešama papildu palīdzība, lūdzu, sāciet jaunu sarunu.'
@@ -272,6 +274,7 @@ lv:
made_with: Veidots ar
header:
go_to_homepage: Tīmekļa vietne
+ visit_website: Visit website
appearance:
system: Sistēma
light: Gaišs
diff --git a/config/locales/ml.yml b/config/locales/ml.yml
index 3acea5598..df5f3f329 100644
--- a/config/locales/ml.yml
+++ b/config/locales/ml.yml
@@ -172,6 +172,8 @@ ml:
sla:
added: '%{user_name} added SLA policy %{sla_name}'
removed: '%{user_name} removed SLA policy %{sla_name}'
+ csat:
+ not_sent_due_to_messaging_window: 'CSAT survey not sent due to outgoing message restrictions'
muted: '%{user_name} has muted the conversation'
unmuted: '%{user_name} has unmuted the conversation'
auto_resolution_message: 'Resolving the conversation as it has been inactive for a while. Please start a new conversation if you need further assistance.'
@@ -272,6 +274,7 @@ ml:
made_with: Made with
header:
go_to_homepage: Website
+ visit_website: Visit website
appearance:
system: System
light: Light
diff --git a/config/locales/ms.yml b/config/locales/ms.yml
index b8a1749d0..536fe8134 100644
--- a/config/locales/ms.yml
+++ b/config/locales/ms.yml
@@ -172,6 +172,8 @@ ms:
sla:
added: '%{user_name} added SLA policy %{sla_name}'
removed: '%{user_name} removed SLA policy %{sla_name}'
+ csat:
+ not_sent_due_to_messaging_window: 'CSAT survey not sent due to outgoing message restrictions'
muted: '%{user_name} has muted the conversation'
unmuted: '%{user_name} has unmuted the conversation'
auto_resolution_message: 'Resolving the conversation as it has been inactive for a while. Please start a new conversation if you need further assistance.'
@@ -272,6 +274,7 @@ ms:
made_with: Made with
header:
go_to_homepage: Website
+ visit_website: Visit website
appearance:
system: System
light: Light
diff --git a/config/locales/ne.yml b/config/locales/ne.yml
index 841c574f6..a1e2e478d 100644
--- a/config/locales/ne.yml
+++ b/config/locales/ne.yml
@@ -172,6 +172,8 @@ ne:
sla:
added: '%{user_name} added SLA policy %{sla_name}'
removed: '%{user_name} removed SLA policy %{sla_name}'
+ csat:
+ not_sent_due_to_messaging_window: 'CSAT survey not sent due to outgoing message restrictions'
muted: '%{user_name} has muted the conversation'
unmuted: '%{user_name} has unmuted the conversation'
auto_resolution_message: 'Resolving the conversation as it has been inactive for a while. Please start a new conversation if you need further assistance.'
@@ -272,6 +274,7 @@ ne:
made_with: Made with
header:
go_to_homepage: Website
+ visit_website: Visit website
appearance:
system: System
light: Light
diff --git a/config/locales/nl.yml b/config/locales/nl.yml
index 735480328..85ca07291 100644
--- a/config/locales/nl.yml
+++ b/config/locales/nl.yml
@@ -172,6 +172,8 @@ nl:
sla:
added: '%{user_name} added SLA policy %{sla_name}'
removed: '%{user_name} removed SLA policy %{sla_name}'
+ csat:
+ not_sent_due_to_messaging_window: 'CSAT survey not sent due to outgoing message restrictions'
muted: '%{user_name} has muted the conversation'
unmuted: '%{user_name} has unmuted the conversation'
auto_resolution_message: 'Resolving the conversation as it has been inactive for a while. Please start a new conversation if you need further assistance.'
@@ -272,6 +274,7 @@ nl:
made_with: Made with
header:
go_to_homepage: Website
+ visit_website: Visit website
appearance:
system: Systeem
light: Light
diff --git a/config/locales/no.yml b/config/locales/no.yml
index a1f50775d..130c554db 100644
--- a/config/locales/no.yml
+++ b/config/locales/no.yml
@@ -172,6 +172,8 @@
sla:
added: '%{user_name} added SLA policy %{sla_name}'
removed: '%{user_name} removed SLA policy %{sla_name}'
+ csat:
+ not_sent_due_to_messaging_window: 'CSAT survey not sent due to outgoing message restrictions'
muted: '%{user_name} har dempet samtalen'
unmuted: '%{user_name} har opphevet dempingen av samtalen'
auto_resolution_message: 'Resolving the conversation as it has been inactive for a while. Please start a new conversation if you need further assistance.'
@@ -272,6 +274,7 @@
made_with: Made with
header:
go_to_homepage: Website
+ visit_website: Visit website
appearance:
system: System
light: Light
diff --git a/config/locales/pl.yml b/config/locales/pl.yml
index 963051c7b..d589af5c5 100644
--- a/config/locales/pl.yml
+++ b/config/locales/pl.yml
@@ -172,6 +172,8 @@ pl:
sla:
added: '%{user_name} added SLA policy %{sla_name}'
removed: '%{user_name} removed SLA policy %{sla_name}'
+ csat:
+ not_sent_due_to_messaging_window: 'CSAT survey not sent due to outgoing message restrictions'
muted: '%{user_name} wyciszył/a rozmowę'
unmuted: '%{user_name} cofnął wyciszenie rozmowy'
auto_resolution_message: 'Resolving the conversation as it has been inactive for a while. Please start a new conversation if you need further assistance.'
@@ -272,6 +274,7 @@ pl:
made_with: Wykonane z
header:
go_to_homepage: Website
+ visit_website: Visit website
appearance:
system: System
light: Light
diff --git a/config/locales/pt.yml b/config/locales/pt.yml
index 1521bf52e..e725e619a 100644
--- a/config/locales/pt.yml
+++ b/config/locales/pt.yml
@@ -172,6 +172,8 @@ pt:
sla:
added: '%{user_name} adicionou uma política de SLA %{sla_name}'
removed: '%{user_name} removeu a política de SLA de %{sla_name}'
+ csat:
+ not_sent_due_to_messaging_window: 'CSAT survey not sent due to outgoing message restrictions'
muted: '%{user_name} bloqueou a conversa'
unmuted: '%{user_name} reativou a conversa'
auto_resolution_message: 'Resolving the conversation as it has been inactive for a while. Please start a new conversation if you need further assistance.'
@@ -272,6 +274,7 @@ pt:
made_with: Feito com
header:
go_to_homepage: Website
+ visit_website: Visit website
appearance:
system: Sistema
light: Claro
diff --git a/config/locales/pt_BR.yml b/config/locales/pt_BR.yml
index 965982b8f..19570ab07 100644
--- a/config/locales/pt_BR.yml
+++ b/config/locales/pt_BR.yml
@@ -138,6 +138,8 @@ pt_BR:
instagram_story_content: '%{story_sender} mencionou você na conversa: '
instagram_deleted_story_content: Este Story não está mais disponível.
deleted: Esta mensagem foi excluída
+ whatsapp:
+ list_button_label: 'Escolha um item'
delivery_status:
error_code: 'Código de erro: %{error_code}'
activity:
@@ -172,6 +174,8 @@ pt_BR:
sla:
added: '%{user_name} adicionou política de SLA %{sla_name}'
removed: '%{user_name} removeu a política de SLA %{sla_name}'
+ csat:
+ not_sent_due_to_messaging_window: 'Pesquisa CSAT não foi enviada devido a restrições de envio de mensagens'
muted: '%{user_name} silenciou a conversa'
unmuted: '%{user_name} reativou a conversa'
auto_resolution_message: 'Resolvendo a conversa dado que está inativa por um tempo. Por favor, inicie uma nova conversa se precisar de mais ajuda.'
@@ -211,7 +215,7 @@ pt_BR:
slack:
name: 'Slack'
short_description: 'Receba notificações e responda as conversas diretamente no Slack.'
- description: "Integre Chatwoot com Slack para manter seu time em sincronia. Essa integração permite que você receba notificações de novas conversas e as responda diretamente na interface do Slack."
+ description: 'Integre Chatwoot com Slack para manter seu time em sincronia. Essa integração permite que você receba notificações de novas conversas e as responda diretamente na interface do Slack.'
webhooks:
name: 'Webhooks'
description: 'Eventos webhook fornecem atualizações sobre atividades em tempo real na sua conta Chatwoot. Você pode se inscrever em seus eventos preferidos, e o Chatwoot enviará as chamadas HTTP com as atualizações.'
@@ -222,7 +226,7 @@ pt_BR:
google_translate:
name: 'Tradutor do Google'
short_description: 'Traduzir automaticamente mensagens de clientes para agentes.'
- description: "Integre o Google Tradutor para ajudar os agentes a traduzir facilmente as mensagens dos clientes. Esta integração detecta automaticamente o idioma e o converte para o idioma preferido do agente ou do administrador."
+ description: 'Integre o Google Tradutor para ajudar os agentes a traduzir facilmente as mensagens dos clientes. Esta integração detecta automaticamente o idioma e o converte para o idioma preferido do agente ou do administrador.'
openai:
name: 'OpenAI'
short_description: 'Sugestões, resumos e aprimoramento de mensagem e resposta com IA.'
@@ -272,6 +276,7 @@ pt_BR:
made_with: Criado com
header:
go_to_homepage: Site
+ visit_website: Visit website
appearance:
system: Sistema
light: Claro
diff --git a/config/locales/ro.yml b/config/locales/ro.yml
index c284925cd..3b5d6858e 100644
--- a/config/locales/ro.yml
+++ b/config/locales/ro.yml
@@ -172,6 +172,8 @@ ro:
sla:
added: '%{user_name} added SLA policy %{sla_name}'
removed: '%{user_name} removed SLA policy %{sla_name}'
+ csat:
+ not_sent_due_to_messaging_window: 'CSAT survey not sent due to outgoing message restrictions'
muted: '%{user_name} a dezactivat conversația'
unmuted: '%{user_name} a activat conversația'
auto_resolution_message: 'Resolving the conversation as it has been inactive for a while. Please start a new conversation if you need further assistance.'
@@ -272,6 +274,7 @@ ro:
made_with: Realizat cu
header:
go_to_homepage: Website
+ visit_website: Visit website
appearance:
system: Sistem
light: Light
diff --git a/config/locales/ru.yml b/config/locales/ru.yml
index 418b6f025..3f6a4bbee 100644
--- a/config/locales/ru.yml
+++ b/config/locales/ru.yml
@@ -172,6 +172,8 @@ ru:
sla:
added: '%{user_name} добавил политику SLA %{sla_name}'
removed: '%{user_name} удалил политику SLA %{sla_name}'
+ csat:
+ not_sent_due_to_messaging_window: 'CSAT survey not sent due to outgoing message restrictions'
muted: '%{user_name} заглушил(а) этот разговор'
unmuted: '%{user_name} включил(а) уведомления для разговора'
auto_resolution_message: 'Разговор закрывается, поскольку он был неактивен в течение длительного времени. Пожалуйста, начните новый разговор, если потребуется дополнительная помощь.'
@@ -272,6 +274,7 @@ ru:
made_with: Сделано с
header:
go_to_homepage: Сайт
+ visit_website: Visit website
appearance:
system: Система
light: Светлая
diff --git a/config/locales/sh.yml b/config/locales/sh.yml
index 4f5be6aaa..204a213c0 100644
--- a/config/locales/sh.yml
+++ b/config/locales/sh.yml
@@ -172,6 +172,8 @@ sh:
sla:
added: '%{user_name} added SLA policy %{sla_name}'
removed: '%{user_name} removed SLA policy %{sla_name}'
+ csat:
+ not_sent_due_to_messaging_window: 'CSAT survey not sent due to outgoing message restrictions'
muted: '%{user_name} has muted the conversation'
unmuted: '%{user_name} has unmuted the conversation'
auto_resolution_message: 'Resolving the conversation as it has been inactive for a while. Please start a new conversation if you need further assistance.'
@@ -272,6 +274,7 @@ sh:
made_with: Made with
header:
go_to_homepage: Website
+ visit_website: Visit website
appearance:
system: System
light: Light
diff --git a/config/locales/sk.yml b/config/locales/sk.yml
index 26bd825ee..29aec1757 100644
--- a/config/locales/sk.yml
+++ b/config/locales/sk.yml
@@ -172,6 +172,8 @@ sk:
sla:
added: '%{user_name} added SLA policy %{sla_name}'
removed: '%{user_name} removed SLA policy %{sla_name}'
+ csat:
+ not_sent_due_to_messaging_window: 'CSAT survey not sent due to outgoing message restrictions'
muted: '%{user_name} stlmil konverzáciu'
unmuted: '%{user_name} odtlmil konverzáciu'
auto_resolution_message: 'Resolving the conversation as it has been inactive for a while. Please start a new conversation if you need further assistance.'
@@ -272,6 +274,7 @@ sk:
made_with: Made with
header:
go_to_homepage: Website
+ visit_website: Visit website
appearance:
system: System
light: Light
diff --git a/config/locales/sl.yml b/config/locales/sl.yml
index 7c2a00b22..eb3f30350 100644
--- a/config/locales/sl.yml
+++ b/config/locales/sl.yml
@@ -172,6 +172,8 @@ sl:
sla:
added: '%{user_name} je dodal politiko SLA %{sla_name}'
removed: '%{user_name} je odstranil politiko SLA %{sla_name}'
+ csat:
+ not_sent_due_to_messaging_window: 'CSAT survey not sent due to outgoing message restrictions'
muted: '%{user_name} je utišal pogovor'
unmuted: '%{user_name} je vklopil sporočila pogovora'
auto_resolution_message: 'Resolving the conversation as it has been inactive for a while. Please start a new conversation if you need further assistance.'
@@ -272,6 +274,7 @@ sl:
made_with: Narejeno z
header:
go_to_homepage: Spletna stran
+ visit_website: Visit website
appearance:
system: Sistem
light: Svetlo
diff --git a/config/locales/sq.yml b/config/locales/sq.yml
index 9d3edc96c..8509dc3d0 100644
--- a/config/locales/sq.yml
+++ b/config/locales/sq.yml
@@ -172,6 +172,8 @@ sq:
sla:
added: '%{user_name} added SLA policy %{sla_name}'
removed: '%{user_name} removed SLA policy %{sla_name}'
+ csat:
+ not_sent_due_to_messaging_window: 'CSAT survey not sent due to outgoing message restrictions'
muted: '%{user_name} has muted the conversation'
unmuted: '%{user_name} has unmuted the conversation'
auto_resolution_message: 'Resolving the conversation as it has been inactive for a while. Please start a new conversation if you need further assistance.'
@@ -272,6 +274,7 @@ sq:
made_with: Made with
header:
go_to_homepage: Website
+ visit_website: Visit website
appearance:
system: System
light: Light
diff --git a/config/locales/sr.yml b/config/locales/sr.yml
index 0dc74a5ae..e8f1006a8 100644
--- a/config/locales/sr.yml
+++ b/config/locales/sr.yml
@@ -172,6 +172,8 @@ sr-Latn:
sla:
added: '%{user_name} added SLA policy %{sla_name}'
removed: '%{user_name} removed SLA policy %{sla_name}'
+ csat:
+ not_sent_due_to_messaging_window: 'CSAT survey not sent due to outgoing message restrictions'
muted: '%{user_name} je utišao razgovor'
unmuted: '%{user_name} je uklonio utišanje razgovora'
auto_resolution_message: 'Resolving the conversation as it has been inactive for a while. Please start a new conversation if you need further assistance.'
@@ -272,6 +274,7 @@ sr-Latn:
made_with: Made with
header:
go_to_homepage: Website
+ visit_website: Visit website
appearance:
system: System
light: Light
diff --git a/config/locales/sv.yml b/config/locales/sv.yml
index 0c4027142..4953c85da 100644
--- a/config/locales/sv.yml
+++ b/config/locales/sv.yml
@@ -172,6 +172,8 @@ sv:
sla:
added: '%{user_name} added SLA policy %{sla_name}'
removed: '%{user_name} removed SLA policy %{sla_name}'
+ csat:
+ not_sent_due_to_messaging_window: 'CSAT survey not sent due to outgoing message restrictions'
muted: '%{user_name} har tystat konversationen'
unmuted: '%{user_name} har avtystat konversationen'
auto_resolution_message: 'Resolving the conversation as it has been inactive for a while. Please start a new conversation if you need further assistance.'
@@ -272,6 +274,7 @@ sv:
made_with: Gjord med
header:
go_to_homepage: Hemsida
+ visit_website: Visit website
appearance:
system: System
light: Ljus
diff --git a/config/locales/ta.yml b/config/locales/ta.yml
index 875701bde..a7571007f 100644
--- a/config/locales/ta.yml
+++ b/config/locales/ta.yml
@@ -172,6 +172,8 @@ ta:
sla:
added: '%{user_name} added SLA policy %{sla_name}'
removed: '%{user_name} removed SLA policy %{sla_name}'
+ csat:
+ not_sent_due_to_messaging_window: 'CSAT survey not sent due to outgoing message restrictions'
muted: '%{user_name} has muted the conversation'
unmuted: '%{user_name} has unmuted the conversation'
auto_resolution_message: 'Resolving the conversation as it has been inactive for a while. Please start a new conversation if you need further assistance.'
@@ -272,6 +274,7 @@ ta:
made_with: Made with
header:
go_to_homepage: Website
+ visit_website: Visit website
appearance:
system: System
light: Light
diff --git a/config/locales/th.yml b/config/locales/th.yml
index 60c8cecb3..b1b9bd8d7 100644
--- a/config/locales/th.yml
+++ b/config/locales/th.yml
@@ -172,6 +172,8 @@ th:
sla:
added: '%{user_name} added SLA policy %{sla_name}'
removed: '%{user_name} removed SLA policy %{sla_name}'
+ csat:
+ not_sent_due_to_messaging_window: 'CSAT survey not sent due to outgoing message restrictions'
muted: '%{user_name} has muted the conversation'
unmuted: '%{user_name} has unmuted the conversation'
auto_resolution_message: 'Resolving the conversation as it has been inactive for a while. Please start a new conversation if you need further assistance.'
@@ -272,6 +274,7 @@ th:
made_with: Made with
header:
go_to_homepage: Website
+ visit_website: Visit website
appearance:
system: System
light: Light
diff --git a/config/locales/tl.yml b/config/locales/tl.yml
index 6d4290bad..8c3d209fa 100644
--- a/config/locales/tl.yml
+++ b/config/locales/tl.yml
@@ -172,6 +172,8 @@ tl:
sla:
added: '%{user_name} added SLA policy %{sla_name}'
removed: '%{user_name} removed SLA policy %{sla_name}'
+ csat:
+ not_sent_due_to_messaging_window: 'CSAT survey not sent due to outgoing message restrictions'
muted: '%{user_name} has muted the conversation'
unmuted: '%{user_name} has unmuted the conversation'
auto_resolution_message: 'Resolving the conversation as it has been inactive for a while. Please start a new conversation if you need further assistance.'
@@ -272,6 +274,7 @@ tl:
made_with: Made with
header:
go_to_homepage: Website
+ visit_website: Visit website
appearance:
system: System
light: Light
diff --git a/config/locales/tr.yml b/config/locales/tr.yml
index e99700e45..2f045ad5a 100644
--- a/config/locales/tr.yml
+++ b/config/locales/tr.yml
@@ -172,6 +172,8 @@ tr:
sla:
added: '%{user_name} added SLA policy %{sla_name}'
removed: '%{user_name} removed SLA policy %{sla_name}'
+ csat:
+ not_sent_due_to_messaging_window: 'CSAT survey not sent due to outgoing message restrictions'
muted: '%{user_name}, sohbeti sessize aldı'
unmuted: '%{user_name} konuşmanın sesini açtı'
auto_resolution_message: 'Resolving the conversation as it has been inactive for a while. Please start a new conversation if you need further assistance.'
@@ -272,6 +274,7 @@ tr:
made_with: İle yapılmıştır
header:
go_to_homepage: Website
+ visit_website: Visit website
appearance:
system: Sistem
light: Açık Mod
diff --git a/config/locales/uk.yml b/config/locales/uk.yml
index 736bbecf8..a51af682d 100644
--- a/config/locales/uk.yml
+++ b/config/locales/uk.yml
@@ -172,6 +172,8 @@ uk:
sla:
added: '%{user_name} додав політику SLA %{sla_name}'
removed: '%{user_name} видалив політику SLA %{sla_name}'
+ csat:
+ not_sent_due_to_messaging_window: 'CSAT survey not sent due to outgoing message restrictions'
muted: '%{user_name} включив безвучний режим'
unmuted: '%{user_name} виключив безвучний режим'
auto_resolution_message: 'Resolving the conversation as it has been inactive for a while. Please start a new conversation if you need further assistance.'
@@ -272,6 +274,7 @@ uk:
made_with: Зроблено з
header:
go_to_homepage: Вебсайт
+ visit_website: Visit website
appearance:
system: Системна
light: Світла
diff --git a/config/locales/ur.yml b/config/locales/ur.yml
index c553bb8ac..2a84171c2 100644
--- a/config/locales/ur.yml
+++ b/config/locales/ur.yml
@@ -172,6 +172,8 @@ ur:
sla:
added: '%{user_name} added SLA policy %{sla_name}'
removed: '%{user_name} removed SLA policy %{sla_name}'
+ csat:
+ not_sent_due_to_messaging_window: 'CSAT survey not sent due to outgoing message restrictions'
muted: '%{user_name} has muted the conversation'
unmuted: '%{user_name} has unmuted the conversation'
auto_resolution_message: 'Resolving the conversation as it has been inactive for a while. Please start a new conversation if you need further assistance.'
@@ -272,6 +274,7 @@ ur:
made_with: Made with
header:
go_to_homepage: Website
+ visit_website: Visit website
appearance:
system: System
light: Light
diff --git a/config/locales/ur_IN.yml b/config/locales/ur_IN.yml
index 884928dbe..4cbeef832 100644
--- a/config/locales/ur_IN.yml
+++ b/config/locales/ur_IN.yml
@@ -172,6 +172,8 @@ ur:
sla:
added: '%{user_name} added SLA policy %{sla_name}'
removed: '%{user_name} removed SLA policy %{sla_name}'
+ csat:
+ not_sent_due_to_messaging_window: 'CSAT survey not sent due to outgoing message restrictions'
muted: '%{user_name} has muted the conversation'
unmuted: '%{user_name} has unmuted the conversation'
auto_resolution_message: 'Resolving the conversation as it has been inactive for a while. Please start a new conversation if you need further assistance.'
@@ -272,6 +274,7 @@ ur:
made_with: Made with
header:
go_to_homepage: Website
+ visit_website: Visit website
appearance:
system: System
light: Light
diff --git a/config/locales/vi.yml b/config/locales/vi.yml
index 1ad0c8654..c626577e6 100644
--- a/config/locales/vi.yml
+++ b/config/locales/vi.yml
@@ -172,6 +172,8 @@ vi:
sla:
added: '%{user_name} added SLA policy %{sla_name}'
removed: '%{user_name} removed SLA policy %{sla_name}'
+ csat:
+ not_sent_due_to_messaging_window: 'CSAT survey not sent due to outgoing message restrictions'
muted: '%{user_name} đã tắt tiếng hội thoại'
unmuted: '%{user_name} đã bật tiếng cuộc trò chuyện'
auto_resolution_message: 'Resolving the conversation as it has been inactive for a while. Please start a new conversation if you need further assistance.'
@@ -272,6 +274,7 @@ vi:
made_with: Tạo bởi
header:
go_to_homepage: Website
+ visit_website: Visit website
appearance:
system: System
light: Light
diff --git a/config/locales/zh_CN.yml b/config/locales/zh_CN.yml
index e7b1d406b..d071daa47 100644
--- a/config/locales/zh_CN.yml
+++ b/config/locales/zh_CN.yml
@@ -172,6 +172,8 @@ zh_CN:
sla:
added: '%{user_name} 添加了 SLA 策略 %{sla_name}'
removed: '%{user_name} 移除了 SLA 策略 %{sla_name}'
+ csat:
+ not_sent_due_to_messaging_window: 'CSAT survey not sent due to outgoing message restrictions'
muted: '%{user_name} 已将会话静音'
unmuted: '%{user_name} 已将会话取消静音'
auto_resolution_message: '由于对话长时间未活跃,已将其标记为已解决。如果您需要进一步帮助,请开始新的对话。'
@@ -272,6 +274,7 @@ zh_CN:
made_with: 制作于
header:
go_to_homepage: 网站
+ visit_website: Visit website
appearance:
system: 系统
light: 浅色
diff --git a/config/locales/zh_TW.yml b/config/locales/zh_TW.yml
index bf96f23e2..26a4b7e5d 100644
--- a/config/locales/zh_TW.yml
+++ b/config/locales/zh_TW.yml
@@ -172,6 +172,8 @@ zh_TW:
sla:
added: '%{user_name} added SLA policy %{sla_name}'
removed: '%{user_name} removed SLA policy %{sla_name}'
+ csat:
+ not_sent_due_to_messaging_window: 'CSAT survey not sent due to outgoing message restrictions'
muted: '%{user_name} 已將對話靜音'
unmuted: '%{user_name} 將對話解除靜音'
auto_resolution_message: 'Resolving the conversation as it has been inactive for a while. Please start a new conversation if you need further assistance.'
@@ -272,6 +274,7 @@ zh_TW:
made_with: Made with
header:
go_to_homepage: Website
+ visit_website: Visit website
appearance:
system: System
light: Light
diff --git a/config/routes.rb b/config/routes.rb
index 0e1d56240..85a50377b 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -344,6 +344,7 @@ Rails.application.routes.draw do
get :agent
get :team
get :inbox
+ get :label
end
end
resources :reports, only: [:index] do
@@ -524,7 +525,7 @@ Rails.application.routes.draw do
resources :access_tokens, only: [:index, :show]
resources :installation_configs, only: [:index, :new, :create, :show, :edit, :update]
- resources :agent_bots, only: [:index, :new, :create, :show, :edit, :update] do
+ resources :agent_bots, only: [:index, :new, :create, :show, :edit, :update, :destroy] do
delete :avatar, on: :member, action: :destroy_avatar
end
resources :platform_apps, only: [:index, :new, :create, :show, :edit, :update, :destroy]
diff --git a/lib/integrations/linear/processor_service.rb b/lib/integrations/linear/processor_service.rb
index 2dfae28dc..a53d17f4c 100644
--- a/lib/integrations/linear/processor_service.rb
+++ b/lib/integrations/linear/processor_service.rb
@@ -28,7 +28,8 @@ class Integrations::Linear::ProcessorService
{
data: { id: response['issueCreate']['issue']['id'],
- title: response['issueCreate']['issue']['title'] }
+ title: response['issueCreate']['issue']['title'],
+ identifier: response['issueCreate']['issue']['identifier'] }
}
end
diff --git a/lib/linear/mutations.rb b/lib/linear/mutations.rb
index 6a9377e13..04774c705 100644
--- a/lib/linear/mutations.rb
+++ b/lib/linear/mutations.rb
@@ -25,6 +25,7 @@ module Linear::Mutations
issue {
id
title
+ identifier
}
}
}
diff --git a/lib/seeders/reports/conversation_creator.rb b/lib/seeders/reports/conversation_creator.rb
new file mode 100644
index 000000000..b6259de7d
--- /dev/null
+++ b/lib/seeders/reports/conversation_creator.rb
@@ -0,0 +1,105 @@
+# frozen_string_literal: true
+
+require 'faker'
+require 'active_support/testing/time_helpers'
+
+class Seeders::Reports::ConversationCreator
+ include ActiveSupport::Testing::TimeHelpers
+
+ def initialize(account:, resources:)
+ @account = account
+ @contacts = resources[:contacts]
+ @inboxes = resources[:inboxes]
+ @teams = resources[:teams]
+ @labels = resources[:labels]
+ @agents = resources[:agents]
+ @priorities = [nil, 'urgent', 'high', 'medium', 'low']
+ end
+
+ def create_conversation(created_at:)
+ conversation = nil
+
+ ActiveRecord::Base.transaction do
+ travel_to(created_at) do
+ conversation = build_conversation
+ conversation.save!
+
+ add_labels_to_conversation(conversation)
+ create_messages_for_conversation(conversation)
+ resolve_conversation_if_needed(conversation)
+ end
+
+ travel_back
+ end
+
+ conversation
+ end
+
+ private
+
+ def build_conversation
+ contact = @contacts.sample
+ inbox = @inboxes.sample
+
+ contact_inbox = find_or_create_contact_inbox(contact, inbox)
+ assignee = select_assignee(inbox)
+ team = select_team
+ priority = @priorities.sample
+
+ contact_inbox.conversations.new(
+ account: @account,
+ inbox: inbox,
+ contact: contact,
+ assignee: assignee,
+ team: team,
+ priority: priority
+ )
+ end
+
+ def find_or_create_contact_inbox(contact, inbox)
+ inbox.contact_inboxes.find_or_create_by!(
+ contact: contact,
+ source_id: SecureRandom.hex
+ )
+ end
+
+ def select_assignee(inbox)
+ rand(10) < 8 ? inbox.members.sample : nil
+ end
+
+ def select_team
+ rand(10) < 7 ? @teams.sample : nil
+ end
+
+ def add_labels_to_conversation(conversation)
+ labels_to_add = @labels.sample(rand(5..20))
+ conversation.update_labels(labels_to_add.map(&:title))
+ end
+
+ def create_messages_for_conversation(conversation)
+ message_creator = Seeders::Reports::MessageCreator.new(
+ account: @account,
+ agents: @agents,
+ conversation: conversation
+ )
+ message_creator.create_messages
+ end
+
+ def resolve_conversation_if_needed(conversation)
+ return unless rand < 0.7
+
+ resolution_delay = rand((30.minutes)..(24.hours))
+ travel(resolution_delay)
+ conversation.update!(status: :resolved)
+
+ trigger_conversation_resolved_event(conversation)
+ end
+
+ def trigger_conversation_resolved_event(conversation)
+ event_data = { conversation: conversation }
+
+ ReportingEventListener.instance.conversation_resolved(
+ Events::Base.new('conversation_resolved', Time.current, event_data)
+ )
+ end
+end
diff --git a/lib/seeders/reports/message_creator.rb b/lib/seeders/reports/message_creator.rb
new file mode 100644
index 000000000..fc10716d9
--- /dev/null
+++ b/lib/seeders/reports/message_creator.rb
@@ -0,0 +1,141 @@
+# frozen_string_literal: true
+
+require 'faker'
+require 'active_support/testing/time_helpers'
+
+class Seeders::Reports::MessageCreator
+ include ActiveSupport::Testing::TimeHelpers
+
+ MESSAGES_PER_CONVERSATION = 5
+
+ def initialize(account:, agents:, conversation:)
+ @account = account
+ @agents = agents
+ @conversation = conversation
+ end
+
+ def create_messages
+ message_count = rand(MESSAGES_PER_CONVERSATION..MESSAGES_PER_CONVERSATION + 5)
+ first_agent_reply = true
+
+ message_count.times do |i|
+ message = create_single_message(i)
+ first_agent_reply = handle_reply_tracking(message, i, first_agent_reply)
+ end
+ end
+
+ def create_single_message(index)
+ is_incoming = index.even?
+ add_realistic_delay(index, is_incoming) if index.positive?
+ create_message(is_incoming)
+ end
+
+ def handle_reply_tracking(message, index, first_agent_reply)
+ return first_agent_reply if index.even? # Skip incoming messages
+
+ handle_agent_reply_events(message, first_agent_reply)
+ false # No longer first reply after any agent message
+ end
+
+ private
+
+ def add_realistic_delay(_message_index, is_incoming)
+ delay = calculate_message_delay(is_incoming)
+ travel(delay)
+ end
+
+ def calculate_message_delay(is_incoming)
+ if is_incoming
+ # Customer response time: 1 minute to 4 hours
+ rand((1.minute)..(4.hours))
+ elsif business_hours_active?(Time.current)
+ # Agent response time varies by business hours
+ rand((30.seconds)..(30.minutes))
+ else
+ rand((1.hour)..(8.hours))
+ end
+ end
+
+ def create_message(is_incoming)
+ if is_incoming
+ create_incoming_message
+ else
+ create_outgoing_message
+ end
+ end
+
+ def create_incoming_message
+ @conversation.messages.create!(
+ account: @account,
+ inbox: @conversation.inbox,
+ message_type: :incoming,
+ content: generate_message_content,
+ sender: @conversation.contact
+ )
+ end
+
+ def create_outgoing_message
+ sender = @conversation.assignee || @agents.sample
+
+ @conversation.messages.create!(
+ account: @account,
+ inbox: @conversation.inbox,
+ message_type: :outgoing,
+ content: generate_message_content,
+ sender: sender
+ )
+ end
+
+ def generate_message_content
+ Faker::Lorem.paragraph(sentence_count: rand(1..5))
+ end
+
+ def handle_agent_reply_events(message, is_first_reply)
+ if is_first_reply
+ trigger_first_reply_event(message)
+ else
+ trigger_reply_event(message)
+ end
+ end
+
+ def business_hours_active?(time)
+ weekday = time.wday
+ hour = time.hour
+ weekday.between?(1, 5) && hour.between?(9, 17)
+ end
+
+ def trigger_first_reply_event(message)
+ event_data = {
+ message: message,
+ conversation: message.conversation
+ }
+
+ ReportingEventListener.instance.first_reply_created(
+ Events::Base.new('first_reply_created', Time.current, event_data)
+ )
+ end
+
+ def trigger_reply_event(message)
+ waiting_since = calculate_waiting_since(message)
+
+ event_data = {
+ message: message,
+ conversation: message.conversation,
+ waiting_since: waiting_since
+ }
+
+ ReportingEventListener.instance.reply_created(
+ Events::Base.new('reply_created', Time.current, event_data)
+ )
+ end
+
+ def calculate_waiting_since(message)
+ last_customer_message = message.conversation.messages
+ .where(message_type: :incoming)
+ .where('created_at < ?', message.created_at)
+ .order(:created_at)
+ .last
+
+ last_customer_message&.created_at || message.conversation.created_at
+ end
+end
diff --git a/lib/seeders/reports/report_data_seeder.rb b/lib/seeders/reports/report_data_seeder.rb
new file mode 100644
index 000000000..909818b72
--- /dev/null
+++ b/lib/seeders/reports/report_data_seeder.rb
@@ -0,0 +1,234 @@
+# frozen_string_literal: true
+
+# Reports Data Seeder
+#
+# Generates realistic test data for performance testing of reports and analytics.
+# Creates conversations, messages, contacts, agents, teams, and labels with proper
+# reporting events (first response times, resolution times, etc.) using time travel
+# to generate historical data with realistic timestamps.
+#
+# Usage:
+# ACCOUNT_ID=1 ENABLE_ACCOUNT_SEEDING=true bundle exec rake db:seed:reports_data
+#
+# This will create:
+# - 1000 conversations with realistic message exchanges
+# - 100 contacts with realistic profiles
+# - 20 agents assigned to teams and inboxes
+# - 5 teams with realistic distribution
+# - 30 labels with random assignments
+# - 3 inboxes with agent assignments
+# - Realistic reporting events with historical timestamps
+#
+# Note: This seeder clears existing data for the account before seeding.
+
+require 'faker'
+require_relative 'conversation_creator'
+require_relative 'message_creator'
+
+# rubocop:disable Rails/Output
+class Seeders::Reports::ReportDataSeeder
+ include ActiveSupport::Testing::TimeHelpers
+
+ TOTAL_CONVERSATIONS = 1000
+ TOTAL_CONTACTS = 100
+ TOTAL_AGENTS = 20
+ TOTAL_TEAMS = 5
+ TOTAL_LABELS = 30
+ TOTAL_INBOXES = 3
+ MESSAGES_PER_CONVERSATION = 5
+ START_DATE = 3.months.ago # rubocop:disable Rails/RelativeDateConstant
+ END_DATE = Time.current
+
+ def initialize(account:)
+ raise 'Account Seeding is not allowed.' unless ENV.fetch('ENABLE_ACCOUNT_SEEDING', !Rails.env.production?)
+
+ @account = account
+ @teams = []
+ @agents = []
+ @labels = []
+ @inboxes = []
+ @contacts = []
+ end
+
+ def perform!
+ puts "Starting reports data seeding for account: #{@account.name}"
+
+ # Clear existing data
+ clear_existing_data
+
+ create_teams
+ create_agents
+ create_labels
+ create_inboxes
+ create_contacts
+
+ create_conversations
+
+ puts "Completed reports data seeding for account: #{@account.name}"
+ end
+
+ private
+
+ def clear_existing_data
+ puts "Clearing existing data for account: #{@account.id}"
+ @account.teams.destroy_all
+ @account.conversations.destroy_all
+ @account.labels.destroy_all
+ @account.inboxes.destroy_all
+ @account.contacts.destroy_all
+ @account.agents.destroy_all
+ @account.reporting_events.destroy_all
+ end
+
+ def create_teams
+ TOTAL_TEAMS.times do |i|
+ team = @account.teams.create!(
+ name: "#{Faker::Company.industry} Team #{i + 1}"
+ )
+ @teams << team
+ print "\rCreating teams: #{i + 1}/#{TOTAL_TEAMS}"
+ end
+
+ print "\n"
+ end
+
+ def create_agents
+ TOTAL_AGENTS.times do |i|
+ user = create_single_agent(i)
+ assign_agent_to_teams(user)
+ @agents << user
+ print "\rCreating agents: #{i + 1}/#{TOTAL_AGENTS}"
+ end
+
+ print "\n"
+ end
+
+ def create_single_agent(index)
+ random_suffix = SecureRandom.hex(4)
+ user = User.create!(
+ name: Faker::Name.name,
+ email: "agent_#{index + 1}_#{random_suffix}@#{@account.domain || 'example.com'}",
+ password: 'Password1!.',
+ confirmed_at: Time.current
+ )
+ user.skip_confirmation!
+ user.save!
+
+ AccountUser.create!(
+ account_id: @account.id,
+ user_id: user.id,
+ role: :agent
+ )
+
+ user
+ end
+
+ def assign_agent_to_teams(user)
+ teams_to_assign = @teams.sample(rand(1..3))
+ teams_to_assign.each do |team|
+ TeamMember.create!(
+ team_id: team.id,
+ user_id: user.id
+ )
+ end
+ end
+
+ def create_labels
+ TOTAL_LABELS.times do |i|
+ label = @account.labels.create!(
+ title: "Label-#{i + 1}-#{Faker::Lorem.word}",
+ description: Faker::Company.catch_phrase,
+ color: Faker::Color.hex_color
+ )
+ @labels << label
+ print "\rCreating labels: #{i + 1}/#{TOTAL_LABELS}"
+ end
+
+ print "\n"
+ end
+
+ def create_inboxes
+ TOTAL_INBOXES.times do |_i|
+ inbox = create_single_inbox
+ assign_agents_to_inbox(inbox)
+ @inboxes << inbox
+ print "\rCreating inboxes: #{@inboxes.size}/#{TOTAL_INBOXES}"
+ end
+
+ print "\n"
+ end
+
+ def create_single_inbox
+ channel = Channel::WebWidget.create!(
+ website_url: "https://#{Faker::Internet.domain_name}",
+ account_id: @account.id
+ )
+
+ @account.inboxes.create!(
+ name: "#{Faker::Company.name} Website",
+ channel: channel
+ )
+ end
+
+ def assign_agents_to_inbox(inbox)
+ agents_to_assign = if @inboxes.empty?
+ # First inbox gets all agents to ensure coverage
+ @agents
+ else
+ # Subsequent inboxes get random selection with some overlap
+ min_agents = [@agents.size / TOTAL_INBOXES, 10].max
+ max_agents = [(@agents.size * 0.8).to_i, 50].min
+ @agents.sample(rand(min_agents..max_agents))
+ end
+
+ agents_to_assign.each do |agent|
+ InboxMember.create!(inbox: inbox, user: agent)
+ end
+ end
+
+ def create_contacts
+ TOTAL_CONTACTS.times do |i|
+ contact = @account.contacts.create!(
+ name: Faker::Name.name,
+ email: Faker::Internet.email,
+ phone_number: Faker::PhoneNumber.cell_phone_in_e164,
+ identifier: SecureRandom.uuid,
+ additional_attributes: {
+ company: Faker::Company.name,
+ city: Faker::Address.city,
+ country: Faker::Address.country,
+ customer_since: Faker::Date.between(from: 2.years.ago, to: Time.zone.today)
+ }
+ )
+ @contacts << contact
+
+ print "\rCreating contacts: #{i + 1}/#{TOTAL_CONTACTS}"
+ end
+
+ print "\n"
+ end
+
+ def create_conversations
+ conversation_creator = Seeders::Reports::ConversationCreator.new(
+ account: @account,
+ resources: {
+ contacts: @contacts,
+ inboxes: @inboxes,
+ teams: @teams,
+ labels: @labels,
+ agents: @agents
+ }
+ )
+
+ TOTAL_CONVERSATIONS.times do |i|
+ created_at = Faker::Time.between(from: START_DATE, to: END_DATE)
+ conversation_creator.create_conversation(created_at: created_at)
+
+ completion_percentage = ((i + 1).to_f / TOTAL_CONVERSATIONS * 100).round
+ print "\rCreating conversations: #{i + 1}/#{TOTAL_CONVERSATIONS} (#{completion_percentage}%)"
+ end
+
+ print "\n"
+ end
+end
+# rubocop:enable Rails/Output
diff --git a/lib/tasks/seed_reports_data.rake b/lib/tasks/seed_reports_data.rake
new file mode 100644
index 000000000..5c80f1e51
--- /dev/null
+++ b/lib/tasks/seed_reports_data.rake
@@ -0,0 +1,24 @@
+namespace :db do
+ namespace :seed do
+ desc 'Seed test data for reports with conversations, contacts, agents, teams, and realistic reporting events'
+ task reports_data: :environment do
+ if ENV['ACCOUNT_ID'].blank?
+ puts 'Please provide an ACCOUNT_ID environment variable'
+ puts 'Usage: ACCOUNT_ID=1 ENABLE_ACCOUNT_SEEDING=true bundle exec rake db:seed:reports_data'
+ exit 1
+ end
+
+ ENV['ENABLE_ACCOUNT_SEEDING'] = 'true' if ENV['ENABLE_ACCOUNT_SEEDING'].blank?
+
+ account_id = ENV.fetch('ACCOUNT_ID', nil)
+ account = Account.find(account_id)
+
+ puts "Starting reports data seeding for account: #{account.name} (ID: #{account.id})"
+
+ seeder = Seeders::Reports::ReportDataSeeder.new(account: account)
+ seeder.perform!
+
+ puts "Finished seeding reports data for account: #{account.name}"
+ end
+ end
+end
diff --git a/spec/builders/v2/reports/label_summary_builder_spec.rb b/spec/builders/v2/reports/label_summary_builder_spec.rb
new file mode 100644
index 000000000..1560008a1
--- /dev/null
+++ b/spec/builders/v2/reports/label_summary_builder_spec.rb
@@ -0,0 +1,317 @@
+require 'rails_helper'
+
+RSpec.describe V2::Reports::LabelSummaryBuilder do
+ include ActiveJob::TestHelper
+
+ let_it_be(:account) { create(:account) }
+ let_it_be(:label_1) { create(:label, title: 'label_1', account: account) }
+ let_it_be(:label_2) { create(:label, title: 'label_2', account: account) }
+ let_it_be(:label_3) { create(:label, title: 'label_3', account: account) }
+
+ let(:params) do
+ {
+ business_hours: business_hours,
+ since: (Time.zone.today - 3.days).to_time.to_i.to_s,
+ until: Time.zone.today.end_of_day.to_time.to_i.to_s,
+ timezone_offset: 0
+ }
+ end
+ let(:builder) { described_class.new(account: account, params: params) }
+
+ describe '#initialize' do
+ let(:business_hours) { false }
+
+ it 'sets account and params' do
+ expect(builder.account).to eq(account)
+ expect(builder.params).to eq(params)
+ end
+
+ it 'sets timezone from timezone_offset' do
+ builder_with_offset = described_class.new(account: account, params: { timezone_offset: -8 })
+ expect(builder_with_offset.instance_variable_get(:@timezone)).to eq('Pacific Time (US & Canada)')
+ end
+
+ it 'defaults timezone when timezone_offset is not provided' do
+ builder_without_offset = described_class.new(account: account, params: {})
+ expect(builder_without_offset.instance_variable_get(:@timezone)).not_to be_nil
+ end
+ end
+
+ describe '#build' do
+ context 'when there are no labels' do
+ let(:business_hours) { false }
+ let(:empty_account) { create(:account) }
+ let(:empty_builder) { described_class.new(account: empty_account, params: params) }
+
+ it 'returns empty array' do
+ expect(empty_builder.build).to eq([])
+ end
+ end
+
+ context 'when there are labels but no conversations' do
+ let(:business_hours) { false }
+
+ it 'returns zero values for all labels' do
+ report = builder.build
+
+ expect(report.length).to eq(3)
+
+ bug_report = report.find { |r| r[:name] == 'label_1' }
+ feature_request = report.find { |r| r[:name] == 'label_2' }
+ customer_support = report.find { |r| r[:name] == 'label_3' }
+
+ [
+ [bug_report, label_1, 'label_1'],
+ [feature_request, label_2, 'label_2'],
+ [customer_support, label_3, 'label_3']
+ ].each do |report_data, label, label_name|
+ expect(report_data).to include(
+ id: label.id,
+ name: label_name,
+ conversations_count: 0,
+ avg_resolution_time: 0,
+ avg_first_response_time: 0,
+ avg_reply_time: 0,
+ resolved_conversations_count: 0
+ )
+ end
+ end
+ end
+
+ context 'when there are labeled conversations with metrics' do
+ before do
+ travel_to(Time.zone.today) do
+ user = create(:user, account: account)
+ inbox = create(:inbox, account: account)
+ create(:inbox_member, user: user, inbox: inbox)
+
+ gravatar_url = 'https://www.gravatar.com'
+ stub_request(:get, /#{gravatar_url}.*/).to_return(status: 404)
+
+ perform_enqueued_jobs do
+ # Create conversations with label_1
+ 3.times do
+ conversation = create(:conversation, account: account,
+ inbox: inbox, assignee: user,
+ created_at: Time.zone.today)
+ create_list(:message, 2, message_type: 'outgoing',
+ account: account, inbox: inbox,
+ conversation: conversation,
+ created_at: Time.zone.today + 1.hour)
+ create_list(:message, 1, message_type: 'incoming',
+ account: account, inbox: inbox,
+ conversation: conversation,
+ created_at: Time.zone.today + 2.hours)
+ conversation.update_labels('label_1')
+ conversation.label_list
+ conversation.save!
+ end
+
+ # Create conversations with label_2
+ 2.times do
+ conversation = create(:conversation, account: account,
+ inbox: inbox, assignee: user,
+ created_at: Time.zone.today)
+ create_list(:message, 1, message_type: 'outgoing',
+ account: account, inbox: inbox,
+ conversation: conversation,
+ created_at: Time.zone.today + 1.hour)
+ conversation.update_labels('label_2')
+ conversation.label_list
+ conversation.save!
+ end
+
+ # Resolve some conversations
+ conversations_to_resolve = account.conversations.first(2)
+ conversations_to_resolve.each(&:toggle_status)
+
+ # Create some reporting events
+ account.conversations.reload.each_with_index do |conv, idx|
+ # First response times
+ create(:reporting_event,
+ account: account,
+ conversation: conv,
+ name: 'first_response',
+ value: (30 + (idx * 10)) * 60,
+ value_in_business_hours: (20 + (idx * 5)) * 60,
+ created_at: Time.zone.today)
+
+ # Reply times
+ create(:reporting_event,
+ account: account,
+ conversation: conv,
+ name: 'reply_time',
+ value: (15 + (idx * 5)) * 60,
+ value_in_business_hours: (10 + (idx * 3)) * 60,
+ created_at: Time.zone.today)
+
+ # Resolution times for resolved conversations
+ next unless conv.resolved?
+
+ create(:reporting_event,
+ account: account,
+ conversation: conv,
+ name: 'conversation_resolved',
+ value: (60 + (idx * 30)) * 60,
+ value_in_business_hours: (45 + (idx * 20)) * 60,
+ created_at: Time.zone.today)
+ end
+ end
+ end
+ end
+
+ context 'when business hours is disabled' do
+ let(:business_hours) { false }
+
+ it 'returns correct label stats using regular values' do
+ report = builder.build
+
+ expect(report.length).to eq(3)
+
+ label_1_report = report.find { |r| r[:name] == 'label_1' }
+ label_2_report = report.find { |r| r[:name] == 'label_2' }
+ label_3_report = report.find { |r| r[:name] == 'label_3' }
+
+ expect(label_1_report).to include(
+ conversations_count: 3,
+ avg_first_response_time: be > 0,
+ avg_reply_time: be > 0
+ )
+
+ expect(label_2_report).to include(
+ conversations_count: 2,
+ avg_first_response_time: be > 0,
+ avg_reply_time: be > 0
+ )
+
+ expect(label_3_report).to include(
+ conversations_count: 0,
+ avg_first_response_time: 0,
+ avg_reply_time: 0
+ )
+ end
+ end
+
+ context 'when business hours is enabled' do
+ let(:business_hours) { true }
+
+ it 'returns correct label stats using business hours values' do
+ report = builder.build
+
+ expect(report.length).to eq(3)
+
+ label_1_report = report.find { |r| r[:name] == 'label_1' }
+ label_2_report = report.find { |r| r[:name] == 'label_2' }
+
+ expect(label_1_report[:conversations_count]).to eq(3)
+ expect(label_1_report[:avg_first_response_time]).to be > 0
+ expect(label_1_report[:avg_reply_time]).to be > 0
+
+ expect(label_2_report[:conversations_count]).to eq(2)
+ expect(label_2_report[:avg_first_response_time]).to be > 0
+ expect(label_2_report[:avg_reply_time]).to be > 0
+ end
+ end
+ end
+
+ context 'when filtering by date range' do
+ let(:business_hours) { false }
+
+ before do
+ travel_to(Time.zone.today) do
+ user = create(:user, account: account)
+ inbox = create(:inbox, account: account)
+ create(:inbox_member, user: user, inbox: inbox)
+
+ gravatar_url = 'https://www.gravatar.com'
+ stub_request(:get, /#{gravatar_url}.*/).to_return(status: 404)
+
+ perform_enqueued_jobs do
+ # Conversation within range
+ conversation_in_range = create(:conversation, account: account,
+ inbox: inbox, assignee: user,
+ created_at: 2.days.ago)
+ conversation_in_range.update_labels('label_1')
+ conversation_in_range.label_list
+ conversation_in_range.save!
+
+ create(:reporting_event,
+ account: account,
+ conversation: conversation_in_range,
+ name: 'first_response',
+ value: 1800,
+ created_at: 2.days.ago)
+
+ # Conversation outside range (too old)
+ conversation_out_of_range = create(:conversation, account: account,
+ inbox: inbox, assignee: user,
+ created_at: 1.week.ago)
+ conversation_out_of_range.update_labels('label_1')
+ conversation_out_of_range.label_list
+ conversation_out_of_range.save!
+
+ create(:reporting_event,
+ account: account,
+ conversation: conversation_out_of_range,
+ name: 'first_response',
+ value: 3600,
+ created_at: 1.week.ago)
+ end
+ end
+ end
+
+ it 'only includes conversations within the date range' do
+ report = builder.build
+
+ expect(report.length).to eq(3)
+
+ label_1_report = report.find { |r| r[:name] == 'label_1' }
+ expect(label_1_report).not_to be_nil
+ expect(label_1_report[:conversations_count]).to eq(1)
+ expect(label_1_report[:avg_first_response_time]).to eq(1800.0)
+ end
+ end
+
+ context 'with business hours parameter' do
+ let(:business_hours) { 'true' }
+
+ before do
+ travel_to(Time.zone.today) do
+ user = create(:user, account: account)
+ inbox = create(:inbox, account: account)
+ create(:inbox_member, user: user, inbox: inbox)
+
+ gravatar_url = 'https://www.gravatar.com'
+ stub_request(:get, /#{gravatar_url}.*/).to_return(status: 404)
+
+ perform_enqueued_jobs do
+ conversation = create(:conversation, account: account,
+ inbox: inbox, assignee: user,
+ created_at: Time.zone.today)
+ conversation.update_labels('label_1')
+ conversation.label_list
+ conversation.save!
+
+ create(:reporting_event,
+ account: account,
+ conversation: conversation,
+ name: 'first_response',
+ value: 3600,
+ value_in_business_hours: 1800,
+ created_at: Time.zone.today)
+ end
+ end
+ end
+
+ it 'properly casts string "true" to boolean and uses business hours values' do
+ report = builder.build
+
+ expect(report.length).to eq(3)
+
+ label_1_report = report.find { |r| r[:name] == 'label_1' }
+ expect(label_1_report).not_to be_nil
+ expect(label_1_report[:avg_first_response_time]).to eq(1800.0)
+ end
+ end
+ end
+end
diff --git a/spec/controllers/api/v1/accounts/integrations/linear_controller_spec.rb b/spec/controllers/api/v1/accounts/integrations/linear_controller_spec.rb
index f2995c3eb..851c5dbaf 100644
--- a/spec/controllers/api/v1/accounts/integrations/linear_controller_spec.rb
+++ b/spec/controllers/api/v1/accounts/integrations/linear_controller_spec.rb
@@ -93,6 +93,8 @@ RSpec.describe 'Linear Integration API', type: :request do
end
describe 'POST /api/v1/accounts/:account_id/integrations/linear/create_issue' do
+ let(:inbox) { create(:inbox, account: account) }
+ let(:conversation) { create(:conversation, account: account, inbox: inbox) }
let(:issue_params) do
{
team_id: 'team1',
@@ -101,32 +103,56 @@ RSpec.describe 'Linear Integration API', type: :request do
assignee_id: 'user1',
priority: 'high',
state_id: 'state1',
- label_ids: ['label1']
+ label_ids: ['label1'],
+ conversation_id: conversation.display_id
}
end
context 'when it is an authenticated user' do
context 'when the issue is created successfully' do
- let(:created_issue) { { data: { 'id' => 'issue1', 'title' => 'Sample Issue' } } }
+ let(:created_issue) { { data: { identifier: 'ENG-123', title: 'Sample Issue' } } }
it 'returns the created issue' do
- allow(processor_service).to receive(:create_issue).with(kind_of(ActionController::Parameters)).and_return(created_issue)
+ allow(processor_service).to receive(:create_issue).with(issue_params.stringify_keys).and_return(created_issue)
+
post "/api/v1/accounts/#{account.id}/integrations/linear/create_issue",
params: issue_params,
headers: agent.create_new_auth_token,
as: :json
+
expect(response).to have_http_status(:ok)
expect(response.body).to include('Sample Issue')
end
+
+ it 'creates activity message when conversation is provided' do
+ allow(processor_service).to receive(:create_issue).with(issue_params.stringify_keys).and_return(created_issue)
+
+ expect do
+ post "/api/v1/accounts/#{account.id}/integrations/linear/create_issue",
+ params: issue_params,
+ headers: agent.create_new_auth_token,
+ as: :json
+ end.to have_enqueued_job(Conversations::ActivityMessageJob)
+ .with(conversation, {
+ account_id: conversation.account_id,
+ inbox_id: conversation.inbox_id,
+ message_type: :activity,
+ content: "Linear issue ENG-123 was created by #{agent.name}"
+ })
+ end
end
context 'when issue creation fails' do
- it 'returns error message' do
- allow(processor_service).to receive(:create_issue).with(kind_of(ActionController::Parameters)).and_return(error: 'error message')
- post "/api/v1/accounts/#{account.id}/integrations/linear/create_issue",
- params: issue_params,
- headers: agent.create_new_auth_token,
- as: :json
+ it 'returns error message and does not create activity message' do
+ allow(processor_service).to receive(:create_issue).with(issue_params.stringify_keys).and_return(error: 'error message')
+
+ expect do
+ post "/api/v1/accounts/#{account.id}/integrations/linear/create_issue",
+ params: issue_params,
+ headers: agent.create_new_auth_token,
+ as: :json
+ end.not_to have_enqueued_job(Conversations::ActivityMessageJob)
+
expect(response).to have_http_status(:unprocessable_entity)
expect(response.body).to include('error message')
end
@@ -135,7 +161,7 @@ RSpec.describe 'Linear Integration API', type: :request do
end
describe 'POST /api/v1/accounts/:account_id/integrations/linear/link_issue' do
- let(:issue_id) { 'issue1' }
+ let(:issue_id) { 'ENG-456' }
let(:conversation) { create(:conversation, account: account) }
let(:link) { "#{ENV.fetch('FRONTEND_URL', nil)}/app/accounts/#{account.id}/conversations/#{conversation.display_id}" }
let(:title) { 'Sample Issue' }
@@ -144,24 +170,38 @@ RSpec.describe 'Linear Integration API', type: :request do
context 'when the issue is linked successfully' do
let(:linked_issue) { { data: { 'id' => 'issue1', 'link' => 'https://linear.app/issue1' } } }
- it 'returns the linked issue' do
+ it 'returns the linked issue and creates activity message' do
allow(processor_service).to receive(:link_issue).with(link, issue_id, title).and_return(linked_issue)
- post "/api/v1/accounts/#{account.id}/integrations/linear/link_issue",
- params: { conversation_id: conversation.display_id, issue_id: issue_id, title: title },
- headers: agent.create_new_auth_token,
- as: :json
+
+ expect do
+ post "/api/v1/accounts/#{account.id}/integrations/linear/link_issue",
+ params: { conversation_id: conversation.display_id, issue_id: issue_id, title: title },
+ headers: agent.create_new_auth_token,
+ as: :json
+ end.to have_enqueued_job(Conversations::ActivityMessageJob)
+ .with(conversation, {
+ account_id: conversation.account_id,
+ inbox_id: conversation.inbox_id,
+ message_type: :activity,
+ content: "Linear issue ENG-456 was linked by #{agent.name}"
+ })
+
expect(response).to have_http_status(:ok)
expect(response.body).to include('https://linear.app/issue1')
end
end
context 'when issue linking fails' do
- it 'returns error message' do
+ it 'returns error message and does not create activity message' do
allow(processor_service).to receive(:link_issue).with(link, issue_id, title).and_return(error: 'error message')
- post "/api/v1/accounts/#{account.id}/integrations/linear/link_issue",
- params: { conversation_id: conversation.display_id, issue_id: issue_id, title: title },
- headers: agent.create_new_auth_token,
- as: :json
+
+ expect do
+ post "/api/v1/accounts/#{account.id}/integrations/linear/link_issue",
+ params: { conversation_id: conversation.display_id, issue_id: issue_id, title: title },
+ headers: agent.create_new_auth_token,
+ as: :json
+ end.not_to have_enqueued_job(Conversations::ActivityMessageJob)
+
expect(response).to have_http_status(:unprocessable_entity)
expect(response.body).to include('error message')
end
@@ -171,29 +211,45 @@ RSpec.describe 'Linear Integration API', type: :request do
describe 'POST /api/v1/accounts/:account_id/integrations/linear/unlink_issue' do
let(:link_id) { 'attachment1' }
+ let(:issue_id) { 'ENG-789' }
+ let(:conversation) { create(:conversation, account: account) }
context 'when it is an authenticated user' do
context 'when the issue is unlinked successfully' do
let(:unlinked_issue) { { data: { 'id' => 'issue1', 'link' => 'https://linear.app/issue1' } } }
- it 'returns the unlinked issue' do
+ it 'returns the unlinked issue and creates activity message' do
allow(processor_service).to receive(:unlink_issue).with(link_id).and_return(unlinked_issue)
- post "/api/v1/accounts/#{account.id}/integrations/linear/unlink_issue",
- params: { link_id: link_id },
- headers: agent.create_new_auth_token,
- as: :json
+
+ expect do
+ post "/api/v1/accounts/#{account.id}/integrations/linear/unlink_issue",
+ params: { link_id: link_id, issue_id: issue_id, conversation_id: conversation.display_id },
+ headers: agent.create_new_auth_token,
+ as: :json
+ end.to have_enqueued_job(Conversations::ActivityMessageJob)
+ .with(conversation, {
+ account_id: conversation.account_id,
+ inbox_id: conversation.inbox_id,
+ message_type: :activity,
+ content: "Linear issue ENG-789 was unlinked by #{agent.name}"
+ })
+
expect(response).to have_http_status(:ok)
expect(response.body).to include('https://linear.app/issue1')
end
end
context 'when issue unlinking fails' do
- it 'returns error message' do
+ it 'returns error message and does not create activity message' do
allow(processor_service).to receive(:unlink_issue).with(link_id).and_return(error: 'error message')
- post "/api/v1/accounts/#{account.id}/integrations/linear/unlink_issue",
- params: { link_id: link_id },
- headers: agent.create_new_auth_token,
- as: :json
+
+ expect do
+ post "/api/v1/accounts/#{account.id}/integrations/linear/unlink_issue",
+ params: { link_id: link_id, issue_id: issue_id, conversation_id: conversation.display_id },
+ headers: agent.create_new_auth_token,
+ as: :json
+ end.not_to have_enqueued_job(Conversations::ActivityMessageJob)
+
expect(response).to have_http_status(:unprocessable_entity)
expect(response.body).to include('error message')
end
diff --git a/spec/controllers/api/v1/profiles_controller_spec.rb b/spec/controllers/api/v1/profiles_controller_spec.rb
index 8af9e30c0..30c5b69f4 100644
--- a/spec/controllers/api/v1/profiles_controller_spec.rb
+++ b/spec/controllers/api/v1/profiles_controller_spec.rb
@@ -94,6 +94,18 @@ RSpec.describe 'Profile API', type: :request do
expect(agent.reload.valid_password?('Test1234!')).to be true
end
+ it 'does not reset the display name if updates the password' do
+ display_name = agent.display_name
+
+ put '/api/v1/profile',
+ params: { profile: { current_password: 'Test123!', password: 'Test1234!', password_confirmation: 'Test1234!' } },
+ headers: agent.create_new_auth_token,
+ as: :json
+
+ expect(response).to have_http_status(:success)
+ expect(agent.reload.display_name).to eq(display_name)
+ end
+
it 'throws error when current password provided is invalid' do
put '/api/v1/profile',
params: { profile: { current_password: 'Test', password: 'test123', password_confirmation: 'test123' } },
diff --git a/spec/controllers/super_admin/agent_bots_controller_spec.rb b/spec/controllers/super_admin/agent_bots_controller_spec.rb
index 4c514c0f0..276d9896f 100644
--- a/spec/controllers/super_admin/agent_bots_controller_spec.rb
+++ b/spec/controllers/super_admin/agent_bots_controller_spec.rb
@@ -43,4 +43,25 @@ RSpec.describe 'Super Admin agent-bots API', type: :request do
end
end
end
+
+ describe 'DELETE /super_admin/agent_bots/:id' do
+ let!(:agent_bot) { create(:agent_bot) }
+
+ context 'when it is an unauthenticated super admin' do
+ it 'returns unauthorized' do
+ delete "/super_admin/agent_bots/#{agent_bot.id}"
+ expect(response).to have_http_status(:redirect)
+ expect(AgentBot.find_by(id: agent_bot.id)).to be_present
+ end
+ end
+
+ context 'when it is an authenticated super admin' do
+ it 'deletes the agent bot' do
+ sign_in(super_admin, scope: :super_admin)
+ delete "/super_admin/agent_bots/#{agent_bot.id}"
+ expect(response).to have_http_status(:redirect)
+ expect(AgentBot.find_by(id: agent_bot.id)).to be_nil
+ end
+ end
+ end
end
diff --git a/spec/jobs/webhooks/instagram_events_job_spec.rb b/spec/jobs/webhooks/instagram_events_job_spec.rb
index 23e7f9e5a..dfd9f1aed 100644
--- a/spec/jobs/webhooks/instagram_events_job_spec.rb
+++ b/spec/jobs/webhooks/instagram_events_job_spec.rb
@@ -234,16 +234,15 @@ describe Webhooks::InstagramEventsJob do
account_id: instagram_inbox.account_id
)
- instagram_inbox.reload
-
expect(instagram_inbox.messages.count).to be 1
instagram_webhook.perform_now(message_events[:unsend][:entry])
- expect(instagram_inbox.messages.last.content).to eq 'This message was deleted'
- expect(instagram_inbox.messages.last.deleted).to be true
- expect(instagram_inbox.messages.last.attachments.count).to be 0
- expect(instagram_inbox.messages.last.reload.deleted).to be true
+ message.reload
+
+ expect(message.content).to eq 'This message was deleted'
+ expect(message.deleted).to be true
+ expect(message.attachments.count).to be 0
end
it 'creates incoming message with attachments in the instagram direct inbox' do
diff --git a/spec/lib/integrations/linear/processor_service_spec.rb b/spec/lib/integrations/linear/processor_service_spec.rb
index 807e93c71..8830e658e 100644
--- a/spec/lib/integrations/linear/processor_service_spec.rb
+++ b/spec/lib/integrations/linear/processor_service_spec.rb
@@ -82,15 +82,27 @@ describe Integrations::Linear::ProcessorService do
end
let(:issue_response) do
{
- 'issueCreate' => { 'issue' => { 'id' => 'issue1', 'title' => 'Issue title' } }
+ 'issueCreate' => {
+ 'issue' => {
+ 'id' => 'issue1',
+ 'title' => 'Issue title',
+ 'identifier' => 'ENG-123'
+ }
+ }
}
end
context 'when Linear client returns valid data' do
- it 'returns parsed issue data' do
+ it 'returns parsed issue data with identifier' do
allow(linear_client).to receive(:create_issue).with(params).and_return(issue_response)
result = service.create_issue(params)
- expect(result).to eq({ data: { id: 'issue1', title: 'Issue title' } })
+ expect(result).to eq({
+ data: {
+ id: 'issue1',
+ title: 'Issue title',
+ identifier: 'ENG-123'
+ }
+ })
end
end
@@ -133,13 +145,13 @@ describe Integrations::Linear::ProcessorService do
describe '#unlink_issue' do
let(:link_id) { 'attachment1' }
- let(:unlink_response) { { data: { link_id: link_id } } }
+ let(:linear_client_response) { { success: true } }
context 'when Linear client returns valid data' do
- it 'returns parsed unlink data' do
- allow(linear_client).to receive(:unlink_issue).with(link_id).and_return(unlink_response)
+ it 'returns unlink data with link_id' do
+ allow(linear_client).to receive(:unlink_issue).with(link_id).and_return(linear_client_response)
result = service.unlink_issue(link_id)
- expect(result).to eq(unlink_response)
+ expect(result).to eq({ data: { link_id: link_id } })
end
end
@@ -207,4 +219,59 @@ describe Integrations::Linear::ProcessorService do
end
end
end
+
+ # Tests specifically for activity message integration
+ describe 'activity message data compatibility' do
+ let(:linear_client_response) { { success: true } }
+
+ describe '#create_issue' do
+ it 'includes identifier field needed for activity messages' do
+ params = { title: 'Test Issue', team_id: 'team1' }
+ response = {
+ 'issueCreate' => {
+ 'issue' => {
+ 'id' => 'internal_id_123',
+ 'title' => 'Test Issue',
+ 'identifier' => 'ENG-456'
+ }
+ }
+ }
+
+ allow(linear_client).to receive(:create_issue).with(params).and_return(response)
+ result = service.create_issue(params)
+
+ expect(result[:data]).to have_key(:identifier)
+ expect(result[:data][:identifier]).to eq('ENG-456')
+ end
+ end
+
+ describe '#link_issue' do
+ it 'returns issue_id in response for activity messages' do
+ link = 'https://example.com'
+ issue_id = 'ENG-789'
+ title = 'Test Issue'
+ response = {
+ 'attachmentLinkURL' => {
+ 'attachment' => { 'id' => 'attachment123' }
+ }
+ }
+
+ allow(linear_client).to receive(:link_issue).with(link, issue_id, title).and_return(response)
+ result = service.link_issue(link, issue_id, title)
+
+ expect(result[:data][:id]).to eq(issue_id)
+ end
+ end
+
+ describe '#unlink_issue' do
+ it 'returns structured data for activity messages' do
+ link_id = 'attachment456'
+
+ allow(linear_client).to receive(:unlink_issue).with(link_id).and_return(linear_client_response)
+ result = service.unlink_issue(link_id)
+
+ expect(result).to eq({ data: { link_id: link_id } })
+ end
+ end
+ end
end
diff --git a/spec/listeners/csat_survey_listener_spec.rb b/spec/listeners/csat_survey_listener_spec.rb
index 9161c4e71..0f957dcb4 100644
--- a/spec/listeners/csat_survey_listener_spec.rb
+++ b/spec/listeners/csat_survey_listener_spec.rb
@@ -13,6 +13,8 @@ describe CsatSurveyListener do
)
end
let!(:event) { Events::Base.new(event_name, Time.zone.now, message: message) }
+ let!(:csat_enabled_inbox) { create(:inbox, account: account, csat_survey_enabled: true) }
+ let!(:resolved_conversation) { create(:conversation, account: account, inbox: csat_enabled_inbox, status: :resolved) }
describe '#message_updated' do
let(:event_name) { 'message.updated' }
@@ -33,4 +35,33 @@ describe CsatSurveyListener do
end
end
end
+
+ describe '#conversation_status_changed' do
+ let(:event_name) { 'conversation.status_changed' }
+ let(:csat_service) { double }
+
+ before do
+ allow(resolved_conversation).to receive(:saved_change_to_status?).and_return(true)
+ end
+
+ context 'when conversation is resolved and CSAT is enabled' do
+ it 'triggers CSAT survey service' do
+ event = Events::Base.new(event_name, Time.zone.now, conversation: resolved_conversation)
+ expect(csat_service).to receive(:perform)
+ expect(CsatSurveyService).to receive(:new).with(conversation: resolved_conversation).and_return(csat_service)
+
+ listener.conversation_status_changed(event)
+ end
+ end
+
+ context 'when conversation is not resolved' do
+ it 'does not trigger CSAT survey service' do
+ open_conversation = create(:conversation, account: account, inbox: csat_enabled_inbox, status: :open)
+ event = Events::Base.new(event_name, Time.zone.now, conversation: open_conversation)
+
+ expect(CsatSurveyService).not_to receive(:new)
+ listener.conversation_status_changed(event)
+ end
+ end
+ end
end
diff --git a/spec/models/channel/telegram_spec.rb b/spec/models/channel/telegram_spec.rb
index 17aa848f6..dc401a165 100644
--- a/spec/models/channel/telegram_spec.rb
+++ b/spec/models/channel/telegram_spec.rb
@@ -114,6 +114,24 @@ RSpec.describe Channel::Telegram do
expect(telegram_channel.send_message_on_telegram(message)).to eq('telegram_123')
end
+ it 'sends message with business_connection_id' do
+ additional_attributes = { 'chat_id' => '123', 'business_connection_id' => 'eooW3KF5WB5HxTD7T826' }
+ message = create(:message, message_type: :outgoing, content: 'test',
+ conversation: create(:conversation, inbox: telegram_channel.inbox, additional_attributes: additional_attributes))
+
+ stub_request(:post, "https://api.telegram.org/bot#{telegram_channel.bot_token}/sendMessage")
+ .with(
+ body: 'chat_id=123&text=test&reply_markup=&parse_mode=HTML&reply_to_message_id=&business_connection_id=eooW3KF5WB5HxTD7T826'
+ )
+ .to_return(
+ status: 200,
+ body: { result: { message_id: 'telegram_123' } }.to_json,
+ headers: { 'Content-Type' => 'application/json' }
+ )
+
+ expect(telegram_channel.send_message_on_telegram(message)).to eq('telegram_123')
+ end
+
it 'send text message failed' do
message = create(:message, message_type: :outgoing, content: 'test',
conversation: create(:conversation, inbox: telegram_channel.inbox, additional_attributes: { 'chat_id' => '123' }))
diff --git a/spec/models/message_spec.rb b/spec/models/message_spec.rb
index b4be3ac5a..3bf64a231 100644
--- a/spec/models/message_spec.rb
+++ b/spec/models/message_spec.rb
@@ -478,32 +478,57 @@ RSpec.describe Message do
describe '#content' do
let(:conversation) { create(:conversation) }
- let(:message) { create(:message, conversation: conversation, content_type: 'input_csat', content: 'Original content') }
- it 'returns original content for web widget inbox' do
- allow(message.inbox).to receive(:web_widget?).and_return(true)
- expect(message.content).to eq('Original content')
+ context 'when message is not input_csat' do
+ let(:message) { create(:message, conversation: conversation, content_type: 'text', content: 'Regular message') }
+
+ it 'returns original content' do
+ expect(message.content).to eq('Regular message')
+ end
end
- context 'when inbox is not a web widget' do
- before do
- allow(message.inbox).to receive(:web_widget?).and_return(false)
- allow(ENV).to receive(:fetch).with('FRONTEND_URL', nil).and_return('https://app.chatwoot.com')
+ context 'when message is input_csat' do
+ let(:message) { create(:message, conversation: conversation, content_type: 'input_csat', content: 'Rate your experience') }
+
+ context 'when inbox is web widget' do
+ before do
+ allow(message.inbox).to receive(:web_widget?).and_return(true)
+ end
+
+ it 'returns original content without survey URL' do
+ expect(message.content).to eq('Rate your experience')
+ end
end
- it 'returns custom message with survey link when csat message is configured' do
- allow(message.inbox).to receive(:csat_config).and_return({ 'message' => 'Custom survey message:' })
- expected_content = "Custom survey message: https://app.chatwoot.com/survey/responses/#{conversation.uuid}"
- expect(message.content).to eq(expected_content)
- end
+ context 'when inbox is not web widget' do
+ before do
+ allow(message.inbox).to receive(:web_widget?).and_return(false)
+ end
- it 'returns default message with survey link when no custom csat message' do
- allow(message.inbox).to receive(:csat_config).and_return(nil)
- allow(I18n).to receive(:t).with('conversations.survey.response', link: "https://app.chatwoot.com/survey/responses/#{conversation.uuid}")
- .and_return("Please rate your conversation: https://app.chatwoot.com/survey/responses/#{conversation.uuid}")
- expected_content = "Please rate your conversation: https://app.chatwoot.com/survey/responses/#{conversation.uuid}"
- expect(message.content).to eq(expected_content)
+ it 'returns only the stored content (clean for dashboard)' do
+ expect(message.content).to eq('Rate your experience')
+ end
+
+ it 'returns only the base content without URL when survey_url stored separately' do
+ message.content_attributes = { 'survey_url' => 'https://app.chatwoot.com/survey/responses/12345' }
+ expect(message.content).to eq('Rate your experience')
+ end
end
end
end
+
+ describe '#outgoing_content' do
+ let(:conversation) { create(:conversation) }
+ let(:message) { create(:message, conversation: conversation, content_type: 'text', content: 'Regular message') }
+
+ it 'delegates to MessageContentPresenter' do
+ presenter = instance_double(MessageContentPresenter)
+ allow(MessageContentPresenter).to receive(:new).with(message).and_return(presenter)
+ allow(presenter).to receive(:outgoing_content).and_return('Presented content')
+
+ expect(message.outgoing_content).to eq('Presented content')
+ expect(MessageContentPresenter).to have_received(:new).with(message)
+ expect(presenter).to have_received(:outgoing_content)
+ end
+ end
end
diff --git a/spec/presenters/message_content_presenter_spec.rb b/spec/presenters/message_content_presenter_spec.rb
new file mode 100644
index 000000000..b1be37ef2
--- /dev/null
+++ b/spec/presenters/message_content_presenter_spec.rb
@@ -0,0 +1,65 @@
+require 'rails_helper'
+
+RSpec.describe MessageContentPresenter do
+ let(:conversation) { create(:conversation) }
+ let(:message) { create(:message, conversation: conversation, content_type: content_type, content: content) }
+ let(:presenter) { described_class.new(message) }
+
+ describe '#outgoing_content' do
+ context 'when message is not input_csat' do
+ let(:content_type) { 'text' }
+ let(:content) { 'Regular message' }
+
+ it 'returns regular content' do
+ expect(presenter.outgoing_content).to eq('Regular message')
+ end
+ end
+
+ context 'when message is input_csat and inbox is web widget' do
+ let(:content_type) { 'input_csat' }
+ let(:content) { 'Rate your experience' }
+
+ before do
+ allow(message.inbox).to receive(:web_widget?).and_return(true)
+ end
+
+ it 'returns regular content without survey URL' do
+ expect(presenter.outgoing_content).to eq('Rate your experience')
+ end
+ end
+
+ context 'when message is input_csat and inbox is not web widget' do
+ let(:content_type) { 'input_csat' }
+ let(:content) { 'Rate your experience' }
+
+ before do
+ allow(message.inbox).to receive(:web_widget?).and_return(false)
+ allow(ENV).to receive(:fetch).with('FRONTEND_URL', nil).and_return('https://app.chatwoot.com')
+ end
+
+ it 'returns I18n default message when no CSAT config and dynamically generates survey URL' do
+ expected_url = "https://app.chatwoot.com/survey/responses/#{conversation.uuid}"
+ allow(I18n).to receive(:t).with('conversations.survey.response', link: expected_url)
+ .and_return("Please rate this conversation, #{expected_url}")
+ expect(presenter.outgoing_content).to eq("Please rate this conversation, #{expected_url}")
+ end
+
+ it 'returns CSAT config message when config exists and dynamically generates survey URL' do
+ allow(message.inbox).to receive(:csat_config).and_return({ 'message' => 'Custom CSAT message' })
+ expected_url = "https://app.chatwoot.com/survey/responses/#{conversation.uuid}"
+ expect(presenter.outgoing_content).to eq("Custom CSAT message #{expected_url}")
+ end
+ end
+ end
+
+ describe 'delegation' do
+ let(:content_type) { 'text' }
+ let(:content) { 'Test message' }
+
+ it 'delegates model methods to the wrapped message' do
+ expect(presenter.content).to eq('Test message')
+ expect(presenter.content_type).to eq('text')
+ expect(presenter.conversation).to eq(conversation)
+ end
+ end
+end
\ No newline at end of file
diff --git a/spec/services/csat_survey_service_spec.rb b/spec/services/csat_survey_service_spec.rb
new file mode 100644
index 000000000..5b3b3279a
--- /dev/null
+++ b/spec/services/csat_survey_service_spec.rb
@@ -0,0 +1,91 @@
+require 'rails_helper'
+
+describe CsatSurveyService do
+ let(:account) { create(:account) }
+ let(:inbox) { create(:inbox, account: account, csat_survey_enabled: true) }
+ let(:conversation) { create(:conversation, inbox: inbox, account: account, status: :resolved) }
+ let(:service) { described_class.new(conversation: conversation) }
+
+ describe '#perform' do
+ let(:csat_template) { instance_double(MessageTemplates::Template::CsatSurvey) }
+
+ before do
+ allow(MessageTemplates::Template::CsatSurvey).to receive(:new).and_return(csat_template)
+ allow(csat_template).to receive(:perform)
+ allow(Conversations::ActivityMessageJob).to receive(:perform_later)
+ end
+
+ context 'when CSAT survey should be sent' do
+ before do
+ allow(conversation).to receive(:can_reply?).and_return(true)
+ end
+
+ it 'sends CSAT survey when within messaging window' do
+ service.perform
+
+ expect(MessageTemplates::Template::CsatSurvey).to have_received(:new).with(conversation: conversation)
+ expect(csat_template).to have_received(:perform)
+ end
+ end
+
+ context 'when outside messaging window' do
+ before do
+ allow(conversation).to receive(:can_reply?).and_return(false)
+ end
+
+ it 'creates activity message instead of sending survey' do
+ service.perform
+
+ expect(Conversations::ActivityMessageJob).to have_received(:perform_later).with(
+ conversation,
+ hash_including(content: I18n.t('conversations.activity.csat.not_sent_due_to_messaging_window'))
+ )
+ expect(MessageTemplates::Template::CsatSurvey).not_to have_received(:new)
+ end
+ end
+
+ context 'when CSAT survey should not be sent' do
+ it 'does nothing when conversation is not resolved' do
+ conversation.update(status: :open)
+
+ service.perform
+
+ expect(MessageTemplates::Template::CsatSurvey).not_to have_received(:new)
+ expect(Conversations::ActivityMessageJob).not_to have_received(:perform_later)
+ end
+
+ it 'does nothing when CSAT survey is not enabled' do
+ inbox.update(csat_survey_enabled: false)
+
+ service.perform
+
+ expect(MessageTemplates::Template::CsatSurvey).not_to have_received(:new)
+ expect(Conversations::ActivityMessageJob).not_to have_received(:perform_later)
+ end
+
+ it 'does nothing when CSAT already sent' do
+ create(:message, conversation: conversation, content_type: :input_csat)
+
+ service.perform
+
+ expect(MessageTemplates::Template::CsatSurvey).not_to have_received(:new)
+ expect(Conversations::ActivityMessageJob).not_to have_received(:perform_later)
+ end
+
+ it 'does nothing for Twitter conversations' do
+ twitter_channel = create(:channel_twitter_profile)
+ twitter_inbox = create(:inbox, channel: twitter_channel, csat_survey_enabled: true)
+ twitter_conversation = create(:conversation,
+ inbox: twitter_inbox,
+ status: :resolved,
+ additional_attributes: { type: 'tweet' })
+ twitter_service = described_class.new(conversation: twitter_conversation)
+
+ twitter_service.perform
+
+ expect(MessageTemplates::Template::CsatSurvey).not_to have_received(:new)
+ expect(Conversations::ActivityMessageJob).not_to have_received(:perform_later)
+ end
+ end
+ end
+end
\ No newline at end of file
diff --git a/spec/services/linear/activity_message_service_spec.rb b/spec/services/linear/activity_message_service_spec.rb
new file mode 100644
index 000000000..4b51f6520
--- /dev/null
+++ b/spec/services/linear/activity_message_service_spec.rb
@@ -0,0 +1,174 @@
+require 'rails_helper'
+
+RSpec.describe Linear::ActivityMessageService, type: :service do
+ let(:account) { create(:account) }
+ let(:inbox) { create(:inbox, account: account) }
+ let(:conversation) { create(:conversation, account: account, inbox: inbox) }
+ let(:user) { create(:user, account: account) }
+
+ describe '#perform' do
+ context 'when action_type is issue_created' do
+ let(:service) do
+ described_class.new(
+ conversation: conversation,
+ action_type: :issue_created,
+ issue_data: { id: 'ENG-123' },
+ user: user
+ )
+ end
+
+ it 'enqueues an activity message job' do
+ expect do
+ service.perform
+ end.to have_enqueued_job(Conversations::ActivityMessageJob)
+ .with(conversation, {
+ account_id: conversation.account_id,
+ inbox_id: conversation.inbox_id,
+ message_type: :activity,
+ content: "Linear issue ENG-123 was created by #{user.name}"
+ })
+ end
+
+ it 'does not enqueue job when issue data lacks id' do
+ service = described_class.new(
+ conversation: conversation,
+ action_type: :issue_created,
+ issue_data: { title: 'Some issue' },
+ user: user
+ )
+
+ expect do
+ service.perform
+ end.not_to have_enqueued_job(Conversations::ActivityMessageJob)
+ end
+
+ it 'does not enqueue job when issue_data is empty' do
+ service = described_class.new(
+ conversation: conversation,
+ action_type: :issue_created,
+ issue_data: {},
+ user: user
+ )
+
+ expect do
+ service.perform
+ end.not_to have_enqueued_job(Conversations::ActivityMessageJob)
+ end
+
+ it 'does not enqueue job when conversation is nil' do
+ service = described_class.new(
+ conversation: nil,
+ action_type: :issue_created,
+ issue_data: { id: 'ENG-123' },
+ user: user
+ )
+
+ expect do
+ service.perform
+ end.not_to have_enqueued_job(Conversations::ActivityMessageJob)
+ end
+
+ it 'does not enqueue job when user is nil' do
+ service = described_class.new(
+ conversation: conversation,
+ action_type: :issue_created,
+ issue_data: { id: 'ENG-123' },
+ user: nil
+ )
+
+ expect do
+ service.perform
+ end.not_to have_enqueued_job(Conversations::ActivityMessageJob)
+ end
+ end
+
+ context 'when action_type is issue_linked' do
+ let(:service) do
+ described_class.new(
+ conversation: conversation,
+ action_type: :issue_linked,
+ issue_data: { id: 'ENG-456' },
+ user: user
+ )
+ end
+
+ it 'enqueues an activity message job' do
+ expect do
+ service.perform
+ end.to have_enqueued_job(Conversations::ActivityMessageJob)
+ .with(conversation, {
+ account_id: conversation.account_id,
+ inbox_id: conversation.inbox_id,
+ message_type: :activity,
+ content: "Linear issue ENG-456 was linked by #{user.name}"
+ })
+ end
+
+ it 'does not enqueue job when issue data lacks id' do
+ service = described_class.new(
+ conversation: conversation,
+ action_type: :issue_linked,
+ issue_data: { title: 'Some issue' },
+ user: user
+ )
+
+ expect do
+ service.perform
+ end.not_to have_enqueued_job(Conversations::ActivityMessageJob)
+ end
+ end
+
+ context 'when action_type is issue_unlinked' do
+ let(:service) do
+ described_class.new(
+ conversation: conversation,
+ action_type: :issue_unlinked,
+ issue_data: { id: 'ENG-789' },
+ user: user
+ )
+ end
+
+ it 'enqueues an activity message job' do
+ expect do
+ service.perform
+ end.to have_enqueued_job(Conversations::ActivityMessageJob)
+ .with(conversation, {
+ account_id: conversation.account_id,
+ inbox_id: conversation.inbox_id,
+ message_type: :activity,
+ content: "Linear issue ENG-789 was unlinked by #{user.name}"
+ })
+ end
+
+ it 'does not enqueue job when issue data lacks id' do
+ service = described_class.new(
+ conversation: conversation,
+ action_type: :issue_unlinked,
+ issue_data: { title: 'Some issue' },
+ user: user
+ )
+
+ expect do
+ service.perform
+ end.not_to have_enqueued_job(Conversations::ActivityMessageJob)
+ end
+ end
+
+ context 'when action_type is unknown' do
+ let(:service) do
+ described_class.new(
+ conversation: conversation,
+ action_type: :unknown_action,
+ issue_data: { id: 'ENG-999' },
+ user: user
+ )
+ end
+
+ it 'does not enqueue job for unknown action types' do
+ expect do
+ service.perform
+ end.not_to have_enqueued_job(Conversations::ActivityMessageJob)
+ end
+ end
+ end
+end
\ No newline at end of file
diff --git a/spec/services/liquid/campaign_template_service_spec.rb b/spec/services/liquid/campaign_template_service_spec.rb
new file mode 100644
index 000000000..40034e410
--- /dev/null
+++ b/spec/services/liquid/campaign_template_service_spec.rb
@@ -0,0 +1,107 @@
+require 'rails_helper'
+
+describe Liquid::CampaignTemplateService do
+ subject(:template_service) { described_class.new(campaign: campaign, contact: contact) }
+
+ let(:account) { create(:account) }
+ let(:agent) { create(:user, account: account) }
+ let(:inbox) { create(:inbox, account: account) }
+ let(:contact) { create(:contact, account: account, name: 'John Doe', phone_number: '+1234567890') }
+ let(:campaign) { create(:campaign, account: account, inbox: inbox, sender: agent, message: message_content) }
+
+ describe '#call' do
+ context 'with liquid template variables' do
+ let(:message_content) { 'Hello {{contact.name}}, this is {{agent.name}} from {{account.name}}' }
+
+ it 'processes liquid template correctly' do
+ result = template_service.call(message_content)
+ agent_drop_name = UserDrop.new(agent).name
+ contact_drop_name = ContactDrop.new(contact).name
+
+ expect(result).to eq("Hello #{contact_drop_name}, this is #{agent_drop_name} from #{account.name}")
+ end
+ end
+
+ context 'with code blocks' do
+ let(:message_content) { 'Check this code: `const x = {{contact.name}}`' }
+
+ it 'preserves code blocks without processing liquid' do
+ result = template_service.call(message_content)
+
+ expect(result).to include('`const x = {{contact.name}}`')
+ expect(result).not_to include(contact.name)
+ end
+ end
+
+ context 'with multiline code blocks' do
+ let(:message_content) do
+ <<~MESSAGE
+ Here's some code:
+ ```
+ function greet() {
+ return "Hello {{contact.name}}";
+ }
+ ```
+ MESSAGE
+ end
+
+ it 'preserves multiline code blocks without processing liquid' do
+ result = template_service.call(message_content)
+
+ expect(result).to include('{{contact.name}}')
+ expect(result).not_to include(contact.name)
+ end
+ end
+
+ context 'with malformed liquid syntax' do
+ let(:message_content) { 'Hello {{contact.name missing closing braces' }
+
+ it 'returns original message when liquid parsing fails' do
+ result = template_service.call(message_content)
+
+ expect(result).to eq(message_content)
+ end
+ end
+
+ context 'with invalid liquid tags' do
+ let(:message_content) { 'Hello {% invalid_tag %} world' }
+
+ it 'returns original message when liquid parsing fails' do
+ result = template_service.call(message_content)
+
+ expect(result).to eq(message_content)
+ end
+ end
+
+ context 'with mixed content' do
+ let(:message_content) { 'Hi {{contact.name}}, use this code: `{{agent.name}}` to contact {{agent.name}}' }
+
+ it 'processes liquid outside code blocks but preserves code blocks' do
+ result = template_service.call(message_content)
+ agent_drop_name = UserDrop.new(agent).name
+ contact_drop_name = ContactDrop.new(contact).name
+
+ expect(result).to include("Hi #{contact_drop_name}")
+ expect(result).to include("contact #{agent_drop_name}")
+ expect(result).to include('`{{agent.name}}`')
+ end
+ end
+
+ context 'with all drop types' do
+ let(:message_content) do
+ 'Contact: {{contact.name}}, Agent: {{agent.name}}, Inbox: {{inbox.name}}, Account: {{account.name}}'
+ end
+
+ it 'processes all available drops' do
+ result = template_service.call(message_content)
+ agent_drop_name = UserDrop.new(agent).name
+ contact_drop_name = ContactDrop.new(contact).name
+
+ expect(result).to include("Contact: #{contact_drop_name}")
+ expect(result).to include("Agent: #{agent_drop_name}")
+ expect(result).to include("Inbox: #{inbox.name}")
+ expect(result).to include("Account: #{account.name}")
+ end
+ end
+ end
+end
diff --git a/spec/services/message_templates/hook_execution_service_spec.rb b/spec/services/message_templates/hook_execution_service_spec.rb
index 24e40ea8d..b80c42fd8 100644
--- a/spec/services/message_templates/hook_execution_service_spec.rb
+++ b/spec/services/message_templates/hook_execution_service_spec.rb
@@ -111,69 +111,6 @@ describe MessageTemplates::HookExecutionService do
end
end
- context 'when CSAT Survey' do
- let(:csat_survey) { double }
- let(:conversation) { create(:conversation) }
-
- before do
- allow(MessageTemplates::Template::CsatSurvey).to receive(:new).and_return(csat_survey)
- allow(csat_survey).to receive(:perform).and_return(true)
- create(:message, conversation: conversation, message_type: 'incoming')
- end
-
- it 'calls ::MessageTemplates::Template::CsatSurvey when a conversation is resolved in an inbox with survey enabled' do
- conversation.inbox.update(csat_survey_enabled: true)
-
- conversation.resolved!
- Conversations::ActivityMessageJob.perform_now(conversation,
- { account_id: conversation.account_id, inbox_id: conversation.inbox_id, message_type: :activity,
- content: 'Conversation marked resolved!!' })
-
- expect(MessageTemplates::Template::CsatSurvey).to have_received(:new).with(conversation: conversation)
- expect(csat_survey).to have_received(:perform)
- end
-
- it 'will not call ::MessageTemplates::Template::CsatSurvey when Csat is not enabled' do
- conversation.inbox.update(csat_survey_enabled: false)
-
- conversation.resolved!
- Conversations::ActivityMessageJob.perform_now(conversation,
- { account_id: conversation.account_id, inbox_id: conversation.inbox_id, message_type: :activity,
- content: 'Conversation marked resolved!!' })
-
- expect(MessageTemplates::Template::CsatSurvey).not_to have_received(:new).with(conversation: conversation)
- expect(csat_survey).not_to have_received(:perform)
- end
-
- it 'will not call ::MessageTemplates::Template::CsatSurvey if its a tweet conversation' do
- twitter_channel = create(:channel_twitter_profile)
- twitter_inbox = create(:inbox, channel: twitter_channel)
- conversation = create(:conversation, inbox: twitter_inbox, additional_attributes: { type: 'tweet' })
- conversation.inbox.update(csat_survey_enabled: true)
-
- conversation.resolved!
- Conversations::ActivityMessageJob.perform_now(conversation,
- { account_id: conversation.account_id, inbox_id: conversation.inbox_id, message_type: :activity,
- content: 'Conversation marked resolved!!' })
-
- expect(MessageTemplates::Template::CsatSurvey).not_to have_received(:new).with(conversation: conversation)
- expect(csat_survey).not_to have_received(:perform)
- end
-
- it 'will not call ::MessageTemplates::Template::CsatSurvey if another Csat was already sent' do
- conversation.inbox.update(csat_survey_enabled: true)
- conversation.messages.create!(message_type: 'outgoing', content_type: :input_csat, account: conversation.account, inbox: conversation.inbox)
-
- conversation.resolved!
- Conversations::ActivityMessageJob.perform_now(conversation,
- { account_id: conversation.account_id, inbox_id: conversation.inbox_id, message_type: :activity,
- content: 'Conversation marked resolved!!' })
-
- expect(MessageTemplates::Template::CsatSurvey).not_to have_received(:new).with(conversation: conversation)
- expect(csat_survey).not_to have_received(:perform)
- end
- end
-
context 'when it is after working hours' do
it 'calls ::MessageTemplates::Template::OutOfOffice' do
contact = create(:contact)
diff --git a/spec/services/sms/oneoff_sms_campaign_service_spec.rb b/spec/services/sms/oneoff_sms_campaign_service_spec.rb
index 2f7e5bbf4..a5c7d7e58 100644
--- a/spec/services/sms/oneoff_sms_campaign_service_spec.rb
+++ b/spec/services/sms/oneoff_sms_campaign_service_spec.rb
@@ -43,5 +43,14 @@ describe Sms::OneoffSmsCampaignService do
assert_requested(:post, 'https://messaging.bandwidth.com/api/v2/users/1/messages', times: 3)
expect(campaign.reload.completed?).to be true
end
+
+ it 'uses liquid template service to process campaign message' do
+ contact = create(:contact, :with_phone_number, account: account)
+ contact.update_labels([label1.title])
+
+ expect(Liquid::CampaignTemplateService).to receive(:new).with(campaign: campaign, contact: contact).and_call_original
+
+ sms_campaign_service.perform
+ end
end
end
diff --git a/spec/services/telegram/incoming_message_service_spec.rb b/spec/services/telegram/incoming_message_service_spec.rb
index 0702baa32..ef43dbe24 100644
--- a/spec/services/telegram/incoming_message_service_spec.rb
+++ b/spec/services/telegram/incoming_message_service_spec.rb
@@ -89,6 +89,61 @@ describe Telegram::IncomingMessageService do
end
end
+ context 'when business connection messages' do
+ subject do
+ described_class.new(inbox: telegram_channel.inbox, params: params).perform
+ end
+
+ let(:business_message_params) { message_params.merge('business_connection_id' => 'eooW3KF5WB5HxTD7T826') }
+ let(:params) do
+ {
+ 'update_id' => 2_342_342_343_242,
+ 'business_message' => { 'text' => 'test' }.deep_merge(business_message_params)
+ }.with_indifferent_access
+ end
+
+ it 'creates appropriate conversations, message and contacts' do
+ subject
+ expect(telegram_channel.inbox.conversations.count).not_to eq(0)
+ expect(telegram_channel.inbox.conversations.last.additional_attributes).to include({ 'chat_id' => 23,
+ 'business_connection_id' => 'eooW3KF5WB5HxTD7T826' })
+ contact = Contact.all.first
+ expect(contact.name).to eq('Sojan Jose')
+ expect(contact.additional_attributes['language_code']).to eq('en')
+ message = telegram_channel.inbox.messages.first
+ expect(message.content).to eq('test')
+ expect(message.message_type).to eq('incoming')
+ expect(message.sender).to eq(contact)
+ end
+
+ context 'when sender is your business account' do
+ let(:business_message_params) do
+ message_params.merge(
+ 'business_connection_id' => 'eooW3KF5WB5HxTD7T826',
+ 'from' => {
+ 'id' => 42, 'is_bot' => false, 'first_name' => 'John', 'last_name' => 'Doe', 'username' => 'johndoe', 'language_code' => 'en'
+ }
+ )
+ end
+
+ it 'creates appropriate conversations, message and contacts' do
+ subject
+ expect(telegram_channel.inbox.conversations.count).not_to eq(0)
+ expect(telegram_channel.inbox.conversations.last.additional_attributes).to include({ 'chat_id' => 23,
+ 'business_connection_id' => 'eooW3KF5WB5HxTD7T826' })
+ contact = Contact.all.first
+ expect(contact.name).to eq('Sojan Jose')
+ # TODO: The language code is not present when we send the first message to the client.
+ # Should we update it when the user replies?
+ expect(contact.additional_attributes['language_code']).to be_nil
+ message = telegram_channel.inbox.messages.first
+ expect(message.content).to eq('test')
+ expect(message.message_type).to eq('outgoing')
+ expect(message.sender).to be_nil
+ end
+ end
+ end
+
context 'when valid audio messages params' do
it 'creates appropriate conversations, message and contacts' do
allow(telegram_channel.inbox.channel).to receive(:get_telegram_file_path).and_return('https://chatwoot-assets.local/sample.mp3')
@@ -179,6 +234,35 @@ describe Telegram::IncomingMessageService do
end
end
+ context 'when valid video_note messages params' do
+ it 'creates appropriate conversations, message and contacts' do
+ allow(telegram_channel.inbox.channel).to receive(:get_telegram_file_path).and_return('https://chatwoot-assets.local/sample.mov')
+ params = {
+ 'update_id' => 2_342_342_343_242,
+ 'message' => {
+ 'video_note' => {
+ 'duration' => 3,
+ 'length' => 240,
+ 'thumb' => {
+ 'file_id' => 'AAMCBQADGQEAA4ZhXd78Xz6_c6gCzbdIkgGiXJcwwwACqwMAAp3x8Fbhf3EWamgCWAEAB20AAyEE',
+ 'file_unique_id' => 'AQADqwMAAp3x8FZy',
+ 'file_size' => 11_462,
+ 'width' => 240,
+ 'height' => 240
+ },
+ 'file_id' => 'DQACAgUAAxkBAAIBY2FdJlhf8PC2E3IalXSvXWO5m8GBAALJAwACwqHgVhb0truM0uhwIQQ',
+ 'file_unique_id' => 'AgADyQMAAsKh4FY',
+ 'file_size' => 132_446
+ }
+ }.merge(message_params)
+ }.with_indifferent_access
+ described_class.new(inbox: telegram_channel.inbox, params: params).perform
+ expect(telegram_channel.inbox.conversations.count).not_to eq(0)
+ expect(Contact.all.first.name).to eq('Sojan Jose')
+ expect(telegram_channel.inbox.messages.first.attachments.first.file_type).to eq('video')
+ end
+ end
+
context 'when valid voice attachment params' do
it 'creates appropriate conversations, message and contacts' do
allow(telegram_channel.inbox.channel).to receive(:get_telegram_file_path).and_return('https://chatwoot-assets.local/sample.ogg')
diff --git a/spec/services/telegram/send_attachments_service_spec.rb b/spec/services/telegram/send_attachments_service_spec.rb
index 2159acddd..f9c625780 100644
--- a/spec/services/telegram/send_attachments_service_spec.rb
+++ b/spec/services/telegram/send_attachments_service_spec.rb
@@ -40,6 +40,22 @@ RSpec.describe Telegram::SendAttachmentsService do
end
end
+ context 'when this is business chat' do
+ before { allow(channel).to receive(:business_connection_id).and_return('eooW3KF5WB5HxTD7T826') }
+
+ it 'sends all types of attachments in seperate groups and returns the last successful message ID from the batch' do
+ attach_files(message)
+ service.perform
+ expect(a_request(:post, "#{telegram_api_url}/sendMediaGroup")
+ .with { |req| req.body =~ /business_connection_id.+eooW3KF5WB5HxTD7T826/m })
+ .to have_been_made.times(2)
+
+ expect(a_request(:post, "#{telegram_api_url}/sendDocument")
+ .with { |req| req.body =~ /business_connection_id.+eooW3KF5WB5HxTD7T826/m })
+ .to have_been_made.once
+ end
+ end
+
context 'when all attachments are photo and video' do
before do
2.times { attach_file_to_message(message, 'image', 'sample.png', 'image/png') }
diff --git a/spec/services/telegram/update_message_service_spec.rb b/spec/services/telegram/update_message_service_spec.rb
index 5c00c9009..33a279796 100644
--- a/spec/services/telegram/update_message_service_spec.rb
+++ b/spec/services/telegram/update_message_service_spec.rb
@@ -53,6 +53,24 @@ describe Telegram::UpdateMessageService do
described_class.new(inbox: telegram_channel.inbox, params: caption_update_params.with_indifferent_access).perform
expect(message.reload.content).to eq('updated caption')
end
+
+ context 'when business message' do
+ let(:text_update_params) do
+ {
+ 'update_id': 1,
+ 'edited_business_message': common_message_params.merge(
+ 'message_id': 48,
+ 'text': 'updated message'
+ )
+ }
+ end
+
+ it 'updates the message text when text is present' do
+ message = create(:message, conversation: conversation, source_id: text_update_params[:edited_business_message][:message_id])
+ described_class.new(inbox: telegram_channel.inbox, params: text_update_params.with_indifferent_access).perform
+ expect(message.reload.content).to eq('updated message')
+ end
+ end
end
context 'when invalid update message params' do
diff --git a/spec/services/twilio/oneoff_sms_campaign_service_spec.rb b/spec/services/twilio/oneoff_sms_campaign_service_spec.rb
index f8858f174..0e6a993c2 100644
--- a/spec/services/twilio/oneoff_sms_campaign_service_spec.rb
+++ b/spec/services/twilio/oneoff_sms_campaign_service_spec.rb
@@ -60,5 +60,15 @@ describe Twilio::OneoffSmsCampaignService do
sms_campaign_service.perform
expect(campaign.reload.completed?).to be true
end
+
+ it 'uses liquid template service to process campaign message' do
+ contact = create(:contact, :with_phone_number, account: account)
+ contact.update_labels([label1.title])
+
+ expect(Liquid::CampaignTemplateService).to receive(:new).with(campaign: campaign, contact: contact).and_call_original
+ expect(twilio_messages).to receive(:create).once
+
+ sms_campaign_service.perform
+ end
end
end
diff --git a/spec/services/whatsapp/providers/whatsapp360_dialog_service_spec.rb b/spec/services/whatsapp/providers/whatsapp360_dialog_service_spec.rb
index bba3f5914..572e96685 100644
--- a/spec/services/whatsapp/providers/whatsapp360_dialog_service_spec.rb
+++ b/spec/services/whatsapp/providers/whatsapp360_dialog_service_spec.rb
@@ -51,16 +51,15 @@ describe Whatsapp::Providers::Whatsapp360DialogService do
end
it 'calls message endpoints with list payload when number of items is greater than 3' do
+ items = %w[Burito Pasta Sushi Salad].map { |i| { title: i, value: i } }
message = create(:message, message_type: :outgoing, content: 'test', inbox: whatsapp_channel.inbox,
- content_type: 'input_select',
- content_attributes: {
- items: [
- { title: 'Burito', value: 'Burito' },
- { title: 'Pasta', value: 'Pasta' },
- { title: 'Sushi', value: 'Sushi' },
- { title: 'Salad', value: 'Salad' }
- ]
- })
+ content_type: 'input_select', content_attributes: { items: items })
+
+ expected_action = {
+ button: I18n.t('conversations.messages.whatsapp.list_button_label'),
+ sections: [{ rows: %w[Burito Pasta Sushi Salad].map { |i| { id: i, title: i } } }]
+ }.to_json
+
stub_request(:post, 'https://waba.360dialog.io/v1/messages')
.with(
body: {
@@ -70,9 +69,9 @@ describe Whatsapp::Providers::Whatsapp360DialogService do
body: {
text: 'test'
},
- action: '{"button":"Choose an item","sections":[{"rows":[{"id":"Burito","title":"Burito"},' \
- '{"id":"Pasta","title":"Pasta"},{"id":"Sushi","title":"Sushi"},{"id":"Salad","title":"Salad"}]}]}'
- }, type: 'interactive'
+ action: expected_action
+ },
+ type: 'interactive'
}.to_json
).to_return(status: 200, body: whatsapp_response.to_json, headers: response_headers)
expect(service.send_message('+123456789', message)).to eq 'message_id'
diff --git a/spec/services/whatsapp/providers/whatsapp_cloud_service_spec.rb b/spec/services/whatsapp/providers/whatsapp_cloud_service_spec.rb
index 134d69fee..1bef635b3 100644
--- a/spec/services/whatsapp/providers/whatsapp_cloud_service_spec.rb
+++ b/spec/services/whatsapp/providers/whatsapp_cloud_service_spec.rb
@@ -124,16 +124,15 @@ describe Whatsapp::Providers::WhatsappCloudService do
end
it 'calls message endpoints with list payload when number of items is greater than 3' do
+ items = %w[Burito Pasta Sushi Salad].map { |i| { title: i, value: i } }
message = create(:message, message_type: :outgoing, content: 'test', inbox: whatsapp_channel.inbox,
- content_type: 'input_select',
- content_attributes: {
- items: [
- { title: 'Burito', value: 'Burito' },
- { title: 'Pasta', value: 'Pasta' },
- { title: 'Sushi', value: 'Sushi' },
- { title: 'Salad', value: 'Salad' }
- ]
- })
+ content_type: 'input_select', content_attributes: { items: items })
+
+ expected_action = {
+ button: I18n.t('conversations.messages.whatsapp.list_button_label'),
+ sections: [{ rows: %w[Burito Pasta Sushi Salad].map { |i| { id: i, title: i } } }]
+ }.to_json
+
stub_request(:post, 'https://graph.facebook.com/v13.0/123456789/messages')
.with(
body: {
@@ -143,9 +142,9 @@ describe Whatsapp::Providers::WhatsappCloudService do
body: {
text: 'test'
},
- action: '{"button":"Choose an item","sections":[{"rows":[{"id":"Burito","title":"Burito"},' \
- '{"id":"Pasta","title":"Pasta"},{"id":"Sushi","title":"Sushi"},{"id":"Salad","title":"Salad"}]}]}'
- }, type: 'interactive'
+ action: expected_action
+ },
+ type: 'interactive'
}.to_json
).to_return(status: 200, body: whatsapp_response.to_json, headers: response_headers)
expect(service.send_message('+123456789', message)).to eq 'message_id'