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..ac5f224e7 100644
--- a/app/models/spree_cm_commissioner/line_item_decorator.rb
+++ b/app/models/spree_cm_commissioner/line_item_decorator.rb
@@ -15,6 +15,11 @@ 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
+ 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..147eb0e3d
--- /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.includes?(event)
+ end
+
+ def matches?(_event, webhook_payload_body, _options = {})
+ payload_body = JSON.parse(webhook_payload_body)
+
+ state = payload_body['data']['attributes']['state']
+
+ preferred_states.includes?(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..84bf6e116
--- /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.includes?(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|
+ return include['id'] if include['type'] == 'vendor'
+ end
+
+ case preferred_match_policy
+ when 'any'
+ preferred_vendors.any? { |vendor_id| vendor_ids.includes?(vendor_id) }
+ when 'all'
+ preferred_vendors.all? { |vendor_id| vendor_ids.includes?(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/rules.html.erb.deface b/app/overrides/spree/admin/webhooks_subscribers/_form/rules.html.erb.deface
new file mode 100644
index 000000000..fe9d4eacd
--- /dev/null
+++ b/app/overrides/spree/admin/webhooks_subscribers/_form/rules.html.erb.deface
@@ -0,0 +1,51 @@
+
+
+
+
+
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 %>
+
+
+ <% else %>
+
+ <%= raw I18n.t('webhooks_subscriber_rules.empty_info') %>
+
+ <% end %>
+
+
+<%= f.field_container :match_policy, class: ['form-group'] 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 %>
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/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