From 609437ee9292d9c848e5090ef13cdc3a0e9da4f2 Mon Sep 17 00:00:00 2001 From: Muhsin Keloth Date: Thu, 31 Jul 2025 14:15:08 +0400 Subject: [PATCH] chore: remove authentication related templates --- .../whatsapp/WhatsAppTemplateParser.vue | 6 +- .../helper/specs/templateHelper.spec.js | 149 -------- .../dashboard/helper/templateHelper.js | 86 +---- .../modules/specs/inboxes/templateFixtures.js | 76 ---- .../whatsapp/template_processor_service.rb | 339 ++++-------------- .../whatsapp/oneoff_campaign_service_spec.rb | 11 +- 6 files changed, 98 insertions(+), 569 deletions(-) diff --git a/app/javascript/dashboard/components-next/whatsapp/WhatsAppTemplateParser.vue b/app/javascript/dashboard/components-next/whatsapp/WhatsAppTemplateParser.vue index a49a8df6e..f651353be 100644 --- a/app/javascript/dashboard/components-next/whatsapp/WhatsAppTemplateParser.vue +++ b/app/javascript/dashboard/components-next/whatsapp/WhatsAppTemplateParser.vue @@ -9,7 +9,6 @@ import { buildTemplateParameters, allKeysRequired, replaceTemplateVariables, - populateAuthenticationButtonParameters, } from 'dashboard/helper/templateHelper'; const props = defineProps({ @@ -92,10 +91,7 @@ const sendMessage = () => { v$.value.$touch(); if (v$.value.$invalid) return; - const finalParams = populateAuthenticationButtonParameters( - props.template, - processedParams.value - ); + const finalParams = processedParams.value; const payload = { message: renderedTemplate.value, diff --git a/app/javascript/dashboard/helper/specs/templateHelper.spec.js b/app/javascript/dashboard/helper/specs/templateHelper.spec.js index 16b669e15..6ee945701 100644 --- a/app/javascript/dashboard/helper/specs/templateHelper.spec.js +++ b/app/javascript/dashboard/helper/specs/templateHelper.spec.js @@ -3,13 +3,11 @@ import { buildTemplateParameters, processVariable, allKeysRequired, - populateAuthenticationButtonParameters, } from '../templateHelper'; import { templates } from '../../store/modules/specs/inboxes/templateFixtures'; describe('templateHelper', () => { const technicianTemplate = templates.find(t => t.name === 'technician_visit'); - const otpTemplate = templates.find(t => t.name === 'basic_otp'); describe('processVariable', () => { it('should remove curly braces from variables', () => { @@ -126,13 +124,6 @@ describe('templateHelper', () => { expect(result.body).toBeUndefined(); }); - it('should handle AUTHENTICATION category templates with special OTP handling', () => { - const result = buildTemplateParameters(otpTemplate, false); - expect(result.body).toEqual({ - otp_code: '', - }); - }); - it('should handle URL buttons with variables for non-authentication templates', () => { const templateWithUrlButton = { category: 'MARKETING', @@ -164,145 +155,5 @@ describe('templateHelper', () => { }, ]); }); - - it('should skip button variables for AUTHENTICATION templates', () => { - const result = buildTemplateParameters(otpTemplate, false); - expect(result.buttons).toBeUndefined(); - }); - }); - - describe('populateAuthenticationButtonParameters', () => { - it('should auto-populate URL button parameters with OTP code for authentication templates', () => { - const processedParams = { - body: { - otp_code: '123456', - }, - buttons: [ - { - type: 'url', - parameter: '', - url: 'https://www.whatsapp.com/otp/code/?code=otp{{1}}', - }, - ], - }; - - const result = populateAuthenticationButtonParameters( - otpTemplate, - processedParams - ); - - expect(result.buttons[0].parameter).toBe('123456'); - }); - - it('should use positional parameter "1" if available for authentication templates', () => { - const processedParams = { - body: { - 1: '654321', - otp_code: '123456', - }, - buttons: [ - { - type: 'url', - parameter: '', - url: 'https://www.whatsapp.com/otp/code/?code=otp{{1}}', - }, - ], - }; - - const result = populateAuthenticationButtonParameters( - otpTemplate, - processedParams - ); - - expect(result.buttons[0].parameter).toBe('654321'); - }); - - it('should not modify non-authentication templates', () => { - const processedParams = { - body: { - name: 'John', - }, - buttons: [ - { - type: 'url', - parameter: '', - url: 'https://example.com/{{name}}', - }, - ], - }; - - const result = populateAuthenticationButtonParameters( - technicianTemplate, - processedParams - ); - - expect(result.buttons[0].parameter).toBe(''); - }); - - it('should not modify non-URL buttons in authentication templates', () => { - const authTemplateWithQuickReply = { - category: 'AUTHENTICATION', - components: [ - { - type: 'BODY', - text: 'Your code is {{1}}', - }, - ], - }; - - const processedParams = { - body: { - otp_code: '123456', - }, - buttons: [ - { - type: 'quick_reply', - parameter: 'original_value', - }, - ], - }; - - const result = populateAuthenticationButtonParameters( - authTemplateWithQuickReply, - processedParams - ); - - expect(result.buttons[0].parameter).toBe('original_value'); - }); - - it('should handle templates without buttons', () => { - const processedParams = { - body: { - otp_code: '123456', - }, - }; - - const result = populateAuthenticationButtonParameters( - otpTemplate, - processedParams - ); - - expect(result).toEqual(processedParams); - }); - - it('should not mutate the original processedParams object', () => { - const processedParams = { - body: { - otp_code: '123456', - }, - buttons: [ - { - type: 'url', - parameter: '', - url: 'https://www.whatsapp.com/otp/code/?code=otp{{1}}', - }, - ], - }; - - const originalParams = JSON.parse(JSON.stringify(processedParams)); - populateAuthenticationButtonParameters(otpTemplate, processedParams); - - expect(processedParams).toEqual(originalParams); - }); }); }); diff --git a/app/javascript/dashboard/helper/templateHelper.js b/app/javascript/dashboard/helper/templateHelper.js index ce05bd783..1bd2f974d 100644 --- a/app/javascript/dashboard/helper/templateHelper.js +++ b/app/javascript/dashboard/helper/templateHelper.js @@ -34,26 +34,7 @@ export const buildTemplateParameters = (template, hasMediaHeaderValue) => { allVariables.body = {}; matchedVariables.forEach(variable => { const key = processVariable(variable); - // Special handling for authentication templates - if (template?.category === 'AUTHENTICATION') { - if ( - key === '1' || - key.toLowerCase().includes('otp') || - key.toLowerCase().includes('code') - ) { - allVariables.body.otp_code = ''; - } else if ( - key === '2' || - key.toLowerCase().includes('expiry') || - key.toLowerCase().includes('minute') - ) { - allVariables.body.expiry_minutes = ''; - } else { - allVariables.body[key] = ''; - } - } else { - allVariables.body[key] = ''; - } + allVariables.body[key] = ''; }); } @@ -71,64 +52,31 @@ export const buildTemplateParameters = (template, hasMediaHeaderValue) => { buttonComponents.forEach(buttonComponent => { if (buttonComponent.buttons) { buttonComponent.buttons.forEach((button, index) => { - // Skip button parameter inputs for authentication templates - // as they are auto-populated with OTP codes - if (template?.category !== 'AUTHENTICATION') { - // Handle URL buttons with variables - if ( - button.type === 'URL' && - button.url && - button.url.includes('{{') - ) { - const buttonVars = button.url.match(/{{([^}]+)}}/g) || []; - if (buttonVars.length > 0) { - if (!allVariables.buttons) allVariables.buttons = []; - allVariables.buttons[index] = { - type: 'url', - parameter: '', - url: button.url, - variables: buttonVars.map(v => processVariable(v)), - }; - } - } - - // Handle copy code buttons - if (button.type === 'COPY_CODE') { + // Handle URL buttons with variables + if (button.type === 'URL' && button.url && button.url.includes('{{')) { + const buttonVars = button.url.match(/{{([^}]+)}}/g) || []; + if (buttonVars.length > 0) { if (!allVariables.buttons) allVariables.buttons = []; allVariables.buttons[index] = { - type: 'copy_code', + type: 'url', parameter: '', + url: button.url, + variables: buttonVars.map(v => processVariable(v)), }; } } + + // Handle copy code buttons + if (button.type === 'COPY_CODE') { + if (!allVariables.buttons) allVariables.buttons = []; + allVariables.buttons[index] = { + type: 'copy_code', + parameter: '', + }; + } }); } }); return allVariables; }; - -export const populateAuthenticationButtonParameters = ( - template, - processedParams -) => { - const finalParams = { ...processedParams }; - - if (template?.category === 'AUTHENTICATION' && finalParams.buttons) { - // Deep copy the buttons array to avoid mutating the original - finalParams.buttons = finalParams.buttons.map(button => ({ ...button })); - - finalParams.buttons.forEach((button, index) => { - if (button.type === 'url') { - // For authentication templates, auto-populate URL button parameter with OTP - if (finalParams.body?.['1']) { - finalParams.buttons[index].parameter = finalParams.body['1']; - } else if (finalParams.body?.otp_code) { - finalParams.buttons[index].parameter = finalParams.body.otp_code; - } - } - }); - } - - return finalParams; -}; diff --git a/app/javascript/dashboard/store/modules/specs/inboxes/templateFixtures.js b/app/javascript/dashboard/store/modules/specs/inboxes/templateFixtures.js index 0cb1e8cc9..c4c6a0b40 100644 --- a/app/javascript/dashboard/store/modules/specs/inboxes/templateFixtures.js +++ b/app/javascript/dashboard/store/modules/specs/inboxes/templateFixtures.js @@ -313,30 +313,6 @@ export const templates = [ ], rejected_reason: 'NONE', }, - { - name: 'basic_otp', - status: 'approved', - category: 'AUTHENTICATION', - language: 'en', - namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9', - components: [ - { - text: '*{{1}}* is your verification code. For your security, do not share this code.', - type: 'BODY', - }, - { - type: 'BUTTONS', - buttons: [ - { - url: 'https://www.whatsapp.com/otp/code/?otp_type=COPY_CODE&code=otp{{1}}', - text: 'Copy code', - type: 'URL', - }, - ], - }, - ], - rejected_reason: 'NONE', - }, { name: 'event_invitation_static', status: 'approved', @@ -384,34 +360,6 @@ export const templates = [ ], rejected_reason: 'NONE', }, - { - name: 'secure_login_otp', - status: 'approved', - category: 'AUTHENTICATION', - language: 'en', - namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9', - components: [ - { - text: '*{{1}}* is your verification code. For your security, do not share this code.', - type: 'BODY', - }, - { - text: 'This code expires in 10 minutes.', - type: 'FOOTER', - }, - { - type: 'BUTTONS', - buttons: [ - { - url: 'https://www.whatsapp.com/otp/code/?otp_type=ZERO_TAP&cta_display_name=Autofill&package_name=com.chatwoot.app&signature_hash=12121212121&code_expiration_minutes=10&code=otp{{1}}', - text: 'Copy code', - type: 'URL', - }, - ], - }, - ], - rejected_reason: 'NONE', - }, { name: 'discount_coupon', status: 'approved', @@ -536,30 +484,6 @@ export const templates = [ ], rejected_reason: 'NONE', }, - { - name: 'otp_verification', - status: 'approved', - category: 'AUTHENTICATION', - language: 'en_US', - namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9', - components: [ - { - text: 'Use code *{{1}}* to verify your transaction of {{2}}.', - type: 'BODY', - }, - { - type: 'BUTTONS', - buttons: [ - { - url: 'https://www.whatsapp.com/otp/code/?otp_type=COPY_CODE&code=otp{{1}}', - text: 'Copy code', - type: 'URL', - }, - ], - }, - ], - rejected_reason: 'NONE', - }, { name: 'feedback_request', status: 'approved', diff --git a/app/services/whatsapp/template_processor_service.rb b/app/services/whatsapp/template_processor_service.rb index 90010286e..afbfe63cf 100644 --- a/app/services/whatsapp/template_processor_service.rb +++ b/app/services/whatsapp/template_processor_service.rb @@ -2,11 +2,9 @@ class Whatsapp::TemplateProcessorService pattr_initialize [:channel!, :template_params, :message] def call - if template_params.present? - process_template_with_params - else - process_template_from_message - end + return [nil, nil, nil, nil] if template_params.blank? + + process_template_with_params end private @@ -20,51 +18,6 @@ class Whatsapp::TemplateProcessorService ] end - def process_template_from_message - return [nil, nil, nil, nil] if message.blank? - - # Delete the following logic once the update for template_params is stable - # see if we can match the message content to a template - # An example template may look like "Your package has been shipped. It will be delivered in {{1}} business days. - # We want to iterate over these templates with our message body and see if we can fit it to any of the templates - # Then we use regex to parse the template varibles and convert them into the proper payload - channel.message_templates&.each do |template| - match_obj = template_match_object(template) - next if match_obj.blank? - - # we have a match, now we need to parse the template variables and convert them into the wa recommended format - processed_parameters = match_obj.captures.map { |x| { type: 'text', text: x } } - - # no need to look up further end the search - return [template['name'], template['namespace'], template['language'], processed_parameters] - end - [nil, nil, nil, nil] - end - - def template_match_object(template) - body_object = validated_body_object(template) - return if body_object.blank? - - template_match_regex = build_template_match_regex(body_object['text']) - message.outgoing_content.match(template_match_regex) - end - - def build_template_match_regex(template_text) - # Converts the whatsapp template to a comparable regex string to check against the message content - # the variables are of the format {{num}} ex:{{1}} - - # transform the template text into a regex string - # we need to replace the {{num}} with matchers that can be used to capture the variables - template_text = template_text.gsub(/{{\d}}/, '(.*)') - # escape if there are regex characters in the template text - template_text = Regexp.escape(template_text) - # ensuring only the variables remain as capture groups - template_text = template_text.gsub(Regexp.escape('(.*)'), '(.*)') - - template_match_string = "^#{template_text}$" - Regexp.new template_match_string - end - def find_template channel.message_templates.find do |t| t['name'] == template_params['name'] && t['language'] == template_params['language'] && t['status']&.downcase == 'approved' @@ -75,159 +28,86 @@ class Whatsapp::TemplateProcessorService template = find_template return if template.blank? - # Handle enhanced template parameters structure - if template_params['processed_params'].is_a?(Hash) && - (template_params['processed_params'].key?('body') || template_params['processed_params'].key?('buttons') || template_params['processed_params'].key?('header')) - process_enhanced_template_params(template) - else - # Check if we have special header types that need processing - header_component = template['components'].find { |c| c['type'] == 'HEADER' } - if header_component&.dig('format')&.in?(%w[IMAGE VIDEO DOCUMENT]) - process_media_template_params(template, header_component) - elsif template['category']&.downcase == 'authentication' - process_authentication_template_params(template) - else - process_legacy_template_params(template) - end - end - end - - def process_media_template_params(_template, header_component) - # For templates with media headers, we need to create proper component parameters - components = [] - - # Add header component with media parameter - media_url = if header_component['example'] && header_component['example']['header_handle'] - # Template has example media URL, use it as a placeholder - header_component['example']['header_handle'].first - else - # No example, need to provide a media URL parameter - # Since we don't have user input, we'll create an empty media parameter - 'https://example.com/placeholder.jpg' # Placeholder URL - end - - components << { - type: 'header', - parameters: [{ - :type => header_component['format'].downcase, - header_component['format'].downcase => { - link: media_url - } - }] - } - - # Add body parameters if any - body_params = template_params['processed_params'].map { |_, value| { type: 'text', text: value } } - components << { type: 'body', parameters: body_params } if body_params.present? - - @template_params = components - end - - def process_authentication_template_params(_template) - # Authentication templates typically have OTP codes and expiration times - components = [] - - # Process body parameters for authentication templates - if template_params['processed_params'].present? - body_params = if template_params['processed_params'].is_a?(Hash) - process_authentication_body_params(template_params['processed_params']) - else - template_params['processed_params'].map { |_, value| { type: 'text', text: value } } - end - components << { type: 'body', parameters: body_params } if body_params.present? - end - - @template_params = components + process_enhanced_template_params(template) end def process_enhanced_template_params(template) processed_params = template_params['processed_params'] components = [] - # Process header parameters first (important for WhatsApp API order) - if processed_params['header'].present? - header_params = [] - - processed_params['header'].each do |key, value| - next if value.blank? - - if key == 'media_url' && processed_params['header']['media_type'].present? - media_type = processed_params['header']['media_type'] - media_param = build_media_parameter(value, media_type) - header_params << media_param if media_param - elsif key != 'media_type' - header_params << build_parameter(value) - end - end - - # Only add header component if we have valid parameters - components << { type: 'header', parameters: header_params } if header_params.present? - end - - # Process body parameters - if processed_params['body'].present? - body_params = processed_params['body'].filter_map do |key, value| - next if value.blank? - - # Handle special authentication parameters - if key == 'otp_code' - build_authentication_parameter(value, 'otp') - elsif key == 'expiry_minutes' - build_authentication_parameter(value, 'expiry') - else - # Check if template uses named parameters - parameter_format = template['parameter_format'] - if parameter_format == 'NAMED' - build_named_parameter(key, value) - else - build_parameter(value) - end - end - end - components << { type: 'body', parameters: body_params } if body_params.present? - end - - # Process footer parameters (rarely used but supported) - if processed_params['footer'].present? - footer_params = processed_params['footer'].filter_map do |_, value| - next if value.blank? - - build_parameter(value) - end - components << { type: 'footer', parameters: footer_params } if footer_params.present? - end - - # Process button parameters - if processed_params['buttons'].present? - button_params = processed_params['buttons'].filter_map.with_index do |button, index| - next if button.blank? - - # For URL buttons, parameter is required even if blank - if button['type'] == 'url' || button['parameter'].present? - button_component = { - type: 'button', - sub_type: button['type'] || 'url', - index: index, - parameters: [build_button_parameter(button)] - } - button_component - end - end - components.concat(button_params) if button_params.present? - end + components.concat(process_header_components(processed_params)) + components.concat(process_body_components(processed_params, template)) + components.concat(process_footer_components(processed_params)) + components.concat(process_button_components(processed_params)) @template_params = components end - def process_legacy_template_params(template) - parameter_format = template['parameter_format'] + def process_header_components(processed_params) + return [] unless processed_params['header'].present? - @template_params = if parameter_format == 'NAMED' - template_params['processed_params']&.map { |key, value| { type: 'text', parameter_name: key, text: value } } - else - template_params['processed_params']&.map { |_, value| { type: 'text', text: value } } - end - @template_params + header_params = [] + processed_params['header'].each do |key, value| + next if value.blank? + + if key == 'media_url' && processed_params['header']['media_type'].present? + media_type = processed_params['header']['media_type'] + media_param = build_media_parameter(value, media_type) + header_params << media_param if media_param + elsif key != 'media_type' + header_params << build_parameter(value) + end + end + + header_params.present? ? [{ type: 'header', parameters: header_params }] : [] + end + + def process_body_components(processed_params, template) + return [] unless processed_params['body'].present? + + body_params = processed_params['body'].filter_map do |key, value| + next if value.blank? + + parameter_format = template['parameter_format'] + if parameter_format == 'NAMED' + build_named_parameter(key, value) + else + build_parameter(value) + end + end + + body_params.present? ? [{ type: 'body', parameters: body_params }] : [] + end + + def process_footer_components(processed_params) + return [] unless processed_params['footer'].present? + + footer_params = processed_params['footer'].filter_map do |_, value| + next if value.blank? + + build_parameter(value) + end + + footer_params.present? ? [{ type: 'footer', parameters: footer_params }] : [] + end + + def process_button_components(processed_params) + return [] unless processed_params['buttons'].present? + + button_params = processed_params['buttons'].filter_map.with_index do |button, index| + next if button.blank? + + if button['type'] == 'url' || button['parameter'].present? + { + type: 'button', + sub_type: button['type'] || 'url', + index: index, + parameters: [build_button_parameter(button)] + } + end + end + + button_params.compact end def build_parameter(value) @@ -265,16 +145,6 @@ class Whatsapp::TemplateProcessorService year: value['year'] } } - when 'location' - { - type: 'location', - location: { - latitude: value['latitude'], - longitude: value['longitude'], - name: value['name'], - address: value['address'] - } - } else { type: 'text', text: value.to_s } end @@ -354,71 +224,6 @@ class Whatsapp::TemplateProcessorService end end - def build_authentication_parameter(value, param_type) - # Authentication-specific parameters - sanitized_value = sanitize_parameter(value) - - case param_type - when 'otp' - # OTP code - typically 4-8 digits - raise ArgumentError, 'OTP code must be numeric' unless sanitized_value.match?(/\A\d+\z/) - raise ArgumentError, 'OTP code must be 4-8 digits' unless sanitized_value.length.between?(4, 8) - - { type: 'text', text: sanitized_value } - when 'expiry' - # Expiry time in minutes - expiry_minutes = sanitized_value.to_i - raise ArgumentError, 'Expiry minutes must be a positive number' unless expiry_minutes.positive? - - { type: 'text', text: expiry_minutes.to_s } - else - { type: 'text', text: sanitized_value } - end - end - - def process_authentication_body_params(processed_params) - # Process authentication-specific body parameters - processed_params.filter_map do |key, value| - next if value.blank? - - case key - when 'otp_code' - build_authentication_parameter(value, 'otp') - when 'expiry_minutes' - build_authentication_parameter(value, 'expiry') - else - build_parameter(value) - end - end - end - - def validated_body_object(template) - # we don't care if its not approved template - return if template['status'] != 'approved' - - # we only care about text body object in template. if not present we discard the template - # we don't support other forms of templates - template['components'].find { |obj| obj['type'] == 'BODY' && obj.key?('text') } - end - - def process_dynamic_button_text(button_text, button_index) - return button_text unless button_text.include?('{{') - - # Replace button text variables with provided parameters - if template_params['processed_params'].present? - button_params = template_params['processed_params']['buttons'] - if button_params.is_a?(Array) && button_params[button_index].present? - button_param = button_params[button_index]['parameter'] || button_params[button_index]['text'] - if button_param.present? - # Replace {{1}}, {{variable}}, etc. with the provided parameter - button_text = button_text.gsub(/\{\{[^}]+\}\}/, button_param.to_s) - end - end - end - - button_text - end - def rich_formatting?(text) # Check if text contains WhatsApp rich formatting markers text.match?(/\*[^*]+\*/) || # Bold: *text* diff --git a/spec/services/whatsapp/oneoff_campaign_service_spec.rb b/spec/services/whatsapp/oneoff_campaign_service_spec.rb index 33107e8de..1f542d631 100644 --- a/spec/services/whatsapp/oneoff_campaign_service_spec.rb +++ b/spec/services/whatsapp/oneoff_campaign_service_spec.rb @@ -19,7 +19,7 @@ describe Whatsapp::OneoffCampaignService do 'namespace' => '23423423_2342423_324234234_2343224', 'category' => 'UTILITY', 'language' => 'en', - 'processed_params' => { 'name' => 'John', 'ticket_id' => '2332' } + 'processed_params' => { 'body' => { 'name' => 'John', 'ticket_id' => '2332' } } } end @@ -125,8 +125,13 @@ describe Whatsapp::OneoffCampaignService do namespace: '23423423_2342423_324234234_2343224', lang_code: 'en', parameters: array_including( - hash_including(type: 'text', parameter_name: 'name', text: 'John'), - hash_including(type: 'text', parameter_name: 'ticket_id', text: '2332') + hash_including( + type: 'body', + parameters: array_including( + hash_including(type: 'text', parameter_name: 'name', text: 'John'), + hash_including(type: 'text', parameter_name: 'ticket_id', text: '2332') + ) + ) ) ) )