feat: remove Notificações Automáticas + ajusta assinatura WhatsApp

1. Captain — remove feature Notificações Automáticas (captain_notification_templates)
   Feature órfã: 0 registros em prod, substituída pela Jornada do Cliente
   (Lifecycle). Sem dependências fora dela própria. Removido:
   - Vue: routes/settings/captain/notifications/ + entry no captain.routes.js
   - Sidebar: item "Notifications" do Captain menu
   - Store: modules captainNotificationTemplates + import no store/index
   - API: api/captain/notificationTemplates.js
   - Controller: api/v1/accounts/captain/notification_templates_controller
   - Model: Captain::NotificationTemplate
   - Job: enterprise/app/jobs/captain/notifications/
   - Routes: resources :notification_templates no config/routes.rb
   - i18n: chaves CAPTAIN_SETTINGS.NOTIFICATIONS + SIDEBAR.CAPTAIN_NOTIFICATIONS
     em pt_BR e en (captain.json + settings.json)

   Tabela captain_notification_templates mantida (0 rows, sem consumidores).
   Se quiser drop, criar migration separada depois.

2. WhatsApp — tira colchetes do prefixo de assinatura
   Era: *[ Jasmine(PrimeAL) ]*\ncontent
   Agora: *Jasmine(PrimeAL)*\ncontent
   Afeta wuzapi_service (outgoing) + incoming_message_wuzapi_service (echo)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Rodribm10 2026-04-23 15:34:45 -03:00
parent c0b54c6783
commit 34d42dfbbd
16 changed files with 3 additions and 787 deletions

View File

@ -1,29 +0,0 @@
import ApiClient from '../ApiClient';
class NotificationTemplatesAPI extends ApiClient {
constructor() {
super('inboxes', { accountScoped: true });
}
getAll(inboxId) {
return this.get(`${inboxId}/notification_templates`);
}
create(inboxId, data) {
return this.post(`${inboxId}/notification_templates`, {
notification_template: data,
});
}
update(inboxId, id, data) {
return this.patch(`${inboxId}/notification_templates/${id}`, {
notification_template: data,
});
}
delete(inboxId, id) {
return this.delete(`${inboxId}/notification_templates/${id}`);
}
}
export default new NotificationTemplatesAPI();

View File

