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.
Files changed (61) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +24 -0
  3. data/LICENSE +29 -0
  4. data/README.md +322 -0
  5. data/app/controllers/spree/admin/notification_channel_providers_controller.rb +152 -0
  6. data/app/controllers/spree/admin/notification_deliveries_controller.rb +69 -0
  7. data/app/controllers/spree/admin/notification_providers_controller.rb +31 -0
  8. data/app/controllers/spree/webhooks/gupshup_controller.rb +61 -0
  9. data/app/helpers/spree/admin/notification_masking_helper.rb +65 -0
  10. data/app/jobs/spree/notification_deliveries/purge_job.rb +26 -0
  11. data/app/models/concerns/spree/notification_provider_settings.rb +81 -0
  12. data/app/models/spree/notification_channel_provider.rb +165 -0
  13. data/app/models/spree/notification_delivery.rb +39 -0
  14. data/app/models/spree/notification_provider_config.rb +32 -0
  15. data/app/models/spree/notification_providers_store_decorator.rb +16 -0
  16. data/app/models/spree/permission_sets/notification_provider_management.rb +20 -0
  17. data/app/views/spree/admin/notification_channel_providers/_input_field.html.erb +51 -0
  18. data/app/views/spree/admin/notification_channel_providers/edit.html.erb +118 -0
  19. data/app/views/spree/admin/notification_deliveries/index.html.erb +127 -0
  20. data/app/views/spree/admin/notification_deliveries/show.html.erb +52 -0
  21. data/app/views/spree/admin/notification_providers/index.html.erb +114 -0
  22. data/config/locales/en.yml +92 -0
  23. data/config/routes.rb +17 -0
  24. data/db/migrate/20261019000001_create_spree_notification_provider_configs.rb +19 -0
  25. data/db/migrate/20261019000002_create_spree_notification_channel_providers.rb +23 -0
  26. data/db/migrate/20261019000003_create_spree_notification_deliveries.rb +29 -0
  27. data/lib/generators/spree_notification_providers/import_env/import_env_generator.rb +23 -0
  28. data/lib/generators/spree_notification_providers/install/install_generator.rb +41 -0
  29. data/lib/generators/spree_notification_providers/install/templates/initializer.rb +14 -0
  30. data/lib/spree_notification_providers/adapters/base.rb +76 -0
  31. data/lib/spree_notification_providers/adapters/gupshup.rb +87 -0
  32. data/lib/spree_notification_providers/adapters/http_outcome.rb +61 -0
  33. data/lib/spree_notification_providers/adapters/log.rb +26 -0
  34. data/lib/spree_notification_providers/adapters/msg91.rb +117 -0
  35. data/lib/spree_notification_providers/adapters/smtp.rb +77 -0
  36. data/lib/spree_notification_providers/bulk_message.rb +33 -0
  37. data/lib/spree_notification_providers/configuration.rb +32 -0
  38. data/lib/spree_notification_providers/engine.rb +59 -0
  39. data/lib/spree_notification_providers/http.rb +42 -0
  40. data/lib/spree_notification_providers/importers/env_importer.rb +43 -0
  41. data/lib/spree_notification_providers/importers/writer.rb +132 -0
  42. data/lib/spree_notification_providers/input_definition.rb +75 -0
  43. data/lib/spree_notification_providers/mail_delivery_method.rb +78 -0
  44. data/lib/spree_notification_providers/message.rb +55 -0
  45. data/lib/spree_notification_providers/message_type.rb +26 -0
  46. data/lib/spree_notification_providers/message_types.rb +8 -0
  47. data/lib/spree_notification_providers/provider_definition.rb +103 -0
  48. data/lib/spree_notification_providers/providers/gupshup.rb +26 -0
  49. data/lib/spree_notification_providers/providers/log.rb +16 -0
  50. data/lib/spree_notification_providers/providers/msg91.rb +27 -0
  51. data/lib/spree_notification_providers/providers/smtp.rb +18 -0
  52. data/lib/spree_notification_providers/registry.rb +79 -0
  53. data/lib/spree_notification_providers/result.rb +73 -0
  54. data/lib/spree_notification_providers/router.rb +205 -0
  55. data/lib/spree_notification_providers/version.rb +5 -0
  56. data/lib/spree_notification_providers.rb +96 -0
  57. data/spec/factories/notification_providers.rb +91 -0
  58. data/spec/support/message_types.rb +15 -0
  59. data/spec/support/provider_http_helpers.rb +46 -0
  60. data/spree_notification_providers.gemspec +44 -0
  61. metadata +189 -0
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SpreeNotificationProviders
4
+ # What one adapter call returned.
5
+ #
6
+ # :delivered - the provider accepted the message.
7
+ # :failed - the provider definitely did not accept it; safe to try the next.
8
+ # :unknown - it may have reached the provider; trying another could duplicate it.
9
+ # :skipped - recorded by the router for providers it did not call.
10
+ #
11
+ # `reason` is a short code or provider message. Never put credentials or full
12
+ # response bodies in it.
13
+ class Result
14
+ OUTCOMES = %i[delivered failed unknown skipped].freeze
15
+
16
+ attr_reader :outcome, :provider_message_id, :reason
17
+
18
+ def self.delivered(provider_message_id = nil) = new(outcome: :delivered, provider_message_id: provider_message_id)
19
+ def self.failed(reason) = new(outcome: :failed, reason: reason)
20
+ def self.unknown(reason) = new(outcome: :unknown, reason: reason)
21
+ def self.skipped(reason) = new(outcome: :skipped, reason: reason)
22
+
23
+ def initialize(outcome:, provider_message_id: nil, reason: nil)
24
+ raise ArgumentError, "unknown outcome #{outcome.inspect}" unless OUTCOMES.include?(outcome)
25
+
26
+ @outcome = outcome
27
+ @provider_message_id = provider_message_id.presence&.to_s
28
+ @reason = reason.presence&.to_s&.truncate(255)
29
+ end
30
+
31
+ OUTCOMES.each do |name|
32
+ define_method(:"#{name}?") { outcome == name }
33
+ end
34
+ end
35
+
36
+ # What `SpreeNotificationProviders.deliver` returns.
37
+ class DeliveryResult
38
+ attr_reader :outcome, :provider_key, :provider_message_id, :message_ref, :attempts
39
+
40
+ def initialize(outcome:, message_ref:, attempts:, provider_key: nil, provider_message_id: nil)
41
+ @outcome = outcome
42
+ @message_ref = message_ref
43
+ @attempts = attempts
44
+ @provider_key = provider_key
45
+ @provider_message_id = provider_message_id
46
+ end
47
+
48
+ def success? = outcome == :delivered
49
+
50
+ # True only when no provider can have taken the message, so sending again
51
+ # (or through another channel) cannot duplicate it.
52
+ def retryable? = %i[failed no_provider].include?(outcome)
53
+
54
+ def reasons
55
+ attempts.filter_map { |attempt| "#{attempt[:provider_key] || 'none'}: #{attempt[:reason]}" if attempt[:reason] }
56
+ end
57
+ end
58
+
59
+ # What `SpreeNotificationProviders.deliver_bulk` returns.
60
+ class BulkResult
61
+ attr_reader :outcome, :provider_key, :message_ref, :delivered_count, :failed_recipients
62
+
63
+ def initialize(outcome:, message_ref:, delivered_count:, failed_recipients:, provider_key: nil)
64
+ @outcome = outcome
65
+ @message_ref = message_ref
66
+ @delivered_count = delivered_count
67
+ @failed_recipients = failed_recipients
68
+ @provider_key = provider_key
69
+ end
70
+
71
+ def success? = failed_recipients.empty? && outcome != :no_provider
72
+ end
73
+ end
@@ -0,0 +1,205 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SpreeNotificationProviders
4
+ # Sends messages through a store's providers in priority order.
5
+ #
6
+ # One-to-one: tries each enabled provider; providers that are not ready,
7
+ # have no template for the message type, or cannot send its kind are
8
+ # recorded as skipped. Stops at the first :delivered or :unknown, so only a
9
+ # definite failure moves on to the next provider.
10
+ #
11
+ # Bulk: the first enabled, ready, bulk-capable provider only. No failover.
12
+ #
13
+ # Every attempt (and every bulk recipient) is a NotificationDelivery row.
14
+ class Router
15
+ attr_reader :store
16
+
17
+ def initialize(store)
18
+ @store = store || SpreeNotificationProviders.configuration.default_store
19
+ end
20
+
21
+ def entries(channel)
22
+ Spree::NotificationChannelProvider
23
+ .where(store_id: store.id, channel: channel.to_s)
24
+ .includes(:store)
25
+ .order(:position)
26
+ .to_a
27
+ end
28
+
29
+ def deliver(message)
30
+ message_ref = SecureRandom.uuid
31
+ attempts = []
32
+ final_entry = nil
33
+ final_result = nil
34
+
35
+ entries(message.channel).select(&:enabled?).each do |entry|
36
+ skip = entry.skip_reason_for(message)
37
+ result = if skip
38
+ Result.skipped(skip)
39
+ else
40
+ call_adapter(entry, template: entry.template_for(message.type, message)) do |adapter, ctx|
41
+ adapter.deliver(message, ctx)
42
+ end
43
+ end
44
+ attempts << record(message, message_ref, attempts.size + 1, entry.provider_key, result)
45
+ next unless result.delivered? || result.unknown?
46
+
47
+ final_entry = entry
48
+ final_result = result
49
+ break
50
+ end
51
+
52
+ if attempts.empty?
53
+ attempts << record(message, message_ref, 1, nil, Result.skipped('no_provider'))
54
+ outcome = :no_provider
55
+ elsif final_result
56
+ outcome = final_result.outcome
57
+ else
58
+ outcome = attempts.any? { |a| a[:outcome] == :failed } ? :failed : :no_provider
59
+ end
60
+
61
+ result = DeliveryResult.new(
62
+ outcome: outcome, message_ref: message_ref, attempts: attempts,
63
+ provider_key: final_entry&.provider_key&.to_sym, provider_message_id: final_result&.provider_message_id
64
+ )
65
+ publish(result, message)
66
+ result
67
+ end
68
+
69
+ # One attempt through one entry, ignoring priority and the enabled flag.
70
+ # Used by the admin "Send test" action.
71
+ def deliver_via(entry, message)
72
+ message_ref = SecureRandom.uuid
73
+ missing = entry.missing_inputs
74
+ result = if entry.definition.nil?
75
+ Result.skipped('unavailable')
76
+ elsif missing.any?
77
+ Result.skipped("not_ready: missing #{missing.join(', ')}")
78
+ else
79
+ call_adapter(entry, template: entry.test_template) do |adapter, ctx|
80
+ adapter.test_message(message.channel, message.to, ctx)
81
+ end
82
+ end
83
+ attempt = record(message, message_ref, 1, entry.provider_key, result)
84
+ DeliveryResult.new(outcome: result.outcome, message_ref: message_ref, attempts: [attempt],
85
+ provider_key: entry.provider_key.to_sym, provider_message_id: result.provider_message_id)
86
+ end
87
+
88
+ def deliver_bulk(bulk)
89
+ message_ref = SecureRandom.uuid
90
+ entry = entries(bulk.channel).find { |candidate| candidate.enabled? && candidate.skip_reason_for(bulk).nil? }
91
+
92
+ unless entry
93
+ insert_bulk_rows(bulk, bulk.recipients, message_ref, nil, Result.skipped('no_provider'))
94
+ return BulkResult.new(outcome: :no_provider, message_ref: message_ref, delivered_count: 0,
95
+ failed_recipients: bulk.recipients.map { |r| { recipient: r, reason: 'no_provider' } })
96
+ end
97
+
98
+ delivered = 0
99
+ failed = []
100
+ bulk.recipients.each_slice(SpreeNotificationProviders.configuration.bulk_batch_size) do |batch|
101
+ result = call_adapter(entry, template: entry.template_for(bulk.type, bulk)) do |adapter, ctx|
102
+ adapter.deliver_bulk(bulk.with_recipients(batch), ctx)
103
+ end
104
+ insert_bulk_rows(bulk, batch, message_ref, entry.provider_key, result)
105
+ if result.delivered?
106
+ delivered += batch.size
107
+ elsif result.failed?
108
+ failed.concat(batch.map { |recipient| { recipient: recipient, reason: result.reason } })
109
+ end
110
+ end
111
+
112
+ outcome = if failed.empty? then :delivered
113
+ elsif delivered.zero? then :failed
114
+ else :partial
115
+ end
116
+ BulkResult.new(outcome: outcome, message_ref: message_ref, delivered_count: delivered,
117
+ failed_recipients: failed, provider_key: entry.provider_key.to_sym)
118
+ end
119
+
120
+ def ready_for?(channel, message_type)
121
+ entries(channel).any? { |entry| entry.ready_for?(message_type) }
122
+ end
123
+
124
+ def channel_status(channel)
125
+ entries(channel).map do |entry|
126
+ { provider_key: entry.provider_key.to_sym, position: entry.position, status: entry.status,
127
+ missing: entry.missing_inputs }
128
+ end
129
+ end
130
+
131
+ private
132
+
133
+ def call_adapter(entry, template: nil)
134
+ adapter = entry.definition.adapter_class.new(entry.definition)
135
+ context = entry.context(template: template)
136
+ result = yield(adapter, context)
137
+ result.is_a?(Result) ? result : Result.failed('invalid_adapter_result')
138
+ rescue StandardError => e
139
+ Rails.logger.error("[NotificationProviders] #{entry.provider_key} raised #{e.class}: #{e.message}")
140
+ Result.failed("exception:#{e.class.name}")
141
+ end
142
+
143
+ def record(message, message_ref, attempt_number, provider_key, result)
144
+ write_delivery(message, message_ref, attempt_number, provider_key, result)
145
+ { provider_key: provider_key&.to_sym, outcome: result.outcome, reason: result.reason }
146
+ end
147
+
148
+ def write_delivery(message, message_ref, attempt_number, provider_key, result)
149
+ Spree::NotificationDelivery.create!(
150
+ store_id: store.id,
151
+ message_ref: message_ref,
152
+ channel: message.channel.to_s,
153
+ provider_key: provider_key&.to_s,
154
+ message_type: message.type.to_s,
155
+ recipient: message.to.presence || '(none)',
156
+ outcome: result.outcome.to_s,
157
+ reason: result.reason,
158
+ provider_message_id: result.provider_message_id,
159
+ attempt_number: attempt_number,
160
+ bulk: false,
161
+ source: message.source.is_a?(ActiveRecord::Base) ? message.source : nil
162
+ )
163
+ rescue StandardError => e
164
+ # A logging failure must never stop a message from being sent.
165
+ Rails.logger.error("[NotificationProviders] could not record delivery: #{e.class}: #{e.message}")
166
+ end
167
+
168
+ def insert_bulk_rows(bulk, recipients, message_ref, provider_key, result)
169
+ return if recipients.empty?
170
+
171
+ now = Time.current
172
+ source = bulk.source.is_a?(ActiveRecord::Base) ? bulk.source : nil
173
+ rows = recipients.map do |recipient|
174
+ {
175
+ store_id: store.id, message_ref: message_ref, channel: bulk.channel.to_s,
176
+ provider_key: provider_key&.to_s, message_type: bulk.type.to_s, recipient: recipient,
177
+ outcome: result.outcome.to_s, reason: result.reason, provider_message_id: result.provider_message_id,
178
+ attempt_number: 1, bulk: true, source_type: source&.class&.base_class&.name, source_id: source&.id,
179
+ created_at: now
180
+ }
181
+ end
182
+ Spree::NotificationDelivery.insert_all(rows)
183
+ rescue StandardError => e
184
+ Rails.logger.error("[NotificationProviders] could not record bulk deliveries: #{e.class}: #{e.message}")
185
+ end
186
+
187
+ def publish(result, message)
188
+ return unless defined?(Spree::Events)
189
+
190
+ payload = {
191
+ 'store_id' => store.id, 'message_ref' => result.message_ref, 'channel' => message.channel.to_s,
192
+ 'provider_key' => result.provider_key&.to_s, 'message_type' => message.type.to_s,
193
+ 'source_type' => message.source.is_a?(ActiveRecord::Base) ? message.source.class.base_class.name : nil,
194
+ 'source_id' => message.source.is_a?(ActiveRecord::Base) ? message.source.id : nil
195
+ }
196
+ if result.success?
197
+ Spree::Events.publish('notification.delivered', payload)
198
+ elsif result.outcome != :unknown
199
+ Spree::Events.publish('notification.failed', payload.merge('reasons' => result.reasons))
200
+ end
201
+ rescue StandardError => e
202
+ Rails.logger.error("[NotificationProviders] could not publish event: #{e.class}: #{e.message}")
203
+ end
204
+ end
205
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SpreeNotificationProviders
4
+ VERSION = '0.1.0'
5
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spree/core'
4
+ # Loaded first so its after_initialize (which creates the admin navigation)
5
+ # runs before ours registers menu entries.
6
+ require 'spree_admin'
7
+ require 'securerandom'
8
+
9
+ require 'spree_notification_providers/version'
10
+
11
+ # Per-channel notification providers with priority and failover.
12
+ #
13
+ # Providers (MSG91, SMTP, Gupshup, or one an app registers) declare the
14
+ # channels they serve and the inputs they need. Each store enables providers
15
+ # per channel and orders them; `deliver` tries them in that order and moves on
16
+ # only when a provider definitely did not take the message. See README.
17
+ module SpreeNotificationProviders
18
+ CHANNELS = %i[email sms whatsapp].freeze
19
+
20
+ class Error < StandardError; end
21
+ class DuplicateProvider < Error; end
22
+ class UnknownChannel < Error; end
23
+ class DeliveryFailed < Error; end
24
+ end
25
+
26
+ require 'spree_notification_providers/configuration'
27
+ require 'spree_notification_providers/input_definition'
28
+ require 'spree_notification_providers/provider_definition'
29
+ require 'spree_notification_providers/message_type'
30
+ require 'spree_notification_providers/registry'
31
+ require 'spree_notification_providers/result'
32
+ require 'spree_notification_providers/message'
33
+ require 'spree_notification_providers/bulk_message'
34
+ require 'spree_notification_providers/http'
35
+ require 'spree_notification_providers/adapters/base'
36
+ require 'spree_notification_providers/adapters/http_outcome'
37
+ require 'spree_notification_providers/adapters/log'
38
+ require 'spree_notification_providers/adapters/msg91'
39
+ require 'spree_notification_providers/adapters/smtp'
40
+ require 'spree_notification_providers/adapters/gupshup'
41
+ require 'spree_notification_providers/router'
42
+ require 'spree_notification_providers/mail_delivery_method'
43
+ require 'spree_notification_providers/importers/env_importer'
44
+
45
+ module SpreeNotificationProviders
46
+ class << self
47
+ def configuration
48
+ @configuration ||= Configuration.new
49
+ end
50
+
51
+ def configure
52
+ yield configuration
53
+ end
54
+
55
+ def registry
56
+ @registry ||= Registry.new
57
+ end
58
+
59
+ delegate :register_provider, :unregister_provider, :provider, :providers, :providers_for,
60
+ :register_message_type, :message_type, :message_types, :message_types_for,
61
+ :register_message_resolver, :resolve_message_store, to: :registry
62
+
63
+ # Sends a one-to-one message through the store's providers for its channel,
64
+ # in priority order. Never raises for provider failures.
65
+ def deliver(store:, message:)
66
+ Router.new(store).deliver(message)
67
+ end
68
+
69
+ # Sends to many recipients through the top-priority ready provider only.
70
+ def deliver_bulk(store:, message:)
71
+ Router.new(store).deliver_bulk(message)
72
+ end
73
+
74
+ def ready_for?(store, channel, message_type)
75
+ Router.new(store).ready_for?(channel, message_type)
76
+ end
77
+
78
+ def channel_status(store, channel)
79
+ Router.new(store).channel_status(channel)
80
+ end
81
+
82
+ def channel!(value)
83
+ channel = value.to_s.to_sym
84
+ raise UnknownChannel, "unknown channel #{value.inspect}" unless CHANNELS.include?(channel)
85
+
86
+ channel
87
+ end
88
+ end
89
+ end
90
+
91
+ require 'spree_notification_providers/providers/msg91'
92
+ require 'spree_notification_providers/providers/smtp'
93
+ require 'spree_notification_providers/providers/gupshup'
94
+ require 'spree_notification_providers/providers/log'
95
+ require 'spree_notification_providers/message_types'
96
+ require 'spree_notification_providers/engine'
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ FactoryBot.define do
4
+ factory :notification_provider_config, class: 'Spree::NotificationProviderConfig' do
5
+ store { Spree::Store.default || association(:store) }
6
+ provider_key { 'msg91' }
7
+ settings { {} }
8
+
9
+ transient do
10
+ secrets { {} }
11
+ end
12
+
13
+ after(:build) do |config, evaluator|
14
+ config.secret_settings = evaluator.secrets.stringify_keys.to_json if evaluator.secrets.present?
15
+ end
16
+ end
17
+
18
+ factory :notification_channel_provider, class: 'Spree::NotificationChannelProvider' do
19
+ store { Spree::Store.default || association(:store) }
20
+ channel { 'sms' }
21
+ provider_key { 'msg91' }
22
+ enabled { true }
23
+ settings { {} }
24
+ templates { {} }
25
+
26
+ transient do
27
+ secrets { {} }
28
+ shared { {} }
29
+ shared_secrets { {} }
30
+ end
31
+
32
+ after(:build) do |entry, evaluator|
33
+ entry.secret_settings = evaluator.secrets.stringify_keys.to_json if evaluator.secrets.present?
34
+ if evaluator.shared.present? || evaluator.shared_secrets.present?
35
+ config = Spree::NotificationProviderConfig.find_or_initialize_by(store_id: entry.store.id,
36
+ provider_key: entry.provider_key)
37
+ config.settings = config.plain_values.merge(evaluator.shared.stringify_keys)
38
+ if evaluator.shared_secrets.present?
39
+ config.secret_settings = config.secret_values.merge(evaluator.shared_secrets.stringify_keys).to_json
40
+ end
41
+ config.save!
42
+ end
43
+ end
44
+
45
+ # MSG91 SMS, ready to send OTPs.
46
+ trait :msg91_sms do
47
+ channel { 'sms' }
48
+ provider_key { 'msg91' }
49
+ shared_secrets { { auth_key: 'msg91-test-auth-key-123456' } }
50
+ templates { { 'otp_code' => 'otp-template-1' } }
51
+ end
52
+
53
+ trait :msg91_email do
54
+ channel { 'email' }
55
+ provider_key { 'msg91' }
56
+ shared_secrets { { auth_key: 'msg91-test-auth-key-123456' } }
57
+ settings { { 'domain' => 'mail.example.com', 'default_from_email' => 'no-reply@mail.example.com' } }
58
+ templates { { 'template_email' => 'msg91-email-template' } }
59
+ end
60
+
61
+ trait :smtp do
62
+ channel { 'email' }
63
+ provider_key { 'smtp' }
64
+ settings { { 'host' => 'smtp.example.com', 'port' => '587', 'username' => 'mailer@example.com' } }
65
+ secrets { { password: 'smtp-test-password' } }
66
+ end
67
+
68
+ trait :gupshup do
69
+ channel { 'whatsapp' }
70
+ provider_key { 'gupshup' }
71
+ shared_secrets { { api_key: 'gupshup-test-api-key-1234', webhook_secret: 'hook-secret-abcdef' } }
72
+ settings { { 'source_number' => '919876543210', 'app_name' => 'shopapp', 'app_id' => 'app-123' } }
73
+ templates { { 'otp_code' => 'c6aecef6-bcb0-4fb1-8100-28c094e3bc6b' } }
74
+ end
75
+
76
+ trait :log do
77
+ provider_key { 'log' }
78
+ end
79
+ end
80
+
81
+ factory :notification_delivery, class: 'Spree::NotificationDelivery' do
82
+ store { Spree::Store.default || association(:store) }
83
+ message_ref { SecureRandom.uuid }
84
+ channel { 'email' }
85
+ provider_key { 'smtp' }
86
+ message_type { 'transactional_email' }
87
+ recipient { 'customer@example.com' }
88
+ outcome { 'delivered' }
89
+ attempt_number { 1 }
90
+ end
91
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Message types the specs use. In the host app they are registered by its
4
+ # initializer and by spree_otp_auth; a standalone run registers them here.
5
+ {
6
+ otp_code: ['Login code', %i[email sms whatsapp], false],
7
+ template_email: ['Email template', %i[email], false],
8
+ back_in_stock: ['Back in stock', %i[email sms], false],
9
+ order_notification: ['Order notification', %i[email], false],
10
+ customer_blast: ['Customer group blast', %i[email sms], true]
11
+ }.each do |key, (label, channels, bulk)|
12
+ next if SpreeNotificationProviders.message_type(key)
13
+
14
+ SpreeNotificationProviders.register_message_type(key, label: label, channels: channels, bulk: bulk)
15
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Fakes SpreeNotificationProviders::Http for adapter and router specs.
4
+ #
5
+ # stub_provider_http(status: 200, body: { 'type' => 'success' })
6
+ # stub_provider_http(error: Net::ReadTimeout)
7
+ # stub_provider_http { |method, url, opts| [200, {...}] } # per-request
8
+ #
9
+ # Every request is recorded in `provider_http_requests` as
10
+ # { method:, url:, headers:, query:, form:, json: }.
11
+ module ProviderHttpHelpers
12
+ FakeResponse = Struct.new(:code, :body)
13
+
14
+ def provider_http_requests
15
+ @provider_http_requests ||= []
16
+ end
17
+
18
+ def stub_provider_http(status: 200, body: {}, error: nil, &block)
19
+ recorder = provider_http_requests
20
+ allow(SpreeNotificationProviders::Http).to receive(:request) do |method, url, **opts|
21
+ recorder << opts.merge(method: method, url: url)
22
+ raise error if error
23
+
24
+ code, payload = block ? block.call(method, url, opts) : [status, body]
25
+ raise payload if payload.is_a?(Class) && payload <= Exception
26
+
27
+ FakeResponse.new(code.to_s, payload.is_a?(String) ? payload : payload.to_json)
28
+ end
29
+ end
30
+ end
31
+
32
+ RSpec.configure do |config|
33
+ config.include ProviderHttpHelpers
34
+
35
+ # Bundled adapters must never reach a real provider from a spec.
36
+ config.before do
37
+ allow(SpreeNotificationProviders::Http).to receive(:request)
38
+ .and_raise('Real provider HTTP call in a spec; use stub_provider_http')
39
+ end
40
+ end
41
+
42
+ # The host app does not load engine factories itself; a standalone run (and
43
+ # apps using this gem's helpers) may already have them through factory_bot.
44
+ unless FactoryBot.factories.registered?(:notification_provider_config)
45
+ Dir[File.expand_path('../factories/**/*.rb', __dir__)].each { |file| require file }
46
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/spree_notification_providers/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'spree_notification_providers'
7
+ spec.version = SpreeNotificationProviders::VERSION
8
+ spec.authors = ['ONTODI']
9
+ spec.summary = 'Per-channel notification providers with priority and failover for Spree Commerce'
10
+ spec.description = <<~DESC.strip
11
+ Lets each store configure Email, SMS and WhatsApp providers (MSG91, SMTP, Gupshup or your own),
12
+ enter only the inputs each provider needs, and rank them by priority. Messages fail over to the
13
+ next provider on definite failures only, every attempt is recorded in an admin Delivery log, and
14
+ secrets are stored encrypted. Ships an Action Mailer delivery method so existing mailers route
15
+ through the same priority list.
16
+ DESC
17
+ spec.license = 'BSD-3-Clause'
18
+
19
+ spec.required_ruby_version = '>= 3.3'
20
+
21
+ spec.files = Dir[
22
+ '{app,config,db,lib}/**/*',
23
+ # Test helpers for apps and extensions that build on this gem.
24
+ 'spec/support/**/*',
25
+ 'spec/factories/**/*',
26
+ 'README.md',
27
+ 'CHANGELOG.md',
28
+ 'LICENSE',
29
+ 'spree_notification_providers.gemspec'
30
+ ]
31
+ spec.require_paths = ['lib']
32
+
33
+ spec.add_dependency 'acts_as_list', '>= 1.0'
34
+ spec.add_dependency 'pagy', '>= 43.0'
35
+ spec.add_dependency 'rails', '>= 8.0'
36
+ spec.add_dependency 'ransack', '>= 4.0'
37
+ spec.add_dependency 'spree_admin', '>= 5.5'
38
+ spec.add_dependency 'spree_core', '>= 5.5'
39
+
40
+ spec.homepage = 'https://github.com/kalanjiyaVishnu/spree_notification_providers'
41
+ spec.metadata['source_code_uri'] = spec.homepage
42
+ spec.metadata['changelog_uri'] = "#{spec.homepage}/blob/main/CHANGELOG.md"
43
+ spec.metadata['rubygems_mfa_required'] = 'true'
44
+ end