mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-02-27 01:43:22 +00:00
Merge pull request #13777 from rioug/13481-webhook-payment
Payment status change webhook
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user