@ -436,12 +436,6 @@ const menuItems = computed(() => {
activeOn: ['captain_settings_reports'],
to: accountScopedRoute('captain_settings_reports'),
},
{
name: 'Notifications',
label: t('SIDEBAR.CAPTAIN_NOTIFICATIONS'),
activeOn: ['captain_settings_notifications'],
to: accountScopedRoute('captain_settings_notifications'),
},
],
},
{

View File

@ -433,46 +433,6 @@
"LABEL": "Available for agent sending"
}
}
},
"NOTIFICATIONS": {
"TITLE": "Automatic Notifications",
"DESCRIPTION": "Configure messages sent automatically before or after the guest's arrival.",
"LOADING": "Loading notifications...",
"ADD": "Add notification",
"ACTIVE": "Active",
"INACTIVE": "Inactive",
"DIRECTION": {
"BEFORE": "before",
"AFTER": "after",
"OF_ARRIVAL": "arrival"
},
"FORM": {
"LABEL_PLACEHOLDER": "Template name (e.g. Arrival Instructions)",
"CONTENT_PLACEHOLDER": "Message to send... Use {{guest_name}}, {{check_in_time}}, {{suite_name}}",
"SEND": "Send",
"MINUTES": "min",
"CANCEL": "Cancel",
"SAVE": "Save"
},
"CREATE": {
"SUCCESS": "Notification created successfully!",
"ERROR": "Error creating notification. Please try again."
},
"UPDATE": {
"SUCCESS": "Notification updated!",
"ERROR": "Error updating notification."
},
"DELETE": {
"SUCCESS": "Notification removed.",
"ERROR": "Error removing notification."
},
"INBOX_LABEL": "Select inbox",
"NO_CAPTAIN_INBOXES": "No inboxes with Captain configured.",
"SELECT_INBOX_HINT": "Click an inbox above to view and configure its templates.",
"EMPTY": {
"TITLE": "No templates configured",
"DESC": "Create automatic message templates for this inbox."
}
}
},
"CAPTAIN": {

View File

@ -347,7 +347,6 @@
"CAPTAIN_FUNNEL": "Conversion Funnel",
"CAPTAIN_LIFECYCLE": "Customer Journey",
"CAPTAIN_REPORTS": "AI Reports",
"CAPTAIN_NOTIFICATIONS": "Automatic Notifications",
"HOME": "Home",
"AGENTS": "Agents",
"AGENT_BOTS": "Bots",

View File

@ -434,47 +434,6 @@
"LABEL": "Disponível para envio pelos agentes"
}
}
},
"NOTIFICATIONS": {
"TITLE": "Notificações Automáticas",
"DESCRIPTION": "Configure mensagens automáticas enviadas antes ou depois da chegada do hóspede.",
"LOADING": "Carregando notificações...",
"ADD": "Adicionar notificação",
"ACTIVE": "Ativo",
"INACTIVE": "Inativo",
"DIRECTION": {
"BEFORE": "antes",
"AFTER": "depois",
"OF_ARRIVAL": "da chegada"
},
"TIMING_LABEL": "da chegada",
"FORM": {
"LABEL_PLACEHOLDER": "Nome do template (ex: Orientações de Chegada)",
"CONTENT_PLACEHOLDER": "Mensagem a enviar... Use {{guest_name}}, {{check_in_time}}, {{suite_name}}",
"SEND": "Enviar",
"MINUTES": "min",
"CANCEL": "Cancelar",
"SAVE": "Salvar"
},
"CREATE": {
"SUCCESS": "Notificação criada com sucesso!",
"ERROR": "Erro ao criar notificação. Tente novamente."
},
"UPDATE": {
"SUCCESS": "Notificação atualizada!",
"ERROR": "Erro ao atualizar notificação."
},
"DELETE": {
"SUCCESS": "Notificação removida.",
"ERROR": "Erro ao remover notificação."
},
"INBOX_LABEL": "Selecione a caixa de entrada",
"NO_CAPTAIN_INBOXES": "Nenhuma caixa de entrada com Captain configurado.",
"SELECT_INBOX_HINT": "Clique em uma caixa de entrada acima para ver e configurar os templates.",
"EMPTY": {
"TITLE": "Nenhum template configurado",
"DESC": "Configure as permissões das informações que o sistema utiliza. Por ex.: Quais imagens enviar durante as aproximações."
}
}
},
"CAPTAIN": {

View File

@ -346,7 +346,6 @@
"CAPTAIN_FUNNEL": "Funil de Conversão",
"CAPTAIN_LIFECYCLE": "Jornada do Cliente",
"CAPTAIN_REPORTS": "Relatórios IA",
"CAPTAIN_NOTIFICATIONS": "Notificações Automáticas",
"HOME": "Principal",
"AGENTS": "Agentes",
"AGENT_BOTS": "Robôs",

View File

@ -8,7 +8,6 @@ import UnitEdit from './units/Edit.vue';
import GalleryIndex from './gallery/Index.vue';
import GalleryEdit from './gallery/Edit.vue';
const ReportsIndex = () => import('./reports/Index.vue');
const NotificationsIndex = () => import('./notifications/Index.vue');
export default {
routes: [
@ -78,14 +77,6 @@ export default {
permissions: ['administrator'],
},
},
{
path: 'notifications',
name: 'captain_settings_notifications',
component: NotificationsIndex,
meta: {
permissions: ['administrator'],
},
},
],
},
],

View File

