diff --git a/app/controllers/spree/admin/webhooks_subscriber_rules_controller.rb b/app/controllers/spree/admin/webhooks_subscriber_rules_controller.rb
new file mode 100644
index 000000000..1514fb275
--- /dev/null
+++ b/app/controllers/spree/admin/webhooks_subscriber_rules_controller.rb
@@ -0,0 +1,44 @@
+module Spree
+ module Admin
+ class WebhooksSubscriberRulesController < Spree::Admin::ResourceController
+ before_action :load_webhooks_subscriber
+
+ def load_webhooks_subscriber
+ @webhooks_subscriber = Spree::Webhooks::Subscriber.find(params[:webhooks_subscriber_id])
+ end
+
+ def scope
+ load_webhooks_subscriber
+
+ @webhooks_subscriber.rules
+ end
+
+ # @overrided
+ def collection
+ scope
+ end
+
+ # @overrided
+ def load_resource_instance
+ return scope.new if new_actions.include?(action)
+
+ scope.find(params[:id])
+ end
+
+ def collection_url(options = {})
+ edit_admin_webhooks_subscriber_url(params[:webhooks_subscriber_id], options)
+ end
+
+ # @overrided
+ def model_class
+ SpreeCmCommissioner::Webhooks::SubscriberRule
+ end
+
+ # @overrided
+ # depend on type of rule eg. spree_cm_commissioner_webhooks_rules_vendors
+ def object_name
+ @object.class.to_s.underscore.tr('/', '_')
+ end
+ end
+ end
+end
diff --git a/app/models/concerns/spree_cm_commissioner/webhooks/subscriber_rulable.rb b/app/models/concerns/spree_cm_commissioner/webhooks/subscriber_rulable.rb
new file mode 100644
index 000000000..68c9d6f1c
--- /dev/null
+++ b/app/models/concerns/spree_cm_commissioner/webhooks/subscriber_rulable.rb
@@ -0,0 +1,53 @@
+module SpreeCmCommissioner
+ module Webhooks
+ module SubscriberRulable
+ extend ActiveSupport::Concern
+
+ MATCH_POLICIES = %i[all any].freeze
+
+ SUPPORTED_RULE_TYPES = [
+ SpreeCmCommissioner::Webhooks::Rules::OrderStates,
+ SpreeCmCommissioner::Webhooks::Rules::OrderVendors
+ ].map(&:to_s)
+
+ included do
+ enum match_policy: MATCH_POLICIES, _prefix: true
+
+ has_many :rules, autosave: true, dependent: :destroy, class_name: 'SpreeCmCommissioner::Webhooks::SubscriberRule'
+ end
+
+ def available_rule_types
+ existing = rules.pluck(:type)
+
+ SUPPORTED_RULE_TYPES.reject { |r| existing.include? r }
+ end
+
+ def match_all?
+ match_policy == 'all'
+ end
+
+ def match_any?
+ match_policy == 'any'
+ end
+
+ def matches?(event, webhook_payload_body, options = {})
+ # Subscriber without rules are always match by default.
+ return true if rules.none?
+
+ # Reject if event is not supported by rules
+ supported_rules = rules.select { |rule| rule.supported?(event) }
+ return false if supported_rules.none?
+
+ if match_all?
+ supported_rules.all? do |rule|
+ rule.matches?(event, webhook_payload_body, options)
+ end
+ elsif match_any?
+ supported_rules.any? do |rule|
+ rule.matches?(event, webhook_payload_body, options)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/models/spree_cm_commissioner/line_item_decorator.rb b/app/models/spree_cm_commissioner/line_item_decorator.rb
index 2b8c104a7..c8ca3fa13 100644
--- a/app/models/spree_cm_commissioner/line_item_decorator.rb
+++ b/app/models/spree_cm_commissioner/line_item_decorator.rb
@@ -15,6 +15,12 @@ def self.prepended(base)
base.before_create :add_due_date, if: :subscription?
base.whitelisted_ransackable_attributes |= %w[to_date from_date]
+
+ def base.json_api_columns
+ json_api_columns = column_names.reject { |c| c.match(/_id$|id|preferences|(.*)password|(.*)token|(.*)api_key/) }
+ json_api_columns << :options_text
+ json_api_columns << :vendor_id
+ end
end
def reservation?
diff --git a/app/models/spree_cm_commissioner/webhooks.rb b/app/models/spree_cm_commissioner/webhooks.rb
new file mode 100644
index 000000000..ef4afeb1f
--- /dev/null
+++ b/app/models/spree_cm_commissioner/webhooks.rb
@@ -0,0 +1,7 @@
+module SpreeCmCommissioner
+ module Webhooks
+ def self.table_name_prefix
+ 'cm_webhooks_'
+ end
+ end
+end
diff --git a/app/models/spree_cm_commissioner/webhooks/rules/order_states.rb b/app/models/spree_cm_commissioner/webhooks/rules/order_states.rb
new file mode 100644
index 000000000..6ed1c4f84
--- /dev/null
+++ b/app/models/spree_cm_commissioner/webhooks/rules/order_states.rb
@@ -0,0 +1,43 @@
+module SpreeCmCommissioner
+ module Webhooks
+ module Rules
+ class OrderStates < SubscriberRule
+ SUPPORTED_EVENTS = [
+ 'order.create',
+ 'order.delete',
+ 'order.update',
+ 'order.canceled',
+ 'order.placed',
+ 'order.resumed',
+ 'order.shipped'
+ ].freeze
+
+ DEFAULT_STATES = %w[
+ cart
+ address
+ payment
+ complete
+ delivery
+ awaiting_return
+ canceled
+ returned
+ resumed
+ ].freeze
+
+ preference :states, :array, default: DEFAULT_STATES
+
+ def supported?(event)
+ SUPPORTED_EVENTS.include?(event)
+ end
+
+ def matches?(_event, webhook_payload_body, _options = {})
+ payload_body = JSON.parse(webhook_payload_body)
+
+ state = payload_body['data']['attributes']['state']
+
+ preferred_states.include?(state)
+ end
+ end
+ end
+ end
+end
diff --git a/app/models/spree_cm_commissioner/webhooks/rules/order_vendors.rb b/app/models/spree_cm_commissioner/webhooks/rules/order_vendors.rb
new file mode 100644
index 000000000..78d92b5c5
--- /dev/null
+++ b/app/models/spree_cm_commissioner/webhooks/rules/order_vendors.rb
@@ -0,0 +1,41 @@
+module SpreeCmCommissioner
+ module Webhooks
+ module Rules
+ class OrderVendors < SubscriberRule
+ MATCH_POLICIES = %w[any all].freeze
+
+ SUPPORTED_EVENTS = [
+ 'order.create',
+ 'order.delete',
+ 'order.update',
+ 'order.canceled',
+ 'order.placed',
+ 'order.resumed',
+ 'order.shipped'
+ ].freeze
+
+ preference :match_policy, :string, default: MATCH_POLICIES.first
+ preference :vendors, :array
+
+ def supported?(event)
+ SUPPORTED_EVENTS.include?(event)
+ end
+
+ def matches?(_event, webhook_payload_body, _options = {})
+ payload_body = JSON.parse(webhook_payload_body)
+
+ vendor_ids = payload_body['included'].filter_map do |include|
+ include['attributes']['vendor_id'].to_s if include['type'] == 'line_item'
+ end
+
+ case preferred_match_policy
+ when 'any'
+ preferred_vendors.any? { |vendor_id| vendor_ids.include?(vendor_id) }
+ when 'all'
+ preferred_vendors.all? { |vendor_id| vendor_ids.include?(vendor_id) }
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/models/spree_cm_commissioner/webhooks/subscriber_decorator.rb b/app/models/spree_cm_commissioner/webhooks/subscriber_decorator.rb
new file mode 100644
index 000000000..c22c3683a
--- /dev/null
+++ b/app/models/spree_cm_commissioner/webhooks/subscriber_decorator.rb
@@ -0,0 +1,13 @@
+module SpreeCmCommissioner
+ module Webhooks
+ module SubscriberDecorator
+ def self.prepended(base)
+ base.include SpreeCmCommissioner::Webhooks::SubscriberRulable
+ end
+ end
+ end
+end
+
+unless Spree::Webhooks::Subscriber.included_modules.include?(SpreeCmCommissioner::Webhooks::SubscriberDecorator)
+ Spree::Webhooks::Subscriber.prepend(SpreeCmCommissioner::Webhooks::SubscriberDecorator)
+end
diff --git a/app/models/spree_cm_commissioner/webhooks/subscriber_rule.rb b/app/models/spree_cm_commissioner/webhooks/subscriber_rule.rb
new file mode 100644
index 000000000..a5d53cc75
--- /dev/null
+++ b/app/models/spree_cm_commissioner/webhooks/subscriber_rule.rb
@@ -0,0 +1,11 @@
+module SpreeCmCommissioner
+ module Webhooks
+ class SubscriberRule < Base
+ belongs_to :subscriber, class_name: 'Spree::Webhooks::Subscriber', inverse_of: :rules
+
+ def matches?(_event, _webhook_payload_body, _options = {})
+ raise 'matches? should be implemented in a sub-class of SpreeCmCommissioner::Webhooks::SubscriberRule'
+ end
+ end
+ end
+end
diff --git a/app/overrides/spree/admin/webhooks_subscribers/_form/api_key.html.erb.deface b/app/overrides/spree/admin/webhooks_subscribers/_form/api_key.html.erb.deface
new file mode 100644
index 000000000..6dc098a03
--- /dev/null
+++ b/app/overrides/spree/admin/webhooks_subscribers/_form/api_key.html.erb.deface
@@ -0,0 +1,7 @@
+
+
+<%= f.field_container :api_key, class: ['form-group'] do %>
+ <%= f.label :api_key, '3rd API key' %>
+ <%= f.text_field :api_key, class: 'form-control', placeholder: 'Headers : X-Api-key' %>
+ <%= f.error_message_on :api_key %>
+<% end %>
diff --git a/app/overrides/spree/admin/webhooks_subscribers/_form/name.html.erb.deface b/app/overrides/spree/admin/webhooks_subscribers/_form/name.html.erb.deface
new file mode 100644
index 000000000..fe71aba79
--- /dev/null
+++ b/app/overrides/spree/admin/webhooks_subscribers/_form/name.html.erb.deface
@@ -0,0 +1,7 @@
+
+
+<%= f.field_container :name, class: ['form-group'] do %>
+ <%= f.label :name, Spree.t(:name) %>
+ <%= f.text_field :name, class: 'form-control' %>
+ <%= f.error_message_on :name %>
+<% end %>
diff --git a/app/overrides/spree/admin/webhooks_subscribers/_form/rules.html.erb.deface b/app/overrides/spree/admin/webhooks_subscribers/_form/rules.html.erb.deface
new file mode 100644
index 000000000..cdb46b49c
--- /dev/null
+++ b/app/overrides/spree/admin/webhooks_subscribers/_form/rules.html.erb.deface
@@ -0,0 +1,52 @@
+
+
+
+
+
Rules
+ <%= button_link_to Spree.t(:new), new_admin_webhooks_subscriber_rule_path(@webhooks_subscriber), class: "btn-light", icon: 'add.svg' %>
+
+
+ <% if @webhooks_subscriber.rules.any? %>
+
+
+
+ <%= Spree.t(:id) %> |
+ <%= Spree.t(:rule) %> |
+ <%= Spree.t(:preferences) %> |
+ <%= Spree.t(:supported_events) %> |
+ <%= Spree.t(:created_at) %> |
+ <%= Spree.t(:updated_at) %> |
+
+
+
+ <% @webhooks_subscriber.rules.each do |rule| %>
+
+ <%= rule.id %> |
+ <%= rule.class.name.demodulize %> |
+ <%= rule.preferences %> |
+ <%= rule.class::SUPPORTED_EVENTS.to_sentence %> |
+ <%= rule.created_at %> |
+ <%= rule.updated_at %> |
+
+
+ <%= link_to_edit rule, url: edit_admin_webhooks_subscriber_rule_path(@webhooks_subscriber, rule), no_text: true if can?(:edit, rule) %>
+ <%= link_to_delete rule, url: admin_webhooks_subscriber_rule_path(@webhooks_subscriber, rule), no_text: true if can?(:delete, rule) %>
+
+ |
+
+ <% end %>
+
+
+
+ <%= f.field_container :match_policy, class: ['form-group mt-3'] do %>
+ <%= f.label :match_policy, Spree.t(:match_policy) %>
+ <%= f.select :match_policy, @object.class::MATCH_POLICIES, {}, :class => "fullwidth select2" %>
+ <%= f.error_message_on :match_policy %>
+ <% end %>
+
+ <% else %>
+
+ <%= raw I18n.t('webhooks_subscriber_rules.empty_info') %>
+
+ <% end %>
+
diff --git a/app/overrides/spree/admin/webhooks_subscribers/index/table.html.erb.deface b/app/overrides/spree/admin/webhooks_subscribers/index/table.html.erb.deface
new file mode 100644
index 000000000..308939487
--- /dev/null
+++ b/app/overrides/spree/admin/webhooks_subscribers/index/table.html.erb.deface
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+ <%= Spree.t('admin.url') %> |
+ <%= Spree.t('name') %> |
+ <% if defined?(Spree::Vendor) && can?(:manage, Spree::Vendor) && !current_spree_vendor %>
+ <%= Spree.t(:vendor) %> |
+ <% end %>
+ <%= Spree.t('admin.active') %> |
+ <%= Spree.t('admin.webhooks_subscribers.subscriptions') %> |
+ <%= Spree.t('admin.webhooks_subscribers.time_of_last_event') %> |
+ <%= Spree.t('admin.webhooks_subscribers.all_events') %> |
+ |
+
+
+
+ <% @webhooks_subscribers.each do |webhooks_subscriber| %>
+
+ <%= webhooks_subscriber.url %> |
+ <%= webhooks_subscriber.name %> |
+ <% if defined?(Spree::Vendor) && can?(:manage, Spree::Vendor) && !current_spree_vendor %>
+
+ <%= link_to webhooks_subscriber.vendor.name, spree.admin_vendor_path(webhooks_subscriber.vendor) if webhooks_subscriber.vendor.present? %>
+ |
+ <% end %>
+ <%= active_badge(webhooks_subscriber.active) %> |
+ <%= webhooks_subscriber.subscriptions&.join(', ') %> |
+ <%= webhooks_subscriber.events.order(:created_at).last&.created_at %> |
+ <%= link_to Spree.t(:view), admin_webhooks_subscriber_path(webhooks_subscriber) %> |
+
+
+ <%= link_to_with_icon 'activity.svg', Spree.t(:event).pluralize, admin_webhooks_events_path({q: {subscriber_id_eq: webhooks_subscriber}}), class: 'btn btn-light btn-sm', no_text: true %>
+ <%= link_to_edit(webhooks_subscriber, no_text: true) if can? :edit, webhooks_subscriber %>
+ <%= link_to_delete(webhooks_subscriber, no_text: true) if can? :delete, webhooks_subscriber %>
+
+ |
+
+ <% end %>
+
+
+
diff --git a/app/services/spree_cm_commissioner/webhooks/subscribers/handle_request_decorator.rb b/app/services/spree_cm_commissioner/webhooks/subscribers/handle_request_decorator.rb
new file mode 100644
index 000000000..599a87053
--- /dev/null
+++ b/app/services/spree_cm_commissioner/webhooks/subscribers/handle_request_decorator.rb
@@ -0,0 +1,19 @@
+module SpreeCmCommissioner
+ module Webhooks
+ module Subscribers
+ module HandleRequestDecorator
+ def self.prepended(_base)
+ delegate :api_key, to: :subscriber
+ end
+
+ # override
+ def request
+ @request ||=
+ SpreeCmCommissioner::Webhooks::Subscribers::MakeRequest.new(url: url, api_key: api_key, webhook_payload_body: body_with_event_metadata)
+ end
+ end
+ end
+ end
+end
+
+Spree::Webhooks::Subscribers::HandleRequest.prepend SpreeCmCommissioner::Webhooks::Subscribers::HandleRequestDecorator
diff --git a/app/services/spree_cm_commissioner/webhooks/subscribers/make_request.rb b/app/services/spree_cm_commissioner/webhooks/subscribers/make_request.rb
new file mode 100644
index 000000000..d542edc1d
--- /dev/null
+++ b/app/services/spree_cm_commissioner/webhooks/subscribers/make_request.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+module SpreeCmCommissioner
+ module Webhooks
+ module Subscribers
+ class MakeRequest < Spree::Webhooks::Subscribers::MakeRequest
+ attr_reader :api_key
+
+ def initialize(url:, api_key:, webhook_payload_body:)
+ @api_key = api_key
+ super(url: url, webhook_payload_body: webhook_payload_body)
+ end
+
+ def headers
+ headers = {}
+
+ headers['Content-Type'] = 'application/json'
+ headers['X-Api-Key'] = api_key if api_key.present?
+
+ headers
+ end
+
+ # overrided
+ def request
+ req = Net::HTTP::Post.new(uri_path, headers)
+ req.body = webhook_payload_body
+ @request ||= begin
+ start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
+ request_result = http.request(req)
+ @execution_time_in_milliseconds = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time).in_milliseconds
+ request_result
+ end
+ rescue Errno::ECONNREFUSED, Net::ReadTimeout, SocketError
+ Class.new do
+ def self.code
+ '0'
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/services/spree_cm_commissioner/webhooks/subscribers/queue_requests_decorator.rb b/app/services/spree_cm_commissioner/webhooks/subscribers/queue_requests_decorator.rb
new file mode 100644
index 000000000..3454129b4
--- /dev/null
+++ b/app/services/spree_cm_commissioner/webhooks/subscribers/queue_requests_decorator.rb
@@ -0,0 +1,16 @@
+module SpreeCmCommissioner
+ module Webhooks
+ module Subscribers
+ module QueueRequestsDecorator
+ # override
+ def filtered_subscribers(event_name, webhook_payload_body, options)
+ Spree::Webhooks::Subscriber.active.with_urls_for(event_name).select do |subscriber|
+ subscriber.matches?(event_name, webhook_payload_body, options)
+ end
+ end
+ end
+ end
+ end
+end
+
+Spree::Webhooks::Subscribers::QueueRequests.prepend SpreeCmCommissioner::Webhooks::Subscribers::QueueRequestsDecorator
diff --git a/app/views/spree/admin/webhooks_subscriber_rules/_form.html.erb b/app/views/spree/admin/webhooks_subscriber_rules/_form.html.erb
new file mode 100644
index 000000000..0322445ea
--- /dev/null
+++ b/app/views/spree/admin/webhooks_subscriber_rules/_form.html.erb
@@ -0,0 +1,18 @@
+
+ <% unless @object.persisted? %>
+ <%= f.field_container :type, class: ['form-group'] do %>
+ <%= f.label :type, Spree.t(:type) %> *
+ <%= f.select :type, @webhooks_subscriber.available_rule_types.map { |type| [type.demodulize, type] }, {}, :class => "fullwidth select2" %>
+ <%= f.error_message_on :type %>
+ <% end %>
+ <% else %>
+ <%= f.field_container :type, class: ['form-group'] do %>
+ <%= f.label :type, Spree.t(:type) %> *
+ <%= f.text_field :type, value: @object.type.demodulize,class: 'form-control', disabled: true %>
+ <% end %>
+ <% end %>
+
+ <% if @object.persisted? %>
+ <%= render partial: @object.class.name.demodulize.underscore, locals: { rule: @object, f: f } %>
+ <% end %>
+
diff --git a/app/views/spree/admin/webhooks_subscriber_rules/_order_states.html.erb b/app/views/spree/admin/webhooks_subscriber_rules/_order_states.html.erb
new file mode 100644
index 000000000..5526e3658
--- /dev/null
+++ b/app/views/spree/admin/webhooks_subscriber_rules/_order_states.html.erb
@@ -0,0 +1,8 @@
+<%= f.field_container :preferred_states, class: ['form-group'] do %>
+ <%= f.label :preferred_states, Spree.t(:states) %>
+ <%= f.select :preferred_states, rule.class::DEFAULT_STATES,
+ { :include_hidden => false },
+ multiple: true,
+ class: 'fullwidth select2'
+ %>
+<% end %>
diff --git a/app/views/spree/admin/webhooks_subscriber_rules/_order_vendors.html.erb b/app/views/spree/admin/webhooks_subscriber_rules/_order_vendors.html.erb
new file mode 100644
index 000000000..3251b9bc0
--- /dev/null
+++ b/app/views/spree/admin/webhooks_subscriber_rules/_order_vendors.html.erb
@@ -0,0 +1,14 @@
+<%= f.field_container :preferred_vendors, class: ['form-group'] do %>
+ <%= f.label :preferred_vendors, Spree.t(:vendors) %>
+ <%= f.select :preferred_vendors,
+ options_from_collection_for_select(Spree::Vendor.all, :id, :name, rule.preferred_vendors),
+ { :include_hidden => false },
+ multiple: true,
+ class: 'fullwidth select2'
+ %>
+<% end %>
+
+<%= f.field_container :preferred_match_policy, class: ['form-group'] do %>
+ <%= f.label :preferred_match_policy, Spree.t(:match_policy) %>
+ <%= f.select :preferred_match_policy, @webhooks_subscriber.class::MATCH_POLICIES, {}, :class => "fullwidth select2" %>
+<% end %>
diff --git a/app/views/spree/admin/webhooks_subscriber_rules/edit.html.erb b/app/views/spree/admin/webhooks_subscriber_rules/edit.html.erb
new file mode 100644
index 000000000..20911f55c
--- /dev/null
+++ b/app/views/spree/admin/webhooks_subscriber_rules/edit.html.erb
@@ -0,0 +1,13 @@
+<% content_for :page_title do %>
+ <%= link_to Spree.t(:webhooks_subscriber_rules), collection_url %> /
+ <%= Spree.t(:edit) %>
+<% end %>
+
+<%= render partial: 'spree/admin/shared/error_messages', locals: { target: @object } %>
+
+<%= form_with model: @object, url: { action: 'update' } do |f| %>
+
+<% end %>
diff --git a/app/views/spree/admin/webhooks_subscriber_rules/new.html.erb b/app/views/spree/admin/webhooks_subscriber_rules/new.html.erb
new file mode 100644
index 000000000..c9256a24c
--- /dev/null
+++ b/app/views/spree/admin/webhooks_subscriber_rules/new.html.erb
@@ -0,0 +1,13 @@
+<% content_for :page_title do %>
+ <%= link_to Spree.t(:webhooks_subscriber_rules), collection_url %> /
+ <%= Spree.t(:new) %>
+<% end %>
+
+<%= render partial: 'spree/admin/shared/error_messages', locals: { target: @object } %>
+
+<%= form_with model: @object, url: { action: 'create' } do |f| %>
+
+<% end %>
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 1a473b3dc..0aabc525d 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -281,6 +281,8 @@ en:
empty_info: 'No Device_token found'
user_identity_providers:
empty_info: 'No User identity providers found'
+ webhooks_subscriber_rules:
+ empty_info: 'No Webhooks Subscriber Rules found'
notification:
send_test_success: 'Test Send Successfully'
diff --git a/config/locales/km.yml b/config/locales/km.yml
index b2935fec3..bee307d10 100644
--- a/config/locales/km.yml
+++ b/config/locales/km.yml
@@ -269,6 +269,8 @@ km:
empty_info: 'រក Device_token មិនឃើញទេ'
user_identity_providers:
empty_info: 'រក User identity providers មិនឃើញទេ'
+ webhooks_subscriber_rules:
+ empty_info: 'រក Webhooks Subscriber Rules មិនឃើញទេ'
notification:
send_test_success: 'សាកផ្ញើរបានជោគជ័យ'
diff --git a/config/routes.rb b/config/routes.rb
index ce1bf0e0e..92f6b17af 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -99,6 +99,10 @@
put :fire_notification
end
end
+
+ resources :webhooks_subscribers do
+ resources :rules, controller: :webhooks_subscriber_rules
+ end
end
namespace :telegram do
diff --git a/db/migrate/20231030101758_create_spree_cm_commissioner_webhooks_subscriber_rules.rb b/db/migrate/20231030101758_create_spree_cm_commissioner_webhooks_subscriber_rules.rb
new file mode 100644
index 000000000..8f1f9bc60
--- /dev/null
+++ b/db/migrate/20231030101758_create_spree_cm_commissioner_webhooks_subscriber_rules.rb
@@ -0,0 +1,11 @@
+class CreateSpreeCmCommissionerWebhooksSubscriberRules < ActiveRecord::Migration[7.0]
+ def change
+ create_table :cm_webhooks_subscriber_rules, if_not_exists: true do |t|
+ t.references :subscriber
+ t.text :preferences
+ t.string :type
+
+ t.timestamps
+ end
+ end
+end
diff --git a/db/migrate/20231101081621_add_api_key_to_spree_webhooks_subscriber.rb b/db/migrate/20231101081621_add_api_key_to_spree_webhooks_subscriber.rb
new file mode 100644
index 000000000..8b2d8ff1e
--- /dev/null
+++ b/db/migrate/20231101081621_add_api_key_to_spree_webhooks_subscriber.rb
@@ -0,0 +1,5 @@
+class AddApiKeyToSpreeWebhooksSubscriber < ActiveRecord::Migration[7.0]
+ def change
+ add_column :spree_webhooks_subscribers, :api_key, :string, if_not_exists: true
+ end
+end
diff --git a/db/migrate/20231101171456_add_match_policy_to_spree_webhooks_subscriber.rb b/db/migrate/20231101171456_add_match_policy_to_spree_webhooks_subscriber.rb
new file mode 100644
index 000000000..772b996e8
--- /dev/null
+++ b/db/migrate/20231101171456_add_match_policy_to_spree_webhooks_subscriber.rb
@@ -0,0 +1,5 @@
+class AddMatchPolicyToSpreeWebhooksSubscriber < ActiveRecord::Migration[7.0]
+ def change
+ add_column :spree_webhooks_subscribers, :match_policy, :integer, default: 0, null: false, if_not_exists: true
+ end
+end
diff --git a/db/migrate/20231102073606_add_name_to_spree_webhooks_subscriber.rb b/db/migrate/20231102073606_add_name_to_spree_webhooks_subscriber.rb
new file mode 100644
index 000000000..9575f2cd0
--- /dev/null
+++ b/db/migrate/20231102073606_add_name_to_spree_webhooks_subscriber.rb
@@ -0,0 +1,5 @@
+class AddNameToSpreeWebhooksSubscriber < ActiveRecord::Migration[7.0]
+ def change
+ add_column :spree_webhooks_subscribers, :name, :string, if_not_exists: true
+ end
+end
diff --git a/lib/spree_cm_commissioner/test_helper/factories/webhook_subscriber_factory.rb b/lib/spree_cm_commissioner/test_helper/factories/webhook_subscriber_factory.rb
new file mode 100644
index 000000000..a9c1ed497
--- /dev/null
+++ b/lib/spree_cm_commissioner/test_helper/factories/webhook_subscriber_factory.rb
@@ -0,0 +1,13 @@
+FactoryBot.define do
+ factory :cm_webhook_subscriber, class: Spree::Webhooks::Subscriber do
+ sequence(:url) { |n| "https://www.url#{n}.com/" }
+
+ trait :active do
+ active { true }
+ end
+
+ trait :inactive do
+ active { false }
+ end
+ end
+end
diff --git a/lib/spree_cm_commissioner/test_helper/factories/webhook_subscriber_rule_factory.rb b/lib/spree_cm_commissioner/test_helper/factories/webhook_subscriber_rule_factory.rb
new file mode 100644
index 000000000..88995c495
--- /dev/null
+++ b/lib/spree_cm_commissioner/test_helper/factories/webhook_subscriber_rule_factory.rb
@@ -0,0 +1,27 @@
+FactoryBot.define do
+ factory :cm_webhook_subscriber_rule, class: SpreeCmCommissioner::Webhooks::SubscriberRule do
+ subscriber { create(:cm_webhook_subscriber) }
+
+ factory :cm_webhook_subscriber_order_vendors_rule, class: SpreeCmCommissioner::Webhooks::Rules::OrderVendors do
+ transient do
+ vendors { [create(:vendor)]}
+ match_policy { 'all' }
+ end
+
+ after(:build) do |rule, evaluator|
+ rule.preferred_vendors = evaluator.vendors.pluck(:id).map(&:to_s)
+ rule.preferred_match_policy = evaluator.match_policy
+ end
+ end
+
+ factory :cm_webhook_subscriber_order_states_rule, class: SpreeCmCommissioner::Webhooks::Rules::OrderStates do
+ transient do
+ states { SpreeCmCommissioner::Webhooks::Rules::OrderStates::DEFAULT_STATES }
+ end
+
+ after(:build) do |rule, evaluator|
+ rule.preferred_states = evaluator.states
+ end
+ end
+ end
+end
diff --git a/spec/models/spree/webhooks/subscriber_spec.rb b/spec/models/spree/webhooks/subscriber_spec.rb
new file mode 100644
index 000000000..c850e4216
--- /dev/null
+++ b/spec/models/spree/webhooks/subscriber_spec.rb
@@ -0,0 +1,93 @@
+require 'spec_helper'
+
+RSpec.describe Spree::Webhooks::Subscriber, type: :model do
+ describe '#matches?' do
+ let(:order) { build(:order) }
+ let(:rule1) { build(:cm_webhook_subscriber_order_vendors_rule) }
+ let(:rule2) { build(:cm_webhook_subscriber_order_states_rule) }
+ let(:subscriber) { build(:cm_webhook_subscriber, rules: [rule1, rule2]) }
+
+ it 'reject when event is not suppported' do
+ event = 'order.fake-event'
+
+ matched = subscriber.matches?(event, order.send(:webhook_payload_body), **order.send(:webhooks_request_options))
+
+ expect(matched).to eq false
+ expect(rule1.supported?(event)).to eq false
+ expect(rule2.supported?(event)).to eq false
+ end
+
+ it 'matched when subscriber has no rules' do
+ subscriber.rules = []
+
+ matched = subscriber.matches?('order.placed', order.send(:webhook_payload_body), **order.send(:webhooks_request_options))
+
+ expect(matched).to eq true
+ expect(subscriber.rules).to eq []
+ end
+
+ context 'any' do
+ it 'matched when one of rules are matched' do
+ allow(subscriber).to receive(:match_policy).and_return('any')
+
+ allow(rule1).to receive(:matches?).and_return(true)
+ allow(rule2).to receive(:matches?).and_return(false)
+
+ matched = subscriber.matches?('order.placed', order.send(:webhook_payload_body), **order.send(:webhooks_request_options))
+
+ expect(matched).to be true
+ expect(subscriber.match_any?).to be true
+ end
+
+ it 'matched when all rules are matched' do
+ allow(subscriber).to receive(:match_policy).and_return('any')
+
+ allow(rule1).to receive(:matches?).and_return(true)
+ allow(rule2).to receive(:matches?).and_return(true)
+
+ matched = subscriber.matches?('order.placed', order.send(:webhook_payload_body), **order.send(:webhooks_request_options))
+
+ expect(matched).to be true
+ expect(subscriber.match_any?).to be true
+ end
+
+ it 'reject when no rules are matched' do
+ allow(subscriber).to receive(:match_policy).and_return('any')
+
+ allow(rule1).to receive(:matches?).and_return(false)
+ allow(rule2).to receive(:matches?).and_return(false)
+
+ matched = subscriber.matches?('order.placed', order.send(:webhook_payload_body), **order.send(:webhooks_request_options))
+
+ expect(matched).to be false
+ expect(subscriber.match_any?).to be true
+ end
+ end
+
+ context 'all' do
+ it 'matched when all rules are matched' do
+ allow(subscriber).to receive(:match_policy).and_return('all')
+
+ allow(rule1).to receive(:matches?).and_return(true)
+ allow(rule2).to receive(:matches?).and_return(true)
+
+ matched = subscriber.matches?('order.placed', order.send(:webhook_payload_body), **order.send(:webhooks_request_options))
+
+ expect(matched).to be true
+ expect(subscriber.match_all?).to be true
+ end
+
+ it 'reject when one of rules is not matched' do
+ allow(subscriber).to receive(:match_policy).and_return('all')
+
+ allow(rule1).to receive(:matches?).and_return(true)
+ allow(rule2).to receive(:matches?).and_return(false)
+
+ matched = subscriber.matches?('order.placed', order.send(:webhook_payload_body), **order.send(:webhooks_request_options))
+
+ expect(matched).to be false
+ expect(subscriber.match_all?).to be true
+ end
+ end
+ end
+end
diff --git a/spec/models/spree_cm_commissioner/webhooks/rules/order_states_spec.rb b/spec/models/spree_cm_commissioner/webhooks/rules/order_states_spec.rb
new file mode 100644
index 000000000..fc29df0d8
--- /dev/null
+++ b/spec/models/spree_cm_commissioner/webhooks/rules/order_states_spec.rb
@@ -0,0 +1,21 @@
+require 'spec_helper'
+
+RSpec.describe SpreeCmCommissioner::Webhooks::Rules::OrderStates, type: :model do
+ describe '#matches?' do
+ let(:order) { create(:order, state: :complete) }
+
+ it 'matched when order state is in rule states' do
+ rule = build(:cm_webhook_subscriber_order_states_rule, states: ['complete'])
+ matched = rule.matches?('order.placed', order.send(:webhook_payload_body), **order.send(:webhooks_request_options))
+
+ expect(matched).to be true
+ end
+
+ it 'reject when order state is no in rule states' do
+ rule = build(:cm_webhook_subscriber_order_states_rule, states: ['canceled'])
+ matched = rule.matches?('order.placed', order.send(:webhook_payload_body), **order.send(:webhooks_request_options))
+
+ expect(matched).to be false
+ end
+ end
+end
diff --git a/spec/models/spree_cm_commissioner/webhooks/rules/order_vendors_spec.rb b/spec/models/spree_cm_commissioner/webhooks/rules/order_vendors_spec.rb
new file mode 100644
index 000000000..a7331533d
--- /dev/null
+++ b/spec/models/spree_cm_commissioner/webhooks/rules/order_vendors_spec.rb
@@ -0,0 +1,48 @@
+require 'spec_helper'
+
+RSpec.describe SpreeCmCommissioner::Webhooks::Rules::OrderVendors, type: :model do
+ describe '#matches?' do
+ let(:vendor1) { create(:vendor) }
+ let(:vendor2) { create(:vendor) }
+ let(:vendor3) { create(:vendor) }
+
+ let(:order) { create(:order_with_line_items, line_items_count: 2) }
+
+ before do
+ allow(order.line_items[0]).to receive(:vendor_id).and_return(vendor1.id)
+ allow(order.line_items[1]).to receive(:vendor_id).and_return(vendor2.id)
+ end
+
+ context 'all' do
+ it 'matched when all preferred vendors are in order' do
+ rule = build(:cm_webhook_subscriber_order_vendors_rule, vendors: [vendor1, vendor2], match_policy: 'all')
+ matched = rule.matches?('order.placed', order.send(:webhook_payload_body), **order.send(:webhooks_request_options))
+
+ expect(matched).to be true
+ end
+
+ it 'reject when one of preferred vendors is not in order' do
+ rule = build(:cm_webhook_subscriber_order_vendors_rule, vendors: [vendor1, vendor2, vendor3], match_policy: 'all')
+ matched = rule.matches?('order.placed', order.send(:webhook_payload_body), **order.send(:webhooks_request_options))
+
+ expect(matched).to be false
+ end
+ end
+
+ context 'any' do
+ it 'matched when one preferred vendors is in order' do
+ rule = build(:cm_webhook_subscriber_order_vendors_rule, vendors: [vendor1], match_policy: 'all')
+ matched = rule.matches?('order.placed', order.send(:webhook_payload_body), **order.send(:webhooks_request_options))
+
+ expect(matched).to be true
+ end
+
+ it 'reject when none of preferred venodrs is in order' do
+ rule = build(:cm_webhook_subscriber_order_vendors_rule, vendors: [vendor3], match_policy: 'all')
+ matched = rule.matches?('order.placed', order.send(:webhook_payload_body), **order.send(:webhooks_request_options))
+
+ expect(matched).to be false
+ end
+ end
+ end
+end
diff --git a/spec/models/spree_cm_commissioner/webhooks/subscriber_rule_spec.rb b/spec/models/spree_cm_commissioner/webhooks/subscriber_rule_spec.rb
new file mode 100644
index 000000000..d75ff73ab
--- /dev/null
+++ b/spec/models/spree_cm_commissioner/webhooks/subscriber_rule_spec.rb
@@ -0,0 +1,5 @@
+require 'spec_helper'
+
+RSpec.describe SpreeCmCommissioner::Webhooks::SubscriberRule, type: :model do
+ pending "add some examples to (or delete) #{__FILE__}"
+end