mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-01-24 20:36:49 +00:00
Add Payments::WebhookService
It enqueues jobs to post the generated payload to the various configured webhook endpoints for payment status change
This commit is contained in:
47
app/services/payments/webhook_service.rb
Normal file
47
app/services/payments/webhook_service.rb
Normal file
@@ -0,0 +1,47 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Create a webhook payload for an payment status event.
|
||||
# The payload will be delivered asynchronously.
|
||||
|
||||
module Payments
|
||||
class WebhookService
|
||||
def self.create_webhook_job(payment:, event:, at:)
|
||||
order = payment.order
|
||||
enterprise = order.distributor
|
||||
|
||||
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
|
||||
|
||||
payload = {
|
||||
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)
|
||||
}
|
||||
|
||||
coordinator = 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.map(&:url)
|
||||
|
||||
# plus url for coordinator manager (ignore duplicate)
|
||||
users_webhook_urls = coordinator.users.flat_map do |user|
|
||||
user.webhook_endpoints.payment_status.map(&:url)
|
||||
end
|
||||
|
||||
webhook_urls | users_webhook_urls
|
||||
end
|
||||
end
|
||||
end
|
||||
124
spec/services/payments/webhook_service_spec.rb
Normal file
124
spec/services/payments/webhook_service_spec.rb
Normal file
@@ -0,0 +1,124 @@
|
||||
# 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.create!(
|
||||
url: "http://coordinator.payment.url", webhook_type: "payment_status_changed"
|
||||
)
|
||||
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 # TODO check this
|
||||
}
|
||||
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.create!(
|
||||
url: "http://user1.payment.url", webhook_type: "payment_status_changed"
|
||||
)
|
||||
user2.webhook_endpoints.create!(
|
||||
url: "http://user2.payment.url", webhook_type: "payment_status_changed"
|
||||
)
|
||||
|
||||
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.create!(
|
||||
url: "http://coordinator.payment.url", webhook_type: "payment_status_changed"
|
||||
)
|
||||
user2.webhook_endpoints.create!(
|
||||
url: "http://user2.payment.url", webhook_type: "payment_status_changed"
|
||||
)
|
||||
|
||||
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
|
||||
Reference in New Issue
Block a user