@ -1,443 +0,0 @@
<script setup>
import { ref, computed, watch, onMounted } from 'vue';
import { useStore, useMapGetter } from 'dashboard/composables/store';
import { useAlert } from 'dashboard/composables';
import { useI18n } from 'vue-i18n';
import SettingsLayout from '../../SettingsLayout.vue';
import BaseSettingsHeader from '../../components/BaseSettingsHeader.vue';
import Button from 'dashboard/components-next/button/Button.vue';
const { t } = useI18n();
const store = useStore();
const inboxes = useMapGetter('inboxes/getInboxes');
const templates = useMapGetter('captainNotificationTemplates/getRecords');
const uiFlags = useMapGetter('captainNotificationTemplates/getUIFlags');
const selectedInboxId = ref(null);
const editingId = ref(null);
const showNewForm = ref(false);
// Todas as inboxes da conta (o usuário escolhe em qual configurar)
const captainInboxes = computed(() => inboxes.value || []);
const hasInboxes = computed(() => captainInboxes.value.length > 0);
// Formulários
const emptyForm = () => ({
label: '',
content: '',
timing_minutes: 10,
timing_direction: 'before',
active: true,
});
const newForm = ref(emptyForm());
const editForm = ref(emptyForm());
const VARIABLES = [
'{{guest_name}}',
'{{check_in_time}}',
'{{check_out_time}}',
'{{suite_name}}',
'{{unit_name}}',
];
// Carregamento
onMounted(async () => {
await store.dispatch('inboxes/get');
});
watch(selectedInboxId, async id => {
if (id) {
await store.dispatch('captainNotificationTemplates/fetch', id);
showNewForm.value = false;
editingId.value = null;
}
});
// Novo template
const openNewForm = () => {
newForm.value = emptyForm();
showNewForm.value = true;
};
const cancelNew = () => {
showNewForm.value = false;
newForm.value = emptyForm();
};
const saveNew = async () => {
if (!newForm.value.label || !newForm.value.content) return;
try {
await store.dispatch('captainNotificationTemplates/create', {
inboxId: selectedInboxId.value,
payload: newForm.value,
});
useAlert(t('CAPTAIN_SETTINGS.NOTIFICATIONS.CREATE.SUCCESS'));
showNewForm.value = false;
newForm.value = emptyForm();
} catch {
useAlert(t('CAPTAIN_SETTINGS.NOTIFICATIONS.CREATE.ERROR'));
}
};
// Edição
const startEdit = template => {
editingId.value = template.id;
editForm.value = { ...template };
};
const cancelEdit = () => {
editingId.value = null;
editForm.value = emptyForm();
};
const saveEdit = async () => {
try {
await store.dispatch('captainNotificationTemplates/update', {
inboxId: selectedInboxId.value,
id: editingId.value,
payload: editForm.value,
});
useAlert(t('CAPTAIN_SETTINGS.NOTIFICATIONS.UPDATE.SUCCESS'));
editingId.value = null;
} catch {
useAlert(t('CAPTAIN_SETTINGS.NOTIFICATIONS.UPDATE.ERROR'));
}
};
// Toggle ativo
const toggleActive = async template => {
try {
await store.dispatch('captainNotificationTemplates/update', {
inboxId: selectedInboxId.value,
id: template.id,
payload: { active: !template.active },
});
} catch {
useAlert(t('CAPTAIN_SETTINGS.NOTIFICATIONS.UPDATE.ERROR'));
}
};
// Exclusão
const deleteTemplate = async template => {
try {
await store.dispatch('captainNotificationTemplates/delete', {
inboxId: selectedInboxId.value,
id: template.id,
});
useAlert(t('CAPTAIN_SETTINGS.NOTIFICATIONS.DELETE.SUCCESS'));
} catch {
useAlert(t('CAPTAIN_SETTINGS.NOTIFICATIONS.DELETE.ERROR'));
}
};
// Variáveis
const insertVariable = (variable, target) => {
if (target === 'new') {
newForm.value.content += variable;
} else {
editForm.value.content += variable;
}
};
const directionLabel = direction =>
direction === 'before'
? t('CAPTAIN_SETTINGS.NOTIFICATIONS.DIRECTION.BEFORE')
: t('CAPTAIN_SETTINGS.NOTIFICATIONS.DIRECTION.AFTER');
const timingDisplay = template =>
`${template.timing_minutes} ${t('CAPTAIN_SETTINGS.NOTIFICATIONS.FORM.MINUTES')} ${directionLabel(template.timing_direction)} ${t('CAPTAIN_SETTINGS.NOTIFICATIONS.DIRECTION.OF_ARRIVAL')}`;
</script>
<template>
<SettingsLayout
:is-loading="uiFlags.isFetching"
:loading-message="t('CAPTAIN_SETTINGS.NOTIFICATIONS.LOADING')"
>
<template #header>
<BaseSettingsHeader
:title="t('CAPTAIN_SETTINGS.NOTIFICATIONS.TITLE')"
:description="t('CAPTAIN_SETTINGS.NOTIFICATIONS.DESCRIPTION')"
>
<template #actions>
<Button
v-if="selectedInboxId && !showNewForm"
icon="i-lucide-plus"
:label="t('CAPTAIN_SETTINGS.NOTIFICATIONS.ADD')"
@click="openNewForm"
/>
</template>
</BaseSettingsHeader>
</template>
<template #body>
<div class="flex flex-col gap-6 px-6 pb-8">
<!-- Seletor de inbox -->
<div class="flex flex-col gap-2">
<label class="text-sm font-medium text-n-slate-12">
{{ t('CAPTAIN_SETTINGS.NOTIFICATIONS.INBOX_LABEL') }}
</label>
<div v-if="!hasInboxes" class="text-sm text-n-slate-10">
{{ t('CAPTAIN_SETTINGS.NOTIFICATIONS.NO_CAPTAIN_INBOXES') }}
</div>
<div v-else class="flex flex-wrap gap-2">
<button
v-for="inbox in captainInboxes"
:key="inbox.id"
class="flex items-center gap-2 rounded-lg border px-4 py-2 text-sm transition-colors"
:class="
selectedInboxId === inbox.id
? 'border-w-500 bg-w-50 text-w-700 font-medium'
: 'border-n-weak text-n-slate-11 hover:border-n-300'
"
@click="selectedInboxId = inbox.id"
>
<span class="i-lucide-message-circle w-4 h-4" />
{{ inbox.name }}
</button>
</div>
</div>
<!-- Conteúdo: aparece após selecionar inbox -->
<div v-if="selectedInboxId" class="flex flex-col gap-3">
<!-- Template list -->
<div
v-for="template in templates"
:key="template.id"
class="rounded-lg border border-n-75 bg-white p-4"
>
<!-- View mode -->
<div
v-if="editingId !== template.id"
class="flex items-start justify-between gap-3"
>
<div class="flex flex-col gap-1 flex-1 min-w-0">
<span class="text-sm font-semibold text-n-slate-12">{{
template.label
}}</span>
<span class="text-sm text-n-slate-11 whitespace-pre-line">{{
template.content
}}</span>
<span class="text-xs text-n-slate-10 mt-1">
{{ timingDisplay(template) }}
</span>
</div>
<div class="flex items-center gap-2 shrink-0">
<button
class="text-xs px-2 py-1 rounded"
:class="
template.active
? 'bg-n-teal-2 text-n-teal-11'
: 'bg-n-slate-3 text-n-slate-11'
"
@click="toggleActive(template)"
>
{{
template.active
? t('CAPTAIN_SETTINGS.NOTIFICATIONS.ACTIVE')
: t('CAPTAIN_SETTINGS.NOTIFICATIONS.INACTIVE')
}}
</button>
<button
class="text-n-slate-10 hover:text-n-slate-12"
@click="startEdit(template)"
>
<span class="i-lucide-pencil w-4 h-4" />
</button>
<button
class="text-n-ruby-9 hover:text-n-ruby-11"
@click="deleteTemplate(template)"
>
<span class="i-lucide-trash-2 w-4 h-4" />
</button>
</div>
</div>
<!-- Edit mode -->
<div v-else class="flex flex-col gap-3">
<input
v-model="editForm.label"
:placeholder="
t('CAPTAIN_SETTINGS.NOTIFICATIONS.FORM.LABEL_PLACEHOLDER')
"
class="w-full rounded border border-n-weak px-3 py-2 text-sm focus:outline-none focus:border-w-500"
/>
<textarea
v-model="editForm.content"
:placeholder="
t('CAPTAIN_SETTINGS.NOTIFICATIONS.FORM.CONTENT_PLACEHOLDER')
"
rows="3"
class="w-full rounded border border-n-weak px-3 py-2 text-sm focus:outline-none focus:border-w-500 resize-none"
/>
<!-- Variable chips -->
<div class="flex flex-wrap gap-1">
<button
v-for="v in VARIABLES"
:key="v"
class="text-xs bg-n-slate-3 text-n-slate-11 px-2 py-0.5 rounded hover:bg-n-slate-4"
@click="insertVariable(v, 'edit')"
>
{{ v }}
</button>
</div>
<!-- Timing row -->
<div class="flex items-center gap-2 text-sm flex-wrap">
<span class="text-n-slate-11">{{
t('CAPTAIN_SETTINGS.NOTIFICATIONS.FORM.SEND')
}}</span>
<input
v-model.number="editForm.timing_minutes"
type="number"
min="1"
class="w-16 rounded border border-n-weak px-2 py-1 text-sm text-center focus:outline-none focus:border-w-500"
/>
<span class="text-n-slate-11">{{
t('CAPTAIN_SETTINGS.NOTIFICATIONS.FORM.MINUTES')
}}</span>
<select
v-model="editForm.timing_direction"
class="rounded border border-n-weak px-2 py-1 text-sm focus:outline-none focus:border-w-500"
>
<option value="before">
{{ t('CAPTAIN_SETTINGS.NOTIFICATIONS.DIRECTION.BEFORE') }}
</option>
<option value="after">
{{ t('CAPTAIN_SETTINGS.NOTIFICATIONS.DIRECTION.AFTER') }}
</option>
</select>
<span class="text-n-slate-11">{{
t('CAPTAIN_SETTINGS.NOTIFICATIONS.DIRECTION.OF_ARRIVAL')
}}</span>
</div>
<div class="flex gap-2 justify-end">
<Button
variant="clear"
:label="t('CAPTAIN_SETTINGS.NOTIFICATIONS.FORM.CANCEL')"
@click="cancelEdit"
/>
<Button
:label="t('CAPTAIN_SETTINGS.NOTIFICATIONS.FORM.SAVE')"
:is-loading="uiFlags.isSaving"
@click="saveEdit"
/>
</div>
</div>
</div>
<!-- New form -->
<div
v-if="showNewForm"
class="rounded-lg border border-w-300 bg-w-25 p-4 flex flex-col gap-3"
>
<input
v-model="newForm.label"
:placeholder="
t('CAPTAIN_SETTINGS.NOTIFICATIONS.FORM.LABEL_PLACEHOLDER')
"
class="w-full rounded border border-n-weak px-3 py-2 text-sm focus:outline-none focus:border-w-500"
/>
<textarea
v-model="newForm.content"
:placeholder="
t('CAPTAIN_SETTINGS.NOTIFICATIONS.FORM.CONTENT_PLACEHOLDER')
"
rows="3"
class="w-full rounded border border-n-weak px-3 py-2 text-sm focus:outline-none focus:border-w-500 resize-none"
/>
<!-- Variable chips -->
<div class="flex flex-wrap gap-1">
<button
v-for="v in VARIABLES"
:key="v"
class="text-xs bg-n-slate-3 text-n-slate-11 px-2 py-0.5 rounded hover:bg-n-slate-4"
@click="insertVariable(v, 'new')"
>
{{ v }}
</button>
</div>
<!-- Timing row -->
<div class="flex items-center gap-2 text-sm flex-wrap">
<span class="text-n-slate-11">{{
t('CAPTAIN_SETTINGS.NOTIFICATIONS.FORM.SEND')
}}</span>
<input
v-model.number="newForm.timing_minutes"
type="number"
min="1"
class="w-16 rounded border border-n-weak px-2 py-1 text-sm text-center focus:outline-none focus:border-w-500"
/>
<span class="text-n-slate-11">{{
t('CAPTAIN_SETTINGS.NOTIFICATIONS.FORM.MINUTES')
}}</span>
<select
v-model="newForm.timing_direction"
class="rounded border border-n-weak px-2 py-1 text-sm focus:outline-none focus:border-w-500"
>
<option value="before">
{{ t('CAPTAIN_SETTINGS.NOTIFICATIONS.DIRECTION.BEFORE') }}
</option>
<option value="after">
{{ t('CAPTAIN_SETTINGS.NOTIFICATIONS.DIRECTION.AFTER') }}
</option>
</select>
<span class="text-n-slate-11">{{
t('CAPTAIN_SETTINGS.NOTIFICATIONS.DIRECTION.OF_ARRIVAL')
}}</span>
</div>
<div class="flex gap-2 justify-end">
<Button
variant="clear"
:label="t('CAPTAIN_SETTINGS.NOTIFICATIONS.FORM.CANCEL')"
@click="cancelNew"
/>
<Button
:label="t('CAPTAIN_SETTINGS.NOTIFICATIONS.FORM.SAVE')"
:is-loading="uiFlags.isSaving"
@click="saveNew"
/>
</div>
</div>
<!-- Empty state (sem templates, sem form aberto) -->
<div
v-if="!templates.length && !showNewForm"
class="flex flex-col items-center justify-center gap-4 py-16 text-center"
>
<div
class="size-14 rounded-full bg-n-slate-3 flex items-center justify-center"
>
<span class="i-lucide-bell w-6 h-6 text-n-slate-10" />
</div>
<div class="flex flex-col gap-1">
<p class="mb-0 text-base font-medium text-n-slate-12">
{{ t('CAPTAIN_SETTINGS.NOTIFICATIONS.EMPTY.TITLE') }}
</p>
<p class="mb-0 max-w-sm text-sm text-n-slate-10">
{{ t('CAPTAIN_SETTINGS.NOTIFICATIONS.EMPTY.DESC') }}
</p>
</div>
<Button
icon="i-lucide-plus"
:label="t('CAPTAIN_SETTINGS.NOTIFICATIONS.ADD')"
@click="openNewForm"
/>
</div>
</div>
<!-- Estado inicial: nenhuma inbox selecionada -->
<div
v-else-if="hasInboxes"
class="flex flex-col items-center justify-center gap-3 py-16 text-center"
>
<span class="i-lucide-mouse-pointer-click w-8 h-8 text-n-slate-9" />
<p class="mb-0 text-sm text-n-slate-10">
{{ t('CAPTAIN_SETTINGS.NOTIFICATIONS.SELECT_INBOX_HINT') }}
</p>
</div>
</div>
</template>
</SettingsLayout>
</template>

