solidus_subscriptions 1.0.0.rc1

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 (204) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +41 -0
  3. data/.gem_release.yml +5 -0
  4. data/.github/stale.yml +17 -0
  5. data/.github_changelog_generator +2 -0
  6. data/.gitignore +20 -0
  7. data/.rspec +2 -0
  8. data/.rubocop.yml +12 -0
  9. data/.rubocop_todo.yml +86 -0
  10. data/CHANGELOG.md +191 -0
  11. data/Gemfile +33 -0
  12. data/LICENSE +26 -0
  13. data/README.md +221 -0
  14. data/Rakefile +6 -0
  15. data/app/assets/javascripts/spree/backend/solidus_subscriptions.js +1 -0
  16. data/app/assets/javascripts/spree/backend/solidus_subscriptions/edit_subscription_payment.js +32 -0
  17. data/app/controllers/solidus_subscriptions/api/v1/base_controller.rb +13 -0
  18. data/app/controllers/solidus_subscriptions/api/v1/line_items_controller.rb +48 -0
  19. data/app/controllers/solidus_subscriptions/api/v1/subscriptions_controller.rb +100 -0
  20. data/app/controllers/spree/admin/installments_controller.rb +25 -0
  21. data/app/controllers/spree/admin/subscription_events_controller.rb +37 -0
  22. data/app/controllers/spree/admin/subscription_orders_controller.rb +35 -0
  23. data/app/controllers/spree/admin/subscriptions_controller.rb +100 -0
  24. data/app/controllers/spree/admin/users/subscriptions_controller.rb +17 -0
  25. data/app/decorators/controllers/solidus_subscriptions/spree/api/line_items_controller/create_subscription_line_items.rb +36 -0
  26. data/app/decorators/controllers/solidus_subscriptions/spree/orders_controller/create_subscription_line_items.rb +35 -0
  27. data/app/decorators/models/solidus_subscriptions/spree/line_item/subscription_line_items_association.rb +26 -0
  28. data/app/decorators/models/solidus_subscriptions/spree/order/after_create.rb +19 -0
  29. data/app/decorators/models/solidus_subscriptions/spree/order/finalize_creates_subscriptions.rb +23 -0
  30. data/app/decorators/models/solidus_subscriptions/spree/order/installment_details_association.rb +15 -0
  31. data/app/decorators/models/solidus_subscriptions/spree/order/subscription_association.rb +15 -0
  32. data/app/decorators/models/solidus_subscriptions/spree/order/subscription_line_items_association.rb +19 -0
  33. data/app/decorators/models/solidus_subscriptions/spree/product/delegate_subscribable.rb +17 -0
  34. data/app/decorators/models/solidus_subscriptions/spree/user/have_many_subscriptions.rb +30 -0
  35. data/app/decorators/models/solidus_subscriptions/spree/variant/auto_delete_from_subscriptions.rb +20 -0
  36. data/app/decorators/models/solidus_subscriptions/spree/variant/variant_pretty_name.rb +17 -0
  37. data/app/decorators/models/solidus_subscriptions/spree/wallet_payment_source/report_default_change_to_subscriptions.rb +28 -0
  38. data/app/jobs/solidus_subscriptions/process_installment_job.rb +13 -0
  39. data/app/jobs/solidus_subscriptions/process_subscription_job.rb +31 -0
  40. data/app/models/solidus_subscriptions/installment.rb +135 -0
  41. data/app/models/solidus_subscriptions/installment_detail.rb +28 -0
  42. data/app/models/solidus_subscriptions/interval.rb +26 -0
  43. data/app/models/solidus_subscriptions/line_item.rb +42 -0
  44. data/app/models/solidus_subscriptions/promotion/rules/subscription_creation_order.rb +44 -0
  45. data/app/models/solidus_subscriptions/promotion/rules/subscription_installment_order.rb +31 -0
  46. data/app/models/solidus_subscriptions/subscription.rb +392 -0
  47. data/app/models/solidus_subscriptions/subscription_event.rb +11 -0
  48. data/app/overrides/views/admin_subscribable_product_checkbox.rb +8 -0
  49. data/app/overrides/views/admin_subscribable_variant_checkbox.rb +8 -0
  50. data/app/overrides/views/admin_subscriptions_menu_link.rb +10 -0
  51. data/app/overrides/views/admin_users_subscriptions_tab.rb +8 -0
  52. data/app/overrides/views/subscription_line_item_fields.rb +8 -0
  53. data/app/subscribers/solidus_subscriptions/churn_buster_subscriber.rb +39 -0
  54. data/app/subscribers/solidus_subscriptions/event_storage_subscriber.rb +64 -0
  55. data/app/views/spree/admin/installments/_state_pill.html.erb +8 -0
  56. data/app/views/spree/admin/installments/index.html.erb +42 -0
  57. data/app/views/spree/admin/products/_subscribable_checkbox.html.erb +8 -0
  58. data/app/views/spree/admin/promotions/rules/_subscription_creation_order.html.erb +0 -0
  59. data/app/views/spree/admin/promotions/rules/_subscription_installment_order.html.erb +0 -0
  60. data/app/views/spree/admin/shared/_subscription_actions.html.erb +35 -0
  61. data/app/views/spree/admin/shared/_subscription_breadcrumbs.html.erb +4 -0
  62. data/app/views/spree/admin/shared/_subscription_sidebar.html.erb +18 -0
  63. data/app/views/spree/admin/shared/_subscription_tab.html.erb +3 -0
  64. data/app/views/spree/admin/shared/_subscription_tabs.html.erb +18 -0
  65. data/app/views/spree/admin/subscription_events/_state_pill.html.erb +8 -0
  66. data/app/views/spree/admin/subscription_events/index.html.erb +42 -0
  67. data/app/views/spree/admin/subscription_orders/index.html.erb +93 -0
  68. data/app/views/spree/admin/subscriptions/_form.html.erb +150 -0
  69. data/app/views/spree/admin/subscriptions/_processing_state_pill.html.erb +9 -0
  70. data/app/views/spree/admin/subscriptions/_state_pill.html.erb +10 -0
  71. data/app/views/spree/admin/subscriptions/edit.html.erb +10 -0
  72. data/app/views/spree/admin/subscriptions/index.html.erb +176 -0
  73. data/app/views/spree/admin/subscriptions/new.html.erb +5 -0
  74. data/app/views/spree/admin/users/_subscription_tab.html.erb +5 -0
  75. data/app/views/spree/admin/users/subscriptions/index.html.erb +44 -0
  76. data/app/views/spree/admin/variants/_subscribable_checkbox.html.erb +6 -0
  77. data/app/views/spree/frontend/products/_subscription_line_item_fields.html.erb +30 -0
  78. data/bin/console +17 -0
  79. data/bin/rails +7 -0
  80. data/bin/rails-engine +13 -0
  81. data/bin/rails-sandbox +16 -0
  82. data/bin/rake +7 -0
  83. data/bin/sandbox +86 -0
  84. data/bin/setup +8 -0
  85. data/config/initializers/permission_sets.rb +11 -0
  86. data/config/initializers/subscribers.rb +9 -0
  87. data/config/locales/en.yml +129 -0
  88. data/config/routes.rb +34 -0
  89. data/db/migrate/20160825164850_create_solidus_subscriptions_subscriptions.rb +11 -0
  90. data/db/migrate/20160825173548_create_solidus_subscriptions_line_items.rb +17 -0
  91. data/db/migrate/20160825202248_create_solidus_subscriptions_installments.rb +23 -0
  92. data/db/migrate/20160825211202_create_solidus_subscriptions_installment_details.rb +22 -0
  93. data/db/migrate/20160825214240_add_subscribable_to_spree_variants.rb +5 -0
  94. data/db/migrate/20160829201653_change_subscription_line_items_installments_to_max_installments.rb +5 -0
  95. data/db/migrate/20160902220242_remove_state_from_solidus_susbscriptions_installment_details.rb +5 -0
  96. data/db/migrate/20160902220604_add_successful_to_solidus_subscriptions_installment_details.rb +5 -0
  97. data/db/migrate/20160902221218_add_message_to_solidus_subscriptions_installment_details.rb +5 -0
  98. data/db/migrate/20160922164101_add_interval_length_and_units_to_subscription_line_items.rb +8 -0
  99. data/db/migrate/20161006191003_add_skip_count_to_solidus_subscriptions_subscriptions.rb +5 -0
  100. data/db/migrate/20161006191127_add_successive_skip_count_to_solidus_subscriptions_subscriptions.rb +5 -0
  101. data/db/migrate/20161014212649_allow_spree_line_item_id_to_be_null.rb +5 -0
  102. data/db/migrate/20161017155749_add_order_id_to_solidus_subscriptions_installment_details.rb +6 -0
  103. data/db/migrate/20161017175509_remove_order_id_from_solidus_subscriptions_installments.rb +5 -0
  104. data/db/migrate/20161017201944_add_subscription_order_to_spree_orders.rb +5 -0
  105. data/db/migrate/20161221155142_add_store_to_solidus_subscriptions_subscriptions.rb +6 -0
  106. data/db/migrate/20161223152905_add_address_id_to_solidus_subscriptions_subscriptions.rb +7 -0
  107. data/db/migrate/20170106224713_change_line_item_max_installments_to_end_date.rb +6 -0
  108. data/db/migrate/20170111224458_change_subscription_actionable_date_to_datetime.rb +5 -0
  109. data/db/migrate/20170111232801_change_inteval_actionable_date_to_datetime.rb +5 -0
  110. data/db/migrate/20170112012407_add_config_options_to_subscriptions.rb +7 -0
  111. data/db/migrate/20200617102749_add_billing_address_to_subscriptions.rb +11 -0
  112. data/db/migrate/20200617155042_add_payment_source_to_subscriptions.rb +6 -0
  113. data/db/migrate/20200618092951_add_payment_method_to_subscriptions.rb +11 -0
  114. data/db/migrate/20200730101242_create_solidus_subscriptions_subscription_events.rb +22 -0
  115. data/db/migrate/20200917072152_add_subscription_reference_to_orders.rb +11 -0
  116. data/db/migrate/20201007140032_add_guest_token_to_subscriptions.rb +6 -0
  117. data/db/migrate/20201123171026_change_actionable_date_to_date.rb +15 -0
  118. data/db/migrate/20210205140422_add_currency_to_subscription.rb +5 -0
  119. data/db/migrate/20210323165714_update_promotion_rule_names.rb +22 -0
  120. data/lib/generators/solidus_subscriptions/install/install_generator.rb +32 -0
  121. data/lib/generators/solidus_subscriptions/install/templates/initializer.rb +99 -0
  122. data/lib/solidus_subscriptions.rb +49 -0
  123. data/lib/solidus_subscriptions/checkout.rb +74 -0
  124. data/lib/solidus_subscriptions/churn_buster/client.rb +48 -0
  125. data/lib/solidus_subscriptions/churn_buster/order_serializer.rb +19 -0
  126. data/lib/solidus_subscriptions/churn_buster/serializer.rb +23 -0
  127. data/lib/solidus_subscriptions/churn_buster/subscription_customer_serializer.rb +28 -0
  128. data/lib/solidus_subscriptions/churn_buster/subscription_payment_method_serializer.rb +37 -0
  129. data/lib/solidus_subscriptions/churn_buster/subscription_serializer.rb +17 -0
  130. data/lib/solidus_subscriptions/configuration.rb +84 -0
  131. data/lib/solidus_subscriptions/dispatcher/base.rb +18 -0
  132. data/lib/solidus_subscriptions/dispatcher/failure_dispatcher.rb +13 -0
  133. data/lib/solidus_subscriptions/dispatcher/out_of_stock_dispatcher.rb +11 -0
  134. data/lib/solidus_subscriptions/dispatcher/payment_failed_dispatcher.rb +19 -0
  135. data/lib/solidus_subscriptions/dispatcher/success_dispatcher.rb +17 -0
  136. data/lib/solidus_subscriptions/engine.rb +56 -0
  137. data/lib/solidus_subscriptions/permission_sets/default_customer.rb +19 -0
  138. data/lib/solidus_subscriptions/permission_sets/subscription_management.rb +12 -0
  139. data/lib/solidus_subscriptions/permitted_attributes.rb +20 -0
  140. data/lib/solidus_subscriptions/processor.rb +17 -0
  141. data/lib/solidus_subscriptions/subscription_generator.rb +77 -0
  142. data/lib/solidus_subscriptions/subscription_line_item_builder.rb +23 -0
  143. data/lib/solidus_subscriptions/testing_support/factories/installment_detail_factory.rb +11 -0
  144. data/lib/solidus_subscriptions/testing_support/factories/installment_factory.rb +29 -0
  145. data/lib/solidus_subscriptions/testing_support/factories/line_item_factory.rb +20 -0
  146. data/lib/solidus_subscriptions/testing_support/factories/spree/line_item_factory.rb +17 -0
  147. data/lib/solidus_subscriptions/testing_support/factories/spree/order_factory.rb +17 -0
  148. data/lib/solidus_subscriptions/testing_support/factories/spree_modification_factory.rb +10 -0
  149. data/lib/solidus_subscriptions/testing_support/factories/subscription_event_factory.rb +8 -0
  150. data/lib/solidus_subscriptions/testing_support/factories/subscription_factory.rb +56 -0
  151. data/lib/solidus_subscriptions/version.rb +5 -0
  152. data/lib/tasks/process_subscriptions.rake +8 -0
  153. data/reference/solidus_subscriptions.v1.yaml +290 -0
  154. data/solidus_subscriptions.gemspec +47 -0
  155. data/spec/controllers/spree/admin/subscriptions_controller_spec.rb +202 -0
  156. data/spec/controllers/spree/api/line_items_controller_spec.rb +103 -0
  157. data/spec/controllers/spree/api/orders_controller_spec.rb +57 -0
  158. data/spec/controllers/spree/api/users_controller_spec.rb +48 -0
  159. data/spec/decorators/controllers/solidus_subscriptions/spree/orders_controller/create_subscription_line_items_spec.rb +80 -0
  160. data/spec/decorators/models/solidus_subscriptions/spree/line_item/subscription_line_items_association_spec.rb +10 -0
  161. data/spec/decorators/models/solidus_subscriptions/spree/order/finalize_creates_subscriptions_spec.rb +32 -0
  162. data/spec/decorators/models/solidus_subscriptions/spree/order/installment_details_association_spec.rb +9 -0
  163. data/spec/decorators/models/solidus_subscriptions/spree/order/subscription_line_items_association_spec.rb +9 -0
  164. data/spec/decorators/models/solidus_subscriptions/spree/user/have_many_subscriptions_spec.rb +22 -0
  165. data/spec/decorators/models/solidus_subscriptions/spree/variant/auto_delete_from_subscriptions_spec.rb +25 -0
  166. data/spec/features/admin/subscription_orders_spec.rb +35 -0
  167. data/spec/features/admin/subscriptions_spec.rb +63 -0
  168. data/spec/features/admin_users_subscription_tabs_spec.rb +61 -0
  169. data/spec/fixtures/cassettes/churn_buster.yml +229 -0
  170. data/spec/jobs/solidus_subscriptions/process_installment_job_spec.rb +38 -0
  171. data/spec/jobs/solidus_subscriptions/process_subscription_job_spec.rb +83 -0
  172. data/spec/lib/solidus_subscriptions/checkout_spec.rb +125 -0
  173. data/spec/lib/solidus_subscriptions/churn_buster/client_spec.rb +59 -0
  174. data/spec/lib/solidus_subscriptions/dispatcher/failure_dispatcher_spec.rb +29 -0
  175. data/spec/lib/solidus_subscriptions/dispatcher/out_of_stock_dispatcher_spec.rb +15 -0
  176. data/spec/lib/solidus_subscriptions/dispatcher/payment_failed_dispatcher_spec.rb +44 -0
  177. data/spec/lib/solidus_subscriptions/dispatcher/success_dispatcher_spec.rb +30 -0
  178. data/spec/lib/solidus_subscriptions/permission_sets/default_customer_spec.rb +95 -0
  179. data/spec/lib/solidus_subscriptions/permission_sets/subscription_management_spec.rb +26 -0
  180. data/spec/lib/solidus_subscriptions/processor_spec.rb +34 -0
  181. data/spec/lib/solidus_subscriptions/promotion/rules/subscription_creation_order_spec.rb +57 -0
  182. data/spec/lib/solidus_subscriptions/promotion/rules/subscription_installment_order_spec.rb +39 -0
  183. data/spec/lib/solidus_subscriptions/subscription_generator_spec.rb +83 -0
  184. data/spec/lib/solidus_subscriptions_spec.rb +30 -0
  185. data/spec/models/solidus_subscriptions/installment_detail_spec.rb +23 -0
  186. data/spec/models/solidus_subscriptions/installment_spec.rb +201 -0
  187. data/spec/models/solidus_subscriptions/line_item_spec.rb +29 -0
  188. data/spec/models/solidus_subscriptions/subscription_spec.rb +814 -0
  189. data/spec/models/spree/variant_spec.rb +16 -0
  190. data/spec/models/spree/wallet_payment_source_spec.rb +20 -0
  191. data/spec/requests/api/v1/line_items_spec.rb +116 -0
  192. data/spec/requests/api/v1/subscriptions_spec.rb +255 -0
  193. data/spec/spec_helper.rb +30 -0
  194. data/spec/subscribers/solidus_subscriptions/churn_buster_subscriber_spec.rb +76 -0
  195. data/spec/support/active_model_mocks.rb +1 -0
  196. data/spec/support/cancancan.rb +1 -0
  197. data/spec/support/factories.rb +1 -0
  198. data/spec/support/helpers/checkout_infrastructure.rb +18 -0
  199. data/spec/support/helpers/config.rb +13 -0
  200. data/spec/support/shoulda.rb +7 -0
  201. data/spec/support/timecop.rb +1 -0
  202. data/spec/support/vcr.rb +10 -0
  203. data/spec/support/version_cake.rb +8 -0
  204. metadata +498 -0
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe SolidusSubscriptions::LineItem, type: :model do
6
+ it { is_expected.to validate_presence_of :subscribable_id }
7
+
8
+ it { is_expected.to validate_numericality_of(:quantity).is_greater_than(0) }
9
+ it { is_expected.to validate_numericality_of(:interval_length).is_greater_than(0) }
10
+
11
+ describe "#interval" do
12
+ subject(:interval) { line_item.interval }
13
+
14
+ let(:line_item) { create :subscription_line_item, :with_subscription }
15
+
16
+ before do
17
+ Timecop.freeze(Date.parse("2016-09-22"))
18
+ line_item.subscription.update!(actionable_date: Date.current)
19
+ end
20
+
21
+ after { Timecop.return }
22
+
23
+ it { is_expected.to be_a ActiveSupport::Duration }
24
+
25
+ it "calculates the duration correctly" do
26
+ expect(interval.from_now).to eq Date.parse("2016-10-22")
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,814 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe SolidusSubscriptions::Subscription, type: :model do
6
+ it { is_expected.to validate_presence_of :user }
7
+ it { is_expected.to validate_presence_of :skip_count }
8
+ it { is_expected.to validate_presence_of :successive_skip_count }
9
+ it { is_expected.to validate_numericality_of(:skip_count).is_greater_than_or_equal_to(0) }
10
+ it { is_expected.to validate_numericality_of(:successive_skip_count).is_greater_than_or_equal_to(0) }
11
+ it { is_expected.to accept_nested_attributes_for(:line_items) }
12
+
13
+ it 'validates currency correctly' do
14
+ expect(subject).to validate_inclusion_of(:currency).
15
+ in_array(::Money::Currency.all.map(&:iso_code)).
16
+ with_message('is not a valid currency code')
17
+ end
18
+
19
+ it 'validates payment_source ownership' do
20
+ subscription = create(:subscription)
21
+
22
+ subscription.update(payment_source: create(:credit_card))
23
+ expect(subscription.errors.messages[:payment_source]).to include('does not belong to the user associated with the subscription')
24
+
25
+ subscription.update(payment_source: create(:credit_card, user: subscription.user))
26
+ expect(subscription.errors.messages[:payment_source]).not_to include('does not belong to the user associated with the subscription')
27
+ end
28
+
29
+ describe 'creating a subscription' do
30
+ it 'tracks the creation' do
31
+ stub_const('Spree::Event', class_spy(Spree::Event))
32
+
33
+ subscription = create(:subscription)
34
+
35
+ expect(Spree::Event).to have_received(:fire).with(
36
+ 'solidus_subscriptions.subscription_created',
37
+ subscription: subscription,
38
+ )
39
+ end
40
+
41
+ it 'generates a guest token' do
42
+ subscription = create(:subscription)
43
+
44
+ expect(subscription.guest_token).to be_present
45
+ end
46
+
47
+ it 'sets default config currency if not given' do
48
+ subscription = create(:subscription, currency: nil)
49
+
50
+ expect(subscription.currency).to eq(Spree::Config.currency)
51
+ end
52
+ end
53
+
54
+ describe 'updating a subscription' do
55
+ it 'tracks interval changes' do
56
+ stub_const('Spree::Event', class_spy(Spree::Event))
57
+ subscription = create(:subscription)
58
+
59
+ subscription.update!(interval_length: subscription.interval_length + 1)
60
+
61
+ expect(Spree::Event).to have_received(:fire).with(
62
+ 'solidus_subscriptions.subscription_frequency_changed',
63
+ subscription: subscription,
64
+ )
65
+ end
66
+
67
+ it 'tracks shipping address changes' do
68
+ stub_const('Spree::Event', class_spy(Spree::Event))
69
+ subscription = create(:subscription)
70
+
71
+ subscription.update!(shipping_address: create(:address))
72
+
73
+ expect(Spree::Event).to have_received(:fire).with(
74
+ 'solidus_subscriptions.subscription_shipping_address_changed',
75
+ subscription: subscription,
76
+ )
77
+ end
78
+
79
+ it 'tracks billing address changes' do
80
+ stub_const('Spree::Event', class_spy(Spree::Event))
81
+ subscription = create(:subscription)
82
+
83
+ subscription.update!(billing_address: create(:address))
84
+
85
+ expect(Spree::Event).to have_received(:fire).with(
86
+ 'solidus_subscriptions.subscription_billing_address_changed',
87
+ subscription: subscription,
88
+ )
89
+ end
90
+
91
+ it 'tracks payment method changes' do
92
+ stub_const('Spree::Event', class_spy(Spree::Event))
93
+
94
+ subscription = create(:subscription)
95
+ subscription.update!(payment_source: create(:credit_card, user: subscription.user))
96
+
97
+ expect(Spree::Event).to have_received(:fire).with(
98
+ 'solidus_subscriptions.subscription_payment_method_changed',
99
+ subscription: subscription,
100
+ )
101
+ end
102
+ end
103
+
104
+ describe '#cancel' do
105
+ subject { subscription.cancel }
106
+
107
+ let(:subscription) do
108
+ create :subscription, :with_line_item, actionable_date: actionable_date
109
+ end
110
+
111
+ around { |e| Timecop.freeze { e.run } }
112
+
113
+ before do
114
+ allow(SolidusSubscriptions.configuration).to receive(:minimum_cancellation_notice) { minimum_cancellation_notice }
115
+ end
116
+
117
+ context 'when the subscription can be canceled' do
118
+ let(:actionable_date) { 1.month.from_now }
119
+ let(:minimum_cancellation_notice) { 1.day }
120
+
121
+ it 'is canceled' do
122
+ subject
123
+ expect(subscription).to be_canceled
124
+ end
125
+
126
+ it 'creates a subscription_canceled event' do
127
+ subject
128
+ expect(subscription.events.last).to have_attributes(event_type: 'subscription_canceled')
129
+ end
130
+ end
131
+
132
+ context 'when the subscription cannot be canceled' do
133
+ let(:actionable_date) { Date.current }
134
+ let(:minimum_cancellation_notice) { 1.day }
135
+
136
+ it 'is pending cancelation' do
137
+ subject
138
+ expect(subscription).to be_pending_cancellation
139
+ end
140
+
141
+ it 'creates a subscription_canceled event' do
142
+ subject
143
+ expect(subscription.events.last).to have_attributes(event_type: 'subscription_canceled')
144
+ end
145
+ end
146
+
147
+ context 'when the minimum cancellation date is 0.days' do
148
+ let(:actionable_date) { Date.current }
149
+ let(:minimum_cancellation_notice) { 0.days }
150
+
151
+ it 'is canceled' do
152
+ subject
153
+ expect(subscription).to be_canceled
154
+ end
155
+ end
156
+ end
157
+
158
+ describe '#skip' do
159
+ subject { subscription.skip&.to_date }
160
+
161
+ let(:total_skips) { 0 }
162
+ let(:successive_skips) { 0 }
163
+ let(:expected_date) { 2.months.from_now.to_date }
164
+
165
+ let(:subscription) do
166
+ create(
167
+ :subscription,
168
+ :with_line_item,
169
+ skip_count: total_skips,
170
+ successive_skip_count: successive_skips
171
+ )
172
+ end
173
+
174
+ before { stub_config(maximum_total_skips: 1) }
175
+
176
+ context 'when the successive skips have been exceeded' do
177
+ let(:successive_skips) { 1 }
178
+
179
+ it { is_expected.to be_falsy }
180
+
181
+ it 'adds errors to the subscription' do
182
+ subject
183
+ expect(subscription.errors[:successive_skip_count]).not_to be_empty
184
+ end
185
+
186
+ it 'does not create an event' do
187
+ expect { subject }.not_to change(subscription.events, :count)
188
+ end
189
+ end
190
+
191
+ context 'when the total skips have been exceeded' do
192
+ let(:total_skips) { 1 }
193
+
194
+ it { is_expected.to be_falsy }
195
+
196
+ it 'adds errors to the subscription' do
197
+ subject
198
+ expect(subscription.errors[:skip_count]).not_to be_empty
199
+ end
200
+
201
+ it 'does not create an event' do
202
+ expect { subject }.not_to change(subscription.events, :count)
203
+ end
204
+ end
205
+
206
+ context 'when the subscription can be skipped' do
207
+ it { is_expected.to eq expected_date }
208
+
209
+ it 'creates a subscription_skipped event' do
210
+ subject
211
+ expect(subscription.events.last).to have_attributes(event_type: 'subscription_skipped')
212
+ end
213
+ end
214
+ end
215
+
216
+ describe '#deactivate' do
217
+ subject { subscription.deactivate }
218
+
219
+ let(:attributes) { {} }
220
+ let(:subscription) do
221
+ create :subscription, :actionable, :with_line_item, attributes do |s|
222
+ s.installments = build_list(:installment, 2)
223
+ end
224
+ end
225
+
226
+ context 'when the subscription can be deactivated' do
227
+ let(:attributes) do
228
+ { end_date: Date.current.ago(2.days) }
229
+ end
230
+
231
+ it 'is inactive' do
232
+ subject
233
+ expect(subscription).to be_inactive
234
+ end
235
+
236
+ it 'creates a subscription_deactivated event' do
237
+ subject
238
+ expect(subscription.events.last).to have_attributes(event_type: 'subscription_ended')
239
+ end
240
+ end
241
+
242
+ context 'when the subscription cannot be deactivated' do
243
+ it { is_expected.to be_falsy }
244
+
245
+ it 'does not create an event' do
246
+ expect { subject }.not_to change(subscription.events, :count)
247
+ end
248
+ end
249
+ end
250
+
251
+ describe '#activate' do
252
+ context 'when the subscription can be activated' do
253
+ it 'activates the subscription' do
254
+ subscription = create(:subscription,
255
+ actionable_date: Time.zone.today,
256
+ end_date: Time.zone.yesterday,)
257
+ subscription.deactivate!
258
+
259
+ subscription.activate
260
+
261
+ expect(subscription.state).to eq('active')
262
+ end
263
+
264
+ it 'creates a subscription_activated event' do
265
+ subscription = create(:subscription,
266
+ actionable_date: Time.zone.today,
267
+ end_date: Time.zone.yesterday,)
268
+ subscription.deactivate!
269
+
270
+ subscription.activate
271
+
272
+ expect(subscription.events.last).to have_attributes(event_type: 'subscription_activated')
273
+ end
274
+ end
275
+
276
+ context 'when the subscription cannot be activated' do
277
+ it 'returns false' do
278
+ subscription = create(:subscription, actionable_date: Time.zone.today)
279
+
280
+ expect(subscription.activate).to eq(false)
281
+ end
282
+
283
+ it 'does not create an event' do
284
+ subscription = create(:subscription, actionable_date: Time.zone.today)
285
+
286
+ expect {
287
+ subscription.activate
288
+ }.not_to change(subscription.events, :count)
289
+ end
290
+ end
291
+ end
292
+
293
+ describe '#next_actionable_date' do
294
+ subject { subscription.next_actionable_date }
295
+
296
+ context "when the subscription is active" do
297
+ let(:expected_date) { Date.current + subscription.interval }
298
+ let(:subscription) do
299
+ build_stubbed(
300
+ :subscription,
301
+ :with_line_item,
302
+ actionable_date: Date.current
303
+ )
304
+ end
305
+
306
+ it { is_expected.to eq expected_date }
307
+ end
308
+
309
+ context "when the subscription is not active" do
310
+ let(:subscription) { build_stubbed :subscription, :with_line_item, state: :canceled }
311
+
312
+ it { is_expected.to be_nil }
313
+ end
314
+ end
315
+
316
+ describe '#advance_actionable_date' do
317
+ subject { subscription.advance_actionable_date }
318
+
319
+ let(:expected_date) { Date.current + subscription.interval }
320
+ let(:subscription) do
321
+ build(
322
+ :subscription,
323
+ :with_line_item,
324
+ actionable_date: Date.current
325
+ )
326
+ end
327
+
328
+ it { is_expected.to eq expected_date }
329
+
330
+ it 'updates the subscription with the new actionable date' do
331
+ subject
332
+ expect(subscription.reload).to have_attributes(
333
+ actionable_date: expected_date
334
+ )
335
+ end
336
+ end
337
+
338
+ describe ".actionable" do
339
+ subject { described_class.actionable }
340
+
341
+ let!(:past_subscription) { create :subscription, actionable_date: 2.days.ago }
342
+ let!(:future_subscription) { create :subscription, actionable_date: 1.month.from_now }
343
+ let!(:inactive_subscription) { create :subscription, state: "inactive", actionable_date: 7.days.ago }
344
+ let!(:canceled_subscription) { create :subscription, state: "canceled", actionable_date: 4.days.ago }
345
+
346
+ it "returns subscriptions that have an actionable date in the past" do
347
+ expect(subject).to include past_subscription
348
+ end
349
+
350
+ it "does not include future subscriptions" do
351
+ expect(subject).not_to include future_subscription
352
+ end
353
+
354
+ it "does not include inactive subscriptions" do
355
+ expect(subject).not_to include inactive_subscription
356
+ end
357
+
358
+ it "does not include canceled subscriptions" do
359
+ expect(subject).not_to include canceled_subscription
360
+ end
361
+ end
362
+
363
+ describe '#processing_state' do
364
+ subject { subscription.processing_state }
365
+
366
+ context 'when the subscription has never been processed' do
367
+ let(:subscription) { build_stubbed :subscription }
368
+
369
+ it { is_expected.to eq 'pending' }
370
+ end
371
+
372
+ context 'when the last processing attempt failed' do
373
+ let(:subscription) do
374
+ create(
375
+ :subscription,
376
+ installments: create_list(:installment, 1, :failed)
377
+ )
378
+ end
379
+
380
+ it { is_expected.to eq 'failed' }
381
+ end
382
+
383
+ context 'when the last processing attempt succeeded' do
384
+ let(:order) { create :completed_order_with_totals }
385
+
386
+ let(:subscription) do
387
+ create(
388
+ :subscription,
389
+ installments: create_list(
390
+ :installment,
391
+ 1,
392
+ :success,
393
+ details: build_list(:installment_detail, 1, order: order, success: true)
394
+ )
395
+ )
396
+ end
397
+
398
+ it { is_expected.to eq 'success' }
399
+ end
400
+ end
401
+
402
+ describe '.ransackable_scopes' do
403
+ subject { described_class.ransackable_scopes }
404
+
405
+ it { is_expected.to match_array [:in_processing_state] }
406
+ end
407
+
408
+ describe '.in_processing_state' do
409
+ subject { described_class.in_processing_state(state) }
410
+
411
+ let!(:new_subs) { create_list :subscription, 2 }
412
+ let!(:failed_subs) { create_list(:installment, 2, :failed).map(&:subscription) }
413
+ let!(:success_subs) { create_list(:installment, 2, :success).map(&:subscription) }
414
+
415
+ context 'with successfull subscriptions' do
416
+ let(:state) { :success }
417
+
418
+ it { is_expected.to match_array success_subs }
419
+ end
420
+
421
+ context 'with failed subscriptions' do
422
+ let(:state) { :failed }
423
+
424
+ it { is_expected.to match_array failed_subs }
425
+ end
426
+
427
+ context 'with new subscriptions' do
428
+ let(:state) { :pending }
429
+
430
+ it { is_expected.to match_array new_subs }
431
+ end
432
+
433
+ context 'with unknown state' do
434
+ let(:state) { :foo }
435
+
436
+ it 'raises an error' do
437
+ expect { subject }.to raise_error ArgumentError, /state must be one of/
438
+ end
439
+ end
440
+ end
441
+
442
+ describe '.processing_states' do
443
+ subject { described_class.processing_states }
444
+
445
+ it { is_expected.to match_array [:pending, :success, :failed] }
446
+ end
447
+
448
+ describe '#payment_source_to_use' do
449
+ context 'when the subscription has a payment method without source' do
450
+ it 'returns nil' do
451
+ payment_method = create(:check_payment_method)
452
+
453
+ subscription = create(:subscription, payment_method: payment_method)
454
+
455
+ expect(subscription.payment_source_to_use).to eq(nil)
456
+ end
457
+ end
458
+
459
+ context 'when the subscription has a payment method with a source' do
460
+ it 'returns the source on the subscription' do
461
+ user = create(:user)
462
+ payment_method = create(:credit_card_payment_method)
463
+ payment_source = create(:credit_card,
464
+ payment_method: payment_method,
465
+ gateway_customer_profile_id: 'BGS-123',
466
+ user: user,)
467
+
468
+ subscription = create(:subscription,
469
+ user: user,
470
+ payment_method: payment_method,
471
+ payment_source: payment_source,)
472
+
473
+ expect(subscription.payment_source_to_use).to eq(payment_source)
474
+ end
475
+ end
476
+
477
+ context 'when the subscription has no payment method' do
478
+ it "returns the default source from the user's wallet_payment_source" do
479
+ user = create(:user)
480
+ payment_source = create(:credit_card, gateway_customer_profile_id: 'BGS-123', user: user)
481
+ wallet_payment_source = user.wallet.add(payment_source)
482
+ user.wallet.default_wallet_payment_source = wallet_payment_source
483
+
484
+ subscription = create(:subscription, user: user)
485
+
486
+ expect(subscription.payment_source_to_use).to eq(payment_source)
487
+ end
488
+ end
489
+ end
490
+
491
+ describe '#payment_method_to_use' do
492
+ context 'when the subscription has a payment method without source' do
493
+ it 'returns the payment method on the subscription' do
494
+ payment_method = create(:check_payment_method)
495
+ subscription = create(:subscription, payment_method: payment_method)
496
+
497
+ expect(subscription.payment_method_to_use).to eq(payment_method)
498
+ end
499
+ end
500
+
501
+ context 'when the subscription has a payment method with a source' do
502
+ it 'returns the payment method on the subscription' do
503
+ user = create(:user)
504
+ payment_method = create(:credit_card_payment_method)
505
+ payment_source = create(:credit_card,
506
+ payment_method: payment_method,
507
+ gateway_customer_profile_id: 'BGS-123',
508
+ user: user,)
509
+
510
+ subscription = create(:subscription,
511
+ user: user,
512
+ payment_method: payment_method,
513
+ payment_source: payment_source,)
514
+
515
+ expect(subscription.payment_method_to_use).to eq(payment_method)
516
+ end
517
+ end
518
+
519
+ context 'when the subscription has no payment method' do
520
+ it "returns the method from the default source in the user's wallet_payment_source" do
521
+ user = create(:user)
522
+ payment_source = create(:credit_card, gateway_customer_profile_id: 'BGS-123', user: user)
523
+ wallet_payment_source = user.wallet.add(payment_source)
524
+ user.wallet.default_wallet_payment_source = wallet_payment_source
525
+
526
+ subscription = create(:subscription, user: user)
527
+
528
+ expect(subscription.payment_method_to_use).to eq(payment_source.payment_method)
529
+ end
530
+ end
531
+ end
532
+
533
+ describe '#billing_address_to_use' do
534
+ context 'when the subscription has a billing address' do
535
+ it 'returns the billing address on the subscription' do
536
+ billing_address = create(:bill_address)
537
+
538
+ subscription = create(:subscription, billing_address: billing_address)
539
+
540
+ expect(subscription.billing_address_to_use).to eq(billing_address)
541
+ end
542
+ end
543
+
544
+ context 'when the subscription has no billing address' do
545
+ it 'returns the billing address on the user' do
546
+ user = create(:user)
547
+ billing_address = create(:bill_address)
548
+ user.bill_address = billing_address
549
+
550
+ subscription = create(:subscription, user: user)
551
+
552
+ expect(subscription.billing_address_to_use).to eq(billing_address)
553
+ end
554
+ end
555
+ end
556
+
557
+ describe '#shipping_address_to_use' do
558
+ context 'when the subscription has a shipping address' do
559
+ it 'returns the shipping address on the subscription' do
560
+ shipping_address = create(:ship_address)
561
+
562
+ subscription = create(:subscription, shipping_address: shipping_address)
563
+
564
+ expect(subscription.shipping_address_to_use).to eq(shipping_address)
565
+ end
566
+ end
567
+
568
+ context 'when the subscription has no shipping address' do
569
+ it 'returns the shipping address on the user' do
570
+ user = create(:user)
571
+ shipping_address = create(:ship_address)
572
+ user.ship_address = shipping_address
573
+
574
+ subscription = create(:subscription, user: user)
575
+
576
+ expect(subscription.shipping_address_to_use).to eq(shipping_address)
577
+ end
578
+ end
579
+ end
580
+
581
+ describe "#update_actionable_date_if_interval_changed" do
582
+ context "with installments" do
583
+ context "when the last installment date would cause the interval to be in the past" do
584
+ it "sets the actionable_date to the current day" do
585
+ subscription = create(:subscription, actionable_date: Time.zone.parse('2016-08-22'))
586
+ create(:installment, subscription: subscription, created_at: Time.zone.parse('2016-07-22'))
587
+
588
+ subscription.update!(interval_length: 1, interval_units: 'month')
589
+
590
+ expect(subscription.actionable_date).to eq(Time.zone.today)
591
+ end
592
+ end
593
+
594
+ context "when the last installment date would cause the interval to be in the future" do
595
+ it "sets the actionable_date to an interval from the last installment" do
596
+ subscription = create(:subscription, actionable_date: Time.zone.parse('2016-08-22'))
597
+ create(:installment, subscription: subscription, created_at: 4.days.ago)
598
+
599
+ subscription.update!(interval_length: 1, interval_units: 'month')
600
+
601
+ expect(subscription.actionable_date).to eq((4.days.ago + 1.month).to_date)
602
+ end
603
+ end
604
+ end
605
+
606
+ context "when there are no installments" do
607
+ context "when the subscription creation date would cause the interval to be in the past" do
608
+ it "sets the actionable_date to the current day" do
609
+ subscription = create(:subscription, created_at: Time.zone.parse('2016-08-22'))
610
+
611
+ subscription.update!(interval_length: 1, interval_units: 'month')
612
+
613
+ expect(subscription.actionable_date).to eq(Time.zone.today)
614
+ end
615
+ end
616
+
617
+ context "when the subscription creation date would cause the interval to be in the future" do
618
+ it "sets the actionable_date to one interval past the subscription creation date" do
619
+ subscription = create(:subscription, created_at: 4.days.ago)
620
+
621
+ subscription.update!(interval_length: 1, interval_units: 'month')
622
+
623
+ expect(subscription.actionable_date).to eq((4.days.ago + 1.month).to_date)
624
+ end
625
+ end
626
+ end
627
+ end
628
+
629
+ describe '#failing_since' do
630
+ context 'when the subscription is not failing' do
631
+ it 'returns nil' do
632
+ subscription = create(:subscription, installments: [
633
+ create(:installment, details: [
634
+ create(:installment_detail, success: false, created_at: '2020-11-11'),
635
+ create(:installment_detail, success: false, created_at: '2020-11-12'),
636
+ create(:installment_detail, success: true, created_at: '2020-11-13'),
637
+ ]),
638
+ create(:installment, details: [
639
+ create(:installment_detail, success: false, created_at: '2020-11-24'),
640
+ create(:installment_detail, success: true, created_at: '2020-11-25'),
641
+ ]),
642
+ ])
643
+
644
+ expect(subscription.failing_since).to eq(nil)
645
+ end
646
+ end
647
+
648
+ context 'when the subscription is failing with a previous success' do
649
+ it 'returns the date of the first failure' do
650
+ subscription = create(:subscription, installments: [
651
+ create(:installment, details: [
652
+ create(:installment_detail, success: false, created_at: '2020-11-11'),
653
+ create(:installment_detail, success: false, created_at: '2020-11-12'),
654
+ create(:installment_detail, success: true, created_at: '2020-11-13'),
655
+ ]),
656
+ create(:installment, details: [
657
+ create(:installment_detail, success: false, created_at: '2020-11-24'),
658
+ create(:installment_detail, success: false, created_at: '2020-11-25'),
659
+ ]),
660
+ create(:installment, details: [
661
+ create(:installment_detail, success: false, created_at: '2020-11-26'),
662
+ create(:installment_detail, success: false, created_at: '2020-11-27'),
663
+ ]),
664
+ ])
665
+
666
+ expect(subscription.failing_since).to eq(Time.zone.parse('2020-11-24'))
667
+ end
668
+ end
669
+
670
+ context 'when the subscription is failing with no previous success' do
671
+ it 'returns the date of the first failure' do
672
+ subscription = create(:subscription, installments: [
673
+ create(:installment, details: [
674
+ create(:installment_detail, success: false, created_at: '2020-11-11'),
675
+ create(:installment_detail, success: false, created_at: '2020-11-12'),
676
+ create(:installment_detail, success: false, created_at: '2020-11-13'),
677
+ ]),
678
+ create(:installment, details: [
679
+ create(:installment_detail, success: false, created_at: '2020-11-24'),
680
+ create(:installment_detail, success: false, created_at: '2020-11-25'),
681
+ ]),
682
+ create(:installment, details: [
683
+ create(:installment_detail, success: false, created_at: '2020-11-26'),
684
+ create(:installment_detail, success: false, created_at: '2020-11-27'),
685
+ ]),
686
+ ])
687
+
688
+ expect(subscription.failing_since).to eq(Time.zone.parse('2020-11-11'))
689
+ end
690
+ end
691
+ end
692
+
693
+ describe '#maximum_reprocessing_time_reached?' do
694
+ context 'when maximum_reprocessing_time is not configured' do
695
+ it 'returns false' do
696
+ stub_config(maximum_reprocessing_time: 5.days)
697
+ subscription = create(:subscription)
698
+
699
+ expect(subscription.maximum_reprocessing_time_reached?).to eq(false)
700
+ end
701
+ end
702
+
703
+ context 'when maximum_reprocessing_time is configured' do
704
+ context 'when the subscription has been failing for too long' do
705
+ it 'returns true' do
706
+ stub_config(maximum_reprocessing_time: 15.days)
707
+
708
+ subscription = create(:subscription, installments: [
709
+ create(:installment, details: [
710
+ create(:installment_detail, success: false, created_at: 20.days.ago),
711
+ create(:installment_detail, success: false, created_at: 19.days.ago),
712
+ create(:installment_detail, success: true, created_at: 18.days.ago),
713
+ ]),
714
+ create(:installment, details: [
715
+ create(:installment_detail, success: false, created_at: 17.days.ago),
716
+ create(:installment_detail, success: false, created_at: 16.days.ago),
717
+ ]),
718
+ create(:installment, details: [
719
+ create(:installment_detail, success: false, created_at: 15.days.ago),
720
+ create(:installment_detail, success: false, created_at: 14.days.ago),
721
+ ]),
722
+ ])
723
+
724
+ expect(subscription.maximum_reprocessing_time_reached?).to eq(true)
725
+ end
726
+ end
727
+
728
+ context 'when the subscription has not been failing for too long' do
729
+ it 'returns false' do
730
+ stub_config(maximum_reprocessing_time: 15.days)
731
+
732
+ subscription = create(:subscription, installments: [
733
+ create(:installment, details: [
734
+ create(:installment_detail, success: false, created_at: 15.days.ago),
735
+ create(:installment_detail, success: false, created_at: 14.days.ago),
736
+ create(:installment_detail, success: true, created_at: 13.days.ago),
737
+ ]),
738
+ create(:installment, details: [
739
+ create(:installment_detail, success: false, created_at: 12.days.ago),
740
+ create(:installment_detail, success: false, created_at: 11.days.ago),
741
+ ]),
742
+ create(:installment, details: [
743
+ create(:installment_detail, success: false, created_at: 10.days.ago),
744
+ create(:installment_detail, success: false, created_at: 9.days.ago),
745
+ ]),
746
+ ])
747
+
748
+ expect(subscription.maximum_reprocessing_time_reached?).to eq(false)
749
+ end
750
+ end
751
+
752
+ context 'when the subscription is not failing' do
753
+ it 'returns false' do
754
+ stub_config(maximum_reprocessing_time: 2.days)
755
+
756
+ subscription = create(:subscription, installments: [
757
+ create(:installment, details: [
758
+ create(:installment_detail, success: false, created_at: 15.days.ago),
759
+ create(:installment_detail, success: false, created_at: 14.days.ago),
760
+ create(:installment_detail, success: true, created_at: 13.days.ago),
761
+ ]),
762
+ create(:installment, details: [
763
+ create(:installment_detail, success: false, created_at: 12.days.ago),
764
+ create(:installment_detail, success: false, created_at: 11.days.ago),
765
+ ]),
766
+ create(:installment, details: [
767
+ create(:installment_detail, success: false, created_at: 10.days.ago),
768
+ create(:installment_detail, success: true, created_at: 9.days.ago),
769
+ ]),
770
+ ])
771
+
772
+ expect(subscription.maximum_reprocessing_time_reached?).to eq(false)
773
+ end
774
+ end
775
+ end
776
+ end
777
+
778
+ describe '#actionable?' do
779
+ context 'when the actionable date is nil' do
780
+ it 'is not actionable' do
781
+ subscription = build_stubbed(:subscription, actionable_date: nil)
782
+
783
+ expect(subscription).not_to be_actionable
784
+ end
785
+ end
786
+
787
+ context 'when the actionable date is in the future' do
788
+ it 'is not actionable' do
789
+ subscription = build_stubbed(:subscription, actionable_date: Time.zone.today + 5.days)
790
+
791
+ expect(subscription).not_to be_actionable
792
+ end
793
+ end
794
+
795
+ context 'when the state is either canceled or inactive' do
796
+ it 'is not actionable' do
797
+ canceled_subscription = build_stubbed(:subscription, :canceled)
798
+ inactive_subscription = build_stubbed(:subscription, :inactive)
799
+
800
+ [canceled_subscription, inactive_subscription].each do |subscription|
801
+ expect(subscription).not_to be_actionable
802
+ end
803
+ end
804
+ end
805
+
806
+ context 'when the active subscription actionable date is today or in the past' do
807
+ it 'is actionable' do
808
+ subscription = build_stubbed(:subscription, actionable_date: Time.zone.today)
809
+
810
+ expect(subscription).to be_actionable
811
+ end
812
+ end
813
+ end
814
+ end