Skip to content

Commit

Permalink
close #766 add webhook subscriber rule
Browse files Browse the repository at this point in the history
  • Loading branch information
theachoem committed Nov 1, 2023
1 parent 9939c5e commit d4ce19d
Show file tree
Hide file tree
Showing 25 changed files with 454 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
5 changes: 5 additions & 0 deletions app/models/spree_cm_commissioner/line_item_decorator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down
7 changes: 7 additions & 0 deletions app/models/spree_cm_commissioner/webhooks.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module SpreeCmCommissioner
module Webhooks
def self.table_name_prefix
'cm_webhooks_'
end
end
end
43 changes: 43 additions & 0 deletions app/models/spree_cm_commissioner/webhooks/rules/order_states.rb
Original file line number Diff line number Diff line change
@@ -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.contains(state)
end
end
end
end
end
41 changes: 41 additions & 0 deletions app/models/spree_cm_commissioner/webhooks/rules/order_vendors.rb
Original file line number Diff line number Diff line change
@@ -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
13 changes: 13 additions & 0 deletions app/models/spree_cm_commissioner/webhooks/subscriber_decorator.rb
Original file line number Diff line number Diff line change
@@ -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
11 changes: 11 additions & 0 deletions app/models/spree_cm_commissioner/webhooks/subscriber_rule.rb
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<!-- insert_top "[data-hook='admin_webhooks_subscriber_form_fields']" -->

<%= 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 %>
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<!-- insert_bottom "[data-hook='admin_webhooks_subscriber_form_fields']" -->

<div class="mb-4">
<div class="mb-2 d-flex justify-content-between align-items-center">
<div>Rules</div>
<%= button_link_to Spree.t(:new), new_admin_webhooks_subscriber_rule_path(@webhooks_subscriber), class: "btn-light", icon: 'add.svg' %>
</div>

<% if @webhooks_subscriber.rules.any? %>
<table class="table border" id="admin_webhooks_subscriber_rules">
<thead class="text-muted">
<tr data-hook="admin_webhooks_subscriber_rules_index_headers">
<th><%= Spree.t(:id) %></th>
<th><%= Spree.t(:rule) %></th>
<th><%= Spree.t(:preferences) %></th>
<th><%= Spree.t(:supported_events) %></th>
<th><%= Spree.t(:created_at) %></th>
<th><%= Spree.t(:updated_at) %></th>
</tr>
</thead>
<tbody>
<% @webhooks_subscriber.rules.each do |rule| %>
<tr id="<%= spree_dom_id rule %>" data-hook="admin_webhooks_subscriber_rules_index_rows">
<td><%= rule.id %></td>
<td><%= rule.class.name.demodulize %></td>
<td><%= rule.preferences %></td>
<td><%= rule.class::SUPPORTED_EVENTS.to_sentence %></td>
<td><%= rule.created_at %></td>
<td><%= rule.updated_at %></td>
<td data-hook="admin_webhooks_subscriber_rules_index_row_actions" class="actions">
<span class="d-flex justify-content-end">
<%= 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) %>
</span>
</td>
</tr>
<% end %>
</tbody>
</table>
<% else %>
<small class="form-text text-muted">
<%= raw I18n.t('webhooks_subscriber_rules.empty_info') %>
</small>
<% end %>
</div>

<%= f.field_container :match_policy, class: ['form-group'] do %>
<%= f.label :match_policy, Spree.t(:match_policy) %></span>
<%= f.select :match_policy, @object.class::MATCH_POLICIES, {}, :class => "fullwidth select2" %>
<%= f.error_message_on :match_policy %>
<% end %>
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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).filter 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
18 changes: 18 additions & 0 deletions app/views/spree/admin/webhooks_subscriber_rules/_form.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<div data-hook="admin_webhooks_subscriber_rules_form_fields">
<% unless @object.persisted? %>
<%= f.field_container :type, class: ['form-group'] do %>
<%= f.label :type, Spree.t(:type) %> <span class="required">*</span>
<%= 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) %> <span class="required">*</span>
<%= 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 %>
</div>
Loading

0 comments on commit d4ce19d

Please sign in to comment.