From 5081ffc17562fa90edf0f2a8dfa0d03991480362 Mon Sep 17 00:00:00 2001 From: Alessandro Desantis Date: Thu, 19 Nov 2020 13:13:46 +0100 Subject: Streamline and simplify `SolidusSubscriptions::Processor` The processor was way too complicated, taking on too many responsibilities. As a result, it was complex to reason about and it was a very brittle part of the extension. The new processor simply does two things: it finds all actionable subscriptions and ensures to take the appropriate action on them (e.g., cancel, deactivate), including creating the new installment for later. Then, it finds all the actionable installments (including the ones that were just created), and schedules them for asynchronous processing. One side effect of this refactoring is that installments are not grouped by address and payment method/source anymore: each installment will always correspond to a new order. Any logistics-related optimizations should be implemented by the individual store. --- lib/solidus_subscriptions/processor.rb | 110 +++------------------ .../factories/installment_factory.rb | 4 + 2 files changed, 19 insertions(+), 95 deletions(-) (limited to 'lib') diff --git a/lib/solidus_subscriptions/processor.rb b/lib/solidus_subscriptions/processor.rb index d56816a..4547cec 100644 --- a/lib/solidus_subscriptions/processor.rb +++ b/lib/solidus_subscriptions/processor.rb @@ -1,116 +1,36 @@ # frozen_string_literal: true -# This class is responsible for finding subscriptions and installments -# which need to be processed. It will group them together by user and attempts -# to process them together. Subscriptions will also be grouped by their -# shiping address id. -# -# This class passes the reponsibility of actually creating the order off onto -# the consolidated installment class. -# -# This class generates `ProcessInstallmentsJob`s module SolidusSubscriptions class Processor class << self - # Find all actionable subscriptions and intallments, group them together - # by user, and schedule a processing job for the group as a batch def run - batched_users_to_be_processed.each { |batch| new(batch).build_jobs } + SolidusSubscriptions::Subscription.actionable.find_each(&method(:process_subscription)) + SolidusSubscriptions::Installment.actionable.with_active_subscription.find_each(&method(:process_installment)) end private - def batched_users_to_be_processed - subscriptions = SolidusSubscriptions::Subscription.arel_table - installments = SolidusSubscriptions::Installment.arel_table + def process_subscription(subscription) + ActiveRecord::Base.transaction do + subscription.successive_skip_count = 0 + subscription.advance_actionable_date - ::Spree.user_class. - joins(:subscriptions). - joins( - subscriptions. - join(installments, Arel::Nodes::OuterJoin). - on(subscriptions[:id].eq(installments[:subscription_id])). - join_sources - ). - where( - SolidusSubscriptions::Subscription.actionable.arel.constraints.reduce(:and). - or(SolidusSubscriptions::Installment.actionable.with_active_subscription.arel.constraints.reduce(:and)) - ). - distinct. - find_in_batches - end - end - - # @return [Array] - attr_reader :users - - # Get a new instance of the SolidusSubscriptions::Processor - # - # @param users [Array] A list of users with actionable - # subscriptions or installments - # - # @return [SolidusSubscriptions::Processor] - def initialize(users) - @users = users - @installments = {} - end - - # Create `ProcessInstallmentsJob`s for the users used to initalize the - # instance - def build_jobs - users.map do |user| - installemts_by_address_and_user = installments(user).group_by do |i| - [i.subscription.shipping_address_id, i.subscription.billing_address_id] - end - - installemts_by_address_and_user.each_value do |grouped_installments| - ProcessInstallmentsJob.perform_later grouped_installments.map(&:id) - end - end - end + subscription.cancel! if subscription.pending_cancellation? + subscription.deactivate! if subscription.can_be_deactivated? - private - - def subscriptions_by_id - @subscriptions_by_id ||= Subscription. - actionable. - includes(:line_items, :user). - where(user_id: user_ids). - group_by(&:user_id) - end - - def retry_installments - @failed_installments ||= Installment. - actionable. - includes(:subscription). - where(solidus_subscriptions_subscriptions: { user_id: user_ids }). - group_by { |i| i.subscription.user_id } - end - - def installments(user) - @installments[user.id] ||= retry_installments.fetch(user.id, []) + new_installments(user) - end - - def new_installments(user) - ActiveRecord::Base.transaction do - subscriptions_by_id.fetch(user.id, []).map do |sub| - sub.successive_skip_count = 0 - sub.advance_actionable_date - sub.cancel! if sub.pending_cancellation? - sub.deactivate! if sub.can_be_deactivated? if SolidusSubscriptions.configuration.clear_past_installments - sub.installments.unfulfilled.each do |installment| - installment.actionable_date = nil - installment.save! + subscription.installments.unfulfilled.actionable.each do |installment| + installment.update!(actionable_date: nil) end end - sub.installments.create! + + subscription.installments.create!(actionable_date: Time.zone.now) end end - end - def user_ids - @user_ids ||= users.map(&:id) + def process_installment(installment) + ProcessInstallmentsJob.perform_later(installment) + end end end end diff --git a/lib/solidus_subscriptions/testing_support/factories/installment_factory.rb b/lib/solidus_subscriptions/testing_support/factories/installment_factory.rb index 2eb5069..20dcc04 100644 --- a/lib/solidus_subscriptions/testing_support/factories/installment_factory.rb +++ b/lib/solidus_subscriptions/testing_support/factories/installment_factory.rb @@ -21,5 +21,9 @@ FactoryBot.define do [association(:installment_detail, :success, installment: @instance, order: order)] end end + + trait :actionable do + actionable_date { Time.zone.now } + end end end -- cgit v1.2.3