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? %> + + + + + + + + + + + + + <% @webhooks_subscriber.rules.each do |rule| %> + + + + + + + + + + <% end %> + +
<%= Spree.t(:id) %><%= Spree.t(:rule) %><%= Spree.t(:preferences) %><%= Spree.t(:supported_events) %><%= Spree.t(:created_at) %><%= Spree.t(:updated_at) %>
<%= 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) %> + +
+ + <%= 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 @@ + + +
+ + + + + + <% if defined?(Spree::Vendor) && can?(:manage, Spree::Vendor) && !current_spree_vendor %> + + <% end %> + + + + + + + + + <% @webhooks_subscribers.each do |webhooks_subscriber| %> + + + + <% if defined?(Spree::Vendor) && can?(:manage, Spree::Vendor) && !current_spree_vendor %> + + <% end %> + + + + + + + <% end %> + +
<%= Spree.t('admin.url') %><%= Spree.t('name') %><%= Spree.t(:vendor) %><%= 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_subscriber.url %><%= webhooks_subscriber.name %> + <%= link_to webhooks_subscriber.vendor.name, spree.admin_vendor_path(webhooks_subscriber.vendor) if webhooks_subscriber.vendor.present? %> + <%= 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 %> + +
+
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| %> +
+ <%= render partial: 'form', locals: { f: f } %> + <%= render partial: 'spree/admin/shared/edit_resource_links' %> +
+<% 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| %> +
+ <%= render partial: 'form', locals: { f: f } %> + <%= render partial: 'spree/admin/shared/new_resource_links' %> +
+<% 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