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:
Gaetan Craig-Riou
2025-11-28 14:12:50 +11:00
parent 7f961d90c2
commit 4a6ba29b99
2 changed files with 171 additions and 0 deletions

View 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

View 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