diff --git a/app/javascript/dashboard/helper/specs/templateHelper.spec.js b/app/javascript/dashboard/helper/specs/templateHelper.spec.js index 6ee945701..6e0661152 100644 --- a/app/javascript/dashboard/helper/specs/templateHelper.spec.js +++ b/app/javascript/dashboard/helper/specs/templateHelper.spec.js @@ -155,5 +155,214 @@ describe('templateHelper', () => { }, ]); }); + + it('should handle templates with no variables', () => { + const emptyTemplate = templates.find( + t => t.name === 'no_variable_template' + ); + const result = buildTemplateParameters(emptyTemplate, false); + expect(result).toEqual({}); + }); + + it('should build parameters for templates with multiple component types', () => { + const complexTemplate = { + components: [ + { type: 'HEADER', format: 'IMAGE' }, + { type: 'BODY', text: 'Hi {{1}}, your order {{2}} is ready!' }, + { type: 'FOOTER', text: 'Thank you for your business' }, + { + type: 'BUTTONS', + buttons: [{ type: 'URL', url: 'https://example.com/{{3}}' }], + }, + ], + }; + + const result = buildTemplateParameters(complexTemplate, true); + + expect(result.header).toEqual({ + media_url: '', + media_type: 'image', + }); + expect(result.body).toEqual({ 1: '', 2: '' }); + expect(result.buttons).toEqual([ + { + type: 'url', + parameter: '', + url: 'https://example.com/{{3}}', + variables: ['3'], + }, + ]); + }); + + it('should handle copy code buttons correctly', () => { + const copyCodeTemplate = templates.find( + t => t.name === 'discount_coupon' + ); + const result = buildTemplateParameters(copyCodeTemplate, false); + + expect(result.body).toBeDefined(); + expect(result.buttons).toEqual([ + { + type: 'copy_code', + parameter: '', + }, + ]); + }); + + it('should handle templates with document headers', () => { + const documentTemplate = templates.find( + t => t.name === 'purchase_receipt' + ); + const result = buildTemplateParameters(documentTemplate, true); + + expect(result.header).toEqual({ + media_url: '', + media_type: 'document', + }); + expect(result.body).toEqual({ + 1: '', + 2: '', + 3: '', + }); + }); + + it('should handle video header templates', () => { + const videoTemplate = templates.find(t => t.name === 'training_video'); + const result = buildTemplateParameters(videoTemplate, true); + + expect(result.header).toEqual({ + media_url: '', + media_type: 'video', + }); + expect(result.body).toEqual({ + name: '', + date: '', + }); + }); + }); + + describe('enhanced format validation', () => { + it('should validate enhanced format structure', () => { + const processedParams = { + body: { 1: 'John', 2: 'Order123' }, + header: { + media_url: 'https://example.com/image.jpg', + media_type: 'image', + }, + buttons: [{ type: 'copy_code', parameter: 'SAVE20' }], + }; + + // Test that structure is properly formed + expect(processedParams.body).toBeDefined(); + expect(typeof processedParams.body).toBe('object'); + expect(processedParams.header).toBeDefined(); + expect(Array.isArray(processedParams.buttons)).toBe(true); + }); + + it('should handle empty component sections', () => { + const processedParams = { + body: {}, + header: {}, + buttons: [], + }; + + expect(allKeysRequired(processedParams.body)).toBe(true); + expect(allKeysRequired(processedParams.header)).toBe(true); + expect(processedParams.buttons.length).toBe(0); + }); + + it('should validate parameter completeness', () => { + const incompleteParams = { + body: { 1: 'John', 2: '' }, + }; + + expect(allKeysRequired(incompleteParams.body)).toBe(false); + }); + + it('should handle edge cases in processVariable', () => { + expect(processVariable('{{')).toBe(''); + expect(processVariable('}}')).toBe(''); + expect(processVariable('')).toBe(''); + expect(processVariable('{{nested{{variable}}}}')).toBe('nestedvariable'); + }); + + it('should handle special characters in template variables', () => { + /* eslint-disable no-template-curly-in-string */ + const templateText = + 'Welcome {{user_name}}, your order #{{order_id}} costs ${{amount}}'; + /* eslint-enable no-template-curly-in-string */ + const processedParams = { + body: { + user_name: 'John & Jane', + order_id: '12345', + amount: '99.99', + }, + }; + + const result = replaceTemplateVariables(templateText, processedParams); + expect(result).toBe( + 'Welcome John & Jane, your order #12345 costs $99.99' + ); + }); + + it('should handle templates with mixed parameter types', () => { + const mixedTemplate = { + components: [ + { type: 'HEADER', format: 'VIDEO' }, + { type: 'BODY', text: 'Order {{order_id}} status: {{status}}' }, + { type: 'FOOTER', text: 'Thank you' }, + { + type: 'BUTTONS', + buttons: [ + { type: 'URL', url: 'https://track.com/{{order_id}}' }, + { type: 'COPY_CODE' }, + { type: 'PHONE_NUMBER', phone_number: '+1234567890' }, + ], + }, + ], + }; + + const result = buildTemplateParameters(mixedTemplate, true); + + expect(result.header).toEqual({ + media_url: '', + media_type: 'video', + }); + expect(result.body).toEqual({ + order_id: '', + status: '', + }); + expect(result.buttons).toHaveLength(2); // URL and COPY_CODE (PHONE_NUMBER doesn't need parameters) + expect(result.buttons[0].type).toBe('url'); + expect(result.buttons[1].type).toBe('copy_code'); + }); + + it('should handle templates with no processable components', () => { + const emptyTemplate = { + components: [ + { type: 'HEADER', format: 'TEXT', text: 'Static Header' }, + { type: 'BODY', text: 'Static body with no variables' }, + { type: 'FOOTER', text: 'Static footer' }, + ], + }; + + const result = buildTemplateParameters(emptyTemplate, false); + expect(result).toEqual({}); + }); + + it('should validate that replaceTemplateVariables preserves unreplaced variables', () => { + const templateText = 'Hi {{name}}, order {{order_id}} is {{status}}'; + const partialParams = { + body: { + name: 'John', + // order_id missing + status: 'ready', + }, + }; + + const result = replaceTemplateVariables(templateText, partialParams); + expect(result).toBe('Hi John, order {{order_id}} is ready'); + expect(result).toContain('{{order_id}}'); // Unreplaced variable preserved + }); }); }); diff --git a/spec/services/whatsapp/send_on_whatsapp_service_spec.rb b/spec/services/whatsapp/send_on_whatsapp_service_spec.rb index 5b90944af..922e4de76 100644 --- a/spec/services/whatsapp/send_on_whatsapp_service_spec.rb +++ b/spec/services/whatsapp/send_on_whatsapp_service_spec.rb @@ -165,6 +165,267 @@ describe Whatsapp::SendOnWhatsappService do described_class.new(message: message).perform expect(message.reload.source_id).to eq('123456789') end + + it 'handles template with header parameters' do + # Test with body parameters only since no media templates in factory + header_template_params = { + name: 'sample_shipping_confirmation', + namespace: '23423423_2342423_324234234_2343224', + language: 'en_US', + category: 'SHIPPING_UPDATE', + processed_params: { + 'body' => { '1' => '3' }, + 'header' => { 'media_url' => 'https://example.com/image.jpg', 'media_type' => 'image' } + } + } + + message = create(:message, additional_attributes: { template_params: header_template_params }, + conversation: conversation, message_type: :outgoing) + + stub_request(:post, 'https://waba.360dialog.io/v1/messages') + .with( + headers: headers, + body: { + to: '123456789', + template: { + name: 'sample_shipping_confirmation', + namespace: '23423423_2342423_324234234_2343224', + language: { 'policy': 'deterministic', 'code': 'en_US' }, + components: [ + { type: 'header', parameters: [{ type: 'image', image: { link: 'https://example.com/image.jpg' } }] }, + { type: 'body', parameters: [{ type: 'text', text: '3' }] } + ] + }, + type: 'template' + }.to_json + ).to_return(status: 200, body: success_response, headers: { 'content-type' => 'application/json' }) + + described_class.new(message: message).perform + expect(message.reload.source_id).to eq('123456789') + end + + it 'handles empty processed_params gracefully' do + empty_template_params = { + name: 'sample_shipping_confirmation', + namespace: '23423423_2342423_324234234_2343224', + language: 'en_US', + category: 'SHIPPING_UPDATE', + processed_params: {} + } + + message = create(:message, additional_attributes: { template_params: empty_template_params }, + conversation: conversation, message_type: :outgoing) + + stub_request(:post, 'https://waba.360dialog.io/v1/messages') + .with( + headers: headers, + body: { + to: '123456789', + template: { + name: 'sample_shipping_confirmation', + namespace: '23423423_2342423_324234234_2343224', + language: { 'policy': 'deterministic', 'code': 'en_US' }, + components: [] + }, + type: 'template' + }.to_json + ).to_return(status: 200, body: success_response, headers: { 'content-type' => 'application/json' }) + + described_class.new(message: message).perform + expect(message.reload.source_id).to eq('123456789') + end + + it 'handles template with button parameters' do + # Test button processing with existing template - simulate buttons being added + button_template_params = { + name: 'sample_shipping_confirmation', + namespace: '23423423_2342423_324234234_2343224', + language: 'en_US', + category: 'SHIPPING_UPDATE', + processed_params: { + 'body' => { '1' => '3' }, + 'buttons' => [{ 'type' => 'url', 'parameter' => 'https://track.example.com/123' }] + } + } + + message = create(:message, additional_attributes: { template_params: button_template_params }, + conversation: conversation, message_type: :outgoing) + + stub_request(:post, 'https://waba.360dialog.io/v1/messages') + .with( + headers: headers, + body: { + to: '123456789', + template: { + name: 'sample_shipping_confirmation', + namespace: '23423423_2342423_324234234_2343224', + language: { 'policy': 'deterministic', 'code': 'en_US' }, + components: [ + { type: 'body', parameters: [{ type: 'text', text: '3' }] }, + { type: 'button', sub_type: 'url', index: 0, parameters: [{ type: 'text', text: 'https://track.example.com/123' }] } + ] + }, + type: 'template' + }.to_json + ).to_return(status: 200, body: success_response, headers: { 'content-type' => 'application/json' }) + + described_class.new(message: message).perform + expect(message.reload.source_id).to eq('123456789') + end + + it 'processes template parameters correctly via integration' do + # Test enhanced format processing through the integration + complex_template_params = { + name: 'sample_shipping_confirmation', + namespace: '23423423_2342423_324234234_2343224', + language: 'en_US', + category: 'SHIPPING_UPDATE', + processed_params: { + 'body' => { '1' => '5' }, + 'footer' => { 'text' => 'Thank you' } + } + } + + message = create(:message, additional_attributes: { template_params: complex_template_params }, + conversation: conversation, message_type: :outgoing) + + stub_request(:post, 'https://waba.360dialog.io/v1/messages') + .with( + headers: headers, + body: { + to: '123456789', + template: { + name: 'sample_shipping_confirmation', + namespace: '23423423_2342423_324234234_2343224', + language: { 'policy': 'deterministic', 'code': 'en_US' }, + components: [ + { type: 'body', parameters: [{ type: 'text', text: '5' }] }, + { type: 'footer', parameters: [{ type: 'text', text: 'Thank you' }] } + ] + }, + type: 'template' + }.to_json + ).to_return(status: 200, body: success_response, headers: { 'content-type' => 'application/json' }) + + # This tests the full integration including TemplateProcessorService + expect { described_class.new(message: message).perform }.not_to raise_error + expect(message.reload.source_id).to eq('123456789') + end + + it 'handles edge case with missing template gracefully' do + # Test the service behavior when template is not found + missing_template_params = { + 'name' => 'non_existent_template', + 'namespace' => 'missing_namespace', + 'language' => 'en_US', + 'category' => 'UTILITY', + 'processed_params' => { 'body' => { '1' => 'test' } } + } + + service = Whatsapp::TemplateProcessorService.new( + channel: whatsapp_channel, + template_params: missing_template_params + ) + + expect { service.call }.not_to raise_error + name, namespace, language, processed_params = service.call + expect(name).to eq('non_existent_template') + expect(namespace).to eq('missing_namespace') + expect(language).to eq('en_US') + expect(processed_params).to be_nil + end + + it 'handles template with blank parameter values correctly' do + # Test that blank/empty values are skipped in parameter processing + blank_values_template_params = { + name: 'sample_shipping_confirmation', + namespace: '23423423_2342423_324234234_2343224', + language: 'en_US', + category: 'SHIPPING_UPDATE', + processed_params: { + 'body' => { '1' => '', '2' => 'valid_value', '3' => nil }, + 'header' => { 'media_url' => '', 'media_type' => 'image' } + } + } + + message = create(:message, additional_attributes: { template_params: blank_values_template_params }, + conversation: conversation, message_type: :outgoing) + + stub_request(:post, 'https://waba.360dialog.io/v1/messages') + .with( + headers: headers, + body: { + to: '123456789', + template: { + name: 'sample_shipping_confirmation', + namespace: '23423423_2342423_324234234_2343224', + language: { 'policy': 'deterministic', 'code': 'en_US' }, + components: [ + { type: 'body', parameters: [{ type: 'text', text: 'valid_value' }] } + ] + }, + type: 'template' + }.to_json + ).to_return(status: 200, body: success_response, headers: { 'content-type' => 'application/json' }) + + described_class.new(message: message).perform + expect(message.reload.source_id).to eq('123456789') + end + + it 'handles nil template_params gracefully' do + # Test service behavior when template_params is completely nil + message = create(:message, additional_attributes: {}, + conversation: conversation, message_type: :outgoing) + + # Should send regular message, not template + stub_request(:post, 'https://waba.360dialog.io/v1/messages') + .with( + headers: headers, + body: { + to: '123456789', + text: { body: message.content }, + type: 'text' + }.to_json + ).to_return(status: 200, body: success_response, headers: { 'content-type' => 'application/json' }) + + expect { described_class.new(message: message).perform }.not_to raise_error + end + + it 'processes template with rich text formatting' do + # Test that rich text formatting is preserved + rich_text_template_params = { + name: 'sample_shipping_confirmation', + namespace: '23423423_2342423_324234234_2343224', + language: 'en_US', + category: 'SHIPPING_UPDATE', + processed_params: { + 'body' => { '1' => '*Bold text* and _italic text_' } + } + } + + message = create(:message, additional_attributes: { template_params: rich_text_template_params }, + conversation: conversation, message_type: :outgoing) + + stub_request(:post, 'https://waba.360dialog.io/v1/messages') + .with( + headers: headers, + body: { + to: '123456789', + template: { + name: 'sample_shipping_confirmation', + namespace: '23423423_2342423_324234234_2343224', + language: { 'policy': 'deterministic', 'code': 'en_US' }, + components: [ + { type: 'body', parameters: [{ type: 'text', text: '*Bold text* and _italic text_' }] } + ] + }, + type: 'template' + }.to_json + ).to_return(status: 200, body: success_response, headers: { 'content-type' => 'application/json' }) + + described_class.new(message: message).perform + expect(message.reload.source_id).to eq('123456789') + end end end end