@itleanchatbot/shared-models-js-postgres 3.1.3 → 3.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@itleanchatbot/shared-models-js-postgres",
3
- "version": "3.1.3",
3
+ "version": "3.1.6",
4
4
  "description": "Shared Models JS Postgres",
5
5
  "main": "index.js",
6
6
  "license": "ISC",
@@ -0,0 +1,236 @@
1
+ 'use strict';
2
+
3
+ module.exports = {
4
+ async up(queryInterface) {
5
+ await queryInterface.sequelize.query(`
6
+ DROP VIEW IF EXISTS channel_message_type_daily;
7
+
8
+ CREATE VIEW channel_message_type_daily AS
9
+ WITH base AS (
10
+ SELECT
11
+ ((bi."createdAt" AT TIME ZONE 'UTC') AT TIME ZONE 'America/Sao_Paulo')::date AS day,
12
+ bi."EnterpriseId" AS "EnterpriseId",
13
+ s."ChannelId" AS "SessionChannelId",
14
+
15
+ LOWER(
16
+ COALESCE(
17
+ NULLIF(bi.context::jsonb ->> 'channelType', ''),
18
+ NULLIF(bi.context::jsonb #>> '{inbound_message,channel}', ''),
19
+ NULLIF(bi.context::jsonb #>> '{inboundMessage,channel}', ''),
20
+ CASE
21
+ WHEN (bi.payload::jsonb #>> '{channelId}') = 'msteams'
22
+ OR (bi.context::jsonb #>> '{parsedMessage,channel,id}') = 'msteams'
23
+ THEN 'microsoft_teams' ELSE NULL END,
24
+ CASE
25
+ WHEN (bi.context::jsonb ->> 'channel') ILIKE 'whats%'
26
+ OR (bi.context::jsonb ->> 'channelType') ILIKE 'whats%'
27
+ THEN 'whatsapp_gupshup' ELSE NULL END
28
+ )
29
+ ) AS inferred_channel_type_raw,
30
+
31
+ COALESCE(
32
+ bi.context::jsonb #>> '{inbound_id}',
33
+ bi.payload::jsonb #>> '{inboundId}',
34
+ bi.context::jsonb #>> '{inbound_message,payload,id}',
35
+ bi.context::jsonb #>> '{inboundMessage,payload,id}',
36
+ bi.context::jsonb #>> '{inbound_message,id}',
37
+ bi.context::jsonb #>> '{inboundMessage,id}',
38
+ bi.payload::jsonb #>> '{transcription,jobId}',
39
+ bi.id::text
40
+ ) AS inbound_id,
41
+
42
+ NULLIF(
43
+ COALESCE(
44
+ bi.payload::jsonb #>> '{transcription,jobId}',
45
+ bi.payload::jsonb ->> 'jobId',
46
+ bi.context::jsonb #>> '{inbound_message,payload,transcription,jobId}',
47
+ bi.context::jsonb #>> '{inboundMessage,payload,transcription,jobId}'
48
+ ),
49
+ ''
50
+ ) AS audio_job_id,
51
+
52
+ /*
53
+ Flags explícitas de áudio (sem regex):
54
+ - contentType/mimetype começando com audio/
55
+ - type = 'audio'
56
+ - fallback por extensão usando ILIKE ANY
57
+ */
58
+ (
59
+ COALESCE(bi.payload::jsonb ->> 'mimetype', '') ILIKE 'audio/%'
60
+ OR COALESCE(bi.payload::jsonb ->> 'contentType', '') ILIKE 'audio/%'
61
+ OR COALESCE(bi.payload::jsonb #>> '{file,contentType}', '') ILIKE 'audio/%'
62
+ OR COALESCE(bi.context::jsonb #>> '{inbound_message,payload,payload,contentType}', '') ILIKE 'audio/%'
63
+ OR COALESCE(bi.context::jsonb #>> '{inboundMessage,payload,payload,contentType}', '') ILIKE 'audio/%'
64
+ OR COALESCE(bi.context::jsonb #>> '{inbound_message,payload,file,contentType}', '') ILIKE 'audio/%'
65
+ OR COALESCE(bi.context::jsonb #>> '{inboundMessage,payload,file,contentType}', '') ILIKE 'audio/%'
66
+ OR COALESCE(bi.context::jsonb #>> '{inbound_message,payload,payload,type}', '') = 'audio'
67
+ OR COALESCE(bi.context::jsonb #>> '{inboundMessage,payload,payload,type}', '') = 'audio'
68
+ OR LOWER(COALESCE(bi.payload::jsonb #>> '{file,name}', '')) ILIKE ANY (ARRAY['%.ogg','%.oga','%.mp3','%.m4a','%.wav','%.opus','%.ogg%','%.oga%','%.mp3%','%.m4a%','%.wav%','%.opus%'])
69
+ OR LOWER(COALESCE(bi.context::jsonb #>> '{inbound_message,payload,file,name}', '')) ILIKE ANY (ARRAY['%.ogg','%.oga','%.mp3','%.m4a','%.wav','%.opus','%.ogg%','%.oga%','%.mp3%','%.m4a%','%.wav%','%.opus%'])
70
+ OR LOWER(COALESCE(bi.context::jsonb #>> '{inboundMessage,payload,file,name}', '')) ILIKE ANY (ARRAY['%.ogg','%.oga','%.mp3','%.m4a','%.wav','%.opus','%.ogg%','%.oga%','%.mp3%','%.m4a%','%.wav%','%.opus%'])
71
+ OR LOWER(COALESCE(bi.payload::jsonb #>> '{file,url}', '')) ILIKE ANY (ARRAY['%.ogg%','%.oga%','%.mp3%','%.m4a%','%.wav%','%.opus%'])
72
+ OR LOWER(COALESCE(bi.context::jsonb #>> '{inbound_message,payload,file,url}', '')) ILIKE ANY (ARRAY['%.ogg%','%.oga%','%.mp3%','%.m4a%','%.wav%','%.opus%'])
73
+ OR LOWER(COALESCE(bi.context::jsonb #>> '{inboundMessage,payload,file,url}', '')) ILIKE ANY (ARRAY['%.ogg%','%.oga%','%.mp3%','%.m4a%','%.wav%','%.opus%'])
74
+ ) AS has_explicit_audio,
75
+
76
+ (
77
+ COALESCE(
78
+ bi.payload::jsonb #>> '{transcription,jobId}',
79
+ bi.payload::jsonb ->> 'jobId',
80
+ bi.context::jsonb #>> '{inbound_message,payload,transcription,jobId}',
81
+ bi.context::jsonb #>> '{inboundMessage,payload,transcription,jobId}'
82
+ ) IS NOT NULL
83
+ AND COALESCE(
84
+ NULLIF(bi.payload::jsonb #>> '{transcription,jobId}', ''),
85
+ NULLIF(bi.payload::jsonb ->> 'jobId', ''),
86
+ NULLIF(bi.context::jsonb #>> '{inbound_message,payload,transcription,jobId}', ''),
87
+ NULLIF(bi.context::jsonb #>> '{inboundMessage,payload,transcription,jobId}', '')
88
+ ) IS NOT NULL
89
+ ) AS has_stt_job
90
+
91
+ FROM "BotIterables" bi
92
+ LEFT JOIN "Sessions" s ON s.id = bi."SessionId"
93
+ WHERE bi.action = 'client'
94
+ AND (
95
+ (bi.context::jsonb ? 'inbound_message') OR
96
+ (bi.context::jsonb ? 'inboundMessage') OR
97
+ (bi.context::jsonb ? 'inbound_id') OR
98
+ (bi.payload::jsonb ? 'inboundId')
99
+ )
100
+ ),
101
+
102
+ norm AS (
103
+ SELECT
104
+ b.*,
105
+ CASE
106
+ WHEN inferred_channel_type_raw IN ('microsoft_teams','msteams','teams') THEN 'microsoft_teams'
107
+ WHEN inferred_channel_type_raw IN ('webchat','webchat-transcript') THEN 'webchat'
108
+ WHEN inferred_channel_type_raw LIKE 'whatsapp%' THEN 'whatsapp_gupshup'
109
+ ELSE NULL
110
+ END AS "channelFamily"
111
+ FROM base b
112
+ ),
113
+
114
+ typed AS (
115
+ SELECT
116
+ n.*,
117
+ CASE
118
+ WHEN n."channelFamily" = 'microsoft_teams' THEN FALSE
119
+ ELSE (n.has_explicit_audio OR (n.has_stt_job AND n."channelFamily" IN ('webchat','whatsapp_gupshup')))
120
+ END AS has_audio_flag
121
+ FROM norm n
122
+ ),
123
+
124
+ consolidated AS (
125
+ SELECT
126
+ t.day,
127
+ t."EnterpriseId",
128
+ t."SessionChannelId",
129
+ t."channelFamily",
130
+ FIRST_VALUE(t.inferred_channel_type_raw) OVER w AS rawChannelTypeAny,
131
+ BOOL_OR(t.has_audio_flag) OVER w AS any_audio_flag,
132
+ FIRST_VALUE(t.audio_job_id) OVER w AS audio_job_id_any,
133
+ t.inbound_id
134
+ FROM typed t
135
+ WINDOW w AS (PARTITION BY t.inbound_id, t."SessionChannelId")
136
+ ),
137
+
138
+ msgs_final AS (
139
+ SELECT
140
+ c.day,
141
+ c."EnterpriseId",
142
+ c."SessionChannelId",
143
+ c."channelFamily",
144
+ c.rawChannelTypeAny AS "rawChannelName_from_infer",
145
+ c.inbound_id,
146
+ CASE WHEN c.any_audio_flag THEN 'audio' ELSE 'text' END AS "messageType"
147
+ FROM consolidated c
148
+ )
149
+
150
+ SELECT
151
+ m.day,
152
+ m."EnterpriseId",
153
+ COALESCE(ch2.id, m."SessionChannelId") AS "ChannelId",
154
+ CASE m."channelFamily"
155
+ WHEN 'microsoft_teams' THEN 'Microsoft Teams'
156
+ WHEN 'webchat' THEN 'WebChat'
157
+ WHEN 'whatsapp_gupshup' THEN 'WhatsApp'
158
+ ELSE COALESCE(m."rawChannelName_from_infer", ch.name)
159
+ END AS "channelName",
160
+ COALESCE(m."rawChannelName_from_infer", ch.name) AS "rawChannelName",
161
+ m."channelFamily",
162
+ m."messageType",
163
+ COUNT(DISTINCT m.inbound_id) AS total
164
+ FROM msgs_final m
165
+ LEFT JOIN "Channels" ch ON ch.id = m."SessionChannelId"
166
+ LEFT JOIN "Channels" ch2 ON ch2."EnterpriseId" = m."EnterpriseId"
167
+ AND (
168
+ (m."channelFamily" = 'microsoft_teams' AND ch2.name ILIKE '%teams%')
169
+ OR (m."channelFamily" = 'webchat' AND ch2.name ILIKE '%webchat%')
170
+ OR (m."channelFamily" = 'whatsapp_gupshup' AND (ch2.name ILIKE '%whatsapp%' OR ch2.name ILIKE '%gupshup%' OR ch2.name ILIKE '%whats%'))
171
+ )
172
+ GROUP BY
173
+ m.day,
174
+ m."EnterpriseId",
175
+ COALESCE(ch2.id, m."SessionChannelId"),
176
+ CASE m."channelFamily"
177
+ WHEN 'microsoft_teams' THEN 'Microsoft Teams'
178
+ WHEN 'webchat' THEN 'WebChat'
179
+ WHEN 'whatsapp_gupshup' THEN 'WhatsApp'
180
+ ELSE COALESCE(m."rawChannelName_from_infer", ch.name)
181
+ END,
182
+ COALESCE(m."rawChannelName_from_infer", ch.name),
183
+ m."channelFamily",
184
+ m."messageType";
185
+
186
+
187
+
188
+ `);
189
+ },
190
+
191
+
192
+ async down(queryInterface) {
193
+ await queryInterface.sequelize.query(`
194
+ DROP VIEW IF EXISTS channel_message_type_daily;
195
+
196
+ CREATE VIEW channel_message_type_daily AS
197
+ WITH base AS (
198
+ SELECT
199
+ ((bi."createdAt" AT TIME ZONE 'UTC') AT TIME ZONE 'America/Sao_Paulo')::date AS day,
200
+ bi."EnterpriseId" AS "EnterpriseId",
201
+ s."ChannelId" AS "ChannelId",
202
+ ch.name AS "channelName",
203
+ COALESCE(
204
+ bi.context::jsonb #>> '{inbound_id}',
205
+ bi.payload::jsonb #>> '{inboundId}',
206
+ bi.context::jsonb #>> '{inbound_message,payload,id}',
207
+ bi.context::jsonb #>> '{inboundMessage,payload,id}',
208
+ bi.context::jsonb #>> '{inbound_message,id}',
209
+ bi.context::jsonb #>> '{inboundMessage,id}',
210
+ bi.payload::jsonb #>> '{transcription,jobId}',
211
+ bi.id::text
212
+ ) AS inbound_id,
213
+ CASE WHEN
214
+ (bi.context::jsonb #>> '{inbound_message,payload,payload,type}') = 'audio' OR
215
+ (bi.context::jsonb #>> '{inboundMessage,payload,payload,type}') = 'audio' OR
216
+ (bi.context::jsonb #>> '{inbound_message,payload,payload,contentType}') ILIKE 'audio/%' OR
217
+ (bi.context::jsonb #>> '{inboundMessage,payload,payload,contentType}') ILIKE 'audio/%' OR
218
+ ((bi.payload::jsonb ->> 'mimetype') ILIKE 'audio/%')
219
+ THEN 'audio' ELSE 'text' END AS "messageType"
220
+ FROM "BotIterables" bi
221
+ LEFT JOIN "Sessions" s ON s.id = bi."SessionId"
222
+ LEFT JOIN "Channels" ch ON ch.id = s."ChannelId"
223
+ WHERE bi.action = 'client'
224
+ AND (
225
+ (bi.context::jsonb ? 'inbound_message') OR
226
+ (bi.context::jsonb ? 'inboundMessage')
227
+ )
228
+ )
229
+ SELECT
230
+ day, "EnterpriseId", "ChannelId", "channelName", "messageType",
231
+ COUNT(DISTINCT inbound_id) AS total
232
+ FROM base
233
+ GROUP BY day, "EnterpriseId", "ChannelId", "channelName", "messageType";
234
+ `);
235
+ }
236
+ };
@@ -1,43 +0,0 @@
1
- /** 20250819-create-view-channel_message_type_daily */
2
- module.exports = {
3
- up: async queryInterface => {
4
- await queryInterface.sequelize.query(`
5
- DROP VIEW IF EXISTS channel_message_type_daily;
6
- CREATE VIEW channel_message_type_daily AS
7
- SELECT
8
- (bi."createdAt")::date AS day,
9
- bi."EnterpriseId" AS "EnterpriseId",
10
- s."ChannelId" AS "ChannelId",
11
- ch.name AS "channelName",
12
- CASE
13
- WHEN
14
- -- transcrição presente no inbound_message => áudio
15
- ((bi.context::jsonb #> '{inbound_message,payload,payload}') ? 'transcription') OR
16
- ((bi.context::jsonb #> '{inboundMessage,payload,payload}') ? 'transcription') OR
17
- -- tipo audio no inbound
18
- ((bi.context::jsonb #>> '{inbound_message,payload,payload,type}') = 'audio') OR
19
- ((bi.context::jsonb #>> '{inboundMessage,payload,payload,type}') = 'audio') OR
20
- -- contentType de áudio em qualquer um dos lugares usuais
21
- ((bi.context::jsonb #>> '{inbound_message,payload,payload,contentType}') ILIKE 'audio/%') OR
22
- ((bi.context::jsonb #>> '{inboundMessage,payload,payload,contentType}') ILIKE 'audio/%') OR
23
- ((coalesce(bi.payload, '{}'::jsonb) ->> 'mimetype') ILIKE 'audio/%') OR
24
- -- fallback mínimo: nossa flag explícita
25
- (bi.message = 'whatsapp_gupshup_audio')
26
- THEN 'audio'
27
- ELSE 'text'
28
- END AS "messageType",
29
- COUNT(*) AS total
30
- FROM "BotIterables" bi
31
- LEFT JOIN "Sessions" s ON s.id = bi."SessionId"
32
- LEFT JOIN "Channels" ch ON ch.id = s."ChannelId"
33
- WHERE bi.action = 'client'
34
- AND bi.type = 'message'
35
- GROUP BY day, bi."EnterpriseId", s."ChannelId", ch.name, "messageType";
36
- `)
37
- },
38
- down: async queryInterface => {
39
- await queryInterface.sequelize.query(`
40
- DROP VIEW IF EXISTS channel_message_type_daily;
41
- `)
42
- },
43
- }
@@ -1,61 +0,0 @@
1
- 'use strict'
2
- /** @type {import('sequelize-cli').Migration} */
3
- module.exports = {
4
- async up(queryInterface) {
5
- await queryInterface.sequelize.query(`
6
- DROP VIEW IF EXISTS channel_message_type_daily;
7
- CREATE VIEW channel_message_type_daily AS
8
- WITH base AS (
9
- SELECT
10
- /* Ajuste de fuso antes do ::date */
11
- ((bi."createdAt" AT TIME ZONE 'UTC') AT TIME ZONE 'America/Sao_Paulo')::date AS day,
12
- bi."EnterpriseId" AS "EnterpriseId",
13
- s."ChannelId" AS "ChannelId",
14
- ch.name AS "channelName",
15
- /* Um id “estável” do inbound para evitar duplicatas (quando existir) */
16
- COALESCE(
17
- bi.context::jsonb #>> '{inbound_message,payload,id}',
18
- bi.context::jsonb #>> '{inboundMessage,payload,id}',
19
- bi.context::jsonb #>> '{inbound_message,id}',
20
- bi.context::jsonb #>> '{inboundMessage,id}',
21
- bi.id::text
22
- ) AS inbound_id,
23
- /* Regra de áudio bem ampla, sempre em jsonb */
24
- CASE WHEN
25
- /* type='audio' em variações do payload */
26
- ((bi.context::jsonb #>> '{inbound_message,payload,payload,type}') = 'audio') OR
27
- ((bi.context::jsonb #>> '{inboundMessage,payload,payload,type}') = 'audio') OR
28
- /* contentType indicando áudio */
29
- ((bi.context::jsonb #>> '{inbound_message,payload,payload,contentType}') ILIKE 'audio/%') OR
30
- ((bi.context::jsonb #>> '{inboundMessage,payload,payload,contentType}') ILIKE 'audio/%') OR
31
- /* mimetype salvo em payload do BotIterable (ex.: webchat/whats) */
32
- ((bi.payload::jsonb ->> 'mimetype') ILIKE 'audio/%')
33
- THEN 'audio'
34
- ELSE 'text'
35
- END AS "messageType"
36
- FROM "BotIterables" bi
37
- LEFT JOIN "Sessions" s ON s.id = bi."SessionId"
38
- LEFT JOIN "Channels" ch ON ch.id = s."ChannelId"
39
- WHERE bi.action = 'client'
40
- AND bi.type = 'message'
41
- /* Garante que é mensagem de entrada “real”
42
- (descarta os BotIterables sintéticos do audio-status) */
43
- AND (
44
- (bi.context::jsonb ? 'inbound_message')
45
- OR
46
- (bi.context::jsonb ? 'inboundMessage')
47
- )
48
- )
49
- SELECT
50
- day, "EnterpriseId", "ChannelId", "channelName", "messageType",
51
- COUNT(DISTINCT inbound_id) AS total
52
- FROM base
53
- GROUP BY day, "EnterpriseId", "ChannelId", "channelName", "messageType";
54
- `)
55
- },
56
- async down(queryInterface) {
57
- await queryInterface.sequelize.query(
58
- `DROP VIEW IF EXISTS channel_message_type_daily;`,
59
- )
60
- },
61
- }