solidus_subscriptions-alpha 0.0.4 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +26 -26
  3. data/README.md +130 -130
  4. data/Rakefile +28 -28
  5. data/app/assets/javascripts/spree/backend/solidus_subscriptions.js +1 -1
  6. data/app/assets/javascripts/spree/frontend/solidus_subscriptions.js +1 -1
  7. data/app/assets/stylesheets/spree/backend/solidus_subscriptions.css +4 -4
  8. data/app/assets/stylesheets/spree/frontend/solidus_subscriptions.css +4 -4
  9. data/app/controllers/solidus_subscriptions/api/v1/line_items_controller.rb +35 -35
  10. data/app/controllers/solidus_subscriptions/api/v1/subscriptions_controller.rb +44 -44
  11. data/app/controllers/spree/admin/subscriptions_controller.rb +66 -66
  12. data/app/decorators/spree/controllers/api/line_items/create_subscription_line_items.rb +28 -28
  13. data/app/decorators/spree/controllers/orders/create_subscription_line_items.rb +33 -33
  14. data/app/decorators/spree/line_items/subscription_line_items_association.rb +22 -22
  15. data/app/decorators/spree/orders/after_create.rb +15 -15
  16. data/app/decorators/spree/orders/finalize_creates_subscriptions.rb +19 -19
  17. data/app/decorators/spree/orders/subscription_line_items_association.rb +15 -15
  18. data/app/decorators/spree/products/subscribable_decorator.rb +11 -11
  19. data/app/decorators/spree/users/have_many_subscriptions.rb +18 -18
  20. data/app/decorators/spree/variant_pretty_name.rb +13 -13
  21. data/app/jobs/solidus_subscriptions/process_installments_job.rb +22 -22
  22. data/app/models/solidus_subscriptions/checkout.rb +141 -141
  23. data/app/models/solidus_subscriptions/dispatcher.rb +32 -32
  24. data/app/models/solidus_subscriptions/failure_dispatcher.rb +19 -19
  25. data/app/models/solidus_subscriptions/installment.rb +126 -126
  26. data/app/models/solidus_subscriptions/installment_detail.rb +23 -23
  27. data/app/models/solidus_subscriptions/interval.rb +24 -24
  28. data/app/models/solidus_subscriptions/line_item.rb +98 -98
  29. data/app/models/solidus_subscriptions/line_item_builder.rb +44 -44
  30. data/app/models/solidus_subscriptions/order_builder.rb +40 -40
  31. data/app/models/solidus_subscriptions/out_of_stock_dispatcher.rb +19 -19
  32. data/app/models/solidus_subscriptions/payment_failed_dispatcher.rb +23 -23
  33. data/app/models/solidus_subscriptions/subscription.rb +217 -217
  34. data/app/models/solidus_subscriptions/subscription_generator.rb +60 -60
  35. data/app/models/solidus_subscriptions/subscription_line_item_builder.rb +21 -21
  36. data/app/models/solidus_subscriptions/subscription_order_promotion_rule.rb +25 -25
  37. data/app/models/solidus_subscriptions/subscription_promotion_rule.rb +38 -38
  38. data/app/models/solidus_subscriptions/success_dispatcher.rb +16 -16
  39. data/app/models/solidus_subscriptions/unsubscribable_error.rb +17 -17
  40. data/app/models/solidus_subscriptions/user_mismatch_error.rb +15 -15
  41. data/app/overrides/views/admin_subscribable_checkbox.rb +13 -13
  42. data/app/overrides/views/admin_subscriptions_menu_link.rb +8 -8
  43. data/app/overrides/views/subscription_line_item_fields.rb +6 -6
  44. data/app/views/spree/admin/products/_subscribable_checkbox.html.erb +8 -8
  45. data/app/views/spree/admin/promotions/rules/_subscription_order_promotion_rule.html.erb +0 -0
  46. data/app/views/spree/admin/promotions/rules/_subscription_promotion_rule.html.erb +0 -0
  47. data/app/views/spree/admin/shared/_no_objects_found.html.erb +4 -4
  48. data/app/views/spree/admin/shared/_subscription_tab.html.erb +3 -3
  49. data/app/views/spree/admin/solidus_subscriptions/subscriptions/_subscription.html.erb +66 -66
  50. data/app/views/spree/admin/subscriptions/_form.html.erb +81 -81
  51. data/app/views/spree/admin/subscriptions/_legacy_form.html.erb +81 -81
  52. data/app/views/spree/admin/subscriptions/_legacy_sidebar.html.erb +28 -28
  53. data/app/views/spree/admin/subscriptions/edit.html.erb +21 -21
  54. data/app/views/spree/admin/subscriptions/index.html.erb +119 -119
  55. data/app/views/spree/admin/subscriptions/new.html.erb +9 -9
  56. data/app/views/spree/admin/variants/_subscribable_checkbox.html.erb +6 -6
  57. data/app/views/spree/frontend/products/_subscription_line_item_fields.html.erb +30 -30
  58. data/config/locales/en.yml +91 -91
  59. data/config/routes.rb +25 -25
  60. data/db/migrate/20160825164850_create_solidus_subscriptions_subscriptions.rb +11 -11
  61. data/db/migrate/20160825173548_create_solidus_subscriptions_line_items.rb +17 -17
  62. data/db/migrate/20160825202248_create_solidus_subscriptions_installments.rb +23 -23
  63. data/db/migrate/20160825211202_create_solidus_subscriptions_installment_details.rb +22 -22
  64. data/db/migrate/20160825214240_add_subscribable_to_spree_variants.rb +5 -5
  65. data/db/migrate/20160829201653_change_subscription_line_items_installments_to_max_installments.rb +5 -5
  66. data/db/migrate/20160902220242_remove_state_from_solidus_susbscriptions_installment_details.rb +5 -5
  67. data/db/migrate/20160902220604_add_successful_to_solidus_subscriptions_installment_details.rb +5 -5
  68. data/db/migrate/20160902221218_add_message_to_solidus_subscriptions_installment_details.rb +5 -5
  69. data/db/migrate/20160922164101_add_interval_length_and_units_to_subscription_line_items.rb +8 -8
  70. data/db/migrate/20161006191003_add_skip_count_to_solidus_subscriptions_subscriptions.rb +5 -5
  71. data/db/migrate/20161006191127_add_successive_skip_count_to_solidus_subscriptions_subscriptions.rb +5 -5
  72. data/db/migrate/20161014212649_allow_spree_line_item_id_to_be_null.rb +5 -5
  73. data/db/migrate/20161017155749_add_order_id_to_solidus_subscriptions_installment_details.rb +6 -6
  74. data/db/migrate/20161017175509_remove_order_id_from_solidus_subscriptions_installments.rb +6 -6
  75. data/db/migrate/20161017201944_add_subscription_order_to_spree_orders.rb +5 -5
  76. data/db/migrate/20161221155142_add_store_to_solidus_subscriptions_subscriptions.rb +6 -6
  77. data/db/migrate/20161223152905_add_address_id_to_solidus_subscriptions_subscriptions.rb +7 -7
  78. data/db/migrate/20170106224713_change_line_item_max_installments_to_end_date.rb +6 -6
  79. data/db/migrate/20170111224458_change_subscription_actionable_date_to_datetime.rb +5 -5
  80. data/db/migrate/20170111232801_change_inteval_actionable_date_to_datetime.rb +5 -5
  81. data/db/migrate/20170112012407_add_config_options_to_subscriptions.rb +7 -7
  82. data/lib/generators/solidus_subscriptions/install/install_generator.rb +30 -30
  83. data/lib/solidus_subscriptions.rb +6 -6
  84. data/lib/solidus_subscriptions/ability.rb +19 -19
  85. data/lib/solidus_subscriptions/config.rb +97 -97
  86. data/lib/solidus_subscriptions/engine.rb +56 -56
  87. data/lib/solidus_subscriptions/permitted_attributes.rb +36 -36
  88. data/lib/solidus_subscriptions/processor.rb +108 -108
  89. data/lib/solidus_subscriptions/testing_support/factories.rb +5 -5
  90. data/lib/solidus_subscriptions/testing_support/factories/installment_detail_factory.rb +7 -7
  91. data/lib/solidus_subscriptions/testing_support/factories/installment_factory.rb +21 -21
  92. data/lib/solidus_subscriptions/testing_support/factories/line_item_factory.rb +18 -18
  93. data/lib/solidus_subscriptions/testing_support/factories/spree/line_item_factory.rb +17 -17
  94. data/lib/solidus_subscriptions/testing_support/factories/spree/order_factory.rb +18 -18
  95. data/lib/solidus_subscriptions/testing_support/factories/spree_modification_factory.rb +8 -8
  96. data/lib/solidus_subscriptions/testing_support/factories/subscription_factory.rb +43 -43
  97. data/lib/solidus_subscriptions/version.rb +3 -3
  98. data/lib/solidus_subscriptions/version.rb~ +3 -0
  99. data/lib/tasks/process_subscriptions.rake +6 -6
  100. metadata +4 -3
