diff --git a/app/controllers/api/v1/accounts/inboxes_controller.rb b/app/controllers/api/v1/accounts/inboxes_controller.rb index ae1d4369a..afc961d58 100644 --- a/app/controllers/api/v1/accounts/inboxes_controller.rb +++ b/app/controllers/api/v1/accounts/inboxes_controller.rb @@ -44,7 +44,12 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController def update inbox_params = permitted_params.except(:channel, :csat_config) - inbox_params[:csat_config] = format_csat_config(permitted_params[:csat_config]) if permitted_params[:csat_config].present? + + if permitted_params[:csat_config].present? + inbox_params[:csat_config] = format_csat_config(permitted_params[:csat_config]) + handle_whatsapp_csat_template_update(inbox_params[:csat_config]) if @inbox.whatsapp? + end + @inbox.update!(inbox_params) update_inbox_working_hours update_channel if channel_update_required? @@ -87,6 +92,30 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController render json: { error: e.message }, status: :unprocessable_entity end + def csat_template_status + return render json: { error: 'CSAT template status only available for WhatsApp channels' }, status: :bad_request unless @inbox.whatsapp? + + template_config = @inbox.csat_config&.dig('template') + return render json: { template_exists: false } unless template_config + + template_name = template_config['name'] || 'customer_satisfaction_survey' + status_result = @inbox.channel.provider_service.get_template_status(template_name) + + if status_result[:success] + render json: { + template_exists: true, + template_name: template_name, + status: status_result[:template][:status], + template_id: status_result[:template][:id] + } + else + render json: { template_exists: false, error: status_result[:error] } + end + rescue StandardError => e + Rails.logger.error "Error fetching CSAT template status: #{e.message}" + render json: { error: e.message }, status: :internal_server_error + end + private def fetch_inbox @@ -151,8 +180,46 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController @inbox.channel.save! end + def handle_whatsapp_csat_template_update(csat_config) + return unless @inbox.channel.is_a?(Channel::Whatsapp) + return if csat_config[:message].blank? + + # Check if message has changed or template doesn't exist + existing_message = @inbox.csat_config&.dig('message') + template_exists = @inbox.csat_config&.dig('template').present? + + create_whatsapp_csat_template(csat_config) if !template_exists || existing_message != csat_config[:message] + rescue StandardError => e + Rails.logger.error "Error handling WhatsApp CSAT template update: #{e.message}" + # Don't fail the entire update if template creation fails + end + + def create_whatsapp_csat_template(csat_config) + template_config = { + message: csat_config[:message], + button_text: 'Please rate us', + base_url: ENV.fetch('FRONTEND_URL', 'http://localhost:3000'), + language: 'en' + } + + result = @inbox.channel.provider_service.create_csat_template(template_config) + + if result[:success] + # Add template info to csat_config + csat_config[:template] = { + name: 'customer_satisfaction_survey', + template_id: result[:template_id], + created_at: Time.current.iso8601, + language: 'en' + } + Rails.logger.info "WhatsApp CSAT template created successfully for inbox #{@inbox.id}" + else + Rails.logger.error "Failed to create WhatsApp CSAT template: #{result[:error]}" + end + end + def format_csat_config(config) - { + formatted = { display_type: config['display_type'] || 'emoji', message: config['message'] || '', survey_rules: { @@ -160,13 +227,20 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController values: config.dig('survey_rules', 'values') || [] } } + + # Preserve existing template config if present + formatted[:template] = config['template'] if config['template'].present? + + formatted end def inbox_attributes [:name, :avatar, :greeting_enabled, :greeting_message, :enable_email_collect, :csat_survey_enabled, :enable_auto_assignment, :working_hours_enabled, :out_of_office_message, :timezone, :allow_messages_after_resolved, :lock_to_single_conversation, :portal_id, :sender_name_type, :business_name, - { csat_config: [:display_type, :message, { survey_rules: [:operator, { values: [] }] }] }] + { csat_config: [:display_type, :message, + { survey_rules: [:operator, { values: [] }], + template: [:name, :template_id, :created_at, :language] }] }] end def permitted_params(channel_attributes = []) diff --git a/app/services/csat_survey_service.rb b/app/services/csat_survey_service.rb index 15d63722c..44130c882 100644 --- a/app/services/csat_survey_service.rb +++ b/app/services/csat_survey_service.rb @@ -4,7 +4,9 @@ class CsatSurveyService def perform return unless should_send_csat_survey? - if within_messaging_window? + if whatsapp_channel? && template_available_and_approved? + send_whatsapp_template_survey + elsif within_messaging_window? ::MessageTemplates::Template::CsatSurvey.new(conversation: conversation).perform else create_csat_not_sent_activity_message @@ -35,6 +37,72 @@ class CsatSurveyService conversation.can_reply? end + def whatsapp_channel? + inbox.channel_type == 'Channel::Whatsapp' + end + + def template_available_and_approved? + template_config = inbox.csat_config&.dig('template') + return false unless template_config + + template_name = template_config['name'] || 'customer_satisfaction_survey' + status_result = inbox.channel.provider_service.get_template_status(template_name) + + status_result[:success] && status_result[:template][:status] == 'APPROVED' + rescue StandardError => e + Rails.logger.error "Error checking CSAT template status: #{e.message}" + false + end + + def send_whatsapp_template_survey + template_config = inbox.csat_config&.dig('template') + template_name = template_config['name'] || 'customer_satisfaction_survey' + + phone_number = conversation.contact_inbox.source_id + template_info = build_template_info(template_name, template_config) + message = create_csat_message + + inbox.channel.provider_service.send_template(phone_number, template_info, message) + rescue StandardError => e + Rails.logger.error "Error sending WhatsApp CSAT template: #{e.message}" + handle_template_send_failure + end + + def build_template_info(template_name, template_config) + { + name: template_name, + lang_code: template_config['language'] || 'en', + parameters: [ + { + type: 'button', + sub_type: 'url', + index: '0', + parameters: [{ type: 'text', text: conversation.uuid }] + } + ] + } + end + + def create_csat_message + message = conversation.messages.build( + account: conversation.account, + inbox: inbox, + message_type: :outgoing, + content: inbox.csat_config&.dig('message') || 'Please rate this conversation', + content_type: :input_csat + ) + message.save! + message + end + + def handle_template_send_failure + if within_messaging_window? + ::MessageTemplates::Template::CsatSurvey.new(conversation: conversation).perform + else + create_csat_not_sent_activity_message + end + end + def create_csat_not_sent_activity_message content = I18n.t('conversations.activity.csat.not_sent_due_to_messaging_window') activity_message_params = { diff --git a/app/services/whatsapp/providers/whatsapp_cloud_service.rb b/app/services/whatsapp/providers/whatsapp_cloud_service.rb index 1968693ff..d81239080 100644 --- a/app/services/whatsapp/providers/whatsapp_cloud_service.rb +++ b/app/services/whatsapp/providers/whatsapp_cloud_service.rb @@ -58,6 +58,75 @@ class Whatsapp::Providers::WhatsappCloudService < Whatsapp::Providers::BaseServi response.success? end + def create_csat_template(template_config) + request_body = { + name: 'customer_satisfaction_survey', + language: template_config[:language] || 'en', + category: 'UTILITY', + components: [ + { + type: 'BODY', + text: template_config[:message] + }, + { + type: 'BUTTONS', + buttons: [ + { + type: 'URL', + text: template_config[:button_text] || 'Please rate us', + url: "#{template_config[:base_url]}/survey/responses/{{1}}", + example: ['12345'] + } + ] + } + ] + } + + response = HTTParty.post( + "#{business_account_path}/message_templates", + headers: api_headers, + body: request_body.to_json + ) + + if response.success? + { + success: true, + template_id: response['id'], + template_name: 'customer_satisfaction_survey', + status: 'PENDING' + } + else + { + success: false, + error: error_message(response) || 'Failed to create template' + } + end + end + + def get_template_status(template_name) + url = "#{business_account_path}/message_templates?name=#{template_name}&access_token=#{whatsapp_channel.provider_config['api_key']}" + response = HTTParty.get(url) + + return { success: false, error: 'API request failed' } unless response.success? + + templates = response['data'] || [] + template = templates.find { |t| t['name'] == template_name } + + if template + { + success: true, + template: { + id: template['id'], + name: template['name'], + status: template['status'], + language: template['language'] + } + } + else + { success: false, error: 'Template not found' } + end + end + def api_headers { 'Authorization' => "Bearer #{whatsapp_channel.provider_config['api_key']}", 'Content-Type' => 'application/json' } end diff --git a/config/routes.rb b/config/routes.rb index 639c51da7..d044dde1f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -196,6 +196,7 @@ Rails.application.routes.draw do delete :avatar, on: :member post :sync_templates, on: :member get :health, on: :member + get :csat_template_status, on: :member end resources :inbox_members, only: [:create, :show], param: :inbox_id do collection do diff --git a/csat-edit-template.png b/csat-edit-template.png new file mode 100644 index 000000000..938352ef0 Binary files /dev/null and b/csat-edit-template.png differ diff --git a/csat-ui-approved.png b/csat-ui-approved.png new file mode 100644 index 000000000..e2ce32650 Binary files /dev/null and b/csat-ui-approved.png differ diff --git a/csat-ui.png b/csat-ui.png new file mode 100644 index 000000000..411f51561 Binary files /dev/null and b/csat-ui.png differ diff --git a/csat_via_template_whataspp.md b/csat_via_template_whataspp.md new file mode 100644 index 000000000..d50d8c43c --- /dev/null +++ b/csat_via_template_whataspp.md @@ -0,0 +1,210 @@ +# CSAT Surveys via WhatsApp Message Templates + +## Overview + +To ensure successful delivery of CSAT (Customer Satisfaction) surveys via WhatsApp, particularly after the 24-hour customer interaction window, messages must be sent using approved WhatsApp message templates. This spec outlines support for using templates, including automation and service integration. + +## Problem + +Messages sent after 24 hours without an approved template result in delivery failures. Currently, CSAT surveys are completely disabled for WhatsApp channels after the 24-hour messaging window expires. + +## Solution + +Support free-form content input and create WhatsApp message templates automatically in the background. Users configure their CSAT message through the existing UI, and the system handles template creation and approval workflow via WhatsApp Business API. + +**Key Design Decisions:** +- Templates cannot be edited once submitted to WhatsApp - editing requires deleting the old template and creating a new one +- Template lifecycle is managed automatically - new templates overwrite previous ones +- **Template Priority**: Approved templates are always preferred over regular CSAT messages, regardless of messaging window +- Fallback to regular CSAT only when templates are unavailable and within messaging window + +## Data Storage + +Template information is stored in the channel's `csat_config` JSONB field: + +```json +{ + "message": "Hello! Can you please take this quick survey and provide us with your feedback.", + "display_type": "emoji", + "survey_rules": { ... }, + "template": { + "name": "customer_satisfaction_survey", + "template_id": "123456789", + "created_at": "2024-01-01T00:00:00Z", + "language": "en" + } +} +``` + +**Template Naming:** +- **Hardcoded template name**: `customer_satisfaction_survey` +- **Single template per channel**: No multiple templates support +- **Language handling**: Deferred to future implementation + +**Note:** Template status is checked in real-time via API calls, not stored in the database. + +## Template Structure + +Template uses conversation UUID as parameter: +- **Message**: User-configured message content +- **Button URL**: `{{base_url}}/survey/responses/{{1}}` where `{{1}}` is the conversation UUID +- **Button Text**: User-configured button text (default: "Please rate us") + +#### Template Creation API + +Send a POST request to the WhatsApp Business Account > Message Templates endpoint to create a template. + +Request Syntax + +POST //message_templates + +Post Body + +{ + "name": "customer_satisfaction_survey", + "category": "MARKETING", + "language": "", + "components": [] +} + +``` +curl --location 'https://graph.facebook.com/v22.0/{{business_account_id}}/message_templates' \ +--header 'Content-Type: application/json' \ +--data '{ + "name": "customer_satisfaction_survey", + "language": "en", + "category": "MARKETING", + "components": [ + { + "type": "BODY", + "text": "Hello! Can you please take this quick survey and provide us with your feedback." + }, + { + "type": "BUTTONS", + "buttons": [ + { + "type": "URL", + "text": "Please rate us", + "url": "{{base_url}}/survey/responses/{{1}}", + "example": [ + "12345" + ] + } + ] + } + ] +}' +``` + +## Service Integration + +### CsatSurveyService Modifications + +Extend `app/services/csat_survey_service.rb` to handle WhatsApp templates: + +1. **Template Check**: Check if template exists in `csat_config` and verify status via real-time API call +2. **Template Priority Logic**: + - If template exists and approved: Always send template (regardless of messaging window) + - If no template or not approved: Fall back to regular CSAT within messaging window + - If outside window and no approved template: Create activity message +3. **Survey Rules**: Apply existing label-based survey rules before sending + +### WhatsApp Provider Integration + +Modify `app/services/whatsapp/send_on_whatsapp_service.rb` to support CSAT templates: +- Add CSAT template sending capability +- Use conversation UUID as template parameter +- Handle template-specific error cases + +### Template Management + +**Creation Workflow:** +1. User updates CSAT configuration in settings +2. System creates template via WhatsApp Business API +3. Template info stored in `csat_config` (without status) +4. Old templates are automatically replaced + +**Send Survey Logic:** +1. Conversation is resolved +2. Check existing survey rules (labels, etc.) +3. Check if template exists and get status via API call +4. **Template Priority:** + - If template approved: Send template (regardless of messaging window) + - If no template or not approved: + - Within messaging window: Send regular CSAT message + - Outside messaging window: Create activity message + +## Error Handling & Fallback + +**Template Creation Failures:** +- Display error message in frontend +- Log error details for debugging +- Continue using regular CSAT within messaging window + +**Template Sending Failures:** +- Log failure reason +- Create activity message indicating survey couldn't be sent +- Track failure metrics for monitoring + +**Fallback Strategy:** +- **Template Priority**: Always prefer approved templates over regular CSAT messages +- **No Template Available**: + - Within messaging window: Send regular CSAT message + - Outside messaging window: Create activity message + +## Implementation Notes + +**Scope:** +- WhatsApp Cloud API channels only (primary focus) +- 360Dialog provider is deprecated and not supported +- Future extension to other WhatsApp providers like twilio can be considered + +**Provider Support:** +- Implement in `app/services/whatsapp/providers/whatsapp_cloud_service.rb` +- Use existing template management methods + +## Template Status Checking + +**Real-time Status API:** + +Check template status before sending surveys: + +```bash +curl --location 'https://graph.facebook.com/v20.0/{{business_account_id}}/message_templates?name={{template_name}}&access_token={{access_token}}' +``` + +Example: +```bash +curl --location 'https://graph.facebook.com/v20.0/1189403312549467/message_templates?name=customer_satisfaction_survey&access_token={{access_token}}' +``` + +**Response Format:** +```json +{ + "data": [ + { + "name": "customer_satisfaction_survey", + "status": "APPROVED|PENDING|REJECTED|DISABLED", + "id": "123456789", + "language": "en", + "category": "MARKETING" + } + ] +} +``` + +**Implementation Points:** + +1. **Frontend Configuration Page**: Check template status when user visits CSAT settings to show approval status +2. **Before Survey Sending**: Real-time API call to verify template is approved - if approved, always use template +3. **Caching Strategy**: Consider short-term caching (5-10 minutes) to avoid excessive API calls during high-volume periods +4. **Template Priority**: Approved templates bypass messaging window restrictions and are always sent + +**Analytics:** +- Template usage tracking not included in initial implementation +- Regular CSAT analytics remain unchanged +- Can be added in future iterations + +**Enterprise Compatibility:** +- No specific enterprise overrides required +- Standard CSAT enterprise policies apply \ No newline at end of file