mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-10-30 18:47:51 +00:00 
			
		
		
		
	| @@ -4,7 +4,7 @@ class ContactInboxBuilder | ||||
|   def perform | ||||
|     @contact = Contact.find(contact_id) | ||||
|     @inbox = @contact.account.inboxes.find(inbox_id) | ||||
|     return unless ['Channel::TwilioSms', 'Channel::Email', 'Channel::Api', 'Channel::Whatsapp'].include? @inbox.channel_type | ||||
|     return unless ['Channel::TwilioSms', 'Channel::Sms', 'Channel::Email', 'Channel::Api', 'Channel::Whatsapp'].include? @inbox.channel_type | ||||
|  | ||||
|     source_id = @source_id || generate_source_id | ||||
|     create_contact_inbox(source_id) if source_id.present? | ||||
| @@ -13,12 +13,18 @@ class ContactInboxBuilder | ||||
|   private | ||||
|  | ||||
|   def generate_source_id | ||||
|     return twilio_source_id if @inbox.channel_type == 'Channel::TwilioSms' | ||||
|     return wa_source_id if @inbox.channel_type == 'Channel::Whatsapp' | ||||
|     return @contact.email if @inbox.channel_type == 'Channel::Email' | ||||
|     return SecureRandom.uuid if @inbox.channel_type == 'Channel::Api' | ||||
|  | ||||
|     nil | ||||
|     case @inbox.channel_type | ||||
|     when 'Channel::TwilioSms' | ||||
|       twilio_source_id | ||||
|     when 'Channel::Whatsapp' | ||||
|       wa_source_id | ||||
|     when 'Channel::Email' | ||||
|       @contact.email | ||||
|     when 'Channel::Sms' | ||||
|       @contact.phone_number | ||||
|     when 'Channel::Api' | ||||
|       SecureRandom.uuid | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def wa_source_id | ||||
|   | ||||
| @@ -91,20 +91,9 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController | ||||
|   end | ||||
|  | ||||
|   def create_channel | ||||
|     case permitted_params[:channel][:type] | ||||
|     when 'web_widget' | ||||
|       Current.account.web_widgets.create!(permitted_params(Channel::WebWidget::EDITABLE_ATTRS)[:channel].except(:type)) | ||||
|     when 'api' | ||||
|       Current.account.api_channels.create!(permitted_params(Channel::Api::EDITABLE_ATTRS)[:channel].except(:type)) | ||||
|     when 'email' | ||||
|       Current.account.email_channels.create!(permitted_params(Channel::Email::EDITABLE_ATTRS)[:channel].except(:type)) | ||||
|     when 'line' | ||||
|       Current.account.line_channels.create!(permitted_params(Channel::Line::EDITABLE_ATTRS)[:channel].except(:type)) | ||||
|     when 'telegram' | ||||
|       Current.account.telegram_channels.create!(permitted_params(Channel::Telegram::EDITABLE_ATTRS)[:channel].except(:type)) | ||||
|     when 'whatsapp' | ||||
|       Current.account.whatsapp_channels.create!(permitted_params(Channel::Whatsapp::EDITABLE_ATTRS)[:channel].except(:type)) | ||||
|     end | ||||
|     return unless %w[web_widget api email line telegram whatsapp sms].include?(permitted_params[:channel][:type]) | ||||
|  | ||||
|     account_channels_method.create!(permitted_params(channel_type_from_params::EDITABLE_ATTRS)[:channel].except(:type)) | ||||
|   end | ||||
|  | ||||
|   def update_channel_feature_flags | ||||
| @@ -123,6 +112,30 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController | ||||
|     ) | ||||
|   end | ||||
|  | ||||
|   def channel_type_from_params | ||||
|     { | ||||
|       'web_widget' => Channel::WebWidget, | ||||
|       'api' => Channel::Api, | ||||
|       'email' => Channel::Email, | ||||
|       'line' => Channel::Line, | ||||
|       'telegram' => Channel::Telegram, | ||||
|       'whatsapp' => Channel::Whatsapp, | ||||
|       'sms' => Channel::Sms | ||||
|     }[permitted_params[:channel][:type]] | ||||
|   end | ||||
|  | ||||
|   def account_channels_method | ||||
|     { | ||||
|       'web_widget' => Current.account.web_widgets, | ||||
|       'api' => Current.account.api_channels, | ||||
|       'email' => Current.account.email_channels, | ||||
|       'line' => Current.account.line_channels, | ||||
|       'telegram' => Current.account.telegram_channels, | ||||
|       'whatsapp' => Current.account.whatsapp_channels, | ||||
|       'sms' => Current.account.sms_channels | ||||
|     }[permitted_params[:channel][:type]] | ||||
|   end | ||||
|  | ||||
|   def get_channel_attributes(channel_type) | ||||
|     if channel_type.constantize.const_defined?('EDITABLE_ATTRS') | ||||
|       channel_type.constantize::EDITABLE_ATTRS.presence | ||||
|   | ||||
							
								
								
									
										6
									
								
								app/controllers/webhooks/sms_controller.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								app/controllers/webhooks/sms_controller.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| class Webhooks::SmsController < ActionController::API | ||||