View File

@ -1,84 +0,0 @@
import notificationTemplatesAPI from '../../api/captain/notificationTemplates';
const state = {
records: [],
uiFlags: {
isFetching: false,
isSaving: false,
},
};
const getters = {
getRecords: $state => $state.records,
getUIFlags: $state => $state.uiFlags,
};
const actions = {
async fetch({ commit }, inboxId) {
commit('SET_UI_FLAG', { isFetching: true });
try {
const { data } = await notificationTemplatesAPI.getAll(inboxId);
commit('SET_RECORDS', data);
} finally {
commit('SET_UI_FLAG', { isFetching: false });
}
},
async create({ commit }, { inboxId, payload }) {
commit('SET_UI_FLAG', { isSaving: true });
try {
const { data } = await notificationTemplatesAPI.create(inboxId, payload);
commit('ADD_RECORD', data);
return data;
} finally {
commit('SET_UI_FLAG', { isSaving: false });
}
},
async update({ commit }, { inboxId, id, payload }) {
commit('SET_UI_FLAG', { isSaving: true });
try {
const { data } = await notificationTemplatesAPI.update(
inboxId,
id,
payload
);
commit('UPDATE_RECORD', data);
return data;
} finally {
commit('SET_UI_FLAG', { isSaving: false });
}
},
async delete({ commit }, { inboxId, id }) {
await notificationTemplatesAPI.delete(inboxId, id);
commit('DELETE_RECORD', id);
},
};
const mutations = {
SET_RECORDS($state, records) {
$state.records = records;
},
ADD_RECORD($state, record) {
$state.records.push(record);
},
UPDATE_RECORD($state, record) {
const idx = $state.records.findIndex(r => r.id === record.id);
if (idx !== -1) $state.records.splice(idx, 1, record);
},
DELETE_RECORD($state, id) {
$state.records = $state.records.filter(r => r.id !== id);
},
SET_UI_FLAG($state, flags) {
$state.uiFlags = { ...$state.uiFlags, ...flags };
},
};
export default {
namespaced: true,
state,
getters,
actions,
mutations,
};

