Files
chatwoot/app/services/whatsapp/incoming_message_service_helpers.rb
de137e8297 feat: Capture CTWA referral metadata for WhatsApp conversations (#14681)
Adds support for capturing Click-to-WhatsApp ad referral metadata from
incoming WhatsApp messages.

This stores Meta Cloud API `referral` payloads on the incoming message
`content_attributes` and normalizes Twilio `Referral*` callback fields
into the same shape. The UI display is intentionally deferred until
Instagram, Messenger, and TikTok referral payloads are captured as well,
so we can design one cross-channel referral surface instead of a
WhatsApp-only sidebar block.

Why message-level storage

Meta sends CTWA referral details as part of the inbound message payload,
not as a stable conversation-level webhook. The referral represents the
exact ad click that produced that specific customer message, and later
messages in the same conversation may not carry the same context.
Storing the normalized referral on the message preserves the original
webhook semantics, avoids adding conversation-level attribution that
could become stale or ambiguous, and keeps support for both Cloud API
and Twilio payloads aligned behind `content_attributes.referral`.
Fixes
https://linear.app/chatwoot/issue/CW-7090/surface-meta-ctwa-referral-on-incoming-whatsapp-messages-cloud-api
Closes https://github.com/chatwoot/chatwoot/issues/13995,
https://github.com/chatwoot/chatwoot/issues/12560,
https://github.com/chatwoot/chatwoot/issues/13006


Related community PRs
- https://github.com/chatwoot/chatwoot/pull/13130
- https://github.com/chatwoot/chatwoot/pull/14180
- https://github.com/chatwoot/chatwoot/pull/14121

Related follow-ups
-
https://linear.app/chatwoot/issue/CW-7301/capture-instagram-ad-referral-metadata-on-incoming-messages
-
https://linear.app/chatwoot/issue/CW-7302/capture-messenger-ad-referral-metadata-on-facebook-page-conversations
-
https://linear.app/chatwoot/issue/CW-7303/capture-tiktok-ad-referral-metadata-from-im-referral-msg-events

How to test
1. Send or replay a WhatsApp Cloud API inbound message that contains a
`messages[0].referral` payload from a Click-to-WhatsApp ad.
2. Confirm the generated incoming message stores the payload under
`content_attributes.referral`.
3. Repeat with a Twilio WhatsApp callback containing `Referral*` fields
and confirm the stored message has the same normalized
`content_attributes.referral` structure.

---------

Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
2026-06-16 09:58:29 +04:00

92 lines
2.3 KiB
Ruby

module Whatsapp::IncomingMessageServiceHelpers
def download_attachment_file(attachment_payload)
Down.download(inbox.channel.media_url(attachment_payload[:id]), headers: inbox.channel.api_headers)
end
def conversation_params
{
account_id: @inbox.account_id,
inbox_id: @inbox.id,
contact_id: @contact.id,
contact_inbox_id: @contact_inbox.id
}
end
def processed_params
@processed_params ||= params
end
def account
@account ||= inbox.account
end
def message_type
messages_data.first[:type]
end
def message_content(message)
# TODO: map interactive messages back to button messages in chatwoot
message.dig(:text, :body) ||
message.dig(:button, :text) ||
message.dig(:interactive, :button_reply, :title) ||
message.dig(:interactive, :list_reply, :title) ||
message.dig(:name, :formatted_name)
end
def file_content_type(file_type)
return :image if %w[image sticker].include?(file_type)
return :audio if %w[audio voice].include?(file_type)
return :video if ['video'].include?(file_type)
return :location if ['location'].include?(file_type)
return :contact if ['contacts'].include?(file_type)
:file
end
def unprocessable_message_type?(message_type)
%w[reaction ephemeral request_welcome].include?(message_type)
end
def processed_waid(waid)
Whatsapp::PhoneNumberNormalizationService.new(inbox).normalize_and_find_contact_by_provider(waid, :cloud)
end
def whatsapp_phone_number(identifier)
identifier = identifier.to_s
return if identifier.blank?
return unless identifier.match?(/\A\d{1,15}\z/)
identifier
end
def error_webhook_event?(message)
message.key?('errors')
end
def log_error(message)
Rails.logger.warn "Whatsapp Error: #{message['errors'][0]['title']} - contact: #{message['from']}"
end
def process_in_reply_to(message)
@in_reply_to_external_id = message['context']&.[]('id')
end
def referral_attributes(message)
return {} if outgoing_echo
message[:referral]&.to_h&.deep_stringify_keys || {}
end
def find_message_by_source_id(source_id)
return unless source_id
@message = Message.find_by(source_id: source_id)
end
def lock_message_source_id!
return false if messages_data.blank?
Whatsapp::MessageDedupLock.new(messages_data.first[:id]).acquire!
end
end