๐Ÿ“ฆ obafemitayor / newsletter-subscription-application

๐Ÿ“„ subscription_service.rb ยท 99 lines
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99# frozen_string_literal: true

class SubscriptionService
  class << self
    def create_subscription(work_email:, first_name:, last_name:, category_guids:)
      return if category_guids.blank?

      ActiveRecord::Base.transaction do
        customer = Customer.find_or_create_by!(work_email: work_email) do |c|
          c.first_name = first_name
          c.last_name = last_name
          c.guid = SecureRandom.uuid
        end
        subscriptions = prepare_subscription_data(customer, category_guids)

        # ignoring rubocop warning because I am positive I have
        # validated the data before insertion so this is safe
        # rubocop:disable Rails/SkipsModelValidations
        Subscription.insert_all!(subscriptions)
        # rubocop:enable Rails/SkipsModelValidations
      end
    end

    def get_subscriptions(category_guids: nil, pagination_id: nil, pagination_direction: 'forward', limit: 10)
      data_size = limit + 1
      subscriptions = fetch_subscriptions_from_db(
        category_guids: category_guids,
        limit: data_size,
        pagination_id: pagination_id,
        pagination_direction: pagination_direction
      )
      subscription_list = build_subscription_list(subscriptions, limit, pagination_direction)

      {
        subscriptions: subscription_list.as_json(except: :id),
        previous_cursor: subscription_list.first&.id,
        next_cursor: subscription_list.last&.id,
        has_more: subscriptions.size > limit
      }
    end

    private

    def get_unsubscribed_category_ids(customer, category_guids)
      subscribed_guids = customer.subscriptions.joins(:category).pluck('categories.guid')
      unsubscribed_category_guids = category_guids - subscribed_guids
      Category.where(guid: unsubscribed_category_guids).pluck(:id)
    end

    def prepare_subscription_data(customer, category_guids)
      unsubscribed_category_ids = get_unsubscribed_category_ids(customer, category_guids)
      # This is a very rare edge case but adding it just to be safe.
      raise ActiveRecord::Rollback if unsubscribed_category_ids.empty?

      unsubscribed_category_ids.map do |category_id|
        {
          customer_id: customer.id,
          category_id: category_id,
          guid: SecureRandom.uuid,
          created_at: Time.current,
          updated_at: Time.current
        }
      end
    end

    def fetch_subscriptions_from_db(limit:, category_guids: nil, pagination_id: nil, pagination_direction: 'forward')
      subscriptions = Subscription.active
                                  .joins(:customer, :category)
                                  .select('subscriptions.id',
                                          'customers.work_email',
                                          'customers.first_name',
                                          'customers.last_name',
                                          'categories.name as category_name')

      subscriptions = subscriptions.where(categories: { guid: category_guids }) if category_guids.present?

      # using cursor-based pagination because it is more efficient and
      # scales better with large data sets than offset-based pagination
      if pagination_id.present?
        subscriptions = subscriptions.where(
          pagination_direction == 'forward' ? 'subscriptions.id > ?' : 'subscriptions.id < ?', pagination_id
        )
      end
      subscriptions = subscriptions.order(id: pagination_direction == 'forward' ? :asc : :desc).limit(limit)

      # Reverse the order when pagination direction is backward to ensure
      # the value of previous cursor is correct
      pagination_direction == 'backward' ? subscriptions.reverse : subscriptions
    end

    def build_subscription_list(subscriptions, limit, pagination_direction)
      return subscriptions.take(limit) if pagination_direction == 'forward'
      return subscriptions.drop(1).take(limit) if subscriptions.size > limit

      subscriptions.take(limit)
    end
  end
end