View File

@ -66,7 +66,6 @@ import captainLifecycleDeliveries from './captain/lifecycleDeliveries';
import captainUnits from './modules/captainUnits';
import captainGalleryItems from './modules/captainGalleryItems';
import captainReports from './modules/captainReports';
import captainNotificationTemplates from './captain/notificationTemplates';
const plugins = [];
@ -138,7 +137,6 @@ export default createStore({
captainUnits,
captainGalleryItems,
captainReports,
captainNotificationTemplates,
},
plugins,
});

View File

@ -158,9 +158,9 @@ class Whatsapp::IncomingMessageWuzapiService < Whatsapp::IncomingMessageBaseServ
# Se a mensagem vier do celular (outgoing) e a assinatura estiver ativa,
# e o conteúdo não parecer já ter uma assinatura (evita duplicar em ecos)
if is_outgoing && inbox_obj.message_signature_enabled? && content.present? && !content.start_with?('*[') && !content.start_with?('*')
if is_outgoing && inbox_obj.message_signature_enabled? && content.present? && !content.start_with?('*')
signature_name = inbox_obj.shift_signature_name
content = "*[ #{signature_name} ]*\n#{content}" if signature_name.present?
content = "*#{signature_name}*\n#{content}" if signature_name.present?
end
msg_params = {

View File

@ -175,7 +175,7 @@ class Whatsapp::Providers::WuzapiService < Whatsapp::Providers::BaseService
return content unless message.inbox.message_signature_enabled?
name = sender_name_for(message)
name.present? ? "*[ #{name} ]*\n#{content}" : content
name.present? ? "*#{name}*\n#{content}" : content
end
def reply_params(message)

View File

@ -275,8 +275,6 @@ Rails.application.routes.draw do
post :sync_templates, on: :member
get :health, on: :member
post :on_whatsapp, on: :member
resources :notification_templates, only: [:index, :create, :update, :destroy],
module: 'captain'
if ChatwootApp.enterprise?
resource :conference, only: %i[create destroy], controller: 'conference' do
get :token, on: :member

View File

@ -1,45 +0,0 @@
class Api::V1::Accounts::Captain::NotificationTemplatesController < Api::V1::Accounts::BaseController
before_action :set_inbox
before_action :set_template, only: [:update, :destroy]
def index
templates = @inbox.captain_notification_templates.ordered
render json: templates
end
def create
template = @inbox.captain_notification_templates.new(template_params)
if template.save
render json: template, status: :created
else
render json: { error: template.errors.full_messages.join(', ') }, status: :unprocessable_entity
end
end
def update
if @template.update(template_params)
render json: @template
else
render json: { error: @template.errors.full_messages.join(', ') }, status: :unprocessable_entity
end
end
def destroy
@template.destroy!
head :no_content
end
private
def set_inbox
@inbox = current_account.inboxes.find(params[:inbox_id])
end
def set_template
@template = @inbox.captain_notification_templates.find(params[:id])
end
def template_params
params.require(:notification_template).permit(:label, :content, :timing_minutes, :timing_direction, :active, :position)
end
end

View File

@ -1,41 +0,0 @@
class Captain::Notifications::NotificationScannerJob < ApplicationJob
queue_as :scheduled_jobs
# Tolerance window around the target time (job runs every 5 min, so ±5 min ensures coverage)
WINDOW_MINUTES = 5
def perform
Captain::NotificationTemplate.active.find_each do |template|
eligible_reservations_for(template).find_each do |reservation|
Captain::Notifications::SendNotificationService.new(reservation, template).perform
end
end
end
private
def eligible_reservations_for(template)
target_time = compute_target_time(template)
window_start = target_time - WINDOW_MINUTES.minutes
window_end = target_time + WINDOW_MINUTES.minutes
Captain::Reservation
.joins(:conversation)
.where(conversations: { inbox_id: template.inbox_id })
.where(status: Captain::Reservation.statuses.slice(:confirmed, :active).values)
.where(check_in_at: window_start..window_end)
.where.not(conversation_id: nil)
.where(
"NOT (captain_reservations.metadata->'notified_templates' @> ?::jsonb)",
"[#{template.id}]"
)
end
def compute_target_time(template)
if template.before?
template.timing_minutes.minutes.from_now
else
template.timing_minutes.minutes.ago
end
end
end

View File

@ -1,40 +0,0 @@
# == Schema Information
#
# Table name: captain_notification_templates
#
# id :bigint not null, primary key
# active :boolean default(TRUE), not null
# content :text not null
# label :string not null
# position :integer default(0), not null
# timing_direction :integer default("before"), not null
# timing_minutes :integer default(10), not null
# created_at :datetime not null
# updated_at :datetime not null
# inbox_id :bigint not null
#
# Indexes
#
# idx_notif_templates_inbox_active (inbox_id,active)
#
# Foreign Keys
#
# fk_rails_... (inbox_id => inboxes.id)
#
class Captain::NotificationTemplate < ApplicationRecord
self.table_name = 'captain_notification_templates'
belongs_to :inbox, inverse_of: :captain_notification_templates
enum timing_direction: { before: 0, after: 1 }
validates :label, presence: true
validates :content, presence: true
validates :timing_minutes, presence: true, numericality: { greater_than: 0 }
validates :timing_direction, presence: true
validates :inbox_id, presence: true
scope :active, -> { where(active: true) }
scope :ordered, -> { order(:position, :id) }
scope :for_inbox, ->(inbox_id) { where(inbox_id: inbox_id) }
end