summaryrefslogtreecommitdiff
path: root/app/models/solidus_subscriptions/installment.rb
blob: 0b5f62772a60960ec147d308cefce32a939ebb33 (plain)
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
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
# frozen_string_literal: true

# This class represents a single iteration of a subscription. It is fulfilled
# by a completed order and maintains an association which tracks all attempts
# successful or otherwise at fulfilling this installment
module SolidusSubscriptions
  class Installment < ApplicationRecord
    has_many :details, class_name: 'SolidusSubscriptions::InstallmentDetail'
    belongs_to(
      :subscription,
      class_name: 'SolidusSubscriptions::Subscription',
      inverse_of: :installments,
    )

    validates :subscription, presence: true

    scope :fulfilled, (lambda do
      joins(:details).where(InstallmentDetail.table_name => { success: true }).distinct
    end)

    scope :unfulfilled, (lambda do
      fulfilled_ids = fulfilled.pluck(:id)
      where.not(id: fulfilled_ids).distinct
    end)

    scope :with_active_subscription, (lambda do
      joins(:subscription).where.not(Subscription.table_name => { state: "canceled" })
    end)

    scope :actionable, (lambda do
      unfulfilled.where("#{table_name}.actionable_date <= ?", Time.zone.today)
    end)

    # Get the builder for the subscription_line_item. This will be an
    # object that can generate the appropriate line item for the subscribable
    # object
    #
    # @return [SolidusSubscriptions::LineItemBuilder]
    delegate :line_item_builder, to: :subscription

    # Mark this installment as out of stock.
    #
    # @return [SolidusSubscriptions::InstallmentDetail] The record of the failed
    #   processing attempt
    def out_of_stock
      advance_actionable_date!

      details.create!(
        success: false,
        message: I18n.t('solidus_subscriptions.installment_details.out_of_stock')
      )
    end

    # Mark this installment as a success
    #
    # @param order [Spree::Order] The order generated for this processing
    #   attempt
    #
    # @return [SolidusSubscriptions::InstallmentDetail] The record of the
    #   successful processing attempt
    def success!(order)
      update!(actionable_date: nil)

      details.create!(
        success: true,
        order: order,
        message: I18n.t('solidus_subscriptions.installment_details.success')
      )
    end

    # Mark this installment as a failure
    #
    # @param order [Spree::Order] The order generated for this processing
    #   attempt
    #
    # @return [SolidusSubscriptions::InstallmentDetail] The record of the
    #   failed processing attempt
    def failed!(order)
      advance_actionable_date!

      details.create!(
        success: false,
        order: order,
        message: I18n.t('solidus_subscriptions.installment_details.failed')
      )
    end

    # Does this installment still need to be fulfilled by a completed order
    #
    # @return [Boolean]
    def unfulfilled?
      !fulfilled?
    end

    # Had this installment been fulfilled by a completed order
    #
    # @return [Boolean]
    def fulfilled?
      details.exists?(success: true)
    end

    # Returns the state of this fulfillment
    #
    # @return [Symbol] :fulfilled/:unfulfilled
    def state
      fulfilled? ? :fulfilled : :unfulfilled
    end

    # Mark this installment as having a failed payment
    #
    # @param order [Spree::Order] The order generated for this processing
    #   attempt
    #
    # @return [SolidusSubscriptions::InstallmentDetail] The record of the
    #   failed processing attempt
    def payment_failed!(order)
      details.create!(
        success: false,
        order: order,
        message: I18n.t('solidus_subscriptions.installment_details.payment_failed')
      )

      if subscription.maximum_reprocessing_time_reached? && !subscription.canceled?
        subscription.force_cancel!
        update!(actionable_date: nil)
      else
        advance_actionable_date!
      end
    end

    private

    def advance_actionable_date!
      update!(actionable_date: next_actionable_date)
    end

    def next_actionable_date
      return if SolidusSubscriptions.configuration.reprocessing_interval.nil?

      (DateTime.current + SolidusSubscriptions.configuration.reprocessing_interval).beginning_of_minute
    end
  end
end