spree_notification_providers 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +24 -0
- data/LICENSE +29 -0
- data/README.md +322 -0
- data/app/controllers/spree/admin/notification_channel_providers_controller.rb +152 -0
- data/app/controllers/spree/admin/notification_deliveries_controller.rb +69 -0
- data/app/controllers/spree/admin/notification_providers_controller.rb +31 -0
- data/app/controllers/spree/webhooks/gupshup_controller.rb +61 -0
- data/app/helpers/spree/admin/notification_masking_helper.rb +65 -0
- data/app/jobs/spree/notification_deliveries/purge_job.rb +26 -0
- data/app/models/concerns/spree/notification_provider_settings.rb +81 -0
- data/app/models/spree/notification_channel_provider.rb +165 -0
- data/app/models/spree/notification_delivery.rb +39 -0
- data/app/models/spree/notification_provider_config.rb +32 -0
- data/app/models/spree/notification_providers_store_decorator.rb +16 -0
- data/app/models/spree/permission_sets/notification_provider_management.rb +20 -0
- data/app/views/spree/admin/notification_channel_providers/_input_field.html.erb +51 -0
- data/app/views/spree/admin/notification_channel_providers/edit.html.erb +118 -0
- data/app/views/spree/admin/notification_deliveries/index.html.erb +127 -0
- data/app/views/spree/admin/notification_deliveries/show.html.erb +52 -0
- data/app/views/spree/admin/notification_providers/index.html.erb +114 -0
- data/config/locales/en.yml +92 -0
- data/config/routes.rb +17 -0
- data/db/migrate/20261019000001_create_spree_notification_provider_configs.rb +19 -0
- data/db/migrate/20261019000002_create_spree_notification_channel_providers.rb +23 -0
- data/db/migrate/20261019000003_create_spree_notification_deliveries.rb +29 -0
- data/lib/generators/spree_notification_providers/import_env/import_env_generator.rb +23 -0
- data/lib/generators/spree_notification_providers/install/install_generator.rb +41 -0
- data/lib/generators/spree_notification_providers/install/templates/initializer.rb +14 -0
- data/lib/spree_notification_providers/adapters/base.rb +76 -0
- data/lib/spree_notification_providers/adapters/gupshup.rb +87 -0
- data/lib/spree_notification_providers/adapters/http_outcome.rb +61 -0
- data/lib/spree_notification_providers/adapters/log.rb +26 -0
- data/lib/spree_notification_providers/adapters/msg91.rb +117 -0
- data/lib/spree_notification_providers/adapters/smtp.rb +77 -0
- data/lib/spree_notification_providers/bulk_message.rb +33 -0
- data/lib/spree_notification_providers/configuration.rb +32 -0
- data/lib/spree_notification_providers/engine.rb +59 -0
- data/lib/spree_notification_providers/http.rb +42 -0
- data/lib/spree_notification_providers/importers/env_importer.rb +43 -0
- data/lib/spree_notification_providers/importers/writer.rb +132 -0
- data/lib/spree_notification_providers/input_definition.rb +75 -0
- data/lib/spree_notification_providers/mail_delivery_method.rb +78 -0
- data/lib/spree_notification_providers/message.rb +55 -0
- data/lib/spree_notification_providers/message_type.rb +26 -0
- data/lib/spree_notification_providers/message_types.rb +8 -0
- data/lib/spree_notification_providers/provider_definition.rb +103 -0
- data/lib/spree_notification_providers/providers/gupshup.rb +26 -0
- data/lib/spree_notification_providers/providers/log.rb +16 -0
- data/lib/spree_notification_providers/providers/msg91.rb +27 -0
- data/lib/spree_notification_providers/providers/smtp.rb +18 -0
- data/lib/spree_notification_providers/registry.rb +79 -0
- data/lib/spree_notification_providers/result.rb +73 -0
- data/lib/spree_notification_providers/router.rb +205 -0
- data/lib/spree_notification_providers/version.rb +5 -0
- data/lib/spree_notification_providers.rb +96 -0
- data/spec/factories/notification_providers.rb +91 -0
- data/spec/support/message_types.rb +15 -0
- data/spec/support/provider_http_helpers.rb +46 -0
- data/spree_notification_providers.gemspec +44 -0
- metadata +189 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
<% scope = 'spree_notification_providers.admin.deliveries' %>
|
|
2
|
+
<% q = params[:q] || {} %>
|
|
3
|
+
<% content_for :page_title do %>
|
|
4
|
+
<%= t("#{scope}.title") %>
|
|
5
|
+
<% end %>
|
|
6
|
+
|
|
7
|
+
<% content_for :page_actions do %>
|
|
8
|
+
<%= link_to t('spree_notification_providers.admin.providers.title'), spree.admin_notification_providers_path, class: 'btn btn-light' %>
|
|
9
|
+
<% end %>
|
|
10
|
+
|
|
11
|
+
<p class="text-sm text-gray-500 mb-4">
|
|
12
|
+
<%= t("#{scope}.intro", days: SpreeNotificationProviders.configuration.delivery_retention_days) %>
|
|
13
|
+
</p>
|
|
14
|
+
|
|
15
|
+
<div class="card mb-6">
|
|
16
|
+
<div class="card-body">
|
|
17
|
+
<%= form_with url: spree.admin_notification_deliveries_path, method: :get, local: true,
|
|
18
|
+
class: 'flex flex-wrap items-end gap-3', id: 'delivery-log-filters' do %>
|
|
19
|
+
<% any = [[t("#{scope}.filters.any"), '']] %>
|
|
20
|
+
<div class="form-group mb-0">
|
|
21
|
+
<%= label_tag :q_channel_eq, t("#{scope}.filters.channel") %>
|
|
22
|
+
<%= select_tag 'q[channel_eq]', options_for_select(any + Spree::NotificationChannelProvider::CHANNELS.map { |c| [notification_channel_label(c), c] }, q[:channel_eq]),
|
|
23
|
+
id: 'q_channel_eq', class: 'form-select' %>
|
|
24
|
+
</div>
|
|
25
|
+
<div class="form-group mb-0">
|
|
26
|
+
<%= label_tag :q_provider_key_eq, t("#{scope}.filters.provider") %>
|
|
27
|
+
<%= select_tag 'q[provider_key_eq]', options_for_select(any + SpreeNotificationProviders.providers.map { |p| [p.label, p.key] }, q[:provider_key_eq]),
|
|
28
|
+
id: 'q_provider_key_eq', class: 'form-select' %>
|
|
29
|
+
</div>
|
|
30
|
+
<div class="form-group mb-0">
|
|
31
|
+
<%= label_tag :q_outcome_eq, t("#{scope}.filters.outcome") %>
|
|
32
|
+
<%= select_tag 'q[outcome_eq]', options_for_select(any + Spree::NotificationDelivery::OUTCOMES.map { |o| [t("spree_notification_providers.outcomes.#{o}"), o] }, q[:outcome_eq]),
|
|
33
|
+
id: 'q_outcome_eq', class: 'form-select' %>
|
|
34
|
+
</div>
|
|
35
|
+
<div class="form-group mb-0">
|
|
36
|
+
<%= label_tag :q_message_type_eq, t("#{scope}.filters.message_type") %>
|
|
37
|
+
<%= select_tag 'q[message_type_eq]', options_for_select(any + SpreeNotificationProviders.message_types.map { |m| [m.label, m.key] }, q[:message_type_eq]),
|
|
38
|
+
id: 'q_message_type_eq', class: 'form-select' %>
|
|
39
|
+
</div>
|
|
40
|
+
<div class="form-group mb-0">
|
|
41
|
+
<%= label_tag :q_created_at_gteq, t("#{scope}.filters.from") %>
|
|
42
|
+
<%= date_field_tag 'q[created_at_gteq]', q[:created_at_gteq], id: 'q_created_at_gteq', class: 'form-control' %>
|
|
43
|
+
</div>
|
|
44
|
+
<div class="form-group mb-0">
|
|
45
|
+
<%= label_tag :q_created_at_lteq, t("#{scope}.filters.to") %>
|
|
46
|
+
<%= date_field_tag 'q[created_at_lteq]', q[:created_at_lteq], id: 'q_created_at_lteq', class: 'form-control' %>
|
|
47
|
+
</div>
|
|
48
|
+
<label class="inline-flex items-center gap-1 mb-2">
|
|
49
|
+
<%= check_box_tag 'q[bulk_eq]', 'true', q[:bulk_eq] == 'true', id: 'q_bulk_eq' %>
|
|
50
|
+
<%= t("#{scope}.filters.bulk") %>
|
|
51
|
+
</label>
|
|
52
|
+
<div class="flex items-center gap-2">
|
|
53
|
+
<%= submit_tag t("#{scope}.filters.apply"), name: nil, class: 'btn btn-primary' %>
|
|
54
|
+
<%= link_to Spree.t(:clear), spree.admin_notification_deliveries_path, class: 'btn btn-light' if q.values.any?(&:present?) %>
|
|
55
|
+
</div>
|
|
56
|
+
<% end %>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<div class="card-lg">
|
|
61
|
+
<% if @groups.any? %>
|
|
62
|
+
<div class="overflow-x-auto">
|
|
63
|
+
<table class="table">
|
|
64
|
+
<thead>
|
|
65
|
+
<tr>
|
|
66
|
+
<th scope="col"><%= t("#{scope}.columns.time") %></th>
|
|
67
|
+
<th scope="col"><%= t("#{scope}.columns.channel") %></th>
|
|
68
|
+
<th scope="col"><%= t("#{scope}.columns.type") %></th>
|
|
69
|
+
<th scope="col"><%= t("#{scope}.columns.recipient") %></th>
|
|
70
|
+
<th scope="col"><%= t("#{scope}.columns.attempts") %></th>
|
|
71
|
+
<th scope="col"><%= t("#{scope}.columns.outcome") %></th>
|
|
72
|
+
</tr>
|
|
73
|
+
</thead>
|
|
74
|
+
<tbody>
|
|
75
|
+
<% @groups.each do |group| %>
|
|
76
|
+
<% first = group.first %>
|
|
77
|
+
<tr id="delivery-<%= group.message_ref %>">
|
|
78
|
+
<td class="whitespace-nowrap text-sm">
|
|
79
|
+
<%= link_to l(first.created_at, format: :short), spree.admin_notification_delivery_path(group.message_ref), class: 'text-primary hover:underline' %>
|
|
80
|
+
</td>
|
|
81
|
+
<td><%= notification_channel_label(first.channel) %></td>
|
|
82
|
+
<td class="text-sm"><%= SpreeNotificationProviders.message_type(first.message_type)&.label || first.message_type %></td>
|
|
83
|
+
<td class="text-sm font-mono">
|
|
84
|
+
<% if group.bulk? %>
|
|
85
|
+
<%= t("#{scope}.bulk_recipients", count: group.bulk_counts.values.sum) %>
|
|
86
|
+
<% else %>
|
|
87
|
+
<%= mask_recipient(first.recipient) %>
|
|
88
|
+
<% end %>
|
|
89
|
+
</td>
|
|
90
|
+
<td class="text-sm">
|
|
91
|
+
<% if group.bulk? %>
|
|
92
|
+
<%= SpreeNotificationProviders.provider(first.provider_key)&.label || first.provider_key || '—' %>
|
|
93
|
+
<% else %>
|
|
94
|
+
<ol class="space-y-1">
|
|
95
|
+
<% group.attempts.each do |attempt| %>
|
|
96
|
+
<li>
|
|
97
|
+
<%= SpreeNotificationProviders.provider(attempt.provider_key)&.label || attempt.provider_key || '—' %>
|
|
98
|
+
<%= notification_status_badge(attempt.outcome) %>
|
|
99
|
+
<% if attempt.reason.present? %><span class="text-xs text-gray-500"><%= attempt.reason %></span><% end %>
|
|
100
|
+
</li>
|
|
101
|
+
<% end %>
|
|
102
|
+
</ol>
|
|
103
|
+
<% end %>
|
|
104
|
+
</td>
|
|
105
|
+
<td>
|
|
106
|
+
<% if group.bulk? %>
|
|
107
|
+
<% group.bulk_counts.each do |outcome, count| %>
|
|
108
|
+
<div><%= notification_status_badge(outcome) %> <span class="text-xs"><%= count %></span></div>
|
|
109
|
+
<% end %>
|
|
110
|
+
<% else %>
|
|
111
|
+
<%= notification_status_badge(group.final.outcome) %>
|
|
112
|
+
<% end %>
|
|
113
|
+
</td>
|
|
114
|
+
</tr>
|
|
115
|
+
<% end %>
|
|
116
|
+
</tbody>
|
|
117
|
+
</table>
|
|
118
|
+
</div>
|
|
119
|
+
<div class="flex justify-between p-4">
|
|
120
|
+
<%= link_to '← ' + Spree.t(:previous), url_for(request.query_parameters.merge(page: @pagy.previous)), class: 'btn btn-light btn-sm' if @pagy.previous %>
|
|
121
|
+
<span></span>
|
|
122
|
+
<%= link_to Spree.t(:next) + ' →', url_for(request.query_parameters.merge(page: @pagy.next)), class: 'btn btn-light btn-sm' if @pagy.next %>
|
|
123
|
+
</div>
|
|
124
|
+
<% else %>
|
|
125
|
+
<div class="card-body text-sm text-gray-500"><%= t("#{scope}.empty") %></div>
|
|
126
|
+
<% end %>
|
|
127
|
+
</div>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
<% scope = 'spree_notification_providers.admin.deliveries' %>
|
|
2
|
+
<% content_for :page_title do %>
|
|
3
|
+
<%= t("#{scope}.show_title", ref: @message_ref.first(8)) %>
|
|
4
|
+
<% end %>
|
|
5
|
+
|
|
6
|
+
<% content_for :page_actions do %>
|
|
7
|
+
<%= link_to t("#{scope}.back"), spree.admin_notification_deliveries_path, class: 'btn btn-light' %>
|
|
8
|
+
<% end %>
|
|
9
|
+
|
|
10
|
+
<div class="card mb-6">
|
|
11
|
+
<div class="card-body grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
|
|
12
|
+
<div><div class="text-gray-500"><%= t("#{scope}.columns.channel") %></div><%= notification_channel_label(@first.channel) %></div>
|
|
13
|
+
<div><div class="text-gray-500"><%= t("#{scope}.columns.type") %></div><%= SpreeNotificationProviders.message_type(@first.message_type)&.label || @first.message_type %></div>
|
|
14
|
+
<div><div class="text-gray-500"><%= t("#{scope}.columns.time") %></div><%= l(@first.created_at, format: :long) %></div>
|
|
15
|
+
<div>
|
|
16
|
+
<div class="text-gray-500"><%= t("#{scope}.columns.source") %></div>
|
|
17
|
+
<%= @first.source_type ? "#{@first.source_type.demodulize} #{@first.source.try(:number) || @first.source_id}" : '—' %>
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<div class="card-lg">
|
|
23
|
+
<table class="table">
|
|
24
|
+
<thead>
|
|
25
|
+
<tr>
|
|
26
|
+
<th scope="col"><%= t("#{scope}.columns.attempt") %></th>
|
|
27
|
+
<th scope="col"><%= t("#{scope}.columns.recipient") %></th>
|
|
28
|
+
<th scope="col"><%= t("#{scope}.columns.provider") %></th>
|
|
29
|
+
<th scope="col"><%= t("#{scope}.columns.outcome") %></th>
|
|
30
|
+
<th scope="col"><%= t("#{scope}.columns.reason") %></th>
|
|
31
|
+
<th scope="col"><%= t("#{scope}.columns.time") %></th>
|
|
32
|
+
</tr>
|
|
33
|
+
</thead>
|
|
34
|
+
<tbody>
|
|
35
|
+
<% @deliveries.each do |delivery| %>
|
|
36
|
+
<tr id="<%= delivery.prefixed_id %>">
|
|
37
|
+
<td><%= delivery.attempt_number %></td>
|
|
38
|
+
<td class="font-mono text-sm"><%= mask_recipient(delivery.recipient) %></td>
|
|
39
|
+
<td><%= SpreeNotificationProviders.provider(delivery.provider_key)&.label || delivery.provider_key || '—' %></td>
|
|
40
|
+
<td><%= notification_status_badge(delivery.outcome) %></td>
|
|
41
|
+
<td class="text-xs text-gray-600"><%= delivery.reason %></td>
|
|
42
|
+
<td class="text-sm whitespace-nowrap"><%= l(delivery.created_at, format: :short) %></td>
|
|
43
|
+
</tr>
|
|
44
|
+
<% end %>
|
|
45
|
+
</tbody>
|
|
46
|
+
</table>
|
|
47
|
+
<div class="flex justify-between p-4">
|
|
48
|
+
<%= link_to '← ' + Spree.t(:previous), url_for(request.query_parameters.merge(page: @pagy.previous)), class: 'btn btn-light btn-sm' if @pagy.previous %>
|
|
49
|
+
<span></span>
|
|
50
|
+
<%= link_to Spree.t(:next) + ' →', url_for(request.query_parameters.merge(page: @pagy.next)), class: 'btn btn-light btn-sm' if @pagy.next %>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
<% scope = 'spree_notification_providers.admin.providers' %>
|
|
2
|
+
<% content_for :page_title do %>
|
|
3
|
+
<%= t("#{scope}.title") %>
|
|
4
|
+
<% end %>
|
|
5
|
+
|
|
6
|
+
<% content_for :page_actions do %>
|
|
7
|
+
<%= link_to t("#{scope}.delivery_log"), spree.admin_notification_deliveries_path(q: { channel_eq: @channel }), class: 'btn btn-light' %>
|
|
8
|
+
<% end %>
|
|
9
|
+
|
|
10
|
+
<p class="text-sm text-gray-500 mb-4"><%= t("#{scope}.intro") %></p>
|
|
11
|
+
|
|
12
|
+
<div class="border-b border-gray-200 mb-6">
|
|
13
|
+
<nav class="-mb-px flex space-x-8" aria-label="Channels">
|
|
14
|
+
<% Spree::NotificationChannelProvider::CHANNELS.each do |channel| %>
|
|
15
|
+
<% active = channel == @channel %>
|
|
16
|
+
<%= link_to notification_channel_label(channel),
|
|
17
|
+
spree.admin_notification_providers_path(channel: channel),
|
|
18
|
+
id: "channel-tab-#{channel}",
|
|
19
|
+
'aria-current': (active ? 'page' : nil),
|
|
20
|
+
class: "whitespace-nowrap py-3 px-1 border-b-2 font-medium text-sm #{active ? 'border-primary text-primary' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'}" %>
|
|
21
|
+
<% end %>
|
|
22
|
+
</nav>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<div class="card-lg mb-6">
|
|
26
|
+
<% if @entries.any? %>
|
|
27
|
+
<table class="table">
|
|
28
|
+
<thead>
|
|
29
|
+
<tr>
|
|
30
|
+
<th scope="col" class="w-5"></th>
|
|
31
|
+
<th scope="col"><%= t('spree_notification_providers.admin.deliveries.columns.provider') %></th>
|
|
32
|
+
<th scope="col"><%= Spree.t(:status) %></th>
|
|
33
|
+
<th scope="col" class="text-center"><%= t("#{scope}.enabled") %></th>
|
|
34
|
+
<th scope="col"></th>
|
|
35
|
+
</tr>
|
|
36
|
+
</thead>
|
|
37
|
+
<tbody data-controller="sortable" data-sortable-handle-value=".move-handle"
|
|
38
|
+
data-sortable-resource-name-value="notification_channel_provider">
|
|
39
|
+
<% @entries.each do |entry| %>
|
|
40
|
+
<% status = entry.status %>
|
|
41
|
+
<tr id="<%= spree_dom_id(entry) %>"
|
|
42
|
+
data-sortable-update-url="<%= spree.position_admin_notification_channel_provider_path(entry) %>">
|
|
43
|
+
<td class="w-5 text-center <%= 'move-handle cursor-move' if @entries.size > 1 %>"
|
|
44
|
+
title="<%= t("#{scope}.drag_to_reorder") if @entries.size > 1 %>">
|
|
45
|
+
<% if @entries.size > 1 %>
|
|
46
|
+
<%= icon('grip-vertical', class: 'rounded-md hover:bg-gray-100 p-2') %>
|
|
47
|
+
<% else %>
|
|
48
|
+
<span class="text-gray-400 text-sm"><%= entry.position %></span>
|
|
49
|
+
<% end %>
|
|
50
|
+
</td>
|
|
51
|
+
<td>
|
|
52
|
+
<div class="font-medium"><%= entry.definition&.label || entry.provider_key.titleize %></div>
|
|
53
|
+
<% if entry.shared_with_channels.size > 1 %>
|
|
54
|
+
<div class="text-xs text-gray-500">
|
|
55
|
+
<%= t('spree_notification_providers.admin.configure.shared_used_by',
|
|
56
|
+
channels: entry.shared_with_channels.map { |c| notification_channel_label(c) }.to_sentence) %>
|
|
57
|
+
</div>
|
|
58
|
+
<% end %>
|
|
59
|
+
</td>
|
|
60
|
+
<td>
|
|
61
|
+
<%= notification_status_badge(status) %>
|
|
62
|
+
<% missing = entry.missing_inputs %>
|
|
63
|
+
<% if missing.any? %>
|
|
64
|
+
<div class="text-xs text-amber-700 mt-1"><%= t("#{scope}.missing", inputs: missing.join(', ')) %></div>
|
|
65
|
+
<% end %>
|
|
66
|
+
</td>
|
|
67
|
+
<td class="text-center">
|
|
68
|
+
<%= form_with url: spree.admin_notification_channel_provider_path(entry), method: :patch, local: true do %>
|
|
69
|
+
<%= hidden_field_tag 'entry[enabled]', entry.enabled? ? '0' : '1' %>
|
|
70
|
+
<%= hidden_field_tag :return_to, 'index' %>
|
|
71
|
+
<button type="submit" role="switch" aria-checked="<%= entry.enabled? %>"
|
|
72
|
+
aria-label="<%= t("#{scope}.enabled") %>"
|
|
73
|
+
class="btn btn-sm <%= entry.enabled? ? 'btn-primary' : 'btn-light' %>">
|
|
74
|
+
<%= entry.enabled? ? Spree.t(:say_yes) : Spree.t(:say_no) %>
|
|
75
|
+
</button>
|
|
76
|
+
<% end %>
|
|
77
|
+
</td>
|
|
78
|
+
<td class="text-right whitespace-nowrap">
|
|
79
|
+
<%= link_to t("#{scope}.configure"), spree.edit_admin_notification_channel_provider_path(entry), class: 'btn btn-light btn-sm' %>
|
|
80
|
+
<%= link_to t("#{scope}.remove"), spree.admin_notification_channel_provider_path(entry),
|
|
81
|
+
class: 'btn btn-light btn-sm text-red-600',
|
|
82
|
+
data: { turbo_method: :delete,
|
|
83
|
+
turbo_confirm: t("#{scope}.remove_confirm",
|
|
84
|
+
provider: entry.definition&.label || entry.provider_key,
|
|
85
|
+
channel: notification_channel_label(entry.channel)) } %>
|
|
86
|
+
</td>
|
|
87
|
+
</tr>
|
|
88
|
+
<% end %>
|
|
89
|
+
</tbody>
|
|
90
|
+
</table>
|
|
91
|
+
<% else %>
|
|
92
|
+
<div class="card-body text-sm text-gray-500"><%= t("#{scope}.none_added") %></div>
|
|
93
|
+
<% end %>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
<% if @available.any? %>
|
|
97
|
+
<h6 class="pb-2"><%= t("#{scope}.available") %></h6>
|
|
98
|
+
<div class="card-lg">
|
|
99
|
+
<table class="table">
|
|
100
|
+
<tbody>
|
|
101
|
+
<% @available.each do |definition| %>
|
|
102
|
+
<tr id="available-<%= definition.key %>">
|
|
103
|
+
<td class="font-medium"><%= definition.label %></td>
|
|
104
|
+
<td class="text-right">
|
|
105
|
+
<%= button_to t("#{scope}.add"), spree.admin_notification_channel_providers_path,
|
|
106
|
+
params: { channel: @channel, provider_key: definition.key },
|
|
107
|
+
class: 'btn btn-primary btn-sm', form: { class: 'inline' } %>
|
|
108
|
+
</td>
|
|
109
|
+
</tr>
|
|
110
|
+
<% end %>
|
|
111
|
+
</tbody>
|
|
112
|
+
</table>
|
|
113
|
+
</div>
|
|
114
|
+
<% end %>
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
en:
|
|
2
|
+
spree:
|
|
3
|
+
admin:
|
|
4
|
+
tab:
|
|
5
|
+
notification_providers: "Notification providers"
|
|
6
|
+
notification_deliveries: "Delivery log"
|
|
7
|
+
notification_providers: "Notification providers"
|
|
8
|
+
notification_deliveries: "Delivery log"
|
|
9
|
+
spree_notification_providers:
|
|
10
|
+
channels:
|
|
11
|
+
email: "Email"
|
|
12
|
+
sms: "SMS"
|
|
13
|
+
whatsapp: "WhatsApp"
|
|
14
|
+
statuses:
|
|
15
|
+
ready: "Ready"
|
|
16
|
+
not_ready: "Not ready"
|
|
17
|
+
disabled: "Disabled"
|
|
18
|
+
unavailable: "Unavailable"
|
|
19
|
+
outcomes:
|
|
20
|
+
delivered: "Delivered"
|
|
21
|
+
failed: "Failed"
|
|
22
|
+
unknown: "Unknown"
|
|
23
|
+
skipped: "Skipped"
|
|
24
|
+
test_message:
|
|
25
|
+
subject: "Test message"
|
|
26
|
+
html: "<p>This is a test message from your store's notification settings.</p>"
|
|
27
|
+
admin:
|
|
28
|
+
providers:
|
|
29
|
+
title: "Notification providers"
|
|
30
|
+
intro: "Providers are tried in this order. A provider is skipped when it is not ready, and the next one is tried only when a provider definitely did not accept the message."
|
|
31
|
+
available: "Available providers"
|
|
32
|
+
none_added: "No providers added for this channel yet."
|
|
33
|
+
add: "Add"
|
|
34
|
+
configure: "Configure"
|
|
35
|
+
remove: "Remove"
|
|
36
|
+
remove_confirm: "Remove %{provider} from %{channel}? Its shared settings are kept while other channels use it."
|
|
37
|
+
enabled: "Enabled"
|
|
38
|
+
missing: "Missing: %{inputs}"
|
|
39
|
+
drag_to_reorder: "Drag to change priority"
|
|
40
|
+
added: "%{provider} added to %{channel}. Fill in its settings and enable it."
|
|
41
|
+
updated: "%{provider} settings saved."
|
|
42
|
+
removed: "%{provider} removed from %{channel}."
|
|
43
|
+
delivery_log: "Delivery log"
|
|
44
|
+
configure:
|
|
45
|
+
title: "%{provider} — %{channel}"
|
|
46
|
+
shared_header: "Shared settings"
|
|
47
|
+
shared_used_by: "Used by: %{channels}"
|
|
48
|
+
channel_header: "%{channel} settings"
|
|
49
|
+
templates_header: "Templates"
|
|
50
|
+
templates_help: "The provider-side template used for each kind of message. Leave blank to skip this provider for that message."
|
|
51
|
+
secret_set: "Set %{hint}"
|
|
52
|
+
secret_not_set: "Not set"
|
|
53
|
+
secret_placeholder: "Leave blank to keep the current value"
|
|
54
|
+
clear: "Clear"
|
|
55
|
+
save: "Save"
|
|
56
|
+
back: "Back to providers"
|
|
57
|
+
test_header: "Send test"
|
|
58
|
+
test_help: "Sends one message through this provider only, ignoring priority."
|
|
59
|
+
test_to_email: "Email address"
|
|
60
|
+
test_to_phone: "Phone number (with country code)"
|
|
61
|
+
test_send: "Send test"
|
|
62
|
+
test_delivered: "Test sent through %{provider}."
|
|
63
|
+
test_failed: "Test through %{provider} did not go out: %{reason}"
|
|
64
|
+
test_invalid_to: "Enter a valid %{kind}."
|
|
65
|
+
deliveries:
|
|
66
|
+
title: "Delivery log"
|
|
67
|
+
intro: "Every attempt to send a message. Recipients are partly hidden. Rows older than %{days} days are removed automatically."
|
|
68
|
+
empty: "No deliveries match these filters."
|
|
69
|
+
filters:
|
|
70
|
+
channel: "Channel"
|
|
71
|
+
provider: "Provider"
|
|
72
|
+
outcome: "Outcome"
|
|
73
|
+
message_type: "Message type"
|
|
74
|
+
from: "From"
|
|
75
|
+
to: "To"
|
|
76
|
+
bulk: "Bulk only"
|
|
77
|
+
apply: "Filter"
|
|
78
|
+
any: "Any"
|
|
79
|
+
columns:
|
|
80
|
+
time: "Time"
|
|
81
|
+
channel: "Channel"
|
|
82
|
+
type: "Type"
|
|
83
|
+
recipient: "Recipient"
|
|
84
|
+
provider: "Provider"
|
|
85
|
+
outcome: "Outcome"
|
|
86
|
+
attempts: "Attempts"
|
|
87
|
+
reason: "Reason"
|
|
88
|
+
attempt: "#"
|
|
89
|
+
source: "Source"
|
|
90
|
+
bulk_recipients: "%{count} recipients"
|
|
91
|
+
show_title: "Message %{ref}"
|
|
92
|
+
back: "Back to delivery log"
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
Spree::Core::Engine.add_routes do
|
|
4
|
+
# Gupshup message-event callback: /webhooks/gupshup?secret=<store webhook secret>
|
|
5
|
+
post 'webhooks/gupshup', to: 'webhooks/gupshup#create', as: :gupshup_webhook
|
|
6
|
+
|
|
7
|
+
namespace :admin do
|
|
8
|
+
resources :notification_providers, only: :index
|
|
9
|
+
resources :notification_channel_providers, only: %i[create edit update destroy] do
|
|
10
|
+
member do
|
|
11
|
+
patch :position, action: :reposition
|
|
12
|
+
post :test
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
resources :notification_deliveries, only: %i[index show], param: :message_ref
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateSpreeNotificationProviderConfigs < ActiveRecord::Migration[8.1]
|
|
4
|
+
def change
|
|
5
|
+
create_table :spree_notification_provider_configs do |t|
|
|
6
|
+
t.references :store, null: false, foreign_key: { to_table: :spree_stores }, index: false
|
|
7
|
+
t.string :provider_key, null: false
|
|
8
|
+
t.jsonb :settings, null: false, default: {}
|
|
9
|
+
t.text :secret_settings
|
|
10
|
+
t.string :import_source
|
|
11
|
+
t.datetime :imported_at
|
|
12
|
+
|
|
13
|
+
t.timestamps
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
add_index :spree_notification_provider_configs, %i[store_id provider_key], unique: true,
|
|
17
|
+
name: 'idx_spree_notif_provider_configs_on_store_provider'
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateSpreeNotificationChannelProviders < ActiveRecord::Migration[8.1]
|
|
4
|
+
def change
|
|
5
|
+
create_table :spree_notification_channel_providers do |t|
|
|
6
|
+
t.references :store, null: false, foreign_key: { to_table: :spree_stores }, index: false
|
|
7
|
+
t.string :channel, null: false
|
|
8
|
+
t.string :provider_key, null: false
|
|
9
|
+
t.boolean :enabled, null: false, default: false
|
|
10
|
+
t.integer :position, null: false
|
|
11
|
+
t.jsonb :settings, null: false, default: {}
|
|
12
|
+
t.text :secret_settings
|
|
13
|
+
t.jsonb :templates, null: false, default: {}
|
|
14
|
+
|
|
15
|
+
t.timestamps
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
add_index :spree_notification_channel_providers, %i[store_id channel provider_key], unique: true,
|
|
19
|
+
name: 'idx_spree_notif_channel_providers_on_store_channel_provider'
|
|
20
|
+
add_index :spree_notification_channel_providers, %i[store_id channel position],
|
|
21
|
+
name: 'idx_spree_notif_channel_providers_on_store_channel_position'
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateSpreeNotificationDeliveries < ActiveRecord::Migration[8.1]
|
|
4
|
+
def change
|
|
5
|
+
create_table :spree_notification_deliveries do |t|
|
|
6
|
+
t.references :store, null: false, foreign_key: { to_table: :spree_stores }, index: false
|
|
7
|
+
t.uuid :message_ref, null: false
|
|
8
|
+
t.string :channel, null: false
|
|
9
|
+
t.string :provider_key
|
|
10
|
+
t.string :message_type, null: false
|
|
11
|
+
t.string :recipient, null: false
|
|
12
|
+
t.string :outcome, null: false
|
|
13
|
+
t.string :reason, limit: 255
|
|
14
|
+
t.string :provider_message_id
|
|
15
|
+
t.integer :attempt_number
|
|
16
|
+
t.boolean :bulk, null: false, default: false
|
|
17
|
+
t.string :source_type
|
|
18
|
+
t.bigint :source_id
|
|
19
|
+
t.datetime :created_at, null: false
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
add_index :spree_notification_deliveries, %i[store_id created_at], name: 'idx_spree_notif_deliveries_on_store_created'
|
|
23
|
+
add_index :spree_notification_deliveries, %i[store_id channel outcome created_at],
|
|
24
|
+
name: 'idx_spree_notif_deliveries_on_store_channel_outcome'
|
|
25
|
+
add_index :spree_notification_deliveries, :message_ref
|
|
26
|
+
add_index :spree_notification_deliveries, :provider_message_id
|
|
27
|
+
add_index :spree_notification_deliveries, %i[source_type source_id]
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rails/generators'
|
|
4
|
+
|
|
5
|
+
module SpreeNotificationProviders
|
|
6
|
+
module Generators
|
|
7
|
+
# Copies provider environment variables (MSG91_AUTH_KEY, SMTP_HOST, ...)
|
|
8
|
+
# into a store's provider settings. Fills blanks only; safe to rerun.
|
|
9
|
+
class ImportEnvGenerator < Rails::Generators::Base
|
|
10
|
+
desc "Import provider settings from environment variables into a store's configuration"
|
|
11
|
+
|
|
12
|
+
class_option :store, type: :string, desc: 'Store code (default: the default store)'
|
|
13
|
+
class_option :dry_run, type: :boolean, default: false, desc: 'Print the report without saving'
|
|
14
|
+
|
|
15
|
+
def import
|
|
16
|
+
store = options[:store].present? ? Spree::Store.find_by!(code: options[:store]) : Spree::Store.default
|
|
17
|
+
writer = SpreeNotificationProviders::Importers::EnvImporter.new(store, dry_run: options[:dry_run]).call
|
|
18
|
+
say writer.report
|
|
19
|
+
say(options[:dry_run] ? 'Dry run: nothing saved.' : 'Saved.')
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rails/generators'
|
|
4
|
+
|
|
5
|
+
module SpreeNotificationProviders
|
|
6
|
+
module Generators
|
|
7
|
+
class InstallGenerator < Rails::Generators::Base
|
|
8
|
+
desc 'Install spree_notification_providers migrations and initializer'
|
|
9
|
+
source_root File.expand_path('templates', __dir__)
|
|
10
|
+
|
|
11
|
+
class_option :migrate, type: :boolean, default: true, banner: 'Run migrations'
|
|
12
|
+
|
|
13
|
+
def add_initializer
|
|
14
|
+
template 'initializer.rb', 'config/initializers/spree_notification_providers.rb'
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def add_migrations
|
|
18
|
+
run 'bundle exec rake railties:install:migrations FROM=spree_notification_providers'
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def run_migrations
|
|
22
|
+
if options[:migrate]
|
|
23
|
+
run 'bundle exec rake db:migrate'
|
|
24
|
+
else
|
|
25
|
+
say_status :skipping, 'db:migrate, remember to run migrations manually'
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def show_next_steps
|
|
30
|
+
say <<~TEXT
|
|
31
|
+
|
|
32
|
+
spree_notification_providers is installed. Next:
|
|
33
|
+
1. Add Spree::PermissionSets::NotificationProviderManagement to non-super-user admin roles.
|
|
34
|
+
2. Set config.action_mailer.delivery_method = :spree_notification_providers to route mailers.
|
|
35
|
+
3. Schedule Spree::NotificationDeliveries::PurgeJob daily.
|
|
36
|
+
4. Admin -> Settings -> Notification providers to add providers.
|
|
37
|
+
TEXT
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
SpreeNotificationProviders.configure do |config|
|
|
4
|
+
# Delivery log rows older than this are removed by
|
|
5
|
+
# Spree::NotificationDeliveries::PurgeJob.
|
|
6
|
+
config.delivery_retention_days = 90
|
|
7
|
+
|
|
8
|
+
# The Log provider writes messages to the Rails log instead of sending them.
|
|
9
|
+
# config.enable_log_provider = !Rails.env.production?
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Each message type gets its own template field on template-based providers.
|
|
13
|
+
# SpreeNotificationProviders.register_message_type :order_confirmation,
|
|
14
|
+
# label: 'Order confirmation', channels: %i[email sms]
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SpreeNotificationProviders
|
|
4
|
+
module Adapters
|
|
5
|
+
# What an adapter gets for each call.
|
|
6
|
+
#
|
|
7
|
+
# settings - the provider's shared and channel inputs merged, secrets
|
|
8
|
+
# decrypted, defaults applied, keyed by Symbol
|
|
9
|
+
# template - the template for this message's type (or the message's own
|
|
10
|
+
# override), or nil
|
|
11
|
+
# templates - every template configured on the channel, keyed by type
|
|
12
|
+
Context = Struct.new(:store, :channel, :settings, :template, :templates, keyword_init: true) do
|
|
13
|
+
def [](name)
|
|
14
|
+
settings[name.to_sym]
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Subclass this to add a provider. Return a Result from every method;
|
|
19
|
+
# handle transport errors yourself (see HttpOutcome). An exception raised
|
|
20
|
+
# here is recorded as a failure by the router.
|
|
21
|
+
class Base
|
|
22
|
+
TEST_CODE = '123456'
|
|
23
|
+
|
|
24
|
+
attr_reader :definition
|
|
25
|
+
|
|
26
|
+
def initialize(definition)
|
|
27
|
+
@definition = definition
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def deliver(_message, _context)
|
|
31
|
+
raise NotImplementedError, "#{self.class.name}#deliver must be implemented"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def deliver_bulk(_bulk, _context)
|
|
35
|
+
raise NotImplementedError, "#{self.class.name}#deliver_bulk must be implemented"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Admin "Send test": a real send through this provider only.
|
|
39
|
+
def test_message(channel, to, context)
|
|
40
|
+
deliver(test_message_for(channel, to), context)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def test_message_for(channel, to)
|
|
44
|
+
capability = definition.capability(channel)
|
|
45
|
+
kind = if capability.supports_kind?(:otp) then :otp
|
|
46
|
+
elsif capability.supports_kind?(:html) then :html
|
|
47
|
+
else :template
|
|
48
|
+
end
|
|
49
|
+
Message.new(
|
|
50
|
+
channel: channel, type: :provider_test, kind: kind, to: to,
|
|
51
|
+
code: TEST_CODE, expiry_minutes: 10,
|
|
52
|
+
subject: I18n.t('spree_notification_providers.test_message.subject', default: 'Test message'),
|
|
53
|
+
html: I18n.t('spree_notification_providers.test_message.html',
|
|
54
|
+
default: '<p>This is a test message from your store.</p>'),
|
|
55
|
+
variables: {}
|
|
56
|
+
)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
protected
|
|
60
|
+
|
|
61
|
+
# Replaces any secret input value found in `text` before it is stored as
|
|
62
|
+
# a reason or logged.
|
|
63
|
+
def scrub(text, context)
|
|
64
|
+
return nil if text.blank?
|
|
65
|
+
|
|
66
|
+
secrets = definition_secret_keys(context.channel).filter_map { |key| context[key].presence&.to_s }
|
|
67
|
+
secrets.reduce(text.to_s) { |memo, secret| memo.gsub(secret, '[FILTERED]') }.truncate(120)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def definition_secret_keys(channel)
|
|
71
|
+
inputs = definition.shared_inputs + Array(definition.capability(channel)&.inputs)
|
|
72
|
+
inputs.select(&:secret?).map(&:name)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|