@@ -1,19 +1,19 @@
1
- # A handler for behaviour that should happen after installments are marked as
2
- # failures
3
- module SolidusSubscriptions
4
- class FailureDispatcher < Dispatcher
5
- def dispatch
6
- order.touch :completed_at
7
- order.cancel!
8
- installments.each { |i| i.failed!(order) }
9
- super
10
- end
11
-
12
- def message
13
- "
14
- Something went wrong processing installments: #{installments.map(&:id).join(', ')}.
15
- They have been marked for reprocessing.
16
- "
17
- end
18
- end
19
- end
1
+ # A handler for behaviour that should happen after installments are marked as
2
+ # failures
3
+ module SolidusSubscriptions
4
+ class FailureDispatcher < Dispatcher
5
+ def dispatch
6
+ order.touch :completed_at
7
+ order.cancel!
8
+ installments.each { |i| i.failed!(order) }
9
+ super
10
+ end
11
+
12
+ def message
13
+ "
14
+ Something went wrong processing installments: #{installments.map(&:id).join(', ')}.
15
+ They have been marked for reprocessing.
16
+ "
17
+ end
18
+ end
19
+ end
@@ -1,126 +1,126 @@
1
- # This class represents a single iteration of a subscription. It is fulfulled
2
- # by a conmpleted order and maintains an association which tracks all attempts
3
- # successful or othewise at fulfulling this installment
4
- module SolidusSubscriptions
5
- class Installment < ActiveRecord::Base
6
- has_many :details, class_name: 'SolidusSubscriptions::InstallmentDetail'
7
- belongs_to(
8
- :subscription,
9
- class_name: 'SolidusSubscriptions::Subscription',
10
- inverse_of: :installments
11
- )
12
-
13
- validates :subscription, presence: true
14
-
15
- scope :fulfilled, (lambda do
16
- joins(:details).where(InstallmentDetail.table_name => { success: true }).distinct
17
- end)
18
-
19
- scope :unfulfilled, (lambda do
20
- fulfilled_ids = fulfilled.pluck(:id)
21
- where.not(id: fulfilled_ids).distinct
22
- end)
23
-
24
- scope :actionable, (lambda do
25
- unfulfilled.where("#{table_name}.actionable_date <= ?", Time.zone.now)
26
- end)
27
-
28
- # Get the builder for the subscription_line_item. This will be an
29
- # object that can generate the appropriate line item for the subscribable
30
- # object
31
- #
32
- # @return [SolidusSubscriptions::LineItemBuilder]
33
- def line_item_builder
34
- subscription.line_item_builder
35
- end
36
-
37
- # Mark this installment as out of stock.
38
- #
39
- # @return [SolidusSubscriptions::InstallmentDetail] The record of the failed
40
- # processing attempt
41
- def out_of_stock
42
- advance_actionable_date!
43
-
44
- details.create!(
45
- success: false,
46
- message: I18n.t('solidus_subscriptions.installment_details.out_of_stock')
47
- )
48
- end
49
-
50
- # Mark this installment as a success
51
- #
52
- # @param order [Spree::Order] The order generated for this processing
53
- # attempt
54
- #
55
- # @return [SolidusSubscriptions::InstallmentDetail] The record of the
56
- # successful processing attempt
57
- def success!(order)
58
- update!(actionable_date: nil)
59
-
60
- details.create!(
61
- success: true,
62
- order: order,
63
- message: I18n.t('solidus_subscriptions.installment_details.success')
64
- )
65
- end
66
-
67
- # Mark this installment as a failure
68
- #
69
- # @param order [Spree::Order] The order generated for this processing
70
- # attempt
71
- #
72
- # @return [SolidusSubscriptions::InstallmentDetail] The record of the
73
- # failed processing attempt
74
- def failed!(order)
75
- advance_actionable_date!
76
-
77
- details.create!(
78
- success: false,
79
- order: order,
80
- message: I18n.t('solidus_subscriptions.installment_details.failed')
81
- )
82
- end
83
-
84
- # Does this installment still need to be fulfilled by a completed order
85
- #
86
- # @return [Boolean]
87
- def unfulfilled?
88
- !fulfilled?
89
- end
90
-
91
- # Had this installment been fulfilled by a completed order
92
- #
93
- # @return [Boolean]
94
- def fulfilled?
95
- details.where(success: true).exists?
96
- end
97
-
98
- # Mark this installment as having a failed payment
99
- #
100
- # @param order [Spree::Order] The order generated for this processing
101
- # attempt
102
- #
103
- # @return [SolidusSubscriptions::InstallmentDetail] The record of the
104
- # failed processing attempt
105
- def payment_failed!(order)
106
- advance_actionable_date!
107
-
108
- details.create!(
109
- success: false,
110
- order: order,
111
- message: I18n.t('solidus_subscriptions.installment_details.payment_failed')
112
- )
113
- end
114
-
115
- private
116
-
117
- def advance_actionable_date!
118
- update!(actionable_date: next_actionable_date)
119
- end
120
-
121
- def next_actionable_date
122
- return if Config.reprocessing_interval.nil?
123
- (DateTime.current + Config.reprocessing_interval).beginning_of_minute
124
- end
125
- end
126
- end
1
+ # This class represents a single iteration of a subscription. It is fulfulled
2
+ # by a conmpleted order and maintains an association which tracks all attempts
3
+ # successful or othewise at fulfulling this installment
4
+ module SolidusSubscriptions
5
+ class Installment < ActiveRecord::Base
6
+ has_many :details, class_name: 'SolidusSubscriptions::InstallmentDetail'
7
+ belongs_to(
8
+ :subscription,
9
+ class_name: 'SolidusSubscriptions::Subscription',
10
+ inverse_of: :installments
11
+ )
12
+
13
+ validates :subscription, presence: true
14
+
15
+ scope :fulfilled, (lambda do
16
+ joins(:details).where(InstallmentDetail.table_name => { success: true }).distinct
17
+ end)
18
+
19
+ scope :unfulfilled, (lambda do
20
+ fulfilled_ids = fulfilled.pluck(:id)
21
+ where.not(id: fulfilled_ids).distinct
22
+ end)
23
+
24
+ scope :actionable, (lambda do
25
+ unfulfilled.where("#{table_name}.actionable_date <= ?", Time.zone.now)
26
+ end)
27
+
28
+ # Get the builder for the subscription_line_item. This will be an
29
+ # object that can generate the appropriate line item for the subscribable
30
+ # object
31
+ #
32
+ # @return [SolidusSubscriptions::LineItemBuilder]
33
+ def line_item_builder
34
+ subscription.line_item_builder
35
+ end
36
+
37
+ # Mark this installment as out of stock.
38
+ #
39
+ # @return [SolidusSubscriptions::InstallmentDetail] The record of the failed
40
+ # processing attempt
41
+ def out_of_stock
42
+ advance_actionable_date!
43
+
44
+ details.create!(
45
+ success: false,
46
+ message: I18n.t('solidus_subscriptions.installment_details.out_of_stock')
47
+ )
48
+ end
49
+
50
+ # Mark this installment as a success
51
+ #
52
+ # @param order [Spree::Order] The order generated for this processing
53
+ # attempt
54
+ #
55
+ # @return [SolidusSubscriptions::InstallmentDetail] The record of the
56
+ # successful processing attempt
57
+ def success!(order)
58
+ update!(actionable_date: nil)
59
+
60
+ details.create!(
61
+ success: true,
62
+ order: order,
63
+ message: I18n.t('solidus_subscriptions.installment_details.success')
64
+ )
65
+ end
66
+
67
+ # Mark this installment as a failure
68
+ #
69
+ # @param order [Spree::Order] The order generated for this processing
70
+ # attempt
71
+ #
72
+ # @return [SolidusSubscriptions::InstallmentDetail] The record of the
73
+ # failed processing attempt
74
+ def failed!(order)
75
+ advance_actionable_date!
76
+
77
+ details.create!(
78
+ success: false,
79
+ order: order,
80
+ message: I18n.t('solidus_subscriptions.installment_details.failed')
81
+ )
82
+ end
83
+
84
+ # Does this installment still need to be fulfilled by a completed order
85
+ #
86
+ # @return [Boolean]
87
+ def unfulfilled?
88
+ !fulfilled?
89
+ end
90
+
91
+ # Had this installment been fulfilled by a completed order
92
+ #
93
+ # @return [Boolean]
94
+ def fulfilled?
95
+ details.where(success: true).exists?
96
+ end
97
+
98
+ # Mark this installment as having a failed payment
99
+ #
100
+ # @param order [Spree::Order] The order generated for this processing
101
+ # attempt
102
+ #
103
+ # @return [SolidusSubscriptions::InstallmentDetail] The record of the
104
+ # failed processing attempt
105
+ def payment_failed!(order)
106
+ advance_actionable_date!
107
+
108
+ details.create!(
109
+ success: false,
110
+ order: order,
111
+ message: I18n.t('solidus_subscriptions.installment_details.payment_failed')
112
+ )
113
+ end
114
+
115
+ private
116
+
117
+ def advance_actionable_date!
118
+ update!(actionable_date: next_actionable_date)
119
+ end
120
+
121
+ def next_actionable_date
122
+ return if Config.reprocessing_interval.nil?
123
+ (DateTime.current + Config.reprocessing_interval).beginning_of_minute
124
+ end
125
+ end
126
+ end
@@ -1,23 +1,23 @@
1
- # This class represents a single attempt to fulfill an installment. It will
2
- # indicate the result of that attept.
3
- module SolidusSubscriptions
4
- class InstallmentDetail < ActiveRecord::Base
5
- belongs_to(
6
- :installment,
7
- class_name: 'SolidusSubscriptions::Installment',
8
- inverse_of: :details
9
- )
10
-
11
- belongs_to(:order, class_name: 'Spree::Order')
12
-
13
- validates :installment, presence: true
14
- alias_attribute :successful, :success
15
-
16
- # Was the attempt at fulfilling this installment a failure?
17
- #
18
- # @return [Boolean]
19
- def failed?
20
- !success
21
- end
22
- end
23
- end
1
+ # This class represents a single attempt to fulfill an installment. It will
2
+ # indicate the result of that attept.
3
+ module SolidusSubscriptions
4
+ class InstallmentDetail < ActiveRecord::Base
5
+ belongs_to(
6
+ :installment,
7
+ class_name: 'SolidusSubscriptions::Installment',
8
+ inverse_of: :details
9
+ )
10
+
11
+ belongs_to(:order, class_name: 'Spree::Order')
12
+
13
+ validates :installment, presence: true
14
+ alias_attribute :successful, :success
15
+
16
+ # Was the attempt at fulfilling this installment a failure?
17
+ #
18
+ # @return [Boolean]
19
+ def failed?
20
+ !success
21
+ end
22
+ end
23
+ end
@@ -1,24 +1,24 @@
1
- # This module is intended to be included into any active record
2
- # modle which needs to be aware of how intervals and stored and
3
- # calculated in the db.
4
- #
5
- # Base models must have the following fields: interval_length (integer) and interval_units (integer)
6
- module SolidusSubscriptions
7
- module Interval
8
- def self.included(base)
9
- base.enum interval_units: {
10
- day: 0,
11
- week: 1,
12
- month: 2,
13
- year: 3
14
- }
15
- end
16
-
17
- # Calculates the number of seconds in the interval.
18
- #
19
- # @return [Integer] The number of seconds.
20
- def interval
21
- ActiveSupport::Duration.new(interval_length, { interval_units.pluralize.to_sym => interval_length })
22
- end
23
- end
24
- end
1
+ # This module is intended to be included into any active record
2
+ # modle which needs to be aware of how intervals and stored and
3
+ # calculated in the db.
4
+ #
5
+ # Base models must have the following fields: interval_length (integer) and interval_units (integer)
6
+ module SolidusSubscriptions
7
+ module Interval
8
+ def self.included(base)
9
+ base.enum interval_units: {
10
+ day: 0,
11
+ week: 1,
12
+ month: 2,
13
+ year: 3
14
+ }
15
+ end
16
+
17
+ # Calculates the number of seconds in the interval.
18
+ #
19
+ # @return [Integer] The number of seconds.
20
+ def interval
21
+ ActiveSupport::Duration.new(interval_length, { interval_units.pluralize.to_sym => interval_length })
22
+ end
23
+ end
24
+ end
@@ -1,98 +1,98 @@
1
- # The LineItem class is responsible for associating Line items to subscriptions. # It tracks the following values:
2
- #
3
- # [Spree::LineItem] :spree_line_item The spree object which created this instance
4
- #
5
- # [SolidusSubscription::Subscription] :subscription The object responsible for
6
- # grouping all information needed to create new subscription orders together
7
- #
8
- # [Integer] :subscribable_id The id of the object to be added to new subscription
9
- # orders when they are placed
10
- #
11
- # [Integer] :quantity How many units of the subscribable should be included in
12
- # future orders
13
- #
14
- # [Integer] :interval How often subscription orders should be placed
15
- #
16
- # [Integer] :installments How many subscription orders should be placed
17
- module SolidusSubscriptions
18
- class LineItem < ActiveRecord::Base
19
- include Interval
20
-
21
- belongs_to :spree_line_item, class_name: 'Spree::LineItem', inverse_of: :subscription_line_items
22
- has_one :order, through: :spree_line_item, class_name: 'Spree::Order'
23
- belongs_to(
24
- :subscription,
25
- class_name: 'SolidusSubscriptions::Subscription',
26
- inverse_of: :line_items
27
- )
28
-
29
- validates :subscribable_id, presence: :true
30
- validates :quantity, numericality: { greater_than: 0 }
31
- validates :interval_length, numericality: { greater_than: 0 }, unless: -> { subscription }
32
-
33
- before_update :update_actionable_date_if_interval_changed
34
-
35
- def next_actionable_date
36
- dummy_subscription.next_actionable_date
37
- end
38
-
39
- def as_json(**options)
40
- options[:methods] ||= [:dummy_line_item, :next_actionable_date]
41
- super(options)
42
- end
43
-
44
- # Get a placeholder line item for calculating the values of future
45
- # subscription orders. It is frozen and cannot be saved
46
- def dummy_line_item
47
- li = LineItemBuilder.new([self]).spree_line_items.first
48
- return unless li
49
-
50
- li.order = dummy_order
51
- li.validate
52
- li.freeze
53
- end
54
-
55
- def interval
56
- subscription.try!(:interval) || super
57
- end
58
-
59
- private
60
-
61
- # Get a placeholder order for calculating the values of future
62
- # subscription orders. It is a frozen duplicate of the current order and
63
- # cannot be saved
64
- def dummy_order
65
- order = spree_line_item ? spree_line_item.order.dup : Spree::Order.create
66
- order.ship_address = subscription.shipping_address || subscription.user.ship_address if subscription
67
-
68
- order.freeze
69
- end
70
-
71
- # A place holder for calculating dynamic values needed to display in the cart
72
- # it is frozen and cannot be saved
73
- def dummy_subscription
74
- Subscription.new(line_items: [dup], interval_length: interval_length, interval_units: interval_units).freeze
75
- end
76
-
77
- def update_actionable_date_if_interval_changed
78
- if persisted? && subscription && (interval_length_changed? || interval_units_changed?)
79
- base_date = if subscription.installments.any?
80
- subscription.installments.last.created_at
81
- else
82
- subscription.created_at
83
- end
84
-
85
- new_date = interval.since(base_date)
86
-
87
- if new_date < Time.zone.now
88
- # if the chosen base time plus the new interval is in the past, set
89
- # the actionable_date to be now to avoid confusion and possible
90
- # mis-processing.
91
- new_date = Time.zone.now
92
- end
93
-
94
- subscription.actionable_date = new_date
95
- end
96
- end
97
- end
98
- end
1
+ # The LineItem class is responsible for associating Line items to subscriptions. # It tracks the following values:
2
+ #
3
+ # [Spree::LineItem] :spree_line_item The spree object which created this instance
4
+ #
5
+ # [SolidusSubscription::Subscription] :subscription The object responsible for
6
+ # grouping all information needed to create new subscription orders together
7
+ #
8
+ # [Integer] :subscribable_id The id of the object to be added to new subscription
9
+ # orders when they are placed
10
+ #
11
+ # [Integer] :quantity How many units of the subscribable should be included in
12
+ # future orders
13
+ #
14
+ # [Integer] :interval How often subscription orders should be placed
15
+ #
16
+ # [Integer] :installments How many subscription orders should be placed
17
+ module SolidusSubscriptions
18
+ class LineItem < ActiveRecord::Base
19
+ include Interval
20
+
21
+ belongs_to :spree_line_item, class_name: 'Spree::LineItem', inverse_of: :subscription_line_items
22
+ has_one :order, through: :spree_line_item, class_name: 'Spree::Order'
23
+ belongs_to(
24
+ :subscription,
25
+ class_name: 'SolidusSubscriptions::Subscription',
26
+ inverse_of: :line_items
27
+ )
28
+
29
+ validates :subscribable_id, presence: :true
30
+ validates :quantity, numericality: { greater_than: 0 }
31
+ validates :interval_length, numericality: { greater_than: 0 }, unless: -> { subscription }
32
+
33
+ before_update :update_actionable_date_if_interval_changed
34
+
35
+ def next_actionable_date
36
+ dummy_subscription.next_actionable_date
37
+ end
38
+
39
+ def as_json(**options)
40
+ options[:methods] ||= [:dummy_line_item, :next_actionable_date]
41
+ super(options)
42
+ end
43
+
44
+ # Get a placeholder line item for calculating the values of future
45
+ # subscription orders. It is frozen and cannot be saved
46
+ def dummy_line_item
47
+ li = LineItemBuilder.new([self]).spree_line_items.first
48
+ return unless li
49
+
50
+ li.order = dummy_order
51
+ li.validate
52
+ li.freeze
53
+ end
54
+
55
+ def interval
56
+ subscription.try!(:interval) || super
57
+ end
58
+
59
+ private
60
+
61
+ # Get a placeholder order for calculating the values of future
62
+ # subscription orders. It is a frozen duplicate of the current order and
63
+ # cannot be saved
64
+ def dummy_order
65
+ order = spree_line_item ? spree_line_item.order.dup : Spree::Order.create
66
+ order.ship_address = subscription.shipping_address || subscription.user.ship_address if subscription
67
+
68
+ order.freeze
69
+ end
70
+
71
+ # A place holder for calculating dynamic values needed to display in the cart
72
+ # it is frozen and cannot be saved
73
+ def dummy_subscription
74
+ Subscription.new(line_items: [dup], interval_length: interval_length, interval_units: interval_units).freeze
75
+ end
76
+
77
+ def update_actionable_date_if_interval_changed
78
+ if persisted? && subscription && (interval_length_changed? || interval_units_changed?)
79
+ base_date = if subscription.installments.any?
80
+ subscription.installments.last.created_at
81
+ else
82
+ subscription.created_at
83
+ end
84
+
85
+ new_date = interval.since(base_date)
86
+
87
+ if new_date < Time.zone.now
88
+ # if the chosen base time plus the new interval is in the past, set
89
+ # the actionable_date to be now to avoid confusion and possible
90
+ # mis-processing.
91
+ new_date = Time.zone.now
92
+ end
93
+
94
+ subscription.actionable_date = new_date
95
+ end
96
+ end
97
+ end
98
+ end