|   def process_payload | ||||
|     Webhooks::SmsEventsJob.perform_later(params['_json']&.first&.to_unsafe_hash) | ||||
|     head :ok | ||||
|   end | ||||
| end | ||||
| @@ -136,8 +136,56 @@ | ||||
|         } | ||||
|       }, | ||||
|       "SMS": { | ||||
|         "TITLE": "SMS Channel via Twilio", | ||||
|         "DESC": "Start supporting your customers via SMS with Twilio integration." | ||||
|         "TITLE": "SMS Channel", | ||||
|         "DESC": "Start supporting your customers via SMS.", | ||||
|         "PROVIDERS": { | ||||
|           "LABEL": "API Provider", | ||||
|           "TWILIO": "Twilio", | ||||
|           "BANDWIDTH": "Bandwidth" | ||||
|         }, | ||||
|         "API": { | ||||
|           "ERROR_MESSAGE": "We were not able to save the SMS channel" | ||||
|         }, | ||||
|         "BANDWIDTH": { | ||||
|           "ACCOUNT_ID": { | ||||
|             "LABEL": "Account ID", | ||||
|             "PLACEHOLDER": "Please enter your Bandwidth Account ID", | ||||
|             "ERROR": "This field is required" | ||||
|           }, | ||||
|           "API_KEY": { | ||||
|             "LABEL": "API Key", | ||||
|             "PLACEHOLDER": "Please enter your Bandwith API Key", | ||||
|             "ERROR": "This field is required" | ||||
|           }, | ||||
|           "API_SECRET": { | ||||
|             "LABEL": "API Secret", | ||||
|             "PLACEHOLDER": "Please enter your Bandwith API Secret", | ||||
|             "ERROR": "This field is required" | ||||
|           }, | ||||
|           "APPLICATION_ID": { | ||||
|             "LABEL": "Application ID", | ||||
|             "PLACEHOLDER": "Please enter your Bandwidth Application ID", | ||||
|             "ERROR": "This field is required" | ||||
|           }, | ||||
|           "INBOX_NAME": { | ||||
|             "LABEL": "Inbox Name", | ||||
|             "PLACEHOLDER": "Please enter a inbox name", | ||||
|             "ERROR": "This field is required" | ||||
|           }, | ||||
|           "PHONE_NUMBER": { | ||||
|             "LABEL": "Phone number", | ||||
|             "PLACEHOLDER": "Please enter the phone number from which message will be sent.", | ||||
|             "ERROR": "Please enter a valid value. Phone number should start with `+` sign." | ||||
|           }, | ||||
|           "SUBMIT_BUTTON": "Create Bandwidth Channel", | ||||
|           "API": { | ||||
|             "ERROR_MESSAGE": "We were not able to authenticate Bandwidth credentials, please try again" | ||||
|           }, | ||||
|           "API_CALLBACK": { | ||||
|             "TITLE": "Callback URL", | ||||
|             "SUBTITLE": "You have to configure the message callback URL in Bandwidth with the URL mentioned here." | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
| 			"WHATSAPP": { | ||||
|         "TITLE": "WhatsApp Channel", | ||||
|   | ||||
| @@ -247,7 +247,7 @@ export default { | ||||
|       if (this.isOngoingType) { | ||||
|         return this.$store.getters['inboxes/getWebsiteInboxes']; | ||||
|       } | ||||
|       return this.$store.getters['inboxes/getTwilioSMSInboxes']; | ||||
|       return this.$store.getters['inboxes/getSMSInboxes']; | ||||
|     }, | ||||
|     sendersAndBotList() { | ||||
|       return [ | ||||
|   | ||||
| @@ -171,7 +171,7 @@ export default { | ||||
|       if (this.isOngoingType) { | ||||
|         return this.$store.getters['inboxes/getWebsiteInboxes']; | ||||
|       } | ||||
|       return this.$store.getters['inboxes/getTwilioSMSInboxes']; | ||||
|       return this.$store.getters['inboxes/getSMSInboxes']; | ||||
|     }, | ||||
|     pageTitle() { | ||||
|       return `${this.$t('CAMPAIGN.EDIT.TITLE')} - ${ | ||||
|   | ||||
| @@ -50,7 +50,7 @@ export default { | ||||
|         { key: 'facebook', name: 'Messenger' }, | ||||
|         { key: 'twitter', name: 'Twitter' }, | ||||
|         { key: 'whatsapp', name: 'WhatsApp' }, | ||||
|         { key: 'sms', name: 'SMS via Twilio' }, | ||||
|         { key: 'sms', name: 'SMS' }, | ||||
|         { key: 'email', name: 'Email' }, | ||||
|         { | ||||
|           key: 'api', | ||||
|   | ||||
| @@ -29,6 +29,14 @@ | ||||
|           > | ||||
|           </woot-code> | ||||
|         </div> | ||||
|         <div class="medium-6 small-offset-3"> | ||||
|           <woot-code | ||||
|             v-if="isASmsInbox" | ||||
|             lang="html" | ||||
|             :script="currentInbox.callback_webhook_url" | ||||
|           > | ||||
|           </woot-code> | ||||
|         </div> | ||||
|         <div class="medium-6 small-offset-3"> | ||||
|           <woot-code | ||||
|             v-if="isAEmailInbox" | ||||
| @@ -86,6 +94,9 @@ export default { | ||||
|     isALineInbox() { | ||||
|       return this.currentInbox.channel_type === 'Channel::Line'; | ||||
|     }, | ||||
|     isASmsInbox() { | ||||
|       return this.currentInbox.channel_type === 'Channel::Sms'; | ||||
|     }, | ||||
|     message() { | ||||
|       if (this.isATwilioInbox) { | ||||
|         return `${this.$t('INBOX_MGMT.FINISH.MESSAGE')}. ${this.$t( | ||||
| @@ -93,6 +104,12 @@ export default { | ||||
|         )}`; | ||||
|       } | ||||
|  | ||||
|       if (this.isASmsInbox) { | ||||
|         return `${this.$t('INBOX_MGMT.FINISH.MESSAGE')}. ${this.$t( | ||||
|           'INBOX_MGMT.ADD.SMS.BANDWIDTH.API_CALLBACK.SUBTITLE' | ||||
|         )}`; | ||||
|       } | ||||
|  | ||||
|       if (this.isALineInbox) { | ||||
|         return `${this.$t('INBOX_MGMT.FINISH.MESSAGE')}. ${this.$t( | ||||
|           'INBOX_MGMT.ADD.LINE_CHANNEL.API_CALLBACK.SUBTITLE' | ||||
| @@ -103,10 +120,11 @@ export default { | ||||
|         return this.$t('INBOX_MGMT.ADD.EMAIL_CHANNEL.FINISH_MESSAGE'); | ||||
|       } | ||||
|  | ||||
|       if (!this.currentInbox.web_widget_script) { | ||||
|         return this.$t('INBOX_MGMT.FINISH.MESSAGE'); | ||||
|       } | ||||
|       if (this.currentInbox.web_widget_script) { | ||||
|         return this.$t('INBOX_MGMT.FINISH.WEBSITE_SUCCESS'); | ||||
|       } | ||||
|  | ||||
|       return this.$t('INBOX_MGMT.FINISH.MESSAGE'); | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
|   | ||||
| @@ -48,6 +48,9 @@ | ||||
|                 <span v-if="item.channel_type === 'Channel::Whatsapp'"> | ||||
|                   Whatsapp | ||||
|                 </span> | ||||
|                 <span v-if="item.channel_type === 'Channel::Sms'"> | ||||
|                   Sms | ||||
|                 </span> | ||||
|                 <span v-if="item.channel_type === 'Channel::Email'"> | ||||
|                   Email | ||||
|                 </span> | ||||
|   | ||||
| @@ -0,0 +1,181 @@ | ||||
| <template> | ||||
|   <form class="row" @submit.prevent="createChannel()"> | ||||
|     <div class="medium-8 columns"> | ||||
|       <label :class="{ error: $v.inboxName.$error }"> | ||||
|         {{ $t('INBOX_MGMT.ADD.SMS.BANDWIDTH.INBOX_NAME.LABEL') }} | ||||
|         <input | ||||
|           v-model.trim="inboxName" | ||||
|           type="text" | ||||
|           :placeholder=" | ||||
|             $t('INBOX_MGMT.ADD.SMS.BANDWIDTH.INBOX_NAME.PLACEHOLDER') | ||||
|           " | ||||
|           @blur="$v.inboxName.$touch" | ||||
|         /> | ||||
|         <span v-if="$v.inboxName.$error" class="message">{{ | ||||
|           $t('INBOX_MGMT.ADD.SMS.BANDWIDTH.INBOX_NAME.ERROR') | ||||
|         }}</span> | ||||
|       </label> | ||||
|     </div> | ||||
|  | ||||
|     <div class="medium-8 columns"> | ||||
|       <label :class="{ error: $v.phoneNumber.$error }"> | ||||
|         {{ $t('INBOX_MGMT.ADD.SMS.BANDWIDTH.PHONE_NUMBER.LABEL') }} | ||||
|         <input | ||||
|           v-model.trim="phoneNumber" | ||||
|           type="text" | ||||
|           :placeholder=" | ||||
|             $t('INBOX_MGMT.ADD.SMS.BANDWIDTH.PHONE_NUMBER.PLACEHOLDER') | ||||
|           " | ||||
|           @blur="$v.phoneNumber.$touch" | ||||
|         /> | ||||
|         <span v-if="$v.phoneNumber.$error" class="message">{{ | ||||
|           $t('INBOX_MGMT.ADD.SMS.BANDWIDTH.PHONE_NUMBER.ERROR') | ||||
|         }}</span> | ||||
|       </label> | ||||
|     </div> | ||||
|  | ||||
|     <div class="medium-8 columns"> | ||||
|       <label :class="{ error: $v.accountId.$error }"> | ||||
|         {{ $t('INBOX_MGMT.ADD.SMS.BANDWIDTH.ACCOUNT_ID.LABEL') }} | ||||
|         <input | ||||
|           v-model.trim="accountId" | ||||
|           type="text" | ||||
|           :placeholder=" | ||||
|             $t('INBOX_MGMT.ADD.SMS.BANDWIDTH.ACCOUNT_ID.PLACEHOLDER') | ||||
|           " | ||||
|           @blur="$v.accountId.$touch" | ||||
|         /> | ||||
|         <span v-if="$v.accountId.$error" class="message">{{ | ||||
|           $t('INBOX_MGMT.ADD.SMS.BANDWIDTH.ACCOUNT_ID.ERROR') | ||||
|         }}</span> | ||||
|       </label> | ||||
|     </div> | ||||
|  | ||||
|     <div class="medium-8 columns"> | ||||
|       <label :class="{ error: $v.applicationId.$error }"> | ||||
|         {{ $t('INBOX_MGMT.ADD.SMS.BANDWIDTH.APPLICATION_ID.LABEL') }} | ||||
|         <input | ||||
|           v-model.trim="applicationId" | ||||
|           type="text" | ||||
|           :placeholder=" | ||||
|             $t('INBOX_MGMT.ADD.SMS.BANDWIDTH.APPLICATION_ID.PLACEHOLDER') | ||||
|           " | ||||
|           @blur="$v.applicationId.$touch" | ||||
|         /> | ||||
|         <span v-if="$v.applicationId.$error" class="message">{{ | ||||
|           $t('INBOX_MGMT.ADD.SMS.BANDWIDTH.APPLICATION_ID.ERROR') | ||||
|         }}</span> | ||||
|       </label> | ||||
|     </div> | ||||
|  | ||||
|     <div class="medium-8 columns"> | ||||
|       <label :class="{ error: $v.apiKey.$error }"> | ||||
|         {{ $t('INBOX_MGMT.ADD.SMS.BANDWIDTH.API_KEY.LABEL') }} | ||||
|         <input | ||||
|           v-model.trim="apiKey" | ||||
|           type="text" | ||||
|           :placeholder="$t('INBOX_MGMT.ADD.SMS.BANDWIDTH.API_KEY.PLACEHOLDER')" | ||||
|           @blur="$v.apiKey.$touch" | ||||
|         /> | ||||
|         <span v-if="$v.apiKey.$error" class="message">{{ | ||||
|           $t('INBOX_MGMT.ADD.SMS.BANDWIDTH.API_KEY.ERROR') | ||||
|         }}</span> | ||||
|       </label> | ||||
|     </div> | ||||
|  | ||||
|     <div class="medium-8 columns"> | ||||
|       <label :class="{ error: $v.apiSecret.$error }"> | ||||
|         {{ $t('INBOX_MGMT.ADD.SMS.BANDWIDTH.API_SECRET.LABEL') }} | ||||
|         <input | ||||
|           v-model.trim="apiSecret" | ||||
|           type="text" | ||||
|           :placeholder=" | ||||
|             $t('INBOX_MGMT.ADD.SMS.BANDWIDTH.API_SECRET.PLACEHOLDER') | ||||
|           " | ||||
|           @blur="$v.apiSecret.$touch" | ||||
|         /> | ||||
|         <span v-if="$v.apiSecret.$error" class="message">{{ | ||||
|           $t('INBOX_MGMT.ADD.SMS.BANDWIDTH.API_SECRET.ERROR') | ||||
|         }}</span> | ||||
|       </label> | ||||
|     </div> | ||||
|  | ||||
|     <div class="medium-12 columns"> | ||||
|       <woot-submit-button | ||||
|         :loading="uiFlags.isCreating" | ||||
|         :button-text="$t('INBOX_MGMT.ADD.SMS.BANDWIDTH.SUBMIT_BUTTON')" | ||||
|       /> | ||||
|     </div> | ||||
|   </form> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import { mapGetters } from 'vuex'; | ||||
| import alertMixin from 'shared/mixins/alertMixin'; | ||||
| import { required } from 'vuelidate/lib/validators'; | ||||
| import router from '../../../../index'; | ||||
|  | ||||
| const shouldStartWithPlusSign = (value = '') => value.startsWith('+'); | ||||
|  | ||||
| export default { | ||||
|   mixins: [alertMixin], | ||||
|   data() { | ||||
|     return { | ||||
|       accountId: '', | ||||
|       apiKey: '', | ||||
|       apiSecret: '', | ||||
|       applicationId: '', | ||||
|       inboxName: '', | ||||
|       phoneNumber: '', | ||||
|     }; | ||||
|   }, | ||||
|   computed: { | ||||
|     ...mapGetters({ | ||||
|       uiFlags: 'inboxes/getUIFlags', | ||||
|       globalConfig: 'globalConfig/get', | ||||
|     }), | ||||
|   }, | ||||
|   validations: { | ||||
|     inboxName: { required }, | ||||
|     phoneNumber: { required, shouldStartWithPlusSign }, | ||||
|     apiKey: { required }, | ||||
|     apiSecret: { required }, | ||||
|     applicationId: { required }, | ||||
|     accountId: { required }, | ||||
|   }, | ||||
|   methods: { | ||||
|     async createChannel() { | ||||
|       this.$v.$touch(); | ||||
|       if (this.$v.$invalid) { | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       try { | ||||
|         const smsChannel = await this.$store.dispatch('inboxes/createChannel', { | ||||
|           name: this.inboxName, | ||||
|           channel: { | ||||
|             type: 'sms', | ||||
|             phone_number: this.phoneNumber, | ||||
|             provider_config: { | ||||
|               api_key: this.apiKey, | ||||
|               api_secret: this.apiSecret, | ||||
|               application_id: this.applicationId, | ||||
|               account_id: this.accountId, | ||||
|             }, | ||||
|           }, | ||||
|         }); | ||||
|  | ||||
|         router.replace({ | ||||
|           name: 'settings_inboxes_add_agents', | ||||
|           params: { | ||||
|             page: 'new', | ||||
|             inbox_id: smsChannel.id, | ||||
|           }, | ||||
|         }); | ||||
|       } catch (error) { | ||||
|         this.showAlert(this.$t('INBOX_MGMT.ADD.SMS.API.ERROR_MESSAGE')); | ||||
|       } | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
| @@ -4,18 +4,39 @@ | ||||
|       :header-title="$t('INBOX_MGMT.ADD.SMS.TITLE')" | ||||
|       :header-content="$t('INBOX_MGMT.ADD.SMS.DESC')" | ||||
|     /> | ||||
|     <twilio type="sms"></twilio> | ||||
|     <div class="medium-8 columns"> | ||||
|       <label> | ||||
|         {{ $t('INBOX_MGMT.ADD.SMS.PROVIDERS.LABEL') }} | ||||
|         <select v-model="provider"> | ||||
|           <option value="twilio"> | ||||
|             {{ $t('INBOX_MGMT.ADD.SMS.PROVIDERS.TWILIO') }} | ||||
|           </option> | ||||
|           <option value="360dialog"> | ||||
|             {{ $t('INBOX_MGMT.ADD.SMS.PROVIDERS.BANDWIDTH') }} | ||||
|           </option> | ||||
|         </select> | ||||
|       </label> | ||||
|     </div> | ||||
|     <twilio v-if="provider === 'twilio'" type="sms"></twilio> | ||||
|     <bandwidth-sms v-else /> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import PageHeader from '../../SettingsSubPageHeader'; | ||||
| import BandwidthSms from './BandwidthSms.vue'; | ||||
| import Twilio from './Twilio'; | ||||
|  | ||||
| export default { | ||||
|   components: { | ||||
|     PageHeader, | ||||
|     Twilio, | ||||
|     BandwidthSms, | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       provider: 'twilio', | ||||
|     }; | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
|   | ||||
| @@ -78,9 +78,11 @@ export const getters = { | ||||
|       item => item.channel_type === INBOX_TYPES.TWILIO | ||||
|     ); | ||||
|   }, | ||||
|   getTwilioSMSInboxes($state) { | ||||
|   getSMSInboxes($state) { | ||||
|     return $state.records.filter( | ||||
|       item => item.channel_type === INBOX_TYPES.TWILIO && item.medium === 'sms' | ||||
|       item => | ||||
|         item.channel_type === INBOX_TYPES.SMS || | ||||
|         (item.channel_type === INBOX_TYPES.TWILIO && item.medium === 'sms') | ||||
|     ); | ||||
|   }, | ||||
|   dialogFlowEnabledInboxes($state) { | ||||
|   | ||||
| @@ -55,4 +55,11 @@ export default [ | ||||
|     website_token: 'randomid125', | ||||
|     enable_auto_assignment: true, | ||||
|   }, | ||||
|   { | ||||
|     id: 6, | ||||
|     channel_id: 6, | ||||
|     name: 'Test Widget 6', | ||||
|     channel_type: 'Channel::Sms', | ||||
|     provider: 'default', | ||||
|   }, | ||||
| ]; | ||||
|   | ||||
| @@ -19,14 +19,14 @@ describe('#getters', () => { | ||||
|     expect(getters.getTwilioInboxes(state).length).toEqual(1); | ||||
|   }); | ||||
|  | ||||
|   it('getTwilioSMSInboxes', () => { | ||||
|   it('getSMSInboxes', () => { | ||||
|     const state = { records: inboxList }; | ||||
|     expect(getters.getTwilioSMSInboxes(state).length).toEqual(1); | ||||
|     expect(getters.getSMSInboxes(state).length).toEqual(2); | ||||
|   }); | ||||
|  | ||||
|   it('dialogFlowEnabledInboxes', () => { | ||||
|     const state = { records: inboxList }; | ||||
|     expect(getters.dialogFlowEnabledInboxes(state).length).toEqual(5); | ||||
|     expect(getters.dialogFlowEnabledInboxes(state).length).toEqual(6); | ||||
|   }); | ||||
|  | ||||
|   it('getInbox', () => { | ||||
|   | ||||
| @@ -8,6 +8,7 @@ export const INBOX_TYPES = { | ||||
|   EMAIL: 'Channel::Email', | ||||
|   TELEGRAM: 'Channel::Telegram', | ||||
|   LINE: 'Channel::Line', | ||||
|   SMS: 'Channel::Sms', | ||||
| }; | ||||
|  | ||||
| export default { | ||||
|   | ||||
| @@ -6,19 +6,20 @@ class SendReplyJob < ApplicationJob | ||||
|     conversation = message.conversation | ||||
|     channel_name = conversation.inbox.channel.class.to_s | ||||
|  | ||||
|     services = { | ||||
|       'Channel::TwitterProfile' => ::Twitter::SendOnTwitterService, | ||||
|       'Channel::TwilioSms' => ::Twilio::SendOnTwilioService, | ||||
|       'Channel::Line' => ::Line::SendOnLineService, | ||||
|       'Channel::Telegram' => ::Telegram::SendOnTelegramService, | ||||
|       'Channel::Whatsapp' => ::Whatsapp::SendOnWhatsappService, | ||||
|       'Channel::Sms' => ::Sms::SendOnSmsService | ||||
|     } | ||||
|  | ||||
|     case channel_name | ||||
|     when 'Channel::FacebookPage' | ||||
|       send_on_facebook_page(message) | ||||
|     when 'Channel::TwitterProfile' | ||||
|       ::Twitter::SendOnTwitterService.new(message: message).perform | ||||
|     when 'Channel::TwilioSms' | ||||
|       ::Twilio::SendOnTwilioService.new(message: message).perform | ||||
|     when 'Channel::Line' | ||||
|       ::Line::SendOnLineService.new(message: message).perform | ||||
|     when 'Channel::Telegram' | ||||
|       ::Telegram::SendOnTelegramService.new(message: message).perform | ||||
|     when 'Channel::Whatsapp' | ||||
|       ::Whatsapp::SendOnWhatsappService.new(message: message).perform | ||||
|     else | ||||
|       services[channel_name].new(message: message).perform if services[channel_name].present? | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   | ||||
							
								
								
									
										13
									
								
								app/jobs/webhooks/sms_events_job.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								app/jobs/webhooks/sms_events_job.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| class Webhooks::SmsEventsJob < ApplicationJob | ||||
|   queue_as :default | ||||
|  | ||||
|   def perform(params = {}) | ||||
|     return unless params[:type] == 'message-received' | ||||
|  | ||||
|     channel = Channel::Sms.find_by(phone_number: params[:to]) | ||||
|     return unless channel | ||||
|  | ||||
|     # TODO: pass to appropriate provider service from here | ||||
|     Sms::IncomingMessageService.new(inbox: channel.inbox, params: params[:message].with_indifferent_access).perform | ||||
|   end | ||||
| end | ||||
| @@ -69,6 +69,7 @@ class Account < ApplicationRecord | ||||
|   has_many :web_widgets, dependent: :destroy_async, class_name: '::Channel::WebWidget' | ||||
|   has_many :webhooks, dependent: :destroy_async | ||||
|   has_many :whatsapp_channels, dependent: :destroy_async, class_name: '::Channel::Whatsapp' | ||||
|   has_many :sms_channels, dependent: :destroy_async, class_name: '::Channel::Sms' | ||||
|   has_many :working_hours, dependent: :destroy_async | ||||
|   has_many :automation_rules, dependent: :destroy | ||||
|  | ||||
|   | ||||
| @@ -58,6 +58,7 @@ class Campaign < ApplicationRecord | ||||
|     return if completed? | ||||
|  | ||||
|     Twilio::OneoffSmsCampaignService.new(campaign: self).perform if inbox.inbox_type == 'Twilio SMS' | ||||
|     Sms::OneoffSmsCampaignService.new(campaign: self).perform if inbox.inbox_type == 'Sms' | ||||
|   end | ||||
|  | ||||
|   private | ||||
| @@ -69,14 +70,14 @@ class Campaign < ApplicationRecord | ||||
|   def validate_campaign_inbox | ||||
|     return unless inbox | ||||
|  | ||||
|     errors.add :inbox, 'Unsupported Inbox type' unless ['Website', 'Twilio SMS'].include? inbox.inbox_type | ||||
|     errors.add :inbox, 'Unsupported Inbox type' unless ['Website', 'Twilio SMS', 'Sms'].include? inbox.inbox_type | ||||
|   end | ||||
|  | ||||
|   # TO-DO we clean up with better validations when campaigns evolve into more inboxes | ||||
|   def ensure_correct_campaign_attributes | ||||
|     return if inbox.blank? | ||||
|  | ||||
|     if inbox.inbox_type == 'Twilio SMS' | ||||
|     if ['Twilio SMS', 'Sms'].include?(inbox.inbox_type) | ||||
|       self.campaign_type = 'one_off' | ||||
|       self.scheduled_at ||= Time.now.utc | ||||
|     else | ||||
|   | ||||
							
								
								
									
										81
									
								
								app/models/channel/sms.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								app/models/channel/sms.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,81 @@ | ||||
| # == Schema Information | ||||
| # | ||||
| # Table name: channel_sms | ||||
| # | ||||
| #  id              :bigint           not null, primary key | ||||
| #  phone_number    :string           not null | ||||
| #  provider        :string           default("default") | ||||
| #  provider_config :jsonb | ||||
| #  created_at      :datetime         not null | ||||
| #  updated_at      :datetime         not null | ||||
| #  account_id      :integer          not null | ||||
| # | ||||
| # Indexes | ||||
| # | ||||
| #  index_channel_sms_on_phone_number  (phone_number) UNIQUE | ||||
| # | ||||
|  | ||||
| class Channel::Sms < ApplicationRecord | ||||
|   include Channelable | ||||
|  | ||||
|   self.table_name = 'channel_sms' | ||||
|   EDITABLE_ATTRS = [:phone_number, { provider_config: {} }].freeze | ||||
|  | ||||
|   validates :phone_number, presence: true, uniqueness: true | ||||
|   # before_save :validate_provider_config | ||||
|  | ||||
|   def name | ||||
|     'Sms' | ||||
|   end | ||||
|  | ||||
|   # all this should happen in provider service . but hack mode on | ||||
|   def api_base_path | ||||
|     'https://messaging.bandwidth.com/api/v2' | ||||
|   end | ||||
|  | ||||
|   # Extract later into provider Service | ||||
|   def send_message(phone_number, message) | ||||
|     if message.attachments.present? | ||||
|       send_attachment_message(phone_number, message) | ||||
|     else | ||||
|       send_text_message(phone_number, message.content) | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def send_text_message(contact_number, message) | ||||
|     response = HTTParty.post( | ||||
|       "#{api_base_path}/users/#{provider_config['account_id']}/messages", | ||||
|       basic_auth: bandwidth_auth, | ||||
|       headers: { 'Content-Type' => 'application/json' }, | ||||
|       body: { | ||||
|         'to' => contact_number, | ||||
|         'from' => phone_number, | ||||
|         'text' => message, | ||||
|         'applicationId' => provider_config['application_id'] | ||||
|       }.to_json | ||||
|     ) | ||||
|  | ||||
|     response.success? ? response.parsed_response['id'] : nil | ||||
|   end | ||||
|  | ||||
|   private | ||||
|  | ||||
|   def send_attachment_message(phone_number, message) | ||||
|     # fix me | ||||
|   end | ||||
|  | ||||
|   def bandwidth_auth | ||||
|     { username: provider_config['api_key'], password: provider_config['api_secret'] } | ||||
|   end | ||||
|  | ||||
|   # Extract later into provider Service | ||||
|   # let's revisit later | ||||
|   def validate_provider_config | ||||
|     response = HTTParty.post( | ||||
|       "#{api_base_path}/users/#{provider_config['account_id']}/messages", | ||||
|       basic_auth: bandwidth_auth, | ||||
|       headers: { 'Content-Type': 'application/json' } | ||||
|     ) | ||||
|     errors.add(:provider_config, 'error setting up') unless response.success? | ||||
|   end | ||||
| end | ||||
| @@ -149,6 +149,6 @@ class Channel::Whatsapp < ApplicationRecord | ||||
|         url: "#{ENV['FRONTEND_URL']}/webhooks/whatsapp/#{phone_number}" | ||||
|       }.to_json | ||||
|     ) | ||||
|     errors.add(:bot_token, 'error setting up the webook') unless response.success? | ||||
|     errors.add(:provider_config, 'error setting up the webook') unless response.success? | ||||
|   end | ||||
| end | ||||
|   | ||||
| @@ -107,6 +107,8 @@ class Inbox < ApplicationRecord | ||||
|     case channel_type | ||||
|     when 'Channel::TwilioSms' | ||||
|       "#{ENV['FRONTEND_URL']}/twilio/callback" | ||||
|     when 'Channel::Sms' | ||||
|       "#{ENV['FRONTEND_URL']}/webhooks/sms/#{channel.phone_number.delete_prefix('+')}" | ||||
|     when 'Channel::Line' | ||||
|       "#{ENV['FRONTEND_URL']}/webhooks/line/#{channel.line_channel_id}" | ||||
|     end | ||||
|   | ||||
| @@ -14,6 +14,8 @@ class Contacts::ContactableInboxesService | ||||
|       twilio_contactable_inbox(inbox) | ||||
|     when 'Channel::Whatsapp' | ||||
|       whatsapp_contactable_inbox(inbox) | ||||
|     when 'Channel::Sms' | ||||
|       sms_contactable_inbox(inbox) | ||||
|     when 'Channel::Email' | ||||
|       email_contactable_inbox(inbox) | ||||
|     when 'Channel::Api' | ||||
| @@ -52,6 +54,12 @@ class Contacts::ContactableInboxesService | ||||
|     { source_id: @contact.phone_number.delete('+'), inbox: inbox } | ||||
|   end | ||||
|  | ||||
|   def sms_contactable_inbox(inbox) | ||||
|     return unless @contact.phone_number | ||||
|  | ||||
|     { source_id: @contact.phone_number, inbox: inbox } | ||||
|   end | ||||
|  | ||||
|   def twilio_contactable_inbox(inbox) | ||||
|     return if @contact.phone_number.blank? | ||||
|  | ||||
|   | ||||
							
								
								
									
										66
									
								
								app/services/sms/incoming_message_service.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								app/services/sms/incoming_message_service.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,66 @@ | ||||
| class Sms::IncomingMessageService | ||||
|   include ::FileTypeHelper | ||||
|  | ||||
|   pattr_initialize [:inbox!, :params!] | ||||
|  | ||||
|   def perform | ||||
|     set_contact | ||||
|     set_conversation | ||||
|     @message = @conversation.messages.create( | ||||
|       content: params[:text], | ||||
|       account_id: @inbox.account_id, | ||||
|       inbox_id: @inbox.id, | ||||
|       message_type: :incoming, | ||||
|       sender: @contact, | ||||
|       source_id: params[:id] | ||||
|     ) | ||||
|   end | ||||
|  | ||||
|   private | ||||
|  | ||||
|   def account | ||||
|     @account ||= @inbox.account | ||||
|   end | ||||
|  | ||||
|   def phone_number | ||||
|     params[:from] | ||||
|   end | ||||
|  | ||||
|   def formatted_phone_number | ||||
|     TelephoneNumber.parse(phone_number).international_number | ||||
|   end | ||||
|  | ||||
|   def set_contact | ||||
|     contact_inbox = ::ContactBuilder.new( | ||||
|       source_id: params[:from], | ||||
|       inbox: @inbox, | ||||
|       contact_attributes: contact_attributes | ||||
|     ).perform | ||||
|  | ||||
|     @contact_inbox = contact_inbox | ||||
|     @contact = contact_inbox.contact | ||||
|   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 set_conversation | ||||
|     @conversation = @contact_inbox.conversations.first | ||||
|     return if @conversation | ||||
|  | ||||
|     @conversation = ::Conversation.create!(conversation_params) | ||||
|   end | ||||
|  | ||||
|   def contact_attributes | ||||
|     { | ||||
|       name: formatted_phone_number, | ||||
|       phone_number: phone_number | ||||
|     } | ||||
|   end | ||||
| end | ||||
							
								
								
									
										32
									
								
								app/services/sms/oneoff_sms_campaign_service.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								app/services/sms/oneoff_sms_campaign_service.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | ||||
| class Sms::OneoffSmsCampaignService | ||||
|   pattr_initialize [:campaign!] | ||||
|  | ||||
|   def perform | ||||
|     raise "Invalid campaign #{campaign.id}" if campaign.inbox.inbox_type != 'Sms' || !campaign.one_off? | ||||
|     raise 'Completed Campaign' if campaign.completed? | ||||
|  | ||||
|     # marks campaign completed so that other jobs won't pick it up | ||||
|     campaign.completed! | ||||
|  | ||||
|     audience_label_ids = campaign.audience.select { |audience| audience['type'] == 'Label' }.pluck('id') | ||||
|     audience_labels = campaign.account.labels.where(id: audience_label_ids).pluck(:title) | ||||
|     process_audience(audience_labels) | ||||
|   end | ||||
|  | ||||
|   private | ||||
|  | ||||
|   delegate :inbox, to: :campaign | ||||
|   delegate :channel, to: :inbox | ||||
|  | ||||
|   def process_audience(audience_labels) | ||||
|     campaign.account.contacts.tagged_with(audience_labels, any: true).each do |contact| | ||||
|       next if contact.phone_number.blank? | ||||
|  | ||||
|       send_message(to: contact.phone_number, content: campaign.message) | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def send_message(to:, content:) | ||||
|     channel.send_text_message(to, content) | ||||
|   end | ||||
| end | ||||
							
								
								
									
										16
									
								
								app/services/sms/send_on_sms_service.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								app/services/sms/send_on_sms_service.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| class Sms::SendOnSmsService < Base::SendOnChannelService | ||||
|   private | ||||
|  | ||||
|   def channel_class | ||||
|     Channel::Sms | ||||
|   end | ||||
|  | ||||
|   def perform_reply | ||||
|     send_on_sms | ||||
|   end | ||||
|  | ||||
|   def send_on_sms | ||||
|     message_id = channel.send_message(message.conversation.contact_inbox.source_id, message) | ||||
|     message.update!(source_id: message_id) if message_id.present? | ||||
|   end | ||||
| end | ||||
| @@ -271,6 +271,7 @@ Rails.application.routes.draw do | ||||
|   post 'webhooks/line/:line_channel_id', to: 'webhooks/line#process_payload' | ||||
|   post 'webhooks/telegram/:bot_token', to: 'webhooks/telegram#process_payload' | ||||
|   post 'webhooks/whatsapp/:phone_number', to: 'webhooks/whatsapp#process_payload' | ||||
|   post 'webhooks/sms/:phone_number', to: 'webhooks/sms#process_payload' | ||||
|   get 'webhooks/instagram', to: 'webhooks/instagram#verify' | ||||
|   post 'webhooks/instagram', to: 'webhooks/instagram#events' | ||||
|  | ||||
|   | ||||
							
								
								
									
										11
									
								
								db/migrate/20220129024443_add_sms_channel.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								db/migrate/20220129024443_add_sms_channel.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| class AddSmsChannel < ActiveRecord::Migration[6.1] | ||||
|   def change | ||||
|     create_table :channel_sms do |t| | ||||
|       t.integer :account_id, null: false | ||||
|       t.string :phone_number, null: false, index: { unique: true } | ||||
|       t.string :provider, default: 'default' | ||||
|       t.jsonb :provider_config, default: {} | ||||
|       t.timestamps | ||||
|     end | ||||
|   end | ||||
| end | ||||
							
								
								
									
										10
									
								
								db/schema.rb
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								db/schema.rb
									
									
									
									
									
								
							| @@ -228,6 +228,16 @@ ActiveRecord::Schema.define(version: 2022_01_31_081750) do | ||||
|     t.index ["line_channel_id"], name: "index_channel_line_on_line_channel_id", unique: true | ||||
|   end | ||||
|  | ||||
|   create_table "channel_sms", force: :cascade do |t| | ||||
|     t.integer "account_id", null: false | ||||
|     t.string "phone_number", null: false | ||||
|     t.string "provider", default: "default" | ||||
|     t.jsonb "provider_config", default: {} | ||||
|     t.datetime "created_at", precision: 6, null: false | ||||
|     t.datetime "updated_at", precision: 6, null: false | ||||
|     t.index ["phone_number"], name: "index_channel_sms_on_phone_number", unique: true | ||||
|   end | ||||
|  | ||||
|   create_table "channel_telegram", force: :cascade do |t| | ||||
|     t.string "bot_name" | ||||
|     t.integer "account_id", null: false | ||||
|   | ||||
| @@ -99,6 +99,53 @@ describe ::ContactInboxBuilder do | ||||
|       end | ||||
|     end | ||||
|  | ||||
|     describe 'sms inbox' do | ||||
|       let!(:sms_channel) { create(:channel_sms, account: account) } | ||||
|       let!(:sms_inbox) { create(:inbox, channel: sms_channel, account: account) } | ||||
|  | ||||
|       it 'does not create contact inbox when contact inbox already exists with the source id provided' do | ||||
|         existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: sms_inbox, source_id: contact.phone_number) | ||||
|         contact_inbox = described_class.new( | ||||
|           contact_id: contact.id, | ||||
|           inbox_id: sms_inbox.id, | ||||
|           source_id: contact.phone_number | ||||
|         ).perform | ||||
|  | ||||
|         expect(contact_inbox.id).to be(existing_contact_inbox.id) | ||||
|       end | ||||
|  | ||||
|       it 'does not create contact inbox when contact inbox already exists with phone number and source id is not provided' do | ||||
|         existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: sms_inbox, source_id: contact.phone_number) | ||||
|         contact_inbox = described_class.new( | ||||
|           contact_id: contact.id, | ||||
|           inbox_id: sms_inbox.id | ||||
|         ).perform | ||||
|  | ||||
|         expect(contact_inbox.id).to be(existing_contact_inbox.id) | ||||
|       end | ||||
|  | ||||
|       it 'creates a new contact inbox when different source id is provided' do | ||||
|         existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: sms_inbox, source_id: contact.phone_number) | ||||
|         contact_inbox = described_class.new( | ||||
|           contact_id: contact.id, | ||||
|           inbox_id: sms_inbox.id, | ||||
|           source_id: '+224213223422' | ||||
|         ).perform | ||||
|  | ||||
|         expect(contact_inbox.id).not_to be(existing_contact_inbox.id) | ||||
|         expect(contact_inbox.source_id).not_to be('+224213223422') | ||||
|       end | ||||
|  | ||||
|       it 'creates a contact inbox with contact phone number when source id not provided and no contact inbox exists' do | ||||
|         contact_inbox = described_class.new( | ||||
|           contact_id: contact.id, | ||||
|           inbox_id: sms_inbox.id | ||||
|         ).perform | ||||
|  | ||||
|         expect(contact_inbox.source_id).not_to be(contact.phone_number) | ||||
|       end | ||||
|     end | ||||
|  | ||||
|     describe 'email inbox' do | ||||
|       let!(:email_channel) { create(:channel_email, account: account) } | ||||
|       let!(:email_inbox) { create(:inbox, channel: email_channel, account: account) } | ||||
|   | ||||
| @@ -309,6 +309,18 @@ RSpec.describe 'Inboxes API', type: :request do | ||||
|         expect(response.body).to include('callback_webhook_url') | ||||
|       end | ||||
|  | ||||
|       it 'creates a sms inbox when administrator' do | ||||
|         post "/api/v1/accounts/#{account.id}/inboxes", | ||||
|              headers: admin.create_new_auth_token, | ||||
|              params: { name: 'Sms Inbox', | ||||
|                        channel: { type: 'sms', phone_number: '+123456789', provider_config: { test: 'test' } } }, | ||||
|              as: :json | ||||
|  | ||||
|         expect(response).to have_http_status(:success) | ||||
|         expect(response.body).to include('Sms Inbox') | ||||
|         expect(response.body).to include('+123456789') | ||||
|       end | ||||
|  | ||||
|       it 'creates the webwidget inbox that allow messages after conversation is resolved' do | ||||
|         post "/api/v1/accounts/#{account.id}/inboxes", | ||||
|              headers: admin.create_new_auth_token, | ||||
|   | ||||
							
								
								
									
										12
									
								
								spec/controllers/webhooks/sms_controller_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								spec/controllers/webhooks/sms_controller_spec.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | ||||
| require 'rails_helper' | ||||
|  | ||||
| RSpec.describe 'Webhooks::SmsController', type: :request do | ||||
|   describe 'POST /webhooks/sms/{:phone_number}' do | ||||
|     it 'call the sms events job with the params' do | ||||
|       allow(Webhooks::SmsEventsJob).to receive(:perform_later) | ||||
|       expect(Webhooks::SmsEventsJob).to receive(:perform_later) | ||||
|       post '/webhooks/sms/123221321', params: { content: 'hello' } | ||||
|       expect(response).to have_http_status(:success) | ||||
|     end | ||||
|   end | ||||
| end | ||||
							
								
								
									
										16
									
								
								spec/factories/channel/channel_sms.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								spec/factories/channel/channel_sms.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| FactoryBot.define do | ||||
|   factory :channel_sms, class: 'Channel::Sms' do | ||||
|     sequence(:phone_number) { |n| "+123456789#{n}1" } | ||||
|     account | ||||
|     provider_config do | ||||
|       { 'account_id' => '1', | ||||
|         'application_id' => '1', | ||||
|         'api_key' => '1', | ||||
|         'api_secret' => '1' } | ||||
|     end | ||||
|  | ||||
|     after(:create) do |channel_sms| | ||||
|       create(:inbox, channel: channel_sms, account: channel_sms.account) | ||||
|     end | ||||
|   end | ||||
| end | ||||
| @@ -75,5 +75,14 @@ RSpec.describe SendReplyJob, type: :job do | ||||
|       expect(process_service).to receive(:perform) | ||||
|       described_class.perform_now(message.id) | ||||
|     end | ||||
|  | ||||
|     it 'calls ::Sms::SendOnSmsService when its sms message' do | ||||
|       sms_channel = create(:channel_sms) | ||||
|       message = create(:message, conversation: create(:conversation, inbox: sms_channel.inbox)) | ||||
|       allow(::Sms::SendOnSmsService).to receive(:new).with(message: message).and_return(process_service) | ||||
|       expect(::Sms::SendOnSmsService).to receive(:new).with(message: message) | ||||
|       expect(process_service).to receive(:perform) | ||||
|       described_class.perform_now(message.id) | ||||
|     end | ||||
|   end | ||||
| end | ||||
|   | ||||
							
								
								
									
										56
									
								
								spec/jobs/webhooks/sms_events_job_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								spec/jobs/webhooks/sms_events_job_spec.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,56 @@ | ||||
| require 'rails_helper' | ||||
|  | ||||
| RSpec.describe Webhooks::SmsEventsJob, type: :job do | ||||
|   subject(:job) { described_class.perform_later(params) } | ||||
|  | ||||
|   let!(:sms_channel) { create(:channel_sms) } | ||||
|   let!(:params) do | ||||
|     { | ||||
|       time: '2022-02-02T23:14:05.309Z', | ||||
|       type: 'message-received', | ||||
|       to: sms_channel.phone_number, | ||||
|       description: 'Incoming message received', | ||||
|       message: { | ||||
|         'id': '3232420-2323-234324', | ||||
|         'owner': sms_channel.phone_number, | ||||
|         'applicationId': '2342349-324234d-32432432', | ||||
|         'time': '2022-02-02T23:14:05.262Z', | ||||
|         'segmentCount': 1, | ||||
|         'direction': 'in', | ||||
|         'to': [ | ||||
|           sms_channel.phone_number | ||||
|         ], | ||||
|         'from': '+14234234234', | ||||
|         'text': 'test message' | ||||
|       } | ||||
|     } | ||||
|   end | ||||
|  | ||||
|   it 'enqueues the job' do | ||||
|     expect { job }.to have_enqueued_job(described_class) | ||||
|       .with(params) | ||||
|       .on_queue('default') | ||||
|   end | ||||
|  | ||||
|   context 'when invalid params' do | ||||
|     it 'returns nil when no bot_token' do | ||||
|       expect(described_class.perform_now({})).to be_nil | ||||
|     end | ||||
|  | ||||
|     it 'returns nil when invalid type' do | ||||
|       expect(described_class.perform_now({ type: 'invalid' })).to be_nil | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   context 'when valid params' do | ||||
|     it 'calls Sms::IncomingMessageService' do | ||||
|       process_service = double | ||||
|       allow(Sms::IncomingMessageService).to receive(:new).and_return(process_service) | ||||
|       allow(process_service).to receive(:perform) | ||||
|       expect(Sms::IncomingMessageService).to receive(:new).with(inbox: sms_channel.inbox, | ||||
|                                                                 params: params[:message].with_indifferent_access) | ||||
|       expect(process_service).to receive(:perform) | ||||
|       described_class.perform_now(params) | ||||
|     end | ||||
|   end | ||||
| end | ||||
| @@ -78,6 +78,27 @@ RSpec.describe Campaign, type: :model do | ||||
|       end | ||||
|     end | ||||
|  | ||||
|     context 'when SMS campaign' do | ||||
|       let!(:sms_channel) { create(:channel_sms) } | ||||
|       let!(:sms_inbox) { create(:inbox, channel: sms_channel) } | ||||
|       let(:campaign) { build(:campaign, inbox: sms_inbox) } | ||||
|  | ||||
|       it 'only saves campaign type as oneoff and wont leave scheduled_at empty' do | ||||
|         campaign.campaign_type = 'ongoing' | ||||
|         campaign.save! | ||||
|         expect(campaign.reload.campaign_type).to eq 'one_off' | ||||
|         expect(campaign.scheduled_at.present?).to eq true | ||||
|       end | ||||
|  | ||||
|       it 'calls sms service on trigger!' do | ||||
|         sms_service = double | ||||
|         expect(Sms::OneoffSmsCampaignService).to receive(:new).with(campaign: campaign).and_return(sms_service) | ||||
|         expect(sms_service).to receive(:perform) | ||||
|         campaign.save! | ||||
|         campaign.trigger! | ||||
|       end | ||||
|     end | ||||
|  | ||||
|     context 'when Website campaign' do | ||||
|       let(:campaign) { build(:campaign) } | ||||
|  | ||||
|   | ||||
| @@ -15,8 +15,8 @@ describe Contacts::ContactableInboxesService do | ||||
|   let!(:email_inbox) { create(:inbox, channel: email_channel, account: account) } | ||||
|   let!(:api_channel) { create(:channel_api, account: account) } | ||||
|   let!(:api_inbox) { create(:inbox, channel: api_channel, account: account) } | ||||
|   let!(:website_channel) { create(:channel_widget, account: account) } | ||||
|   let!(:website_inbox) { create(:inbox, channel: website_channel, account: account) } | ||||
|   let!(:website_inbox) { create(:inbox, channel: create(:channel_widget, account: account), account: account) } | ||||
|   let!(:sms_inbox) { create(:inbox, channel: create(:channel_sms, account: account), account: account) } | ||||
|  | ||||
|   describe '#get' do | ||||
|     it 'returns the contactable inboxes for the contact' do | ||||
| @@ -25,7 +25,7 @@ describe Contacts::ContactableInboxesService do | ||||
|       expect(contactable_inboxes).to include({ source_id: contact.phone_number, inbox: twilio_sms_inbox }) | ||||
|       expect(contactable_inboxes).to include({ source_id: "whatsapp:#{contact.phone_number}", inbox: twilio_whatsapp_inbox }) | ||||
|       expect(contactable_inboxes).to include({ source_id: contact.email, inbox: email_inbox }) | ||||
|       expect(contactable_inboxes.pluck(:inbox)).to include(api_inbox) | ||||
|       expect(contactable_inboxes).to include({ source_id: contact.phone_number, inbox: sms_inbox }) | ||||
|     end | ||||
|  | ||||
|     it 'doest not return the non contactable inboxes for the contact' do | ||||
|   | ||||
							
								
								
									
										31
									
								
								spec/services/sms/incoming_message_service_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								spec/services/sms/incoming_message_service_spec.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | ||||
| require 'rails_helper' | ||||
|  | ||||
| describe Sms::IncomingMessageService do | ||||
|   describe '#perform' do | ||||
|     let!(:sms_channel) { create(:channel_sms) } | ||||
|  | ||||
|     context 'when valid text message params' do | ||||
|       it 'creates appropriate conversations, message and contacts' do | ||||
|         params =  { | ||||
|  | ||||
|           'id': '3232420-2323-234324', | ||||
|           'owner': sms_channel.phone_number, | ||||
|           'applicationId': '2342349-324234d-32432432', | ||||
|           'time': '2022-02-02T23:14:05.262Z', | ||||
|           'segmentCount': 1, | ||||
|           'direction': 'in', | ||||
|           'to': [ | ||||
|             sms_channel.phone_number | ||||
|           ], | ||||
|           'from': '+14234234234', | ||||
|           'text': 'test message' | ||||
|  | ||||
|         }.with_indifferent_access | ||||
|         described_class.new(inbox: sms_channel.inbox, params: params).perform | ||||
|         expect(sms_channel.inbox.conversations.count).not_to eq(0) | ||||
|         expect(Contact.all.first.name).to eq('+1 423-423-4234') | ||||
|         expect(sms_channel.inbox.messages.first.content).to eq('test message') | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
							
								
								
									
										47
									
								
								spec/services/sms/oneoff_sms_campaign_service_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								spec/services/sms/oneoff_sms_campaign_service_spec.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,47 @@ | ||||
| require 'rails_helper' | ||||
|  | ||||
| describe Sms::OneoffSmsCampaignService do | ||||
|   subject(:sms_campaign_service) { described_class.new(campaign: campaign) } | ||||
|  | ||||
|   let(:account) { create(:account) } | ||||
|   let!(:sms_channel) { create(:channel_sms) } | ||||
|   let!(:sms_inbox) { create(:inbox, channel: sms_channel) } | ||||
|   let(:label1) { create(:label, account: account) } | ||||
|   let(:label2) { create(:label, account: account) } | ||||
|   let!(:campaign) do | ||||
|     create(:campaign, inbox: sms_inbox, account: account, | ||||
|                       audience: [{ type: 'Label', id: label1.id }, { type: 'Label', id: label2.id }]) | ||||
|   end | ||||
|  | ||||
|   describe 'perform' do | ||||
|     before do | ||||
|       stub_request(:post, 'https://messaging.bandwidth.com/api/v2/users/1/messages').to_return( | ||||
|         status: 200, | ||||
|         body: { 'id' => '1' }.to_json, | ||||
|         headers: {} | ||||
|       ) | ||||
|     end | ||||
|  | ||||
|     it 'raises error if the campaign is completed' do | ||||
|       campaign.completed! | ||||
|  | ||||
|       expect { sms_campaign_service.perform }.to raise_error 'Completed Campaign' | ||||
|     end | ||||
|  | ||||
|     it 'raises error invalid campaign when its not a oneoff sms campaign' do | ||||
|       campaign = create(:campaign) | ||||
|  | ||||
|       expect { described_class.new(campaign: campaign).perform }.to raise_error "Invalid campaign #{campaign.id}" | ||||
|     end | ||||
|  | ||||
|     it 'send messages to contacts in the audience and marks the campaign completed' do | ||||
|       contact_with_label1, contact_with_label2, contact_with_both_labels = FactoryBot.create_list(:contact, 3, :with_phone_number, account: account) | ||||
|       contact_with_label1.update_labels([label1.title]) | ||||
|       contact_with_label2.update_labels([label2.title]) | ||||
|       contact_with_both_labels.update_labels([label1.title, label2.title]) | ||||
|       sms_campaign_service.perform | ||||
|       assert_requested(:post, 'https://messaging.bandwidth.com/api/v2/users/1/messages', times: 3) | ||||
|       expect(campaign.reload.completed?).to eq true | ||||
|     end | ||||
|   end | ||||
| end | ||||
							
								
								
									
										28
									
								
								spec/services/sms/send_on_sms_service_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								spec/services/sms/send_on_sms_service_spec.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | ||||
| require 'rails_helper' | ||||
|  | ||||
| describe Sms::SendOnSmsService do | ||||
|   describe '#perform' do | ||||
|     context 'when a valid message' do | ||||
|       let(:sms_request) { double } | ||||
|       let!(:sms_channel) { create(:channel_sms) } | ||||
|       let!(:contact_inbox) { create(:contact_inbox, inbox: sms_channel.inbox, source_id: '+123456789') } | ||||
|       let!(:conversation) { create(:conversation, contact_inbox: contact_inbox, inbox: sms_channel.inbox) } | ||||
|  | ||||
|       it 'calls channel.send_message' do | ||||
|         message = create(:message, message_type: :outgoing, content: 'test', | ||||
|                                    conversation: conversation) | ||||
|         allow(HTTParty).to receive(:post).and_return(sms_request) | ||||
|         allow(sms_request).to receive(:success?).and_return(true) | ||||
|         allow(sms_request).to receive(:parsed_response).and_return({ 'id' => '123456789' }) | ||||
|         expect(HTTParty).to receive(:post).with( | ||||
|           'https://messaging.bandwidth.com/api/v2/users/1/messages', | ||||
|           basic_auth: { username: '1', password: '1' }, | ||||
|           headers: { 'Content-Type' => 'application/json' }, | ||||
|           body: { 'to' => '+123456789', 'from' => sms_channel.phone_number, 'text' => 'test', 'applicationId' => '1' }.to_json | ||||
|         ) | ||||
|         described_class.new(message: message).perform | ||||
|         expect(message.reload.source_id).to eq('123456789') | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
		Reference in New Issue
	
	Block a user
	 Sojan Jose
					Sojan Jose