mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-01-11 18:26:50 +00:00
Compare commits
58 Commits
7c2ae3bb4d
...
8f592f8004
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f592f8004 | ||
|
|
80bd6defcb | ||
|
|
bd367cb154 | ||
|
|
d0f48687e2 | ||
|
|
25063d2c4d | ||
|
|
bad04b70a9 | ||
|
|
5479572a08 | ||
|
|
06bfd07fec | ||
|
|
e98cf78b4c | ||
|
|
13229cc0c1 | ||
|
|
3173c79e8f | ||
|
|
ca14d557c1 | ||
|
|
59a3a5bd92 | ||
|
|
a226088f5c | ||
|
|
c65fcc1072 | ||
|
|
3bb68ec07e | ||
|
|
2c97638aa1 | ||
|
|
ceee9671d9 | ||
|
|
6b494be7ff | ||
|
|
a6855e6bc1 | ||
|
|
7ca43eb4a1 | ||
|
|
74b5ac559f | ||
|
|
07c236497c | ||
|
|
caf2ff9bb4 | ||
|
|
1b2a17d7e4 | ||
|
|
ce46115139 | ||
|
|
9fd2ff7620 | ||
|
|
584b976dff | ||
|
|
4073238654 | ||
|
|
d7505bcef4 | ||
|
|
f6a7225c47 | ||
|
|
377f33b64f | ||
|
|
73b27f14ab | ||
|
|
0b497fbb77 | ||
|
|
5e4df41ec8 | ||
|
|
72085be896 | ||
|
|
e0bc8f9cdc | ||
|
|
efcb442a80 | ||
|
|
a38023475c | ||
|
|
4a6ba29b99 | ||
|
|
7f961d90c2 | ||
|
|
0ac4021729 | ||
|
|
ac662de789 | ||
|
|
23c57cb354 | ||
|
|
d6ef56af6e | ||
|
|
059e36318e | ||
|
|
c01cca33c7 | ||
|
|
631306cfb3 | ||
|
|
f4d59305d7 | ||
|
|
c526e72539 | ||
|
|
e217a6fca8 | ||
|
|
6aa7ef3c21 | ||
|
|
bf0e5c0d44 | ||
|
|
6bd2f5af8d | ||
|
|
7bf54088a6 | ||
|
|
4792040240 | ||
|
|
dc631026d4 | ||
|
|
c05532c166 |
10
.github/PULL_REQUEST_TEMPLATE.md
vendored
10
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,4 +1,4 @@
|
||||
#### What? Why?
|
||||
## What? Why?
|
||||
|
||||
- Closes # <!-- Insert issue number here. -->
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
|
||||
|
||||
#### What should we test?
|
||||
## What should we test?
|
||||
<!-- List which features should be tested and how.
|
||||
This can be similar to the Steps to Reproduce in the issue.
|
||||
Also think of other parts of the app which could be affected
|
||||
@@ -16,7 +16,7 @@
|
||||
- Visit ... page.
|
||||
-
|
||||
|
||||
#### Release notes
|
||||
## Release notes
|
||||
|
||||
<!-- Please select one for your PR and delete the other. -->
|
||||
|
||||
@@ -33,12 +33,12 @@ Changelog Category (reviewers may add a label for the release notes):
|
||||
The title of the pull request will be included in the release notes.
|
||||
|
||||
|
||||
#### Dependencies
|
||||
## Dependencies
|
||||
<!-- Does this PR depend on another one?
|
||||
Add the link or remove this section. -->
|
||||
|
||||
|
||||
|
||||
#### Documentation updates
|
||||
## Documentation updates
|
||||
<!-- Are there any wiki pages that need updating after merging this PR?
|
||||
List them here or remove this section. -->
|
||||
|
||||
55
Gemfile.lock
55
Gemfile.lock
@@ -104,11 +104,12 @@ GEM
|
||||
rails-html-sanitizer (~> 1.6)
|
||||
active_model_serializers (0.8.4)
|
||||
activemodel (>= 3.0)
|
||||
active_storage_validations (1.1.4)
|
||||
activejob (>= 5.2.0)
|
||||
activemodel (>= 5.2.0)
|
||||
activestorage (>= 5.2.0)
|
||||
activesupport (>= 5.2.0)
|
||||
active_storage_validations (3.0.3)
|
||||
activejob (>= 6.1.4)
|
||||
activemodel (>= 6.1.4)
|
||||
activestorage (>= 6.1.4)
|
||||
activesupport (>= 6.1.4)
|
||||
marcel (>= 1.0.3)
|
||||
activejob (7.1.6)
|
||||
activesupport (= 7.1.6)
|
||||
globalid (>= 0.3.6)
|
||||
@@ -158,8 +159,8 @@ GEM
|
||||
zeitwerk (>= 2.4, < 3.0)
|
||||
acts_as_list (1.0.4)
|
||||
activerecord (>= 4.2)
|
||||
addressable (2.8.7)
|
||||
public_suffix (>= 2.0.2, < 7.0)
|
||||
addressable (2.8.8)
|
||||
public_suffix (>= 2.0.2, < 8.0)
|
||||
aes_key_wrap (1.1.0)
|
||||
afm (0.2.2)
|
||||
angular-rails-templates (1.4.0)
|
||||
@@ -176,8 +177,8 @@ GEM
|
||||
ast (2.4.3)
|
||||
attr_required (1.0.2)
|
||||
aws-eventstream (1.4.0)
|
||||
aws-partitions (1.1191.0)
|
||||
aws-sdk-core (3.239.2)
|
||||
aws-partitions (1.1196.0)
|
||||
aws-sdk-core (3.240.0)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.992.0)
|
||||
aws-sigv4 (~> 1.9)
|
||||
@@ -188,7 +189,7 @@ GEM
|
||||
aws-sdk-kms (1.118.0)
|
||||
aws-sdk-core (~> 3, >= 3.239.1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.206.0)
|
||||
aws-sdk-s3 (1.208.0)
|
||||
aws-sdk-core (~> 3, >= 3.234.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
@@ -233,7 +234,7 @@ GEM
|
||||
marcel (~> 1.0)
|
||||
nokogiri (~> 1.10, >= 1.10.4)
|
||||
rubyzip (>= 1.3.0, < 3)
|
||||
cgi (0.5.0)
|
||||
cgi (0.5.1)
|
||||
childprocess (5.0.0)
|
||||
choice (0.2.0)
|
||||
chronic (0.10.2)
|
||||
@@ -248,10 +249,10 @@ GEM
|
||||
combine_pdf (1.0.31)
|
||||
matrix
|
||||
ruby-rc4 (>= 0.1.5)
|
||||
concurrent-ruby (1.3.5)
|
||||
connection_pool (2.5.5)
|
||||
concurrent-ruby (1.3.6)
|
||||
connection_pool (3.0.2)
|
||||
cookiejar (0.3.4)
|
||||
crack (1.0.0)
|
||||
crack (1.0.1)
|
||||
bigdecimal
|
||||
rexml
|
||||
crass (1.0.6)
|
||||
@@ -289,7 +290,7 @@ GEM
|
||||
diff-lcs (1.6.2)
|
||||
digest (3.2.1)
|
||||
docile (1.4.1)
|
||||
dotenv (3.1.8)
|
||||
dotenv (3.2.0)
|
||||
drb (2.2.3)
|
||||
em-http-request (1.1.7)
|
||||
addressable (>= 2.3.4)
|
||||
@@ -379,7 +380,7 @@ GEM
|
||||
temple (>= 0.8.2)
|
||||
thor
|
||||
tilt
|
||||
haml_lint (0.67.0)
|
||||
haml_lint (0.68.0)
|
||||
haml (>= 5.0)
|
||||
parallel (~> 1.10)
|
||||
rainbow
|
||||
@@ -468,7 +469,7 @@ GEM
|
||||
rb-fsevent (~> 0.10, >= 0.10.3)
|
||||
rb-inotify (~> 0.9, >= 0.9.10)
|
||||
logger (1.7.0)
|
||||
loofah (2.24.1)
|
||||
loofah (2.25.0)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.12.0)
|
||||
mail (2.9.0)
|
||||
@@ -491,7 +492,8 @@ GEM
|
||||
logger
|
||||
mini_mime (1.1.5)
|
||||
mini_portile2 (2.8.6)
|
||||
minitest (5.26.2)
|
||||
minitest (6.0.1)
|
||||
prism (~> 1.5)
|
||||
monetize (1.13.0)
|
||||
money (~> 6.12)
|
||||
money (6.16.0)
|
||||
@@ -513,7 +515,7 @@ GEM
|
||||
net-protocol
|
||||
newrelic_rpm (9.24.0)
|
||||
nio4r (2.7.5)
|
||||
nokogiri (1.18.10)
|
||||
nokogiri (1.19.0)
|
||||
mini_portile2 (~> 2.8.2)
|
||||
racc (~> 1.4)
|
||||
nokogiri-html5-inference (0.3.0)
|
||||
@@ -576,7 +578,7 @@ GEM
|
||||
pp (0.6.3)
|
||||
prettyprint
|
||||
prettyprint (0.2.0)
|
||||
prism (1.6.0)
|
||||
prism (1.7.0)
|
||||
private_address_check (0.5.0)
|
||||
pry (0.15.2)
|
||||
coderay (~> 1.1)
|
||||
@@ -584,7 +586,7 @@ GEM
|
||||
psych (5.2.6)
|
||||
date
|
||||
stringio
|
||||
public_suffix (6.0.2)
|
||||
public_suffix (7.0.0)
|
||||
puffing-billy (4.0.2)
|
||||
addressable (~> 2.5)
|
||||
em-http-request (~> 1.1, >= 1.1.0)
|
||||
@@ -875,13 +877,13 @@ GEM
|
||||
faraday (~> 2.0)
|
||||
faraday-follow_redirects
|
||||
sysexits (1.2.0)
|
||||
temple (0.8.2)
|
||||
temple (0.10.4)
|
||||
terminal-table (4.0.0)
|
||||
unicode-display_width (>= 1.1.1, < 4)
|
||||
thor (1.4.0)
|
||||
thread-local (1.1.0)
|
||||
tilt (2.6.1)
|
||||
timeout (0.4.4)
|
||||
timeout (0.6.0)
|
||||
tsort (0.2.0)
|
||||
ttfunk (1.8.0)
|
||||
bigdecimal (~> 3.1)
|
||||
@@ -902,7 +904,7 @@ GEM
|
||||
simplecov_json_formatter
|
||||
unicode-display_width (3.2.0)
|
||||
unicode-emoji (~> 4.1)
|
||||
unicode-emoji (4.1.0)
|
||||
unicode-emoji (4.2.0)
|
||||
uniform_notifier (1.17.0)
|
||||
uri (1.1.1)
|
||||
valid_email2 (5.2.3)
|
||||
@@ -913,7 +915,8 @@ GEM
|
||||
public_suffix
|
||||
validates_lengths_from_database (0.8.0)
|
||||
activerecord (>= 4)
|
||||
vcr (6.2.0)
|
||||
vcr (6.3.1)
|
||||
base64
|
||||
view_component (4.1.1)
|
||||
actionview (>= 7.1.0, < 8.2)
|
||||
activesupport (>= 7.1.0, < 8.2)
|
||||
@@ -935,7 +938,7 @@ GEM
|
||||
activesupport
|
||||
faraday (~> 2.0)
|
||||
faraday-follow_redirects
|
||||
webmock (3.25.1)
|
||||
webmock (3.26.1)
|
||||
addressable (>= 2.8.0)
|
||||
crack (>= 0.3.2)
|
||||
hashdiff (>= 0.4.0, < 2.0.0)
|
||||
|
||||
@@ -38,16 +38,13 @@ angular.module("admin.indexUtils").directive "objForUpdate", (switchClass, pendi
|
||||
# To ensure the customer is still updated, we check on the $destroy event to see if
|
||||
# the attribute has changed, if so we queue up the change.
|
||||
scope.$on '$destroy', (value) ->
|
||||
# No update
|
||||
return if scope.object()[scope.attr] is scope.savedValue
|
||||
currentValue = scope.object()[scope.attr] || ""
|
||||
|
||||
# For some reason the code attribute is removed from the object when cleared, so we add
|
||||
# an emptyvalue so it gets updated properly
|
||||
if scope.attr is "code" and scope.object()[scope.attr] is undefined
|
||||
scope.object()["code"] = ""
|
||||
# No update
|
||||
return if currentValue is scope.savedValue
|
||||
|
||||
# Queuing up change
|
||||
addPendingChange(scope.attr, scope.object()[scope.attr])
|
||||
addPendingChange(scope.attr, currentValue)
|
||||
|
||||
# private
|
||||
|
||||
|
||||
16
app/components/webhook_endpoint_form_component.rb
Normal file
16
app/components/webhook_endpoint_form_component.rb
Normal file
@@ -0,0 +1,16 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class WebhookEndpointFormComponent < ViewComponent::Base
|
||||
def initialize(webhooks:, webhook_type:)
|
||||
@webhooks = webhooks
|
||||
@webhook_type = webhook_type
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :webhooks, :webhook_type
|
||||
|
||||
def is_webhook_payment_status?
|
||||
webhook_type == "payment_status_changed"
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,27 @@
|
||||
-# Create new endpoints
|
||||
- if webhooks.empty? # Only one allowed for now.
|
||||
%tr
|
||||
%td= t("components.webhook_endpoint_form.event_types.#{webhook_type}")
|
||||
%td
|
||||
= form_with(url: helpers.account_webhook_endpoints_path, id: "#{webhook_type}_webhook_endpoint") do |f|
|
||||
= f.url_field :'webhook_endpoint[url]', id: "#{webhook_type}_webhook_endpoint_url", placeholder: t('components.webhook_endpoint_form.url.create_placeholder'), required: true, size: 64
|
||||
= f.hidden_field :'webhook_endpoint[webhook_type]', id: "#{webhook_type}_webhook_endpoint_webhook_type", value: webhook_type
|
||||
%td.actions
|
||||
= button_tag t(:create), class: 'button primary tiny no-margin', form: "#{webhook_type}_webhook_endpoint"
|
||||
|
||||
-# Existing endpoints
|
||||
- webhooks.each do |webhook_endpoint|
|
||||
%tr
|
||||
%td= t("components.webhook_endpoint_form.event_types.#{webhook_type}")
|
||||
%td= webhook_endpoint.url
|
||||
%td.actions.endpoints-actions
|
||||
- if webhook_endpoint.persisted?
|
||||
= button_to helpers.account_webhook_endpoint_path(webhook_endpoint), method: :delete,
|
||||
class: "tiny alert no-margin",
|
||||
data: { confirm: I18n.t(:are_you_sure) } do
|
||||
= I18n.t(:delete)
|
||||
|
||||
- if is_webhook_payment_status?
|
||||
= form_tag helpers.webhook_endpoint_test_account_path(webhook_endpoint), class: "button_to", 'data-turbo': true do
|
||||
= button_tag type: "submit", class: "tiny alert no-margin", data: { confirm: I18n.t(:are_you_sure) } do
|
||||
= I18n.t("components.webhook_endpoint_form.test_endpoint")
|
||||
@@ -162,6 +162,18 @@ module Admin
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
if @object.destroy
|
||||
flash.now[:success] = flash_message_for(@object, :successfully_removed)
|
||||
else
|
||||
flash.now[:error] = @object.errors.full_messages.to_sentence
|
||||
end
|
||||
|
||||
respond_to do |format|
|
||||
format.turbo_stream { render :destroy, status: :ok }
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def delete_custom_tab
|
||||
|
||||
@@ -61,7 +61,7 @@ module Admin
|
||||
|
||||
def destroy
|
||||
if @object.destroy
|
||||
flash[:success] = flash_message_for(@object, :successfully_removed)
|
||||
flash[:success] = Spree.t(:successfully_removed)
|
||||
respond_with(@object) do |format|
|
||||
format.html { redirect_to collection_url }
|
||||
format.js { render partial: "spree/admin/shared/destroy" }
|
||||
@@ -76,7 +76,7 @@ module Admin
|
||||
protected
|
||||
|
||||
def resource_not_found
|
||||
flash[:error] = flash_message_for(model_class.new, :not_found)
|
||||
flash[:error] = Spree.t(:not_found)
|
||||
redirect_to collection_url
|
||||
end
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ module Admin
|
||||
|
||||
status = :ok
|
||||
if @rule.destroy
|
||||
flash[:success] = Spree.t(:successfully_removed, resource: "Tag Rule")
|
||||
flash[:success] = Spree.t(:successfully_removed, resource: Spree.t(:tag_rule))
|
||||
else
|
||||
flash.now[:error] = t(".destroy_error")
|
||||
status = :internal_server_error
|
||||
|
||||
@@ -14,7 +14,7 @@ module Admin
|
||||
)
|
||||
|
||||
if @voucher.save
|
||||
flash[:success] = I18n.t(:successfully_created, resource: "Voucher")
|
||||
flash[:success] = I18n.t(:successfully_created, resource: Spree.t(:voucher))
|
||||
redirect_to edit_admin_enterprise_path(@enterprise, anchor: :vouchers_panel)
|
||||
else
|
||||
render_error
|
||||
|
||||
@@ -68,7 +68,7 @@ module Spree
|
||||
destroy_before
|
||||
|
||||
if @object.destroy
|
||||
flash[:success] = flash_message_for(@object, :successfully_removed)
|
||||
flash[:success] = Spree.t(:successfully_removed)
|
||||
end
|
||||
|
||||
redirect_to location_after_save
|
||||
|
||||
@@ -16,7 +16,7 @@ module Spree
|
||||
@url_filters = ::ProductFilters.new.extract(request.query_parameters)
|
||||
|
||||
if @object.destroy
|
||||
flash[:success] = flash_message_for(@object, :successfully_removed)
|
||||
flash[:success] = Spree.t(:successfully_removed)
|
||||
end
|
||||
# if destroy fails it won't show any errors to the user
|
||||
redirect_to spree.admin_product_product_properties_url(params[:product_id], @url_filters)
|
||||
|
||||
@@ -36,7 +36,7 @@ module Spree
|
||||
end
|
||||
|
||||
@object.touch :deleted_at
|
||||
flash[:success] = flash_message_for(@object, :successfully_removed)
|
||||
flash[:success] = Spree.t(:successfully_removed)
|
||||
|
||||
respond_with(@object) do |format|
|
||||
format.html { redirect_to collection_url }
|
||||
|
||||
@@ -5,7 +5,7 @@ module Spree
|
||||
class TaxCategoriesController < ::Admin::ResourceController
|
||||
def destroy
|
||||
if @object.destroy
|
||||
flash[:success] = flash_message_for(@object, :successfully_removed)
|
||||
flash[:success] = Spree.t(:successfully_removed)
|
||||
respond_with(@object) do |format|
|
||||
format.html { redirect_to collection_url }
|
||||
format.js { render partial: "spree/admin/shared/destroy" }
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class WebhookEndpointsController < BaseController
|
||||
before_action :load_resource, only: :destroy
|
||||
before_action :load_resource, only: [:destroy, :test]
|
||||
|
||||
def create
|
||||
webhook_endpoint = spree_current_user.webhook_endpoints.new(webhook_endpoint_params)
|
||||
@@ -25,12 +25,30 @@ class WebhookEndpointsController < BaseController
|
||||
redirect_to redirect_path
|
||||
end
|
||||
|
||||
def test
|
||||
at = Time.zone.now
|
||||
test_payload = Payments::WebhookPayload.test_data.to_hash
|
||||
|
||||
WebhookDeliveryJob.perform_later(@webhook_endpoint.url, "payment.completed", test_payload, at:)
|
||||
|
||||
flash[:success] = t(".success")
|
||||
respond_with do |format|
|
||||
format.turbo_stream do
|
||||
render turbo_stream: turbo_stream.update(
|
||||
:flashes, partial: "shared/flashes", locals: { flashes: flash }
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_resource
|
||||
@webhook_endpoint = spree_current_user.webhook_endpoints.find(params[:id])
|
||||
end
|
||||
|
||||
def webhook_endpoint_params
|
||||
params.require(:webhook_endpoint).permit(:url)
|
||||
params.require(:webhook_endpoint).permit(:url, :webhook_type)
|
||||
end
|
||||
|
||||
def redirect_path
|
||||
|
||||
@@ -63,8 +63,11 @@ module EnterprisesHelper
|
||||
url = object_url(enterprise)
|
||||
name = t(:delete)
|
||||
options = {}
|
||||
options[:class] = "delete-resource"
|
||||
options[:data] = { action: 'remove', confirm: enterprise_confirm_delete_message(enterprise) }
|
||||
options[:data] = {
|
||||
turbo: true,
|
||||
'turbo-method': 'delete',
|
||||
'turbo-confirm': enterprise_confirm_delete_message(enterprise)
|
||||
}
|
||||
link_to_with_icon 'icon-trash', name, url, options
|
||||
end
|
||||
|
||||
|
||||
@@ -50,11 +50,11 @@ class Enterprise < ApplicationRecord
|
||||
has_many :distributed_orders, class_name: 'Spree::Order',
|
||||
foreign_key: 'distributor_id',
|
||||
inverse_of: :distributor,
|
||||
dependent: :restrict_with_exception
|
||||
dependent: :restrict_with_error
|
||||
|
||||
belongs_to :address, class_name: 'Spree::Address'
|
||||
belongs_to :business_address, optional: true, class_name: 'Spree::Address', dependent: :destroy
|
||||
has_many :enterprise_fees, dependent: :restrict_with_exception
|
||||
has_many :enterprise_fees, dependent: :restrict_with_error
|
||||
has_many :enterprise_roles, dependent: :destroy
|
||||
has_many :users, through: :enterprise_roles
|
||||
belongs_to :owner, class_name: 'Spree::User',
|
||||
@@ -62,18 +62,18 @@ class Enterprise < ApplicationRecord
|
||||
has_many :distributor_payment_methods,
|
||||
inverse_of: :distributor,
|
||||
foreign_key: :distributor_id,
|
||||
dependent: :restrict_with_exception
|
||||
dependent: :restrict_with_error
|
||||
has_many :distributor_shipping_methods,
|
||||
inverse_of: :distributor,
|
||||
foreign_key: :distributor_id,
|
||||
dependent: :restrict_with_exception
|
||||
dependent: :restrict_with_error
|
||||
has_many :payment_methods, through: :distributor_payment_methods
|
||||
has_many :shipping_methods, through: :distributor_shipping_methods
|
||||
has_many :customers, dependent: :destroy
|
||||
has_many :inventory_items, dependent: :destroy
|
||||
has_many :tag_rules, dependent: :destroy
|
||||
has_one :stripe_account, dependent: :destroy
|
||||
has_many :vouchers, dependent: :restrict_with_exception
|
||||
has_many :vouchers, dependent: :restrict_with_error
|
||||
has_many :connected_apps, dependent: :destroy
|
||||
has_many :dfc_permissions, dependent: :destroy
|
||||
has_one :custom_tab, dependent: :destroy
|
||||
@@ -111,14 +111,14 @@ class Enterprise < ApplicationRecord
|
||||
end
|
||||
|
||||
validates :logo,
|
||||
processable_image: true,
|
||||
content_type: %r{\Aimage/(png|jpeg|gif|jpg|svg\+xml|webp)\Z}
|
||||
processable_file: true,
|
||||
content_type: ::Spree::Image::ACCEPTED_CONTENT_TYPES
|
||||
validates :promo_image,
|
||||
processable_image: true,
|
||||
content_type: %r{\Aimage/(png|jpeg|gif|jpg|svg\+xml|webp)\Z}
|
||||
processable_file: true,
|
||||
content_type: ::Spree::Image::ACCEPTED_CONTENT_TYPES
|
||||
validates :white_label_logo,
|
||||
processable_image: true,
|
||||
content_type: %r{\Aimage/(png|jpeg|gif|jpg|svg\+xml|webp)\Z}
|
||||
processable_file: true,
|
||||
content_type: ::Spree::Image::ACCEPTED_CONTENT_TYPES
|
||||
validates :terms_and_conditions, content_type: {
|
||||
in: "application/pdf",
|
||||
message: I18n.t(:enterprise_terms_and_conditions_type_error),
|
||||
|
||||
@@ -29,11 +29,11 @@ class EnterpriseGroup < ApplicationRecord
|
||||
has_one_attached :promo_image, service: image_service
|
||||
|
||||
validates :logo,
|
||||
processable_image: true,
|
||||
content_type: %r{\Aimage/(png|jpeg|gif|jpg|svg\+xml|webp)\Z}
|
||||
processable_file: true,
|
||||
content_type: ::Spree::Image::ACCEPTED_CONTENT_TYPES
|
||||
validates :promo_image,
|
||||
processable_image: true,
|
||||
content_type: %r{\Aimage/(png|jpeg|gif|jpg|svg\+xml|webp)\Z}
|
||||
processable_file: true,
|
||||
content_type: ::Spree::Image::ACCEPTED_CONTENT_TYPES
|
||||
|
||||
scope :by_position, -> { order('position ASC') }
|
||||
scope :on_front_page, -> { where(on_front_page: true) }
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
module Spree
|
||||
class Image < Asset
|
||||
ACCEPTED_CONTENT_TYPES = %r{\Aimage/(png|jpeg|gif|jpg|svg\+xml|webp)\Z}
|
||||
|
||||
has_one_attached :attachment, service: image_service do |attachment|
|
||||
attachment.variant :mini, resize_to_fill: [48, 48]
|
||||
attachment.variant :small, resize_to_fill: [227, 227]
|
||||
@@ -11,8 +13,8 @@ module Spree
|
||||
|
||||
validates :attachment,
|
||||
attached: true,
|
||||
processable_image: true,
|
||||
content_type: %r{\Aimage/(png|jpeg|gif|jpg|svg\+xml|webp)\Z}
|
||||
processable_file: true,
|
||||
content_type: ACCEPTED_CONTENT_TYPES
|
||||
validate :no_attachment_errors
|
||||
|
||||
def self.default_image_url(size)
|
||||
|
||||
@@ -101,6 +101,24 @@ module Spree
|
||||
end
|
||||
|
||||
after_transition to: :completed, do: :set_captured_at
|
||||
after_transition do |payment, transition|
|
||||
# Catch any exceptions to prevent any rollback potentially
|
||||
# preventing payment from going through
|
||||
ActiveSupport::Notifications.instrument(
|
||||
"ofn.payment_transition", payment: payment, event: transition.to
|
||||
)
|
||||
rescue StandardError => e
|
||||
Rails.logger.fatal "ActiveSupport::Notification.instrument failed params: " \
|
||||
"<event_type:ofn.payment_transition> " \
|
||||
"<payment_id:#{payment.id}> " \
|
||||
"<event:#{transition.to}>"
|
||||
Alert.raise(
|
||||
e,
|
||||
metadata: {
|
||||
event_tye: "ofn.payment_transition", payment_id: payment.id, event: transition.to
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def money
|
||||
|
||||
@@ -2,5 +2,11 @@
|
||||
|
||||
# Records a webhook url to send notifications to
|
||||
class WebhookEndpoint < ApplicationRecord
|
||||
WEBHOOK_TYPES = %w(order_cycle_opened payment_status_changed).freeze
|
||||
|
||||
validates :url, presence: true
|
||||
validates :webhook_type, presence: true, inclusion: { in: WEBHOOK_TYPES }
|
||||
|
||||
scope :order_cycle_opened, -> { where(webhook_type: "order_cycle_opened") }
|
||||
scope :payment_status, -> { where(webhook_type: "payment_status_changed") }
|
||||
end
|
||||
|
||||
@@ -11,7 +11,9 @@ class ImageImporter
|
||||
|
||||
image = Spree::Image.create do |img|
|
||||
PrivateAddressCheck.only_public_connections do
|
||||
img.attachment.attach(io: valid_url.open, filename:, metadata:)
|
||||
io = valid_url.open
|
||||
content_type = Marcel::MimeType.for(io)
|
||||
img.attachment.attach(io:, filename:, metadata:, content_type:)
|
||||
end
|
||||
end
|
||||
product.image = image if image
|
||||
|
||||
@@ -11,10 +11,12 @@ module OrderCycles
|
||||
.merge(coordinator_name: order_cycle.coordinator.name)
|
||||
|
||||
# Endpoints for coordinator owner
|
||||
webhook_endpoints = order_cycle.coordinator.owner.webhook_endpoints
|
||||
webhook_endpoints = order_cycle.coordinator.owner.webhook_endpoints.order_cycle_opened
|
||||
|
||||
# Plus unique endpoints for distributor owners (ignore duplicates)
|
||||
webhook_endpoints |= order_cycle.distributors.map(&:owner).flat_map(&:webhook_endpoints)
|
||||
webhook_endpoints |= order_cycle.distributors.map(&:owner).flat_map { |owner|
|
||||
owner.webhook_endpoints.order_cycle_opened
|
||||
}
|
||||
|
||||
webhook_endpoints.each do |endpoint|
|
||||
WebhookDeliveryJob.perform_later(endpoint.url, event, webhook_payload, at:)
|
||||
|
||||
13
app/services/payments/status_changed_listener_service.rb
Normal file
13
app/services/payments/status_changed_listener_service.rb
Normal file
@@ -0,0 +1,13 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Called by "ActiveSupport::Notifications" when an "ofn.payment_transition" occurs
|
||||
# Event originate from Spree::Payment event machine
|
||||
#
|
||||
module Payments
|
||||
class StatusChangedListenerService
|
||||
def call(_name, started, _finished, _unique_id, payload)
|
||||
event = "payment.#{payload[:event]}"
|
||||
Payments::WebhookService.create_webhook_job(payment: payload[:payment], event:, at: started)
|
||||
end
|
||||
end
|
||||
end
|
||||
84
app/services/payments/webhook_payload.rb
Normal file
84
app/services/payments/webhook_payload.rb
Normal file
@@ -0,0 +1,84 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Payments
|
||||
class WebhookPayload
|
||||
def initialize(payment:, order:, enterprise:)
|
||||
@payment = payment
|
||||
@order = order
|
||||
@enterprise = enterprise
|
||||
end
|
||||
|
||||
def to_hash
|
||||
{
|
||||
payment: @payment.slice(:updated_at, :amount, :state),
|
||||
enterprise: @enterprise.slice(:abn, :acn, :name)
|
||||
.merge(address: @enterprise.address.slice(:address1, :address2, :city, :zipcode)),
|
||||
order: @order.slice(:total, :currency).merge(line_items: line_items)
|
||||
}.with_indifferent_access
|
||||
end
|
||||
|
||||
def self.test_data
|
||||
new(payment: test_payment, order: test_order, enterprise: test_enterprise)
|
||||
end
|
||||
|
||||
def self.test_payment
|
||||
{
|
||||
updated_at: Time.zone.now,
|
||||
amount: 0.00,
|
||||
state: "completed"
|
||||
}
|
||||
end
|
||||
|
||||
def self.test_order
|
||||
order = Spree::Order.new(
|
||||
total: 0.00,
|
||||
currency: "AUD",
|
||||
)
|
||||
|
||||
tax_category = Spree::TaxCategory.new(name: "VAT")
|
||||
product = Spree::Product.new(name: "Test product")
|
||||
Spree::Variant.new(product:, display_name: "")
|
||||
order.line_items << Spree::LineItem.new(
|
||||
quantity: 1,
|
||||
price: 20.00,
|
||||
tax_category:,
|
||||
product:,
|
||||
unit_presentation: "1kg"
|
||||
)
|
||||
|
||||
order
|
||||
end
|
||||
|
||||
def self.test_enterprise
|
||||
enterprise = Enterprise.new(
|
||||
abn: "65797115831",
|
||||
acn: "",
|
||||
name: "TEST Enterprise",
|
||||
)
|
||||
enterprise.address = Spree::Address.new(
|
||||
address1: "1 testing street",
|
||||
address2: "",
|
||||
city: "TestCity",
|
||||
zipcode: "1234"
|
||||
)
|
||||
|
||||
enterprise
|
||||
end
|
||||
|
||||
private_class_method :test_payment, :test_order, :test_enterprise
|
||||
|
||||
private
|
||||
|
||||
def line_items
|
||||
@order.line_items.map do |li|
|
||||
li.slice(:quantity, :price)
|
||||
.merge(
|
||||
tax_category_name: li.tax_category&.name,
|
||||
product_name: li.product.name,
|
||||
name_to_display: li.display_name,
|
||||
unit_to_display: li.unit_presentation
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
30
app/services/payments/webhook_service.rb
Normal file
30
app/services/payments/webhook_service.rb
Normal file
@@ -0,0 +1,30 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Create a webhook payload for a payment status event.
|
||||
# The payload will be delivered asynchronously.
|
||||
|
||||
module Payments
|
||||
class WebhookService
|
||||
def self.create_webhook_job(payment:, event:, at:)
|
||||
order = payment.order
|
||||
payload = WebhookPayload.new(payment:, order:, enterprise: order.distributor).to_hash
|
||||
|
||||
coordinator = payment.order.order_cycle.coordinator
|
||||
webhook_urls(coordinator).each do |url|
|
||||
WebhookDeliveryJob.perform_later(url, event, payload, at:)
|
||||
end
|
||||
end
|
||||
|
||||
def self.webhook_urls(coordinator)
|
||||
# url for coordinator owner
|
||||
webhook_urls = coordinator.owner.webhook_endpoints.payment_status.pluck(:url)
|
||||
|
||||
# plus url for coordinator manager (ignore duplicate)
|
||||
users_webhook_urls = coordinator.users.flat_map do |user|
|
||||
user.webhook_endpoints.payment_status.pluck(:url)
|
||||
end
|
||||
|
||||
webhook_urls | users_webhook_urls
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -22,7 +22,7 @@
|
||||
%tbody
|
||||
= f.fields_for :collection do |enterprise_form|
|
||||
- enterprise = enterprise_form.object
|
||||
%tr{class: "enterprise-#{enterprise.id}"}
|
||||
%tr{class: "enterprise-#{enterprise.id}", id: "resource-#{enterprise.id}"}
|
||||
%td= link_to enterprise.name, main_app.edit_admin_enterprise_path(enterprise)
|
||||
%td
|
||||
= enterprise_form.check_box :is_primary_producer
|
||||
|
||||
4
app/views/admin/enterprises/destroy.turbo_stream.haml
Normal file
4
app/views/admin/enterprises/destroy.turbo_stream.haml
Normal file
@@ -0,0 +1,4 @@
|
||||
- unless flash[:error]
|
||||
= turbo_stream.remove "resource-#{@object.id}"
|
||||
= turbo_stream.append "flashes" do
|
||||
= render(partial: 'admin/shared/flashes', locals: { flashes: flash })
|
||||
@@ -18,7 +18,7 @@
|
||||
%span{ "ofn-with-tip": '{{ orderCycle.producerNames }}', "ng-show": 'orderCycle.producers.length > 3' }
|
||||
{{ orderCycle.producers.length }}
|
||||
= t('.suppliers')
|
||||
%span{ "ng-hide": 'orderCycle.producers.length > 3', "ng-bind": 'orderCycle.producerNames' }
|
||||
%span{ "ng-hide": 'orderCycle.producers.length > 3', "ng-bind-html": 'orderCycle.producerNames' }
|
||||
%td.coordinator{ "ng-show": 'columns.coordinator.visible', "ng-bind-html": 'orderCycle.coordinator.name' }
|
||||
%td.shops{ "ng-show": 'columns.shops.visible' }
|
||||
%span{ "ofn-with-tip": '{{ orderCycle.shopNames }}', "ng-show": 'orderCycle.shops.length > 3' }
|
||||
|
||||
@@ -10,24 +10,5 @@
|
||||
%th= t('.url.header')
|
||||
%th.actions
|
||||
%tbody
|
||||
-# Existing endpoints
|
||||
- @user.webhook_endpoints.each do |webhook_endpoint|
|
||||
%tr
|
||||
%td= t('.event_types.order_cycle_opened') # For now, we only support one type.
|
||||
%td= webhook_endpoint.url
|
||||
%td.actions
|
||||
- if webhook_endpoint.persisted?
|
||||
= button_to account_webhook_endpoint_path(webhook_endpoint), method: :delete,
|
||||
class: "tiny alert no-margin",
|
||||
data: { confirm: I18n.t(:are_you_sure)} do
|
||||
= I18n.t(:delete)
|
||||
|
||||
-# Create new
|
||||
- if @user.webhook_endpoints.empty? # Only one allowed for now.
|
||||
%tr
|
||||
%td= t('.event_types.order_cycle_opened') # For now, we only support one type.
|
||||
%td
|
||||
= form_for(@user.webhook_endpoints.build, url: account_webhook_endpoints_path, id: 'new_webhook_endpoint') do |f|
|
||||
= f.url_field :url, placeholder: t('.url.create_placeholder'), required: true, size: 64
|
||||
%td.actions
|
||||
= button_tag t(:create), class: 'button primary tiny no-margin', form: 'new_webhook_endpoint'
|
||||
= render WebhookEndpointFormComponent.new(webhooks: @user.webhook_endpoints.order_cycle_opened, webhook_type: "order_cycle_opened")
|
||||
= render WebhookEndpointFormComponent.new(webhooks: @user.webhook_endpoints.payment_status, webhook_type: "payment_status_changed")
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
.saved_cards, .no_cards {
|
||||
.saved_cards,
|
||||
.no_cards {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
@@ -26,7 +27,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.authorised_shops{
|
||||
.authorised_shops {
|
||||
table {
|
||||
width: 100%;
|
||||
}
|
||||
@@ -39,7 +40,9 @@
|
||||
a {
|
||||
color: $clr-brick;
|
||||
|
||||
&:hover, &:active, &:focus {
|
||||
&:hover,
|
||||
&:active,
|
||||
&:focus {
|
||||
color: $clr-brick-med-bright;
|
||||
}
|
||||
}
|
||||
@@ -60,7 +63,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
i.ofn-i_059-producer, i.ofn-i_060-producer-reversed {
|
||||
i.ofn-i_059-producer,
|
||||
i.ofn-i_060-producer-reversed {
|
||||
font-size: 3rem;
|
||||
display: inline-block;
|
||||
margin-right: 0.25rem;
|
||||
@@ -92,7 +96,8 @@
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.transaction-group {}
|
||||
.transaction-group {
|
||||
}
|
||||
|
||||
table {
|
||||
border-radius: $radius-medium $radius-medium 0 0;
|
||||
@@ -161,6 +166,15 @@ table {
|
||||
//
|
||||
// Unfortunately we can't use Scss's interpolation
|
||||
// https://sass-lang.com/documentation/interpolation. We're using a too old version perhaps?
|
||||
right: calc(12px + 2*2px + 2*1px);
|
||||
right: calc(12px + 2 * 2px + 2 * 1px);
|
||||
}
|
||||
}
|
||||
|
||||
// Webhook Endpoints
|
||||
td.endpoints-actions {
|
||||
display: flex;
|
||||
|
||||
form {
|
||||
padding-right: $padding-small;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,10 +58,15 @@ module Openfoodnetwork
|
||||
Spree::Core::Engine.routes.default_url_options[:host] = ENV["SITE_URL"] if Rails.env == 'test'
|
||||
end
|
||||
|
||||
# We reload the routes here
|
||||
# so that the appended/prepended routes are available to the application.
|
||||
config.after_initialize do
|
||||
# We reload the routes here
|
||||
# so that the appended/prepended routes are available to the application.
|
||||
Rails.application.routes_reloader.reload!
|
||||
|
||||
# Subscribe to payment transition events
|
||||
ActiveSupport::Notifications.subscribe(
|
||||
"ofn.payment_transition", Payments::StatusChangedListenerService.new
|
||||
)
|
||||
end
|
||||
|
||||
initializer "spree.environment", before: :load_config_initializers do |app|
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
# `button_to` view helper will render `<button>` element, regardless of whether
|
||||
# or not the content is passed as the first argument or as a block.
|
||||
# Rails.application.config.action_view.button_to_generates_button_tag = true
|
||||
Rails.application.config.action_view.button_to_generates_button_tag = true
|
||||
|
||||
# `stylesheet_link_tag` view helper will not render the media attribute by default.
|
||||
Rails.application.config.action_view.apply_stylesheet_media_default = false
|
||||
|
||||
@@ -93,7 +93,7 @@
|
||||
# serializer. Therefore, this setting should only be enabled after all replicas
|
||||
# have been successfully upgraded to Rails 7.1.
|
||||
#++
|
||||
# Rails.application.config.active_job.use_big_decimal_serializer = true
|
||||
Rails.application.config.active_job.use_big_decimal_serializer = true
|
||||
|
||||
###
|
||||
# Specify if an `ArgumentError` should be raised if `Rails.cache` `fetch` or
|
||||
|
||||
@@ -138,26 +138,66 @@ en:
|
||||
# Used by active_storage_validations
|
||||
errors:
|
||||
messages:
|
||||
content_type_invalid: "has an invalid content type"
|
||||
file_size_out_of_range: "size %{file_size} is not between required range"
|
||||
limit_out_of_range: "total number is out of range"
|
||||
image_metadata_missing: "is not a valid image"
|
||||
dimension_min_inclusion: "must be greater than or equal to %{width} x %{height} pixel."
|
||||
dimension_max_inclusion: "must be less than or equal to %{width} x %{height} pixel."
|
||||
dimension_width_inclusion: "width is not included between %{min} and %{max} pixel."
|
||||
dimension_height_inclusion: "height is not included between %{min} and %{max} pixel."
|
||||
dimension_width_greater_than_or_equal_to: "width must be greater than or equal to %{length} pixel."
|
||||
dimension_height_greater_than_or_equal_to: "height must be greater than or equal to %{length} pixel."
|
||||
dimension_width_less_than_or_equal_to: "width must be less than or equal to %{length} pixel."
|
||||
dimension_height_less_than_or_equal_to: "height must be less than or equal to %{length} pixel."
|
||||
dimension_width_equal_to: "width must be equal to %{length} pixel."
|
||||
dimension_height_equal_to: "height must be equal to %{length} pixel."
|
||||
aspect_ratio_not_square: "must be a square image"
|
||||
aspect_ratio_not_portrait: "must be a portrait image"
|
||||
aspect_ratio_not_landscape: "must be a landscape image"
|
||||
aspect_ratio_is_not: "must have an aspect ratio of %{aspect_ratio}"
|
||||
aspect_ratio_unknown: "has an unknown aspect ratio"
|
||||
image_not_processable: "is not a valid image"
|
||||
content_type_invalid:
|
||||
one: "has an invalid content type (authorized content type is %{authorized_human_content_types})"
|
||||
other: "has an invalid content type (authorized content types are %{authorized_human_content_types})"
|
||||
content_type_spoofed:
|
||||
one: "has a content type that is not equivalent to the one that is detected through its content (authorized content type is %{authorized_human_content_types})"
|
||||
other: "has a content type that is not equivalent to the one that is detected through its content (authorized content types are %{authorized_human_content_types})"
|
||||
file_size_not_less_than: "file size must be less than %{max} (current size is %{file_size})"
|
||||
file_size_not_less_than_or_equal_to: "file size must be less than or equal to %{max} (current size is %{file_size})"
|
||||
file_size_not_greater_than: "file size must be greater than %{min} (current size is %{file_size})"
|
||||
file_size_not_greater_than_or_equal_to: "file size must be greater than or equal to %{min} (current size is %{file_size})"
|
||||
file_size_not_between: "file size must be between %{min} and %{max} (current size is %{file_size})"
|
||||
file_size_not_equal_to: "file size must be equal to %{exact} (current size is %{file_size})"
|
||||
total_file_size_not_less_than: "total file size must be less than %{max} (current size is %{total_file_size})"
|
||||
total_file_size_not_less_than_or_equal_to: "total file size must be less than or equal to %{max} (current size is %{total_file_size})"
|
||||
total_file_size_not_greater_than: "total file size must be greater than %{min} (current size is %{total_file_size})"
|
||||
total_file_size_not_greater_than_or_equal_to: "total file size must be greater than or equal to %{min} (current size is %{total_file_size})"
|
||||
total_file_size_not_between: "total file size must be between %{min} and %{max} (current size is %{total_file_size})"
|
||||
total_file_size_not_equal_to: "total file size must be equal to %{exact} (current size is %{total_file_size})"
|
||||
duration_not_less_than: "duration must be less than %{max} (current duration is %{duration})"
|
||||
duration_not_less_than_or_equal_to: "duration must be less than or equal to %{max} (current duration is %{duration})"
|
||||
duration_not_greater_than: "duration must be greater than %{min} (current duration is %{duration})"
|
||||
duration_not_greater_than_or_equal_to: "duration must be greater than or equal to %{min} (current duration is %{duration})"
|
||||
duration_not_between: "duration must be between %{min} and %{max} (current duration is %{duration})"
|
||||
duration_not_equal_to: "duration must be equal to %{exact} (current duration is %{duration})"
|
||||
limit_out_of_range:
|
||||
zero: "no files attached (must have between %{min} and %{max} files)"
|
||||
one: "only 1 file attached (must have between %{min} and %{max} files)"
|
||||
other: "total number of files must be between %{min} and %{max} files (there are %{count} files attached)"
|
||||
limit_min_not_reached:
|
||||
zero: "no files attached (must have at least %{min} files)"
|
||||
one: "only 1 file attached (must have at least %{min} files)"
|
||||
other: "%{count} files attached (must have at least %{min} files)"
|
||||
limit_max_exceeded:
|
||||
zero: "no files attached (maximum is %{max} files)"
|
||||
one: "too many files attached (maximum is %{max} files, got %{count})"
|
||||
other: "too many files attached (maximum is %{max} files, got %{count})"
|
||||
attachment_missing: "is missing its attachment"
|
||||
media_metadata_missing: "is not a valid media file"
|
||||
dimension_min_not_included_in: "must be greater than or equal to %{width} x %{height} pixels"
|
||||
dimension_max_not_included_in: "must be less than or equal to %{width} x %{height} pixels"
|
||||
dimension_width_not_included_in: "width is not included between %{min} and %{max} pixels"
|
||||
dimension_height_not_included_in: "height is not included between %{min} and %{max} pixels"
|
||||
dimension_width_not_greater_than_or_equal_to: "width must be greater than or equal to %{length} pixels"
|
||||
dimension_height_not_greater_than_or_equal_to: "height must be greater than or equal to %{length} pixels"
|
||||
dimension_width_not_less_than_or_equal_to: "width must be less than or equal to %{length} pixels"
|
||||
dimension_height_not_less_than_or_equal_to: "height must be less than or equal to %{length} pixels"
|
||||
dimension_width_not_equal_to: "width must be equal to %{length} pixels"
|
||||
dimension_height_not_equal_to: "height must be equal to %{length} pixels"
|
||||
aspect_ratio_not_square: "must be square (current file is %{width}x%{height}px)"
|
||||
aspect_ratio_not_portrait: "must be portrait (current file is %{width}x%{height}px)"
|
||||
aspect_ratio_not_landscape: "must be landscape (current file is %{width}x%{height}px)"
|
||||
aspect_ratio_not_x_y: "must be %{authorized_aspect_ratios} (current file is %{width}x%{height}px)"
|
||||
aspect_ratio_invalid: "has an invalid aspect ratio (valid aspect ratios are %{authorized_aspect_ratios})"
|
||||
file_not_processable: "is not identified as a valid media file"
|
||||
pages_not_less_than: "page count must be less than %{max} (current page count is %{pages})"
|
||||
pages_not_less_than_or_equal_to: "page count must be less than or equal to %{max} (current page count is %{pages})"
|
||||
pages_not_greater_than: "page count must be greater than %{min} (current page count is %{pages})"
|
||||
pages_not_greater_than_or_equal_to: "page count must be greater than or equal to %{min} (current page count is %{pages})"
|
||||
pages_not_between: "page count must be between %{min} and %{max} (current page count is %{pages})"
|
||||
pages_not_equal_to: "page count must be equal to %{exact} (current page count is %{pages})"
|
||||
not_found:
|
||||
title: "The page you were looking for doesn't exist (404)"
|
||||
message_html: "<b>Please try again</b>
|
||||
@@ -4088,6 +4128,8 @@ en:
|
||||
destroy:
|
||||
success: Webhook endpoint successfully deleted
|
||||
error: Webhook endpoint failed to delete
|
||||
test:
|
||||
success: Some test data will be sent to the webhook url
|
||||
|
||||
spree:
|
||||
order_updated: "Order Updated"
|
||||
@@ -4195,6 +4237,8 @@ en:
|
||||
logourl: "Logourl"
|
||||
are_you_sure_delete: "Are you sure you want to delete this record?"
|
||||
confirm_delete: "Confirm Deletion"
|
||||
tag_rule: "Tag Rule"
|
||||
voucher: "Voucher"
|
||||
|
||||
configurations: "Configurations"
|
||||
general_settings: "General Settings"
|
||||
@@ -4938,13 +4982,10 @@ en:
|
||||
webhook_endpoints:
|
||||
title: Webhook Endpoints
|
||||
description: Events in the system may trigger webhooks to external systems.
|
||||
event_types:
|
||||
order_cycle_opened: Order Cycle Opened
|
||||
event_type:
|
||||
header: Event type
|
||||
url:
|
||||
header: Endpoint URL
|
||||
create_placeholder: Enter the URL of the remote webhook endpoint
|
||||
developer_settings:
|
||||
title: Developer Settings
|
||||
form:
|
||||
@@ -5093,7 +5134,13 @@ en:
|
||||
add_tag_rule_modal:
|
||||
select_rule_type: "Select a rule type:"
|
||||
add_rule: "Add Rule"
|
||||
|
||||
webhook_endpoint_form:
|
||||
url:
|
||||
create_placeholder: Enter the URL of the remote webhook endpoint
|
||||
event_types:
|
||||
order_cycle_opened: Order Cycle Opened
|
||||
payment_status_changed: Post webhook on Payment status change
|
||||
test_endpoint: Test webhook endpoint
|
||||
|
||||
# Gem to prevent bot form submissions
|
||||
invisible_captcha:
|
||||
|
||||
@@ -34,6 +34,7 @@ Spree::Core::Engine.routes.draw do
|
||||
|
||||
resource :account, :controller => 'users' do
|
||||
resources :webhook_endpoints, only: [:create, :destroy], controller: '/webhook_endpoints'
|
||||
post '/webhook_endpoints/:id/test', to: "/webhook_endpoints#test", as: "webhook_endpoint_test"
|
||||
end
|
||||
|
||||
match '/admin/orders/bulk_management' => 'admin/orders#bulk_management', :as => "admin_bulk_order_management", via: :get
|
||||
|
||||
16
db/migrate/20251124043324_add_type_to_webhook_endpoints.rb
Normal file
16
db/migrate/20251124043324_add_type_to_webhook_endpoints.rb
Normal file
@@ -0,0 +1,16 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class AddTypeToWebhookEndpoints < ActiveRecord::Migration[7.1]
|
||||
def up
|
||||
# Using "order_cycle_opened" as default will update existing record
|
||||
change_table(:webhook_endpoints, bulk: true) do |t|
|
||||
t.column :webhook_type, :string, limit: 255, null: false, default: "order_cycle_opened"
|
||||
end
|
||||
# Drop the default value
|
||||
change_column_default :webhook_endpoints, :webhook_type, nil
|
||||
end
|
||||
|
||||
def down
|
||||
remove_column :webhook_endpoints, :webhook_type
|
||||
end
|
||||
end
|
||||
@@ -1143,6 +1143,7 @@ ActiveRecord::Schema[7.1].define(version: 2025_11_26_005628) do
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.bigint "user_id", default: 0, null: false
|
||||
t.string "webhook_type", limit: 255, null: false
|
||||
t.index ["user_id"], name: "index_webhook_endpoints_on_user_id"
|
||||
end
|
||||
|
||||
|
||||
@@ -13,14 +13,7 @@ module DfcProvider
|
||||
# It means that our permissions to access data on another platform changed.
|
||||
# We will need to pull the updated data.
|
||||
def create
|
||||
unless current_user.is_a? ApiUser
|
||||
unauthorized "You need to authenticate as authorised platform (client_id)."
|
||||
return
|
||||
end
|
||||
unless current_user.id == "lf-dev"
|
||||
unauthorized "Your client_id is not authorised on this platform."
|
||||
return
|
||||
end
|
||||
return if rendered_errors?
|
||||
|
||||
event = JSON.parse(request.body.read)
|
||||
enterprises_url = event["enterpriseUrlid"]
|
||||
@@ -45,8 +38,23 @@ module DfcProvider
|
||||
|
||||
private
|
||||
|
||||
def unauthorized(message)
|
||||
render_message(:unauthorized, message)
|
||||
def rendered_errors?
|
||||
unless current_user.is_a? ApiUser
|
||||
render_message(
|
||||
:unauthorized,
|
||||
"You need to authenticate as authorised platform (client_id).",
|
||||
)
|
||||
return true
|
||||
end
|
||||
unless current_user.id == "lf-dev"
|
||||
render_message(
|
||||
:unauthorized,
|
||||
"Your client_id is not authorised on this platform.",
|
||||
)
|
||||
return true
|
||||
end
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
def render_message(status, message)
|
||||
|
||||
@@ -19,12 +19,14 @@ class ImageBuilder < DfcBuilder
|
||||
|
||||
def self.import(image_link)
|
||||
url = URI.parse(image_link)
|
||||
filename = File.basename(image_link)
|
||||
filename = File.basename(url.path)
|
||||
metadata = { custom: { origin: image_link } }
|
||||
|
||||
Spree::Image.new.tap do |image|
|
||||
PrivateAddressCheck.only_public_connections do
|
||||
image.attachment.attach(io: url.open, filename:, metadata:)
|
||||
io = url.open
|
||||
content_type = Marcel::MimeType.for(io)
|
||||
image.attachment.attach(io:, filename:, metadata:, content_type:)
|
||||
end
|
||||
end
|
||||
rescue StandardError
|
||||
|
||||
@@ -251,7 +251,7 @@ RSpec.describe SuppliedProductImporter do
|
||||
supplied_product.isVariantOf << tomatoes
|
||||
|
||||
imported_product = importer.import_variant(supplied_product, supplier).product
|
||||
expect(imported_product.image.attachment.filename).to eq "tomato.png?v=1"
|
||||
expect(imported_product.image.attachment.filename).to eq "tomato.png"
|
||||
|
||||
expect {
|
||||
importer.import_variant(supplied_product, supplier).product
|
||||
@@ -266,7 +266,7 @@ RSpec.describe SuppliedProductImporter do
|
||||
}
|
||||
.to change { imported_product.image }
|
||||
|
||||
expect(imported_product.image.attachment.filename).to eq "tomato.png?v=2"
|
||||
expect(imported_product.image.attachment.filename).to eq "tomato.png"
|
||||
end
|
||||
|
||||
context "when spree_product_uri doesn't match the server host" do
|
||||
|
||||
@@ -45,7 +45,8 @@ RSpec.describe Api::V0::ProductImagesController do
|
||||
expect(response).to have_http_status :unprocessable_entity
|
||||
expect(product_without_image.image).to be_nil
|
||||
expect(json_response["id"]).to eq nil
|
||||
expect(json_response["errors"]).to include "Attachment has an invalid content type"
|
||||
expect(json_response["errors"]).to include "Attachment is " \
|
||||
"not identified as a valid media file"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -11,14 +11,16 @@ RSpec.describe WebhookEndpointsController do
|
||||
describe "#create" do
|
||||
it "creates a webhook_endpoint" do
|
||||
expect {
|
||||
spree_post :create, { url: "https://url" }
|
||||
spree_post :create, { url: "https://url", webhook_type: "order_cycle_opened" }
|
||||
}.to change {
|
||||
user.webhook_endpoints.count
|
||||
}.by(1)
|
||||
|
||||
expect(flash[:success]).to be_present
|
||||
expect(flash[:error]).to be_blank
|
||||
expect(user.webhook_endpoints.first.url).to eq "https://url"
|
||||
webhook = user.webhook_endpoints.first
|
||||
expect(webhook.url).to eq "https://url"
|
||||
expect(webhook.webhook_type).to eq "order_cycle_opened"
|
||||
end
|
||||
|
||||
it "shows error if parameters not specified" do
|
||||
@@ -33,17 +35,20 @@ RSpec.describe WebhookEndpointsController do
|
||||
end
|
||||
|
||||
it "redirects back to referrer" do
|
||||
spree_post :create, { url: "https://url" }
|
||||
spree_post :create, { url: "https://url", webhook_type: "order_cycle_opened" }
|
||||
|
||||
expect(response).to redirect_to "/account#/developer_settings"
|
||||
end
|
||||
end
|
||||
|
||||
describe "#destroy" do
|
||||
let!(:webhook_endpoint) { user.webhook_endpoints.create(url: "https://url") }
|
||||
let!(:webhook_endpoint) {
|
||||
user.webhook_endpoints.create(url: "https://url", webhook_type: "order_cycle_opened")
|
||||
}
|
||||
|
||||
it "destroys a webhook_endpoint" do
|
||||
webhook_endpoint2 = user.webhook_endpoints.create!(url: "https://url2")
|
||||
webhook_endpoint2 = user.webhook_endpoints.create!(url: "https://url2",
|
||||
webhook_type: "order_cycle_opened")
|
||||
|
||||
expect {
|
||||
spree_delete :destroy, { id: webhook_endpoint.id }
|
||||
@@ -64,4 +69,22 @@ RSpec.describe WebhookEndpointsController do
|
||||
expect(response).to redirect_to "/account#/developer_settings"
|
||||
end
|
||||
end
|
||||
|
||||
describe "#test" do
|
||||
let(:webhook_endpoint) {
|
||||
user.webhook_endpoints.create(url: "https://url", webhook_type: "payment_status_changed" )
|
||||
}
|
||||
|
||||
subject { spree_post :test, id: webhook_endpoint.id, format: :turbo_stream }
|
||||
|
||||
it "enqueus a webhook job" do
|
||||
expect { subject }.to enqueue_job(WebhookDeliveryJob).exactly(1).times
|
||||
end
|
||||
|
||||
it "shows a success mesage" do
|
||||
subject
|
||||
|
||||
expect(flash[:success]).to eq "Some test data will be sent to the webhook url"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
BIN
spec/fixtures/files/logo.bmp
vendored
Normal file
BIN
spec/fixtures/files/logo.bmp
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.8 KiB |
@@ -58,37 +58,40 @@ RSpec.describe Enterprise do
|
||||
expect(EnterpriseRelationship.where(id: [er1, er2])).to be_empty
|
||||
end
|
||||
|
||||
it "raises a DeleteRestrictionError on destroy if distributed_orders exist" do
|
||||
it "does not destroy distributed_orders upon destroy" do
|
||||
enterprise = create(:distributor_enterprise)
|
||||
create_list(:order, 2, distributor: enterprise)
|
||||
|
||||
expect do
|
||||
enterprise.destroy
|
||||
end.to raise_error(ActiveRecord::DeleteRestrictionError,
|
||||
/Cannot delete record because of dependent distributed_orders/)
|
||||
.and change { Spree::Order.count }.by(0)
|
||||
expect(enterprise.errors.full_messages).to eq(
|
||||
["Cannot delete record because dependent distributed orders exist"]
|
||||
)
|
||||
end.to change { Spree::Order.count }.by(0)
|
||||
end
|
||||
|
||||
it "raises an DeleteRestrictionError on destroy if distributor_payment_methods exist" do
|
||||
it "does not destroy distributor_payment_methods upon destroy" do
|
||||
enterprise = create(:distributor_enterprise)
|
||||
create_list(:distributor_payment_method, 2, distributor: enterprise)
|
||||
|
||||
expect do
|
||||
enterprise.destroy
|
||||
end.to raise_error(ActiveRecord::DeleteRestrictionError,
|
||||
/Cannot delete record because of dependent distributor_payment_methods/)
|
||||
.and change { DistributorPaymentMethod.count }.by(0)
|
||||
expect(enterprise.errors.full_messages).to eq(
|
||||
["Cannot delete record because dependent distributor payment methods exist"]
|
||||
)
|
||||
end.to change { Spree::Order.count }.by(0)
|
||||
end
|
||||
|
||||
it "raises an DeleteRestrictionError on destroy if distributor_shipping_methods exist" do
|
||||
it "does not destroy distributor_shipping_methods upon destroy" do
|
||||
enterprise = create(:distributor_enterprise)
|
||||
create_list(:distributor_shipping_method, 2, distributor: enterprise)
|
||||
|
||||
expect do
|
||||
enterprise.destroy
|
||||
end.to raise_error(ActiveRecord::DeleteRestrictionError,
|
||||
/Cannot delete record because of dependent distributor_shipping_methods/)
|
||||
.and change { DistributorShippingMethod.count }.by(0)
|
||||
expect(enterprise.errors.full_messages).to eq(
|
||||
["Cannot delete record because dependent distributor shipping methods exist"]
|
||||
)
|
||||
end.to change { Spree::Order.count }.by(0)
|
||||
end
|
||||
|
||||
it "does not destroy enterprise_fees upon destroy" do
|
||||
@@ -97,9 +100,10 @@ RSpec.describe Enterprise do
|
||||
|
||||
expect do
|
||||
enterprise.destroy
|
||||
end.to raise_error(ActiveRecord::DeleteRestrictionError,
|
||||
/Cannot delete record because of dependent enterprise_fees/)
|
||||
.and change { EnterpriseFee.count }.by(0)
|
||||
expect(enterprise.errors.full_messages).to eq(
|
||||
["Cannot delete record because dependent enterprise fees exist"]
|
||||
)
|
||||
end.to change { Spree::Order.count }.by(0)
|
||||
end
|
||||
|
||||
it "does not destroy vouchers upon destroy" do
|
||||
@@ -110,9 +114,10 @@ RSpec.describe Enterprise do
|
||||
|
||||
expect do
|
||||
enterprise.destroy
|
||||
end.to raise_error(ActiveRecord::DeleteRestrictionError,
|
||||
/Cannot delete record because of dependent vouchers/)
|
||||
.and change { Voucher.count }.by(0)
|
||||
expect(enterprise.errors.full_messages).to eq(
|
||||
["Cannot delete record because dependent vouchers exist"]
|
||||
)
|
||||
end.to change { Spree::Order.count }.by(0)
|
||||
end
|
||||
|
||||
describe "relationships to other enterprises" do
|
||||
@@ -391,24 +396,8 @@ RSpec.describe Enterprise do
|
||||
let(:content_type) { 'image/png' }
|
||||
|
||||
before do
|
||||
blob = instance_double(
|
||||
"ActiveStorage::Blob",
|
||||
filename: ActiveStorage::Filename.new('white-label-logo.png'),
|
||||
content_type:,
|
||||
byte_size: 1024
|
||||
)
|
||||
|
||||
# InstanceDouble is not working for attachment case as the blob method is not yet defined
|
||||
# on instantiation.
|
||||
attachment = double(
|
||||
"ActiveStorage::Attached::One",
|
||||
blank?: false,
|
||||
attached?: true,
|
||||
blob:
|
||||
)
|
||||
|
||||
allow(enterprise)
|
||||
.to receive(:white_label_logo).and_return(attachment)
|
||||
blob = Rack::Test::UploadedFile.new('spec/fixtures/files/logo.png', content_type)
|
||||
enterprise.white_label_logo.attach(blob)
|
||||
end
|
||||
|
||||
context 'when the file attached is a PNG image' do
|
||||
@@ -419,6 +408,12 @@ RSpec.describe Enterprise do
|
||||
|
||||
context 'when the file attached is a BMP image' do
|
||||
let(:content_type) { 'image/bmp' }
|
||||
|
||||
before do
|
||||
blob = Rack::Test::UploadedFile.new('spec/fixtures/files/logo.bmp', content_type)
|
||||
enterprise.white_label_logo.attach(blob)
|
||||
end
|
||||
|
||||
it 'is not valid' do
|
||||
expect(enterprise).not_to be_valid
|
||||
end
|
||||
|
||||
@@ -35,6 +35,12 @@ module Spree
|
||||
}
|
||||
|
||||
before do
|
||||
# mock the call with "ofn.payment_transition" so we don't call the related listener
|
||||
# and services
|
||||
allow(ActiveSupport::Notifications).to receive(:instrument).and_call_original
|
||||
allow(ActiveSupport::Notifications).to receive(:instrument)
|
||||
.with("ofn.payment_transition", any_args).and_return(nil)
|
||||
|
||||
allow(order).to receive_message_chain(:line_items, :empty?).and_return(false)
|
||||
allow(order).to receive_messages total: 100
|
||||
stub_request(:get, "https://api.stripe.com/v1/payment_intents/12345").
|
||||
|
||||
@@ -3,6 +3,13 @@
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Spree::Payment do
|
||||
before do
|
||||
# mock the call with "ofn.payment_transition" so we don't call the related listener and services
|
||||
allow(ActiveSupport::Notifications).to receive(:instrument).and_call_original
|
||||
allow(ActiveSupport::Notifications).to receive(:instrument)
|
||||
.with("ofn.payment_transition", any_args).and_return(nil)
|
||||
end
|
||||
|
||||
context 'original specs from Spree' do
|
||||
before { Stripe.api_key = "sk_test_12345" }
|
||||
let(:order) { create(:order) }
|
||||
@@ -1064,4 +1071,17 @@ RSpec.describe Spree::Payment do
|
||||
expect(payment.captured_at).to be_present
|
||||
end
|
||||
end
|
||||
|
||||
describe "payment transition" do
|
||||
it "notifies of payment status change" do
|
||||
payment = create(:payment)
|
||||
|
||||
allow(ActiveSupport::Notifications).to receive(:instrument).and_call_original
|
||||
expect(ActiveSupport::Notifications).to receive(:instrument).with(
|
||||
"ofn.payment_transition", payment: payment, event: "processing"
|
||||
)
|
||||
|
||||
payment.started_processing!
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -5,5 +5,9 @@ require 'spec_helper'
|
||||
RSpec.describe WebhookEndpoint do
|
||||
describe "validations" do
|
||||
it { is_expected.to validate_presence_of(:url) }
|
||||
it {
|
||||
is_expected.to validate_inclusion_of(:webhook_type)
|
||||
.in_array(%w(order_cycle_opened payment_status_changed))
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -7,7 +7,7 @@ RSpec.describe ImageImporter do
|
||||
let(:product) { create(:product) }
|
||||
|
||||
describe "#import" do
|
||||
it "downloads from the Internet", :vcr do
|
||||
it "downloads from the Internet", :vcr, :aggregate_failures do
|
||||
expect {
|
||||
subject.import(ofn_url, product)
|
||||
}.to change {
|
||||
|
||||
@@ -22,7 +22,7 @@ RSpec.describe OrderCycles::WebhookService do
|
||||
# The co-ordinating enterprise has a non-owner user with an endpoint.
|
||||
# They shouldn't receive a notification.
|
||||
coordinator_user = create(:user, enterprises: [coordinator])
|
||||
coordinator_user.webhook_endpoints.create!(url: "http://coordinator_user_url")
|
||||
coordinator_user.webhook_endpoints.order_cycle_opened.create!(url: "http://coordinator_user_url")
|
||||
|
||||
expect{ subject }
|
||||
.not_to enqueue_job(WebhookDeliveryJob).with("http://coordinator_user_url", any_args)
|
||||
@@ -30,7 +30,7 @@ RSpec.describe OrderCycles::WebhookService do
|
||||
|
||||
context "coordinator owner has endpoint configured" do
|
||||
before do
|
||||
coordinator.owner.webhook_endpoints.create! url: "http://coordinator_owner_url"
|
||||
coordinator.owner.webhook_endpoints.order_cycle_opened.create!(url: "http://coordinator_owner_url")
|
||||
end
|
||||
|
||||
it "creates webhook payload for order cycle coordinator" do
|
||||
@@ -77,7 +77,7 @@ RSpec.describe OrderCycles::WebhookService do
|
||||
let(:two_distributors) {
|
||||
(1..2).map do |i|
|
||||
user = create(:user)
|
||||
user.webhook_endpoints.create!(url: "http://distributor#{i}_owner_url")
|
||||
user.webhook_endpoints.order_cycle_opened.create!(url: "http://distributor#{i}_owner_url")
|
||||
create(:distributor_enterprise, owner: user)
|
||||
end
|
||||
}
|
||||
@@ -109,7 +109,7 @@ RSpec.describe OrderCycles::WebhookService do
|
||||
}
|
||||
|
||||
it "creates only one webhook payload for the user's endpoint" do
|
||||
user.webhook_endpoints.create! url: "http://coordinator_owner_url"
|
||||
user.webhook_endpoints.order_cycle_opened.create!(url: "http://coordinator_owner_url")
|
||||
|
||||
expect{ subject }
|
||||
.to enqueue_job(WebhookDeliveryJob).with("http://coordinator_owner_url", any_args)
|
||||
@@ -128,7 +128,7 @@ RSpec.describe OrderCycles::WebhookService do
|
||||
}
|
||||
let(:supplier) {
|
||||
user = create(:user)
|
||||
user.webhook_endpoints.create!(url: "http://supplier_owner_url")
|
||||
user.webhook_endpoints.order_cycle_opened.create!(url: "http://supplier_owner_url")
|
||||
create(:supplier_enterprise, owner: user)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Payments::StatusChangedListenerService do
|
||||
let(:name) { "ofn.payment_transition" }
|
||||
let(:started) { Time.zone.parse("2025-11-28 09:00:00") }
|
||||
let(:finished) { Time.zone.parse("2025-11-28 09:00:02") }
|
||||
let(:unique_id) { "d3a7ac9f635755fcff2c" }
|
||||
let(:payload) { { payment:, event: "completed" } }
|
||||
let(:payment) { build(:payment) }
|
||||
|
||||
subject { described_class.new }
|
||||
|
||||
describe "#call" do
|
||||
it "calls Payments::WebhookService" do
|
||||
expect(Payments::WebhookService).to receive(:create_webhook_job).with(
|
||||
payment:, event: "payment.completed", at: started
|
||||
)
|
||||
|
||||
subject.call(name, started, finished, unique_id, payload)
|
||||
end
|
||||
end
|
||||
end
|
||||
95
spec/services/payments/webhook_payload_spec.rb
Normal file
95
spec/services/payments/webhook_payload_spec.rb
Normal file
@@ -0,0 +1,95 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Payments::WebhookPayload do
|
||||
describe "#to_hash" do
|
||||
let(:order) { create(:completed_order_with_totals, order_cycle: ) }
|
||||
let(:order_cycle) { create(:simple_order_cycle) }
|
||||
let(:payment) { create(:payment, :completed, amount: order.total, order:) }
|
||||
let(:tax_category) { create(:tax_category) }
|
||||
|
||||
subject { described_class.new(payment:, order:, enterprise: order.distributor) }
|
||||
|
||||
it "returns a hash with the relevant data" do
|
||||
order.line_items.update_all(tax_category_id: tax_category.id)
|
||||
|
||||
enterprise = order.distributor
|
||||
line_items = order.line_items.map do |li|
|
||||
{
|
||||
quantity: li.quantity,
|
||||
price: li.price,
|
||||
tax_category_name: li.tax_category&.name,
|
||||
product_name: li.product.name,
|
||||
name_to_display: li.display_name,
|
||||
unit_to_display: li.unit_presentation
|
||||
}
|
||||
end
|
||||
|
||||
payload = {
|
||||
payment: {
|
||||
updated_at: payment.updated_at,
|
||||
amount: payment.amount,
|
||||
state: payment.state
|
||||
},
|
||||
enterprise: {
|
||||
abn: enterprise.abn,
|
||||
acn: enterprise.acn,
|
||||
name: enterprise.name,
|
||||
address: {
|
||||
address1: enterprise.address.address1,
|
||||
address2: enterprise.address.address2,
|
||||
city: enterprise.address.city,
|
||||
zipcode: enterprise.address.zipcode
|
||||
}
|
||||
},
|
||||
order: {
|
||||
total: order.total,
|
||||
currency: order.currency,
|
||||
line_items: line_items
|
||||
}
|
||||
}.with_indifferent_access
|
||||
|
||||
expect(subject.to_hash).to eq(payload)
|
||||
end
|
||||
end
|
||||
|
||||
describe ".test_data" do
|
||||
it "returns a hash with test data" do
|
||||
test_payload = {
|
||||
payment: {
|
||||
updated_at: kind_of(Time),
|
||||
amount: 0.00,
|
||||
state: "completed"
|
||||
},
|
||||
enterprise: {
|
||||
abn: "65797115831",
|
||||
acn: "",
|
||||
name: "TEST Enterprise",
|
||||
address: {
|
||||
address1: "1 testing street",
|
||||
address2: "",
|
||||
city: "TestCity",
|
||||
zipcode: "1234"
|
||||
}
|
||||
},
|
||||
order: {
|
||||
total: 0.00,
|
||||
currency: "AUD",
|
||||
line_items: [
|
||||
{
|
||||
quantity: 1,
|
||||
price: 20.00.to_d,
|
||||
tax_category_name: "VAT",
|
||||
product_name: "Test product",
|
||||
name_to_display: nil,
|
||||
unit_to_display: "1kg"
|
||||
}
|
||||
]
|
||||
}
|
||||
}.with_indifferent_access
|
||||
|
||||
expect(described_class.test_data.to_hash).to match(test_payload)
|
||||
end
|
||||
end
|
||||
end
|
||||
116
spec/services/payments/webhook_service_spec.rb
Normal file
116
spec/services/payments/webhook_service_spec.rb
Normal file
@@ -0,0 +1,116 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Payments::WebhookService do
|
||||
let(:order) { create(:completed_order_with_totals, order_cycle: ) }
|
||||
let(:order_cycle) { create(:simple_order_cycle) }
|
||||
let(:payment) { create(:payment, :completed, amount: order.total, order:) }
|
||||
let(:tax_category) { create(:tax_category) }
|
||||
let(:at) { Time.zone.parse("2025-11-26 09:00:02") }
|
||||
|
||||
subject { described_class.create_webhook_job(payment: payment, event: "payment.completed", at:) }
|
||||
|
||||
describe "creating payloads" do
|
||||
context "with order cycle coordinator owner webhook endpoints configured" do
|
||||
before do
|
||||
order.order_cycle.coordinator.owner.webhook_endpoints.payment_status.create!(
|
||||
url: "http://coordinator.payment.url"
|
||||
)
|
||||
end
|
||||
|
||||
it "calls endpoint for the owner if the order cycle coordinator" do
|
||||
expect{ subject }
|
||||
.to enqueue_job(WebhookDeliveryJob).exactly(1).times
|
||||
.with("http://coordinator.payment.url", "payment.completed", any_args)
|
||||
end
|
||||
|
||||
it "creates webhook payload with payment details" do
|
||||
order.line_items.update_all(tax_category_id: tax_category.id)
|
||||
|
||||
enterprise = order.distributor
|
||||
line_items = order.line_items.map do |li|
|
||||
{
|
||||
quantity: li.quantity,
|
||||
price: li.price,
|
||||
tax_category_name: li.tax_category&.name,
|
||||
product_name: li.product.name,
|
||||
name_to_display: li.display_name,
|
||||
unit_to_display: li.unit_presentation
|
||||
}
|
||||
end
|
||||
|
||||
data = {
|
||||
payment: {
|
||||
updated_at: payment.updated_at,
|
||||
amount: payment.amount,
|
||||
state: payment.state
|
||||
},
|
||||
enterprise: {
|
||||
abn: enterprise.abn,
|
||||
acn: enterprise.acn,
|
||||
name: enterprise.name,
|
||||
address: {
|
||||
address1: enterprise.address.address1,
|
||||
address2: enterprise.address.address2,
|
||||
city: enterprise.address.city,
|
||||
zipcode: enterprise.address.zipcode
|
||||
}
|
||||
},
|
||||
order: {
|
||||
total: order.total,
|
||||
currency: order.currency,
|
||||
line_items: line_items
|
||||
}
|
||||
}
|
||||
|
||||
expect{ subject }
|
||||
.to enqueue_job(WebhookDeliveryJob).exactly(1).times
|
||||
.with("http://coordinator.payment.url", "payment.completed", hash_including(data), at:)
|
||||
end
|
||||
|
||||
context "with coordinator manager with webhook endpoint configured" do
|
||||
let(:user1) { create(:user) }
|
||||
let(:user2) { create(:user) }
|
||||
|
||||
before do
|
||||
coordinator = order.order_cycle.coordinator
|
||||
coordinator.users << user1
|
||||
coordinator.users << user2
|
||||
end
|
||||
|
||||
it "calls endpoint for all user managing the order cycle coordinator" do
|
||||
user1.webhook_endpoints.payment_status.create!(url: "http://user1.payment.url")
|
||||
user2.webhook_endpoints.payment_status.create!(url: "http://user2.payment.url")
|
||||
|
||||
expect{ subject }
|
||||
.to enqueue_job(WebhookDeliveryJob)
|
||||
.with("http://coordinator.payment.url", "payment.completed", any_args)
|
||||
.and enqueue_job(WebhookDeliveryJob)
|
||||
.with("http://user1.payment.url", "payment.completed", any_args)
|
||||
.and enqueue_job(WebhookDeliveryJob)
|
||||
.with("http://user2.payment.url", "payment.completed", any_args)
|
||||
end
|
||||
|
||||
context "wiht duplicate webhook endpoints configured" do
|
||||
it "calls each unique configured endpoint" do
|
||||
user1.webhook_endpoints.payment_status.create!(url: "http://coordinator.payment.url")
|
||||
user2.webhook_endpoints.payment_status.create!(url: "http://user2.payment.url")
|
||||
|
||||
expect{ subject }
|
||||
.to enqueue_job(WebhookDeliveryJob)
|
||||
.with("http://coordinator.payment.url", "payment.completed", any_args)
|
||||
.and enqueue_job(WebhookDeliveryJob)
|
||||
.with("http://user2.payment.url", "payment.completed", any_args)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "with no webhook configured" do
|
||||
it "does not call endpoint" do
|
||||
expect{ subject }.not_to enqueue_job(WebhookDeliveryJob)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -55,4 +55,15 @@ RSpec.describe "Tax Categories" do
|
||||
expect(page).to have_content("desc 99")
|
||||
end
|
||||
end
|
||||
|
||||
context "admin deleting a tax category" do
|
||||
it "should be able to delete an existing tax category" do
|
||||
create(:tax_category, name: "To be removed")
|
||||
click_link "Tax Categories"
|
||||
accept_confirm('Are you sure?') do
|
||||
within_row(1) { find(".icon-trash").click }
|
||||
end
|
||||
expect(page).not_to have_content("To be removed")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -111,6 +111,7 @@ RSpec.describe 'Customers' do
|
||||
end
|
||||
end
|
||||
expect(page).not_to have_selector "tr#c_#{customer2.id}"
|
||||
expect(page).not_to have_content 'You have unsaved changes'
|
||||
}.to change{ Customer.count }.by(-1)
|
||||
end
|
||||
|
||||
|
||||
@@ -68,6 +68,48 @@ RSpec.describe '
|
||||
expect(page).to have_checked_field "enterprise_visible_only_through_links"
|
||||
end
|
||||
|
||||
it "deleting an existing enterprise successfully" do
|
||||
enterprise = create(:enterprise)
|
||||
|
||||
user = create(:user)
|
||||
|
||||
admin = login_as_admin
|
||||
|
||||
visit '/admin/enterprises'
|
||||
|
||||
expect do
|
||||
accept_alert do
|
||||
within "tr.enterprise-#{enterprise.id}" do
|
||||
first("a", text: 'Delete').click
|
||||
end
|
||||
end
|
||||
|
||||
expect(page).to have_content("Successfully Removed")
|
||||
end.to change{ Enterprise.count }.by(-1)
|
||||
end
|
||||
|
||||
it "deleting an existing enterprise unsuccessfully" do
|
||||
enterprise = create(:enterprise)
|
||||
create(:order, distributor: enterprise)
|
||||
|
||||
user = create(:user)
|
||||
|
||||
admin = login_as_admin
|
||||
|
||||
visit '/admin/enterprises'
|
||||
|
||||
expect do
|
||||
accept_alert do
|
||||
within "tr.enterprise-#{enterprise.id}" do
|
||||
first("a", text: 'Delete').click
|
||||
end
|
||||
end
|
||||
|
||||
expect(page).to have_content("Cannot delete record because dependent distributed order")
|
||||
expect(page).to have_content(enterprise.name)
|
||||
end.to change{ Enterprise.count }.by(0)
|
||||
end
|
||||
|
||||
it "editing an existing enterprise" do
|
||||
@enterprise = create(:enterprise)
|
||||
e2 = create(:enterprise)
|
||||
|
||||
@@ -604,7 +604,7 @@ RSpec.describe '
|
||||
click_button "Create"
|
||||
|
||||
expect(page).to have_text "Attachment has an invalid content type"
|
||||
expect(page).to have_text "Attachment is not a valid image"
|
||||
expect(page).to have_text "Attachment is not identified as a valid media file"
|
||||
end
|
||||
|
||||
it "deleting product images" do
|
||||
|
||||
@@ -770,12 +770,12 @@ RSpec.describe 'As an enterprise user, I can update my products' do
|
||||
end
|
||||
end
|
||||
|
||||
it 'shows a modal telling not a valid image when uploading a non valid image file' do
|
||||
it 'shows a modal telling not a valid image when uploading an invalid image file' do
|
||||
within ".reveal-modal" do
|
||||
attach_file 'image[attachment]',
|
||||
Rails.public_path.join('invalid_image.jpg'),
|
||||
visible: false
|
||||
expect(page).to have_content /Attachment is not a valid image/
|
||||
expect(page).to have_content /Attachment is not identified as a valid media file/
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -39,15 +39,33 @@ RSpec.describe "Developer Settings" do
|
||||
describe "Webhook Endpoints" do
|
||||
it "creates a new webhook endpoint and deletes it" do
|
||||
within "#webhook_endpoints" do
|
||||
fill_in "webhook_endpoint_url", with: "https://url"
|
||||
within(:table_row, ["Order Cycle Opened"]) do
|
||||
fill_in "order_cycle_opened_webhook_endpoint_url", with: "https://url"
|
||||
|
||||
click_button I18n.t(:create)
|
||||
expect(page.document).to have_content I18n.t('webhook_endpoints.create.success')
|
||||
expect(page).to have_content "https://url"
|
||||
click_button "Create"
|
||||
expect(page.document).to have_content "Webhook endpoint successfully created"
|
||||
expect(page).to have_content "https://url"
|
||||
|
||||
click_button I18n.t(:delete)
|
||||
expect(page.document).to have_content I18n.t('webhook_endpoints.destroy.success')
|
||||
accept_confirm do
|
||||
click_button "Delete"
|
||||
end
|
||||
end
|
||||
expect(page.document).to have_content "Webhook endpoint successfully deleted"
|
||||
expect(page).not_to have_content "https://url"
|
||||
|
||||
within(:table_row, ["Post webhook on Payment status change"]) do
|
||||
fill_in "payment_status_changed_webhook_endpoint_url", with: "https://url/payment"
|
||||
click_button "Create"
|
||||
expect(page.document).to have_content "Webhook endpoint successfully created"
|
||||
expect(page).to have_content "https://url/payment"
|
||||
|
||||
accept_confirm do
|
||||
click_button "Delete"
|
||||
end
|
||||
end
|
||||
|
||||
expect(page.document).to have_content "Webhook endpoint successfully deleted"
|
||||
expect(page).not_to have_content "https://url/payment"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -9621,9 +9621,9 @@ tr46@^5.1.0:
|
||||
punycode "^2.3.1"
|
||||
|
||||
trix@*:
|
||||
version "2.1.15"
|
||||
resolved "https://registry.yarnpkg.com/trix/-/trix-2.1.15.tgz#fabad796ea779a8ae96522402fbc214cbfc4015f"
|
||||
integrity sha512-LoaXWczdTUV8+3Box92B9b1iaDVbxD14dYemZRxi3PwY+AuDm97BUJV2aHLBUFPuDABhxp0wzcbf0CxHCVmXiw==
|
||||
version "2.1.16"
|
||||
resolved "https://registry.yarnpkg.com/trix/-/trix-2.1.16.tgz#601be839258b87cc83019915650c50eb7cbc161e"
|
||||
integrity sha512-XtZgWI+oBvLzX7CWnkIf+ZWC+chL+YG/TkY43iMTV0Zl+CJjn18B1GJUCEWJ8qgfpcyMBuysnNAfPWiv2sV14A==
|
||||
dependencies:
|
||||
dompurify "^3.2.5"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user