@@ -49,6 +49,14 @@ export class ChatwootService {
4949
5050 private provider : any ;
5151
52+ // Cache para deduplicação de orderMessage (evita mensagens duplicadas)
53+ private processedOrderIds : Map < string , number > = new Map ( ) ;
54+ private readonly ORDER_CACHE_TTL_MS = 30000 ; // 30 segundos
55+
56+ // Cache para mapeamento LID → Número Normal (resolve problema de @lid)
57+ private lidToPhoneMap : Map < string , { phone : string ; timestamp : number } > = new Map ( ) ;
58+ private readonly LID_CACHE_TTL_MS = 3600000 ; // 1 hora
59+
5260 constructor (
5361 private readonly waMonitor : WAMonitoringService ,
5462 private readonly configService : ConfigService ,
@@ -632,10 +640,32 @@ export class ChatwootService {
632640 public async createConversation ( instance : InstanceDto , body : any ) {
633641 const isLid = body . key . addressingMode === 'lid' ;
634642 const isGroup = body . key . remoteJid . endsWith ( '@g.us' ) ;
635- const phoneNumber = isLid && ! isGroup ? body . key . remoteJidAlt : body . key . remoteJid ;
636- const { remoteJid } = body . key ;
637- const cacheKey = `${ instance . instanceName } :createConversation-${ remoteJid } ` ;
638- const lockKey = `${ instance . instanceName } :lock:createConversation-${ remoteJid } ` ;
643+ let phoneNumber = isLid && ! isGroup ? body . key . remoteJidAlt : body . key . remoteJid ;
644+ let { remoteJid } = body . key ;
645+
646+ // CORREÇÃO LID: Resolve LID para número normal antes de processar
647+ if ( isLid && ! isGroup ) {
648+ const resolvedPhone = await this . resolveLidToPhone ( instance , body . key ) ;
649+
650+ if ( resolvedPhone && resolvedPhone !== remoteJid ) {
651+ this . logger . verbose ( `LID detected and resolved: ${ remoteJid } → ${ resolvedPhone } ` ) ;
652+ phoneNumber = resolvedPhone ;
653+
654+ // Salva mapeamento se temos remoteJidAlt
655+ if ( body . key . remoteJidAlt ) {
656+ this . saveLidMapping ( remoteJid , body . key . remoteJidAlt ) ;
657+ }
658+ } else if ( body . key . remoteJidAlt ) {
659+ // Se não resolveu mas tem remoteJidAlt, usa ele
660+ phoneNumber = body . key . remoteJidAlt ;
661+ this . saveLidMapping ( remoteJid , body . key . remoteJidAlt ) ;
662+ this . logger . verbose ( `Using remoteJidAlt for LID: ${ remoteJid } → ${ phoneNumber } ` ) ;
663+ }
664+ }
665+
666+ // Usa phoneNumber como base para cache (não o LID)
667+ const cacheKey = `${ instance . instanceName } :createConversation-${ phoneNumber } ` ;
668+ const lockKey = `${ instance . instanceName } :lock:createConversation-${ phoneNumber } ` ;
639669 const maxWaitTime = 5000 ; // 5 seconds
640670 const client = await this . clientCw ( instance ) ;
641671 if ( ! client ) return null ;
@@ -943,20 +973,39 @@ export class ChatwootService {
943973
944974 const sourceReplyId = quotedMsg ?. chatwootMessageId || null ;
945975
976+ // Filtra valores null/undefined do content_attributes para evitar erro 406
977+ const filteredReplyToIds = Object . fromEntries (
978+ Object . entries ( replyToIds ) . filter ( ( [ _ , value ] ) => value != null )
979+ ) ;
980+
981+ // Monta o objeto data, incluindo content_attributes apenas se houver dados válidos
982+ const messageData : any = {
983+ content : content ,
984+ message_type : messageType ,
985+ content_type : 'text' , // Explicitamente define como texto para Chatwoot 4.x
986+ attachments : attachments ,
987+ private : privateMessage || false ,
988+ } ;
989+
990+ // Adiciona source_id apenas se existir
991+ if ( sourceId ) {
992+ messageData . source_id = sourceId ;
993+ }
994+
995+ // Adiciona content_attributes apenas se houver dados válidos
996+ if ( Object . keys ( filteredReplyToIds ) . length > 0 ) {
997+ messageData . content_attributes = filteredReplyToIds ;
998+ }
999+
1000+ // Adiciona source_reply_id apenas se existir
1001+ if ( sourceReplyId ) {
1002+ messageData . source_reply_id = sourceReplyId . toString ( ) ;
1003+ }
1004+
9461005 const message = await client . messages . create ( {
9471006 accountId : this . provider . accountId ,
9481007 conversationId : conversationId ,
949- data : {
950- content : content ,
951- message_type : messageType ,
952- attachments : attachments ,
953- private : privateMessage || false ,
954- source_id : sourceId ,
955- content_attributes : {
956- ...replyToIds ,
957- } ,
958- source_reply_id : sourceReplyId ? sourceReplyId . toString ( ) : null ,
959- } ,
1008+ data : messageData ,
9601009 } ) ;
9611010
9621011 if ( ! message ) {
@@ -1082,11 +1131,14 @@ export class ChatwootService {
10821131 if ( messageBody && instance ) {
10831132 const replyToIds = await this . getReplyToIds ( messageBody , instance ) ;
10841133
1085- if ( replyToIds . in_reply_to || replyToIds . in_reply_to_external_id ) {
1086- const content = JSON . stringify ( {
1087- ...replyToIds ,
1088- } ) ;
1089- data . append ( 'content_attributes' , content ) ;
1134+ // Filtra valores null/undefined antes de enviar
1135+ const filteredReplyToIds = Object . fromEntries (
1136+ Object . entries ( replyToIds ) . filter ( ( [ _ , value ] ) => value != null )
1137+ ) ;
1138+
1139+ if ( Object . keys ( filteredReplyToIds ) . length > 0 ) {
1140+ const contentAttrs = JSON . stringify ( filteredReplyToIds ) ;
1141+ data . append ( 'content_attributes' , contentAttrs ) ;
10901142 }
10911143 }
10921144
@@ -1789,41 +1841,127 @@ export class ChatwootService {
17891841 }
17901842
17911843 private getTypeMessage ( msg : any ) {
1792- const types = {
1793- conversation : msg . conversation ,
1794- imageMessage : msg . imageMessage ?. caption ,
1795- videoMessage : msg . videoMessage ?. caption ,
1796- extendedTextMessage : msg . extendedTextMessage ?. text ,
1797- messageContextInfo : msg . messageContextInfo ?. stanzaId ,
1798- stickerMessage : undefined ,
1799- documentMessage : msg . documentMessage ?. caption ,
1800- documentWithCaptionMessage : msg . documentWithCaptionMessage ?. message ?. documentMessage ?. caption ,
1801- audioMessage : msg . audioMessage ? ( msg . audioMessage . caption ?? '' ) : undefined ,
1802- contactMessage : msg . contactMessage ?. vcard ,
1803- contactsArrayMessage : msg . contactsArrayMessage ,
1804- locationMessage : msg . locationMessage ,
1805- liveLocationMessage : msg . liveLocationMessage ,
1806- listMessage : msg . listMessage ,
1807- listResponseMessage : msg . listResponseMessage ,
1808- viewOnceMessageV2 :
1809- msg ?. message ?. viewOnceMessageV2 ?. message ?. imageMessage ?. url ||
1810- msg ?. message ?. viewOnceMessageV2 ?. message ?. videoMessage ?. url ||
1811- msg ?. message ?. viewOnceMessageV2 ?. message ?. audioMessage ?. url ,
1812- } ;
1813-
1814- return types ;
1815- }
1844+ const types = {
1845+ conversation : msg . conversation ,
1846+ imageMessage : msg . imageMessage ?. caption ,
1847+ videoMessage : msg . videoMessage ?. caption ,
1848+ extendedTextMessage : msg . extendedTextMessage ?. text ,
1849+ messageContextInfo : msg . messageContextInfo ?. stanzaId ,
1850+ stickerMessage : undefined ,
1851+ documentMessage : msg . documentMessage ?. caption ,
1852+ documentWithCaptionMessage : msg . documentWithCaptionMessage ?. message ?. documentMessage ?. caption ,
1853+ audioMessage : msg . audioMessage ? ( msg . audioMessage . caption ?? '' ) : undefined ,
1854+ contactMessage : msg . contactMessage ?. vcard ,
1855+ contactsArrayMessage : msg . contactsArrayMessage ,
1856+ locationMessage : msg . locationMessage ,
1857+ liveLocationMessage : msg . liveLocationMessage ,
1858+ listMessage : msg . listMessage ,
1859+ listResponseMessage : msg . listResponseMessage ,
1860+ orderMessage : msg . orderMessage ,
1861+ quotedProductMessage : msg . contextInfo ?. quotedMessage ?. productMessage ,
1862+ viewOnceMessageV2 :
1863+ msg ?. message ?. viewOnceMessageV2 ?. message ?. imageMessage ?. url ||
1864+ msg ?. message ?. viewOnceMessageV2 ?. message ?. videoMessage ?. url ||
1865+ msg ?. message ?. viewOnceMessageV2 ?. message ?. audioMessage ?. url ,
1866+ } ;
1867+
1868+ return types ;
1869+ }
18161870
18171871 private getMessageContent ( types : any ) {
18181872 const typeKey = Object . keys ( types ) . find ( ( key ) => types [ key ] !== undefined ) ;
18191873
18201874 let result = typeKey ? types [ typeKey ] : undefined ;
18211875
1822- // Remove externalAdReplyBody| in Chatwoot (Already Have)
1876+ // Remove externalAdReplyBody| in Chatwoot
18231877 if ( result && typeof result === 'string' && result . includes ( 'externalAdReplyBody|' ) ) {
18241878 result = result . split ( 'externalAdReplyBody|' ) . filter ( Boolean ) . join ( '' ) ;
18251879 }
18261880
1881+ // Tratamento de Pedidos do Catálogo (WhatsApp Business Catalog)
1882+ if ( typeKey === 'orderMessage' && result . orderId ) {
1883+ const now = Date . now ( ) ;
1884+ // Limpa entradas antigas do cache
1885+ this . processedOrderIds . forEach ( ( timestamp , id ) => {
1886+ if ( now - timestamp > this . ORDER_CACHE_TTL_MS ) {
1887+ this . processedOrderIds . delete ( id ) ;
1888+ }
1889+ } ) ;
1890+ // Verifica se já processou este orderId
1891+ if ( this . processedOrderIds . has ( result . orderId ) ) {
1892+ return undefined ; // Ignora duplicado
1893+ }
1894+ this . processedOrderIds . set ( result . orderId , now ) ;
1895+ }
1896+ // Tratamento de Produto citado (WhatsApp Desktop)
1897+ if ( typeKey === 'quotedProductMessage' && result ?. product ) {
1898+ const product = result . product ;
1899+
1900+ // Extrai preço
1901+ let rawPrice = 0 ;
1902+ const amount = product . priceAmount1000 ;
1903+
1904+ if ( Long . isLong ( amount ) ) {
1905+ rawPrice = amount . toNumber ( ) ;
1906+ } else if ( amount && typeof amount === 'object' && 'low' in amount ) {
1907+ rawPrice = Long . fromValue ( amount ) . toNumber ( ) ;
1908+ } else if ( typeof amount === 'number' ) {
1909+ rawPrice = amount ;
1910+ }
1911+
1912+ const price = ( rawPrice / 1000 ) . toLocaleString ( 'pt-BR' , {
1913+ style : 'currency' ,
1914+ currency : product . currencyCode || 'BRL' ,
1915+ } ) ;
1916+
1917+ const productTitle = product . title || 'Produto do catálogo' ;
1918+ const productId = product . productId || 'N/A' ;
1919+
1920+ return (
1921+ `🛒 *PRODUTO DO CATÁLOGO (Desktop)*\n` +
1922+ `━━━━━━━━━━━━━━━━━━━━━\n` +
1923+ `📦 *Produto:* ${ productTitle } \n` +
1924+ `💰 *Preço:* ${ price } \n` +
1925+ `🆔 *Código:* ${ productId } \n` +
1926+ `━━━━━━━━━━━━━━━━━━━━━\n` +
1927+ `_Cliente perguntou: "${ types . conversation || 'Me envia este produto?' } "_`
1928+ ) ;
1929+ }
1930+ if ( typeKey === 'orderMessage' ) {
1931+ // Extrai o valor - pode ser Long, objeto {low, high}, ou número direto
1932+ let rawPrice = 0 ;
1933+ const amount = result . totalAmount1000 ;
1934+
1935+ if ( Long . isLong ( amount ) ) {
1936+ rawPrice = amount . toNumber ( ) ;
1937+ } else if ( amount && typeof amount === 'object' && 'low' in amount ) {
1938+ // Formato {low: number, high: number, unsigned: boolean}
1939+ rawPrice = Long . fromValue ( amount ) . toNumber ( ) ;
1940+ } else if ( typeof amount === 'number' ) {
1941+ rawPrice = amount ;
1942+ }
1943+
1944+ const price = ( rawPrice / 1000 ) . toLocaleString ( 'pt-BR' , {
1945+ style : 'currency' ,
1946+ currency : result . totalCurrencyCode || 'BRL' ,
1947+ } ) ;
1948+
1949+ const itemCount = result . itemCount || 1 ;
1950+ const orderTitle = result . orderTitle || 'Produto do catálogo' ;
1951+ const orderId = result . orderId || 'N/A' ;
1952+
1953+ return (
1954+ `🛒 *NOVO PEDIDO NO CATÁLOGO*\n` +
1955+ `━━━━━━━━━━━━━━━━━━━━━\n` +
1956+ `📦 *Produto:* ${ orderTitle } \n` +
1957+ `📊 *Quantidade:* ${ itemCount } \n` +
1958+ `💰 *Total:* ${ price } \n` +
1959+ `🆔 *Pedido:* #${ orderId } \n` +
1960+ `━━━━━━━━━━━━━━━━━━━━━\n` +
1961+ `_Responda para atender este pedido!_`
1962+ ) ;
1963+ }
1964+
18271965 if ( typeKey === 'locationMessage' || typeKey === 'liveLocationMessage' ) {
18281966 const latitude = result . degreesLatitude ;
18291967 const longitude = result . degreesLongitude ;
@@ -2024,6 +2162,29 @@ export class ChatwootService {
20242162 }
20252163 }
20262164
2165+ // CORREÇÃO LID: Resolve LID para número normal antes de processar evento
2166+ if ( body ?. key ?. remoteJid && body . key . remoteJid . includes ( '@lid' ) && ! body . key . remoteJid . endsWith ( '@g.us' ) ) {
2167+ const originalJid = body . key . remoteJid ;
2168+ const resolvedPhone = await this . resolveLidToPhone ( instance , body . key ) ;
2169+
2170+ if ( resolvedPhone && resolvedPhone !== originalJid ) {
2171+ this . logger . verbose ( `Event LID resolved: ${ originalJid } → ${ resolvedPhone } ` ) ;
2172+ body . key . remoteJid = resolvedPhone ;
2173+
2174+ // Salva mapeamento se temos remoteJidAlt
2175+ if ( body . key . remoteJidAlt ) {
2176+ this . saveLidMapping ( originalJid , body . key . remoteJidAlt ) ;
2177+ }
2178+ } else if ( body . key . remoteJidAlt && ! body . key . remoteJidAlt . includes ( '@lid' ) ) {
2179+ // Se não resolveu mas tem remoteJidAlt válido, usa ele
2180+ this . logger . verbose ( `Using remoteJidAlt for event: ${ originalJid } → ${ body . key . remoteJidAlt } ` ) ;
2181+ body . key . remoteJid = body . key . remoteJidAlt ;
2182+ this . saveLidMapping ( originalJid , body . key . remoteJidAlt ) ;
2183+ } else {
2184+ this . logger . warn ( `Could not resolve LID for event, keeping original: ${ originalJid } ` ) ;
2185+ }
2186+ }
2187+
20272188 if ( event === 'messages.upsert' || event === 'send.message' ) {
20282189 this . logger . info ( `[${ event } ] New message received - Instance: ${ JSON . stringify ( body , null , 2 ) } ` ) ;
20292190 if ( body . key . remoteJid === 'status@broadcast' ) {
@@ -2568,6 +2729,82 @@ export class ChatwootService {
25682729 return remoteJid . replace ( / : \d + / , '' ) . split ( '@' ) [ 0 ] ;
25692730 }
25702731
2732+ /**
2733+ * Limpa entradas antigas do cache de mapeamento LID
2734+ */
2735+ private cleanLidCache ( ) {
2736+ const now = Date . now ( ) ;
2737+ this . lidToPhoneMap . forEach ( ( value , lid ) => {
2738+ if ( now - value . timestamp > this . LID_CACHE_TTL_MS ) {
2739+ this . lidToPhoneMap . delete ( lid ) ;
2740+ }
2741+ } ) ;
2742+ }
2743+
2744+ /**
2745+ * Salva mapeamento LID → Número Normal
2746+ */
2747+ private saveLidMapping ( lid : string , phoneNumber : string ) {
2748+ if ( ! lid || ! phoneNumber || ! lid . includes ( '@lid' ) ) {
2749+ return ;
2750+ }
2751+
2752+ this . cleanLidCache ( ) ;
2753+ this . lidToPhoneMap . set ( lid , {
2754+ phone : phoneNumber ,
2755+ timestamp : Date . now ( ) ,
2756+ } ) ;
2757+
2758+ this . logger . verbose ( `LID mapping saved: ${ lid } → ${ phoneNumber } ` ) ;
2759+ }
2760+
2761+ /**
2762+ * Resolve LID para Número Normal
2763+ * Retorna o número normal se encontrado, ou o LID original se não encontrado
2764+ */
2765+ private async resolveLidToPhone ( instance : InstanceDto , messageKey : any ) : Promise < string | null > {
2766+ const { remoteJid, remoteJidAlt } = messageKey ;
2767+
2768+ // Se não for LID, retorna o próprio remoteJid
2769+ if ( ! remoteJid || ! remoteJid . includes ( '@lid' ) ) {
2770+ return remoteJid ;
2771+ }
2772+
2773+ // 1. Tenta buscar no cache
2774+ const cached = this . lidToPhoneMap . get ( remoteJid ) ;
2775+ if ( cached ) {
2776+ this . logger . verbose ( `LID resolved from cache: ${ remoteJid } → ${ cached . phone } ` ) ;
2777+ return cached . phone ;
2778+ }
2779+
2780+ // 2. Se tem remoteJidAlt (número alternativo), usa ele e salva no cache
2781+ if ( remoteJidAlt && ! remoteJidAlt . includes ( '@lid' ) ) {
2782+ this . saveLidMapping ( remoteJid , remoteJidAlt ) ;
2783+ this . logger . verbose ( `LID resolved from remoteJidAlt: ${ remoteJid } → ${ remoteJidAlt } ` ) ;
2784+ return remoteJidAlt ;
2785+ }
2786+
2787+ // 3. Tenta buscar no banco de dados do Chatwoot
2788+ try {
2789+ const lidIdentifier = this . normalizeJidIdentifier ( remoteJid ) ;
2790+ const contact = await this . findContactByIdentifier ( instance , lidIdentifier ) ;
2791+
2792+ if ( contact && contact . phone_number ) {
2793+ // Converte +554498860240 → 554498860240@s.whatsapp.net
2794+ const phoneNumber = contact . phone_number . replace ( '+' , '' ) + '@s.whatsapp.net' ;
2795+ this . saveLidMapping ( remoteJid , phoneNumber ) ;
2796+ this . logger . verbose ( `LID resolved from database: ${ remoteJid } → ${ phoneNumber } ` ) ;
2797+ return phoneNumber ;
2798+ }
2799+ } catch ( error ) {
2800+ this . logger . warn ( `Error resolving LID from database: ${ error } ` ) ;
2801+ }
2802+
2803+ // 4. Se não encontrou, retorna null (será necessário criar novo contato)
2804+ this . logger . warn ( `Could not resolve LID: ${ remoteJid } ` ) ;
2805+ return null ;
2806+ }
2807+
25712808 public startImportHistoryMessages ( instance : InstanceDto ) {
25722809 if ( ! this . isImportHistoryAvailable ( ) ) {
25732810 return ;
0 commit comments