chore: remove authentication related templates
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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*
|
||||
|
||||
@@ -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')
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user