Skip to content
Open

Ing 462 #6367

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions app/controllers/api/v1/subscriptions_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,12 @@ def create_params
:payment_method_type,
:payment_method_id
],
connections: [
payment: [:behavior, :code],
tax: [:behavior, :code],
accounting: [:behavior, :code],
crm: [:behavior, :code]
],
usage_thresholds: usage_thresholds_params,
plan_overrides:
)
Expand All @@ -206,6 +212,12 @@ def update_params
:payment_method_type,
:payment_method_id
],
connections: [
payment: [:behavior, :code],
tax: [:behavior, :code],
accounting: [:behavior, :code],
crm: [:behavior, :code]
],
usage_thresholds: usage_thresholds_params,
plan_overrides:
)
Expand Down
24 changes: 24 additions & 0 deletions app/controllers/concerns/wallet_actions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,12 @@ def input_params
payment_method: [
:payment_method_type,
:payment_method_id
],
connections: [
payment: [:behavior, :code],
tax: [:behavior, :code],
accounting: [:behavior, :code],
crm: [:behavior, :code]
]
],
applies_to: [
Expand All @@ -153,6 +159,12 @@ def input_params
payment_method: [
:payment_method_type,
:payment_method_id
],
connections: [
payment: [:behavior, :code],
tax: [:behavior, :code],
accounting: [:behavior, :code],
crm: [:behavior, :code]
]
)
end
Expand Down Expand Up @@ -196,6 +208,12 @@ def update_params
payment_method: [
:payment_method_type,
:payment_method_id
],
connections: [
payment: [:behavior, :code],
tax: [:behavior, :code],
accounting: [:behavior, :code],
crm: [:behavior, :code]
]
],
applies_to: [
Expand All @@ -209,6 +227,12 @@ def update_params
payment_method: [
:payment_method_type,
:payment_method_id
],
connections: [
payment: [:behavior, :code],
tax: [:behavior, :code],
accounting: [:behavior, :code],
crm: [:behavior, :code]
]
)
end
Expand Down
105 changes: 105 additions & 0 deletions app/services/billing_object_connections/attach_to_resource_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# frozen_string_literal: true

module BillingObjectConnections
class AttachToResourceService < BaseService
Result = BaseResult[:billing_object_connections]

INHERIT_BEHAVIOR = "inherit"

def initialize(resource:, params:)
@resource = resource
@params = params
super
end

def call
return result unless params.key?(:connections)
return result if connections.blank?

ActiveRecord::Base.transaction do
connections.each do |category, choice|
next if choice.blank?

apply_choice(category.to_s, choice)
end
end

result.billing_object_connections = resource.billing_object_connections.reload
result
rescue ActiveRecord::RecordInvalid => e
result.record_validation_failure!(record: e.record)
rescue BaseService::FailedResult => e
# raise_if_error! unwinds the transaction on an unresolvable code; the failure is returned
# rather than propagated so `.call` keeps the BaseService contract.
e.result
end

private

attr_reader :resource, :params

def connections
params[:connections]
end

def customer
resource.customer
end

def apply_choice(category, choice)
behavior = choice[:behavior].to_s

if behavior == INHERIT_BEHAVIOR
destroy_override(category)
elsif behavior == BillingObjectConnection::BEHAVIORS[:skip]
upsert_override(category, behavior: :skip, connection: nil)
else
connection = resolve_connection(category, choice[:code])

if connection.nil?
result.single_validation_failure!(field: :connections, error_code: "connection_not_found")
result.raise_if_error!
end

upsert_override(category, behavior: :specific, connection:)
end
end

def destroy_override(category)
resource.billing_object_connections.find_by(category:)&.destroy!
end

def upsert_override(category, behavior:, connection:)
override = resource.billing_object_connections.find_or_initialize_by(category:)

override.organization_id = resource.organization_id
override.behavior = behavior
override.payment_provider_customer = nil
override.integration_customer = nil

if payment?(category)
override.payment_provider_customer = connection
else
override.integration_customer = connection
end

override.save!
end

# Both foreign keys are optional on the model and the category/column pairing lives only in
# ConnectionResolvable, so the mapping is mirrored here.
def resolve_connection(category, code)
return nil if code.blank? || customer.nil?

if payment?(category)
customer.payment_connection(code)
else
customer.integration_customers.find_by(category:, code:)
end
end

def payment?(category)
category == BillingObjectConnection::CATEGORIES[:payment]
end
end
end
70 changes: 70 additions & 0 deletions app/services/billing_object_connections/validate_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# frozen_string_literal: true

module BillingObjectConnections
class ValidateService < BaseValidator
CATEGORIES = BillingObjectConnection::CATEGORIES.values.freeze
# "specific" is never sent: it is implied by supplying a code. "inherit" is params-only
# and means "destroy the override row", since row absence is what ConnectionResolvable
# reads as inheritance.
BEHAVIORS = %w[inherit skip].freeze

# Pure validator: it accumulates error codes and leaves surfacing them to the caller, because
# the wallet create path merges them into an accumulating validator while the update and
# recurring-rule paths fail the result directly.
def valid?
validate_connections if connections.present?

!errors?
end

def error_codes
errors[:connections] || []
end

private

def connections
args[:connections]
end

def validate_connections
unless connections.is_a?(Hash)
add_error(field: :connections, error_code: "invalid_connections")
return
end

connections.each do |category, choice|
validate_category(category)
validate_choice(choice)
end
end

def validate_category(category)
return true if CATEGORIES.include?(category.to_s)

add_error(field: :connections, error_code: "invalid_connection_category")
end

def validate_choice(choice)
unless choice.is_a?(Hash)
add_error(field: :connections, error_code: "invalid_connection_choice")
return
end

code = choice[:code]
behavior = choice[:behavior]

if code.present? && behavior.present?
return add_error(field: :connections, error_code: "invalid_connection_choice")
end

if code.blank? && behavior.blank?
return add_error(field: :connections, error_code: "invalid_connection_choice")
end

return true if behavior.blank? || BEHAVIORS.include?(behavior.to_s)

add_error(field: :connections, error_code: "invalid_connection_behavior")
end
end
end
14 changes: 14 additions & 0 deletions app/services/subscriptions/create_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,14 @@ def call
subscription_at:,
ending_at: params[:ending_at],
payment_method: params[:payment_method],
connections: params[:connections],
activation_rules: params[:activation_rules],
subscription_type:,
consolidate_invoice: params[:consolidate_invoice],
consolidate_invoice_provided: params.key?(:consolidate_invoice)
)
return result.forbidden_failure! if !License.premium? && params.key?(:plan_overrides)
return result.forbidden_failure! if connections_requested? && organization_flag_disabled?(:multi_connection)

if params.key?(:plan_overrides) && plan.organization.product_catalog_enabled?
return result.single_validation_failure!(field: :plan_overrides, error_code: "legacy_billing_disabled")
Expand Down Expand Up @@ -83,6 +85,10 @@ def call
end
InvoiceCustomSections::AttachToResourceService.call(resource: subscription, params:) unless downgrade?

if connections_requested?
BillingObjectConnections::AttachToResourceService.call!(resource: subscription, params:)
end

result.subscription = subscription
end
end
Expand Down Expand Up @@ -114,6 +120,14 @@ def valid?(args)
Subscriptions::ValidateService.new(result, **args).valid?
end

def connections_requested?
params[:connections].present?
end

def organization_flag_disabled?(flag)
!customer.organization.feature_flag_enabled?(flag)
end

def handle_subscription
return upgrade_subscription if upgrade?
return downgrade_subscription if downgrade?
Expand Down
14 changes: 14 additions & 0 deletions app/services/subscriptions/update_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ def call
on_termination_credit_note: params[:on_termination_credit_note],
on_termination_invoice: params[:on_termination_invoice],
payment_method: params[:payment_method],
connections: params[:connections],
activation_rules: params[:activation_rules],
subscription_type: "update",
subscription:,
Expand All @@ -55,6 +56,7 @@ def call
end

return result.forbidden_failure! if !License.premium? && params.key?(:plan_overrides)
return result.forbidden_failure! if connections_requested? && organization_flag_disabled?(:multi_connection)

if params.key?(:plan_overrides) && subscription.plan.organization.product_catalog_enabled?
return result.single_validation_failure!(field: :plan_overrides, error_code: "legacy_billing_disabled")
Expand Down Expand Up @@ -123,6 +125,10 @@ def call
end

InvoiceCustomSections::AttachToResourceService.call(resource: subscription, params:)

if connections_requested?
BillingObjectConnections::AttachToResourceService.call!(resource: subscription, params:)
end
end

result.subscription = subscription
Expand Down Expand Up @@ -287,6 +293,14 @@ def valid?(args)
Subscriptions::ValidateService.new(result, **args).valid?
end

def connections_requested?
params[:connections].present?
end

def organization_flag_disabled?(flag)
!subscription.organization.feature_flag_enabled?(flag)
end

def payment_method
return @payment_method if defined? @payment_method
return nil if params[:payment_method].blank? || params[:payment_method][:payment_method_id].blank?
Expand Down
12 changes: 12 additions & 0 deletions app/services/subscriptions/validate_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ def valid?
valid_on_termination_credit_note?
valid_on_termination_invoice?
valid_payment_method?
valid_connections?
valid_activation_rules?
valid_consolidate_invoice?

Expand Down Expand Up @@ -101,6 +102,17 @@ def valid_payment_method?
false
end

def valid_connections?
return true if args[:connections].blank?

validator = BillingObjectConnections::ValidateService.new(result, connections: args[:connections])
return true if validator.valid?

validator.error_codes.each { |error_code| add_error(field: :connections, error_code:) }

false
end

def valid_activation_rules?
return true unless args[:activation_rules]

Expand Down
15 changes: 15 additions & 0 deletions app/services/wallets/create_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ def call
result.payment_method = payment_method

return result unless valid?
return result.forbidden_failure! if connections_requested? && organization_flag_disabled?(:multi_connection)

code = params[:code]

Expand Down Expand Up @@ -88,6 +89,10 @@ def call
InvoiceCustomSections::AttachToResourceService.call(resource: wallet, params:)
end

if connections_requested?
BillingObjectConnections::AttachToResourceService.call!(resource: wallet, params:)
end

billable_metrics.each do |bm|
WalletTarget.create!(wallet:, billable_metric: bm, organization_id:)
end
Expand Down Expand Up @@ -163,6 +168,16 @@ def organization_flag_enabled?(flag)
customer.organization.feature_flag_enabled?(flag)
end

def organization_flag_disabled?(flag)
!organization_flag_enabled?(flag)
end

def connections_requested?
return true if params[:connections].present?

Array(params[:recurring_transaction_rules]).any? { |rule| rule[:connections].present? }
end

def valid?
Wallets::ValidateService.new(result, **params).valid?
end
Expand Down
Loading
Loading