Created
November 22, 2024 13:34
-
-
Save LeonidFilbert/bee73a53bc94745c71fa2589ae84694b to your computer and use it in GitHub Desktop.
EXAMPLE: Command pattern -> Loyaltie
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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