Skip to content

Instantly share code, notes, and snippets.

@LeonidFilbert
Created November 22, 2024 13:34
Show Gist options
  • Save LeonidFilbert/bee73a53bc94745c71fa2589ae84694b to your computer and use it in GitHub Desktop.
Save LeonidFilbert/bee73a53bc94745c71fa2589ae84694b to your computer and use it in GitHub Desktop.
EXAMPLE: Command pattern -> Loyaltie
# frozen_string_literal: true
#app/commands/application_command.rb
class ApplicationCommand
include ActiveModel::Model
include ActiveModel::Validations
SENSITIVE_FIELDS = %i[password token credit_card_number].freeze
ERROR_ATTRIBUTES = %i[command oauth_service].freeze
def self.call(*args)
new(*args).call
end
def call
raise NotImplementedError
end
def errors
@errors ||= ActiveModel::Errors.new(self)
end
def failure?
status?(:failure)
end
def initialize(*args)
super
self.status = :pending
end
def status?(value)
ensure_execution!
status == value.to_sym
end
def success?
status?(:success)
end
def transaction(&block)
ActiveRecord::Base.transaction(&block) if block_given?
end
def read_attribute_for_validation(attr_name)
if attr_name.in?(ERROR_ATTRIBUTES)
self
else
public_send(attr_name)
end
end
def self.i18n_scope
:commands
end
protected
attr_accessor :status
def ensure_empty_errors!
return if errors.empty?
error!("`#{self.class}` contains errors.")
end
def ensure_execution!
return if status.present?
error!("`#{self.class}` was not executed yet.")
end
def ensure_finished_status!
return unless %i[failure success].include? status
error!("`#{self.class}` was already executed.")
end
def error!(message = nil)
message ||= "`#{self.class}` raised error."
if defined?(Sentry)
Sentry.capture_message(
message,
extra: {
command: self.class.name,
params: masked_params,
user: masked_user_info,
status:,
errors: errors.full_messages.join(', ')
}
)
end
raise ApplicationError, message
end
def failure!(message = '', attribute = :command)
ensure_finished_status!
errors.add(attribute, :failed, message:) if message.present?
if defined?(Sentry)
Sentry.capture_message(
message.to_s,
extra: {
command: self.class.name,
params: masked_params,
user: masked_user_info,
status:,
errors: errors.full_messages,
line: caller_locations(1, 1)[0]&.lineno
}
)
end
self.status = :failure
self
end
def success!
ensure_empty_errors!
ensure_finished_status!
self.status = :success
self
end
private
def masked_params
return if @params.blank?
permit_params(@params).to_h.transform_values do |v|
if SENSITIVE_FIELDS.include?(v)
'[FILTERED]'
else
v
end
end
end
def masked_user_info
return if @user.blank?
{
id: @user.id,
email: @user.email,
name: @user.full_name
}
end
def permit_params(params)
params.permit!
rescue ActionController::UnfilteredParameters
{}
end
end
# frozen_string_literal: true
#app/commands/stripe/subscription_webhook/handle_paused_command.rb
module Stripe
module SubscriptionWebhook
class HandlePausedCommand < Stripe::SubscriptionWebhookCommand
def call
return error! unless valid? && subscription_paused_event?
extract_data_from_event
return self if failure?
pause_local_subscription
return self if failure?
success!
end
private
def pause_local_subscription
subscription.hold! if subscription_status == 'paused'
rescue StandardError => e
failure!(e.message)
end
def subscription_paused_event?
event[:type] == SUBSCRIPTION_PAUSED_EVENT
end
end
end
end
# frozen_string_literal: true
#app/commands/subscribe_user_command.rb
class SubscribeUserCommand < ApplicationCommand
attr_accessor :user, :params, :with_activation, :onboarding
attr_reader :subscription_plan
validates :user, :params, presence: true
validates :with_activation, :onboarding, inclusion: { in: [true, false] }
def call
return error! unless valid? && user.member?
transaction do
find_subscription_plan
return failure!(I18n.t('errors.messages.subscription_plan.invalid_or_expired')) unless subscription_plan&.active?
validate_unique_subscription
return self if failure?
validate_currency_matches
return self if failure?
handle_payment_method
return self if failure?
create_subscription
return self if failure?
success!
end
rescue StandardError => e
failure!(e.message) unless failure?
end
private
def find_subscription_plan
@subscription_plan = SubscriptionPlan.find_by(id: params[:id] || params[:subscription_plan_id])
end
def handle_payment_method
result = PaymentService.export_payment_method(
user,
payment_params[:payment_method_id],
payment_params[:payment_token],
payment_params[:payment_provider] || DEFAULT_PAYMENT_PROVIDER,
subscription_plan.business_id,
onboarding:
)
handle_command_result(result)
end
def create_subscription
result = PaymentService.accept_subscription(
payment_params[:accepted_terms],
subscription_plan,
user,
payment_params[:payment_provider] || DEFAULT_PAYMENT_PROVIDER,
with_activation:
)
handle_command_result(result)
end
def payment_params
params.require(:payment_info).permit(:accepted_terms, :payment_method_id, :payment_provider, :payment_token)
end
def handle_command_result(result)
return result if result.success?
failure!(
I18n.t(
'commands.failures.execution_failed',
initiator: result.class.name,
errors: result.errors.full_messages.to_sentence
)
)
end
def validate_currency_matches
return true if user.current_currency.nil? || user.current_currency == subscription_plan.currency
failure!(I18n.t('commands.failures.user.currency_mismatch'))
end
def validate_unique_subscription
return true unless user.subscriptions.not_canceled.for_business(subscription_plan.business.id).exists?
# TODO: adjust error to be aligned with the condition: if there any subscription with the same business
failure!(I18n.t('activerecord.errors.models.subscription.attributes.subscription_plan.already_been_taken'))
end
end
# frozen_string_literal: true
#app/commands/stripe/subscription_webhook_command.rb
module Stripe
class SubscriptionWebhookCommand < StripeCommand
attr_accessor :event
attr_reader :subscription, :subscription_status, :user
validates :event, presence: true
private
def extract_data_from_event
subscription_uid = event[:subscription_id]
customer_uid = event[:customer_id]
@subscription_status = event[:subscription_status]
@subscription = ::Subscription.by_stripe_uid(subscription_uid).first
@user = User.find_by(stripe_uid: customer_uid)
return if [subscription, user, subscription_status].all?(&:present?)
failure!(I18n.t('commands.failures.not_found', data: 'subscription, user, status'))
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment