loading
Generated 2026-02-24T08:09:27+00:00

All Files ( 90.81% covered at 17.75 hits/line )

191 files in total.
3198 relevant lines, 2904 lines covered and 294 lines missed. ( 90.81% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line
app/builders/contact_builder.rb 100.00 % 21 13 13 0 5.38
app/builders/deal_builder.rb 100.00 % 36 21 21 0 4.67
app/builders/deal_product_builder.rb 100.00 % 43 25 25 0 4.60
app/builders/event_builder.rb 97.44 % 62 39 38 1 10.90
app/builders/evolution_api_builder.rb 100.00 % 14 10 10 0 1.70
app/builders/product_builder.rb 100.00 % 16 10 10 0 4.00
app/builders/reports/base_timeseries_builder.rb 100.00 % 36 20 20 0 16.00
app/builders/reports/deals/base_report_builder.rb 100.00 % 42 17 17 0 9.59
app/builders/reports/deals/metric_builder.rb 100.00 % 32 14 14 0 3.50
app/builders/reports/deals/report_builder.rb 100.00 % 21 11 11 0 2.36
app/builders/reports/deals/timeseries/base_report_builder.rb 100.00 % 57 26 26 0 11.15
app/builders/reports/deals/timeseries/count_report_builder.rb 100.00 % 18 6 6 0 2.50
app/builders/reports/deals/timeseries/sum_report_builder.rb 100.00 % 18 6 6 0 2.17
app/builders/reports/pipeline/stages_metric_builder.rb 100.00 % 36 20 20 0 4.50
app/channels/application_cable/channel.rb 100.00 % 4 2 2 0 1.00
app/channels/application_cable/connection.rb 100.00 % 4 2 2 0 1.00
app/controllers/accounts/advanced_searches_controller.rb 100.00 % 14 7 7 0 1.29
app/controllers/accounts/apps/ai_assistents_controller.rb 100.00 % 26 12 12 0 1.33
app/controllers/accounts/apps/chatwoots_controller.rb 100.00 % 47 25 25 0 1.32
app/controllers/accounts/apps/evolution_apis_controller.rb 100.00 % 67 35 35 0 1.54
app/controllers/accounts/apps_controller.rb 36.67 % 65 30 11 19 0.37
app/controllers/accounts/attachments_controller.rb 100.00 % 16 9 9 0 1.00
app/controllers/accounts/contacts/chatwoot_embed_controller.rb 96.67 % 67 30 29 1 2.77
app/controllers/accounts/contacts/events_controller.rb 100.00 % 60 29 29 0 5.76
app/controllers/accounts/contacts_controller.rb 87.50 % 135 64 56 8 2.14
app/controllers/accounts/deal_assignees_controller.rb 100.00 % 46 25 25 0 1.48
app/controllers/accounts/deal_products_controller.rb 100.00 % 49 27 27 0 1.15
app/controllers/accounts/deals_controller.rb 80.58 % 200 103 83 20 1.75
app/controllers/accounts/events_controller.rb 100.00 % 38 14 14 0 1.71
app/controllers/accounts/pipelines_controller.rb 36.51 % 235 126 46 80 0.40
app/controllers/accounts/products_controller.rb 100.00 % 87 43 43 0 2.70
app/controllers/accounts/reports_controller.rb 94.44 % 104 36 34 2 2.53
app/controllers/accounts/settings/accounts_controller.rb 100.00 % 25 13 13 0 1.15
app/controllers/accounts/settings/custom_attributes_definitions_controller.rb 100.00 % 51 23 23 0 1.74
app/controllers/accounts/settings/deals/deal_lost_reasons_controller.rb 91.43 % 70 35 32 3 1.20
app/controllers/accounts/settings/deals_controller.rb 100.00 % 4 2 2 0 1.00
app/controllers/accounts/settings/webhooks_controller.rb 96.15 % 51 26 25 1 1.31
app/controllers/accounts/settings_controller.rb 100.00 % 4 2 2 0 1.00
app/controllers/accounts/stages_controller.rb 100.00 % 22 12 12 0 3.33
app/controllers/accounts/stores_controller.rb 100.00 % 7 5 5 0 1.60
app/controllers/accounts/users_controller.rb 100.00 % 74 35 35 0 3.34
app/controllers/accounts/webpush_subscriptions_controller.rb 100.00 % 15 6 6 0 1.83
app/controllers/accounts/welcome_controller.rb 100.00 % 5 2 2 0 1.00
app/controllers/api/concerns/request_exception_handler.rb 70.37 % 54 27 19 8 6.63
app/controllers/api/v1/accounts/accounts_controller.rb 93.33 % 31 15 14 1 1.20
app/controllers/api/v1/accounts/contacts_controller.rb 97.06 % 68 34 33 1 1.88
app/controllers/api/v1/accounts/deal_assignees_controller.rb 93.75 % 31 16 15 1 1.44
app/controllers/api/v1/accounts/deal_products_controller.rb 94.74 % 36 19 18 1 1.42
app/controllers/api/v1/accounts/deals/events_controller.rb 100.00 % 19 11 11 0 1.64
app/controllers/api/v1/accounts/deals_controller.rb 95.83 % 48 24 23 1 2.25
app/controllers/api/v1/accounts/products_controller.rb 95.65 % 49 23 22 1 1.83
app/controllers/api/v1/accounts/users_controller.rb 100.00 % 34 15 15 0 1.67
app/controllers/api/v1/contacts_controller.rb 37.50 % 16 8 3 5 0.38
app/controllers/api/v1/internal_controller.rb 100.00 % 19 12 12 0 38.25
app/controllers/api/v1/public_controller.rb 100.00 % 2 1 1 0 1.00
app/controllers/application_controller.rb 60.00 % 24 15 9 6 0.60
app/controllers/apps/chatwoots_controller.rb 67.74 % 51 31 21 10 0.97
app/controllers/apps/evolution_apis_controller.rb 100.00 % 6 4 4 0 9.50
app/controllers/concerns/account_concern.rb 100.00 % 5 3 3 0 2.67
app/controllers/concerns/deal_concern.rb 100.00 % 17 3 3 0 14.33
app/controllers/concerns/deal_product_concern.rb 100.00 % 5 3 3 0 5.33
app/controllers/concerns/localized.rb 100.00 % 31 17 17 0 161.18
app/controllers/concerns/product_concern.rb 100.00 % 8 3 3 0 3.67
app/controllers/concerns/user_concern.rb 100.00 % 6 3 3 0 9.33
app/controllers/embedded/accounts/apps/chatwoots_controller.rb 66.67 % 6 3 2 1 0.67
app/controllers/embedded/internal_controller.rb 33.33 % 17 9 3 6 0.33
app/controllers/installation_controller.rb 100.00 % 90 50 50 0 3.60
app/controllers/internal_controller.rb 100.00 % 9 6 6 0 65.83
app/controllers/pwa_controller.rb 100.00 % 9 4 4 0 1.00
app/controllers/settings_controller.rb 100.00 % 5 2 2 0 1.00
app/controllers/users/registrations_controller.rb 100.00 % 13 5 5 0 1.80
app/helpers/advanced_search_helper.rb 100.00 % 14 3 3 0 1.33
app/helpers/application_helper.rb 91.67 % 19 12 11 1 49.58
app/helpers/date_range_helper.rb 100.00 % 19 8 8 0 18.38
app/helpers/deals_helper.rb 100.00 % 2 1 1 0 1.00
app/helpers/timezone_helper.rb 100.00 % 19 7 7 0 58.29
app/jobs/application_job.rb 100.00 % 7 1 1 0 1.00
app/jobs/apps/chatwoot/connection/refresh_job.rb 60.00 % 11 5 3 2 0.60
app/jobs/webhook/status/refresh_job.rb 50.00 % 10 6 3 3 0.50
app/listeners/webhook_listener.rb 92.50 % 80 40 37 3 55.20
app/listeners/woofbot_listener.rb 100.00 % 7 3 3 0 250.33
app/mailers/application_mailer.rb 100.00 % 4 3 3 0 1.00
app/models/account.rb 90.91 % 129 44 40 4 93.27
app/models/app.rb 100.00 % 15 2 2 0 1.00
app/models/application_record.rb 100.00 % 14 7 7 0 90.57
app/models/apps.rb 100.00 % 5 3 3 0 1.67
app/models/apps/ai_assistent.rb 100.00 % 31 9 9 0 3.56
app/models/apps/chatwoot.rb 86.67 % 162 60 52 8 6.70
app/models/apps/chatwoot/api_client.rb 100.00 % 49 23 23 0 4.43
app/models/apps/chatwoot/api_client/user_profile.rb 100.00 % 11 5 5 0 2.60
app/models/apps/chatwoot/connection/refresh.rb 100.00 % 19 10 10 0 1.70
app/models/apps/chatwoot/migrations/remove_trailing_slashes_from_chatwoot_endpoint_url_job.rb 33.33 % 14 9 3 6 0.33
app/models/apps/evolution_api.rb 100.00 % 50 17 17 0 3.18
app/models/attachment.rb 100.00 % 74 30 30 0 7.23
app/models/concerns/account/settings.rb 100.00 % 22 12 12 0 1.83
app/models/concerns/applicable.rb 100.00 % 15 9 9 0 343.11
app/models/concerns/chatwoot_labels.rb 63.64 % 18 11 7 4 0.64
app/models/concerns/contact/presenters.rb 75.00 % 7 4 3 1 0.75
app/models/concerns/custom_attribute_definition/broadcastable.rb 100.00 % 16 9 9 0 3.67
app/models/concerns/custom_attributes.rb 60.00 % 12 5 3 2 0.60
app/models/concerns/deal/broadcastable.rb 100.00 % 38 18 18 0 91.39
app/models/concerns/deal/event_creator.rb 100.00 % 73 26 26 0 83.77
app/models/concerns/deal/handle_in_cents_values.rb 71.43 % 13 7 5 2 0.71
app/models/concerns/deal_product/broadcastable.rb 100.00 % 13 7 7 0 6.86
app/models/concerns/deal_product/event_creator.rb 100.00 % 60 22 22 0 9.00
app/models/concerns/deal_product/handle_in_cents_values.rb 100.00 % 9 6 6 0 7.33
app/models/concerns/evolution_api/broadcastable.rb 100.00 % 26 12 12 0 15.92
app/models/concerns/labelable.rb 63.64 % 18 11 7 4 0.64
app/models/concerns/product/broadcastable.rb 92.31 % 25 13 12 1 12.00
app/models/concerns/stage/decorators.rb 100.00 % 7 4 4 0 2.50
app/models/contact.rb 96.97 % 93 33 32 1 105.67
app/models/contact/integrations/chatwoot/generate_conversation_link.rb 100.00 % 33 19 19 0 2.37
app/models/contact/merge.rb 100.00 % 55 34 34 0 5.79
app/models/contact/migrations/merge_duplicate_contacts_job.rb 32.00 % 51 25 8 17 0.32
app/models/current.rb 100.00 % 5 3 3 0 1788.00
app/models/custom_attribute_definition.rb 87.50 % 26 8 7 1 0.88
app/models/deal.rb 89.36 % 113 47 42 5 52.11
app/models/deal/create_or_update.rb 100.00 % 38 25 25 0 17.64
app/models/deal/migrations/populate_deal_lost_at_and_won_at_job.rb 23.08 % 27 13 3 10 0.23
app/models/deal/recalculate_and_save_all_monetary_values.rb 100.00 % 18 10 10 0 5.00
app/models/deal_assignee.rb 100.00 % 27 4 4 0 1.00
app/models/deal_lost_reason.rb 100.00 % 12 2 2 0 1.00
app/models/deal_product.rb 100.00 % 35 8 8 0 1.00
app/models/deal_product/create_or_update.rb 100.00 % 41 23 23 0 5.35
app/models/deal_product/destroy.rb 100.00 % 13 8 8 0 1.00
app/models/embedding_documment.rb 100.00 % 22 3 3 0 1.00
app/models/event.rb 94.31 % 267 123 116 7 97.79
app/models/installation.rb 91.67 % 41 12 11 1 56.42
app/models/installation/complete.rb 100.00 % 36 15 15 0 2.73
app/models/pipeline.rb 100.00 % 15 5 5 0 1.00
app/models/product.rb 94.74 % 49 19 18 1 19.37
app/models/query/advanced_search.rb 94.34 % 112 53 50 3 6.91
app/models/query/filter.rb 100.00 % 18 10 10 0 7.00
app/models/stage.rb 100.00 % 41 13 13 0 6.54
app/models/user.rb 100.00 % 93 18 18 0 11.44
app/models/webhook.rb 100.00 % 51 21 21 0 3.38
app/models/webhook/api_client.rb 100.00 % 32 19 19 0 4.53
app/models/webpush_subscription.rb 100.00 % 41 8 8 0 1.13
app/use_cases/accounts/apps/chatwoots/create.rb 100.00 % 11 7 7 0 1.29
app/use_cases/accounts/apps/chatwoots/create_conversation.rb 100.00 % 17 6 6 0 2.50
app/use_cases/accounts/apps/chatwoots/delete.rb 83.33 % 10 6 5 1 0.83
app/use_cases/accounts/apps/chatwoots/export_contact.rb 100.00 % 85 38 38 0 3.26
app/use_cases/accounts/apps/chatwoots/export_contact_worker.rb 100.00 % 10 7 7 0 1.00
app/use_cases/accounts/apps/chatwoots/find_or_create_conversation.rb 100.00 % 13 5 5 0 4.60
app/use_cases/accounts/apps/chatwoots/get_conversation_and_send_message.rb 100.00 % 9 4 4 0 4.00
app/use_cases/accounts/apps/chatwoots/get_conversations.rb 100.00 % 21 9 9 0 8.67
app/use_cases/accounts/apps/chatwoots/get_inboxes.rb 100.00 % 12 5 5 0 4.00
app/use_cases/accounts/apps/chatwoots/messages/delivery_job.rb 92.31 % 23 13 12 1 4.38
app/use_cases/accounts/apps/chatwoots/remove_chatwoot_id_from_contacts.rb 100.00 % 10 6 6 0 1.00
app/use_cases/accounts/apps/chatwoots/remove_chatwoot_id_from_contacts_worker.rb 100.00 % 9 6 6 0 1.00
app/use_cases/accounts/apps/chatwoots/search_contact.rb 100.00 % 19 8 8 0 2.50
app/use_cases/accounts/apps/chatwoots/send_message.rb 96.15 % 49 26 25 1 3.38
app/use_cases/accounts/apps/chatwoots/sync_chatwoot_worker.rb 50.00 % 12 8 4 4 0.50
app/use_cases/accounts/apps/chatwoots/sync_export_contacts.rb 100.00 % 15 9 9 0 1.00
app/use_cases/accounts/apps/chatwoots/sync_import_contacts.rb 93.48 % 74 46 43 3 7.26
app/use_cases/accounts/apps/chatwoots/sync_inboxes.rb 40.00 % 13 5 2 3 0.40
app/use_cases/accounts/apps/chatwoots/webhooks/events/contact.rb 100.00 % 5 3 3 0 1.67
app/use_cases/accounts/apps/chatwoots/webhooks/events/conversation_updated.rb 100.00 % 6 4 4 0 1.00
app/use_cases/accounts/apps/chatwoots/webhooks/events/message.rb 100.00 % 8 5 5 0 15.40
app/use_cases/accounts/apps/chatwoots/webhooks/import_contact.rb 93.88 % 88 49 46 3 21.67
app/use_cases/accounts/apps/chatwoots/webhooks/import_message.rb 100.00 % 97 46 46 0 12.46
app/use_cases/accounts/apps/chatwoots/webhooks/process_webhook.rb 91.67 % 24 12 11 1 14.92
app/use_cases/accounts/apps/chatwoots/webhooks/process_webhook_job.rb 100.00 % 19 8 8 0 13.13
app/use_cases/accounts/apps/evolution_apis/create.rb 100.00 % 11 7 7 0 1.29
app/use_cases/accounts/apps/evolution_apis/instance/create.rb 91.67 % 63 12 11 1 2.08
app/use_cases/accounts/apps/evolution_apis/instance/delete_disconnected.rb 100.00 % 38 18 18 0 3.39
app/use_cases/accounts/apps/evolution_apis/instance/delete_disconnected_worker.rb 100.00 % 8 5 5 0 1.00
app/use_cases/accounts/apps/evolution_apis/instance/sessions_refresh_status_job.rb 100.00 % 11 5 5 0 1.20
app/use_cases/accounts/apps/evolution_apis/message/delivery_job.rb 100.00 % 17 12 12 0 5.75
app/use_cases/accounts/apps/evolution_apis/message/import.rb 97.87 % 103 47 46 1 3.74
app/use_cases/accounts/apps/evolution_apis/message/send.rb 100.00 % 94 31 31 0 3.84
app/use_cases/accounts/apps/evolution_apis/webhooks/events/connection_created.rb 100.00 % 14 10 10 0 1.00
app/use_cases/accounts/apps/evolution_apis/webhooks/events/connection_deleted.rb 100.00 % 8 5 5 0 1.80
app/use_cases/accounts/apps/evolution_apis/webhooks/events/import_message.rb 100.00 % 8 5 5 0 6.20
app/use_cases/accounts/apps/evolution_apis/webhooks/events/qrcode_connect_refresh.rb 100.00 % 8 5 5 0 1.40
app/use_cases/accounts/apps/evolution_apis/webhooks/process_webhook.rb 100.00 % 35 20 20 0 6.50
app/use_cases/accounts/apps/evolution_apis/webhooks/process_webhook_worker.rb 100.00 % 10 6 6 0 6.67
app/use_cases/accounts/contacts/events/enqueue.rb 100.00 % 10 6 6 0 1.17
app/use_cases/accounts/contacts/events/enqueue_worker.rb 100.00 % 8 5 5 0 1.40
app/use_cases/accounts/contacts/events/generate_ai_response.rb 97.37 % 114 38 37 1 2.50
app/use_cases/accounts/contacts/events/send_now.rb 100.00 % 15 10 10 0 4.90
app/use_cases/accounts/contacts/events/woofbot.rb 100.00 % 48 21 21 0 1.00
app/use_cases/accounts/contacts/events/woofbot_worker.rb 60.00 % 10 5 3 2 0.60
app/use_cases/accounts/contacts/get_by_params.rb 100.00 % 59 33 33 0 32.45
app/use_cases/accounts/create/embed_company_site_job.rb 60.00 % 8 5 3 2 0.60
app/use_cases/accounts/create/embeded_company_site.rb 94.59 % 70 37 35 2 2.95
app/use_cases/accounts/create/page_crawler.rb 89.29 % 51 28 25 3 5.54
app/use_cases/open_ai/embeddings.rb 100.00 % 24 10 10 0 4.00
app/use_cases/pwa/send_notifications_worker.rb 100.00 % 22 8 8 0 25.38
app/use_cases/users/json_web_token.rb 100.00 % 18 10 10 0 37.90
app/workers/webhook_worker.rb 75.00 % 11 4 3 1 0.75

Controllers ( 84.59% covered at 5.07 hits/line )

55 files in total.
1142 relevant lines, 966 lines covered and 176 lines missed. ( 84.59% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line
app/controllers/accounts/advanced_searches_controller.rb 100.00 % 14 7 7 0 1.29
app/controllers/accounts/apps/ai_assistents_controller.rb 100.00 % 26 12 12 0 1.33
app/controllers/accounts/apps/chatwoots_controller.rb 100.00 % 47 25 25 0 1.32
app/controllers/accounts/apps/evolution_apis_controller.rb 100.00 % 67 35 35 0 1.54
app/controllers/accounts/apps_controller.rb 36.67 % 65 30 11 19 0.37
app/controllers/accounts/attachments_controller.rb 100.00 % 16 9 9 0 1.00
app/controllers/accounts/contacts/chatwoot_embed_controller.rb 96.67 % 67 30 29 1 2.77
app/controllers/accounts/contacts/events_controller.rb 100.00 % 60 29 29 0 5.76
app/controllers/accounts/contacts_controller.rb 87.50 % 135 64 56 8 2.14
app/controllers/accounts/deal_assignees_controller.rb 100.00 % 46 25 25 0 1.48
app/controllers/accounts/deal_products_controller.rb 100.00 % 49 27 27 0 1.15
app/controllers/accounts/deals_controller.rb 80.58 % 200 103 83 20 1.75
app/controllers/accounts/events_controller.rb 100.00 % 38 14 14 0 1.71
app/controllers/accounts/pipelines_controller.rb 36.51 % 235 126 46 80 0.40
app/controllers/accounts/products_controller.rb 100.00 % 87 43 43 0 2.70
app/controllers/accounts/reports_controller.rb 94.44 % 104 36 34 2 2.53
app/controllers/accounts/settings/accounts_controller.rb 100.00 % 25 13 13 0 1.15
app/controllers/accounts/settings/custom_attributes_definitions_controller.rb 100.00 % 51 23 23 0 1.74
app/controllers/accounts/settings/deals/deal_lost_reasons_controller.rb 91.43 % 70 35 32 3 1.20
app/controllers/accounts/settings/deals_controller.rb 100.00 % 4 2 2 0 1.00
app/controllers/accounts/settings/webhooks_controller.rb 96.15 % 51 26 25 1 1.31
app/controllers/accounts/settings_controller.rb 100.00 % 4 2 2 0 1.00
app/controllers/accounts/stages_controller.rb 100.00 % 22 12 12 0 3.33
app/controllers/accounts/stores_controller.rb 100.00 % 7 5 5 0 1.60
app/controllers/accounts/users_controller.rb 100.00 % 74 35 35 0 3.34
app/controllers/accounts/webpush_subscriptions_controller.rb 100.00 % 15 6 6 0 1.83
app/controllers/accounts/welcome_controller.rb 100.00 % 5 2 2 0 1.00
app/controllers/api/concerns/request_exception_handler.rb 70.37 % 54 27 19 8 6.63
app/controllers/api/v1/accounts/accounts_controller.rb 93.33 % 31 15 14 1 1.20
app/controllers/api/v1/accounts/contacts_controller.rb 97.06 % 68 34 33 1 1.88
app/controllers/api/v1/accounts/deal_assignees_controller.rb 93.75 % 31 16 15 1 1.44
app/controllers/api/v1/accounts/deal_products_controller.rb 94.74 % 36 19 18 1 1.42
app/controllers/api/v1/accounts/deals/events_controller.rb 100.00 % 19 11 11 0 1.64
app/controllers/api/v1/accounts/deals_controller.rb 95.83 % 48 24 23 1 2.25
app/controllers/api/v1/accounts/products_controller.rb 95.65 % 49 23 22 1 1.83
app/controllers/api/v1/accounts/users_controller.rb 100.00 % 34 15 15 0 1.67
app/controllers/api/v1/contacts_controller.rb 37.50 % 16 8 3 5 0.38
app/controllers/api/v1/internal_controller.rb 100.00 % 19 12 12 0 38.25
app/controllers/api/v1/public_controller.rb 100.00 % 2 1 1 0 1.00
app/controllers/application_controller.rb 60.00 % 24 15 9 6 0.60
app/controllers/apps/chatwoots_controller.rb 67.74 % 51 31 21 10 0.97
app/controllers/apps/evolution_apis_controller.rb 100.00 % 6 4 4 0 9.50
app/controllers/concerns/account_concern.rb 100.00 % 5 3 3 0 2.67
app/controllers/concerns/deal_concern.rb 100.00 % 17 3 3 0 14.33
app/controllers/concerns/deal_product_concern.rb 100.00 % 5 3 3 0 5.33
app/controllers/concerns/localized.rb 100.00 % 31 17 17 0 161.18
app/controllers/concerns/product_concern.rb 100.00 % 8 3 3 0 3.67
app/controllers/concerns/user_concern.rb 100.00 % 6 3 3 0 9.33
app/controllers/embedded/accounts/apps/chatwoots_controller.rb 66.67 % 6 3 2 1 0.67
app/controllers/embedded/internal_controller.rb 33.33 % 17 9 3 6 0.33
app/controllers/installation_controller.rb 100.00 % 90 50 50 0 3.60
app/controllers/internal_controller.rb 100.00 % 9 6 6 0 65.83
app/controllers/pwa_controller.rb 100.00 % 9 4 4 0 1.00
app/controllers/settings_controller.rb 100.00 % 5 2 2 0 1.00
app/controllers/users/registrations_controller.rb 100.00 % 13 5 5 0 1.80

Channels ( 100.0% covered at 1.0 hits/line )

2 files in total.
4 relevant lines, 4 lines covered and 0 lines missed. ( 100.0% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line
app/channels/application_cable/channel.rb 100.00 % 4 2 2 0 1.00
app/channels/application_cable/connection.rb 100.00 % 4 2 2 0 1.00

Models ( 92.01% covered at 40.4 hits/line )

56 files in total.
976 relevant lines, 898 lines covered and 78 lines missed. ( 92.01% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line
app/models/account.rb 90.91 % 129 44 40 4 93.27
app/models/app.rb 100.00 % 15 2 2 0 1.00
app/models/application_record.rb 100.00 % 14 7 7 0 90.57
app/models/apps.rb 100.00 % 5 3 3 0 1.67
app/models/apps/ai_assistent.rb 100.00 % 31 9 9 0 3.56
app/models/apps/chatwoot.rb 86.67 % 162 60 52 8 6.70
app/models/apps/chatwoot/api_client.rb 100.00 % 49 23 23 0 4.43
app/models/apps/chatwoot/api_client/user_profile.rb 100.00 % 11 5 5 0 2.60
app/models/apps/chatwoot/connection/refresh.rb 100.00 % 19 10 10 0 1.70
app/models/apps/chatwoot/migrations/remove_trailing_slashes_from_chatwoot_endpoint_url_job.rb 33.33 % 14 9 3 6 0.33
app/models/apps/evolution_api.rb 100.00 % 50 17 17 0 3.18
app/models/attachment.rb 100.00 % 74 30 30 0 7.23
app/models/concerns/account/settings.rb 100.00 % 22 12 12 0 1.83
app/models/concerns/applicable.rb 100.00 % 15 9 9 0 343.11
app/models/concerns/chatwoot_labels.rb 63.64 % 18 11 7 4 0.64
app/models/concerns/contact/presenters.rb 75.00 % 7 4 3 1 0.75
app/models/concerns/custom_attribute_definition/broadcastable.rb 100.00 % 16 9 9 0 3.67
app/models/concerns/custom_attributes.rb 60.00 % 12 5 3 2 0.60
app/models/concerns/deal/broadcastable.rb 100.00 % 38 18 18 0 91.39
app/models/concerns/deal/event_creator.rb 100.00 % 73 26 26 0 83.77
app/models/concerns/deal/handle_in_cents_values.rb 71.43 % 13 7 5 2 0.71
app/models/concerns/deal_product/broadcastable.rb 100.00 % 13 7 7 0 6.86
app/models/concerns/deal_product/event_creator.rb 100.00 % 60 22 22 0 9.00
app/models/concerns/deal_product/handle_in_cents_values.rb 100.00 % 9 6 6 0 7.33
app/models/concerns/evolution_api/broadcastable.rb 100.00 % 26 12 12 0 15.92
app/models/concerns/labelable.rb 63.64 % 18 11 7 4 0.64
app/models/concerns/product/broadcastable.rb 92.31 % 25 13 12 1 12.00
app/models/concerns/stage/decorators.rb 100.00 % 7 4 4 0 2.50
app/models/contact.rb 96.97 % 93 33 32 1 105.67
app/models/contact/integrations/chatwoot/generate_conversation_link.rb 100.00 % 33 19 19 0 2.37
app/models/contact/merge.rb 100.00 % 55 34 34 0 5.79
app/models/contact/migrations/merge_duplicate_contacts_job.rb 32.00 % 51 25 8 17 0.32
app/models/current.rb 100.00 % 5 3 3 0 1788.00
app/models/custom_attribute_definition.rb 87.50 % 26 8 7 1 0.88
app/models/deal.rb 89.36 % 113 47 42 5 52.11
app/models/deal/create_or_update.rb 100.00 % 38 25 25 0 17.64
app/models/deal/migrations/populate_deal_lost_at_and_won_at_job.rb 23.08 % 27 13 3 10 0.23
app/models/deal/recalculate_and_save_all_monetary_values.rb 100.00 % 18 10 10 0 5.00
app/models/deal_assignee.rb 100.00 % 27 4 4 0 1.00
app/models/deal_lost_reason.rb 100.00 % 12 2 2 0 1.00
app/models/deal_product.rb 100.00 % 35 8 8 0 1.00
app/models/deal_product/create_or_update.rb 100.00 % 41 23 23 0 5.35
app/models/deal_product/destroy.rb 100.00 % 13 8 8 0 1.00
app/models/embedding_documment.rb 100.00 % 22 3 3 0 1.00
app/models/event.rb 94.31 % 267 123 116 7 97.79
app/models/installation.rb 91.67 % 41 12 11 1 56.42
app/models/installation/complete.rb 100.00 % 36 15 15 0 2.73
app/models/pipeline.rb 100.00 % 15 5 5 0 1.00
app/models/product.rb 94.74 % 49 19 18 1 19.37
app/models/query/advanced_search.rb 94.34 % 112 53 50 3 6.91
app/models/query/filter.rb 100.00 % 18 10 10 0 7.00
app/models/stage.rb 100.00 % 41 13 13 0 6.54
app/models/user.rb 100.00 % 93 18 18 0 11.44
app/models/webhook.rb 100.00 % 51 21 21 0 3.38
app/models/webhook/api_client.rb 100.00 % 32 19 19 0 4.53
app/models/webpush_subscription.rb 100.00 % 41 8 8 0 1.13

Mailers ( 100.0% covered at 1.0 hits/line )

1 files in total.
3 relevant lines, 3 lines covered and 0 lines missed. ( 100.0% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line
app/mailers/application_mailer.rb 100.00 % 4 3 3 0 1.00

Helpers ( 96.77% covered at 37.26 hits/line )

5 files in total.
31 relevant lines, 30 lines covered and 1 lines missed. ( 96.77% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line
app/helpers/advanced_search_helper.rb 100.00 % 14 3 3 0 1.33
app/helpers/application_helper.rb 91.67 % 19 12 11 1 49.58
app/helpers/date_range_helper.rb 100.00 % 19 8 8 0 18.38
app/helpers/deals_helper.rb 100.00 % 2 1 1 0 1.00
app/helpers/timezone_helper.rb 100.00 % 19 7 7 0 58.29

Jobs ( 62.5% covered at 0.63 hits/line )

4 files in total.
16 relevant lines, 10 lines covered and 6 lines missed. ( 62.5% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line
app/jobs/application_job.rb 100.00 % 7 1 1 0 1.00
app/jobs/apps/chatwoot/connection/refresh_job.rb 60.00 % 11 5 3 2 0.60
app/jobs/webhook/status/refresh_job.rb 50.00 % 10 6 3 3 0.50
app/workers/webhook_worker.rb 75.00 % 11 4 3 1 0.75

Libraries ( 100.0% covered at 0.0 hits/line )

0 files in total.
0 relevant lines, 0 lines covered and 0 lines missed. ( 100.0% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line

Ungrouped ( 96.78% covered at 10.09 hits/line )

68 files in total.
1026 relevant lines, 993 lines covered and 33 lines missed. ( 96.78% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line
app/builders/contact_builder.rb 100.00 % 21 13 13 0 5.38
app/builders/deal_builder.rb 100.00 % 36 21 21 0 4.67
app/builders/deal_product_builder.rb 100.00 % 43 25 25 0 4.60
app/builders/event_builder.rb 97.44 % 62 39 38 1 10.90
app/builders/evolution_api_builder.rb 100.00 % 14 10 10 0 1.70
app/builders/product_builder.rb 100.00 % 16 10 10 0 4.00
app/builders/reports/base_timeseries_builder.rb 100.00 % 36 20 20 0 16.00
app/builders/reports/deals/base_report_builder.rb 100.00 % 42 17 17 0 9.59
app/builders/reports/deals/metric_builder.rb 100.00 % 32 14 14 0 3.50
app/builders/reports/deals/report_builder.rb 100.00 % 21 11 11 0 2.36
app/builders/reports/deals/timeseries/base_report_builder.rb 100.00 % 57 26 26 0 11.15
app/builders/reports/deals/timeseries/count_report_builder.rb 100.00 % 18 6 6 0 2.50
app/builders/reports/deals/timeseries/sum_report_builder.rb 100.00 % 18 6 6 0 2.17
app/builders/reports/pipeline/stages_metric_builder.rb 100.00 % 36 20 20 0 4.50
app/listeners/webhook_listener.rb 92.50 % 80 40 37 3 55.20
app/listeners/woofbot_listener.rb 100.00 % 7 3 3 0 250.33
app/use_cases/accounts/apps/chatwoots/create.rb 100.00 % 11 7 7 0 1.29
app/use_cases/accounts/apps/chatwoots/create_conversation.rb 100.00 % 17 6 6 0 2.50
app/use_cases/accounts/apps/chatwoots/delete.rb 83.33 % 10 6 5 1 0.83
app/use_cases/accounts/apps/chatwoots/export_contact.rb 100.00 % 85 38 38 0 3.26
app/use_cases/accounts/apps/chatwoots/export_contact_worker.rb 100.00 % 10 7 7 0 1.00
app/use_cases/accounts/apps/chatwoots/find_or_create_conversation.rb 100.00 % 13 5 5 0 4.60
app/use_cases/accounts/apps/chatwoots/get_conversation_and_send_message.rb 100.00 % 9 4 4 0 4.00
app/use_cases/accounts/apps/chatwoots/get_conversations.rb 100.00 % 21 9 9 0 8.67
app/use_cases/accounts/apps/chatwoots/get_inboxes.rb 100.00 % 12 5 5 0 4.00
app/use_cases/accounts/apps/chatwoots/messages/delivery_job.rb 92.31 % 23 13 12 1 4.38
app/use_cases/accounts/apps/chatwoots/remove_chatwoot_id_from_contacts.rb 100.00 % 10 6 6 0 1.00
app/use_cases/accounts/apps/chatwoots/remove_chatwoot_id_from_contacts_worker.rb 100.00 % 9 6 6 0 1.00
app/use_cases/accounts/apps/chatwoots/search_contact.rb 100.00 % 19 8 8 0 2.50
app/use_cases/accounts/apps/chatwoots/send_message.rb 96.15 % 49 26 25 1 3.38
app/use_cases/accounts/apps/chatwoots/sync_chatwoot_worker.rb 50.00 % 12 8 4 4 0.50
app/use_cases/accounts/apps/chatwoots/sync_export_contacts.rb 100.00 % 15 9 9 0 1.00
app/use_cases/accounts/apps/chatwoots/sync_import_contacts.rb 93.48 % 74 46 43 3 7.26
app/use_cases/accounts/apps/chatwoots/sync_inboxes.rb 40.00 % 13 5 2 3 0.40
app/use_cases/accounts/apps/chatwoots/webhooks/events/contact.rb 100.00 % 5 3 3 0 1.67
app/use_cases/accounts/apps/chatwoots/webhooks/events/conversation_updated.rb 100.00 % 6 4 4 0 1.00
app/use_cases/accounts/apps/chatwoots/webhooks/events/message.rb 100.00 % 8 5 5 0 15.40
app/use_cases/accounts/apps/chatwoots/webhooks/import_contact.rb 93.88 % 88 49 46 3 21.67
app/use_cases/accounts/apps/chatwoots/webhooks/import_message.rb 100.00 % 97 46 46 0 12.46
app/use_cases/accounts/apps/chatwoots/webhooks/process_webhook.rb 91.67 % 24 12 11 1 14.92
app/use_cases/accounts/apps/chatwoots/webhooks/process_webhook_job.rb 100.00 % 19 8 8 0 13.13
app/use_cases/accounts/apps/evolution_apis/create.rb 100.00 % 11 7 7 0 1.29
app/use_cases/accounts/apps/evolution_apis/instance/create.rb 91.67 % 63 12 11 1 2.08
app/use_cases/accounts/apps/evolution_apis/instance/delete_disconnected.rb 100.00 % 38 18 18 0 3.39
app/use_cases/accounts/apps/evolution_apis/instance/delete_disconnected_worker.rb 100.00 % 8 5 5 0 1.00
app/use_cases/accounts/apps/evolution_apis/instance/sessions_refresh_status_job.rb 100.00 % 11 5 5 0 1.20
app/use_cases/accounts/apps/evolution_apis/message/delivery_job.rb 100.00 % 17 12 12 0 5.75
app/use_cases/accounts/apps/evolution_apis/message/import.rb 97.87 % 103 47 46 1 3.74
app/use_cases/accounts/apps/evolution_apis/message/send.rb 100.00 % 94 31 31 0 3.84
app/use_cases/accounts/apps/evolution_apis/webhooks/events/connection_created.rb 100.00 % 14 10 10 0 1.00
app/use_cases/accounts/apps/evolution_apis/webhooks/events/connection_deleted.rb 100.00 % 8 5 5 0 1.80
app/use_cases/accounts/apps/evolution_apis/webhooks/events/import_message.rb 100.00 % 8 5 5 0 6.20
app/use_cases/accounts/apps/evolution_apis/webhooks/events/qrcode_connect_refresh.rb 100.00 % 8 5 5 0 1.40
app/use_cases/accounts/apps/evolution_apis/webhooks/process_webhook.rb 100.00 % 35 20 20 0 6.50
app/use_cases/accounts/apps/evolution_apis/webhooks/process_webhook_worker.rb 100.00 % 10 6 6 0 6.67
app/use_cases/accounts/contacts/events/enqueue.rb 100.00 % 10 6 6 0 1.17
app/use_cases/accounts/contacts/events/enqueue_worker.rb 100.00 % 8 5 5 0 1.40
app/use_cases/accounts/contacts/events/generate_ai_response.rb 97.37 % 114 38 37 1 2.50
app/use_cases/accounts/contacts/events/send_now.rb 100.00 % 15 10 10 0 4.90
app/use_cases/accounts/contacts/events/woofbot.rb 100.00 % 48 21 21 0 1.00
app/use_cases/accounts/contacts/events/woofbot_worker.rb 60.00 % 10 5 3 2 0.60
app/use_cases/accounts/contacts/get_by_params.rb 100.00 % 59 33 33 0 32.45
app/use_cases/accounts/create/embed_company_site_job.rb 60.00 % 8 5 3 2 0.60
app/use_cases/accounts/create/embeded_company_site.rb 94.59 % 70 37 35 2 2.95
app/use_cases/accounts/create/page_crawler.rb 89.29 % 51 28 25 3 5.54
app/use_cases/open_ai/embeddings.rb 100.00 % 24 10 10 0 4.00
app/use_cases/pwa/send_notifications_worker.rb 100.00 % 22 8 8 0 25.38
app/use_cases/users/json_web_token.rb 100.00 % 18 10 10 0 37.90

app/builders/contact_builder.rb

100.0% lines covered

13 relevant lines. 13 lines covered and 0 lines missed.
    
  1. 1 class ContactBuilder
  2. 1 def initialize(user, params, search_if_exists = false)
  3. 7 @params = params
  4. 7 @user = user
  5. 7 @search_if_exists = search_if_exists
  6. end
  7. 1 def perform
  8. 7 if @search_if_exists
  9. 5 @contact = Accounts::Contacts::GetByParams.call(Current.account, contact_params.slice(:phone, :email).to_h)[:ok]
  10. end
  11. 7 @contact ||= Contact.new
  12. 7 @contact.assign_attributes(contact_params)
  13. 7 @contact
  14. end
  15. 1 def contact_params
  16. 12 @params.permit(:full_name, :phone, :email, :label_list,
  17. custom_attributes: {}, additional_attributes: {})
  18. end
  19. end

app/builders/deal_builder.rb

100.0% lines covered

21 relevant lines. 21 lines covered and 0 lines missed.
    
  1. 1 class DealBuilder
  2. 1 include DealConcern
  3. 1 def initialize(user, params)
  4. 10 @params = params
  5. 10 @user = user
  6. end
  7. 1 def build
  8. 6 @deal = Deal.new(deal_params.merge(created_by_id: user.id))
  9. 6 attach_contact_if_needed
  10. 6 assign_user_to_deal
  11. 6 deal
  12. end
  13. 1 def perform = build
  14. 1 private
  15. 1 attr_reader :user, :params, :deal
  16. 1 def attach_contact_if_needed
  17. 9 return if deal_params[:contact_id].present? || deal_params[:contact_attributes].blank?
  18. 3 contact = ContactBuilder.new(user, deal_params[:contact_attributes], true).perform
  19. 3 deal.contact = contact
  20. end
  21. 1 def assign_user_to_deal
  22. 6 deal.deal_assignees.build(user:)
  23. end
  24. 1 def deal_params
  25. 23 params.permit(*permitted_deal_params)
  26. end
  27. end

app/builders/deal_product_builder.rb

100.0% lines covered

25 relevant lines. 25 lines covered and 0 lines missed.
    
  1. 1 class DealProductBuilder
  2. 1 include DealProductConcern
  3. 1 def initialize(params)
  4. 7 @params = params
  5. end
  6. 1 def build
  7. 7 @deal_product = DealProduct.new(deal_product_params)
  8. 7 set_unit_amount_in_cents
  9. 7 set_product_identifier
  10. 7 set_product_name
  11. 7 @deal_product
  12. end
  13. 1 def perform
  14. 7 build
  15. 7 @deal_product
  16. end
  17. 1 private
  18. 1 def set_unit_amount_in_cents
  19. 7 product_amount_in_cents = @deal_product.product&.amount_in_cents
  20. 7 @deal_product.unit_amount_in_cents = product_amount_in_cents
  21. end
  22. 1 def set_product_identifier
  23. 7 product_identifier = @deal_product.product&.identifier
  24. 7 @deal_product.product_identifier = product_identifier
  25. end
  26. 1 def set_product_name
  27. 7 product_name = @deal_product.product&.name
  28. 7 @deal_product.product_name = product_name
  29. end
  30. 1 def deal_product_params
  31. 7 @params.permit(
  32. *permitted_deal_product_params
  33. )
  34. end
  35. end

app/builders/event_builder.rb

97.44% lines covered

39 relevant lines. 38 lines covered and 1 lines missed.
    
  1. 1 class EventBuilder
  2. 1 def initialize(user, params)
  3. 23 @params = params
  4. 23 @user = user
  5. 23 @account = user.account
  6. end
  7. 1 def build
  8. 23 @event = @user.account.events.new(@params)
  9. 23 set_contact
  10. 23 set_deal
  11. 23 @event.done = true if @event.kind == 'note'
  12. # clean_html_codes()
  13. 23 build_files if @params.key?('files')
  14. 23 @event
  15. end
  16. 1 def clean_html_codes
  17. @event.content.body = '' if @event.content.present? && @event.kind != 'note'
  18. end
  19. 1 def set_contact
  20. 23 if @params.key?(:contact_id)
  21. 23 @contact = @account.contacts.find(@params[:contact_id])
  22. 23 @event.contact = @contact
  23. end
  24. end
  25. 1 def set_deal
  26. 23 if @params.key?(:deal_id)
  27. 23 @deal = @account.deals.find(@params[:deal_id])
  28. 23 @event.deal = @deal
  29. end
  30. end
  31. 1 def build_files
  32. 2 result = @params['files'].map.with_index do |file, index|
  33. 8 if index.zero?
  34. 2 @event = set_attachment(@event, file)
  35. 2 next
  36. else
  37. 6 file_event_params = @params.except(:content, :files)
  38. 6 file_event = EventBuilder.new(@user, file_event_params).build
  39. 6 file_event = set_attachment(file_event, file)
  40. 6 file_event
  41. end
  42. end
  43. 2 @event.files_events = result.compact
  44. end
  45. 1 def set_attachment(event, file)
  46. 8 attachment = event.build_attachment
  47. 8 attachment.file = file
  48. 7 attachment.file_type = attachment.check_file_type
  49. 7 event
  50. rescue StandardError
  51. 1 @event.invalid_files = true
  52. 1 event
  53. end
  54. end

app/builders/evolution_api_builder.rb

100.0% lines covered

10 relevant lines. 10 lines covered and 0 lines missed.
    
  1. 1 class EvolutionApiBuilder
  2. 1 def initialize(user, params)
  3. 2 @params = params
  4. 2 @user = user
  5. end
  6. 1 def build
  7. 2 @evolution_api = @user.account.apps_evolution_apis.new(@params)
  8. 2 @evolution_api.instance = @evolution_api.generate_token('instance')
  9. 2 @evolution_api.token = @evolution_api.generate_token('token')
  10. 2 @evolution_api.endpoint_url = ENV['EVOLUTION_API_ENDPOINT']
  11. 2 @evolution_api
  12. end
  13. end

app/builders/product_builder.rb

100.0% lines covered

10 relevant lines. 10 lines covered and 0 lines missed.
    
  1. 1 class ProductBuilder
  2. 1 def initialize(user, params)
  3. 6 @params = params
  4. 6 @user = user
  5. end
  6. 1 def build
  7. 6 @product = @user.account.products.new(@params)
  8. 6 @product
  9. end
  10. 1 def perform
  11. 6 build
  12. 6 @product
  13. end
  14. end

app/builders/reports/base_timeseries_builder.rb

100.0% lines covered

20 relevant lines. 20 lines covered and 0 lines missed.
    
  1. 1 class Reports::BaseTimeseriesBuilder
  2. 1 include TimezoneHelper
  3. 1 include DateRangeHelper
  4. 1 DEFAULT_GROUP_BY = 'month'.freeze
  5. 1 attr_reader :account, :params
  6. 1 def initialize(account, params)
  7. 45 raise ArgumentError, 'account is required' unless account
  8. 44 raise ArgumentError, 'params is required' unless params
  9. 43 @account = account
  10. 43 @params = params
  11. end
  12. 1 def scope
  13. 18 case params[:type].to_sym
  14. when :account
  15. 17 account
  16. when :stage
  17. 1 stage
  18. end
  19. end
  20. 1 def stage
  21. 3 @stage ||= Stage.find(params[:id])
  22. end
  23. 1 def group_by
  24. 16 @group_by ||= %w[day week month year hour].include?(params[:group_by]) ? params[:group_by] : DEFAULT_GROUP_BY
  25. end
  26. 1 def timezone
  27. 80 @timezone ||= timezone_name_from_offset(params[:timezone_offset])
  28. end
  29. end

app/builders/reports/deals/base_report_builder.rb

100.0% lines covered

17 relevant lines. 17 lines covered and 0 lines missed.
    
  1. 1 class Reports::Deals::BaseReportBuilder
  2. 1 def initialize(account, params)
  3. 29 raise ArgumentError, 'account is required' unless account
  4. 27 raise ArgumentError, 'params is required' unless params
  5. 25 @account = account
  6. 25 @params = params
  7. end
  8. 1 private
  9. 1 attr_reader :account, :params
  10. COUNT_METRICS = %w[
  11. 1 won_deals_count
  12. lost_deals_count
  13. open_deals_count
  14. all_deals_count
  15. ].freeze
  16. SUM_METRICS = %w[
  17. 1 won_deals_sum
  18. lost_deals_sum
  19. open_deals_sum
  20. all_deals_sum
  21. ].freeze
  22. 1 def builder_class(metric)
  23. 23 case metric
  24. when *COUNT_METRICS
  25. 12 Reports::Deals::Timeseries::CountReportBuilder
  26. when *SUM_METRICS
  27. 8 Reports::Deals::Timeseries::SumReportBuilder
  28. end
  29. end
  30. 1 def log_invalid_metric
  31. 3 Rails.logger.error "ReportBuilder: Invalid metric - #{params[:metric]}"
  32. 3 {}
  33. end
  34. end

app/builders/reports/deals/metric_builder.rb

100.0% lines covered

14 relevant lines. 14 lines covered and 0 lines missed.
    
  1. 1 class Reports::Deals::MetricBuilder < Reports::Deals::BaseReportBuilder
  2. 1 def summary
  3. {
  4. 5 title: fetch_summary_name,
  5. amount_in_cents: count("#{params[:metric]}_sum"),
  6. quantity: count("#{params[:metric]}_count")
  7. }
  8. end
  9. 1 private
  10. 1 def count(metric)
  11. 10 builder_class(metric).new(account, builder_params(metric)).aggregate_value
  12. end
  13. 1 def builder_params(metric)
  14. 10 params.merge({ metric: })
  15. end
  16. 1 def fetch_summary_name
  17. 9 case params[:metric].to_sym
  18. when :open_deals
  19. 2 I18n.t('activerecord.models.deal.open_deals')
  20. when :lost_deals
  21. 2 I18n.t('activerecord.models.deal.lost_deals')
  22. when :won_deals
  23. 3 I18n.t('activerecord.models.deal.won_deals')
  24. when :all_deals
  25. 2 I18n.t('activerecord.models.deal.created_deals')
  26. end
  27. end
  28. end

app/builders/reports/deals/report_builder.rb

100.0% lines covered

11 relevant lines. 11 lines covered and 0 lines missed.
    
  1. 1 class Reports::Deals::ReportBuilder < Reports::Deals::BaseReportBuilder
  2. 1 def timeseries
  3. 4 perform_action(:timeseries)
  4. end
  5. 1 def aggregate_value
  6. 2 perform_action(:aggregate_value)
  7. end
  8. 1 private
  9. 1 def perform_action(method_name)
  10. 6 return builder.new(account, params).public_send(method_name) if builder.present?
  11. 2 log_invalid_metric
  12. end
  13. 1 def builder
  14. 6 builder_class(params[:metric])
  15. end
  16. end

app/builders/reports/deals/timeseries/base_report_builder.rb

100.0% lines covered

26 relevant lines. 26 lines covered and 0 lines missed.
    
  1. 1 class Reports::Deals::Timeseries::BaseReportBuilder < Reports::BaseTimeseriesBuilder
  2. 1 def timeseries
  3. 3 grouped_count.each_with_object([]) do |element, arr|
  4. 69 event_date, event_count = element
  5. # The `event_date` is in Date format (without time), such as "Wed, 15 May 2024".
  6. # We need a timestamp for the start of the day. However, we can't use `event_date.to_time.to_i`
  7. # because it converts the date to 12:00 AM server timezone.
  8. # The desired output should be 12:00 AM in the specified timezone.
  9. 69 arr << { value: event_count, timestamp: event_date.in_time_zone(timezone).to_i }
  10. end
  11. end
  12. 1 private
  13. 1 def grouped_count
  14. # Override this method
  15. end
  16. 1 def metric
  17. 31 filtered_metric = params[:metric].gsub(/_(sum|count)\z/, '')
  18. 31 @metric ||= filtered_metric
  19. end
  20. 1 def object_scope
  21. 16 scope = send("scope_for_#{metric}")
  22. 16 Query::Filter.new(scope, params[:filter]).call
  23. end
  24. 1 def scope_for_won_deals
  25. 6 scope.deals.won.where(won_at: range)
  26. end
  27. 1 def scope_for_lost_deals
  28. 4 scope.deals.lost.where(lost_at: range)
  29. end
  30. 1 def scope_for_open_deals
  31. 3 scope.deals.open.where(created_at: range)
  32. end
  33. 1 def scope_for_all_deals
  34. 3 scope.deals.where(created_at: range)
  35. end
  36. 1 def grouping_field
  37. 14 case metric.to_sym
  38. when :won_deals
  39. 10 :won_at
  40. when :lost_deals
  41. 2 :lost_at
  42. else
  43. 2 :created_at
  44. end
  45. end
  46. end

app/builders/reports/deals/timeseries/count_report_builder.rb

100.0% lines covered

6 relevant lines. 6 lines covered and 0 lines missed.
    
  1. 1 class Reports::Deals::Timeseries::CountReportBuilder < Reports::Deals::Timeseries::BaseReportBuilder
  2. 1 def aggregate_value
  3. 5 object_scope.count
  4. end
  5. 1 private
  6. 1 def grouped_count
  7. 6 @grouped_values = object_scope.group_by_period(
  8. group_by,
  9. grouping_field,
  10. default_value: 0,
  11. range:,
  12. permit: %w[day week month year hour],
  13. time_zone: timezone
  14. ).count
  15. end
  16. end

app/builders/reports/deals/timeseries/sum_report_builder.rb

100.0% lines covered

6 relevant lines. 6 lines covered and 0 lines missed.
    
  1. 1 class Reports::Deals::Timeseries::SumReportBuilder < Reports::Deals::Timeseries::BaseReportBuilder
  2. 1 def aggregate_value
  3. 5 object_scope.sum(:total_deal_products_amount_in_cents)
  4. end
  5. 1 private
  6. 1 def grouped_count
  7. 4 @grouped_values = object_scope.group_by_period(
  8. group_by,
  9. grouping_field,
  10. default_value: 0,
  11. range:,
  12. permit: %w[day week month year hour],
  13. time_zone: timezone
  14. ).sum(:total_deal_products_amount_in_cents)
  15. end
  16. end

app/builders/reports/pipeline/stages_metric_builder.rb

100.0% lines covered

20 relevant lines. 20 lines covered and 0 lines missed.
    
  1. 1 class Reports::Pipeline::StagesMetricBuilder
  2. 1 include DateRangeHelper
  3. 1 def initialize(account, params)
  4. 14 raise ArgumentError, 'account is required' unless account
  5. 13 raise ArgumentError, 'params is required' unless params
  6. 12 @account = account
  7. 12 @params = params
  8. end
  9. 1 def metrics
  10. 4 return build_metrics if valid_deal_status?
  11. 1 raise ArgumentError, 'invalid metric'
  12. end
  13. 1 private
  14. 1 attr_reader :account, :params
  15. 1 def pipeline
  16. 5 @pipeline ||= Pipeline.find(params[:id])
  17. end
  18. 1 def valid_deal_status?
  19. 9 %i[won_deals lost_deals open_deals all_deals].include?(params[:metric]&.to_sym)
  20. end
  21. 1 def build_metrics
  22. 3 pipeline.stages.order(:position).each_with_object({}) do |stage, hash|
  23. 4 params_stage = params.merge(metric: "#{params[:metric]}_count", id: stage.id, type: :stage)
  24. 4 hash[stage.name] = Reports::Deals::ReportBuilder.new(account, params_stage).aggregate_value
  25. end
  26. end
  27. end

app/channels/application_cable/channel.rb

100.0% lines covered

2 relevant lines. 2 lines covered and 0 lines missed.
    
  1. 1 module ApplicationCable
  2. 1 class Channel < ActionCable::Channel::Base
  3. end
  4. end

app/channels/application_cable/connection.rb

100.0% lines covered

2 relevant lines. 2 lines covered and 0 lines missed.
    
  1. 1 module ApplicationCable
  2. 1 class Connection < ActionCable::Connection::Base
  3. end
  4. end

app/controllers/accounts/advanced_searches_controller.rb

100.0% lines covered

7 relevant lines. 7 lines covered and 0 lines missed.
    
  1. 1 class Accounts::AdvancedSearchesController < InternalController
  2. 1 def index
  3. end
  4. 1 def results
  5. 2 @results = Query::AdvancedSearch.new(current_user, current_user.account, search_params).call
  6. end
  7. 1 private
  8. 1 def search_params
  9. 2 params.permit(:q, :search_type)
  10. end
  11. end

app/controllers/accounts/apps/ai_assistents_controller.rb

100.0% lines covered

12 relevant lines. 12 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 class Accounts::Apps::AiAssistentsController < InternalController
  3. 1 before_action :set_ai_assistent
  4. 1 def edit; end
  5. 1 def update
  6. 2 if @ai_assistent.update(ai_assistent_params)
  7. 1 redirect_to edit_account_apps_ai_assistent_path(current_user.account),
  8. notice: t('flash_messages.updated', model: Apps::AiAssistent.model_name.human)
  9. else
  10. 1 render :edit, status: :unprocessable_entity
  11. end
  12. end
  13. 1 private
  14. 1 def set_ai_assistent
  15. 3 @ai_assistent = Apps::AiAssistent.first.presence || Apps::AiAssistent.create
  16. end
  17. 1 def ai_assistent_params
  18. 2 params.require(:apps_ai_assistent).permit(:auto_reply, :model, :api_key, :enabled)
  19. end
  20. end

app/controllers/accounts/apps/chatwoots_controller.rb

100.0% lines covered

25 relevant lines. 25 lines covered and 0 lines missed.
    
  1. 1 class Accounts::Apps::ChatwootsController < InternalController
  2. 1 before_action :set_chatwoot, only: %i[edit update destroy]
  3. 1 def new
  4. 2 if current_user.account.apps_chatwoots.blank?
  5. 1 @chatwoot = current_user.account.apps_chatwoots.new
  6. else
  7. 1 redirect_to edit_account_apps_chatwoot_path(current_user.account, current_user.account.apps_chatwoots.first)
  8. end
  9. end
  10. 1 def edit; end
  11. 1 def create
  12. 2 result = Accounts::Apps::Chatwoots::Create.call(current_user.account, chatwoot_params)
  13. 2 @chatwoot = result[result.keys.first]
  14. 2 if result.key?(:ok)
  15. 1 redirect_to edit_account_apps_chatwoot_path(current_user.account, @chatwoot),
  16. notice: t('flash_messages.created', model: Apps::Chatwoot.model_name.human)
  17. else
  18. 1 render :new
  19. end
  20. end
  21. 1 def destroy
  22. 1 result = Accounts::Apps::Chatwoots::Delete.call(current_user.account, @chatwoot)
  23. 1 if result.key?(:ok)
  24. 1 redirect_to account_settings_path(current_user.account),
  25. notice: t('flash_messages.deleted', model: Apps::Chatwoot.model_name.human)
  26. end
  27. end
  28. 1 def update
  29. 1 @chatwoot.update(chatwoot_params)
  30. 1 redirect_to edit_account_apps_chatwoot_path(current_user.account, current_user.account.apps_chatwoots.first)
  31. end
  32. 1 private
  33. 1 def set_chatwoot
  34. 3 @chatwoot = current_user.account.apps_chatwoots.first
  35. end
  36. 1 def chatwoot_params
  37. 3 params.require(:apps_chatwoot).permit(:chatwoot_endpoint_url, :chatwoot_account_id, :chatwoot_user_token, :active)
  38. end
  39. end

app/controllers/accounts/apps/evolution_apis_controller.rb

100.0% lines covered

35 relevant lines. 35 lines covered and 0 lines missed.
    
  1. 1 class Accounts::Apps::EvolutionApisController < InternalController
  2. 1 before_action :set_evolution_api, only: %i[edit update refresh_qr_code pair_qr_code destroy]
  3. 1 def new
  4. 1 @evolution_api = Apps::EvolutionApi.new
  5. end
  6. 1 def create
  7. 2 result = Accounts::Apps::EvolutionApis::Create.call(current_user, evolution_api_params)
  8. 2 @evolution_api = result[result.keys.first]
  9. 2 if result.key?(:ok)
  10. 1 redirect_to pair_qr_code_account_apps_evolution_api_path(current_user.account, @evolution_api.id)
  11. else
  12. 1 @evolution_api = result[:error]
  13. 1 render :new, status: :unprocessable_entity
  14. end
  15. end
  16. 1 def index
  17. 3 @evolution_apis = current_user.account.apps_evolution_apis.order(updated_at: :desc)
  18. 3 @pagy, @evolution_apis = pagy(@evolution_apis)
  19. end
  20. 1 def edit; end
  21. 1 def update
  22. 2 if @evolution_api.update(evolution_api_params)
  23. 1 flash[:notice] = t('flash_messages.updated', model: Apps::EvolutionApi.model_name.human)
  24. 1 redirect_to edit_account_apps_evolution_api_path(current_user.account, @evolution_api)
  25. else
  26. 1 render :edit, status: :unprocessable_entity
  27. end
  28. end
  29. 1 def refresh_qr_code
  30. 1 @evolution_api.update({
  31. instance: @evolution_api.generate_token('instance'),
  32. token: @evolution_api.generate_token('token')
  33. })
  34. 1 Accounts::Apps::EvolutionApis::Instance::Create.call(@evolution_api)
  35. end
  36. 1 def destroy
  37. 1 if @evolution_api.destroy
  38. 1 respond_to do |format|
  39. 1 format.html do
  40. 1 redirect_to account_apps_evolution_apis_path(current_user.account),
  41. notice: t('flash_messages.deleted', model: Apps::EvolutionApi.model_name.human)
  42. end
  43. 1 format.turbo_stream
  44. end
  45. end
  46. end
  47. 1 def pair_qr_code; end
  48. 1 private
  49. 1 def set_evolution_api
  50. 9 @evolution_api = current_user.account.apps_evolution_apis.find(params[:id])
  51. end
  52. 1 def evolution_api_params
  53. 4 params.require(:apps_evolution_api).permit(:name)
  54. end
  55. end

app/controllers/accounts/apps_controller.rb

36.67% lines covered

30 relevant lines. 11 lines covered and 19 lines missed.
    
  1. 1 class Accounts::AppsController < InternalController
  2. 1 before_action :set_contact, only: %i[show edit update destroy]
  3. # GET /contacts or /contacts.json
  4. 1 def index
  5. @apps = current_user.account.apps
  6. @pagy, @apps = pagy(@apps)
  7. end
  8. # GET /contacts/new
  9. 1 def new
  10. @contact = Contact.new
  11. end
  12. # GET /contacts/1/edit
  13. 1 def edit; end
  14. # POST /contacts or /contacts.json
  15. 1 def create
  16. @contact = current_user.account.contacts.new(contact_params)
  17. if @contact.save
  18. redirect_to account_contact_path(current_user.account, @contact),
  19. notice: t('flash_messages.created', model: Contact.model_name.human)
  20. else
  21. render :new, status: :unprocessable_entity
  22. end
  23. end
  24. # PATCH/PUT /contacts/1 or /contacts/1.json
  25. 1 def update
  26. respond_to do |format|
  27. if @contact.update(contact_params)
  28. redirect_to account_contact_path(current_user.account, @contact),
  29. notice: t('flash_messages.updated', model: Contact.model_name.human)
  30. else
  31. format.html { render :edit, status: :unprocessable_entity }
  32. format.json { render json: @contact.errors, status: :unprocessable_entity }
  33. end
  34. end
  35. end
  36. # DELETE /contacts/1 or /contacts/1.json
  37. 1 def destroy
  38. @contact.destroy
  39. respond_to do |format|
  40. format.html do
  41. redirect_to contacts_url, notice: t('flash_messages.deleted', model: Contact.model_name.human)
  42. end
  43. format.json { head :no_content }
  44. end
  45. end
  46. 1 private
  47. # Use callbacks to share common setup or constraints between actions.
  48. 1 def set_contact
  49. @contact = Contact.find(params[:id])
  50. end
  51. # Only allow a list of trusted parameters through.
  52. 1 def contact_params
  53. params.require(:contact).permit(:full_name, :phone, :email, custom_attributes: {})
  54. end
  55. end

app/controllers/accounts/attachments_controller.rb

100.0% lines covered

9 relevant lines. 9 lines covered and 0 lines missed.
    
  1. 1 class Accounts::AttachmentsController < InternalController
  2. 1 before_action :set_attachment, only: %i[destroy]
  3. 1 def destroy
  4. 1 @attachment.destroy
  5. 1 respond_to do |format|
  6. 1 format.turbo_stream
  7. end
  8. end
  9. 1 private
  10. 1 def set_attachment
  11. 1 @attachment = Attachment.find(params[:id])
  12. end
  13. end

app/controllers/accounts/contacts/chatwoot_embed_controller.rb

96.67% lines covered

30 relevant lines. 29 lines covered and 1 lines missed.
    
  1. 1 class Accounts::Contacts::ChatwootEmbedController < InternalController
  2. 1 layout 'embed'
  3. 1 before_action :set_contact, only: %i[show]
  4. 1 def search
  5. 6 contact = contact_search
  6. 6 if contact.present?
  7. 3 redirect_to account_chatwoot_embed_path(current_user.account, contact)
  8. else
  9. 3 chatwoot_contact = JSON.parse(params['chatwoot_contact'])
  10. 3 @contact = current_user.account.contacts.new({
  11. full_name: chatwoot_contact['name'],
  12. email: chatwoot_contact['email'],
  13. phone: chatwoot_contact['phone_number'],
  14. additional_attributes: { 'chatwoot_id': chatwoot_contact['id'] }
  15. })
  16. 3 render :new
  17. end
  18. end
  19. 1 def show; end
  20. 1 def new
  21. 1 chatwoot_contact = JSON.parse(params['chatwoot_contact'])
  22. 1 @contact = current_user.account.contacts.new({
  23. full_name: chatwoot_contact['name'],
  24. email: chatwoot_contact['email'],
  25. phone: chatwoot_contact['phone_number'],
  26. additional_attributes: { 'chatwoot_id': chatwoot_contact['id'] }
  27. })
  28. end
  29. 1 def create
  30. 1 @contact = current_user.account.contacts.new(contact_params)
  31. 1 if @contact.save
  32. 1 redirect_to account_chatwoot_embed_path(current_user.account, @contact),
  33. notice: t('flash_messages.created', model: Contact.model_name.human)
  34. else
  35. render :new, status: :unprocessable_entity
  36. end
  37. end
  38. 1 private
  39. 1 def set_contact
  40. 5 @contact = Contact.find(params[:id])
  41. end
  42. 1 def contact_params
  43. 1 params.require(:contact).permit(:full_name, :phone, :email, additional_attributes: {})
  44. end
  45. 1 def chatwoot_contact
  46. 18 @chatwoot_contact ||= JSON.parse(params['chatwoot_contact'])
  47. end
  48. 1 def contact_search
  49. 6 result = current_user.account.contacts.by_chatwoot_id(chatwoot_contact['id']).first
  50. 6 return result if result.present?
  51. 6 Accounts::Contacts::GetByParams.call(current_user.account,
  52. { email: chatwoot_contact['email'],
  53. phone: chatwoot_contact['phone_number'] })[:ok]
  54. end
  55. end

app/controllers/accounts/contacts/events_controller.rb

100.0% lines covered

29 relevant lines. 29 lines covered and 0 lines missed.
    
  1. 1 class Accounts::Contacts::EventsController < InternalController
  2. 1 before_action :set_event, only: %i[show edit update destroy show]
  3. 1 before_action :set_contact, only: %i[show edit update destroy new]
  4. 1 def new
  5. # @event = current_user.account.events.new(event_params.merge({contact: @contact}))
  6. 2 @event = EventBuilder.new(current_user,
  7. event_params.merge({ contact_id: @contact.id, kind: params[:kind],
  8. deal_id: params[:deal_id] })).build
  9. end
  10. 1 def edit; end
  11. 1 def create
  12. 14 @event = EventBuilder.new(current_user, event_params).build
  13. 14 if @event.save
  14. 13 respond_to do |format|
  15. 13 format.html do
  16. 13 redirect_to(new_account_contact_event_path(account_id: current_user.account, contact_id: @event.deal.contact.id,
  17. deal_id: @event.deal.id))
  18. end
  19. 13 format.turbo_stream
  20. end
  21. else
  22. 1 render :new, status: :unprocessable_entity
  23. end
  24. end
  25. 1 def destroy
  26. 1 @event.destroy
  27. end
  28. 1 def update
  29. 8 @deal = current_user.account.deals.find(params[:deal_id])
  30. 8 @events = @deal.contact.events
  31. 8 render :edit, status: :unprocessable_entity unless @event.update(event_params)
  32. end
  33. 1 def show; end
  34. 1 private
  35. # Use callbacks to share common setup or constraints between actions.
  36. 1 def set_event
  37. 9 @event = current_user.account.events.find(params[:id])
  38. end
  39. 1 def set_contact
  40. 11 @contact = current_user.account.contacts.find(params[:contact_id])
  41. end
  42. # Only allow a list of trusted parameters through.
  43. 1 def event_params
  44. 24 params.require(:event).permit(:content, :contact_id, :send_now, :done, :deal_id, :auto_done, :title, :scheduled_at, :from_me, :kind, :app_type,
  45. :app_id, files: [], custom_attributes: {}, additional_attributes: {})
  46. rescue StandardError
  47. 2 {}
  48. end
  49. end

app/controllers/accounts/contacts_controller.rb

87.5% lines covered

64 relevant lines. 56 lines covered and 8 lines missed.
    
  1. 1 class Accounts::ContactsController < InternalController
  2. 1 before_action :set_contact, only: %i[show edit update destroy chatwoot_conversation_link hovercard_preview]
  3. 1 def show
  4. 4 @pagy_deals, @deals = pagy(@contact.deals.order(created_at: :desc), items: 10, page_param: :deals_page)
  5. 4 respond_to do |format|
  6. 4 format.html
  7. 4 format.turbo_stream
  8. end
  9. end
  10. # GET /contacts or /contacts.json
  11. 1 def index
  12. 8 @contacts = if params[:query].present?
  13. 7 Contact.where(
  14. 'full_name ILIKE :search OR email ILIKE :search OR phone ILIKE :search', search: "%#{params[:query]}%"
  15. ).order(updated_at: :desc)
  16. else
  17. 1 Contact.all.order(created_at: :desc)
  18. end
  19. 8 @pagy, @contacts = pagy(@contacts)
  20. end
  21. 1 def select_contact_search
  22. 10 @contacts = if params[:query].present?
  23. 2 current_user.account.contacts.where(
  24. 'full_name ILIKE :search OR email ILIKE :search OR phone ILIKE :search', search: "%#{params[:query]}%"
  25. ).order(updated_at: :desc).limit(5)
  26. else
  27. 8 current_user.account.contacts.order(updated_at: :desc).limit(5)
  28. end
  29. end
  30. 1 def search
  31. @contacts = current_user.account.contacts.where(
  32. 'full_name ILIKE :search OR email ILIKE :search OR phone ILIKE :search', search: "%#{params[:q]}%"
  33. ).limit(5).map(&:attributes)
  34. @results = @contacts.each do |c|
  35. c[:text] = "#{c['full_name']} - #{c['email']} - #{c['phone']}"
  36. c
  37. end
  38. @results.insert(0, { "id": 0, "text": 'New contact' })
  39. json = {
  40. "results": @results
  41. }
  42. render json:
  43. end
  44. # GET /contacts/new
  45. 1 def new
  46. 1 @contact = Contact.new
  47. end
  48. # GET /contacts/1/edit
  49. 1 def edit; end
  50. 1 def edit_custom_attributes
  51. 1 @contact = current_user.account.contacts.find(params[:contact_id])
  52. 1 @custom_attribute_definitions = current_user.account.custom_attribute_definitions.contact_attribute
  53. end
  54. # POST /contacts or /contacts.json
  55. 1 def create
  56. 2 @contact = current_user.account.contacts.new(contact_params)
  57. 2 if @contact.save
  58. 1 @pagy_deals, @deals = pagy(@contact.deals.order(created_at: :desc), items: 10, page_param: :deals_page)
  59. 1 respond_to do |format|
  60. 1 format.html do
  61. 1 redirect_to account_contact_path(current_user.account, @contact),
  62. notice: t('flash_messages.created', model: Contact.model_name.human)
  63. end
  64. 1 format.turbo_stream
  65. end
  66. else
  67. 1 render :new, status: :unprocessable_entity
  68. end
  69. end
  70. # PATCH/PUT /contacts/1 or /contacts/1.json
  71. 1 def update
  72. 2 if params[:contact][:att_key].present?
  73. @contact.custom_attributes[params[:contact][:att_key]] = params[:contact][:att_value]
  74. end
  75. 2 if @contact.update(contact_params)
  76. 1 flash[:notice] = t('flash_messages.updated', model: Contact.model_name.human)
  77. 1 respond_to do |format|
  78. 2 format.html { redirect_to account_contact_path(current_user.account, @contact) }
  79. 1 format.turbo_stream
  80. end
  81. else
  82. 1 render :edit, status: :unprocessable_entity
  83. end
  84. end
  85. # DELETE /contacts/1 or /contacts/1.json
  86. 1 def destroy
  87. 1 @contact.destroy
  88. 1 respond_to do |format|
  89. 1 format.html do
  90. 1 redirect_to account_contacts_path(current_user.account),
  91. notice: t('flash_messages.deleted', model: Contact.model_name.human)
  92. end
  93. 1 format.json { head :no_content }
  94. end
  95. end
  96. 1 def chatwoot_conversation_link
  97. 6 @display_format = params[:display_format].presence || 'icon'
  98. 6 @chatwoot_conversation_link = Contact::Integrations::Chatwoot::GenerateConversationLink.new(@contact).call[:ok]
  99. rescue Faraday::TimeoutError, Faraday::ConnectionFailed, JSON::ParserError
  100. 2 @connection_error = true
  101. end
  102. 1 def hovercard_preview
  103. end
  104. 1 private
  105. # Use callbacks to share common setup or constraints between actions.
  106. 1 def set_contact
  107. 14 @contact = Contact.find(params[:id])
  108. end
  109. # Only allow a list of trusted parameters through.
  110. 1 def contact_params
  111. 4 params.require(:contact).permit(:full_name, :phone, :email, :label_list,
  112. custom_attributes: {})
  113. end
  114. end

app/controllers/accounts/deal_assignees_controller.rb

100.0% lines covered

25 relevant lines. 25 lines covered and 0 lines missed.
    
  1. 1 class Accounts::DealAssigneesController < InternalController
  2. 1 before_action :set_deal_assignee, only: %i[destroy]
  3. 1 before_action :set_deal, only: %i[new]
  4. 1 def destroy
  5. 1 return unless @deal_assignee.destroy
  6. 1 respond_to do |format|
  7. 1 format.html do
  8. 1 redirect_to account_deal_path(current_user.account, @deal_assignee.deal),
  9. notice: t('flash_messages.deleted', model: DealAssignee.model_name.human)
  10. end
  11. 1 format.turbo_stream
  12. end
  13. end
  14. 1 def new
  15. 1 @deal_assignee = @deal.deal_assignees.new
  16. end
  17. 1 def create
  18. 4 @deal_assignee = DealAssignee.new(deal_assignees_params)
  19. 4 if @deal_assignee.save
  20. 1 respond_to do |format|
  21. 2 format.html { redirect_to account_deal_path(@deal_assignee.account, @deal_assignee.deal) }
  22. 1 format.turbo_stream
  23. end
  24. else
  25. 3 render :new, status: :unprocessable_entity
  26. end
  27. end
  28. 1 private
  29. 1 def deal_assignees_params
  30. 4 params.require(:deal_assignee).permit(:user_id, :deal_id)
  31. end
  32. 1 def set_deal
  33. 1 @deal = Deal.find(params[:deal_id])
  34. end
  35. 1 def set_deal_assignee
  36. 1 @deal_assignee = DealAssignee.find(params[:id])
  37. end
  38. end

app/controllers/accounts/deal_products_controller.rb

100.0% lines covered

27 relevant lines. 27 lines covered and 0 lines missed.
    
  1. 1 class Accounts::DealProductsController < InternalController
  2. 1 include DealProductConcern
  3. 1 before_action :set_deal_product, only: %i[destroy]
  4. 1 before_action :set_deal, only: %i[new]
  5. 1 def destroy
  6. 1 if DealProduct::Destroy.new(@deal_product).call
  7. 1 respond_to do |format|
  8. 1 format.html do
  9. 1 redirect_to account_deal_path(current_user.account, @deal_product.deal),
  10. notice: t('flash_messages.deleted', model: Product.model_name.human)
  11. end
  12. 1 format.turbo_stream
  13. end
  14. end
  15. end
  16. 1 def new
  17. 1 @deal_product = @deal.deal_products.new
  18. end
  19. 1 def create
  20. 2 @deal_product = DealProductBuilder.new(deal_product_params).perform
  21. 2 if DealProduct::CreateOrUpdate.new(@deal_product, {}).call
  22. 1 @deal_product.reload
  23. 1 respond_to do |format|
  24. 2 format.html { redirect_to account_deal_path(@deal_product.account, @deal_product.deal) }
  25. 1 format.turbo_stream
  26. end
  27. else
  28. 1 render :new, status: :unprocessable_entity
  29. end
  30. end
  31. 1 private
  32. 1 def deal_product_params
  33. 2 params.require(:deal_product).permit(*permitted_deal_product_params)
  34. end
  35. 1 def set_deal
  36. 1 @deal = current_user.account.deals.find(params[:deal_id])
  37. end
  38. 1 def set_deal_product
  39. 1 @deal_product = current_user.account.deal_products.find(params[:id])
  40. end
  41. end

app/controllers/accounts/deals_controller.rb

80.58% lines covered

103 relevant lines. 83 lines covered and 20 lines missed.
    
  1. 1 class Accounts::DealsController < InternalController
  2. 1 include DealProductConcern
  3. 1 include DealConcern
  4. 1 before_action :set_deal,
  5. only: %i[show edit update destroy events_to_do events_done deal_products deal_assignees mark_as_lost mark_as_won]
  6. 1 before_action :set_deal_product, only: %i[edit_deal_product
  7. update_deal_product]
  8. # GET /deals or /deals.json
  9. 1 def index
  10. 8 @first_pipeline = Pipeline.first
  11. 8 @deals = if params[:query].present?
  12. 7 Deal.left_joins(:contact)
  13. .where(
  14. 'deals.name ILIKE :search OR ' +
  15. 'contacts.full_name ILIKE :search OR ' +
  16. 'deals.id = :id',
  17. search: "%#{params[:query]}%",
  18. id: params[:query].to_i
  19. )
  20. .order(updated_at: :desc)
  21. else
  22. 1 Deal.all.order(created_at: :desc)
  23. end
  24. 8 @pagy, @deals = pagy(@deals)
  25. end
  26. # GET /deals/1 or /deals/1.json
  27. 1 def show; end
  28. # GET /deals/new
  29. 1 def new
  30. 2 @deal = Deal.new
  31. 2 @stages = Stage.ordered_by_pipeline_and_position
  32. 2 @deal.contact_id = params.dig(:deal, :contact_id)
  33. 2 if @deal.contact_id.blank?
  34. 1 @deal.errors.add(:contact, :blank)
  35. 1 render :new_select_contact, status: :unprocessable_entity
  36. return
  37. end
  38. end
  39. 1 def new_select_contact
  40. 1 @deal = Deal.new
  41. end
  42. 1 def add_contact
  43. @deal = Deal.find(params[:deal_id])
  44. end
  45. 1 def commit_add_contact
  46. @deal = Deal.find(params[:deal_id])
  47. @new_contact = Contact.find(params['deal']['contact_id'])
  48. @deal.contacts.push(@new_contact)
  49. if Deal::CreateOrUpdate.new(@deal, deal_params).call
  50. redirect_to account_deal_path(current_user.account, @deal)
  51. else
  52. render :add_contact, status: :unprocessable_entity
  53. end
  54. rescue StandardError
  55. render :add_contact, status: :unprocessable_entity
  56. end
  57. 1 def remove_contact
  58. @deal = Deal.find(params[:deal_id])
  59. @contacts_deal = @deal.contacts_deals.find_by_contact_id(params['contact_id'])
  60. if @contacts_deal.destroy
  61. redirect_to account_deal_path(current_user.account, @deal)
  62. else
  63. render :show, status: :unprocessable_entity
  64. end
  65. rescue StandardError
  66. render :show, status: :unprocessable_entity
  67. end
  68. # GET /deals/1/edit
  69. 1 def edit
  70. 5 @stages = Stage.ordered_by_pipeline_and_position
  71. end
  72. 1 def edit_custom_attributes
  73. @deal = current_user.account.deals.find(params[:deal_id])
  74. @custom_attribute_definitions = current_user.account.custom_attribute_definitions.deal_attribute
  75. end
  76. # POST /deals or /deals.json
  77. 1 def create
  78. 1 @stages = Stage.ordered_by_pipeline_and_position
  79. 1 @deal = DealBuilder.new(current_user, deal_params).perform
  80. 1 if Deal::CreateOrUpdate.new(@deal, deal_params).call
  81. 1 redirect_to account_deal_path(current_user.account, @deal)
  82. else
  83. render :new, status: :unprocessable_entity
  84. end
  85. end
  86. # PATCH/PUT /deals/1 or /deals/1.json
  87. 1 def update
  88. 4 @stages = Stage.ordered_by_pipeline_and_position
  89. 4 if params[:deal][:att_key].present?
  90. @deal.custom_attributes[params[:deal][:att_key]] = params[:deal][:att_value]
  91. end
  92. 4 if Deal::CreateOrUpdate.new(@deal, deal_params).call
  93. 4 respond_to do |format|
  94. 8 format.html { redirect_to account_deal_path(current_user.account, @deal) }
  95. 4 format.turbo_stream
  96. end
  97. else
  98. render :edit, status: :unprocessable_entity
  99. end
  100. end
  101. # DELETE /deals/1 or /deals/1.json
  102. 1 def destroy
  103. 1 @deal.destroy
  104. 1 respond_to do |format|
  105. 1 format.turbo_stream
  106. 2 format.html { redirect_to root_path, notice: t('flash_messages.deleted', model: Deal.model_name.human) }
  107. 1 format.json { head :no_content }
  108. end
  109. end
  110. 1 def events_to_do
  111. 2 @pagy, @events = pagy(@deal.contact.events.where(deal_id: [nil, @deal.id]).to_do, items: 5)
  112. 2 respond_to do |format|
  113. 2 format.turbo_stream
  114. 2 format.html
  115. end
  116. end
  117. 1 def events_done
  118. 2 @pagy, @events = pagy(@deal.contact.events.where(deal_id: [nil, @deal.id]).done, items: 5)
  119. 2 respond_to do |format|
  120. 2 format.turbo_stream
  121. 2 format.html
  122. end
  123. end
  124. 1 def deal_products
  125. 1 @deal_products = @deal.deal_products
  126. end
  127. 1 def deal_assignees
  128. 1 @deal_assignees = @deal.deal_assignees
  129. end
  130. 1 def edit_deal_product
  131. end
  132. 1 def update_deal_product
  133. 1 if DealProduct::CreateOrUpdate.new(@deal_product, deal_product_params).call
  134. 1 respond_to do |format|
  135. 1 format.html do
  136. 1 redirect_to deal_products_account_deal_path(current_user.account, @deal_product.deal)
  137. end
  138. 1 format.turbo_stream
  139. end
  140. else
  141. render :edit_deal_product, status: :unprocessable_entity
  142. end
  143. end
  144. 1 def mark_as_lost
  145. 2 @stages = Stage.ordered_by_pipeline_and_position
  146. 2 @lost_reasons = DealLostReason.order(:name).pluck(:name).uniq
  147. 2 @exists_deal_lost_reasons = DealLostReason.exists?
  148. 2 @allow_edit_lost_at = Current.account.deal_allow_edit_lost_at_won_at
  149. end
  150. 1 def mark_as_won
  151. 1 @stages = Stage.ordered_by_pipeline_and_position
  152. 1 @allow_edit_won_at = Current.account.deal_allow_edit_lost_at_won_at
  153. end
  154. 1 private
  155. 1 def set_deal
  156. 22 @deal = current_user.account.deals.find(params[:id])
  157. end
  158. 1 def set_deal_product
  159. 2 @deal_product = current_user.account.deal_products.find(params[:deal_product_id])
  160. end
  161. 1 def deal_product_params
  162. 1 params.require(:deal_product).permit(*permitted_deal_product_params)
  163. end
  164. # Only allow a list of trusted parameters through.
  165. 1 def deal_params
  166. 6 params.require(:deal).permit(*permitted_deal_params)
  167. end
  168. end

app/controllers/accounts/events_controller.rb

100.0% lines covered

14 relevant lines. 14 lines covered and 0 lines missed.
    
  1. 1 class Accounts::EventsController < InternalController
  2. 1 def calendar
  3. end
  4. 1 def calendar_events
  5. 1 start_date = Time.zone.parse(params[:start])
  6. 1 end_date = Time.zone.parse(params[:end])
  7. 1 events = Event.planned.where(scheduled_at: start_date..end_date)
  8. 1 render json: events.map { |event| {
  9. 3 id: event.id,
  10. title: "#{event.title} - #{event.contact.full_name}",
  11. start: event.scheduled_at.iso8601,
  12. backgroundColor: events_kind_color(event.kind),
  13. borderColor: events_kind_color(event.kind),
  14. extendedProps: {
  15. account_id: Current.account.id,
  16. contact_id: event.contact_id,
  17. deal_id: event.deal_id
  18. },
  19. url: account_deal_path(Current.account, event.deal)
  20. }}
  21. end
  22. 1 private
  23. 1 def events_kind_color(kind)
  24. 6 case kind
  25. when 'chatwoot_message'
  26. 2 '#369EF2'
  27. when 'evolution_api_message'
  28. 2 '#26D367'
  29. else
  30. 2 '#6857D9'
  31. end
  32. end
  33. end

app/controllers/accounts/pipelines_controller.rb

36.51% lines covered

126 relevant lines. 46 lines covered and 80 lines missed.
    
  1. 1 require 'csv'
  2. 1 require 'json_csv'
  3. 1 class Accounts::PipelinesController < InternalController
  4. 1 before_action :set_pipeline, only: %i[show edit update destroy bulk_action new_bulk_action]
  5. 1 before_action :set_bulk_action_event, only: %i[bulk_action new_bulk_action]
  6. 1 before_action :set_stage, only: %i[bulk_action new_bulk_action]
  7. # GET /pipelines or /pipelines.json
  8. 1 def index
  9. 2 pipeline = Pipeline.first
  10. 2 if pipeline
  11. 1 redirect_to(account_pipeline_path(Current.account, pipeline))
  12. else
  13. 1 redirect_to account_welcome_index_path(Current.account)
  14. end
  15. end
  16. # GET /pipelines/1 or /pipelines/1.json
  17. 1 def show
  18. @pipelines = Pipeline.all
  19. @filter_status_deal = if params[:filter_status_deal].present?
  20. params[:filter_status_deal]
  21. else
  22. 'open'
  23. end
  24. end
  25. # GET /pipelines/new
  26. 1 def new
  27. 1 @pipeline = Pipeline.new
  28. end
  29. # GET /pipelines/1/edit
  30. 1 def edit
  31. 1 @stages = @pipeline.stages.order(:position)
  32. end
  33. # POST /pipelines/1/import_file
  34. 1 def import_file
  35. @pipeline = Pipeline.find(params[:pipeline_id])
  36. uploaded_io = params[:import_file]
  37. csv_text = uploaded_io.read
  38. csv = CSV.parse(csv_text, headers: true)
  39. path_to_output_csv_file = "#{Rails.root}/tmp/deals-#{Time.current.to_i}.csv"
  40. line = 0
  41. CSV.open(path_to_output_csv_file, 'wb') do |csv_output|
  42. csv.each do |row|
  43. csv_output << row.to_h.keys + ['result'] if line == 0
  44. row_json = JsonCsv.csv_row_hash_to_hierarchical_json_hash(row, {})
  45. row_params = ActionController::Parameters.new(row_json).merge({ "stage_id": params[:stage_id] })
  46. deal = DealBuilder.new(current_user, row_params).perform
  47. csv_output << if deal.save
  48. row.to_h.values + [I18n.t('activerecord.models.deal.import_file_success', deal_id: deal.id)]
  49. else
  50. row.to_h.values + [I18n.t('activerecord.models.deal.import_file_failed',
  51. message_error: deal.errors.messages)]
  52. end
  53. line += 1
  54. end
  55. end
  56. response.headers['Content-Type'] = 'text/csv'
  57. response.headers['Content-Disposition'] = 'attachment; filename=deals.csv'
  58. # flash[:notice] = 'Arquivo processado com sucesso.'
  59. send_file path_to_output_csv_file
  60. # redirect_to account_pipeline_path(current_user.id, @pipeline.id), notice: 'Arquivo processado com sucesso.'
  61. end
  62. # GET /pipelines/1/import
  63. 1 def import
  64. @pipeline = Pipeline.find(params[:pipeline_id])
  65. @stage = Stage.find(params[:stage_id])
  66. respond_to do |format|
  67. format.turbo_stream
  68. format.html
  69. format.csv do
  70. path_to_output_csv_file = "#{Rails.root}/tmp/deals-#{Time.current.to_i}.csv"
  71. # headers = Deal.csv_header(Current.account)
  72. headers = ['name', 'contact_attributes.full_name', 'contact_attributes.phone']
  73. CSV.open(path_to_output_csv_file, 'w') do |csv|
  74. csv << headers
  75. end
  76. response.headers['Content-Type'] = 'text/csv'
  77. response.headers['Content-Disposition'] = 'attachment; filename=deals.csv'
  78. render file: path_to_output_csv_file
  79. end
  80. end
  81. end
  82. # GET /pipelines/1/export
  83. 1 def export
  84. @deals = Deal.where(stage_id: params['stage_id'])
  85. path_to_output_csv_file = "#{Rails.root}/tmp/deals-#{Time.current.to_i}.csv"
  86. JsonCsv.create_csv_for_json_records(path_to_output_csv_file) do |csv_builder|
  87. @deals.each do |deal|
  88. json = JSON.parse(deal.to_json(include: :contacts))
  89. csv_builder.add(json)
  90. end
  91. end
  92. respond_to do |format|
  93. format.html
  94. format.csv do
  95. response.headers['Content-Type'] = 'text/csv'
  96. response.headers['Content-Disposition'] = 'attachment; filename=deals.csv'
  97. render file: path_to_output_csv_file
  98. end
  99. end
  100. end
  101. 1 def bulk_action; end
  102. 1 def new_bulk_action; end
  103. 1 def create_bulk_action
  104. @deals = Deal.where(stage_id: params['event']['stage_id'], status: 'open')
  105. @stage = Stage.find(params['event']['stage_id'])
  106. if params['event']['send_now'] == 'true'
  107. time_start = DateTime.current
  108. elsif !params['event']['scheduled_at'].nil?
  109. time_start = params['event']['scheduled_at'].in_time_zone
  110. end
  111. @result = @deals.each_with_index do |deal, index|
  112. if params['event']['kind'] == 'chatwoot_message' || params['event']['kind'] == 'evolution_api_message'
  113. if params['event']['send_now'] == 'true'
  114. time_start += rand(10..15).seconds
  115. params['event']['send_now'] = 'false'
  116. elsif !time_start.nil?
  117. time_start += rand(10..15).seconds
  118. end
  119. end
  120. @event = EventBuilder.new(current_user,
  121. event_params.merge({ contact: deal.contact, scheduled_at: time_start })).build
  122. @event.deal = deal
  123. if !@event.valid? && index == 0
  124. render :new_bulk_action, status: :unprocessable_entity
  125. return
  126. end
  127. @event.save
  128. end
  129. respond_to do |format|
  130. format.turbo_stream
  131. end
  132. end
  133. 1 def bulk_action_2; end
  134. # POST /pipelines or /pipelines.json
  135. 1 def create
  136. 1 @pipeline = Pipeline.new(pipeline_params)
  137. 1 respond_to do |format|
  138. 1 if @pipeline.save
  139. 1 format.html do
  140. 1 redirect_to account_pipeline_path(Current.account, @pipeline),
  141. notice: t('flash_messages.created', model: Pipeline.model_name.human)
  142. end
  143. 1 format.json { render :show, status: :created, location: @pipeline }
  144. else
  145. format.html { render :new, status: :unprocessable_entity }
  146. format.json { render json: @pipeline.errors, status: :unprocessable_entity }
  147. end
  148. end
  149. end
  150. # PATCH/PUT /pipelines/1 or /pipelines/1.json
  151. 1 def update
  152. 1 if @pipeline.update(pipeline_params)
  153. 1 respond_to do |format|
  154. 1 format.html do
  155. 1 redirect_to account_pipeline_path(Current.account, @pipeline),
  156. notice: t('flash_messages.updated', model: Pipeline.model_name.human)
  157. end
  158. 1 format.turbo_stream
  159. end
  160. end
  161. end
  162. # DELETE /pipelines/1 or /pipelines/1.json
  163. 1 def destroy
  164. @pipeline.destroy
  165. respond_to do |format|
  166. format.html { redirect_to pipelines_url, notice: t('flash_messages.deleted', model: Pipeline.model_name.human) }
  167. format.json { head :no_content }
  168. end
  169. end
  170. 1 private
  171. # Use callbacks to share common setup or constraints between actions.
  172. 1 def set_pipeline
  173. 2 @pipeline = Pipeline.find(params[:id])
  174. end
  175. # Only allow a list of trusted parameters through.
  176. 1 def pipeline_params
  177. 2 params.require(:pipeline).permit(:name, stages_attributes: %i[id name _destroy account_id position])
  178. end
  179. 1 def set_bulk_action_event
  180. @event = EventBuilder.new(current_user,
  181. { kind: params[:kind] }).build
  182. end
  183. 1 def set_stage
  184. @stage = Stage.find(params[:stage_id])
  185. end
  186. 1 def deal_params(params)
  187. params.permit(
  188. :name, :status, :stage_id, :contact_id,
  189. contact_attributes: %i[id full_name phone email],
  190. custom_attributes: {}
  191. )
  192. end
  193. 1 def event_params
  194. params.require(:event).permit(:content, :send_now, :done, :auto_done, :title, :kind, :app_type, :app_id, :from_me, files: [],
  195. custom_attributes: {}, additional_attributes: {})
  196. rescue StandardError
  197. {}
  198. end
  199. end

app/controllers/accounts/products_controller.rb

100.0% lines covered

43 relevant lines. 43 lines covered and 0 lines missed.
    
  1. 1 class Accounts::ProductsController < InternalController
  2. 1 include ProductConcern
  3. 1 before_action :set_product, only: %i[edit destroy update show edit_custom_attributes update_custom_attributes]
  4. 1 def new
  5. 1 @product = current_user.account.products.new
  6. 1 @product.attachments.build
  7. end
  8. 1 def create
  9. 6 @product = ProductBuilder.new(current_user, product_params).perform
  10. 6 if @product.save
  11. 2 respond_to do |format|
  12. 2 format.html do
  13. 2 redirect_to account_products_path(current_user.account),
  14. notice: t('flash_messages.created', model: Product.model_name.human)
  15. end
  16. 2 format.turbo_stream
  17. end
  18. else
  19. 4 render :new, status: :unprocessable_entity
  20. end
  21. end
  22. 1 def edit; end
  23. 1 def update
  24. 3 if @product.update(product_params)
  25. 1 redirect_to edit_account_product_path(current_user.account, @product),
  26. notice: t('flash_messages.updated', model: Product.model_name.human)
  27. else
  28. 2 render :edit, status: :unprocessable_entity
  29. end
  30. end
  31. 1 def edit_custom_attributes
  32. 1 @custom_attribute_definitions = current_user.account.custom_attribute_definitions.product_attribute
  33. end
  34. 1 def update_custom_attributes
  35. 1 @product.custom_attributes[params[:product][:att_key]] = params[:product][:att_value]
  36. 1 render :edit_custom_attributes, status: :unprocessable_entity unless @product.save
  37. end
  38. 1 def index
  39. 9 @products = if params[:query].present?
  40. 8 Product.where(
  41. 'name ILIKE :search OR identifier ILIKE :search', search: "%#{params[:query]}%"
  42. ).order(updated_at: :desc)
  43. else
  44. 1 Product.all.order(created_at: :desc)
  45. end
  46. 9 @pagy, @products = pagy(@products)
  47. end
  48. 1 def destroy
  49. 2 @product.destroy
  50. 2 respond_to do |format|
  51. 2 format.html do
  52. 2 redirect_to account_products_path(current_user.account),
  53. notice: t('flash_messages.deleted', model: Product.model_name.human)
  54. end
  55. 2 format.json { head :no_content }
  56. end
  57. end
  58. 1 def select_product_search
  59. 10 @products = if params[:query].present?
  60. 2 current_user.account.products.where(
  61. 'name ILIKE :search', search: "%#{params[:query]}%"
  62. ).order(updated_at: :desc).limit(5)
  63. else
  64. 8 current_user.account.products.order(updated_at: :desc).limit(5)
  65. end
  66. end
  67. 1 def show
  68. end
  69. 1 private
  70. 1 def set_product
  71. 9 @product = current_user.account.products.find(params[:id])
  72. end
  73. end

app/controllers/accounts/reports_controller.rb

94.44% lines covered

36 relevant lines. 34 lines covered and 2 lines missed.
    
  1. 1 class Accounts::ReportsController < InternalController
  2. 1 before_action :set_date_range
  3. 1 def index
  4. 1 @users = User.all
  5. end
  6. 1 def summary
  7. 2 @deal_summary = build_deal_sumary
  8. 2 @deals_timeseries_count = build_chart_deals_timeseries_body
  9. end
  10. 1 def pipeline_summary
  11. 3 pipeline_id = params[:pipeline_id].presence || Pipeline.first&.id
  12. 3 if pipeline_id
  13. 2 series_data = Reports::Pipeline::StagesMetricBuilder.new(Current.account, report_params.merge(id: pipeline_id)).metrics
  14. 1 @pipeline_summary = build_chart_pipeline_summary_body(series_data)
  15. else
  16. 1 @pipeline_summary = {}
  17. end
  18. end
  19. 1 private
  20. 1 def build_deal_sumary
  21. [
  22. 2 Reports::Deals::MetricBuilder.new(Current.account, report_params.merge(metric: 'open_deals')).summary,
  23. Reports::Deals::MetricBuilder.new(Current.account, report_params.merge(metric: 'all_deals')).summary,
  24. Reports::Deals::MetricBuilder.new(Current.account, report_params.merge(metric: 'won_deals')).summary,
  25. Reports::Deals::MetricBuilder.new(Current.account, report_params.merge(metric: 'lost_deals')).summary
  26. ]
  27. end
  28. 1 def report_params
  29. 14 common_params.merge({
  30. metric: params[:metric],
  31. since: params[:since].to_time.to_i.to_s,
  32. until: params[:until].to_time.to_i.to_s,
  33. timezone_offset: params[:timezone_offset]
  34. })
  35. end
  36. 1 def common_params
  37. {
  38. 14 type: params[:type]&.to_sym,
  39. id: params[:id],
  40. group_by: params[:group_by],
  41. filter: params[:filter]&.to_unsafe_h
  42. }
  43. end
  44. 1 def build_chart_deals_timeseries_body
  45. {
  46. 2 chart_type: 'column',
  47. data: [
  48. { name: I18n.t('activerecord.models.deal.won_deals'),
  49. color: metric_color('won_deals'),
  50. series_data: Reports::Deals::ReportBuilder.new(Current.account,
  51. report_params.merge(metric: 'won_deals_count')).timeseries },
  52. { name: I18n.t('activerecord.models.deal.lost_deals'),
  53. color: metric_color('lost_deals'),
  54. series_data: Reports::Deals::ReportBuilder.new(Current.account,
  55. report_params.merge(metric: 'lost_deals_count')).timeseries }
  56. ]
  57. }.to_json
  58. end
  59. 1 def build_chart_pipeline_summary_body(series_data)
  60. {
  61. 1 chart_type: 'funnel',
  62. data: [
  63. { name: Deal.model_name.human,
  64. color: metric_color(params[:metric]),
  65. series_data: series_data
  66. }
  67. ]
  68. }.to_json
  69. end
  70. 1 def metric_color(metric)
  71. 5 case metric
  72. when 'lost_deals'
  73. 2 '#CF4F27'
  74. when 'won_deals'
  75. 3 '#259C50'
  76. when 'open_deals'
  77. '#5491F5'
  78. else
  79. '#6857D9'
  80. end
  81. end
  82. 1 def set_date_range
  83. 6 if params[:date_range].present?
  84. 4 starts_str, ends_str = params[:date_range].split(' - ')
  85. 4 params[:since] = starts_str
  86. 4 params[:until] = ends_str
  87. else
  88. 2 params[:date_range] = "#{params[:since]} - #{params[:until]}"
  89. end
  90. end
  91. end

app/controllers/accounts/settings/accounts_controller.rb

100.0% lines covered

13 relevant lines. 13 lines covered and 0 lines missed.
    
  1. 1 class Accounts::Settings::AccountsController < InternalController
  2. 1 include AccountConcern
  3. 1 def edit; end
  4. 1 def update
  5. 2 if @account.update(account_params)
  6. 1 respond_to do |format|
  7. 1 format.html do
  8. 1 redirect_to edit_account_settings_account_path(@account),
  9. notice: t('flash_messages.updated', model: Account.model_name.human)
  10. end
  11. 1 format.turbo_stream
  12. end
  13. else
  14. 1 render :edit, status: :unprocessable_entity
  15. end
  16. end
  17. 1 private
  18. 1 def account_params
  19. 2 params.require(:account).permit(*permitted_account_params)
  20. end
  21. end

app/controllers/accounts/settings/custom_attributes_definitions_controller.rb

100.0% lines covered

23 relevant lines. 23 lines covered and 0 lines missed.
    
  1. 1 class Accounts::Settings::CustomAttributesDefinitionsController < InternalController
  2. 1 before_action :set_custom_attribute_deffinition, only: %i[edit update destroy]
  3. 1 def index
  4. 1 @custom_attributes_definitions = current_user.account.custom_attributes_definitions
  5. end
  6. 1 def new
  7. 1 @custom_attribute_definition = current_user.account.custom_attributes_definitions.new
  8. end
  9. 1 def create
  10. 4 @custom_attribute_definition = current_user.account.custom_attributes_definitions.new(custom_attribute_definition_params)
  11. 4 if @custom_attribute_definition.save
  12. 2 redirect_to account_custom_attributes_definitions_path(current_user.account),
  13. notice: t('flash_messages.created', model: CustomAttributeDefinition.model_name.human)
  14. else
  15. 2 render :new, status: :unprocessable_entity
  16. end
  17. end
  18. 1 def edit; end
  19. 1 def update
  20. 2 if @custom_attribute_definition.update(custom_attribute_definition_params)
  21. 1 redirect_to edit_account_custom_attributes_definition_path(current_user.account, @custom_attribute_definition),
  22. notice: t('flash_messages.updated', model: CustomAttributeDefinition.model_name.human)
  23. else
  24. 1 render :edit, status: :unprocessable_entity
  25. end
  26. end
  27. 1 def destroy
  28. 1 render :index, status: :unprocessable_entity unless @custom_attribute_definition.destroy
  29. end
  30. 1 private
  31. 1 def set_custom_attribute_deffinition
  32. 4 @custom_attribute_definition = current_user.account.custom_attribute_definitions.find(params[:id])
  33. end
  34. 1 def custom_attribute_definition_params
  35. 6 params.require(:custom_attribute_definition).permit(
  36. :attribute_model,
  37. :attribute_key,
  38. :attribute_display_name,
  39. :attribute_description
  40. )
  41. end
  42. end

app/controllers/accounts/settings/deals/deal_lost_reasons_controller.rb

91.43% lines covered

35 relevant lines. 32 lines covered and 3 lines missed.
    
  1. 1 class Accounts::Settings::Deals::DealLostReasonsController < InternalController
  2. 1 before_action :set_deal_lost_reason, only: %i[edit update destroy]
  3. 1 def index
  4. 2 @deal_lost_reasons = DealLostReason.all
  5. end
  6. 1 def edit; end
  7. 1 def update
  8. 2 if @deal_lost_reason.update(deal_lost_reason_params)
  9. 1 respond_to do |format|
  10. 1 format.html do
  11. 1 redirect_to edit_account_settings_deals_deal_lost_reason_path(Current.account, @deal_lost_reason),
  12. notice: t('flash_messages.updated', model: DealLostReason.model_name.human)
  13. end
  14. 1 format.turbo_stream
  15. end
  16. else
  17. 1 render :edit, status: :unprocessable_entity
  18. end
  19. end
  20. 1 def new
  21. 1 @deal_lost_reason = DealLostReason.new
  22. end
  23. 1 def create
  24. 2 @deal_lost_reason = DealLostReason.new(deal_lost_reason_params)
  25. 2 if @deal_lost_reason.save
  26. 1 respond_to do |format|
  27. 1 format.html do
  28. 1 redirect_to account_settings_deals_deal_lost_reasons_path(Current.account),
  29. notice: t('flash_messages.created', model: DealLostReason.model_name.human)
  30. end
  31. 1 format.turbo_stream
  32. end
  33. else
  34. 1 render :new, status: :unprocessable_entity
  35. end
  36. end
  37. 1 def destroy
  38. 1 if @deal_lost_reason.destroy
  39. 1 respond_to do |format|
  40. 1 format.html do
  41. 1 redirect_to account_settings_deals_deal_lost_reasons_path(Current.account),
  42. notice: t('flash_messages.deleted', model: DealLostReason.model_name.human)
  43. end
  44. end
  45. else
  46. respond_to do |format|
  47. format.html do
  48. redirect_to account_settings_deals_deal_lost_reasons_path(Current.account),
  49. flash: { error: @deal_lost_reason.errors.full_messages.to_sentence }
  50. end
  51. end
  52. end
  53. end
  54. 1 private
  55. 1 def set_deal_lost_reason
  56. 4 @deal_lost_reason = DealLostReason.find(params[:id])
  57. end
  58. 1 def deal_lost_reason_params
  59. 4 params.require(:deal_lost_reason).permit(:name)
  60. end
  61. end

app/controllers/accounts/settings/deals_controller.rb

100.0% lines covered

2 relevant lines. 2 lines covered and 0 lines missed.
    
  1. 1 class Accounts::Settings::DealsController < InternalController
  2. 1 def edit
  3. end
  4. end

app/controllers/accounts/settings/webhooks_controller.rb

96.15% lines covered

26 relevant lines. 25 lines covered and 1 lines missed.
    
  1. 1 class Accounts::Settings::WebhooksController < InternalController
  2. 1 before_action :set_webhook, only: %i[edit update destroy]
  3. 1 def index
  4. 1 @webhooks = current_user.account.webhooks
  5. 1 @pagy, @webhooks = pagy(@webhooks)
  6. end
  7. 1 def new
  8. 1 @webhook = Webhook.new
  9. end
  10. 1 def create
  11. 2 @webhook = current_user.account.webhooks.new(webhook_params)
  12. 2 if @webhook.save
  13. 1 redirect_to account_webhooks_path(current_user.account),
  14. notice: t('flash_messages.created', model: Webhook.model_name.human)
  15. else
  16. 1 render :new, status: :unprocessable_entity
  17. end
  18. end
  19. 1 def edit; end
  20. 1 def update
  21. 2 if @webhook.update(webhook_params)
  22. 1 redirect_to edit_account_webhook_path(current_user.account, @webhook),
  23. notice: t('flash_messages.updated', model: Webhook.model_name.human)
  24. else
  25. 1 render :edit, status: :unprocessable_entity
  26. end
  27. end
  28. 1 def destroy
  29. 1 if @webhook.destroy
  30. 1 flash[:notice] = t('flash_messages.deleted', model: Webhook.model_name.human)
  31. else
  32. render :index, status: :unprocessable_entity
  33. end
  34. end
  35. 1 private
  36. 1 def set_webhook
  37. 4 @webhook = current_user.account.webhooks.find(params[:id])
  38. end
  39. 1 def webhook_params
  40. 4 params.require(:webhook).permit(:url, :status)
  41. end
  42. end

app/controllers/accounts/settings_controller.rb

100.0% lines covered

2 relevant lines. 2 lines covered and 0 lines missed.
    
  1. 1 class Accounts::SettingsController < InternalController
  2. 1 def index
  3. end
  4. end

app/controllers/accounts/stages_controller.rb

100.0% lines covered

12 relevant lines. 12 lines covered and 0 lines missed.
    
  1. 1 class Accounts::StagesController < InternalController
  2. 1 before_action :set_stage, only: %i[show]
  3. 1 def show
  4. 7 @filter_status_deal = if params[:filter_status_deal].present?
  5. 4 params[:filter_status_deal]
  6. else
  7. 3 'open'
  8. end
  9. 7 if @filter_status_deal == 'all'
  10. 1 @pagy, @deals = pagy(@stage.deals.order(position: :desc), items: 8)
  11. else
  12. 6 @pagy, @deals = pagy(@stage.deals.where(status: @filter_status_deal).order(position: :desc), items: 8)
  13. end
  14. end
  15. 1 private
  16. 1 def set_stage
  17. 7 @stage = Stage.find(params[:id])
  18. end
  19. end

app/controllers/accounts/stores_controller.rb

100.0% lines covered

5 relevant lines. 5 lines covered and 0 lines missed.
    
  1. 1 class Accounts::StoresController < InternalController
  2. 1 def show
  3. 2 @store_base_url = ENV.fetch('STORE_URL', 'https://store.woofedcrm.com')
  4. 2 @path = params[:path] || ''
  5. 2 @store_url = "#{@store_base_url}/#{@path}"
  6. end
  7. end

app/controllers/accounts/users_controller.rb

100.0% lines covered

35 relevant lines. 35 lines covered and 0 lines missed.
    
  1. 1 class Accounts::UsersController < InternalController
  2. 1 include UserConcern
  3. 1 before_action :set_user, only: %i[edit update destroy hovercard_preview]
  4. 1 def index
  5. 9 @users = if params[:query].present?
  6. 7 User.where(
  7. 'full_name ILIKE :search OR email ILIKE :search OR phone ILIKE :search', search: "%#{params[:query]}%"
  8. ).order(updated_at: :desc)
  9. else
  10. 2 User.all.order(created_at: :desc)
  11. end
  12. 9 @pagy, @users = pagy(@users)
  13. end
  14. 1 def edit; end
  15. 1 def update
  16. 17 params_without_blank_password = user_params.reject { |key, value| value.blank? && key.include?('password') }
  17. 4 if @user.update(params_without_blank_password)
  18. 3 flash[:notice] = t('flash_messages.updated', model: User.model_name.human)
  19. 3 redirect_to edit_account_user_path(current_user.account, @user)
  20. else
  21. 1 render :edit, status: :unprocessable_entity
  22. end
  23. end
  24. 1 def new
  25. 2 @user = User.new
  26. end
  27. 1 def create
  28. 2 @user = current_user.account.users.new(user_params)
  29. 2 if @user.save
  30. 1 redirect_to account_users_path(current_user.account),
  31. notice: t('flash_messages.created', model: User.model_name.human)
  32. else
  33. 1 render :new, status: :unprocessable_entity
  34. end
  35. end
  36. 1 def destroy
  37. 2 if @user.destroy
  38. 2 redirect_to account_users_path(current_user.account),
  39. notice: t('flash_messages.deleted', model: User.model_name.human)
  40. end
  41. end
  42. 1 def select_user_search
  43. 10 @users = if params[:query].present?
  44. 2 User.where(
  45. 'full_name ILIKE :search OR email ILIKE :search OR phone ILIKE :search', search: "%#{params[:query]}%"
  46. ).order(updated_at: :desc).limit(5)
  47. else
  48. 8 User.order(updated_at: :desc).limit(5)
  49. end
  50. end
  51. 1 def hovercard_preview
  52. end
  53. 1 private
  54. 1 def set_user
  55. 10 @user = current_user.account.users.find(params[:id])
  56. end
  57. 1 def user_params
  58. 6 params.require(:user).permit(*permitted_user_params)
  59. end
  60. end

app/controllers/accounts/webpush_subscriptions_controller.rb

100.0% lines covered

6 relevant lines. 6 lines covered and 0 lines missed.
    
  1. 1 class Accounts::WebpushSubscriptionsController < InternalController
  2. 1 def create
  3. 3 webpush_subscription = WebpushSubscription.new(
  4. user: current_user,
  5. endpoint: params[:endpoint],
  6. auth_key: params[:keys][:auth],
  7. p256dh_key: params[:keys][:p256dh]
  8. )
  9. 3 if webpush_subscription.save
  10. 1 render json: webpush_subscription
  11. else
  12. 2 render json: webpush_subscription.errors.full_messages, status: :unprocessable_entity
  13. end
  14. end
  15. end

app/controllers/accounts/welcome_controller.rb

100.0% lines covered

2 relevant lines. 2 lines covered and 0 lines missed.
    
  1. 1 class Accounts::WelcomeController < InternalController
  2. 1 def index
  3. end
  4. end

app/controllers/api/concerns/request_exception_handler.rb

70.37% lines covered

27 relevant lines. 19 lines covered and 8 lines missed.
    
  1. 1 module Api::Concerns::RequestExceptionHandler
  2. 1 extend ActiveSupport::Concern
  3. 1 included do
  4. 1 rescue_from ActiveRecord::RecordInvalid, with: :render_record_invalid
  5. end
  6. 1 private
  7. 1 def handle_with_exception
  8. 63 yield
  9. rescue ActiveRecord::RecordNotFound => e
  10. 10 log_handled_error(e)
  11. 10 render_not_found_error('Resource could not be found')
  12. rescue ActionController::ParameterMissing => e
  13. log_handled_error(e)
  14. render_could_not_create_error(e.message)
  15. ensure
  16. # to address the thread variable leak issues in Puma/Thin webserver
  17. 63 Current.reset
  18. end
  19. 1 def render_unauthorized(message)
  20. render json: { error: message }, status: :unauthorized
  21. end
  22. 1 def render_not_found_error(message)
  23. 10 render json: { error: message }, status: :not_found
  24. end
  25. 1 def render_could_not_create_error(message)
  26. render json: { error: message }, status: :unprocessable_entity
  27. end
  28. 1 def render_payment_required(message)
  29. render json: { error: message }, status: :payment_required
  30. end
  31. 1 def render_internal_server_error(message)
  32. render json: { error: message }, status: :internal_server_error
  33. end
  34. 1 def render_record_invalid(exception)
  35. log_handled_error(exception)
  36. render json: {
  37. message: exception.record.errors.full_messages.join(', '),
  38. attributes: exception.record.errors.attribute_names
  39. }, status: :unprocessable_entity
  40. end
  41. 1 def log_handled_error(exception)
  42. 10 logger.info("Handled error: #{exception.inspect}")
  43. end
  44. end

app/controllers/api/v1/accounts/accounts_controller.rb

93.33% lines covered

15 relevant lines. 14 lines covered and 1 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 class Api::V1::Accounts::AccountsController < Api::V1::InternalController
  3. 1 before_action :set_account, only: %i[show update]
  4. 1 def show
  5. 1 if @account
  6. 1 render json: @account, status: :ok
  7. else
  8. render json: { errors: 'Not found' }, status: :not_found
  9. end
  10. end
  11. 1 def update
  12. 2 if @account.update(account_params)
  13. 1 render json: @account, status: :ok
  14. else
  15. 1 render json: { errors: @account.errors.full_messages }, status: :unprocessable_entity
  16. end
  17. end
  18. 1 private
  19. 1 def set_account
  20. 3 @account = Account.find(params['id'])
  21. end
  22. 1 def account_params
  23. 2 params.permit(:name, :number_of_employees, :segment, :site_url, :woofbot_auto_reply)
  24. end
  25. end

app/controllers/api/v1/accounts/contacts_controller.rb

97.06% lines covered

34 relevant lines. 33 lines covered and 1 lines missed.
    
  1. 1 class Api::V1::Accounts::ContactsController < Api::V1::InternalController
  2. 1 before_action :set_contact, only: %i[show destroy]
  3. 1 def show
  4. 1 render json: @contact, include: %i[deals events], status: :ok
  5. end
  6. 1 def create
  7. 2 @contact = Contact.new(contact_params)
  8. 2 if @contact.save
  9. 1 render json: @contact, status: :created
  10. else
  11. 1 render json: { errors: @contact.errors.full_messages }, status: :unprocessable_entity
  12. end
  13. end
  14. 1 def upsert
  15. 3 existing_contact = Accounts::Contacts::GetByParams.call(Account.first, contact_params.to_h)[:ok]
  16. 3 if existing_contact.nil?
  17. 2 @contact = Contact.new(contact_params)
  18. 2 status = :created
  19. else
  20. 1 @contact = existing_contact
  21. 1 @contact.assign_attributes(contact_params)
  22. 1 status = :ok
  23. end
  24. 3 if @contact.save
  25. 2 render(json: @contact, status:)
  26. else
  27. 1 render json: @contact.errors, status: :unprocessable_entity
  28. end
  29. end
  30. 1 def search
  31. 5 contacts = Contact.ransack(params[:query])
  32. 4 @pagy, @contacts = pagy(contacts.result, metadata: %i[page items count pages from last to prev next])
  33. 4 render json: { data: @contacts,
  34. pagination: pagy_metadata(@pagy) }
  35. rescue ArgumentError => e
  36. 1 render json: {
  37. errors: 'Invalid search parameters',
  38. details: e.message
  39. }, status: :unprocessable_entity
  40. end
  41. 1 def destroy
  42. 1 if @contact.destroy
  43. 1 head :no_content
  44. else
  45. render json: { errors: @contact.errors.full_messages }, status: :unprocessable_entity
  46. end
  47. end
  48. 1 private
  49. 1 def contact_params
  50. 8 params.permit(:full_name, :phone, :email, :label_list,
  51. custom_attributes: {})
  52. end
  53. 1 def set_contact
  54. 4 @contact = Contact.find(params[:id])
  55. end
  56. end

app/controllers/api/v1/accounts/deal_assignees_controller.rb

93.75% lines covered

16 relevant lines. 15 lines covered and 1 lines missed.
    
  1. 1 class Api::V1::Accounts::DealAssigneesController < Api::V1::InternalController
  2. 1 before_action :set_deal_assignee, only: %i[destroy]
  3. 1 def destroy
  4. 1 if @deal_assignee.destroy
  5. 1 head :no_content
  6. else
  7. render json: { errors: @deal_assignee.errors.full_messages }, status: :unprocessable_entity
  8. end
  9. end
  10. 1 def create
  11. 3 @deal_assignee = DealAssignee.new(deal_assignees_params)
  12. 3 if @deal_assignee.save
  13. 1 render json: @deal_assignee, status: :created
  14. else
  15. 2 render json: { errors: @deal_assignee.errors.full_messages }, status: :unprocessable_entity
  16. end
  17. end
  18. 1 private
  19. 1 def deal_assignees_params
  20. 3 params.permit(:user_id, :deal_id)
  21. end
  22. 1 def set_deal_assignee
  23. 2 @deal_assignee = DealAssignee.find(params[:id])
  24. end
  25. end

app/controllers/api/v1/accounts/deal_products_controller.rb

94.74% lines covered

19 relevant lines. 18 lines covered and 1 lines missed.
    
  1. 1 class Api::V1::Accounts::DealProductsController < Api::V1::InternalController
  2. 1 include DealProductConcern
  3. 1 def show
  4. 2 @deal_product = DealProduct.find(params['id'])
  5. 1 if @deal_product
  6. 1 render json: @deal_product, include: %i[product deal], status: :ok
  7. else
  8. render json: { errors: 'Not found' }, status: :not_found
  9. end
  10. end
  11. 1 def create
  12. 2 @deal_product = DealProductBuilder.new(deal_product_params).perform
  13. 2 if DealProduct::CreateOrUpdate.new(@deal_product, {}).call
  14. 1 render json: @deal_product, include: %i[product deal], status: :created
  15. else
  16. 1 render json: { errors: @deal_product.errors.full_messages }, status: :unprocessable_entity
  17. end
  18. end
  19. 1 def update
  20. 3 @deal_product = DealProduct.find(params['id'])
  21. 2 if DealProduct::CreateOrUpdate.new(@deal_product, deal_product_params).call
  22. 1 render json: @deal_product, include: %i[product deal], status: :ok
  23. else
  24. 1 render json: { errors: @deal_product.errors.full_messages }, status: :unprocessable_entity
  25. end
  26. end
  27. 1 def deal_product_params
  28. 4 params.permit(*permitted_deal_product_params)
  29. end
  30. end

app/controllers/api/v1/accounts/deals/events_controller.rb

100.0% lines covered

11 relevant lines. 11 lines covered and 0 lines missed.
    
  1. 1 class Api::V1::Accounts::Deals::EventsController < Api::V1::InternalController
  2. 1 def create
  3. 3 @deal = Deal.find(params['deal_id'])
  4. 2 event = @deal.events.new(event_params)
  5. 2 event.contact = @deal.contact
  6. 2 event.from_me = true
  7. 2 if event.save
  8. 1 render json: event, status: :created
  9. else
  10. 1 render json: { errors: event.errors.full_messages }, status: :unprocessable_entity
  11. end
  12. end
  13. 1 def event_params
  14. 2 params.permit(:content, :send_now, :done, :auto_done, :done_at, :title, :scheduled_at, :kind, :app_type, :app_id,
  15. custom_attributes: {}, additional_attributes: {})
  16. end
  17. end

app/controllers/api/v1/accounts/deals_controller.rb

95.83% lines covered

24 relevant lines. 23 lines covered and 1 lines missed.
    
  1. 1 class Api::V1::Accounts::DealsController < Api::V1::InternalController
  2. 1 include DealConcern
  3. 1 def show
  4. 2 @deal = Deal.find(params['id'])
  5. 1 if @deal
  6. 1 render json: @deal, include: %i[contact stage pipeline deal_assignees deal_products], status: :ok
  7. else
  8. render json: { errors: 'Not found' }, status: :not_found
  9. end
  10. end
  11. 1 def create
  12. 2 @deal = DealBuilder.new(current_user, deal_params).perform
  13. 2 if Deal::CreateOrUpdate.new(@deal, deal_params).call
  14. 1 render json: @deal, status: :created
  15. else
  16. 1 render json: { errors: @deal.errors.full_messages }, status: :unprocessable_entity
  17. end
  18. end
  19. 1 def upsert
  20. 3 @deal = Deal.where(
  21. contact_id: params['contact_id']
  22. ).first_or_initialize
  23. 3 if Deal::CreateOrUpdate.new(@deal, deal_params).call
  24. 2 render json: @deal, status: :ok
  25. else
  26. 1 render json: { errors: @deal.errors.full_messages }, status: :unprocessable_entity
  27. end
  28. end
  29. 1 def update
  30. 6 @deal = Deal.find(params['id'])
  31. 5 if Deal::CreateOrUpdate.new(@deal, deal_params).call
  32. 4 render json: @deal, status: :ok
  33. else
  34. 1 render json: { errors: @deal.errors.full_messages }, status: :unprocessable_entity
  35. end
  36. end
  37. 1 def deal_params
  38. 12 params.permit(*permitted_deal_params)
  39. end
  40. end

app/controllers/api/v1/accounts/products_controller.rb

95.65% lines covered

23 relevant lines. 22 lines covered and 1 lines missed.
    
  1. 1 class Api::V1::Accounts::ProductsController < Api::V1::InternalController
  2. 1 def show
  3. 2 @product = Product.find(params['id'])
  4. 1 if @product
  5. 1 render json: @product, include: %i[deal_products], status: :ok
  6. else
  7. render json: { errors: 'Not found' }, status: :not_found
  8. end
  9. end
  10. 1 def create
  11. 3 @product = Product.new(product_params)
  12. 3 if @product.save
  13. 1 render json: @product, status: :created
  14. else
  15. 2 render json: { errors: @product.errors.full_messages }, status: :unprocessable_entity
  16. end
  17. end
  18. 1 def search
  19. 4 products = Product.ransack(params[:query])
  20. 3 @pagy, @products = pagy(products.result, metadata: %i[page items count pages from last to prev next])
  21. 3 render json: { data: @products,
  22. pagination: pagy_metadata(@pagy) }
  23. rescue ArgumentError => e
  24. 1 render json: {
  25. errors: 'Invalid search parameters',
  26. details: e.message
  27. }, status: :unprocessable_entity
  28. end
  29. 1 def update
  30. 3 @product = Product.find(params['id'])
  31. 2 if @product.update(product_params)
  32. 1 render json: @product, status: :ok
  33. else
  34. 1 render json: { errors: @product.errors.full_messages }, status: :unprocessable_entity
  35. end
  36. end
  37. 1 def product_params
  38. 5 params.permit(:identifier, :amount_in_cents, :quantity_available, :description, :name, attachments_attributes: %i[file _destroy id],
  39. custom_attributes: {}, additional_attributes: {})
  40. end
  41. end

app/controllers/api/v1/accounts/users_controller.rb

100.0% lines covered

15 relevant lines. 15 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 class Api::V1::Accounts::UsersController < Api::V1::InternalController
  3. 1 include UserConcern
  4. 1 def search
  5. 4 users = User.ransack(params[:query])
  6. 3 @pagy, @users = pagy(users.result, metadata: %i[page items count pages from last to prev next])
  7. 3 render json: { data: @users,
  8. pagination: pagy_metadata(@pagy) }
  9. rescue ArgumentError => e
  10. 1 render json: {
  11. errors: 'Invalid search parameters',
  12. details: e.message
  13. }, status: :unprocessable_entity
  14. end
  15. 1 def create
  16. 2 @user = User.new(user_params)
  17. 2 if @user.save
  18. 1 render json: @user, status: :created
  19. else
  20. 1 render json: { errors: @user.errors.full_messages }, status: :unprocessable_entity
  21. end
  22. end
  23. 1 private
  24. 1 def user_params
  25. 2 params.permit(*permitted_user_params)
  26. end
  27. end

app/controllers/api/v1/contacts_controller.rb

37.5% lines covered

8 relevant lines. 3 lines covered and 5 lines missed.
    
  1. 1 class Api::V1::ContactsController < Api::V1::InternalController
  2. 1 def create
  3. @contact = Contact.new(contact_params)
  4. if @contact.save
  5. render json: @contact, status: :created
  6. else
  7. render json: { errors: @contact.errors.full_messages }, status: :unprocessable_entity
  8. end
  9. end
  10. 1 def contact_params
  11. params.permit(:full_name, :phone, :email)
  12. end
  13. end

app/controllers/api/v1/internal_controller.rb

100.0% lines covered

12 relevant lines. 12 lines covered and 0 lines missed.
    
  1. 1 class Api::V1::InternalController < ActionController::API
  2. 1 include Pagy::Backend
  3. 1 include Api::Concerns::RequestExceptionHandler
  4. 1 before_action :authenticate_user
  5. 1 around_action :handle_with_exception, unless: :devise_controller?
  6. 1 def authenticate_user
  7. 86 header = request.headers['Authorization']
  8. 86 header = header.split(' ').last if header
  9. begin
  10. 86 decoded = Users::JsonWebToken.decode_user(header)
  11. 86 @current_user = decoded[:ok]
  12. 86 @current_account = @current_user.account
  13. rescue
  14. 23 render json: { errors: 'Unauthorized' }, status: :unauthorized
  15. end
  16. end
  17. end

app/controllers/api/v1/public_controller.rb

100.0% lines covered

1 relevant lines. 1 lines covered and 0 lines missed.
    
  1. 1 class Api::V1::PublicController < ActionController::API
  2. end

app/controllers/application_controller.rb

60.0% lines covered

15 relevant lines. 9 lines covered and 6 lines missed.
    
  1. 1 class ApplicationController < ActionController::Base
  2. 1 include Localized
  3. 1 include Pagy::Backend
  4. 1 if ENV['HIGHLIGHT_PROJECT_ID'].present?
  5. require 'highlight'
  6. include Highlight::Integrations::Rails
  7. around_action :with_highlight_context
  8. end
  9. 1 before_action :set_account
  10. 1 before_action :setup_installation if Installation.installation_flow?
  11. 1 private
  12. 1 def setup_installation
  13. if Installation.installation_flow? && !request.path.include?('/installation')
  14. redirect_to installation_new_path and return
  15. end
  16. end
  17. 1 def set_account
  18. @account = Current.account
  19. end
  20. end

app/controllers/apps/chatwoots_controller.rb

67.74% lines covered

31 relevant lines. 21 lines covered and 10 lines missed.
    
  1. 1 class Apps::ChatwootsController < ActionController::Base
  2. 1 before_action :load_chatwoot
  3. 1 before_action :authenticate_by_token, if: :check_user_authentication
  4. 1 skip_before_action :verify_authenticity_token, except: :embedding
  5. 1 layout 'embed'
  6. 1 def webhooks
  7. 2 return render json: { error: 'Chatwoot is inactive' }, status: :unprocessable_entity if @chatwoot.inactive?
  8. 1 Accounts::Apps::Chatwoots::Webhooks::ProcessWebhookJob.perform_later(params.to_json, @chatwoot.account_id)
  9. 1 render json: { ok: true }, status: 200
  10. end
  11. 1 def embedding
  12. end
  13. 1 def embedding_init_authenticate
  14. @token = params['token']
  15. end
  16. 1 def embedding_authenticate
  17. event = JSON.parse(params['event'])
  18. @user_email = event['data']['currentAgent']['email']
  19. user = User.find_by(email: @user_email)
  20. return render 'user_not_found', status: 400 if user.blank?
  21. sign_out_all_scopes
  22. sign_in(user)
  23. redirect_to embedding_apps_chatwoots_path(token: params['token'])
  24. end
  25. 1 private
  26. 1 def check_user_authentication
  27. 3 User.find_by_id(current_user&.id).blank?
  28. end
  29. 1 def authenticate_by_token
  30. 3 if @chatwoot.present? && action_name == 'embedding'
  31. if action_name != 'embedding_authenticate'
  32. redirect_to embedding_init_authenticate_apps_chatwoots_path(token: params['token'])
  33. end
  34. 3 elsif @chatwoot.blank?
  35. 1 render plain: 'Unauthorized', status: 400
  36. end
  37. end
  38. 1 def load_chatwoot
  39. 3 @chatwoot = Apps::Chatwoot.find_by(embedding_token: params['token'])
  40. end
  41. end

app/controllers/apps/evolution_apis_controller.rb

100.0% lines covered

4 relevant lines. 4 lines covered and 0 lines missed.
    
  1. 1 class Apps::EvolutionApisController < Api::V1::PublicController
  2. 1 def webhooks
  3. 18 Accounts::Apps::EvolutionApis::Webhooks::ProcessWebhookWorker.perform_async(params.to_json)
  4. 18 render json: { ok: true }, status: 200
  5. end
  6. end

app/controllers/concerns/account_concern.rb

100.0% lines covered

3 relevant lines. 3 lines covered and 0 lines missed.
    
  1. 1 module AccountConcern
  2. 1 def permitted_account_params
  3. 6 %i[name number_of_employees segment site_url woofbot_auto_reply currency_code deal_free_form_lost_reasons deal_allow_edit_lost_at_won_at]
  4. end
  5. end

app/controllers/concerns/deal_concern.rb

100.0% lines covered

3 relevant lines. 3 lines covered and 0 lines missed.
    
  1. 1 module DealConcern
  2. 1 def permitted_deal_params
  3. [
  4. 41 :name,
  5. :status,
  6. :stage_id,
  7. :pipeline_id,
  8. :contact_id,
  9. :position,
  10. :lost_reason,
  11. :lost_at,
  12. :won_at,
  13. { contact_attributes: %i[id full_name phone email] },
  14. { custom_attributes: {} }
  15. ]
  16. end
  17. end

app/controllers/concerns/deal_product_concern.rb

100.0% lines covered

3 relevant lines. 3 lines covered and 0 lines missed.
    
  1. 1 module DealProductConcern
  2. 1 def permitted_deal_product_params
  3. 14 %i[product_id deal_id quantity unit_amount_in_cents product_name product_identifier]
  4. end
  5. end

app/controllers/concerns/localized.rb

100.0% lines covered

17 relevant lines. 17 lines covered and 0 lines missed.
    
  1. 1 module Localized
  2. 1 extend ActiveSupport::Concern
  3. 1 included do
  4. 1 around_action :set_locale
  5. 1 before_action :set_time_zone
  6. end
  7. 1 def set_locale(&block)
  8. 428 I18n.with_locale(requested_locale || I18n.default_locale, &block)
  9. end
  10. 1 private
  11. 1 def requested_locale
  12. 428 if respond_to?(:user_signed_in?) && user_signed_in?
  13. 295 requested_locale_name ||= available_locale_or_nil(current_user.language)
  14. end
  15. 428 requested_locale_name
  16. end
  17. 1 def available_locale_or_nil(locale_name)
  18. 295 locale_name.to_sym if locale_name.present? && I18n.available_locales.map(&:to_s).include?(locale_name.to_s)
  19. end
  20. 1 def set_time_zone
  21. 428 browser_timezone = cookies[:browser_timezone].presence || ENV.fetch('DEFAULT_TIMEZONE', 'Brasilia')
  22. 428 Time.zone = (browser_timezone if ActiveSupport::TimeZone[browser_timezone])
  23. end
  24. end

app/controllers/concerns/product_concern.rb

100.0% lines covered

3 relevant lines. 3 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ProductConcern
  3. 1 def product_params
  4. 9 params.require(:product).permit(:identifier, :amount_in_cents, :quantity_available, :description, :name, attachments_attributes: %i[file _destroy id],
  5. custom_attributes: {}, additional_attributes: {})
  6. end
  7. end

app/controllers/concerns/user_concern.rb

100.0% lines covered

3 relevant lines. 3 lines covered and 0 lines missed.
    
  1. 1 module UserConcern
  2. 1 def permitted_user_params
  3. 26 %i[email password password_confirmation full_name phone language avatar_url job_description
  4. webpush_notify_on_event_expired]
  5. end
  6. end

app/controllers/embedded/accounts/apps/chatwoots_controller.rb

66.67% lines covered

3 relevant lines. 2 lines covered and 1 lines missed.
    
  1. 1 class Embedded::Accounts::Apps::ChatwootsController < Embedded::InternalController
  2. 1 def index
  3. #redirect_to account_contact_note_path(@current_account, @current_account.contacts.first)
  4. redirect_to root_path
  5. end
  6. end

app/controllers/embedded/internal_controller.rb

33.33% lines covered

9 relevant lines. 3 lines covered and 6 lines missed.
    
  1. 1 class Embedded::InternalController < ApplicationController
  2. 1 before_action :authenticate_app
  3. 1 def authenticate_app
  4. token = params['token']
  5. begin
  6. @chatwoot = Apps::Chatwoot.find_by_embedding_token(token)
  7. @current_account = @chatwoot.account
  8. @current_user = @current_account.users.first
  9. rescue ActiveRecord::RecordNotFound => e
  10. render json: { errors: e.message }, status: :unauthorized
  11. rescue JWT::DecodeError => e
  12. render json: { errors: e.message }, status: :unauthorized
  13. end
  14. end
  15. end

app/controllers/installation_controller.rb

100.0% lines covered

50 relevant lines. 50 lines covered and 0 lines missed.
    
  1. 1 class InstallationController < ApplicationController
  2. 1 include AccountConcern
  3. 1 include UserConcern
  4. 1 before_action :authenticate_user!, except: %i[new create]
  5. 1 before_action :set_user, except: %i[new create]
  6. 1 before_action :set_account, only: %i[step_3 update_step_3]
  7. 1 layout 'devise'
  8. 1 def new
  9. end
  10. 1 def step_1
  11. end
  12. 1 def update_step_1
  13. 3 if @user.update(user_params)
  14. 2 redirect_to installation_step_2_path
  15. else
  16. 1 render :step_1, status: :unprocessable_entity
  17. end
  18. end
  19. 1 def step_2
  20. end
  21. 1 def update_step_2
  22. 2 if @user.update(user_params)
  23. 1 bypass_sign_in(@user)
  24. 1 redirect_to installation_step_3_path
  25. else
  26. 1 render :step_2, status: :unprocessable_entity
  27. end
  28. end
  29. 1 def step_3
  30. end
  31. 1 def update_step_3
  32. 4 if @account.update(account_params)
  33. 2 redirect_to installation_loading_path
  34. else
  35. 2 render :step_3, status: :unprocessable_entity
  36. end
  37. end
  38. 1 def loading
  39. 1 Installation.first.complete_installation!
  40. end
  41. 1 def create
  42. 7 installation = Installation.first_or_initialize
  43. 7 user = User.find_or_initialize_by(user_params.slice('email'))
  44. 7 installation.assign_attributes(installation_params)
  45. 6 user.assign_attributes(user_params)
  46. 6 user.password = SecureRandom.hex(8)
  47. 6 installation.user = user
  48. 6 ActiveRecord::Base.transaction do
  49. 6 installation.save!
  50. 6 user.save!
  51. end
  52. 5 sign_in(user)
  53. 5 redirect_to installation_step_1_path
  54. rescue ActiveRecord::RecordInvalid, ActionController::ParameterMissing
  55. 2 render :new, status: :unprocessable_entity
  56. end
  57. 1 private
  58. 1 def set_user
  59. 15 @user = current_user
  60. end
  61. 1 def set_account
  62. 25 @account = Account.first_or_initialize
  63. end
  64. 1 def installation_params
  65. 7 params.require(:installation).permit(:id, :key1, :key2, :token)
  66. end
  67. 1 def user_params
  68. 18 params.require(:user).permit(*permitted_user_params)
  69. end
  70. 1 def account_params
  71. 4 params.require(:account).permit(*permitted_account_params)
  72. end
  73. end

app/controllers/internal_controller.rb

100.0% lines covered

6 relevant lines. 6 lines covered and 0 lines missed.
    
  1. 1 class InternalController < ApplicationController
  2. 1 before_action :sign_in_preview_env
  3. 1 before_action :authenticate_user!
  4. 1 layout "internal"
  5. 1 def sign_in_preview_env
  6. 390 sign_in User.first if ENV['PREVIEW_APP'].present? && current_user.blank?
  7. end
  8. end

app/controllers/pwa_controller.rb

100.0% lines covered

4 relevant lines. 4 lines covered and 0 lines missed.
    
  1. 1 class PwaController < ApplicationController
  2. 1 skip_forgery_protection
  3. # We need a stable URL at the root, so we can't use the regular asset path here.
  4. 1 def service_worker; end
  5. # Need ERB interpolation for paths, so can't use asset path here either.
  6. 1 def manifest; end
  7. end

app/controllers/settings_controller.rb

100.0% lines covered

2 relevant lines. 2 lines covered and 0 lines missed.
    
  1. 1 class SettingsController < InternalController
  2. 1 def index
  3. end
  4. end

app/controllers/users/registrations_controller.rb

100.0% lines covered

5 relevant lines. 5 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 class Users::RegistrationsController < Devise::RegistrationsController
  3. 1 before_action :configure_permitted_parameters
  4. 1 protected
  5. 1 def configure_permitted_parameters
  6. 5 devise_parameter_sanitizer.permit(:sign_up,
  7. keys: [:full_name, :email, :phone, :password, :password_confirmation,
  8. { account_attributes: %i[name site_url] }])
  9. end
  10. end

app/helpers/advanced_search_helper.rb

100.0% lines covered

3 relevant lines. 3 lines covered and 0 lines missed.
    
  1. 1 module AdvancedSearchHelper
  2. 1 def search_types
  3. [
  4. 2 { key: :contacts, type: :contact, value: 'contact', icon: 'contact',
  5. label: t('activerecord.models.contact.other') },
  6. { key: :deals, type: :deal, value: 'deal', icon: 'clipboard-list', label: t('activerecord.models.deal.other') },
  7. { key: :products, type: :product, value: 'product', icon: 'box', label: t('activerecord.models.product.other') },
  8. { key: :pipelines, type: :pipeline, value: 'pipeline', icon: 'funnel',
  9. label: t('activerecord.models.pipeline.other') },
  10. { key: :activities, type: :activity, value: 'activity', icon: 'calendar-check-2',
  11. label: Event.human_enum_name(:kind, :activity).pluralize }
  12. ]
  13. end
  14. end

app/helpers/application_helper.rb

91.67% lines covered

12 relevant lines. 11 lines covered and 1 lines missed.
    
  1. 1 module ApplicationHelper
  2. 1 include Pagy::Frontend
  3. 1 def embedded_svg(filename, options = {})
  4. 74 assets = Rails.application.assets
  5. 74 asset = assets.find_asset(filename)
  6. 74 if asset
  7. 74 file = asset.source.force_encoding("UTF-8")
  8. 74 doc = Nokogiri::HTML::DocumentFragment.parse file
  9. 74 svg = doc.at_css "svg"
  10. 74 svg["class"] = options[:class] if options[:class].present?
  11. else
  12. doc = "<!-- SVG #{filename} not found -->"
  13. end
  14. 74 raw doc
  15. end
  16. end

app/helpers/date_range_helper.rb

100.0% lines covered

8 relevant lines. 8 lines covered and 0 lines missed.
    
  1. ##############################################
  2. # Helpers to implement date range filtering to APIs
  3. # Include in your controller or service class where params is available
  4. ##############################################
  5. 1 module DateRangeHelper
  6. 1 def range
  7. 18 return if params[:since].blank? || params[:until].blank?
  8. 18 parse_date_time(params[:since])...parse_date_time(params[:until])
  9. end
  10. 1 def parse_date_time(datetime)
  11. 36 return datetime if datetime.is_a?(DateTime)
  12. 36 return datetime.to_datetime if datetime.is_a?(Time) || datetime.is_a?(Date)
  13. 36 DateTime.strptime(datetime, '%s')
  14. end
  15. end

app/helpers/deals_helper.rb

100.0% lines covered

1 relevant lines. 1 lines covered and 0 lines missed.
    
  1. 1 module DealsHelper
  2. end

app/helpers/timezone_helper.rb

100.0% lines covered

7 relevant lines. 7 lines covered and 0 lines missed.
    
  1. 1 module TimezoneHelper
  2. # ActiveSupport TimeZone is not aware of the current time, so ActiveSupport::Timezone[offset]
  3. # would return the timezone without considering day light savings. To get the correct timezone,
  4. # this method uses zone.now.utc_offset for comparison as referenced in the issues below
  5. #
  6. # https://github.com/rails/rails/pull/22243
  7. # https://github.com/rails/rails/issues/21501
  8. # https://github.com/rails/rails/issues/7297
  9. 1 def timezone_name_from_offset(offset)
  10. 11 return 'UTC' if offset.blank?
  11. 11 offset_in_seconds = offset.to_f * 3600
  12. 11 matching_zone = ActiveSupport::TimeZone.all.find do |zone|
  13. 362 zone.now.utc_offset == offset_in_seconds
  14. end
  15. 11 matching_zone.name if matching_zone
  16. end
  17. end

app/jobs/application_job.rb

100.0% lines covered

1 relevant lines. 1 lines covered and 0 lines missed.
    
  1. 1 class ApplicationJob < ActiveJob::Base
  2. # Automatically retry jobs that encountered a deadlock
  3. # retry_on ActiveRecord::Deadlocked
  4. # Most jobs are safe to ignore if the underlying records are no longer available
  5. # discard_on ActiveJob::DeserializationError
  6. end

app/jobs/apps/chatwoot/connection/refresh_job.rb

60.0% lines covered

5 relevant lines. 3 lines covered and 2 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 class Apps::Chatwoot::Connection::RefreshJob < ApplicationJob
  3. 1 self.queue_adapter = :good_job
  4. 1 def perform
  5. Apps::Chatwoot.active.find_each do |chatwoot_app|
  6. Apps::Chatwoot::Connection::Refresh.new(chatwoot_app).call
  7. end
  8. end
  9. end

app/jobs/webhook/status/refresh_job.rb

50.0% lines covered

6 relevant lines. 3 lines covered and 3 lines missed.
    
  1. 1 class Webhook::Status::RefreshJob < ApplicationJob
  2. 1 self.queue_adapter = :good_job
  3. 1 def perform
  4. Webhook.active.find_each do |webhook|
  5. next if webhook.valid_url?
  6. webhook.inactive!
  7. end
  8. end
  9. end

app/listeners/webhook_listener.rb

92.5% lines covered

40 relevant lines. 37 lines covered and 3 lines missed.
    
  1. 1 class WebhookListener
  2. 1 def extract_changed_attributes(event)
  3. 5 changed_attributes = event.previous_changes
  4. 5 return nil if changed_attributes.blank?
  5. 39 changed_attributes.map { |k, v| { k => { previous_value: v[0], current_value: v[1] } } }
  6. end
  7. ## Contact
  8. 1 def contact_updated(contact)
  9. 25 Webhook.active.find_each do | wh |
  10. WebhookWorker.perform_async(wh.url, build_contact_payload( 'contact_updated', contact))
  11. end
  12. end
  13. 1 def contact_created(contact)
  14. 808 if Webhook.active.present?
  15. 1 Webhook.active.find_each do | wh |
  16. 1 WebhookWorker.perform_async(wh.url, build_contact_payload( 'contact_created', contact))
  17. end
  18. end
  19. end
  20. ## Deal
  21. 1 def deal_updated(deal)
  22. 35 if Webhook.active.present?
  23. 1 Webhook.active.find_each do | wh |
  24. 1 WebhookWorker.perform_async(wh.url, build_deal_payload( 'deal_updated', deal))
  25. end
  26. end
  27. end
  28. 1 def deal_created(deal)
  29. 481 if Webhook.active.present?
  30. 1 Webhook.active.find_each do | wh |
  31. 1 WebhookWorker.perform_async(wh.url, build_deal_payload( 'deal_created', deal))
  32. end
  33. end
  34. end
  35. 1 def build_deal_payload(event, deal)
  36. 2 changed_attributes = extract_changed_attributes(deal)
  37. 2 deal_json = deal.as_json(:include => :contact).merge({changed_attributes: changed_attributes})
  38. 2 { event: event, data: deal_json }.to_json
  39. end
  40. 1 def build_contact_payload(event, contact)
  41. 1 changed_attributes = extract_changed_attributes(contact)
  42. 1 contact_json = contact.as_json(:include => :deals).merge({changed_attributes: changed_attributes})
  43. 1 { event: event, data: contact_json }.to_json
  44. end
  45. ## Events
  46. 1 def event_created(event)
  47. 749 if Webhook.active.present?
  48. 2 Webhook.active.find_each do | wh |
  49. 2 WebhookWorker.perform_async(wh.url, build_event_payload( 'event_created', event))
  50. end
  51. end
  52. end
  53. 1 def event_updated(event)
  54. 25 if Webhook.active.present?
  55. Webhook.active.find_each do | wh |
  56. WebhookWorker.perform_async(wh.url, build_event_payload( 'event_updated', event))
  57. end
  58. end
  59. end
  60. 1 def build_event_payload(event, event_model)
  61. 2 changed_attributes = extract_changed_attributes(event_model)
  62. 2 event_json = event_model.as_json(include: %i[deal contact],
  63. methods: :content).merge({ changed_attributes: changed_attributes })
  64. 2 { event: event, data: event_json }.to_json
  65. end
  66. end

app/listeners/woofbot_listener.rb

100.0% lines covered

3 relevant lines. 3 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 class WoofbotListener
  3. 1 def event_created(event)
  4. 749 Accounts::Contacts::Events::WoofbotWorker.perform_async(event.id) if Apps::AiAssistent.first&.auto_reply?
  5. end
  6. end

app/mailers/application_mailer.rb

100.0% lines covered

3 relevant lines. 3 lines covered and 0 lines missed.
    
  1. 1 class ApplicationMailer < ActionMailer::Base
  2. 1 default from: 'from@example.com'
  3. 1 layout 'mailer'
  4. end

app/models/account.rb

90.91% lines covered

44 relevant lines. 40 lines covered and 4 lines missed.
    
  1. # == Schema Information
  2. #
  3. # Table name: accounts
  4. #
  5. # id :bigint not null, primary key
  6. # ai_usage :jsonb not null
  7. # currency_code :string default("BRL"), not null
  8. # name :string default(""), not null
  9. # number_of_employees :string default("1-10"), not null
  10. # segment :string default("other"), not null
  11. # settings :jsonb not null
  12. # site_url :string default(""), not null
  13. # woofbot_auto_reply :boolean default(FALSE), not null
  14. # created_at :datetime not null
  15. # updated_at :datetime not null
  16. #
  17. 1 class Account < ApplicationRecord
  18. 1 include Account::Settings
  19. 1 validates :name, presence: true
  20. 1 validates :name, length: { maximum: 255 }
  21. 1 validates :currency_code, presence: true, inclusion: { in: Money::Currency.table.keys.map(&:to_s).map(&:upcase) }
  22. 1 enum segment: {
  23. technology: 'technology',
  24. health: 'health',
  25. finance: 'finance',
  26. education: 'education',
  27. retail: 'retail',
  28. services: 'services',
  29. manufacturing: 'manufacturing',
  30. telecommunications: 'telecommunications',
  31. transportation_logistics: 'transportation_logistics',
  32. real_estate: 'real_estate',
  33. energy: 'energy',
  34. agriculture: 'agriculture',
  35. tourism_hospitality: 'tourism_hospitality',
  36. entertainment_media: 'entertainment_media',
  37. construction: 'construction',
  38. public_sector: 'public_sector',
  39. consulting: 'consulting',
  40. startup: 'startup',
  41. ecommerce: 'ecommerce',
  42. security: 'security',
  43. automotive: 'automotive',
  44. other: 'other'
  45. }
  46. 1 enum number_of_employees: {
  47. '1-10' => '1-10',
  48. '11-50' => '11-50',
  49. '51-200' => '51-200',
  50. '201-500' => '201-500',
  51. '501+' => '501+'
  52. }
  53. 1 def events
  54. 39 Event.all
  55. end
  56. 1 def apps
  57. App.all
  58. end
  59. 1 def users
  60. 13 User.all
  61. end
  62. 1 def contacts
  63. 163 Contact.all
  64. end
  65. 1 def deals
  66. 70 Deal.all
  67. end
  68. 1 def custom_attribute_definitions
  69. 23 CustomAttributeDefinition.all
  70. end
  71. 1 def custom_attributes_definitions
  72. 17 custom_attribute_definitions
  73. end
  74. 1 def apps_wpp_connects
  75. Apps::WppConnect.all
  76. end
  77. 1 def apps_chatwoots
  78. 907 Apps::Chatwoot.all
  79. end
  80. 1 def apps_evolution_apis
  81. 14 Apps::EvolutionApi.all
  82. end
  83. 1 def webhooks
  84. 8 Webhook.all
  85. end
  86. 1 def stages
  87. Stage.all
  88. end
  89. 1 def products
  90. 26 Product.all
  91. end
  92. 1 def embedding_documments
  93. 3 EmbeddingDocumment.all
  94. end
  95. 1 def deal_products
  96. 3 DealProduct.all
  97. end
  98. 1 def apps_ai_assistents
  99. Apps::AiAssistent.all
  100. end
  101. 1 def site_url=(url)
  102. 931 super(normalize_url(url))
  103. end
  104. 1 def normalize_url(url)
  105. 931 url = "https://#{url}" unless url.match?(%r{\Ahttp(s)?://})
  106. 931 url
  107. end
  108. end

app/models/app.rb

100.0% lines covered

2 relevant lines. 2 lines covered and 0 lines missed.
    
  1. # == Schema Information
  2. #
  3. # Table name: apps
  4. #
  5. # id :bigint not null, primary key
  6. # active :boolean default(FALSE), not null
  7. # kind :string
  8. # name :string
  9. # settings :jsonb not null
  10. # created_at :datetime not null
  11. # updated_at :datetime not null
  12. #
  13. 1 class App < ApplicationRecord
  14. 1 enum kind: { 'wpp_connect': 'wpp_connect' }
  15. end

app/models/application_record.rb

100.0% lines covered

7 relevant lines. 7 lines covered and 0 lines missed.
    
  1. 1 class ApplicationRecord < ActiveRecord::Base
  2. 1 include Applicable
  3. 1 self.abstract_class = true
  4. 1 def self.human_enum_name(enum_name, enum_value)
  5. 462 I18n.t("activerecord.attributes.#{model_name
  6. .i18n_key}.#{enum_name.to_s.pluralize}.#{enum_value}")
  7. end
  8. 1 def sanitize_amount(amount)
  9. 167 amount.is_a?(String) ? amount.gsub(/[^\d-]/, '').to_i : amount
  10. end
  11. end

app/models/apps.rb

100.0% lines covered

3 relevant lines. 3 lines covered and 0 lines missed.
    
  1. 1 module Apps
  2. 1 def self.table_name_prefix
  3. 3 'apps_'
  4. end
  5. end

app/models/apps/ai_assistent.rb

100.0% lines covered

9 relevant lines. 9 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. # == Schema Information
  3. #
  4. # Table name: apps_ai_assistents
  5. #
  6. # id :bigint not null, primary key
  7. # api_key :string default(""), not null
  8. # auto_reply :boolean default(FALSE), not null
  9. # enabled :boolean default(FALSE), not null
  10. # model :string default("gpt-4o"), not null
  11. # usage :jsonb not null
  12. # created_at :datetime not null
  13. # updated_at :datetime not null
  14. #
  15. 1 class Apps::AiAssistent < ApplicationRecord
  16. 1 validates :model, presence: true
  17. 1 validates :api_key, presence: true, if: :enabled?
  18. 10 after_update :embed_company_site, if: -> { saved_change_to_enabled? || saved_change_to_api_key? }
  19. 1 def embed_company_site
  20. 4 Accounts::Create::EmbedCompanySiteJob.perform_later(id) if Current.account.site_url.present? && enabled?
  21. end
  22. 1 def exceeded_usage_limit?
  23. 9 return false if usage['limit'].blank?
  24. 4 usage['tokens'] >= usage['limit']
  25. end
  26. end

app/models/apps/chatwoot.rb

86.67% lines covered

60 relevant lines. 52 lines covered and 8 lines missed.
    
  1. # == Schema Information
  2. #
  3. # Table name: apps_chatwoots
  4. #
  5. # id :bigint not null, primary key
  6. # chatwoot_endpoint_url :string default(""), not null
  7. # chatwoot_user_token :string default(""), not null
  8. # embedding_token :string default(""), not null
  9. # inboxes :jsonb not null
  10. # name :string
  11. # status :string default("active"), not null
  12. # created_at :datetime not null
  13. # updated_at :datetime not null
  14. # chatwoot_account_id :integer not null
  15. # chatwoot_dashboard_app_id :integer not null
  16. # chatwoot_webhook_id :integer not null
  17. #
  18. 1 class Apps::Chatwoot < ApplicationRecord
  19. 1 scope :actives, -> { where(active: true) }
  20. 126 normalizes :chatwoot_endpoint_url, with: ->(value) { value&.gsub(/\/+\z/, '') }
  21. 1 enum status: {
  22. 'inactive': 'inactive',
  23. 'active': 'active',
  24. 'sync': 'sync',
  25. 'pair': 'pair'
  26. }
  27. 1 validate :validate_chatwoot, on: :create
  28. 1 before_destroy :chatwoot_delete_flow
  29. 1 def request_headers
  30. 204 { 'api_access_token': chatwoot_user_token.to_s, 'Content-Type': 'application/json' }
  31. end
  32. 1 def validate_chatwoot
  33. 2 if invalid_token?
  34. 1 errors.add(:chatwoot_user_token, I18n.t('activerecord.errors.messages.invalid_chatwoot_token'))
  35. 1 return
  36. end
  37. 1 chatwoot_create_flow
  38. 1 if chatwoot_dashboard_app_id.blank? || chatwoot_webhook_id.blank?
  39. errors.add(:chatwoot_endpoint_url, I18n.t('activerecord.errors.messages.invalid_chatwoot_configuration'))
  40. errors.add(:chatwoot_user_token, I18n.t('activerecord.errors.messages.invalid_chatwoot_configuration'))
  41. end
  42. end
  43. 1 def invalid_token?
  44. 2 !valid_token?
  45. end
  46. 1 def valid_token?
  47. 6 return false if chatwoot_account_is_suspended?
  48. 2 response = Apps::Chatwoot::ApiClient.new(self).user_profile
  49. 2 return false if response[:error].present?
  50. 2 account = response[:ok]['accounts'].select do |acc|
  51. 4 acc['id'] == chatwoot_account_id
  52. end
  53. 2 return true if account&.dig(0, 'role') == 'administrator'
  54. false
  55. rescue
  56. 1 false
  57. end
  58. 1 def chatwoot_create_flow
  59. 1 self.embedding_token = generate_token
  60. 1 dashboard_apps_response = Faraday.post(
  61. "#{chatwoot_endpoint_url}/api/v1/accounts/#{chatwoot_account_id}/dashboard_apps",
  62. {
  63. "title": 'WoofedCRM',
  64. "content": [{ "type": 'frame', "url": woofedcrm_embedding_url }]
  65. }.to_json,
  66. { 'api_access_token': chatwoot_user_token.to_s, 'Content-Type': 'application/json' }
  67. )
  68. 1 webhook_response = Faraday.post(
  69. "#{chatwoot_endpoint_url}/api/v1/accounts/#{chatwoot_account_id}/webhooks",
  70. {
  71. "webhook": {
  72. "url": woofedcrm_webhooks_url,
  73. "subscriptions": %w[
  74. contact_created
  75. contact_updated
  76. conversation_created
  77. conversation_status_changed
  78. conversation_updated
  79. message_created
  80. message_updated
  81. webwidget_triggered
  82. ]
  83. }
  84. }.to_json,
  85. { 'api_access_token': chatwoot_user_token.to_s, 'Content-Type': 'application/json' }
  86. )
  87. 1 self.inboxes = Accounts::Apps::Chatwoots::GetInboxes.call(self)[:ok]
  88. 1 if dashboard_apps_response.status == 200 && webhook_response.status == 200
  89. 1 dashboard_apps_body = JSON.parse(dashboard_apps_response.body)
  90. 1 webhook_body = JSON.parse(webhook_response.body)
  91. 1 self.chatwoot_dashboard_app_id = dashboard_apps_body['id']
  92. 1 self.chatwoot_webhook_id = webhook_body['payload']['webhook']['id']
  93. 1 true
  94. else
  95. false
  96. end
  97. rescue Exception => e
  98. Rails.logger.error('Chatwoot connection error')
  99. Rails.logger.error(e.inspect)
  100. false
  101. end
  102. 1 def chatwoot_delete_flow
  103. 1 dashboard_apps_response = Faraday.delete(
  104. "#{chatwoot_endpoint_url}/api/v1/accounts/#{chatwoot_account_id}/dashboard_apps/#{chatwoot_dashboard_app_id}",
  105. {},
  106. { 'api_access_token': chatwoot_user_token.to_s, 'Content-Type': 'application/json' }
  107. )
  108. 1 webhook_response = Faraday.delete(
  109. "#{chatwoot_endpoint_url}/api/v1/accounts/#{chatwoot_account_id}/webhooks/#{chatwoot_webhook_id}",
  110. {},
  111. { 'api_access_token': chatwoot_user_token.to_s, 'Content-Type': 'application/json' }
  112. )
  113. 1 true
  114. rescue StandardError
  115. true
  116. end
  117. 1 def chatwoot_account_is_suspended?
  118. 6 response = Accounts::Apps::Chatwoots::GetInboxes.call(self)
  119. 4 response.key?(:error) && JSON.parse(response[:error]) == {"error"=>"Account is suspended"}
  120. rescue Faraday::TimeoutError, Faraday::ConnectionFailed
  121. 1 false
  122. end
  123. 1 private
  124. 1 def woofedcrm_webhooks_url
  125. 1 "#{ENV['FRONTEND_URL']}/apps/chatwoots/webhooks?token=#{embedding_token}"
  126. end
  127. 1 def woofedcrm_embedding_url
  128. 1 "#{ENV['FRONTEND_URL']}/apps/chatwoots/embedding?token=#{embedding_token}"
  129. end
  130. 1 def generate_token
  131. 1 loop do
  132. 1 token = SecureRandom.hex(10)
  133. 1 break token unless Apps::Chatwoot.where(embedding_token: token).exists?
  134. end
  135. end
  136. end

app/models/apps/chatwoot/api_client.rb

100.0% lines covered

23 relevant lines. 23 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 class Apps::Chatwoot::ApiClient
  3. 1 include Apps::Chatwoot::ApiClient::UserProfile
  4. 1 def initialize(chatwoot)
  5. 7 @chatwoot = chatwoot
  6. 7 @request_headers = chatwoot.request_headers
  7. 7 @connection = create_connection
  8. end
  9. 1 def create_connection
  10. retry_options = {
  11. 7 max: 5,
  12. interval: 0.05,
  13. interval_randomness: 0.5,
  14. backoff_factor: 2,
  15. exceptions: [
  16. Faraday::ConnectionFailed,
  17. Faraday::TimeoutError,
  18. 'Timeout::Error'
  19. ]
  20. }
  21. 7 Faraday.new(@chatwoot.chatwoot_endpoint_url) do |faraday|
  22. 7 faraday.options.open_timeout = 5
  23. 7 faraday.options.timeout = 10
  24. 7 faraday.headers = { 'api_access_token': @chatwoot.chatwoot_user_token.to_s, 'Content-Type': 'application/json' }
  25. 7 faraday.request :retry, retry_options
  26. end
  27. end
  28. 1 def get_request(path, params = {})
  29. 7 response = @connection.get(path, params)
  30. 7 if response.success?
  31. 4 { ok: JSON.parse(response.body), request: response }
  32. else
  33. 3 logger_error('Failed get_request', response)
  34. 3 { error: response.body, request: response }
  35. end
  36. end
  37. 1 def logger_error(message, request)
  38. 3 Rails.logger.error "Chatwoot Api Client error #{message} - Chatwoot #{@chatwoot.id}"
  39. 3 Rails.logger.error "Chatwoot: #{@chatwoot.inspect}"
  40. 3 Rails.logger.error "Request: #{request.inspect}"
  41. end
  42. end

app/models/apps/chatwoot/api_client/user_profile.rb

100.0% lines covered

5 relevant lines. 5 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module Apps::Chatwoot::ApiClient::UserProfile
  3. 1 def user_profile
  4. 4 response = get_request('/api/v1/profile')
  5. 4 return { error: 'Failed to fetch user profile', request: response[:request] } if response[:request].status != 200
  6. 3 response
  7. end
  8. end

app/models/apps/chatwoot/connection/refresh.rb

100.0% lines covered

10 relevant lines. 10 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 class Apps::Chatwoot::Connection::Refresh
  3. 1 def initialize(chatwoot)
  4. 3 @chatwoot = chatwoot
  5. end
  6. 1 def call
  7. 3 return @chatwoot.inactive! if @chatwoot.invalid_token?
  8. 2 inboxes = Accounts::Apps::Chatwoots::GetInboxes.call(@chatwoot)
  9. 2 if inboxes.key?(:ok)
  10. 1 @chatwoot.inboxes = inboxes[:ok]
  11. 1 @chatwoot.save!
  12. end
  13. 2 true
  14. end
  15. end

app/models/apps/chatwoot/migrations/remove_trailing_slashes_from_chatwoot_endpoint_url_job.rb

33.33% lines covered

9 relevant lines. 3 lines covered and 6 lines missed.
    
  1. 1 class Apps::Chatwoot::Migrations::RemoveTrailingSlashesFromChatwootEndpointUrlJob < ApplicationJob
  2. 1 self.queue_adapter = :good_job
  3. 1 def perform(chatwoot_id)
  4. chatwoot = Apps::Chatwoot.find_by(id: chatwoot_id)
  5. return unless chatwoot
  6. return unless chatwoot.chatwoot_endpoint_url.present?
  7. cleaned_url = chatwoot.chatwoot_endpoint_url.gsub(/\/+\z/, '')
  8. if chatwoot.chatwoot_endpoint_url != cleaned_url
  9. chatwoot.update_column(:chatwoot_endpoint_url, cleaned_url)
  10. end
  11. end
  12. end

app/models/apps/evolution_api.rb

100.0% lines covered

17 relevant lines. 17 lines covered and 0 lines missed.
    
  1. # == Schema Information
  2. #
  3. # Table name: apps_evolution_apis
  4. #
  5. # id :bigint not null, primary key
  6. # active :boolean default(TRUE), not null
  7. # additional_attributes :jsonb
  8. # connection_status :string default("disconnected"), not null
  9. # endpoint_url :string default(""), not null
  10. # instance :string default(""), not null
  11. # name :string default(""), not null
  12. # phone :string default(""), not null
  13. # qrcode :string default(""), not null
  14. # token :string default(""), not null
  15. # created_at :datetime not null
  16. # updated_at :datetime not null
  17. #
  18. 1 class Apps::EvolutionApi < ApplicationRecord
  19. 1 include Rails.application.routes.url_helpers
  20. 1 include EvolutionApi::Broadcastable
  21. 1 validates :endpoint_url, presence: true
  22. 1 validates :token, presence: true
  23. 1 validates :name, presence: true
  24. 1 validates :instance, presence: true
  25. 1 scope :actives, -> { where(active: true) }
  26. 1 enum connection_status: {
  27. 'disconnected': 'disconnected',
  28. 'connected': 'connected',
  29. 'sync': 'sync',
  30. 'connecting': 'connecting'
  31. }
  32. 1 def request_instance_headers
  33. 20 { 'apiKey': token.to_s, 'Content-Type': 'application/json' }
  34. end
  35. 1 def woofedcrm_webhooks_url
  36. 4 "#{ENV['FRONTEND_URL']}/apps/evolution_apis/webhooks"
  37. end
  38. 1 def generate_token(field)
  39. 6 loop do
  40. 6 security_token = SecureRandom.hex(10)
  41. 6 break security_token unless Apps::EvolutionApi.where(field => security_token).exists?
  42. end
  43. end
  44. end

app/models/attachment.rb

100.0% lines covered

30 relevant lines. 30 lines covered and 0 lines missed.
    
  1. # == Schema Information
  2. #
  3. # Table name: attachments
  4. #
  5. # id :bigint not null, primary key
  6. # attachable_type :string not null
  7. # file_type :integer
  8. # created_at :datetime not null
  9. # updated_at :datetime not null
  10. # attachable_id :bigint not null
  11. #
  12. # Indexes
  13. #
  14. # index_attachments_on_attachable (attachable_type,attachable_id)
  15. #
  16. 1 class Attachment < ApplicationRecord
  17. ACCEPTABLE_FILE_TYPES = %w[
  18. 1 text/csv text/plain text/rtf
  19. application/json application/pdf
  20. application/zip application/x-7z-compressed application/vnd.rar application/x-tar
  21. application/msword application/vnd.ms-excel application/vnd.ms-powerpoint application/rtf
  22. application/vnd.oasis.opendocument.text
  23. application/vnd.openxmlformats-officedocument.presentationml.presentation
  24. application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
  25. application/vnd.openxmlformats-officedocument.wordprocessingml.document application/x-rar-compressed;version=5
  26. ].freeze
  27. 1 belongs_to :attachable, polymorphic: true
  28. 1 has_one_attached :file
  29. 1 validates :file, presence: true
  30. 1 validate :acceptable_file
  31. 1 enum file_type: { image: 0, audio: 1, video: 2, file: 3, location: 4, fallback: 5, share: 6, story_mention: 7,
  32. contact: 8 }
  33. 1 before_validation :fill_file_type
  34. 1 def media_file?(file_content_type)
  35. 17 file_content_type.start_with?('image/', 'video/', 'audio/')
  36. end
  37. 39 scope :by_file_type, ->(file_type) { where(file_type: file_types[file_type]) }
  38. 1 def check_file_type
  39. 17 if media_file?(file.content_type)
  40. 11 file.content_type.split('/').first
  41. 6 elsif ACCEPTABLE_FILE_TYPES.include?(file.content_type)
  42. 6 'file'
  43. end
  44. end
  45. 1 def file_download
  46. 2 file_url = Rails.application.routes.url_helpers.rails_blob_url(file)
  47. 2 file_temp = Down.download(file_url)
  48. 2 FileUtils.mv(file_temp.path, "tmp/#{file_temp.original_filename}")
  49. 2 File.open("tmp/#{file_temp.original_filename}")
  50. end
  51. 1 def download_url
  52. 3 file.attached? ? Rails.application.routes.url_helpers.rails_blob_url(file) : ''
  53. end
  54. 1 def fill_file_type
  55. 24 self.file_type = check_file_type if file_type.blank?
  56. end
  57. 1 def acceptable_file
  58. 24 errors.add(:file, I18n.t('activerecord.errors.messages.file_type_not_supported')) if file_type.blank?
  59. 24 errors.add(:file, I18n.t('activerecord.errors.messages.file_size_too_big')) if acceptable_file_size
  60. end
  61. 1 def acceptable_file_size
  62. 23 file.byte_size > 40.megabytes
  63. end
  64. end

app/models/concerns/account/settings.rb

100.0% lines covered

12 relevant lines. 12 lines covered and 0 lines missed.
    
  1. 1 module Account::Settings
  2. 1 extend ActiveSupport::Concern
  3. 1 included do
  4. 1 store_accessor :settings, :free_form_lost_reasons, prefix: :deal
  5. 1 store_accessor :settings, :allow_edit_lost_at_won_at, prefix: :deal
  6. 1 def deal_free_form_lost_reasons
  7. 6 return false if DealLostReason.none?
  8. 4 super
  9. end
  10. 1 def deal_free_form_lost_reasons=(value)
  11. 3 super(ActiveRecord::Type::Boolean.new.cast(value))
  12. end
  13. 1 def deal_allow_edit_lost_at_won_at=(value)
  14. 1 super(ActiveRecord::Type::Boolean.new.cast(value))
  15. end
  16. end
  17. end

app/models/concerns/applicable.rb

100.0% lines covered

9 relevant lines. 9 lines covered and 0 lines missed.
    
  1. 1 module Applicable
  2. 1 extend ActiveSupport::Concern
  3. 1 included do
  4. 1 belongs_to :account, optional: true
  5. 1 attribute :account_id
  6. 1 def account
  7. 2771 Current.account
  8. end
  9. 1 def account_id
  10. 310 Current.account&.id
  11. end
  12. end
  13. end

app/models/concerns/chatwoot_labels.rb

63.64% lines covered

11 relevant lines. 7 lines covered and 4 lines missed.
    
  1. 1 module ChatwootLabels
  2. 1 extend ActiveSupport::Concern
  3. 1 included do
  4. 1 acts_as_taggable_on :chatwoot_conversations_labels
  5. 1 acts_as_taggable_tenant :account_id
  6. end
  7. 1 def update_chatwoot_conversations_labels(labels = nil)
  8. update!(chatwoot_conversations_label_list: labels)
  9. end
  10. 1 def add_chatwoot_conversations_labels(new_labels = nil)
  11. new_labels = Array(new_labels) # Make sure new_labels is an array
  12. combined_labels = chatwoot_conversations_labels + new_labels
  13. update!(chatwoot_conversations_label_list: combined_labels)
  14. end
  15. end

app/models/concerns/contact/presenters.rb

75.0% lines covered

4 relevant lines. 3 lines covered and 1 lines missed.
    
  1. 1 module Contact::Presenters
  2. 1 extend ActiveSupport::Concern
  3. 1 def full_name_at_format
  4. full_name.blank? ? I18n.t('activerecord.models.contact.unknown', locale: I18n.locale) : full_name
  5. end
  6. end

app/models/concerns/custom_attribute_definition/broadcastable.rb

100.0% lines covered

9 relevant lines. 9 lines covered and 0 lines missed.
    
  1. 1 module CustomAttributeDefinition::Broadcastable
  2. 1 extend ActiveSupport::Concern
  3. 1 included do
  4. 1 after_create_commit do
  5. 25 broadcast_append_later_to [account, :custom_attribute_definition],
  6. target: 'custom_attributes_definitions', partial: 'accounts/settings/custom_attributes_definitions/custom_attribute_definition', locals: { custom_attributes_definition: self }
  7. end
  8. 1 after_update_commit do
  9. 1 broadcast_replace_later_to [account, :custom_attribute_definition], target: self,
  10. partial: 'accounts/settings/custom_attributes_definitions/custom_attribute_definition', locals: { custom_attributes_definition: self }
  11. end
  12. 1 after_destroy_commit do
  13. 1 broadcast_remove_to [account, :custom_attribute_definition], target: self
  14. end
  15. end
  16. end

app/models/concerns/custom_attributes.rb

60.0% lines covered

5 relevant lines. 3 lines covered and 2 lines missed.
    
  1. 1 module CustomAttributes
  2. 1 extend ActiveSupport::Concern
  3. 1 def custom_attribute_display_name(attribute_key)
  4. account.custom_attribute_definitions.where(
  5. attribute_model: "#{self.class.name.downcase}_attribute",
  6. attribute_key: attribute_key
  7. ).first.attribute_display_name
  8. rescue StandardError
  9. attribute_key
  10. end
  11. end

app/models/concerns/deal/broadcastable.rb

100.0% lines covered

18 relevant lines. 18 lines covered and 0 lines missed.
    
  1. 1 module Deal::Broadcastable
  2. 1 extend ActiveSupport::Concern
  3. 1 included do
  4. 1 after_create_commit do
  5. 753 if done == false
  6. 141 broadcast_prepend_later_to [contact_id, 'events'],
  7. partial: 'accounts/contacts/events/event',
  8. target: "events_to_do_#{contact.id}"
  9. else
  10. 612 broadcast_prepend_later_to [contact_id, 'events'],
  11. partial: 'accounts/contacts/events/event',
  12. target: "events_done_#{contact.id}"
  13. end
  14. end
  15. 1 def broadcast_events
  16. 17 events_to_do = deal.contact.events.to_do.limit(5).to_a
  17. 17 events_done = deal.contact.events.done.limit(5).to_a
  18. 17 broadcast_replace_later_to [contact_id, 'events'], target: "events_to_do_#{contact.id}",
  19. partial: 'accounts/contacts/events/events_to_do', locals: { deal: deal, events: events_to_do, pagy: 1 }
  20. 17 broadcast_replace_later_to [contact_id, 'events'], target: "events_done_#{contact.id}",
  21. partial: 'accounts/contacts/events/events_done', locals: { deal: deal, events: events_done, pagy: 1 }
  22. end
  23. 1 after_update_commit do
  24. 31 if saved_change_to_done_at?
  25. 17 broadcast_events
  26. else
  27. 14 broadcast_replace_later_to [contact_id, 'events'],
  28. partial: 'accounts/contacts/events/event'
  29. end
  30. end
  31. 1 after_destroy_commit do
  32. 2 broadcast_remove_to [contact_id, 'events']
  33. end
  34. end
  35. end

app/models/concerns/deal/event_creator.rb

100.0% lines covered

26 relevant lines. 26 lines covered and 0 lines missed.
    
  1. 1 module Deal::EventCreator
  2. 1 extend ActiveSupport::Concern
  3. 1 included do
  4. 1 around_create :create_deal_and_event
  5. 1 around_update :update_deal_and_create_event
  6. 1 def create_deal_and_event
  7. 481 transaction do
  8. 481 yield
  9. 481 create_event_log('deal_opened')
  10. end
  11. end
  12. 1 def update_deal_and_create_event
  13. 35 transaction do
  14. 35 yield
  15. 35 create_event_based_on_changes
  16. end
  17. end
  18. 1 def create_event_based_on_changes
  19. 35 return create_event_log('deal_won') if status_previously_changed?(to: 'won')
  20. 29 return create_event_log('deal_lost') if status_previously_changed?(to: 'lost')
  21. 23 return create_event_log('deal_reopened') if status_previously_changed?(to: 'open')
  22. 20 create_event_log_stage_changes if stage_previously_changed?
  23. end
  24. end
  25. 1 private
  26. 1 def create_event_log_stage_changes
  27. 4 old_stage_id, new_stage_id = previous_changes['stage_id']
  28. 4 old_stage = Stage.find_by(id: old_stage_id) if old_stage_id
  29. 4 new_stage = Stage.find_by(id: new_stage_id) if new_stage_id
  30. 4 Event.create!(
  31. deal: self,
  32. kind: 'deal_stage_change',
  33. done: true,
  34. contact:,
  35. from_me: true,
  36. additional_attributes: {
  37. old_stage_id: old_stage.id,
  38. old_stage_name: old_stage.name,
  39. old_stage_pipeline_id: old_stage.pipeline.id,
  40. old_stage_pipeline_name: old_stage.pipeline.name,
  41. new_stage_id: new_stage.id,
  42. new_stage_name: new_stage.name,
  43. new_stage_pipeline_id: new_stage.pipeline.id,
  44. new_stage_pipeline_name: new_stage.pipeline.name,
  45. deal_name: name
  46. }
  47. )
  48. end
  49. 1 def create_event_log(log_kind)
  50. 496 Event.create!(
  51. deal: self,
  52. kind: log_kind,
  53. done: true,
  54. from_me: true,
  55. contact:,
  56. additional_attributes: {
  57. stage_id: stage.id,
  58. stage_name: stage.name,
  59. pipeline_id: pipeline.id,
  60. pipeline_name: pipeline.name,
  61. deal_name: name
  62. }
  63. )
  64. end
  65. end

app/models/concerns/deal/handle_in_cents_values.rb

71.43% lines covered

7 relevant lines. 5 lines covered and 2 lines missed.
    
  1. 1 module Deal::HandleInCentsValues
  2. 1 extend ActiveSupport::Concern
  3. 1 included do
  4. 1 %i[
  5. total_amount_in_cents
  6. ].each do |attribute|
  7. 1 define_method("#{attribute}=") do |amount|
  8. amount = sanitize_amount(amount)
  9. super(amount)
  10. end
  11. end
  12. end
  13. end

app/models/concerns/deal_product/broadcastable.rb

100.0% lines covered

7 relevant lines. 7 lines covered and 0 lines missed.
    
  1. 1 module DealProduct::Broadcastable
  2. 1 extend ActiveSupport::Concern
  3. 1 included do
  4. 1 after_destroy_commit do
  5. 4 broadcast_remove_to [account.id, :deal], target: self
  6. end
  7. 1 after_create_commit do
  8. 39 broadcast_append_later_to [deal.id, :deal_product], target: 'deal_products',
  9. partial: '/accounts/deals/details/deal_products/deal_product',
  10. locals: { deal_product: self }
  11. end
  12. end
  13. end

app/models/concerns/deal_product/event_creator.rb

100.0% lines covered

22 relevant lines. 22 lines covered and 0 lines missed.
    
  1. 1 module DealProduct::EventCreator
  2. 1 extend ActiveSupport::Concern
  3. 1 included do
  4. 1 around_create :create_deal_product_and_event
  5. 1 around_destroy :destroy_deal_product_and_create_event
  6. 1 def destroy_deal_product_and_create_event
  7. 4 transaction do
  8. 4 create_event_log_destroy
  9. 4 yield
  10. end
  11. end
  12. 1 def create_deal_product_and_event
  13. 39 transaction do
  14. 39 yield
  15. 39 create_event_log
  16. end
  17. end
  18. 1 private
  19. 1 def create_event_log
  20. 39 Event.create!(
  21. deal:,
  22. kind: 'deal_product_added',
  23. done: true,
  24. from_me: true,
  25. contact: deal.contact,
  26. additional_attributes: {
  27. product_id: product.id,
  28. deal_id: deal.id,
  29. product_name: product.name,
  30. deal_name: deal.name
  31. }
  32. )
  33. end
  34. 1 def create_event_log_destroy
  35. 4 product_name = product.name
  36. 4 deal_name = deal.name
  37. 4 product_id = product.id
  38. 4 deal_id = deal.id
  39. 4 Event.create!(
  40. deal:,
  41. kind: 'deal_product_removed',
  42. done: true,
  43. from_me: true,
  44. contact: deal.contact,
  45. additional_attributes: {
  46. product_id:,
  47. deal_id:,
  48. product_name:,
  49. deal_name:
  50. }
  51. )
  52. end
  53. end
  54. end

app/models/concerns/deal_product/handle_in_cents_values.rb

100.0% lines covered

6 relevant lines. 6 lines covered and 0 lines missed.
    
  1. 1 module DealProduct::HandleInCentsValues
  2. 1 extend ActiveSupport::Concern
  3. 1 included do
  4. 1 def unit_amount_in_cents=(amount)
  5. 20 amount = sanitize_amount(amount)
  6. 20 super(amount)
  7. end
  8. end
  9. end

app/models/concerns/evolution_api/broadcastable.rb

100.0% lines covered

12 relevant lines. 12 lines covered and 0 lines missed.
    
  1. 1 module EvolutionApi::Broadcastable
  2. 1 extend ActiveSupport::Concern
  3. 1 included do
  4. 81 after_commit :broadcast_update_qrcode, if: -> { saved_change_to_qrcode? }
  5. 1 after_update_commit do
  6. 13 if saved_change_to_connection_status?(from: 'connecting', to: 'connected')
  7. 1 broadcast_replace_later_to "qrcode_#{self.id}_#{self.account.id}", target: self, partial: '/components/redirect_page',
  8. locals: { path: Rails.application.routes.url_helpers.account_apps_evolution_apis_path(self.account) }
  9. end
  10. 13 broadcast_replace_later_to "evolution_apis_#{account_id}", target: self, partial: '/accounts/apps/evolution_apis/evolution_api',
  11. locals: { evolution_api: self }
  12. end
  13. 1 after_create_commit do
  14. 66 broadcast_append_later_to "evolution_apis_#{account_id}", target: 'evolution_apis', partial: '/accounts/apps/evolution_apis/evolution_api',
  15. locals: { evolution_api: self }
  16. end
  17. 1 def broadcast_update_qrcode
  18. 11 broadcast_replace_later_to "qrcode_#{self.id}_#{self.account.id}", target: self, partial: 'accounts/apps/evolution_apis/qrcode',
  19. locals: { evolution_api: self }
  20. end
  21. end
  22. end

app/models/concerns/labelable.rb

63.64% lines covered

11 relevant lines. 7 lines covered and 4 lines missed.
    
  1. 1 module Labelable
  2. 1 extend ActiveSupport::Concern
  3. 1 included do
  4. 1 acts_as_taggable_on :labels
  5. 1 acts_as_taggable_tenant :account_id
  6. end
  7. 1 def update_labels(labels = nil)
  8. update!(label_list: labels)
  9. end
  10. 1 def add_labels(new_labels = nil)
  11. new_labels = Array(new_labels) # Make sure new_labels is an array
  12. combined_labels = labels + new_labels
  13. update!(label_list: combined_labels)
  14. end
  15. end

app/models/concerns/product/broadcastable.rb

92.31% lines covered

13 relevant lines. 12 lines covered and 1 lines missed.
    
  1. 1 module Product::Broadcastable
  2. 1 extend ActiveSupport::Concern
  3. 1 included do
  4. 4 after_update_commit { deal_products_broadcasts }
  5. 1 after_create_commit do
  6. 137 broadcast_append_later_to [account.id, :product], target: 'products', partial: '/accounts/products/product',
  7. locals: { product: self }
  8. end
  9. 1 after_update_commit do
  10. 3 broadcast_replace_later_to [account.id, :product], target: self, partial: '/accounts/products/product',
  11. locals: { product: self }
  12. end
  13. 1 after_destroy_commit do
  14. 2 broadcast_remove_to [account.id, :product], target: self
  15. end
  16. 1 def deal_products_broadcasts
  17. 3 deal_products.each do |deal_product|
  18. broadcast_replace_later_to [account.id, :deal], target: deal_product,
  19. partial: '/accounts/deals/details/deal_products/deal_product', locals: { deal_product: deal_product }
  20. end
  21. end
  22. end
  23. end

app/models/concerns/stage/decorators.rb

100.0% lines covered

4 relevant lines. 4 lines covered and 0 lines missed.
    
  1. 1 module Stage::Decorators
  2. 1 include ActionView::Helpers::NumberHelper
  3. 1 def total_quantity_deals_resume(filter_status_deal)
  4. 7 number_to_human(total_quantity_deals(filter_status_deal), units: { thousand: 'K', million: 'M', billion: 'B' })
  5. end
  6. end

app/models/contact.rb

96.97% lines covered

33 relevant lines. 32 lines covered and 1 lines missed.
    
  1. # == Schema Information
  2. #
  3. # Table name: contacts
  4. #
  5. # id :bigint not null, primary key
  6. # additional_attributes :jsonb
  7. # app_type :string
  8. # custom_attributes :jsonb
  9. # email :string default(""), not null
  10. # full_name :string default(""), not null
  11. # phone :string default(""), not null
  12. # created_at :datetime not null
  13. # updated_at :datetime not null
  14. # app_id :bigint
  15. #
  16. # Indexes
  17. #
  18. # index_contacts_on_app (app_type,app_id)
  19. # index_contacts_on_chatwoot_id (((additional_attributes ->> 'chatwoot_id'::text)), id)
  20. # index_contacts_on_lower_email (lower(NULLIF((email)::text, ''::text))) UNIQUE
  21. # index_contacts_on_phone (NULLIF((phone)::text, ''::text)) UNIQUE
  22. #
  23. 1 class Contact < ApplicationRecord
  24. 1 include Labelable
  25. 1 include ChatwootLabels
  26. 1 include CustomAttributes
  27. 1 include Contact::Presenters
  28. 1 has_many :events
  29. 1 attr_accessor :skip_validation
  30. 1 validates :email, allow_blank: true, uniqueness: { case_sensitive: false },
  31. format: { with: Devise.email_regexp,
  32. message: I18n.t('activerecord.errors.contact.email.invalid',
  33. locale: I18n.locale) }, unless: :skip_validation
  34. 1 validates :phone, allow_blank: true, uniqueness: true,
  35. format: { with: /\+[1-9]\d{1,14}\z/,
  36. message: I18n.t('activerecord.errors.contact.phone.invalid',
  37. locale: I18n.locale) }, unless: :skip_validation
  38. 1 has_many :deals, dependent: :destroy
  39. 1 belongs_to :app, polymorphic: true, optional: true
  40. 1 scope :by_chatwoot_id, lambda { |chatwoot_id|
  41. 35 chatwoot_id.present? ? where("additional_attributes->>'chatwoot_id' = ?", chatwoot_id.to_s) : none
  42. }
  43. 1 def self.ransackable_attributes(_auth_object = nil)
  44. 29 %w[additional_attributes app_id app_type created_at custom_attributes email full_name id
  45. phone updated_at]
  46. end
  47. 1 def connected_with_chatwoot?
  48. additional_attributes['chatwoot_id'].present?
  49. end
  50. 1 FORM_FIELDS = %i[full_name email phone label_list chatwoot_conversations_label_list]
  51. 1 SHOW_FIELDS = { details: %i[full_name email phone id label_list custom_attributes created_at
  52. updated_at],
  53. deal_page_overview_details: %i[full_name email phone label_list
  54. chatwoot_conversations_label_list] }.freeze
  55. 1 after_commit :export_contact_to_chatwoot, on: %i[create update], unless: :skip_validation
  56. 1 def phone=(value)
  57. 866 value = "+#{value}" if value.present? && !value.start_with?('+')
  58. 866 super(value)
  59. end
  60. ## Events
  61. 1 include Wisper::Publisher
  62. 1 after_commit :publish_created, on: :create, unless: :skip_validation
  63. 1 after_commit :publish_updated, on: :update, unless: :skip_validation
  64. 1 private
  65. 1 def export_contact_to_chatwoot
  66. 833 account.apps_chatwoots.present? && Accounts::Apps::Chatwoots::ExportContactWorker.perform_async(
  67. account.apps_chatwoots.first.id, id
  68. )
  69. end
  70. 1 def publish_created
  71. 808 broadcast(:contact_created, self)
  72. end
  73. 1 def publish_updated
  74. 25 broadcast(:contact_updated, self)
  75. end
  76. end

app/models/contact/integrations/chatwoot/generate_conversation_link.rb

100.0% lines covered

19 relevant lines. 19 lines covered and 0 lines missed.
    
  1. 1 class Contact::Integrations::Chatwoot::GenerateConversationLink
  2. 1 def initialize(contact)
  3. 5 @contact = contact
  4. end
  5. 1 def call
  6. 5 chatwoot = fetch_chatwoot_for_account
  7. 5 chatwoot_contact_id = @contact.additional_attributes['chatwoot_id']
  8. 5 return { error: 'no_chatwoot_or_id' } unless chatwoot && chatwoot_contact_id
  9. 3 conversation_id = fetch_conversation_id(chatwoot, chatwoot_contact_id)
  10. 2 return { error: 'no_conversation' } unless conversation_id
  11. 1 { ok: build_conversation_url(chatwoot, conversation_id) }
  12. end
  13. 1 private
  14. 1 def fetch_chatwoot_for_account
  15. 5 Apps::Chatwoot.first
  16. end
  17. 1 def fetch_conversation_id(chatwoot, chatwoot_contact_id)
  18. 3 conversations = Accounts::Apps::Chatwoots::GetConversations.call(chatwoot, chatwoot_contact_id)
  19. 2 conversations.dig(:ok, 0, 'id')
  20. end
  21. 1 def build_conversation_url(chatwoot, conversation_id)
  22. 1 conversation_path = "/app/accounts/#{chatwoot.chatwoot_account_id}/conversations/#{conversation_id}"
  23. 1 chatwoot.chatwoot_endpoint_url + conversation_path
  24. end
  25. end

app/models/contact/merge.rb

100.0% lines covered

34 relevant lines. 34 lines covered and 0 lines missed.
    
  1. 1 class Contact::Merge
  2. 1 MERGEABLE_KEYS = %w[full_name email phone additional_attributes custom_attributes].freeze
  3. 1 def initialize(base_contact:, mergee_contact:)
  4. 17 raise ArgumentError, 'base_contact is required' unless base_contact
  5. 17 raise ArgumentError, 'mergee_contact is required' unless mergee_contact
  6. 17 @base_contact = base_contact
  7. 17 @mergee_contact = mergee_contact
  8. end
  9. 1 def perform
  10. 5 ActiveRecord::Base.transaction do
  11. 5 validate_contacts
  12. 4 merge_deals
  13. 4 merge_events
  14. 4 merge_labels
  15. 4 merge_and_remove_mergee_contact
  16. end
  17. 3 @base_contact
  18. end
  19. 1 private
  20. 1 def validate_contacts
  21. 7 return if @base_contact != @mergee_contact
  22. 2 raise StandardError, 'contact does merge with same contact'
  23. end
  24. 1 def merge_deals
  25. 5 @mergee_contact.deals.update_all(contact_id: @base_contact.id)
  26. end
  27. 1 def merge_events
  28. 5 @mergee_contact.events.update_all(contact_id: @base_contact.id)
  29. end
  30. 1 def merge_labels
  31. 9 merged_labels = (@base_contact.label_list + @mergee_contact.label_list)
  32. 9 merged_labels_chatwoot_conversations_labels = (@base_contact.chatwoot_conversations_label_list + @mergee_contact.chatwoot_conversations_label_list)
  33. 9 @base_contact.label_list.add(merged_labels) unless merged_labels.blank?
  34. 9 @base_contact.chatwoot_conversations_label_list.add(merged_labels_chatwoot_conversations_labels) unless merged_labels_chatwoot_conversations_labels.blank?
  35. end
  36. 1 def merge_and_remove_mergee_contact
  37. 7 base_attrs = @base_contact.attributes.slice(*MERGEABLE_KEYS).compact_blank
  38. 7 mergee_attrs = @mergee_contact.attributes.slice(*MERGEABLE_KEYS).compact_blank
  39. 7 merged_attrs = mergee_attrs.deep_merge(base_attrs)
  40. 7 @mergee_contact.destroy!
  41. 7 @base_contact.update!(merged_attrs)
  42. end
  43. end

app/models/contact/migrations/merge_duplicate_contacts_job.rb

32.0% lines covered

25 relevant lines. 8 lines covered and 17 lines missed.
    
  1. 1 class Contact::Migrations::MergeDuplicateContactsJob < ApplicationJob
  2. 1 self.queue_adapter = :good_job
  3. 1 def perform
  4. phone_groups = group_duplicate_contacts_by_phone
  5. merge_process(phone_groups) if phone_groups.present?
  6. email_groups = group_duplicate_contacts_by_email
  7. merge_process(email_groups) if email_groups.present?
  8. end
  9. 1 private
  10. 1 def group_duplicate_contacts_by_email
  11. Contact.where.not(email: [nil, ''])
  12. .group(:email)
  13. .having('COUNT(*) > 1')
  14. .pluck(:email)
  15. .map { |email| Contact.where(email:).order(:id).pluck(:id) }
  16. end
  17. 1 def group_duplicate_contacts_by_phone
  18. Contact.where.not(phone: [nil, ''])
  19. .group(:phone)
  20. .having('COUNT(*) > 1')
  21. .pluck(:phone)
  22. .map { |phone| Contact.where(phone:).order(:id).pluck(:id) }
  23. end
  24. 1 def merge_process(contact_groups)
  25. contact_groups.each do |contact_ids|
  26. next if contact_ids.size < 2
  27. merge_group(contact_ids)
  28. end
  29. end
  30. 1 def merge_group(contact_ids)
  31. contacts = Contact.where(id: contact_ids).order(:id).to_a
  32. return if contacts.size < 2
  33. base_contact = contacts.shift
  34. base_contact.skip_validation = true
  35. contacts.each do |mergee_contact|
  36. Contact::Merge.new(base_contact:, mergee_contact:).perform
  37. end
  38. end
  39. end

app/models/current.rb

100.0% lines covered

3 relevant lines. 3 lines covered and 0 lines missed.
    
  1. 1 class Current < ActiveSupport::CurrentAttributes
  2. 1 def account
  3. 5362 Account.first
  4. end
  5. end

app/models/custom_attribute_definition.rb

87.5% lines covered

8 relevant lines. 7 lines covered and 1 lines missed.
    
  1. # == Schema Information
  2. #
  3. # Table name: custom_attribute_definitions
  4. #
  5. # id :bigint not null, primary key
  6. # attribute_description :text
  7. # attribute_display_name :string
  8. # attribute_key :string
  9. # attribute_model :integer default("contact_attribute")
  10. # created_at :datetime not null
  11. # updated_at :datetime not null
  12. #
  13. 1 class CustomAttributeDefinition < ApplicationRecord
  14. 1 include CustomAttributeDefinition::Broadcastable
  15. 1 scope :with_attribute_model, lambda { |attribute_model|
  16. attribute_model.presence && where(attribute_model: attribute_model)
  17. }
  18. 1 validates :attribute_display_name, presence: true
  19. 1 validates :attribute_key,
  20. presence: true,
  21. uniqueness: { scope: %i[attribute_model] }
  22. 1 validates :attribute_model, presence: true
  23. 1 enum attribute_model: { contact_attribute: 0, deal_attribute: 1, product_attribute: 2 }
  24. end

app/models/deal.rb

89.36% lines covered

47 relevant lines. 42 lines covered and 5 lines missed.
    
  1. # == Schema Information
  2. #
  3. # Table name: deals
  4. #
  5. # id :bigint not null, primary key
  6. # custom_attributes :jsonb
  7. # lost_at :datetime
  8. # lost_reason :string default(""), not null
  9. # name :string default(""), not null
  10. # position :integer
  11. # status :string default("open"), not null
  12. # total_deal_products_amount_in_cents :bigint default(0), not null
  13. # won_at :datetime
  14. # created_at :datetime not null
  15. # updated_at :datetime not null
  16. # contact_id :bigint not null
  17. # created_by_id :integer
  18. # pipeline_id :bigint
  19. # stage_id :bigint not null
  20. #
  21. # Indexes
  22. #
  23. # index_deals_on_contact_id (contact_id)
  24. # index_deals_on_created_by_id (created_by_id)
  25. # index_deals_on_pipeline_id (pipeline_id)
  26. # index_deals_on_stage_id (stage_id)
  27. #
  28. # Foreign Keys
  29. #
  30. # fk_rails_... (contact_id => contacts.id)
  31. # fk_rails_... (created_by_id => users.id) ON DELETE => nullify
  32. # fk_rails_... (stage_id => stages.id)
  33. #
  34. 1 class Deal < ApplicationRecord
  35. 1 include CustomAttributes
  36. 1 include Deal::EventCreator
  37. 1 include Deal::HandleInCentsValues
  38. 1 belongs_to :contact
  39. 1 belongs_to :stage
  40. 1 belongs_to :pipeline, touch: true
  41. 1 belongs_to :creator, class_name: 'User', foreign_key: 'created_by_id', optional: true
  42. 1 acts_as_list scope: :stage
  43. 1 has_many :events, dependent: :destroy
  44. 1 has_many :activities
  45. 1 has_many :contact_events, through: :primary_contact, source: :events
  46. 1 has_many :deal_products, dependent: :destroy
  47. 1 has_many :deal_assignees, dependent: :destroy
  48. 1 has_many :users, through: :deal_assignees
  49. 1 accepts_nested_attributes_for :contact
  50. 1 enum status: { 'open': 'open', 'won': 'won', 'lost': 'lost' }
  51. 1 FORM_FIELDS = %i[name creator total_amount_in_cents]
  52. 1 SHOW_FIELDS = { deal_page_overview_details: [:name,
  53. { relations: { stage: :name, creator: :full_name } }, :total_amount_in_cents] }.freeze
  54. 1 before_validation do
  55. 554 self.account = @current_account if account.blank? && @current_account.present?
  56. 554 self.pipeline = stage.pipeline if pipeline.blank? && stage.present?
  57. 554 self.stage = pipeline.stages.first if stage.blank? && pipeline.present?
  58. end
  59. 1 def self.ransackable_attributes(_auth_object = nil)
  60. 88 %w[]
  61. end
  62. 1 def self.ransackable_associations(_auth_object = nil)
  63. 44 %w[users]
  64. end
  65. 1 def total_amount_in_cents
  66. 59 total_deal_products_amount_in_cents
  67. end
  68. 1 def next_event_planned?
  69. 24 next_event_planned
  70. rescue StandardError
  71. false
  72. end
  73. 1 def next_event_planned
  74. 24 events.planned.first
  75. rescue StandardError
  76. nil
  77. end
  78. 1 def self.csv_header(account_id)
  79. custom_fields = CustomAttributeDefinition.where(attribute_model: 'deal_attribute').map do |i|
  80. "custom_attributes.#{i.attribute_key}"
  81. end
  82. column_names.excluding('account_id', 'created_at', 'updated_at', 'id', 'custom_attributes') + custom_fields
  83. end
  84. ## Events
  85. 1 include Wisper::Publisher
  86. 1 after_commit :publish_created, on: :create
  87. 1 after_commit :publish_updated, on: :update
  88. 1 private
  89. 1 def publish_created
  90. 481 broadcast(:deal_created, self)
  91. end
  92. 1 def publish_updated
  93. 35 broadcast(:deal_updated, self)
  94. end
  95. end

app/models/deal/create_or_update.rb

100.0% lines covered

25 relevant lines. 25 lines covered and 0 lines missed.
    
  1. 1 class Deal::CreateOrUpdate
  2. 1 def initialize(deal, params)
  3. 41 @deal = deal
  4. 41 @params = params
  5. end
  6. 1 def call
  7. 38 @deal.assign_attributes(@params)
  8. 38 return false if @deal.invalid?
  9. 35 set_lost_at_and_won_at if should_update_lost_at_or_won_at?
  10. 35 @deal.save!
  11. 35 @deal
  12. end
  13. 1 private
  14. 1 def should_update_lost_at_or_won_at?
  15. 38 @deal.status_changed? || @deal.new_record?
  16. end
  17. 1 def set_lost_at_and_won_at
  18. 25 allow_edit = Current.account.deal_allow_edit_lost_at_won_at
  19. 25 if @deal.won?
  20. 8 @deal.won_at = Time.current unless allow_edit && @params[:won_at].present?
  21. 8 @deal.lost_at = nil
  22. 8 @deal.lost_reason = ''
  23. 17 elsif @deal.lost?
  24. 8 @deal.lost_at = Time.current unless allow_edit && @params[:lost_at].present?
  25. 8 @deal.won_at = nil
  26. else
  27. 9 @deal.lost_at = nil
  28. 9 @deal.won_at = nil
  29. 9 @deal.lost_reason = ''
  30. end
  31. end
  32. end

app/models/deal/migrations/populate_deal_lost_at_and_won_at_job.rb

23.08% lines covered

13 relevant lines. 3 lines covered and 10 lines missed.
    
  1. 1 class Deal::Migrations::PopulateDealLostAtAndWonAtJob < ApplicationJob
  2. 1 self.queue_adapter = :good_job
  3. 1 def perform(deal_id)
  4. deal = Deal.find_by(id: deal_id)
  5. return unless deal
  6. ActiveRecord::Base.transaction do
  7. if deal.won?
  8. latest_won_event = deal.events
  9. .where(kind: 'deal_won')
  10. .order(created_at: :desc)
  11. .select(:created_at)
  12. .first
  13. deal.update_column(:won_at, latest_won_event&.created_at) if latest_won_event
  14. elsif deal.lost?
  15. latest_lost_event = deal.events
  16. .where(kind: 'deal_lost')
  17. .order(created_at: :desc)
  18. .select(:created_at)
  19. .first
  20. deal.update_column(:lost_at, latest_lost_event&.created_at) if latest_lost_event
  21. end
  22. end
  23. Rails.logger.info "Processed deal #{deal.id} for lost_at and won_at"
  24. end
  25. end

app/models/deal/recalculate_and_save_all_monetary_values.rb

100.0% lines covered

10 relevant lines. 10 lines covered and 0 lines missed.
    
  1. 1 class Deal::RecalculateAndSaveAllMonetaryValues
  2. 1 def initialize(deal)
  3. 9 @deal = deal
  4. end
  5. 1 def call
  6. 9 ActiveRecord::Base.transaction do
  7. 9 recalculate_deal
  8. end
  9. end
  10. 1 private
  11. 1 def recalculate_deal
  12. 9 @deal.total_deal_products_amount_in_cents = @deal.deal_products.sum(:total_amount_in_cents)
  13. 9 @deal.save!
  14. end
  15. end

app/models/deal_assignee.rb

100.0% lines covered

4 relevant lines. 4 lines covered and 0 lines missed.
    
  1. # == Schema Information
  2. #
  3. # Table name: deal_assignees
  4. #
  5. # id :bigint not null, primary key
  6. # created_at :datetime not null
  7. # updated_at :datetime not null
  8. # deal_id :bigint not null
  9. # user_id :bigint not null
  10. #
  11. # Indexes
  12. #
  13. # index_deal_assignees_on_deal_id (deal_id)
  14. # index_deal_assignees_on_deal_id_and_user_id (deal_id,user_id) UNIQUE
  15. # index_deal_assignees_on_user_id (user_id)
  16. #
  17. # Foreign Keys
  18. #
  19. # fk_rails_... (deal_id => deals.id)
  20. # fk_rails_... (user_id => users.id)
  21. #
  22. 1 class DealAssignee < ApplicationRecord
  23. 1 belongs_to :deal
  24. 1 belongs_to :user
  25. 1 validates :user_id, uniqueness: { scope: :deal_id, message: :taken }
  26. end

app/models/deal_lost_reason.rb

100.0% lines covered

2 relevant lines. 2 lines covered and 0 lines missed.
    
  1. # == Schema Information
  2. #
  3. # Table name: deal_lost_reasons
  4. #
  5. # id :bigint not null, primary key
  6. # name :string default(""), not null
  7. # created_at :datetime not null
  8. # updated_at :datetime not null
  9. #
  10. 1 class DealLostReason < ApplicationRecord
  11. 1 validates :name, presence: true
  12. end

app/models/deal_product.rb

100.0% lines covered

8 relevant lines. 8 lines covered and 0 lines missed.
    
  1. # == Schema Information
  2. #
  3. # Table name: deal_products
  4. #
  5. # id :bigint not null, primary key
  6. # product_identifier :string default(""), not null
  7. # product_name :string default(""), not null
  8. # quantity :bigint default(1), not null
  9. # total_amount_in_cents :bigint default(0), not null
  10. # unit_amount_in_cents :bigint default(0), not null
  11. # created_at :datetime not null
  12. # updated_at :datetime not null
  13. # deal_id :bigint not null
  14. # product_id :bigint not null
  15. #
  16. # Indexes
  17. #
  18. # index_deal_products_on_deal_id (deal_id)
  19. # index_deal_products_on_product_id (product_id)
  20. #
  21. # Foreign Keys
  22. #
  23. # fk_rails_... (deal_id => deals.id)
  24. # fk_rails_... (product_id => products.id)
  25. #
  26. 1 class DealProduct < ApplicationRecord
  27. 1 include DealProduct::Broadcastable
  28. 1 include DealProduct::EventCreator
  29. 1 include DealProduct::HandleInCentsValues
  30. 1 belongs_to :product
  31. 1 belongs_to :deal
  32. 1 validates :product_id, uniqueness: { scope: :deal_id, message: :taken }
  33. 1 FORM_FIELDS = %i[product_name unit_amount_in_cents product_identifier]
  34. end

app/models/deal_product/create_or_update.rb

100.0% lines covered

23 relevant lines. 23 lines covered and 0 lines missed.
    
  1. 1 class DealProduct::CreateOrUpdate
  2. 1 def initialize(deal_product, params)
  3. 11 @deal_product = deal_product
  4. 11 @params = params
  5. end
  6. 1 def call
  7. 11 @deal_product.assign_attributes(@params)
  8. 11 return false if @deal_product.invalid?
  9. 7 if needs_recalculation?
  10. 6 ActiveRecord::Base.transaction do
  11. 6 update_deal_product
  12. 6 @deal_product.save!
  13. 6 Deal::RecalculateAndSaveAllMonetaryValues.new(@deal_product.deal).call
  14. end
  15. else
  16. 1 @deal_product.save!
  17. end
  18. 7 @deal_product
  19. end
  20. 1 private
  21. 1 def needs_recalculation?
  22. 7 should_recalculate_base_values?
  23. end
  24. 1 def update_deal_product
  25. 6 recalculate_from_base_values if should_recalculate_base_values?
  26. end
  27. 1 def recalculate_from_base_values
  28. 6 @deal_product.total_amount_in_cents = @deal_product.quantity * @deal_product.unit_amount_in_cents
  29. end
  30. 1 def should_recalculate_base_values?
  31. 13 @deal_product.quantity_changed? || @deal_product.unit_amount_in_cents_changed? || @deal_product.new_record?
  32. end
  33. end

app/models/deal_product/destroy.rb

100.0% lines covered

8 relevant lines. 8 lines covered and 0 lines missed.
    
  1. 1 class DealProduct::Destroy
  2. 1 def initialize(deal_product)
  3. 1 @deal_product = deal_product
  4. end
  5. 1 def call
  6. 1 ActiveRecord::Base.transaction do
  7. 1 @deal_product.destroy!
  8. 1 Deal::RecalculateAndSaveAllMonetaryValues.new(@deal_product.deal).call
  9. end
  10. 1 @deal_product
  11. end
  12. end

app/models/embedding_documment.rb

100.0% lines covered

3 relevant lines. 3 lines covered and 0 lines missed.
    
  1. # == Schema Information
  2. #
  3. # Table name: embedding_documments
  4. #
  5. # id :bigint not null, primary key
  6. # content :text
  7. # embedding :vector(1536)
  8. # source_reference :string
  9. # source_type :string
  10. # status :integer default(0)
  11. # created_at :datetime not null
  12. # updated_at :datetime not null
  13. # source_id :bigint
  14. #
  15. # Indexes
  16. #
  17. # index_embedding_documments_on_source (source_type,source_id)
  18. #
  19. 1 class EmbeddingDocumment < ApplicationRecord
  20. 1 belongs_to :source, polymorphic: true, optional: true
  21. 1 has_neighbors :embedding, normalize: true
  22. end

app/models/event.rb

94.31% lines covered

123 relevant lines. 116 lines covered and 7 lines missed.
    
  1. # == Schema Information
  2. #
  3. # Table name: events
  4. #
  5. # id :bigint not null, primary key
  6. # additional_attributes :jsonb
  7. # app_type :string
  8. # auto_done :boolean default(FALSE)
  9. # custom_attributes :jsonb
  10. # done_at :datetime
  11. # from_me :boolean
  12. # kind :string not null
  13. # scheduled_at :datetime
  14. # status :integer
  15. # title :string default(""), not null
  16. # created_at :datetime not null
  17. # updated_at :datetime not null
  18. # app_id :bigint
  19. # contact_id :bigint
  20. # deal_id :bigint
  21. #
  22. # Indexes
  23. #
  24. # index_events_on_app (app_type,app_id)
  25. # index_events_on_contact_id (contact_id)
  26. # index_events_on_deal_id (deal_id)
  27. #
  28. 1 class Event < ApplicationRecord
  29. 1 include Deal::Broadcastable
  30. # default_scope { order('created_at DESC') }
  31. 1 DEAL_UPDATE_KINDS = %w[deal_stage_change deal_opened deal_won deal_lost deal_reopened deal_product_added
  32. deal_product_removed].freeze
  33. 1 belongs_to :deal, optional: true
  34. 1 belongs_to :contact
  35. # belongs_to :event_kind, default: -> { EventKind }
  36. # belongs_to :record, polymorphic: true
  37. 1 belongs_to :app, polymorphic: true, optional: true
  38. 1 has_rich_text :content
  39. 1 alias original_content content
  40. 1 attribute :done, :boolean
  41. 1 attribute :send_now, :boolean
  42. 1 validates :kind, presence: true
  43. 1 has_one :attachment, as: :attachable
  44. 1 after_commit do
  45. # To refactory
  46. 786 if send_now == true
  47. 10 Accounts::Contacts::Events::SendNow.call(self)
  48. 776 elsif scheduled_delivery_event?
  49. 9 Accounts::Contacts::Events::EnqueueWorker.perform_async(id)
  50. end
  51. 786 schedule_webpush_notifications
  52. end
  53. 1 attribute :files, default: []
  54. 1 attribute :files_events, default: []
  55. 1 attribute :invalid_files
  56. 1 validate :validate_invalid_files
  57. 1 def validate_invalid_files
  58. 805 errors.add(:files, 'Invalid files') if invalid_files == true
  59. end
  60. 1 def save
  61. 73 ActiveRecord::Base.transaction do
  62. 73 @result = super
  63. 73 return @result if @result == false
  64. 71 if files_events.present?
  65. 1 files_events.each do |file_event|
  66. 5 file_event.save!
  67. end
  68. end
  69. end
  70. 71 @result
  71. end
  72. 1 def schedule_webpush_notifications
  73. 786 return unless scheduled_at.present? && saved_change_to_scheduled_at? && !send_now
  74. 98 Pwa::SendNotificationsWorker.set(wait_until: scheduled_at).perform_later(id)
  75. end
  76. 1 def content=(value)
  77. 213 original_content.body = value
  78. end
  79. 1 def content
  80. 50 if text_content? && original_content.body.present?
  81. 21 original_content.body.to_plain_text
  82. else
  83. 29 original_content
  84. end
  85. end
  86. 1 def text_content?
  87. 50 chatwoot_message? || evolution_api_message?
  88. end
  89. 1 def generate_content_hash(key, value)
  90. 14 if content_is_blank?(value)
  91. 5 { key.to_s => '' }
  92. else
  93. 9 { key.to_s => value }
  94. end
  95. end
  96. 1 def content_is_blank?(value)
  97. 14 value.respond_to?(:body)
  98. end
  99. 1 def should_delivery_event_scheduled?
  100. 88 !done? && (Time.current.in_time_zone > scheduled_at)
  101. end
  102. 1 def changed_scheduled_values?
  103. 776 saved_change_to_scheduled_at? || saved_change_to_auto_done?
  104. end
  105. 1 def scheduled_delivery_event?
  106. 776 changed_scheduled_values? && (auto_done == true && scheduled_at.present? && done_at.blank?)
  107. end
  108. 1 def done
  109. 1630 done_at.present?
  110. end
  111. 1 def done?
  112. 114 done
  113. end
  114. 1 def done=(value)
  115. 622 value_boolean = ActiveRecord::Type::Boolean.new.cast(value)
  116. 622 return if value_boolean == done
  117. 618 self.done_at = (Time.now if value_boolean == true)
  118. end
  119. 1 def send_now=(value)
  120. 24 self[:send_now] = ActiveRecord::Type::Boolean.new.cast(value)
  121. end
  122. 1 scope :to_do, lambda {
  123. 49 where('done_at IS NULL').order(:scheduled_at)
  124. }
  125. 1 scope :planned, lambda {
  126. 27 to_do.where('auto_done = false AND scheduled_at IS NOT NULL').order(:scheduled_at)
  127. }
  128. 1 scope :scheduled, lambda {
  129. 1 to_do.where('auto_done = true AND scheduled_at IS NOT NULL')
  130. }
  131. 1 scope :planned_overdue, lambda {
  132. 1 planned.where('scheduled_at < ?', DateTime.current)
  133. }
  134. 1 scope :planned_without_date, lambda {
  135. 1 to_do.where('auto_done = false AND scheduled_at IS NULL')
  136. }
  137. 1 scope :done, lambda {
  138. 20 where('done_at IS NOT NULL').order(done_at: :desc)
  139. }
  140. 1 scope :by_message_id, lambda { |message_id|
  141. 1 where("additional_attributes ->> 'message_id' = ?", message_id)
  142. }
  143. 1 enum kind: {
  144. 'note': 'note',
  145. 'evolution_api_message': 'evolution_api_message',
  146. 'activity': 'activity',
  147. 'chatwoot_message': 'chatwoot_message',
  148. 'deal_stage_change': 'deal_stage_change',
  149. 'deal_opened': 'deal_opened',
  150. 'deal_won': 'deal_won',
  151. 'deal_lost': 'deal_lost',
  152. 'deal_reopened': 'deal_reopened',
  153. 'deal_product_added': 'deal_product_added',
  154. 'deal_product_removed': 'deal_product_removed'
  155. }
  156. 1 enum status: { sent: 0, delivered: 1, read: 2, failed: 3 }
  157. 1 before_validation do
  158. 805 self.done = false if scheduled_at.present? && done.nil?
  159. end
  160. 1 def icon_key
  161. 22 if note?
  162. 'menu-square'
  163. 22 elsif activity?
  164. 22 'calendar-check-2'
  165. elsif chatwoot_message?
  166. 'message-circle'
  167. end
  168. end
  169. 1 def editable?
  170. 19 return true if %w[note activity].include?(kind)
  171. 6 return true if %w[chatwoot_message evolution_api_message].include?(kind) && !done?
  172. 5 false
  173. end
  174. 1 def deal_updates?
  175. 13 DEAL_UPDATE_KINDS.include?(kind)
  176. end
  177. 1 def kind_message?
  178. chatwoot_message? || evolution_api_message?
  179. end
  180. 1 def overdue?
  181. return false if done == true || scheduled_at.blank?
  182. DateTime.current > scheduled_at
  183. end
  184. 1 def primary_date
  185. 11 if scheduled_at.present?
  186. scheduled_at.iso8601
  187. else
  188. 11 created_at.iso8601
  189. end
  190. end
  191. 1 def from
  192. 13 if from_me == true
  193. 2 'from-me'
  194. else
  195. 11 'from-contacts'
  196. end
  197. end
  198. 1 def scheduled_kind
  199. 13 if done == true
  200. 7 'done'
  201. else
  202. 6 'scheduled'
  203. end
  204. end
  205. 1 def has_media_attachment?
  206. 11 attachment.present? && (attachment.image? || attachment.file? || attachment.video?)
  207. end
  208. ## Events
  209. 1 include Wisper::Publisher
  210. 1 after_commit :publish_created, on: :create
  211. 1 after_commit :publish_updated, on: :update
  212. 1 private
  213. 1 def publish_created
  214. 749 broadcast(:event_created, self)
  215. end
  216. 1 def publish_updated
  217. 25 broadcast(:event_updated, self)
  218. end
  219. end

app/models/installation.rb

91.67% lines covered

12 relevant lines. 11 lines covered and 1 lines missed.
    
  1. # == Schema Information
  2. #
  3. # Table name: installations
  4. #
  5. # id :string not null, primary key
  6. # key1 :string default(""), not null
  7. # key2 :string default(""), not null
  8. # status :integer default("in_progress"), not null
  9. # token :string default(""), not null
  10. # created_at :datetime not null
  11. # updated_at :datetime not null
  12. # user_id :bigint
  13. #
  14. # Indexes
  15. #
  16. # index_installations_on_user_id (user_id)
  17. #
  18. 1 class Installation < ApplicationRecord
  19. 1 include Installation::Complete
  20. 1 belongs_to :user, optional: true
  21. 1 validates_presence_of :key1
  22. 1 validates_presence_of :key2
  23. 1 validates_presence_of :token
  24. 1 enum status: {
  25. in_progress: 0,
  26. completed: 1
  27. }
  28. 1 def self.installation_url
  29. 8 "#{ENV.fetch('STORE_URL', 'https://store.woofedcrm.com')}/installations/new?installation_params=#{{ url: ENV.fetch('FRONTEND_URL', 'http://localhost:3001'),
  30. kind: :self_hosted }.to_json}"
  31. end
  32. 1 def self.installation_flow?
  33. 660 Installation.first&.status != 'completed'
  34. rescue StandardError
  35. true
  36. end
  37. end

app/models/installation/complete.rb

100.0% lines covered

15 relevant lines. 15 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module Installation::Complete
  3. 1 def complete_installation!
  4. 3 return unless Installation.installation_flow?
  5. 2 return unless register_completed_install
  6. 1 completed!
  7. 1 app_reload
  8. 1 true
  9. end
  10. 1 def register_completed_install
  11. 5 return false if Current.account.blank?
  12. 4 user = self.user
  13. 4 result_request = Faraday.post(
  14. "#{ENV.fetch('STORE_URL', 'https://store.woofedcrm.com')}/installations/complete",
  15. {
  16. user_details: { name: user.full_name, email: user.email,
  17. phone_number: user.phone, job_description: user.job_description },
  18. company_details: { name: Current.account.name, site_url: Current.account.site_url,
  19. segment: Current.account.segment, number_of_employees: Current.account.number_of_employees }
  20. }.to_json,
  21. { 'Content-Type' => 'application/json', 'Authorization' => "Bearer #{token}" }
  22. )
  23. 4 result_request.status == 200
  24. end
  25. 1 def app_reload
  26. 6 load "#{Rails.root}/app/controllers/application_controller.rb"
  27. 6 Rails.application.reload_routes!
  28. end
  29. end

app/models/pipeline.rb

100.0% lines covered

5 relevant lines. 5 lines covered and 0 lines missed.
    
  1. # == Schema Information
  2. #
  3. # Table name: pipelines
  4. #
  5. # id :bigint not null, primary key
  6. # name :string default(""), not null
  7. # created_at :datetime not null
  8. # updated_at :datetime not null
  9. #
  10. 1 class Pipeline < ApplicationRecord
  11. 1 broadcasts_refreshes
  12. 1 has_many :stages
  13. 1 has_many :deals
  14. 1 accepts_nested_attributes_for :stages, reject_if: :all_blank, allow_destroy: true
  15. end

app/models/product.rb

94.74% lines covered

19 relevant lines. 18 lines covered and 1 lines missed.
    
  1. # == Schema Information
  2. #
  3. # Table name: products
  4. #
  5. # id :bigint not null, primary key
  6. # additional_attributes :jsonb
  7. # amount_in_cents :integer default(0), not null
  8. # custom_attributes :jsonb
  9. # description :text default(""), not null
  10. # identifier :string default(""), not null
  11. # name :string default(""), not null
  12. # quantity_available :integer default(0), not null
  13. # created_at :datetime not null
  14. # updated_at :datetime not null
  15. #
  16. 1 class Product < ApplicationRecord
  17. 1 include Product::Broadcastable
  18. 1 include CustomAttributes
  19. 1 has_many :attachments, as: :attachable
  20. 1 validates :quantity_available, :amount_in_cents,
  21. numericality: { greater_than_or_equal_to: 0, message: 'Can not be negative' }
  22. 1 has_many :deal_products, dependent: :destroy
  23. 1 accepts_nested_attributes_for :attachments, reject_if: :all_blank, allow_destroy: true
  24. 1 FORM_FIELDS = %i[name amount_in_cents quantity_available identifier]
  25. 1 SHOW_FIELDS = { details: %i[name amount_in_cents quantity_available identifier description custom_attributes created_at
  26. updated_at] }.freeze
  27. 1 %i[image file video].each do |file_type|
  28. 3 define_method "#{file_type}_attachments" do
  29. 38 attachments.by_file_type(file_type)
  30. end
  31. end
  32. 1 def self.ransackable_associations(auth_object = nil)
  33. %w[account attachments deal_products]
  34. end
  35. 1 def self.ransackable_attributes(_auth_object = nil)
  36. 20 %w[identifier amount_in_cents quantity_available description name created_at updated_at]
  37. end
  38. 1 def amount_in_cents=(amount)
  39. 147 amount = sanitize_amount(amount)
  40. 147 super(amount)
  41. end
  42. end

app/models/query/advanced_search.rb

94.34% lines covered

53 relevant lines. 50 lines covered and 3 lines missed.
    
  1. 1 class Query::AdvancedSearch
  2. 1 def initialize(current_user, current_account, params)
  3. 23 raise ArgumentError, 'current_user is required' if current_user.blank?
  4. 22 raise ArgumentError, 'current_account is required' if current_account.blank?
  5. 21 raise ArgumentError, 'params is required' if params.blank?
  6. 20 @current_user = current_user
  7. 20 @current_account = current_account
  8. 20 @params = params
  9. 20 @limit = 7
  10. end
  11. 1 def call
  12. 6 case search_type
  13. when 'contact'
  14. 2 { contacts: filter_contacts }
  15. when 'deal'
  16. 1 { deals: filter_deals }
  17. when 'product'
  18. { products: filter_products }
  19. when 'pipeline'
  20. { pipelines: filter_pipelines }
  21. when 'activity'
  22. { activities: filter_activities }
  23. else
  24. 3 { contacts: filter_contacts, deals: filter_deals, products: filter_products, pipelines: filter_pipelines,
  25. activities: filter_activities }
  26. end
  27. end
  28. 1 private
  29. 1 attr_reader :current_user, :current_account, :params, :limit
  30. 1 def filter_contacts
  31. 8 scope = Contact
  32. 8 if search_query.present?
  33. 6 pattern = "%#{search_query}%"
  34. 6 scope = scope.where(
  35. 'full_name ILIKE :q OR email ILIKE :q OR phone ILIKE :q',
  36. q: pattern
  37. )
  38. end
  39. 8 scope.reorder('updated_at DESC').limit(limit)
  40. end
  41. 1 def filter_deals
  42. 6 scope = Deal
  43. 6 if search_query.present?
  44. 5 pattern = "%#{search_query}%"
  45. 5 scope = scope.where(
  46. 'name ILIKE :q',
  47. q: pattern
  48. )
  49. end
  50. 6 scope.reorder('updated_at DESC').limit(limit)
  51. end
  52. 1 def filter_products
  53. 6 scope = Product
  54. 6 if search_query.present?
  55. 5 pattern = "%#{search_query}%"
  56. 5 scope = scope.where(
  57. 'name ILIKE :q OR identifier ILIKE :q',
  58. q: pattern
  59. )
  60. end
  61. 6 scope.reorder('updated_at DESC').limit(limit)
  62. end
  63. 1 def filter_pipelines
  64. 5 scope = Pipeline
  65. 5 if search_query.present?
  66. 4 pattern = "%#{search_query}%"
  67. 4 scope = scope.where(
  68. 'name ILIKE :q',
  69. q: pattern
  70. )
  71. end
  72. 5 scope.reorder('updated_at DESC').limit(limit)
  73. end
  74. 1 def filter_activities
  75. 4 scope = Event.activity
  76. 4 if search_query.present?
  77. 4 pattern = "%#{search_query}%"
  78. 4 scope = scope.where(
  79. 'title ILIKE :q',
  80. q: pattern
  81. )
  82. end
  83. 4 scope.reorder('updated_at DESC').limit(limit)
  84. end
  85. 1 def search_type
  86. 7 @search_type ||= params[:search_type]&.downcase
  87. end
  88. 1 def search_query
  89. 54 @search_query ||= params[:q].to_s.strip
  90. end
  91. end

app/models/query/filter.rb

100.0% lines covered

10 relevant lines. 10 lines covered and 0 lines missed.
    
  1. 1 class Query::Filter
  2. 1 def initialize(rel, params)
  3. 16 @rel = rel
  4. 16 @params = params
  5. end
  6. 1 def call
  7. 16 apply_filters
  8. end
  9. 1 private
  10. 1 attr_reader :rel, :params
  11. 1 def apply_filters
  12. 16 rel.ransack(params).result
  13. end
  14. end

app/models/stage.rb

100.0% lines covered

13 relevant lines. 13 lines covered and 0 lines missed.
    
  1. # == Schema Information
  2. #
  3. # Table name: stages
  4. #
  5. # id :bigint not null, primary key
  6. # name :string default(""), not null
  7. # position :integer
  8. # created_at :datetime not null
  9. # updated_at :datetime not null
  10. # pipeline_id :bigint not null
  11. #
  12. # Indexes
  13. #
  14. # index_stages_on_pipeline_id (pipeline_id)
  15. #
  16. # Foreign Keys
  17. #
  18. # fk_rails_... (pipeline_id => pipelines.id)
  19. #
  20. 1 class Stage < ApplicationRecord
  21. 1 include Stage::Decorators
  22. 1 belongs_to :pipeline, touch: true
  23. 1 acts_as_list scope: :pipeline
  24. 1 has_many :deals, dependent: :destroy
  25. 1 scope :ordered_by_pipeline_and_position, lambda {
  26. 15 joins(:pipeline).order('pipelines.name ASC, stages.position ASC')
  27. }
  28. 1 def total_amount_deals(filter_status_deal)
  29. 17 return deals.sum(&:total_amount_in_cents) if filter_status_deal == 'all'
  30. 14 deals.where(status: filter_status_deal).sum(&:total_amount_in_cents)
  31. end
  32. 1 def total_quantity_deals(filter_status_deal)
  33. 17 return deals.count if filter_status_deal == 'all'
  34. 14 deals.where(status: filter_status_deal).count
  35. end
  36. end

app/models/user.rb

100.0% lines covered

18 relevant lines. 18 lines covered and 0 lines missed.
    
  1. # == Schema Information
  2. #
  3. # Table name: users
  4. #
  5. # id :bigint not null, primary key
  6. # avatar_url :string default(""), not null
  7. # email :string default(""), not null
  8. # encrypted_password :string default(""), not null
  9. # full_name :string default(""), not null
  10. # job_description :string default("other"), not null
  11. # language :string default("en"), not null
  12. # notifications :jsonb not null
  13. # phone :string
  14. # remember_created_at :datetime
  15. # reset_password_sent_at :datetime
  16. # reset_password_token :string
  17. # created_at :datetime not null
  18. # updated_at :datetime not null
  19. #
  20. # Indexes
  21. #
  22. # index_users_on_email (email) UNIQUE
  23. # index_users_on_reset_password_token (reset_password_token) UNIQUE
  24. #
  25. 1 class User < ApplicationRecord
  26. 1 has_one :installation
  27. 1 has_many :webpush_subscriptions
  28. 1 has_many :deal_assignees, dependent: :destroy
  29. 1 has_many :deals, through: :deal_assignees
  30. 1 has_many :created_deals,
  31. class_name: 'Deal',
  32. foreign_key: 'created_by_id',
  33. dependent: :nullify,
  34. inverse_of: :creator
  35. # Include default devise modules. Others available are:
  36. # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  37. 1 devise :database_authenticatable, :registerable,
  38. :recoverable, :rememberable, :validatable
  39. 1 accepts_nested_attributes_for :account
  40. 1 attribute :language, :string, default: ENV.fetch('LANGUAGE', 'en')
  41. 1 validates :phone,
  42. allow_blank: true,
  43. format: { with: /\+[1-9]\d{1,14}\z/ }
  44. 1 store :notifications, accessors: [
  45. :webpush_notify_on_event_expired
  46. ], coder: JSON
  47. 1 enum job_description: {
  48. ceo: 'ceo',
  49. cfo: 'cfo',
  50. cto: 'cto',
  51. project_manager: 'project_manager',
  52. software_engineer: 'software_engineer',
  53. marketing_manager: 'marketing_manager',
  54. sales_representative: 'sales_representative',
  55. hr_specialist: 'hr_specialist',
  56. customer_support: 'customer_support',
  57. product_manager: 'product_manager',
  58. operations_manager: 'operations_manager',
  59. business_development_manager: 'business_development_manager',
  60. data_analyst: 'data_analyst',
  61. account_manager: 'account_manager',
  62. consultant: 'consultant',
  63. financial_analyst: 'financial_analyst',
  64. graphic_designer: 'graphic_designer',
  65. ux_ui_designer: 'ux_ui_designer',
  66. content_creator: 'content_creator',
  67. legal_counsel: 'legal_counsel',
  68. research_scientist: 'research_scientist',
  69. it_administrator: 'it_administrator',
  70. system_administrator: 'system_administrator',
  71. project_coordinator: 'project_coordinator',
  72. operations_coordinator: 'operations_coordinator',
  73. executive_assistant: 'executive_assistant',
  74. other: 'other'
  75. }
  76. 1 def self.ransackable_attributes(_auth_object = nil)
  77. 119 %w[full_name email created_at updated_at phone language job_description id
  78. avatar_url]
  79. end
  80. 1 def get_jwt_token
  81. 70 Users::JsonWebToken.encode_user(self)
  82. end
  83. 1 def webpush_notify_on_event_expired=(value)
  84. 2 self[:notifications][:webpush_notify_on_event_expired] = ActiveRecord::Type::Boolean.new.cast(value)
  85. end
  86. end

app/models/webhook.rb

100.0% lines covered

21 relevant lines. 21 lines covered and 0 lines missed.
    
  1. # == Schema Information
  2. #
  3. # Table name: webhooks
  4. #
  5. # id :bigint not null, primary key
  6. # status :string default("active")
  7. # url :string default(""), not null
  8. # created_at :datetime not null
  9. # updated_at :datetime not null
  10. #
  11. 1 class Webhook < ApplicationRecord
  12. 1 validates :url, presence: true, format: URI::DEFAULT_PARSER.make_regexp(%w[http https])
  13. 1 validates :status, presence: true
  14. 1 validate :validate_webhook_url, if: :active?
  15. 1 enum status: {
  16. inactive: 'inactive',
  17. active: 'active'
  18. }
  19. 1 after_update_commit do
  20. 1 broadcast_replace_later_to "webhooks_#{account_id}", target: self, partial: 'accounts/settings/webhooks/webhook',
  21. locals: { webhook: self }
  22. end
  23. 1 after_create_commit do
  24. 22 broadcast_append_later_to "webhooks_#{account_id}", target: 'webhooks',
  25. partial: 'accounts/settings/webhooks/webhook', locals: { webhook: self }
  26. end
  27. 1 after_destroy_commit do
  28. 1 broadcast_remove_to "webhooks_#{account_id}", target: self
  29. end
  30. 1 def valid_url?
  31. 9 return false if url.blank?
  32. 6 response = Webhook::ApiClient.new(self).post_request
  33. 4 return false if response.key?(:error)
  34. 3 true
  35. rescue Faraday::ConnectionFailed, Faraday::TimeoutError, Faraday::SSLError
  36. 2 false
  37. end
  38. 1 private
  39. 1 def validate_webhook_url
  40. 9 return if valid_url?
  41. 3 errors.add(:url)
  42. end
  43. end

app/models/webhook/api_client.rb

100.0% lines covered

19 relevant lines. 19 lines covered and 0 lines missed.
    
  1. 1 class Webhook::ApiClient
  2. 1 def initialize(webhook)
  3. 8 @webhook = webhook
  4. 8 @connection = create_connection
  5. end
  6. 1 def create_connection
  7. 8 Faraday.new(@webhook.url) do |faraday|
  8. 8 faraday.options.timeout = 5
  9. 8 faraday.headers = { 'Content-Type': 'application/json' }
  10. end
  11. end
  12. 1 def post_request
  13. 8 response = @connection.post
  14. 8 if response.success?
  15. 4 { ok: response.status, request: response }
  16. else
  17. 4 logger_error('Failed to validate webhook URL', response)
  18. 4 { error: "Invalid or unreachable URL (status: #{response.status})", request: response }
  19. end
  20. end
  21. 1 private
  22. 1 def logger_error(message, response)
  23. 4 Rails.logger.error "Webhook Api Client error: #{message} - Webhook #{@webhook.id || 'new'}"
  24. 4 Rails.logger.error "Webhook: #{@webhook.inspect}"
  25. 4 Rails.logger.error "Request: #{response.inspect}"
  26. end
  27. end

app/models/webpush_subscription.rb

100.0% lines covered

8 relevant lines. 8 lines covered and 0 lines missed.
    
  1. # == Schema Information
  2. #
  3. # Table name: webpush_subscriptions
  4. #
  5. # id :bigint not null, primary key
  6. # auth_key :string default(""), not null
  7. # endpoint :string default(""), not null
  8. # p256dh_key :string default(""), not null
  9. # created_at :datetime not null
  10. # updated_at :datetime not null
  11. # user_id :bigint not null
  12. #
  13. # Indexes
  14. #
  15. # index_webpush_subscriptions_on_user_id (user_id)
  16. #
  17. # Foreign Keys
  18. #
  19. # fk_rails_... (user_id => users.id)
  20. #
  21. 1 class WebpushSubscription < ApplicationRecord
  22. 1 belongs_to :user
  23. 1 validates :endpoint, presence: true
  24. 1 validates :p256dh_key, presence: true
  25. 1 validates :auth_key, presence: true, uniqueness: true
  26. 1 def send_notification(message)
  27. 2 WebPush.payload_send(
  28. message: JSON.generate(message),
  29. endpoint: endpoint,
  30. p256dh: p256dh_key,
  31. auth: auth_key,
  32. vapid: {
  33. private_key: ENV['VAPID_PRIVATE_KEY'],
  34. public_key: ENV['VAPID_PUBLIC_KEY']
  35. }
  36. )
  37. rescue WebPush::ExpiredSubscription
  38. 1 destroy
  39. end
  40. end

app/use_cases/accounts/apps/chatwoots/create.rb

100.0% lines covered

7 relevant lines. 7 lines covered and 0 lines missed.
    
  1. 1 class Accounts::Apps::Chatwoots::Create
  2. 1 def self.call(account, chatwoot_params)
  3. 2 chatwoot = account.apps_chatwoots.build(chatwoot_params)
  4. 2 if chatwoot.save
  5. 1 Accounts::Apps::Chatwoots::SyncChatwootWorker.perform_async(account.id, chatwoot.id)
  6. 1 { ok: chatwoot }
  7. else
  8. 1 { error: chatwoot }
  9. end
  10. end
  11. end

app/use_cases/accounts/apps/chatwoots/create_conversation.rb

100.0% lines covered

6 relevant lines. 6 lines covered and 0 lines missed.
    
  1. 1 class Accounts::Apps::Chatwoots::CreateConversation
  2. 1 def self.call(chatwoot, contact_id, inbox_id)
  3. 4 request = Faraday.post(
  4. "#{chatwoot.chatwoot_endpoint_url}/api/v1/accounts/#{chatwoot.chatwoot_account_id}/conversations",
  5. build_body(contact_id, inbox_id).to_json,
  6. chatwoot.request_headers
  7. )
  8. 4 return { ok: JSON.parse(request.body) }
  9. end
  10. 1 def self.build_body(contact_id, inbox_id)
  11. {
  12. 4 "inbox_id": inbox_id,
  13. "contact_id": contact_id,
  14. }
  15. end
  16. end

app/use_cases/accounts/apps/chatwoots/delete.rb

83.33% lines covered

6 relevant lines. 5 lines covered and 1 lines missed.
    
  1. 1 class Accounts::Apps::Chatwoots::Delete
  2. 1 def self.call(account, chatwoot)
  3. 1 if chatwoot.destroy
  4. 1 Accounts::Apps::Chatwoots::RemoveChatwootIdFromContactsWorker.perform_async(account.id)
  5. 1 { ok: 'Chatwoot was successfully destroyed.' }
  6. else
  7. { error: chatwoot }
  8. end
  9. end
  10. end

app/use_cases/accounts/apps/chatwoots/export_contact.rb

100.0% lines covered

38 relevant lines. 38 lines covered and 0 lines missed.
    
  1. 1 class Accounts::Apps::Chatwoots::ExportContact
  2. 1 def self.call(chatwoot, contact)
  3. 9 create_or_update_contact(chatwoot, contact)
  4. end
  5. 1 def self.create_or_update_contact(chatwoot, contact)
  6. 9 contact_chatwoot_id = contact['additional_attributes']['chatwoot_id']
  7. 9 if contact_chatwoot_id.present?
  8. 2 update_contact(chatwoot, contact)
  9. else
  10. 7 create_contact(chatwoot, contact)
  11. end
  12. end
  13. 1 def self.update_contact(chatwoot, contact)
  14. 2 request = Faraday.put(
  15. "#{chatwoot.chatwoot_endpoint_url}/api/v1/accounts/#{chatwoot.chatwoot_account_id}/contacts/#{contact.additional_attributes['chatwoot_id']}",
  16. build_body(contact),
  17. chatwoot.request_headers
  18. )
  19. 2 if request.status == 200
  20. 1 export_contact_tags(chatwoot, contact)
  21. 1 { ok: contact }
  22. else
  23. 1 { error: request.body }
  24. end
  25. end
  26. 1 def self.export_contact_tags(chatwoot, contact)
  27. 4 request = Faraday.post(
  28. "#{chatwoot.chatwoot_endpoint_url}/api/v1/accounts/#{chatwoot.chatwoot_account_id}/contacts/#{contact.additional_attributes['chatwoot_id']}/labels",
  29. { labels: contact.label_list }.to_json,
  30. chatwoot.request_headers
  31. )
  32. 4 JSON.parse(request.body)['payload']
  33. end
  34. 1 def self.create_contact(chatwoot, contact)
  35. 7 request = Faraday.post(
  36. "#{chatwoot.chatwoot_endpoint_url}/api/v1/accounts/#{chatwoot.chatwoot_account_id}/contacts",
  37. build_body(contact),
  38. chatwoot.request_headers
  39. )
  40. 7 response_body = JSON.parse(request.body)
  41. 7 if response_body['message'] == 'Email has already been taken' && request.status == 422
  42. 1 search_chatwoot_contact = Accounts::Apps::Chatwoots::SearchContact.call(chatwoot, contact['email'])
  43. 1 update_contact_chatwoot_id_and_identifier(contact, search_chatwoot_contact['id'],
  44. search_chatwoot_contact['identifier'])
  45. 1 { ok: contact }
  46. 6 elsif response_body['message'] == 'Phone number has already been taken' && request.status == 422
  47. 1 search_chatwoot_contact = Accounts::Apps::Chatwoots::SearchContact.call(chatwoot, contact['phone'])
  48. 1 update_contact_chatwoot_id_and_identifier(contact, search_chatwoot_contact['id'],
  49. search_chatwoot_contact['identifier'])
  50. 1 { ok: contact }
  51. 5 elsif request.status == 200
  52. 3 update_contact_chatwoot_id_and_identifier(contact, response_body['payload']['contact']['id'],
  53. response_body['payload']['contact']['identifier'])
  54. 3 export_contact_tags(chatwoot, contact)
  55. 3 { ok: contact }
  56. else
  57. 2 Rails.logger.error(
  58. 'Error when export contact to chatwoot,' +
  59. "Chatwoot Apps: #{chatwoot.inspect}," +
  60. "Chatwoot request: #{request.inspect}," +
  61. "Chatwoot response: #{request.body}"
  62. )
  63. 2 { error: request.body }
  64. end
  65. end
  66. 1 def self.update_contact_chatwoot_id_and_identifier(contact, chatwoot_id, chatwoot_identifier)
  67. 5 contact.update(additional_attributes: contact['additional_attributes'].merge({ chatwoot_id: chatwoot_id,
  68. chatwoot_identifier: chatwoot_identifier }))
  69. end
  70. 1 def self.build_body(contact)
  71. {
  72. 9 "name": contact['full_name'],
  73. "email": contact['email'],
  74. "phone_number": contact['phone'],
  75. "custom_attributes": contact['custom_attributes']
  76. }.to_json
  77. end
  78. end

app/use_cases/accounts/apps/chatwoots/export_contact_worker.rb

100.0% lines covered

7 relevant lines. 7 lines covered and 0 lines missed.
    
  1. 1 class Accounts::Apps::Chatwoots::ExportContactWorker
  2. 1 include Sidekiq::Worker
  3. 1 sidekiq_options queue: :chatwoot_webhooks
  4. 1 def perform(chatwoot_id, contact_id)
  5. 1 contact = Contact.find_by_id(contact_id)
  6. 1 chatwoot = Apps::Chatwoot.find_by_id(chatwoot_id)
  7. 1 Accounts::Apps::Chatwoots::ExportContact.call(chatwoot, contact) if contact.present? && chatwoot.present?
  8. end
  9. end

app/use_cases/accounts/apps/chatwoots/find_or_create_conversation.rb

100.0% lines covered

5 relevant lines. 5 lines covered and 0 lines missed.
    
  1. 1 class Accounts::Apps::Chatwoots::FindOrCreateConversation
  2. 1 def self.call(chatwoot, contact_id, inbox_id)
  3. 9 conversations = Accounts::Apps::Chatwoots::GetConversations.call(
  4. chatwoot, contact_id, inbox_id
  5. )
  6. 9 return { ok: conversations.dig(:ok, 0) } if conversations.dig(:ok, 0, 'id').present?
  7. 3 Accounts::Apps::Chatwoots::CreateConversation.call(
  8. chatwoot, contact_id, inbox_id
  9. )
  10. end
  11. end

app/use_cases/accounts/apps/chatwoots/get_conversation_and_send_message.rb

100.0% lines covered

4 relevant lines. 4 lines covered and 0 lines missed.
    
  1. 1 class Accounts::Apps::Chatwoots::GetConversationAndSendMessage
  2. 1 def self.call(chatwoot, contact_id, inbox_id, event)
  3. 7 conversation = Accounts::Apps::Chatwoots::FindOrCreateConversation.call(
  4. chatwoot, contact_id, inbox_id
  5. )[:ok]
  6. 7 Accounts::Apps::Chatwoots::SendMessage.call(chatwoot, conversation['id'], event)
  7. end
  8. end

app/use_cases/accounts/apps/chatwoots/get_conversations.rb

100.0% lines covered

9 relevant lines. 9 lines covered and 0 lines missed.
    
  1. 1 class Accounts::Apps::Chatwoots::GetConversations
  2. 1 def self.call(chatwoot, contact_id, inbox_id = nil)
  3. 11 request = Faraday.get(
  4. "#{chatwoot.chatwoot_endpoint_url}/api/v1/accounts/#{chatwoot.chatwoot_account_id}/contacts/#{contact_id}/conversations",
  5. {},
  6. chatwoot.request_headers
  7. )
  8. 11 conversation_list = JSON.parse(request.body)['payload']
  9. 11 return { ok: conversation_list } if inbox_id.nil?
  10. 5 { ok: list_conversations_by_inbox(conversation_list, inbox_id) }
  11. end
  12. 1 def self.list_conversations_by_inbox(conversation_list, inbox_id)
  13. 5 conversation_list.select do |conversation|
  14. 32 conversation['inbox_id'] == inbox_id.to_i
  15. end
  16. end
  17. end

app/use_cases/accounts/apps/chatwoots/get_inboxes.rb

100.0% lines covered

5 relevant lines. 5 lines covered and 0 lines missed.
    
  1. 1 class Accounts::Apps::Chatwoots::GetInboxes
  2. 1 def self.call(chatwoot)
  3. 8 inboxes_request = Faraday.get(
  4. "#{chatwoot.chatwoot_endpoint_url}/api/v1/accounts/#{chatwoot.chatwoot_account_id}/inboxes",
  5. {},
  6. chatwoot.request_headers
  7. )
  8. 7 return { ok: JSON.parse(inboxes_request.body)['payload'] } if inboxes_request.status == 200
  9. 3 { error: inboxes_request.body }
  10. end
  11. end

app/use_cases/accounts/apps/chatwoots/messages/delivery_job.rb

92.31% lines covered

13 relevant lines. 12 lines covered and 1 lines missed.
    
  1. 1 class Accounts::Apps::Chatwoots::Messages::DeliveryJob < ApplicationJob
  2. 1 self.queue_adapter = :good_job
  3. 1 def perform(event_id)
  4. 6 event = Event.find(event_id)
  5. 6 if event.should_delivery_event_scheduled?
  6. 6 result = Accounts::Apps::Chatwoots::GetConversationAndSendMessage.call(
  7. event.app,
  8. event.contact.additional_attributes['chatwoot_id'],
  9. event.additional_attributes['chatwoot_inbox_id'],
  10. event
  11. )
  12. 6 if result.key?(:ok)
  13. 6 event.additional_attributes['chatwoot_id'] = result[:ok]['id']
  14. 6 event.additional_attributes['chatwoot_conversation_id'] = result[:ok]['conversation_id']
  15. 6 event.done = true
  16. 6 event.save!
  17. 6 { ok: event }
  18. else
  19. { error: result[:error] }
  20. end
  21. end
  22. end
  23. end

app/use_cases/accounts/apps/chatwoots/remove_chatwoot_id_from_contacts.rb

100.0% lines covered

6 relevant lines. 6 lines covered and 0 lines missed.
    
  1. 1 class Accounts::Apps::Chatwoots::RemoveChatwootIdFromContacts
  2. 1 def self.call(account)
  3. 1 account.contacts.where("additional_attributes -> 'chatwoot_id' IS NOT NULL").find_each do |contact|
  4. 1 contact.additional_attributes.delete('chatwoot_id')
  5. 1 contact.save
  6. end
  7. 1 return { ok: 'Contacts chatwoot id was successfully removed.' }
  8. end
  9. end

app/use_cases/accounts/apps/chatwoots/remove_chatwoot_id_from_contacts_worker.rb

100.0% lines covered

6 relevant lines. 6 lines covered and 0 lines missed.
    
  1. 1 class Accounts::Apps::Chatwoots::RemoveChatwootIdFromContactsWorker
  2. 1 include Sidekiq::Worker
  3. 1 sidekiq_options queue: :chatwoot_webhooks
  4. 1 def perform(account_id)
  5. 1 account = Account.find(account_id)
  6. 1 Accounts::Apps::Chatwoots::RemoveChatwootIdFromContacts.call(account)
  7. end
  8. end

app/use_cases/accounts/apps/chatwoots/search_contact.rb

100.0% lines covered

8 relevant lines. 8 lines covered and 0 lines missed.
    
  1. 1 class Accounts::Apps::Chatwoots::SearchContact
  2. 1 def self.call(chatwoot, params)
  3. 4 request = Faraday.get(
  4. "#{chatwoot.chatwoot_endpoint_url}/api/v1/accounts/#{chatwoot.chatwoot_account_id}/contacts/search",
  5. build_body(params),
  6. chatwoot.request_headers
  7. )
  8. 4 body = JSON.parse(request.body)
  9. 4 return body['payload'].first if body['payload'].present?
  10. 1 return { error: 'Contact not found' }
  11. end
  12. 1 def self.build_body(content)
  13. {
  14. 4 "q": content,
  15. }
  16. end
  17. end

app/use_cases/accounts/apps/chatwoots/send_message.rb

96.15% lines covered

26 relevant lines. 25 lines covered and 1 lines missed.
    
  1. 1 class Accounts::Apps::Chatwoots::SendMessage
  2. 1 def self.call(chatwoot, conversation_id, event)
  3. 10 if event.attachment.present?
  4. 2 send_message_with_attachment(chatwoot, conversation_id, event)
  5. else
  6. 8 send_message_without_attachment(chatwoot, conversation_id, event)
  7. end
  8. end
  9. 1 def self.send_message_with_attachment(chatwoot, conversation_id, event)
  10. 2 require 'uri'
  11. 2 require 'net/http'
  12. 2 url = URI("#{chatwoot.chatwoot_endpoint_url}/api/v1/accounts/#{chatwoot.chatwoot_account_id}/conversations/#{conversation_id}/messages")
  13. 2 https = Net::HTTP.new(url.host, url.port)
  14. 2 https.use_ssl = true
  15. 2 request = Net::HTTP::Post.new(url)
  16. 2 request['api_access_token'] = chatwoot.chatwoot_user_token
  17. 2 form_data = [['attachments[]', event.attachment.file_download],
  18. ['content', event.generate_content_hash('content', event.content)['content'].to_s]]
  19. 2 request.set_form form_data, 'multipart/form-data'
  20. 2 response = https.request(request)
  21. 2 { ok: JSON.parse(response.read_body) }
  22. end
  23. 1 def self.send_message_without_attachment(chatwoot, conversation_id, event)
  24. 8 request = Faraday.post(
  25. "#{chatwoot.chatwoot_endpoint_url}/api/v1/accounts/#{chatwoot.chatwoot_account_id}/conversations/#{conversation_id}/messages",
  26. build_body(event).to_json,
  27. request_headers(event, chatwoot)
  28. )
  29. 8 { ok: JSON.parse(request.body) }
  30. end
  31. 1 def self.build_body(event)
  32. 8 event.generate_content_hash('content', event.content)
  33. end
  34. 1 def self.request_headers(event, chatwoot)
  35. 8 if event.attachment.present?
  36. { 'api_access_token': chatwoot.chatwoot_user_token.to_s,
  37. 'Content-Type': 'multipart/form-data; boundary=----WebKitFormBoundary' }
  38. else
  39. 8 chatwoot.request_headers
  40. end
  41. end
  42. end

app/use_cases/accounts/apps/chatwoots/sync_chatwoot_worker.rb

50.0% lines covered

8 relevant lines. 4 lines covered and 4 lines missed.
    
  1. 1 class Accounts::Apps::Chatwoots::SyncChatwootWorker
  2. 1 include Sidekiq::Worker
  3. 1 sidekiq_options queue: :chatwoot_webhooks
  4. 1 def perform(account_id, chatwoot_id)
  5. chatwoot = Apps::Chatwoot.find(chatwoot_id)
  6. account = Account.find(account_id)
  7. Accounts::Apps::Chatwoots::SyncImportContacts.new(chatwoot).call
  8. Accounts::Apps::Chatwoots::SyncExportContacts.call(account)
  9. end
  10. end

app/use_cases/accounts/apps/chatwoots/sync_export_contacts.rb

100.0% lines covered

9 relevant lines. 9 lines covered and 0 lines missed.
    
  1. 1 class Accounts::Apps::Chatwoots::SyncExportContacts
  2. 1 def self.call(account)
  3. 1 export_contacts(account)
  4. end
  5. 1 def self.export_contacts(account)
  6. 1 account.contacts.where("additional_attributes -> 'chatwoot_id' IS NULL").find_in_batches(batch_size: 30) do |group|
  7. 1 group.each do |contact|
  8. 1 Accounts::Apps::Chatwoots::ExportContact.call(account.apps_chatwoots.first, contact)
  9. end
  10. 1 sleep(15)
  11. end
  12. 1 { ok: 'Contacts exported successfully' }
  13. end
  14. end

app/use_cases/accounts/apps/chatwoots/sync_import_contacts.rb

93.48% lines covered

46 relevant lines. 43 lines covered and 3 lines missed.
    
  1. 1 class Accounts::Apps::Chatwoots::SyncImportContacts
  2. 1 def initialize(chatwoot)
  3. 6 @chatwoot = chatwoot
  4. 6 @account = @chatwoot.account
  5. end
  6. 1 def call
  7. 6 response = update_or_create_contact
  8. 6 { ok: response }
  9. end
  10. 1 def update_or_create_contact
  11. 6 contacts_imported = 0
  12. 6 contacts_failed = 0
  13. 6 contacts_updated = 0
  14. 6 quantity_per_page = 1
  15. 6 page = 0
  16. 6 until quantity_per_page.zero?
  17. 18 page += 1
  18. 18 request = Faraday.get(
  19. "#{@chatwoot.chatwoot_endpoint_url}/api/v1/accounts/#{@chatwoot.chatwoot_account_id}/contacts/",
  20. { page: page },
  21. @chatwoot.request_headers
  22. )
  23. 18 if request.status == 200
  24. 18 chatwoot_contacts = JSON.parse(request.body)['payload']
  25. 18 quantity_per_page = chatwoot_contacts.count
  26. 18 chatwoot_contacts.each do |chatwoot_contact|
  27. 12 contact = Accounts::Contacts::GetByParams.call(@account,
  28. chatwoot_contact.slice('email',
  29. 'phone', 'identifier').transform_values(&:to_s))
  30. 12 if contact[:ok]
  31. 3 update_contact_chatwoot_id(contact[:ok], chatwoot_contact['id'])
  32. 3 import_labels(contact[:ok])
  33. 3 contact[:ok].save
  34. 3 contacts_updated += 1
  35. else
  36. 9 contact = build_contact_att(chatwoot_contact)
  37. 9 import_labels(contact)
  38. 9 if contact.save
  39. 9 contacts_imported += 1
  40. else
  41. contacts_failed += 1
  42. Rails.logger.error("Error import contact from chatwoot #{contact.errors.inspect}, chatwoot: #{@chatwoot.inspect}")
  43. end
  44. end
  45. end
  46. else
  47. return { error: request.body }
  48. end
  49. end
  50. 6 "Contacts imported #{contacts_imported} / Contacts updated #{contacts_updated} / Contacts failed #{contacts_failed}"
  51. end
  52. 1 def update_contact_chatwoot_id(contact, chatwoot_id)
  53. 3 contact.additional_attributes.merge!({ 'chatwoot_id' => chatwoot_id })
  54. end
  55. 1 def import_labels(contact)
  56. 12 Accounts::Apps::Chatwoots::Webhooks::ImportContact.import_contact_tags(@chatwoot, contact)
  57. 12 Accounts::Apps::Chatwoots::Webhooks::ImportContact.import_contact_converstions_tags(@chatwoot, contact)
  58. end
  59. 1 def build_contact_att(body)
  60. 9 contact = @account.contacts.new(
  61. full_name: body['name'],
  62. 9 email: (body['email']).to_s,
  63. 9 phone: (body['phone_number']).to_s
  64. )
  65. 9 contact.additional_attributes.merge!({ 'chatwoot_id' => body['id'], 'chatwoot_identifier' => body['identifier'] })
  66. 9 contact.custom_attributes.merge!(body['custom_attributes'])
  67. 9 contact
  68. end
  69. end

app/use_cases/accounts/apps/chatwoots/sync_inboxes.rb

40.0% lines covered

5 relevant lines. 2 lines covered and 3 lines missed.
    
  1. 1 class Accounts::Apps::Chatwoots::SyncInboxes
  2. 1 def self.call(chatwoot)
  3. inboxes_request = Faraday.get(
  4. "#{chatwoot.chatwoot_endpoint_url}/api/v1/accounts/#{chatwoot.chatwoot_account_id}/inboxes",
  5. {},
  6. chatwoot.request_headers
  7. )
  8. chatwoot.update(inboxes: JSON.parse(inboxes_request.body)['payload'])
  9. return true
  10. end
  11. end

app/use_cases/accounts/apps/chatwoots/webhooks/events/contact.rb

100.0% lines covered

3 relevant lines. 3 lines covered and 0 lines missed.
    
  1. 1 class Accounts::Apps::Chatwoots::Webhooks::Events::Contact
  2. 1 def self.call(chatwoot, webhook)
  3. 3 return Accounts::Apps::Chatwoots::Webhooks::ImportContact.call(chatwoot, webhook['id'])
  4. end
  5. end

app/use_cases/accounts/apps/chatwoots/webhooks/events/conversation_updated.rb

100.0% lines covered

4 relevant lines. 4 lines covered and 0 lines missed.
    
  1. 1 class Accounts::Apps::Chatwoots::Webhooks::Events::ConversationUpdated
  2. 1 def self.call(chatwoot, webhook)
  3. 1 contact = Accounts::Apps::Chatwoots::Webhooks::ImportContact.call(chatwoot, webhook['contact_inbox']['contact_id'])
  4. 1 return { ok: contact }
  5. end
  6. end

app/use_cases/accounts/apps/chatwoots/webhooks/events/message.rb

100.0% lines covered

5 relevant lines. 5 lines covered and 0 lines missed.
    
  1. 1 class Accounts::Apps::Chatwoots::Webhooks::Events::Message
  2. 1 def self.call(chatwoot, webhook)
  3. 25 contact = Accounts::Apps::Chatwoots::Webhooks::ImportContact.call(chatwoot, webhook['conversation']['contact_inbox']['contact_id'])[:ok]
  4. 25 message = Accounts::Apps::Chatwoots::Webhooks::ImportMessage.new(chatwoot, contact, webhook).call
  5. 25 return { ok: message }
  6. end
  7. end

app/use_cases/accounts/apps/chatwoots/webhooks/import_contact.rb

93.88% lines covered

49 relevant lines. 46 lines covered and 3 lines missed.
    
  1. 1 class Accounts::Apps::Chatwoots::Webhooks::ImportContact
  2. 1 def self.call(chatwoot, contact_id)
  3. 29 contact = get_or_import_contact(chatwoot, contact_id)
  4. 29 { ok: contact }
  5. end
  6. 1 def self.get_or_import_contact(chatwoot, contact_id)
  7. 29 contact = Contact.by_chatwoot_id(contact_id).first
  8. 29 contact_att = get_contact(chatwoot, contact_id)
  9. 29 return 'Contact not found' if contact_att == false
  10. 28 contact = if contact.present?
  11. 1 update_contact(contact, contact_att)
  12. else
  13. 27 import_contact(chatwoot, contact_att)
  14. end
  15. 28 contact = import_contact_tags(chatwoot, contact)
  16. 28 contact = import_contact_converstions_tags(chatwoot, contact)
  17. 28 contact.save
  18. 28 contact
  19. end
  20. 1 def self.get_contact(chatwoot, contact_id)
  21. 29 contact_response = Faraday.get(
  22. "#{chatwoot.chatwoot_endpoint_url}/api/v1/accounts/#{chatwoot.chatwoot_account_id}/contacts/#{contact_id}",
  23. {},
  24. chatwoot.request_headers
  25. )
  26. 29 if contact_response.status == 200
  27. 28 body = JSON.parse(contact_response.body)
  28. 28 body['payload']
  29. 1 elsif contact_response.status == 404
  30. 1 Rails.logger.info "Contact id #{contact_id} not found in Chatwoot App #{chatwoot.id}"
  31. 1 false
  32. else
  33. Rails.logger.info "contact_response: #{contact_response.inspect}"
  34. Rails.logger.info "contact_response body: #{contact_response.body}"
  35. raise 'ErrorChatwootGetContact'
  36. end
  37. end
  38. 1 def self.import_contact_converstions_tags(chatwoot, contact)
  39. 40 contact_response = Faraday.get(
  40. "#{chatwoot.chatwoot_endpoint_url}/api/v1/accounts/#{chatwoot.chatwoot_account_id}/contacts/#{contact.additional_attributes['chatwoot_id']}/conversations",
  41. {},
  42. chatwoot.request_headers
  43. )
  44. 40 body = JSON.parse(contact_response.body)
  45. 80 conversations_tags = body['payload'].map { |c| c['labels'] }.flatten.uniq
  46. 40 contact.assign_attributes({ chatwoot_conversations_label_list: conversations_tags })
  47. 40 contact
  48. end
  49. 1 def self.import_contact_tags(chatwoot, contact)
  50. 40 contact_response = Faraday.get(
  51. "#{chatwoot.chatwoot_endpoint_url}/api/v1/accounts/#{chatwoot.chatwoot_account_id}/contacts/#{contact.additional_attributes['chatwoot_id']}/labels",
  52. {},
  53. chatwoot.request_headers
  54. )
  55. 40 body = JSON.parse(contact_response.body)
  56. 40 contact.assign_attributes({ label_list: body['payload'] })
  57. 40 contact
  58. end
  59. 1 def self.import_contact(chatwoot, contact_att)
  60. 27 contact = chatwoot.account.contacts.new
  61. 27 build_contact_att(contact, contact_att)
  62. end
  63. 1 def self.update_contact(contact, contact_att)
  64. 1 build_contact_att(contact, contact_att)
  65. end
  66. 1 def self.build_contact_att(contact, body)
  67. 28 contact.assign_attributes({
  68. full_name: body['name'],
  69. 28 email: (body['email']).to_s,
  70. 28 phone: (body['phone_number']).to_s
  71. })
  72. 28 contact.additional_attributes.merge!({ 'chatwoot_id' => body['id'], 'chatwoot_identifier' => body['identifier'] })
  73. 28 contact.custom_attributes.merge!(body['custom_attributes'])
  74. 28 contact
  75. end
  76. end

app/use_cases/accounts/apps/chatwoots/webhooks/import_message.rb

100.0% lines covered

46 relevant lines. 46 lines covered and 0 lines missed.
    
  1. 1 class Accounts::Apps::Chatwoots::Webhooks::ImportMessage
  2. 1 require 'open-uri'
  3. 1 def initialize(chatwoot, contact, webhook)
  4. 25 @chatwoot = chatwoot
  5. 25 @contact = contact
  6. 25 @webhook = webhook
  7. end
  8. 1 def call
  9. 25 message = get_or_import_message()
  10. 25 { ok: message }
  11. end
  12. 1 def get_or_import_message()
  13. 25 message = @contact.events.where(
  14. '? <@ additional_attributes', { chatwoot_id: @webhook['id'] }.to_json
  15. ).first
  16. 25 if message.nil?
  17. 25 if attachments?
  18. 3 message = import_message_with_attachments()
  19. else
  20. 22 message = import_message()
  21. end
  22. end
  23. 25 message
  24. end
  25. 1 def import_message_with_attachments()
  26. 3 @webhook['attachments'].reverse.map.with_index do |attachment, index|
  27. 5 import_message(
  28. { attachment: attachment, order: index, last_element: last_element?(index)}
  29. )
  30. end
  31. end
  32. 1 def last_element?(index)
  33. 5 index == @webhook['attachments'].count - 1
  34. end
  35. 1 def import_message(attachment_params = {})
  36. 27 message = @contact.events.new(
  37. account: @chatwoot.account,
  38. kind: 'chatwoot_message',
  39. from_me: is_from_me?(),
  40. contact: @contact,
  41. done: true,
  42. done_at: build_done_at(attachment_params[:order]),
  43. app: @chatwoot
  44. )
  45. 27 message.additional_attributes.merge!({ 'chatwoot_id' => @webhook['conversation']['messages'].first['id'] })
  46. 27 if attachment_params.present?
  47. 5 create_attachment(message, attachment_params[:attachment])
  48. 5 message.content = @webhook['content'] if attachment_params[:last_element] == true
  49. else
  50. 22 message.content = @webhook['content']
  51. end
  52. 27 message.save
  53. 27 message
  54. end
  55. 1 def create_attachment(event, attachment_params)
  56. begin
  57. 5 downloaded_file = URI.open(attachment_params['data_url'])
  58. 4 attachment = event.build_attachment(
  59. file_type: attachment_params['file_type']
  60. )
  61. 4 attachment.file.attach(io: downloaded_file,
  62. filename: File.basename(attachment_params['data_url']))
  63. rescue OpenURI::HTTPError
  64. 1 event.status = 'failed'
  65. end
  66. end
  67. 1 def build_done_at(order = nil)
  68. 27 if order.present?
  69. 5 created_at = @webhook['created_at'].dup
  70. 5 created_at.to_time + miliseconds(order)
  71. else
  72. 22 @webhook['created_at'].to_time
  73. end
  74. end
  75. 1 def miliseconds(miliseconds)
  76. 5 miliseconds/1000.0
  77. end
  78. 1 def attachments?()
  79. 25 @webhook['attachments'].present?
  80. end
  81. 1 def is_from_me?()
  82. 27 @webhook.dig('sender', 'type') == 'user' if @webhook.dig('sender', 'id').present?
  83. end
  84. end

app/use_cases/accounts/apps/chatwoots/webhooks/process_webhook.rb

91.67% lines covered

12 relevant lines. 11 lines covered and 1 lines missed.
    
  1. 1 class Accounts::Apps::Chatwoots::Webhooks::ProcessWebhook
  2. 1 def self.call(webhook)
  3. 24 chatwoot = Apps::Chatwoot.find_by(embedding_token: webhook['token'])
  4. 24 return { error: 'Chatwoot integration not found' } if chatwoot.blank?
  5. 23 return { error: 'Chatwoot integration inactive' } if chatwoot.inactive?
  6. 22 if webhook['event'].include?('contact_')
  7. 2 Accounts::Apps::Chatwoots::Webhooks::Events::Contact.call(
  8. chatwoot, webhook
  9. )
  10. 20 elsif webhook['event'] == 'conversation_updated'
  11. Accounts::Apps::Chatwoots::Webhooks::Events::ConversationUpdated.call(
  12. chatwoot, webhook
  13. )
  14. 20 elsif webhook['event'].include?('message_')
  15. 20 Accounts::Apps::Chatwoots::Webhooks::Events::Message.call(
  16. chatwoot, webhook
  17. )
  18. end
  19. 22 { ok: chatwoot }
  20. end
  21. end

app/use_cases/accounts/apps/chatwoots/webhooks/process_webhook_job.rb

100.0% lines covered

8 relevant lines. 8 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 class Accounts::Apps::Chatwoots::Webhooks::ProcessWebhookJob < ApplicationJob
  3. 1 include GoodJob::ActiveJobExtensions::Concurrency
  4. 1 self.queue_adapter = :good_job
  5. 1 good_job_control_concurrency_with(
  6. # Maximum number of jobs with the concurrency key to be
  7. # concurrently performed (excludes enqueued jobs)
  8. perform_limit: 1,
  9. 60 key: -> { "#{self.class.name}-#{arguments.last}" }
  10. )
  11. 1 def perform(event, _token)
  12. 20 event_hash = JSON.parse(event)
  13. 20 Accounts::Apps::Chatwoots::Webhooks::ProcessWebhook.call(event_hash)
  14. end
  15. end

app/use_cases/accounts/apps/evolution_apis/create.rb

100.0% lines covered

7 relevant lines. 7 lines covered and 0 lines missed.
    
  1. 1 class Accounts::Apps::EvolutionApis::Create
  2. 1 def self.call(user, evolution_apis_params)
  3. 2 evolution_api = EvolutionApiBuilder.new(user, evolution_apis_params).build
  4. 2 if evolution_api.save
  5. 1 Accounts::Apps::EvolutionApis::Instance::Create.call(evolution_api)
  6. 1 return { ok: evolution_api }
  7. else
  8. 1 return { error: evolution_api }
  9. end
  10. end
  11. end

app/use_cases/accounts/apps/evolution_apis/instance/create.rb

91.67% lines covered

12 relevant lines. 11 lines covered and 1 lines missed.
    
  1. 1 class Accounts::Apps::EvolutionApis::Instance::Create
  2. 1 def self.call(evolution_api)
  3. 4 evolution_api.update(connection_status: 'connecting')
  4. 4 request = Faraday.post(
  5. "#{evolution_api.endpoint_url}/instance/create",
  6. build_body(evolution_api).to_json,
  7. {'apiKey': "#{ENV['EVOLUTION_API_ENDPOINT_TOKEN']}", 'Content-Type': 'application/json'}
  8. )
  9. 4 if request.status == 201
  10. # set_settings(evolution_api)
  11. 3 return { ok: JSON.parse(request.body) }
  12. else
  13. 1 evolution_api.update(connection_status: 'disconnected')
  14. 1 return { error: JSON.parse(request.body) }
  15. end
  16. end
  17. 1 def self.set_settings(evolution_api)
  18. Faraday.post(
  19. "#{evolution_api.endpoint_url}/settings/set/#{evolution_api.instance}",
  20. {
  21. "reject_call": false,
  22. "groups_ignore": false,
  23. "always_online": false,
  24. "read_messages": false,
  25. "read_status": false
  26. }.to_json,
  27. evolution_api.request_instance_headers
  28. )
  29. end
  30. 1 def self.build_body(evolution_api)
  31. {
  32. 4 "instanceName": evolution_api.instance,
  33. "token": evolution_api.token,
  34. "qrcode": true,
  35. "webhook": evolution_api.woofedcrm_webhooks_url,
  36. "events": [
  37. "QRCODE_UPDATED",
  38. "MESSAGES_SET",
  39. "MESSAGES_UPSERT",
  40. "MESSAGES_UPDATE",
  41. "MESSAGES_DELETE",
  42. "SEND_MESSAGE",
  43. "CONTACTS_SET",
  44. "CONTACTS_UPSERT",
  45. "CONTACTS_UPDATE",
  46. "PRESENCE_UPDATE",
  47. "CHATS_SET",
  48. "CHATS_UPSERT",
  49. "CHATS_UPDATE",
  50. "CHATS_DELETE",
  51. "GROUPS_UPSERT",
  52. "GROUP_UPDATE",
  53. "GROUP_PARTICIPANTS_UPDATE",
  54. "CONNECTION_UPDATE",
  55. "CALL"
  56. ]
  57. }
  58. end
  59. end

app/use_cases/accounts/apps/evolution_apis/instance/delete_disconnected.rb

100.0% lines covered

18 relevant lines. 18 lines covered and 0 lines missed.
    
  1. 1 class Accounts::Apps::EvolutionApis::Instance::DeleteDisconnected
  2. 1 def initialize(evolution_api)
  3. 6 @evolution_api = evolution_api
  4. end
  5. 1 def call
  6. 6 if disconnected_or_deleted?
  7. 4 send_delete_instance_request
  8. 4 @evolution_api.update(connection_status: 'disconnected', qrcode: '', phone: '')
  9. else
  10. 2 { error: 'Cannot delete, instance is already active on evolution API server' }
  11. end
  12. end
  13. 1 def send_delete_instance_request
  14. 4 return unless @evolution_api_instance_found
  15. 2 request = Faraday.delete(
  16. "#{@evolution_api.endpoint_url}/instance/delete/#{@evolution_api.instance}",
  17. {},
  18. @evolution_api.request_instance_headers
  19. )
  20. 2 { ok: JSON.parse(request.body) }
  21. end
  22. 1 def disconnected_or_deleted?
  23. 6 request = Faraday.get(
  24. "#{@evolution_api.endpoint_url}/instance/connectionState/#{@evolution_api.instance}",
  25. {},
  26. @evolution_api.request_instance_headers
  27. )
  28. 6 @evolution_api_instance_found = request.status == 200
  29. 6 request_body = JSON.parse(request.body)
  30. 6 return true if request_body.dig('instance', 'state') == 'close' || request.status != 200
  31. 2 false
  32. end
  33. end

app/use_cases/accounts/apps/evolution_apis/instance/delete_disconnected_worker.rb

100.0% lines covered

5 relevant lines. 5 lines covered and 0 lines missed.
    
  1. 1 class Accounts::Apps::EvolutionApis::Instance::DeleteDisconnectedWorker
  2. 1 include Sidekiq::Worker
  3. 1 def perform(evolution_api_id)
  4. 1 evolution_api = Apps::EvolutionApi.find(evolution_api_id)
  5. 1 Accounts::Apps::EvolutionApis::Instance::DeleteDisconnected.new(evolution_api).call
  6. end
  7. end

app/use_cases/accounts/apps/evolution_apis/instance/sessions_refresh_status_job.rb

100.0% lines covered

5 relevant lines. 5 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 class Accounts::Apps::EvolutionApis::Instance::SessionsRefreshStatusJob < ApplicationJob
  3. 1 self.queue_adapter = :good_job
  4. 1 def perform
  5. 1 Apps::EvolutionApi.connected.find_each do |evolution_api|
  6. 2 Accounts::Apps::EvolutionApis::Instance::DeleteDisconnected.new(evolution_api).call
  7. end
  8. end
  9. end

app/use_cases/accounts/apps/evolution_apis/message/delivery_job.rb

100.0% lines covered

12 relevant lines. 12 lines covered and 0 lines missed.
    
  1. 1 class Accounts::Apps::EvolutionApis::Message::DeliveryJob < ApplicationJob
  2. 1 self.queue_adapter = :good_job
  3. 1 def perform(event_id)
  4. 9 @event = Event.find(event_id)
  5. 9 if @event.should_delivery_event_scheduled?
  6. 9 result = Accounts::Apps::EvolutionApis::Message::Send.new(@event).call
  7. 9 if result.key?(:ok)
  8. 7 @event.done = true
  9. 7 @event.additional_attributes.merge!({ 'message_id' => result[:ok]['key']['id'] })
  10. 7 @event.save!
  11. 7 { ok: @event }
  12. else
  13. 2 { error: result[:error] }
  14. end
  15. end
  16. end
  17. end

app/use_cases/accounts/apps/evolution_apis/message/import.rb

97.87% lines covered

47 relevant lines. 46 lines covered and 1 lines missed.
    
  1. 1 class Accounts::Apps::EvolutionApis::Message::Import
  2. 1 def initialize(evolution_api, webhook, content)
  3. 9 @evolution_api = evolution_api
  4. 9 @webhook = webhook
  5. 9 @content = content
  6. end
  7. 1 def call
  8. 9 result = create_evolution_api_message_event
  9. 9 { ok: result }
  10. end
  11. 1 def create_evolution_api_message_event
  12. 9 contact = find_or_create_contact
  13. 9 import_message(contact)
  14. end
  15. 1 def find_or_create_contact
  16. 9 if group_message?
  17. 1 find_or_create_group_contact
  18. else
  19. 8 find_or_create_person_contact
  20. end
  21. end
  22. 1 def find_or_create_person_contact
  23. 8 phone_number = '+' + @webhook['data']['key']['remoteJid'].gsub(/\D/, '')
  24. 8 contact = Accounts::Contacts::GetByParams.call(@evolution_api.account, { phone: phone_number })[:ok]
  25. 8 contact = create_person_contact(phone_number) if contact.blank?
  26. 8 update_contact_name_if_missing(contact)
  27. 8 contact
  28. end
  29. 1 def update_contact_name_if_missing(contact)
  30. 8 if @webhook['data']['key']['fromMe'].to_s == 'false' && contact.full_name.blank?
  31. 1 contact.update(full_name: @webhook['data']['pushName'])
  32. end
  33. end
  34. 1 def find_or_create_group_contact
  35. 1 group_id = @webhook['data']['key']['remoteJid']
  36. 1 group_details = group_details(group_id)
  37. contact_params = {
  38. 1 full_name: "#{group_details[:group_name]} - Grupo",
  39. additional_attributes: group_details
  40. }
  41. 1 contact = @evolution_api.account.contacts.where('additional_attributes @> ?', { group_id: group_id }.to_json).first
  42. 1 if contact.present?
  43. 1 contact.full_name = contact_params[:full_name]
  44. 1 contact.additional_attributes = contact.additional_attributes.merge(contact_params[:additional_attributes])
  45. else
  46. contact = ContactBuilder.new(
  47. @evolution_api.account.users.first,
  48. ActionController::Parameters.new(contact_params)
  49. ).perform
  50. end
  51. 1 contact.save!
  52. 1 contact
  53. end
  54. 1 def create_person_contact(phone_number)
  55. 2 if @webhook['data']['key']['fromMe'].to_s == 'true'
  56. 1 Contact.create(phone: phone_number,
  57. account: @evolution_api.account)
  58. else
  59. 1 Contact.create(full_name: @webhook['data']['pushName'], phone: phone_number,
  60. account: @evolution_api.account)
  61. end
  62. end
  63. 1 def group_details(group_id)
  64. 1 request = Faraday.get(
  65. "#{@evolution_api.endpoint_url}/group/findGroupInfos/#{@evolution_api.instance}?groupJid=#{group_id}",
  66. {},
  67. @evolution_api.request_instance_headers
  68. )
  69. 1 request_body = JSON.parse(request.body)
  70. 1 { group_id: group_id,
  71. group_name: request_body['subject'],
  72. group_owner_id: request_body['subjectOwner'] }
  73. end
  74. 1 def group_message?
  75. 9 @webhook['data']['key']['remoteJid'].gsub(/\D/, '').size > 15
  76. end
  77. 1 def import_message(contact)
  78. 9 Event.create(
  79. account: @evolution_api.account,
  80. kind: 'evolution_api_message',
  81. from_me: @webhook['data']['key']['fromMe'],
  82. contact: contact,
  83. content: @content,
  84. done: true,
  85. done_at: @webhook['date_time'],
  86. app: @evolution_api,
  87. additional_attributes: { message_id: @webhook['data']['key']['id'] }
  88. )
  89. end
  90. end

app/use_cases/accounts/apps/evolution_apis/message/send.rb

100.0% lines covered

31 relevant lines. 31 lines covered and 0 lines missed.
    
  1. 1 class Accounts::Apps::EvolutionApis::Message::Send
  2. 1 def initialize(event)
  3. 9 @event = event
  4. 9 @evolution_api = event.app
  5. 9 @phone = sending_to_group? ? event.contact.additional_attributes['group_id'] : @event.contact.phone
  6. end
  7. 1 def call
  8. 9 if @event.attachment.present?
  9. 3 send_message_with_attachment
  10. else
  11. 6 send_request('sendText', build_message_text_body)
  12. end
  13. end
  14. 1 def send_message_with_attachment
  15. 3 if @event.attachment.audio?
  16. 1 send_request('sendWhatsAppAudio', build_message_audio_body)
  17. else
  18. 2 send_request('sendMedia', build_message_file_body)
  19. end
  20. end
  21. 1 def send_request(type, body)
  22. 9 request = Faraday.post(
  23. "#{@evolution_api.endpoint_url}/message/#{type}/#{@evolution_api.instance}",
  24. body.to_json,
  25. @evolution_api.request_instance_headers
  26. )
  27. 9 if request.status == 201
  28. 7 { ok: JSON.parse(request.body) }
  29. else
  30. 2 { error: JSON.parse(request.body) }
  31. end
  32. end
  33. 1 def build_message_file_body
  34. 2 file_media_type = if @event.attachment.image? || @event.attachment.video?
  35. 1 @event.attachment.file_type
  36. else
  37. 1 'document'
  38. end
  39. {
  40. 2 "number": normalize_phone,
  41. "options": {
  42. "delay": 1200,
  43. "presence": 'composing',
  44. "linkPreview": false
  45. },
  46. "mediaMessage": {
  47. "mediatype": file_media_type,
  48. "caption": @event.generate_content_hash('content', @event.content)['content'],
  49. "media": @event.attachment.download_url
  50. }
  51. }
  52. end
  53. 1 def build_message_audio_body
  54. {
  55. 1 "number": normalize_phone,
  56. "options": {
  57. "delay": 1200,
  58. "presence": 'recording',
  59. "linkPreview": false
  60. },
  61. "audioMessage": {
  62. "audio": @event.attachment.download_url
  63. }
  64. }
  65. end
  66. 1 def build_message_text_body
  67. {
  68. 6 "number": normalize_phone,
  69. "options": {
  70. "delay": 1200,
  71. "presence": 'composing',
  72. "linkPreview": false
  73. },
  74. "textMessage": {
  75. "text": @event.content
  76. }
  77. }
  78. end
  79. 1 def normalize_phone
  80. 9 @phone.sub(/^\+/, '')
  81. end
  82. 1 def sending_to_group?
  83. 9 @event.contact.additional_attributes['group_id'].present?
  84. end
  85. end

app/use_cases/accounts/apps/evolution_apis/webhooks/events/connection_created.rb

100.0% lines covered

10 relevant lines. 10 lines covered and 0 lines missed.
    
  1. 1 class Accounts::Apps::EvolutionApis::Webhooks::Events::ConnectionCreated
  2. 1 def self.call(evolution_api, phone)
  3. 1 response = update_evolution_api(evolution_api, phone)
  4. 1 { ok: response }
  5. end
  6. 1 def self.update_evolution_api(evolution_api, phone)
  7. 1 evolution_api.connection_status = 'connected'
  8. 1 evolution_api.phone = "+#{phone}"
  9. 1 evolution_api.qrcode = ''
  10. 1 evolution_api.save
  11. 1 evolution_api
  12. end
  13. end

app/use_cases/accounts/apps/evolution_apis/webhooks/events/connection_deleted.rb

100.0% lines covered

5 relevant lines. 5 lines covered and 0 lines missed.
    
  1. 1 class Accounts::Apps::EvolutionApis::Webhooks::Events::ConnectionDeleted
  2. 1 def self.call(evolution_api)
  3. 3 if evolution_api.connected?
  4. 1 Accounts::Apps::EvolutionApis::Instance::DeleteDisconnectedWorker.perform_in(1.seconds, evolution_api.id)
  5. end
  6. 3 Accounts::Apps::EvolutionApis::Instance::DeleteDisconnected.new(evolution_api).call if evolution_api.connecting?
  7. end
  8. end

app/use_cases/accounts/apps/evolution_apis/webhooks/events/import_message.rb

100.0% lines covered

5 relevant lines. 5 lines covered and 0 lines missed.
    
  1. 1 class Accounts::Apps::EvolutionApis::Webhooks::Events::ImportMessage
  2. 1 def self.call(evolution_api, webhook, content)
  3. 11 if evolution_api.connected?
  4. 9 response = Accounts::Apps::EvolutionApis::Message::Import.new(evolution_api, webhook, content).call
  5. 9 { ok: response }
  6. end
  7. end
  8. end

app/use_cases/accounts/apps/evolution_apis/webhooks/events/qrcode_connect_refresh.rb

100.0% lines covered

5 relevant lines. 5 lines covered and 0 lines missed.
    
  1. 1 class Accounts::Apps::EvolutionApis::Webhooks::Events::QrcodeConnectRefresh
  2. 1 def self.call(evolution_api, qrcode)
  3. 3 if evolution_api.connecting?
  4. 1 evolution_api.update(qrcode: qrcode)
  5. 1 { ok: evolution_api }
  6. end
  7. end
  8. end

app/use_cases/accounts/apps/evolution_apis/webhooks/process_webhook.rb

100.0% lines covered

20 relevant lines. 20 lines covered and 0 lines missed.
    
  1. 1 class Accounts::Apps::EvolutionApis::Webhooks::ProcessWebhook
  2. 1 def self.call(webhook)
  3. 18 evolution_api = Apps::EvolutionApi.find_by(instance: webhook['instance'])
  4. 18 if webhook['event'] == 'qrcode.updated'
  5. 3 Accounts::Apps::EvolutionApis::Webhooks::Events::QrcodeConnectRefresh.call(
  6. evolution_api, webhook['data']['qrcode']['base64']
  7. )
  8. 15 elsif webhook['event'] == 'connection.update'
  9. 4 if connection_created?(webhook)
  10. 1 Accounts::Apps::EvolutionApis::Webhooks::Events::ConnectionCreated.call(evolution_api,
  11. webhook['sender'].gsub(/\D/, ''))
  12. 3 elsif connection_deleted?(webhook)
  13. 3 Accounts::Apps::EvolutionApis::Webhooks::Events::ConnectionDeleted.call(evolution_api)
  14. end
  15. 11 elsif webhook['event'] == 'messages.upsert'
  16. 11 if webhook['data']['messageType'] == 'extendedTextMessage'
  17. 8 Accounts::Apps::EvolutionApis::Webhooks::Events::ImportMessage.call(evolution_api, webhook,
  18. webhook['data']['message']['extendedTextMessage']['text'])
  19. 3 elsif webhook['data']['messageType'] == 'conversation'
  20. 3 Accounts::Apps::EvolutionApis::Webhooks::Events::ImportMessage.call(evolution_api, webhook,
  21. webhook['data']['message']['conversation'])
  22. end
  23. end
  24. 18 { ok: evolution_api }
  25. end
  26. 1 def self.connection_created?(webhook)
  27. 4 webhook['data']['statusReason'].to_i == 200 && webhook['data']['state'] == 'open'
  28. end
  29. 1 def self.connection_deleted?(webhook)
  30. 3 (webhook['data']['statusReason'].to_i == 401 || webhook['data']['statusReason'].to_i == 428) && webhook['data']['state'] == 'close'
  31. end
  32. end

app/use_cases/accounts/apps/evolution_apis/webhooks/process_webhook_worker.rb

100.0% lines covered

6 relevant lines. 6 lines covered and 0 lines missed.
    
  1. 1 class Accounts::Apps::EvolutionApis::Webhooks::ProcessWebhookWorker
  2. 1 include Sidekiq::Worker
  3. 1 sidekiq_options queue: :evolution_api_webhooks
  4. 1 def perform(event)
  5. 18 event_hash = JSON.parse(event)
  6. 18 Accounts::Apps::EvolutionApis::Webhooks::ProcessWebhook.call(event_hash)
  7. end
  8. end

app/use_cases/accounts/contacts/events/enqueue.rb

100.0% lines covered

6 relevant lines. 6 lines covered and 0 lines missed.
    
  1. 1 class Accounts::Contacts::Events::Enqueue
  2. 1 def self.call(event)
  3. 2 if event.chatwoot_message?
  4. 1 Accounts::Apps::Chatwoots::Messages::DeliveryJob.set(wait_until: event.scheduled_at).perform_later(event.id)
  5. 1 elsif event.evolution_api_message?
  6. 1 Accounts::Apps::EvolutionApis::Message::DeliveryJob.set(wait_until: event.scheduled_at).perform_later(event.id)
  7. end
  8. end
  9. end

app/use_cases/accounts/contacts/events/enqueue_worker.rb

100.0% lines covered

5 relevant lines. 5 lines covered and 0 lines missed.
    
  1. 1 class Accounts::Contacts::Events::EnqueueWorker
  2. 1 include Sidekiq::Worker
  3. 1 def perform(event_id)
  4. 2 event = Event.find(event_id)
  5. 2 Accounts::Contacts::Events::Enqueue.call(event)
  6. end
  7. end

app/use_cases/accounts/contacts/events/generate_ai_response.rb

97.37% lines covered

38 relevant lines. 37 lines covered and 1 lines missed.
    
  1. 1 class Accounts::Contacts::Events::GenerateAiResponse
  2. 1 def initialize(event)
  3. 4 @event = event
  4. 4 @account = event.account
  5. 4 @ai_assistent = Apps::AiAssistent.first
  6. end
  7. 1 def call
  8. 4 return '' if @ai_assistent.exceeded_usage_limit?
  9. 3 question = @event.content.to_s
  10. 3 context = get_context(question)
  11. 3 data = prepare_data(context, question)
  12. 3 response = post_request(data)
  13. 3 response_body = JSON.parse(response.body)
  14. 3 update_ai_usage(response_body['usage']['total_tokens'])
  15. 3 content = response_body.dig('output', 0, 'content', 0, 'text')
  16. 3 JSON.parse(content)['response']
  17. rescue StandardError
  18. ''
  19. end
  20. 1 def update_ai_usage(tokens)
  21. 3 @ai_assistent.usage['tokens'] += tokens
  22. 3 @ai_assistent.save
  23. end
  24. 1 def get_context(query)
  25. 3 embedding = OpenAi::Embeddings.new.get_embedding(@ai_assistent, query, 'text-embedding-3-small')
  26. 3 documents = EmbeddingDocumment.nearest_neighbors(:embedding, embedding, distance: 'cosine').first(6)
  27. 3 documents.pluck(:content, :source_reference)
  28. end
  29. 1 def post_request(data)
  30. 3 Rails.logger.info "Requesting Chat GPT with body: #{data}"
  31. 3 response = Faraday.post(
  32. 'https://api.openai.com/v1/responses',
  33. data.to_json,
  34. headers
  35. )
  36. 3 Rails.logger.info "Chat GPT response: #{response.body}"
  37. 3 response
  38. end
  39. 1 def headers
  40. {
  41. 3 'Content-Type' => 'application/json',
  42. 'Authorization' => "Bearer #{@ai_assistent.api_key}"
  43. }
  44. end
  45. 1 def prepare_data(context, question)
  46. {
  47. 3 model: @ai_assistent.model,
  48. input: build_prompt(context, question),
  49. text: response_format,
  50. max_output_tokens: 2048,
  51. temperature: 0.3,
  52. }
  53. end
  54. 1 def response_format
  55. {
  56. 3 format: {
  57. type: 'json_schema',
  58. name: 'suggestion',
  59. schema: {
  60. type: 'object',
  61. properties: {
  62. response: {
  63. type: 'string'
  64. },
  65. confidence: {
  66. type: 'integer'
  67. }
  68. },
  69. required: %w[response confidence],
  70. additionalProperties: false
  71. },
  72. strict: true
  73. }
  74. }
  75. end
  76. 1 def build_prompt(context, question)
  77. 3 system_prompt_message = <<~SYSTEM_PROMPT_MESSAGE
  78. You are an assistant that will help answer questions from potential customers.
  79. Only respond if you are 100% certain; otherwise, your response should be left blank.
  80. If it is relevant to the response, include the link to the page where the information was found so the user can obtain more details.
  81. Respond in the language the customer used to ask the question.
  82. Never make up information.
  83. Respond in a short and objective manner, always in plain text, without Markdown formatting, without lists, without bold text, without formatted code, and without special symbols.
  84. SYSTEM_PROMPT_MESSAGE
  85. 3 user_prompt_message = <<~USER_PROMPT_MESSAGE
  86. Context sections:
  87. #{context}
  88. Question:
  89. #{question}
  90. USER_PROMPT_MESSAGE
  91. [
  92. {
  93. 3 role: 'system',
  94. content: system_prompt_message
  95. },
  96. {
  97. role: 'user',
  98. content: user_prompt_message
  99. }
  100. ]
  101. end
  102. end

app/use_cases/accounts/contacts/events/send_now.rb

100.0% lines covered

10 relevant lines. 10 lines covered and 0 lines missed.
    
  1. 1 class Accounts::Contacts::Events::SendNow
  2. 1 def self.call(event)
  3. 10 event.send_now = nil
  4. 10 if event.chatwoot_message? || event.evolution_api_message?
  5. 7 event.update(scheduled_at: DateTime.current, auto_done: false)
  6. 7 if event.chatwoot_message?
  7. 4 Accounts::Apps::Chatwoots::Messages::DeliveryJob.perform_later(event.id)
  8. 3 elsif event.evolution_api_message?
  9. 3 Accounts::Apps::EvolutionApis::Message::DeliveryJob.perform_later(event.id)
  10. end
  11. else
  12. 3 event.update(done: true)
  13. end
  14. end
  15. end

app/use_cases/accounts/contacts/events/woofbot.rb

100.0% lines covered

21 relevant lines. 21 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 class Accounts::Contacts::Events::Woofbot
  3. 1 def initialize(event)
  4. 1 @event = event
  5. 1 @account = event.account
  6. 1 @ai_assistent = Apps::AiAssistent.first
  7. end
  8. 1 def call
  9. 1 return unless woofbot_should_be_run?
  10. 1 @woofbot_response = Accounts::Contacts::Events::GenerateAiResponse.new(@event).call
  11. 1 create_reply_event if @woofbot_response.present?
  12. end
  13. 1 def create_reply_event
  14. event_params = {
  15. 1 kind: @event.kind,
  16. contact_id: @event.contact_id,
  17. app_type: @event.app_type,
  18. app_id: @event.app_id,
  19. from_me: true,
  20. send_now: true,
  21. content: "#{@woofbot_response}\n\n🤖 Mensagem automática"
  22. }
  23. 1 event_params.merge!({ deal_id: @event.deal_id }) if @event.deal_id.present?
  24. 1 @response_event = EventBuilder.new(@account.users.first,
  25. event_params).build
  26. 1 @response_event.save
  27. 1 @response_event
  28. end
  29. 1 def woofbot_should_be_run?
  30. 1 @event.from_me == false && woofbot_active? && event_is_question?
  31. end
  32. 1 def event_is_question?
  33. 1 @event.content.to_s.include?('?')
  34. end
  35. 1 def woofbot_active?
  36. 1 @account.site_url.present? && @ai_assistent.auto_reply
  37. end
  38. end

app/use_cases/accounts/contacts/events/woofbot_worker.rb

60.0% lines covered

5 relevant lines. 3 lines covered and 2 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 class Accounts::Contacts::Events::WoofbotWorker
  3. 1 include Sidekiq::Worker
  4. 1 def perform(event_id)
  5. event = Event.find(event_id)
  6. Accounts::Contacts::Events::Woofbot.new(event).call
  7. end
  8. end

app/use_cases/accounts/contacts/get_by_params.rb

100.0% lines covered

33 relevant lines. 33 lines covered and 0 lines missed.
    
  1. 1 class Accounts::Contacts::GetByParams
  2. 1 def self.call(account, params)
  3. 47 params.stringify_keys!
  4. 47 return { error: 'Not found' } if params.blank?
  5. 125 params.reject! { |_key, value| value.blank? }
  6. 46 params = params.slice('email', 'phone', 'identifier')
  7. 46 query_params = build_query_conditions(params)
  8. 46 if params.key?('phone')
  9. 22 query_params << "phone ILIKE '%#{sanitized_phone(params['phone'])}%'"
  10. 22 query_params << "phone ILIKE '%#{phone_with_9_digit(params['phone'])}%'"
  11. 22 query_params << "phone ILIKE '%#{phone_number_without_9_digit(params['phone'])}%'"
  12. end
  13. 46 contact = account.contacts.where(query_params.join(' OR ')).first if query_params.present?
  14. 46 { ok: contact }
  15. end
  16. 1 def self.build_query_conditions(params)
  17. 46 params.map do |field, value|
  18. 54 case field
  19. when 'identifier'
  20. 3 "additional_attributes ->> 'chatwoot_identifier' = '#{value}'"
  21. else
  22. 51 "#{field} ILIKE '%#{value}%'"
  23. end
  24. end
  25. end
  26. 1 def self.phone_number_without_9_digit(phone)
  27. 22 sanitized_phone = sanitized_phone(phone)
  28. 22 if sanitized_phone.size == 13
  29. 8 sanitized_phone
  30. else
  31. 14 "#{sanitized_phone[0..4]}#{sanitized_phone[6..-1]}"
  32. end
  33. end
  34. 1 def self.phone_with_9_digit(phone)
  35. 22 sanitized_phone = sanitized_phone(phone)
  36. 22 if sanitized_phone.size >= 14
  37. 10 sanitized_phone
  38. else
  39. 12 "#{sanitized_phone[0..4]}9#{sanitized_phone[5..-1]}"
  40. end
  41. end
  42. 1 def self.sanitized_phone(phone_number)
  43. 66 raise TypeError, 'phone_number must be a String' unless phone_number.is_a?(String)
  44. 66 cleaned_phone_number = phone_number.gsub(/\D/, '')
  45. 66 cleaned_phone_number.prepend('+')
  46. 66 cleaned_phone_number
  47. end
  48. end

app/use_cases/accounts/create/embed_company_site_job.rb

60.0% lines covered

5 relevant lines. 3 lines covered and 2 lines missed.
    
  1. 1 class Accounts::Create::EmbedCompanySiteJob < ApplicationJob
  2. 1 self.queue_adapter = :good_job
  3. 1 def perform(account_id)
  4. account = Account.find(account_id)
  5. Accounts::Create::EmbededCompanySite.new(account).call
  6. end
  7. end

app/use_cases/accounts/create/embeded_company_site.rb

94.59% lines covered

37 relevant lines. 35 lines covered and 2 lines missed.
    
  1. 1 class Accounts::Create::EmbededCompanySite
  2. 1 def initialize(account)
  3. 1 @account = account
  4. 1 @ai_assistent = Apps::AiAssistent.first
  5. 1 @start_url = @account.site_url
  6. 1 @start_url_host = URI.parse(@start_url).host
  7. end
  8. 1 def call(max_pages = 100)
  9. 1 crawl_website(@start_url, max_pages)
  10. end
  11. 1 private
  12. 1 def clean_data
  13. @account.embedding_documments.where(source: @account).destroy_all
  14. end
  15. 1 def crawl_website(start_url, max_pages)
  16. 1 visited = []
  17. 1 queue = [start_url]
  18. 1 pages_visited = 0
  19. 1 while !queue.empty? && pages_visited < max_pages
  20. 3 current_url = queue.shift
  21. 3 next if visited.include?(current_url)
  22. 3 visited.push(current_url)
  23. begin
  24. 3 page = Accounts::Create::PageCrawler.new(current_url)
  25. 3 next unless page.valid_page?
  26. 3 embed_page(page)
  27. 3 links = filter_site_subpages(page.page_links) - visited
  28. 3 links.each do |link|
  29. 12 queue << link
  30. end
  31. 3 pages_visited += 1
  32. rescue => e
  33. puts "Failed to fetch #{current_url}: #{e.message}"
  34. end
  35. end
  36. 1 visited
  37. end
  38. 1 def embed_page(page)
  39. 3 splitter = ::TextSplitters::RecursiveCharacterTextSplitter.new(chunk_size: 1000, chunk_overlap: 100)
  40. 3 page_filter_links = page.body_text_content.gsub(/\[(.*?)\]\(.*?\)/m, '')
  41. 3 output = splitter.split(page_filter_links)
  42. 3 output.each do |content_split|
  43. 3 @account.embedding_documments.create(
  44. source_reference: page.page_link,
  45. source: @account,
  46. content: content_split,
  47. embedding: OpenAi::Embeddings.new.get_embedding(@ai_assistent, content_split, 'text-embedding-3-small')
  48. )
  49. end
  50. end
  51. 1 def filter_site_subpages(links)
  52. 3 links.filter do |link|
  53. 34 link.include?(@start_url_host)
  54. end
  55. end
  56. end

app/use_cases/accounts/create/page_crawler.rb

89.29% lines covered

28 relevant lines. 25 lines covered and 3 lines missed.
    
  1. 1 require 'faraday/follow_redirects'
  2. 1 class Accounts::Create::PageCrawler
  3. 1 attr_reader :page_link
  4. 1 def initialize(page_link)
  5. 3 @page_link = page_link
  6. 3 conn = Faraday.new() do |faraday|
  7. 3 faraday.response :follow_redirects
  8. 3 faraday.adapter Faraday.default_adapter
  9. end
  10. 3 @response = conn.get(page_link)
  11. 3 @doc = Nokogiri::HTML(@response.body)
  12. end
  13. 1 def valid_page?
  14. 3 @response.status == 200 && @doc.at_xpath('//body').present?
  15. end
  16. 1 def page_links
  17. 3 sitemap? ? extract_links_from_sitemap : extract_links_from_html
  18. end
  19. 1 def page_title
  20. title_element = @doc.at_xpath('//title')
  21. title_element&.text&.strip
  22. end
  23. 1 def body_text_content
  24. 3 ReverseMarkdown.convert @doc.at_xpath('//body'), unknown_tags: :bypass, github_flavored: true
  25. end
  26. 1 private
  27. 1 def sitemap?
  28. 3 @page_link.end_with?('.xml')
  29. end
  30. 1 def extract_links_from_sitemap
  31. @doc.xpath('//loc').to_set(&:text)
  32. end
  33. 1 def extract_links_from_html
  34. 3 @doc.xpath('//a/@href').to_set do |link|
  35. 55 absolute_url = URI.join(@page_link, URI::Parser.new.escape(link.value)).to_s
  36. 55 absolute_url
  37. end
  38. end
  39. end

app/use_cases/open_ai/embeddings.rb

100.0% lines covered

10 relevant lines. 10 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 class OpenAi::Embeddings
  3. 1 def get_embedding(ai_assistent, content, model = 'text-embedding-ada-002')
  4. 6 fetch_embeddings(ai_assistent, content, model)
  5. end
  6. 1 private
  7. 1 def fetch_embeddings(ai_assistent, input, model)
  8. 6 url = 'https://api.openai.com/v1/embeddings'
  9. headers = {
  10. 6 'Authorization' => "Bearer #{ai_assistent.api_key}",
  11. 'Content-Type' => 'application/json'
  12. }
  13. data = {
  14. 6 input: input,
  15. model: model
  16. }
  17. 6 response = Net::HTTP.post(URI(url), data.to_json, headers)
  18. 6 JSON.parse(response.body)['data']&.pick('embedding')
  19. end
  20. end

app/use_cases/pwa/send_notifications_worker.rb

100.0% lines covered

8 relevant lines. 8 lines covered and 0 lines missed.
    
  1. 1 class Pwa::SendNotificationsWorker < ApplicationJob
  2. 1 self.queue_adapter = :good_job
  3. 1 def perform(event_id)
  4. 73 event = Event.find(event_id)
  5. 73 if event.present? && event.should_delivery_event_scheduled?
  6. 50 WebpushSubscription.find_each do |subscription|
  7. 2 if subscription.user.webpush_notify_on_event_expired
  8. 2 subscription.send_notification(
  9. {
  10. title: "#{Event.human_enum_name(:kind, event.kind)} #{event.title}",
  11. body: I18n.t('use_cases.pwa.send_notifications_worker.body',
  12. event_kind: Event.human_enum_name(:kind, event.kind), event_title: event.title, deal_name: event.deal.name),
  13. icon: ActionController::Base.helpers.image_url('logo-patinha.svg'),
  14. url: Rails.application.routes.url_helpers.account_deal_url(Current.account, event.deal)
  15. }
  16. )
  17. end
  18. end
  19. end
  20. end
  21. end

app/use_cases/users/json_web_token.rb

100.0% lines covered

10 relevant lines. 10 lines covered and 0 lines missed.
    
  1. 1 class Users::JsonWebToken
  2. 1 SECRET_KEY = Rails.application.secrets.secret_key_base.to_s
  3. 1 def self.encode_user(user)
  4. 70 hmac_secret = SECRET_KEY
  5. 70 JWT.encode({ sub: user.id }, hmac_secret)
  6. end
  7. 1 def self.decode_user(token)
  8. begin
  9. 86 decoded = JWT.decode(token, SECRET_KEY)[0]
  10. 63 user = User.find(decoded["sub"])
  11. 63 return { ok: user }
  12. rescue => e
  13. 23 return { error: e }
  14. end
  15. end
  16. end

app/workers/webhook_worker.rb

75.0% lines covered

4 relevant lines. 3 lines covered and 1 lines missed.
    
  1. 1 class WebhookWorker
  2. 1 include Sidekiq::Worker
  3. 1 def perform(url, payload)
  4. Faraday.post(
  5. url,
  6. payload,
  7. {'Content-Type': 'application/json'}
  8. )
  9. end
  10. end