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.
- checksums.yaml +7 -0
- data/.circleci/config.yml +41 -0
- data/.gem_release.yml +5 -0
- data/.github/stale.yml +17 -0
- data/.github_changelog_generator +2 -0
- data/.gitignore +20 -0
- data/.rspec +2 -0
- data/.rubocop.yml +12 -0
- data/.rubocop_todo.yml +86 -0
- data/CHANGELOG.md +191 -0
- data/Gemfile +33 -0
- data/LICENSE +26 -0
- data/README.md +221 -0
- data/Rakefile +6 -0
- data/app/assets/javascripts/spree/backend/solidus_subscriptions.js +1 -0
- data/app/assets/javascripts/spree/backend/solidus_subscriptions/edit_subscription_payment.js +32 -0
- data/app/controllers/solidus_subscriptions/api/v1/base_controller.rb +13 -0
- data/app/controllers/solidus_subscriptions/api/v1/line_items_controller.rb +48 -0
- data/app/controllers/solidus_subscriptions/api/v1/subscriptions_controller.rb +100 -0
- data/app/controllers/spree/admin/installments_controller.rb +25 -0
- data/app/controllers/spree/admin/subscription_events_controller.rb +37 -0
- data/app/controllers/spree/admin/subscription_orders_controller.rb +35 -0
- data/app/controllers/spree/admin/subscriptions_controller.rb +100 -0
- data/app/controllers/spree/admin/users/subscriptions_controller.rb +17 -0
- data/app/decorators/controllers/solidus_subscriptions/spree/api/line_items_controller/create_subscription_line_items.rb +36 -0
- data/app/decorators/controllers/solidus_subscriptions/spree/orders_controller/create_subscription_line_items.rb +35 -0
- data/app/decorators/models/solidus_subscriptions/spree/line_item/subscription_line_items_association.rb +26 -0
- data/app/decorators/models/solidus_subscriptions/spree/order/after_create.rb +19 -0
- data/app/decorators/models/solidus_subscriptions/spree/order/finalize_creates_subscriptions.rb +23 -0
- data/app/decorators/models/solidus_subscriptions/spree/order/installment_details_association.rb +15 -0
- data/app/decorators/models/solidus_subscriptions/spree/order/subscription_association.rb +15 -0
- data/app/decorators/models/solidus_subscriptions/spree/order/subscription_line_items_association.rb +19 -0
- data/app/decorators/models/solidus_subscriptions/spree/product/delegate_subscribable.rb +17 -0
- data/app/decorators/models/solidus_subscriptions/spree/user/have_many_subscriptions.rb +30 -0
- data/app/decorators/models/solidus_subscriptions/spree/variant/auto_delete_from_subscriptions.rb +20 -0
- data/app/decorators/models/solidus_subscriptions/spree/variant/variant_pretty_name.rb +17 -0
- data/app/decorators/models/solidus_subscriptions/spree/wallet_payment_source/report_default_change_to_subscriptions.rb +28 -0
- data/app/jobs/solidus_subscriptions/process_installment_job.rb +13 -0
- data/app/jobs/solidus_subscriptions/process_subscription_job.rb +31 -0
- data/app/models/solidus_subscriptions/installment.rb +135 -0
- data/app/models/solidus_subscriptions/installment_detail.rb +28 -0
- data/app/models/solidus_subscriptions/interval.rb +26 -0
- data/app/models/solidus_subscriptions/line_item.rb +42 -0
- data/app/models/solidus_subscriptions/promotion/rules/subscription_creation_order.rb +44 -0
- data/app/models/solidus_subscriptions/promotion/rules/subscription_installment_order.rb +31 -0
- data/app/models/solidus_subscriptions/subscription.rb +392 -0
- data/app/models/solidus_subscriptions/subscription_event.rb +11 -0
- data/app/overrides/views/admin_subscribable_product_checkbox.rb +8 -0
- data/app/overrides/views/admin_subscribable_variant_checkbox.rb +8 -0
- data/app/overrides/views/admin_subscriptions_menu_link.rb +10 -0
- data/app/overrides/views/admin_users_subscriptions_tab.rb +8 -0
- data/app/overrides/views/subscription_line_item_fields.rb +8 -0
- data/app/subscribers/solidus_subscriptions/churn_buster_subscriber.rb +39 -0
- data/app/subscribers/solidus_subscriptions/event_storage_subscriber.rb +64 -0
- data/app/views/spree/admin/installments/_state_pill.html.erb +8 -0
- data/app/views/spree/admin/installments/index.html.erb +42 -0
- data/app/views/spree/admin/products/_subscribable_checkbox.html.erb +8 -0
- data/app/views/spree/admin/promotions/rules/_subscription_creation_order.html.erb +0 -0
- data/app/views/spree/admin/promotions/rules/_subscription_installment_order.html.erb +0 -0
- data/app/views/spree/admin/shared/_subscription_actions.html.erb +35 -0
- data/app/views/spree/admin/shared/_subscription_breadcrumbs.html.erb +4 -0
- data/app/views/spree/admin/shared/_subscription_sidebar.html.erb +18 -0
- data/app/views/spree/admin/shared/_subscription_tab.html.erb +3 -0
- data/app/views/spree/admin/shared/_subscription_tabs.html.erb +18 -0
- data/app/views/spree/admin/subscription_events/_state_pill.html.erb +8 -0
- data/app/views/spree/admin/subscription_events/index.html.erb +42 -0
- data/app/views/spree/admin/subscription_orders/index.html.erb +93 -0
- data/app/views/spree/admin/subscriptions/_form.html.erb +150 -0
- data/app/views/spree/admin/subscriptions/_processing_state_pill.html.erb +9 -0
- data/app/views/spree/admin/subscriptions/_state_pill.html.erb +10 -0
- data/app/views/spree/admin/subscriptions/edit.html.erb +10 -0
- data/app/views/spree/admin/subscriptions/index.html.erb +176 -0
- data/app/views/spree/admin/subscriptions/new.html.erb +5 -0
- data/app/views/spree/admin/users/_subscription_tab.html.erb +5 -0
- data/app/views/spree/admin/users/subscriptions/index.html.erb +44 -0
- data/app/views/spree/admin/variants/_subscribable_checkbox.html.erb +6 -0
- data/app/views/spree/frontend/products/_subscription_line_item_fields.html.erb +30 -0
- data/bin/console +17 -0
- data/bin/rails +7 -0
- data/bin/rails-engine +13 -0
- data/bin/rails-sandbox +16 -0
- data/bin/rake +7 -0
- data/bin/sandbox +86 -0
- data/bin/setup +8 -0
- data/config/initializers/permission_sets.rb +11 -0
- data/config/initializers/subscribers.rb +9 -0
- data/config/locales/en.yml +129 -0
- data/config/routes.rb +34 -0
- data/db/migrate/20160825164850_create_solidus_subscriptions_subscriptions.rb +11 -0
- data/db/migrate/20160825173548_create_solidus_subscriptions_line_items.rb +17 -0
- data/db/migrate/20160825202248_create_solidus_subscriptions_installments.rb +23 -0
- data/db/migrate/20160825211202_create_solidus_subscriptions_installment_details.rb +22 -0
- data/db/migrate/20160825214240_add_subscribable_to_spree_variants.rb +5 -0
- data/db/migrate/20160829201653_change_subscription_line_items_installments_to_max_installments.rb +5 -0
- data/db/migrate/20160902220242_remove_state_from_solidus_susbscriptions_installment_details.rb +5 -0
- data/db/migrate/20160902220604_add_successful_to_solidus_subscriptions_installment_details.rb +5 -0
- data/db/migrate/20160902221218_add_message_to_solidus_subscriptions_installment_details.rb +5 -0
- data/db/migrate/20160922164101_add_interval_length_and_units_to_subscription_line_items.rb +8 -0
- data/db/migrate/20161006191003_add_skip_count_to_solidus_subscriptions_subscriptions.rb +5 -0
- data/db/migrate/20161006191127_add_successive_skip_count_to_solidus_subscriptions_subscriptions.rb +5 -0
- data/db/migrate/20161014212649_allow_spree_line_item_id_to_be_null.rb +5 -0
- data/db/migrate/20161017155749_add_order_id_to_solidus_subscriptions_installment_details.rb +6 -0
- data/db/migrate/20161017175509_remove_order_id_from_solidus_subscriptions_installments.rb +5 -0
- data/db/migrate/20161017201944_add_subscription_order_to_spree_orders.rb +5 -0
- data/db/migrate/20161221155142_add_store_to_solidus_subscriptions_subscriptions.rb +6 -0
- data/db/migrate/20161223152905_add_address_id_to_solidus_subscriptions_subscriptions.rb +7 -0
- data/db/migrate/20170106224713_change_line_item_max_installments_to_end_date.rb +6 -0
- data/db/migrate/20170111224458_change_subscription_actionable_date_to_datetime.rb +5 -0
- data/db/migrate/20170111232801_change_inteval_actionable_date_to_datetime.rb +5 -0
- data/db/migrate/20170112012407_add_config_options_to_subscriptions.rb +7 -0
- data/db/migrate/20200617102749_add_billing_address_to_subscriptions.rb +11 -0
- data/db/migrate/20200617155042_add_payment_source_to_subscriptions.rb +6 -0
- data/db/migrate/20200618092951_add_payment_method_to_subscriptions.rb +11 -0
- data/db/migrate/20200730101242_create_solidus_subscriptions_subscription_events.rb +22 -0
- data/db/migrate/20200917072152_add_subscription_reference_to_orders.rb +11 -0
- data/db/migrate/20201007140032_add_guest_token_to_subscriptions.rb +6 -0
- data/db/migrate/20201123171026_change_actionable_date_to_date.rb +15 -0
- data/db/migrate/20210205140422_add_currency_to_subscription.rb +5 -0
- data/db/migrate/20210323165714_update_promotion_rule_names.rb +22 -0
- data/lib/generators/solidus_subscriptions/install/install_generator.rb +32 -0
- data/lib/generators/solidus_subscriptions/install/templates/initializer.rb +99 -0
- data/lib/solidus_subscriptions.rb +49 -0
- data/lib/solidus_subscriptions/checkout.rb +74 -0
- data/lib/solidus_subscriptions/churn_buster/client.rb +48 -0
- data/lib/solidus_subscriptions/churn_buster/order_serializer.rb +19 -0
- data/lib/solidus_subscriptions/churn_buster/serializer.rb +23 -0
- data/lib/solidus_subscriptions/churn_buster/subscription_customer_serializer.rb +28 -0
- data/lib/solidus_subscriptions/churn_buster/subscription_payment_method_serializer.rb +37 -0
- data/lib/solidus_subscriptions/churn_buster/subscription_serializer.rb +17 -0
- data/lib/solidus_subscriptions/configuration.rb +84 -0
- data/lib/solidus_subscriptions/dispatcher/base.rb +18 -0
- data/lib/solidus_subscriptions/dispatcher/failure_dispatcher.rb +13 -0
- data/lib/solidus_subscriptions/dispatcher/out_of_stock_dispatcher.rb +11 -0
- data/lib/solidus_subscriptions/dispatcher/payment_failed_dispatcher.rb +19 -0
- data/lib/solidus_subscriptions/dispatcher/success_dispatcher.rb +17 -0
- data/lib/solidus_subscriptions/engine.rb +56 -0
- data/lib/solidus_subscriptions/permission_sets/default_customer.rb +19 -0
- data/lib/solidus_subscriptions/permission_sets/subscription_management.rb +12 -0
- data/lib/solidus_subscriptions/permitted_attributes.rb +20 -0
- data/lib/solidus_subscriptions/processor.rb +17 -0
- data/lib/solidus_subscriptions/subscription_generator.rb +77 -0
- data/lib/solidus_subscriptions/subscription_line_item_builder.rb +23 -0
- data/lib/solidus_subscriptions/testing_support/factories/installment_detail_factory.rb +11 -0
- data/lib/solidus_subscriptions/testing_support/factories/installment_factory.rb +29 -0
- data/lib/solidus_subscriptions/testing_support/factories/line_item_factory.rb +20 -0
- data/lib/solidus_subscriptions/testing_support/factories/spree/line_item_factory.rb +17 -0
- data/lib/solidus_subscriptions/testing_support/factories/spree/order_factory.rb +17 -0
- data/lib/solidus_subscriptions/testing_support/factories/spree_modification_factory.rb +10 -0
- data/lib/solidus_subscriptions/testing_support/factories/subscription_event_factory.rb +8 -0
- data/lib/solidus_subscriptions/testing_support/factories/subscription_factory.rb +56 -0
- data/lib/solidus_subscriptions/version.rb +5 -0
- data/lib/tasks/process_subscriptions.rake +8 -0
- data/reference/solidus_subscriptions.v1.yaml +290 -0
- data/solidus_subscriptions.gemspec +47 -0
- data/spec/controllers/spree/admin/subscriptions_controller_spec.rb +202 -0
- data/spec/controllers/spree/api/line_items_controller_spec.rb +103 -0
- data/spec/controllers/spree/api/orders_controller_spec.rb +57 -0
- data/spec/controllers/spree/api/users_controller_spec.rb +48 -0
- data/spec/decorators/controllers/solidus_subscriptions/spree/orders_controller/create_subscription_line_items_spec.rb +80 -0
- data/spec/decorators/models/solidus_subscriptions/spree/line_item/subscription_line_items_association_spec.rb +10 -0
- data/spec/decorators/models/solidus_subscriptions/spree/order/finalize_creates_subscriptions_spec.rb +32 -0
- data/spec/decorators/models/solidus_subscriptions/spree/order/installment_details_association_spec.rb +9 -0
- data/spec/decorators/models/solidus_subscriptions/spree/order/subscription_line_items_association_spec.rb +9 -0
- data/spec/decorators/models/solidus_subscriptions/spree/user/have_many_subscriptions_spec.rb +22 -0
- data/spec/decorators/models/solidus_subscriptions/spree/variant/auto_delete_from_subscriptions_spec.rb +25 -0
- data/spec/features/admin/subscription_orders_spec.rb +35 -0
- data/spec/features/admin/subscriptions_spec.rb +63 -0
- data/spec/features/admin_users_subscription_tabs_spec.rb +61 -0
- data/spec/fixtures/cassettes/churn_buster.yml +229 -0
- data/spec/jobs/solidus_subscriptions/process_installment_job_spec.rb +38 -0
- data/spec/jobs/solidus_subscriptions/process_subscription_job_spec.rb +83 -0
- data/spec/lib/solidus_subscriptions/checkout_spec.rb +125 -0
- data/spec/lib/solidus_subscriptions/churn_buster/client_spec.rb +59 -0
- data/spec/lib/solidus_subscriptions/dispatcher/failure_dispatcher_spec.rb +29 -0
- data/spec/lib/solidus_subscriptions/dispatcher/out_of_stock_dispatcher_spec.rb +15 -0
- data/spec/lib/solidus_subscriptions/dispatcher/payment_failed_dispatcher_spec.rb +44 -0
- data/spec/lib/solidus_subscriptions/dispatcher/success_dispatcher_spec.rb +30 -0
- data/spec/lib/solidus_subscriptions/permission_sets/default_customer_spec.rb +95 -0
- data/spec/lib/solidus_subscriptions/permission_sets/subscription_management_spec.rb +26 -0
- data/spec/lib/solidus_subscriptions/processor_spec.rb +34 -0
- data/spec/lib/solidus_subscriptions/promotion/rules/subscription_creation_order_spec.rb +57 -0
- data/spec/lib/solidus_subscriptions/promotion/rules/subscription_installment_order_spec.rb +39 -0
- data/spec/lib/solidus_subscriptions/subscription_generator_spec.rb +83 -0
- data/spec/lib/solidus_subscriptions_spec.rb +30 -0
- data/spec/models/solidus_subscriptions/installment_detail_spec.rb +23 -0
- data/spec/models/solidus_subscriptions/installment_spec.rb +201 -0
- data/spec/models/solidus_subscriptions/line_item_spec.rb +29 -0
- data/spec/models/solidus_subscriptions/subscription_spec.rb +814 -0
- data/spec/models/spree/variant_spec.rb +16 -0
- data/spec/models/spree/wallet_payment_source_spec.rb +20 -0
- data/spec/requests/api/v1/line_items_spec.rb +116 -0
- data/spec/requests/api/v1/subscriptions_spec.rb +255 -0
- data/spec/spec_helper.rb +30 -0
- data/spec/subscribers/solidus_subscriptions/churn_buster_subscriber_spec.rb +76 -0
- data/spec/support/active_model_mocks.rb +1 -0
- data/spec/support/cancancan.rb +1 -0
- data/spec/support/factories.rb +1 -0
- data/spec/support/helpers/checkout_infrastructure.rb +18 -0
- data/spec/support/helpers/config.rb +13 -0
- data/spec/support/shoulda.rb +7 -0
- data/spec/support/timecop.rb +1 -0
- data/spec/support/vcr.rb +10 -0
- data/spec/support/version_cake.rb +8 -0
- metadata +498 -0
data/app/decorators/models/solidus_subscriptions/spree/variant/auto_delete_from_subscriptions.rb
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidusSubscriptions
|
|
4
|
+
module Spree
|
|
5
|
+
module Variant
|
|
6
|
+
module AutoDeleteFromSubscriptions
|
|
7
|
+
def self.prepended(base)
|
|
8
|
+
base.after_discard(:remove_from_subscriptions)
|
|
9
|
+
base.after_destroy(:remove_from_subscriptions)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def remove_from_subscriptions
|
|
13
|
+
SolidusSubscriptions::LineItem.where(subscribable: self).delete_all
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
Spree::Variant.prepend(SolidusSubscriptions::Spree::Variant::AutoDeleteFromSubscriptions)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidusSubscriptions
|
|
4
|
+
module Spree
|
|
5
|
+
module Variant
|
|
6
|
+
module VariantPrettyName
|
|
7
|
+
def pretty_name
|
|
8
|
+
name = product.name
|
|
9
|
+
name += " - #{options_text}" if options_text.present?
|
|
10
|
+
name
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
Spree::Variant.prepend(SolidusSubscriptions::Spree::Variant::VariantPrettyName)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidusSubscriptions
|
|
4
|
+
module Spree
|
|
5
|
+
module WalletPaymentSource
|
|
6
|
+
module ReportDefaultChangeToSubscriptions
|
|
7
|
+
def self.prepended(base)
|
|
8
|
+
base.after_save :report_default_change_to_subscriptions
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def report_default_change_to_subscriptions
|
|
14
|
+
return if !previous_changes.key?('default') || !default?
|
|
15
|
+
|
|
16
|
+
user.subscriptions.with_default_payment_source.each do |subscription|
|
|
17
|
+
::Spree::Event.fire(
|
|
18
|
+
'solidus_subscriptions.subscription_payment_method_changed',
|
|
19
|
+
subscription: subscription,
|
|
20
|
+
)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
Spree::WalletPaymentSource.prepend(SolidusSubscriptions::Spree::WalletPaymentSource::ReportDefaultChangeToSubscriptions)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidusSubscriptions
|
|
4
|
+
class ProcessInstallmentJob < ApplicationJob
|
|
5
|
+
queue_as { SolidusSubscriptions.configuration.processing_queue }
|
|
6
|
+
|
|
7
|
+
def perform(installment)
|
|
8
|
+
Checkout.new(installment).process
|
|
9
|
+
rescue StandardError => e
|
|
10
|
+
SolidusSubscriptions.configuration.processing_error_handler&.call(e)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidusSubscriptions
|
|
4
|
+
class ProcessSubscriptionJob < ApplicationJob
|
|
5
|
+
queue_as { SolidusSubscriptions.configuration.processing_queue }
|
|
6
|
+
|
|
7
|
+
def perform(subscription)
|
|
8
|
+
ActiveRecord::Base.transaction do
|
|
9
|
+
if SolidusSubscriptions.configuration.clear_past_installments
|
|
10
|
+
subscription.installments.unfulfilled.actionable.each do |installment|
|
|
11
|
+
installment.update!(actionable_date: nil)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
if subscription.actionable?
|
|
16
|
+
subscription.successive_skip_count = 0
|
|
17
|
+
subscription.advance_actionable_date
|
|
18
|
+
|
|
19
|
+
subscription.installments.create!(actionable_date: Time.zone.now)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
subscription.cancel! if subscription.pending_cancellation?
|
|
23
|
+
subscription.deactivate! if subscription.can_be_deactivated?
|
|
24
|
+
|
|
25
|
+
subscription.installments.actionable.find_each do |installment|
|
|
26
|
+
SolidusSubscriptions::ProcessInstallmentJob.perform_later(installment)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# This class represents a single iteration of a subscription. It is fulfilled
|
|
4
|
+
# by a completed order and maintains an association which tracks all attempts
|
|
5
|
+
# successful or otherwise at fulfilling this installment
|
|
6
|
+
module SolidusSubscriptions
|
|
7
|
+
class Installment < ApplicationRecord
|
|
8
|
+
has_many :details, class_name: 'SolidusSubscriptions::InstallmentDetail'
|
|
9
|
+
belongs_to(
|
|
10
|
+
:subscription,
|
|
11
|
+
class_name: 'SolidusSubscriptions::Subscription',
|
|
12
|
+
inverse_of: :installments,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
validates :subscription, presence: true
|
|
16
|
+
|
|
17
|
+
scope :fulfilled, (lambda do
|
|
18
|
+
joins(:details).where(InstallmentDetail.table_name => { success: true }).distinct
|
|
19
|
+
end)
|
|
20
|
+
|
|
21
|
+
scope :unfulfilled, (lambda do
|
|
22
|
+
where.not(id: Installment.fulfilled).distinct
|
|
23
|
+
end)
|
|
24
|
+
|
|
25
|
+
scope :with_active_subscription, (lambda do
|
|
26
|
+
joins(:subscription).where.not(Subscription.table_name => { state: "canceled" })
|
|
27
|
+
end)
|
|
28
|
+
|
|
29
|
+
scope :actionable, (lambda do
|
|
30
|
+
unfulfilled.where("#{table_name}.actionable_date <= ?", Time.zone.today)
|
|
31
|
+
end)
|
|
32
|
+
|
|
33
|
+
# Mark this installment as out of stock.
|
|
34
|
+
#
|
|
35
|
+
# @return [SolidusSubscriptions::InstallmentDetail] The record of the failed
|
|
36
|
+
# processing attempt
|
|
37
|
+
def out_of_stock
|
|
38
|
+
advance_actionable_date!
|
|
39
|
+
|
|
40
|
+
details.create!(
|
|
41
|
+
success: false,
|
|
42
|
+
message: I18n.t('solidus_subscriptions.installment_details.out_of_stock')
|
|
43
|
+
)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Mark this installment as a success
|
|
47
|
+
#
|
|
48
|
+
# @param order [Spree::Order] The order generated for this processing
|
|
49
|
+
# attempt
|
|
50
|
+
#
|
|
51
|
+
# @return [SolidusSubscriptions::InstallmentDetail] The record of the
|
|
52
|
+
# successful processing attempt
|
|
53
|
+
def success!(order)
|
|
54
|
+
update!(actionable_date: nil)
|
|
55
|
+
|
|
56
|
+
details.create!(
|
|
57
|
+
success: true,
|
|
58
|
+
order: order,
|
|
59
|
+
message: I18n.t('solidus_subscriptions.installment_details.success')
|
|
60
|
+
)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Mark this installment as a failure
|
|
64
|
+
#
|
|
65
|
+
# @param order [Spree::Order] The order generated for this processing
|
|
66
|
+
# attempt
|
|
67
|
+
#
|
|
68
|
+
# @return [SolidusSubscriptions::InstallmentDetail] The record of the
|
|
69
|
+
# failed processing attempt
|
|
70
|
+
def failed!(order)
|
|
71
|
+
advance_actionable_date!
|
|
72
|
+
|
|
73
|
+
details.create!(
|
|
74
|
+
success: false,
|
|
75
|
+
order: order,
|
|
76
|
+
message: I18n.t('solidus_subscriptions.installment_details.failed')
|
|
77
|
+
)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Does this installment still need to be fulfilled by a completed order
|
|
81
|
+
#
|
|
82
|
+
# @return [Boolean]
|
|
83
|
+
def unfulfilled?
|
|
84
|
+
!fulfilled?
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Had this installment been fulfilled by a completed order
|
|
88
|
+
#
|
|
89
|
+
# @return [Boolean]
|
|
90
|
+
def fulfilled?
|
|
91
|
+
details.exists?(success: true)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Returns the state of this fulfillment
|
|
95
|
+
#
|
|
96
|
+
# @return [Symbol] :fulfilled/:unfulfilled
|
|
97
|
+
def state
|
|
98
|
+
fulfilled? ? :fulfilled : :unfulfilled
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Mark this installment as having a failed payment
|
|
102
|
+
#
|
|
103
|
+
# @param order [Spree::Order] The order generated for this processing
|
|
104
|
+
# attempt
|
|
105
|
+
#
|
|
106
|
+
# @return [SolidusSubscriptions::InstallmentDetail] The record of the
|
|
107
|
+
# failed processing attempt
|
|
108
|
+
def payment_failed!(order)
|
|
109
|
+
details.create!(
|
|
110
|
+
success: false,
|
|
111
|
+
order: order,
|
|
112
|
+
message: I18n.t('solidus_subscriptions.installment_details.payment_failed')
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
if subscription.maximum_reprocessing_time_reached? && !subscription.canceled?
|
|
116
|
+
subscription.force_cancel!
|
|
117
|
+
update!(actionable_date: nil)
|
|
118
|
+
else
|
|
119
|
+
advance_actionable_date!
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
private
|
|
124
|
+
|
|
125
|
+
def advance_actionable_date!
|
|
126
|
+
update!(actionable_date: next_actionable_date)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def next_actionable_date
|
|
130
|
+
return if SolidusSubscriptions.configuration.reprocessing_interval.nil?
|
|
131
|
+
|
|
132
|
+
(DateTime.current + SolidusSubscriptions.configuration.reprocessing_interval).beginning_of_minute
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# This class represents a single attempt to fulfill an installment. It will
|
|
4
|
+
# indicate the result of that attempt.
|
|
5
|
+
module SolidusSubscriptions
|
|
6
|
+
class InstallmentDetail < ApplicationRecord
|
|
7
|
+
belongs_to(
|
|
8
|
+
:installment,
|
|
9
|
+
class_name: 'SolidusSubscriptions::Installment',
|
|
10
|
+
inverse_of: :details
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
belongs_to(:order, class_name: '::Spree::Order', optional: true)
|
|
14
|
+
|
|
15
|
+
validates :installment, presence: true
|
|
16
|
+
alias_attribute :successful, :success
|
|
17
|
+
|
|
18
|
+
scope :succeeded, -> { where success: true }
|
|
19
|
+
scope :failed, -> { where success: false }
|
|
20
|
+
|
|
21
|
+
# Was the attempt at fulfilling this installment a failure?
|
|
22
|
+
#
|
|
23
|
+
# @return [Boolean]
|
|
24
|
+
def failed?
|
|
25
|
+
!success
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# This module is intended to be included into any active record
|
|
4
|
+
# model which needs to be aware of how intervals are stored and
|
|
5
|
+
# calculated in the db.
|
|
6
|
+
#
|
|
7
|
+
# Base models must have the following fields: interval_length (integer) and interval_units (integer)
|
|
8
|
+
module SolidusSubscriptions
|
|
9
|
+
module Interval
|
|
10
|
+
def self.included(base)
|
|
11
|
+
base.enum interval_units: {
|
|
12
|
+
day: 0,
|
|
13
|
+
week: 1,
|
|
14
|
+
month: 2,
|
|
15
|
+
year: 3
|
|
16
|
+
}
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Calculates the number of seconds in the interval.
|
|
20
|
+
#
|
|
21
|
+
# @return [Integer] The number of seconds.
|
|
22
|
+
def interval
|
|
23
|
+
ActiveSupport::Duration.new(interval_length, { interval_units.pluralize.to_sym => interval_length })
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# The LineItem class is responsible for associating Line items to subscriptions. # It tracks the following values:
|
|
4
|
+
#
|
|
5
|
+
# [Spree::LineItem] :spree_line_item The spree object which created this instance
|
|
6
|
+
#
|
|
7
|
+
# [SolidusSubscription::Subscription] :subscription The object responsible for
|
|
8
|
+
# grouping all information needed to create new subscription orders together
|
|
9
|
+
#
|
|
10
|
+
# [Integer] :subscribable_id The id of the object to be added to new subscription
|
|
11
|
+
# orders when they are placed
|
|
12
|
+
#
|
|
13
|
+
# [Integer] :quantity How many units of the subscribable should be included in
|
|
14
|
+
# future orders
|
|
15
|
+
#
|
|
16
|
+
# [Integer] :interval How often subscription orders should be placed
|
|
17
|
+
#
|
|
18
|
+
# [Integer] :installments How many subscription orders should be placed
|
|
19
|
+
module SolidusSubscriptions
|
|
20
|
+
class LineItem < ApplicationRecord
|
|
21
|
+
include Interval
|
|
22
|
+
|
|
23
|
+
belongs_to(
|
|
24
|
+
:spree_line_item,
|
|
25
|
+
class_name: '::Spree::LineItem',
|
|
26
|
+
inverse_of: :subscription_line_items,
|
|
27
|
+
optional: true,
|
|
28
|
+
)
|
|
29
|
+
has_one :order, through: :spree_line_item, class_name: '::Spree::Order'
|
|
30
|
+
belongs_to(
|
|
31
|
+
:subscription,
|
|
32
|
+
class_name: 'SolidusSubscriptions::Subscription',
|
|
33
|
+
inverse_of: :line_items,
|
|
34
|
+
optional: true
|
|
35
|
+
)
|
|
36
|
+
belongs_to :subscribable, class_name: "::#{SolidusSubscriptions.configuration.subscribable_class}"
|
|
37
|
+
|
|
38
|
+
validates :subscribable_id, presence: true
|
|
39
|
+
validates :quantity, numericality: { greater_than: 0 }
|
|
40
|
+
validates :interval_length, numericality: { greater_than: 0 }, unless: -> { subscription }
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidusSubscriptions
|
|
4
|
+
module Promotion
|
|
5
|
+
module Rules
|
|
6
|
+
class SubscriptionCreationOrder < ::Spree::PromotionRule
|
|
7
|
+
# Promotion can be applied to an entire order. Will only be true
|
|
8
|
+
# for Spree::Order
|
|
9
|
+
#
|
|
10
|
+
# @param promotable [Object] Any object which could have this
|
|
11
|
+
# promotion rule applied to it.
|
|
12
|
+
#
|
|
13
|
+
# @return [Boolean]
|
|
14
|
+
def applicable?(promotable)
|
|
15
|
+
promotable.is_a? ::Spree::Order
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# An order is eligible if it contains a line item with an associates
|
|
19
|
+
# subscription_line_item. This rule applies to order and so its eligibility
|
|
20
|
+
# will always be considered against an order. Will only return true for
|
|
21
|
+
# orders containing Spree::Line item with associated subscription_line_items
|
|
22
|
+
#
|
|
23
|
+
# @param order [Spree::Order] The order which could have this rule applied
|
|
24
|
+
# to it.
|
|
25
|
+
#
|
|
26
|
+
# @return [Boolean]
|
|
27
|
+
def eligible?(order, **_options)
|
|
28
|
+
order.subscription_line_items.any?
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Certain actions create adjustments on line items. In this case, only
|
|
32
|
+
# line items with associated subscription_line_items are eligible to be
|
|
33
|
+
# adjusted. Will only return true # if :line_item has an associated
|
|
34
|
+
# subscription.
|
|
35
|
+
#
|
|
36
|
+
# @param line_item [Spree::LineItem] The line item which could be adjusted
|
|
37
|
+
# by the promotion.
|
|
38
|
+
def actionable?(line_item)
|
|
39
|
+
line_item.subscription_line_items.present?
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidusSubscriptions
|
|
4
|
+
module Promotion
|
|
5
|
+
module Rules
|
|
6
|
+
class SubscriptionInstallmentOrder < ::Spree::PromotionRule
|
|
7
|
+
# Promotion can be applied to an entire order. Will only be true
|
|
8
|
+
# for Spree::Order
|
|
9
|
+
#
|
|
10
|
+
# @param promotable [Object] Any object which could have this
|
|
11
|
+
# promotion rule applied to it.
|
|
12
|
+
#
|
|
13
|
+
# @return [Boolean]
|
|
14
|
+
def applicable?(promotable)
|
|
15
|
+
promotable.is_a? ::Spree::Order
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# An order is eligible if it fulfills a subscription Installment. Will only
|
|
19
|
+
# return true if the order fulfills one or more Installments
|
|
20
|
+
#
|
|
21
|
+
# @param order [Spree::Order] The order which could have this rule applied
|
|
22
|
+
# to it.
|
|
23
|
+
#
|
|
24
|
+
# @return [Boolean]
|
|
25
|
+
def eligible?(order, **_options)
|
|
26
|
+
order.subscription_order?
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# The subscription class is responsible for grouping together the
|
|
4
|
+
# information required for the system to place a subscriptions order on
|
|
5
|
+
# behalf of a specific user.
|
|
6
|
+
module SolidusSubscriptions
|
|
7
|
+
class Subscription < ApplicationRecord
|
|
8
|
+
include Interval
|
|
9
|
+
|
|
10
|
+
PROCESSING_STATES = [:pending, :failed, :success].freeze
|
|
11
|
+
|
|
12
|
+
belongs_to :user, class_name: "::#{::Spree.user_class}"
|
|
13
|
+
has_many :line_items, class_name: 'SolidusSubscriptions::LineItem', inverse_of: :subscription
|
|
14
|
+
has_many :installments, class_name: 'SolidusSubscriptions::Installment'
|
|
15
|
+
has_many :installment_details, class_name: 'SolidusSubscriptions::InstallmentDetail', through: :installments, source: :details
|
|
16
|
+
has_many :events, class_name: 'SolidusSubscriptions::SubscriptionEvent'
|
|
17
|
+
has_many :orders, class_name: '::Spree::Order', inverse_of: :subscription
|
|
18
|
+
belongs_to :store, class_name: '::Spree::Store'
|
|
19
|
+
belongs_to :shipping_address, class_name: '::Spree::Address', optional: true
|
|
20
|
+
belongs_to :billing_address, class_name: '::Spree::Address', optional: true
|
|
21
|
+
belongs_to :payment_method, class_name: '::Spree::PaymentMethod', optional: true
|
|
22
|
+
belongs_to :payment_source, polymorphic: true, optional: true
|
|
23
|
+
|
|
24
|
+
validates :user, presence: true
|
|
25
|
+
validates :skip_count, :successive_skip_count, presence: true, numericality: { greater_than_or_equal_to: 0 }
|
|
26
|
+
validates :interval_length, numericality: { greater_than: 0 }
|
|
27
|
+
validates :payment_method, presence: true, if: -> { payment_source }
|
|
28
|
+
validates :payment_source, presence: true, if: -> { payment_method&.source_required? }
|
|
29
|
+
validates :currency, inclusion: { in: ::Money::Currency.all.map(&:iso_code) }
|
|
30
|
+
|
|
31
|
+
validate :validate_payment_source_ownership
|
|
32
|
+
|
|
33
|
+
accepts_nested_attributes_for :shipping_address
|
|
34
|
+
accepts_nested_attributes_for :billing_address
|
|
35
|
+
accepts_nested_attributes_for :line_items, allow_destroy: true, reject_if: ->(p) { p[:quantity].blank? }
|
|
36
|
+
|
|
37
|
+
before_validation :set_payment_method
|
|
38
|
+
before_validation :set_currency
|
|
39
|
+
before_create :generate_guest_token
|
|
40
|
+
after_create :emit_event_for_creation
|
|
41
|
+
before_update :update_actionable_date_if_interval_changed
|
|
42
|
+
after_update :emit_events_for_update
|
|
43
|
+
|
|
44
|
+
# Find all subscriptions that are "actionable"; that is, ones that have an
|
|
45
|
+
# actionable_date in the past and are not invalid or canceled.
|
|
46
|
+
scope :actionable, (lambda do
|
|
47
|
+
where("#{table_name}.actionable_date <= ?", Time.zone.today).
|
|
48
|
+
where.not(state: ["canceled", "inactive"])
|
|
49
|
+
end)
|
|
50
|
+
|
|
51
|
+
# Find subscriptions based on their processing state. This state is not a
|
|
52
|
+
# model attribute.
|
|
53
|
+
#
|
|
54
|
+
# @param state [Symbol] One of :pending, :success, or failed
|
|
55
|
+
#
|
|
56
|
+
# pending: New subscriptions, never been processed
|
|
57
|
+
# failed: Subscriptions which failed to be processed on the last attempt
|
|
58
|
+
# success: Subscriptions which were successfully processed on the last attempt
|
|
59
|
+
scope :in_processing_state, (lambda do |state|
|
|
60
|
+
case state.to_sym
|
|
61
|
+
when :success
|
|
62
|
+
fulfilled.joins(:installments)
|
|
63
|
+
when :failed
|
|
64
|
+
fulfilled_ids = fulfilled.pluck(:id)
|
|
65
|
+
where.not(id: fulfilled_ids)
|
|
66
|
+
when :pending
|
|
67
|
+
includes(:installments).where(solidus_subscriptions_installments: { id: nil })
|
|
68
|
+
else
|
|
69
|
+
raise ArgumentError, "state must be one of: :success, :failed, :pending"
|
|
70
|
+
end
|
|
71
|
+
end)
|
|
72
|
+
|
|
73
|
+
scope :fulfilled, (lambda do
|
|
74
|
+
unfulfilled_ids = unfulfilled.pluck(:id)
|
|
75
|
+
where.not(id: unfulfilled_ids)
|
|
76
|
+
end)
|
|
77
|
+
|
|
78
|
+
scope :unfulfilled, (lambda do
|
|
79
|
+
joins(:installments).merge(Installment.unfulfilled)
|
|
80
|
+
end)
|
|
81
|
+
|
|
82
|
+
scope :with_default_payment_source, (lambda do
|
|
83
|
+
where(payment_method: nil, payment_source: nil)
|
|
84
|
+
end)
|
|
85
|
+
|
|
86
|
+
def self.ransackable_scopes(_auth_object = nil)
|
|
87
|
+
[:in_processing_state]
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def self.processing_states
|
|
91
|
+
PROCESSING_STATES
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# The subscription state determines the behaviours around when it is
|
|
95
|
+
# processed. Here is a brief description of the states and how they affect
|
|
96
|
+
# the subscription.
|
|
97
|
+
#
|
|
98
|
+
# [active] Default state when created. Subscription can be processed
|
|
99
|
+
# [canceled] The user has ended their subscription. Subscription will not
|
|
100
|
+
# be processed.
|
|
101
|
+
# [pending_cancellation] The user has ended their subscription, but the
|
|
102
|
+
# conditions for canceling the subscription have not been met. Subscription
|
|
103
|
+
# will continue to be processed until the subscription is canceled and
|
|
104
|
+
# the conditions are met.
|
|
105
|
+
# [inactive] The number of installments has been fulfilled. The subscription
|
|
106
|
+
# will no longer be processed
|
|
107
|
+
state_machine :state, initial: :active do
|
|
108
|
+
event :cancel do
|
|
109
|
+
transition [:active, :pending_cancellation] => :canceled,
|
|
110
|
+
if: ->(subscription) { subscription.can_be_canceled? }
|
|
111
|
+
|
|
112
|
+
transition active: :pending_cancellation
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
event :force_cancel do
|
|
116
|
+
transition [:active, :pending_cancellation] => :canceled
|
|
117
|
+
transition inactive: :inactive
|
|
118
|
+
transition canceled: :canceled
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
after_transition to: :canceled, do: :advance_actionable_date
|
|
122
|
+
|
|
123
|
+
event :deactivate do
|
|
124
|
+
transition active: :inactive,
|
|
125
|
+
if: ->(subscription) { subscription.can_be_deactivated? }
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
event :activate do
|
|
129
|
+
transition any - [:active] => :active
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
after_transition to: :active, do: :advance_actionable_date
|
|
133
|
+
after_transition do: :emit_event_for_transition
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# This method determines if a subscription may be canceled. Canceled
|
|
137
|
+
# subcriptions will not be processed. By default subscriptions may always be
|
|
138
|
+
# canceled. If this method is overridden to return false, the subscription
|
|
139
|
+
# will be moved to the :pending_cancellation state until it is canceled
|
|
140
|
+
# again and this condition is true.
|
|
141
|
+
#
|
|
142
|
+
# USE CASE: Subscriptions can only be canceled more than 10 days before they
|
|
143
|
+
# are processed. Override this method to be:
|
|
144
|
+
#
|
|
145
|
+
# def can_be_canceled?
|
|
146
|
+
# return true if actionable_date.nil?
|
|
147
|
+
# (actionable_date - 10.days.from_now.to_date) > 0
|
|
148
|
+
# end
|
|
149
|
+
#
|
|
150
|
+
# If a user cancels this subscription less than 10 days before it will
|
|
151
|
+
# be processed the subscription will be bumped into the
|
|
152
|
+
# :pending_cancellation state instead of being canceled. Subscriptions
|
|
153
|
+
# pending cancellation will still be processed.
|
|
154
|
+
def can_be_canceled?
|
|
155
|
+
return true if actionable_date.nil?
|
|
156
|
+
|
|
157
|
+
cancel_by = actionable_date - SolidusSubscriptions.configuration.minimum_cancellation_notice
|
|
158
|
+
cancel_by.future? || cancel_by.today?
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def skip(check_skip_limits: true)
|
|
162
|
+
if check_skip_limits
|
|
163
|
+
check_successive_skips_exceeded
|
|
164
|
+
check_total_skips_exceeded
|
|
165
|
+
|
|
166
|
+
return if errors.any?
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
increment(:skip_count)
|
|
170
|
+
increment(:successive_skip_count)
|
|
171
|
+
save!
|
|
172
|
+
|
|
173
|
+
advance_actionable_date.tap do
|
|
174
|
+
events.create!(event_type: 'subscription_skipped')
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# This method determines if a subscription can be deactivated. A deactivated
|
|
179
|
+
# subscription will not be processed. By default a subscription can be
|
|
180
|
+
# deactivated if the end_date defined on
|
|
181
|
+
# the subscription is less than the current date
|
|
182
|
+
# In this case the subscription has been fulfilled and
|
|
183
|
+
# should not be processed again. Subscriptions without an end_date
|
|
184
|
+
# value cannot be deactivated.
|
|
185
|
+
def can_be_deactivated?
|
|
186
|
+
active? && end_date && actionable_date && actionable_date > end_date
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Get the date after the current actionable_date where this subscription
|
|
190
|
+
# will be actionable again
|
|
191
|
+
#
|
|
192
|
+
# @return [Date] The current actionable_date plus 1 interval. The next
|
|
193
|
+
# date after the current actionable_date this subscription will be
|
|
194
|
+
# eligible to be processed.
|
|
195
|
+
def next_actionable_date
|
|
196
|
+
return nil unless active?
|
|
197
|
+
|
|
198
|
+
new_date = actionable_date || Time.zone.today
|
|
199
|
+
|
|
200
|
+
new_date + interval
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Advance the actionable date to the next_actionable_date value. Will modify
|
|
204
|
+
# the record.
|
|
205
|
+
#
|
|
206
|
+
# @return [Date] The next date after the current actionable_date this
|
|
207
|
+
# subscription will be eligible to be processed.
|
|
208
|
+
def advance_actionable_date
|
|
209
|
+
update! actionable_date: next_actionable_date
|
|
210
|
+
|
|
211
|
+
actionable_date
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# The state of the last attempt to process an installment associated to
|
|
215
|
+
# this subscription
|
|
216
|
+
#
|
|
217
|
+
# @return [String] pending if the no installments have been processed,
|
|
218
|
+
# failed if the last installment has not been fulfilled and, success
|
|
219
|
+
# if the last installment was fulfilled.
|
|
220
|
+
def processing_state
|
|
221
|
+
return 'pending' if installments.empty?
|
|
222
|
+
|
|
223
|
+
installments.last.fulfilled? ? 'success' : 'failed'
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def payment_method_to_use
|
|
227
|
+
payment_method || user.wallet.default_wallet_payment_source&.payment_source&.payment_method
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def payment_source_to_use
|
|
231
|
+
if payment_method
|
|
232
|
+
payment_source
|
|
233
|
+
else
|
|
234
|
+
user.wallet.default_wallet_payment_source&.payment_source
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def shipping_address_to_use
|
|
239
|
+
shipping_address || user.ship_address
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def billing_address_to_use
|
|
243
|
+
billing_address || user.bill_address
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def failing_since
|
|
247
|
+
failing_details = installment_details.failed.order('solidus_subscriptions_installment_details.created_at ASC')
|
|
248
|
+
|
|
249
|
+
last_successful_detail = installment_details
|
|
250
|
+
.succeeded
|
|
251
|
+
.order('solidus_subscriptions_installment_details.created_at DESC')
|
|
252
|
+
.first
|
|
253
|
+
if last_successful_detail
|
|
254
|
+
failing_details = failing_details.where(
|
|
255
|
+
'solidus_subscriptions_installment_details.created_at > ?',
|
|
256
|
+
last_successful_detail.created_at,
|
|
257
|
+
)
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
first_failing_detail = failing_details.first
|
|
261
|
+
|
|
262
|
+
first_failing_detail&.created_at
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def maximum_reprocessing_time_reached?
|
|
266
|
+
return false unless SolidusSubscriptions.configuration.maximum_reprocessing_time
|
|
267
|
+
return false unless failing_since
|
|
268
|
+
|
|
269
|
+
Time.zone.now > (failing_since + SolidusSubscriptions.configuration.maximum_reprocessing_time)
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def actionable?
|
|
273
|
+
actionable_date && actionable_date <= Time.zone.today && ["canceled", "inactive"].exclude?(state)
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
private
|
|
277
|
+
|
|
278
|
+
def validate_payment_source_ownership
|
|
279
|
+
return if payment_source.blank?
|
|
280
|
+
|
|
281
|
+
if payment_source.respond_to?(:user_id) &&
|
|
282
|
+
payment_source.user_id != user_id
|
|
283
|
+
errors.add(:payment_source, :not_owned_by_user)
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def check_successive_skips_exceeded
|
|
288
|
+
return unless SolidusSubscriptions.configuration.maximum_successive_skips
|
|
289
|
+
|
|
290
|
+
if successive_skip_count >= SolidusSubscriptions.configuration.maximum_successive_skips
|
|
291
|
+
errors.add(:successive_skip_count, :exceeded)
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def check_total_skips_exceeded
|
|
296
|
+
return unless SolidusSubscriptions.configuration.maximum_total_skips
|
|
297
|
+
|
|
298
|
+
if skip_count >= SolidusSubscriptions.configuration.maximum_total_skips
|
|
299
|
+
errors.add(:skip_count, :exceeded)
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def update_actionable_date_if_interval_changed
|
|
304
|
+
if persisted? && (interval_length_previously_changed? || interval_units_previously_changed?)
|
|
305
|
+
base_date = if installments.any?
|
|
306
|
+
installments.last.created_at
|
|
307
|
+
else
|
|
308
|
+
created_at
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
new_date = interval.since(base_date)
|
|
312
|
+
|
|
313
|
+
if new_date < Time.zone.now
|
|
314
|
+
# if the chosen base time plus the new interval is in the past, set
|
|
315
|
+
# the actionable_date to be now to avoid confusion and possible
|
|
316
|
+
# mis-processing.
|
|
317
|
+
new_date = Time.zone.now
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
self.actionable_date = new_date
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def set_payment_method
|
|
325
|
+
if payment_source
|
|
326
|
+
self.payment_method = payment_source.payment_method
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def set_currency
|
|
331
|
+
self.currency ||= ::Spree::Config[:currency]
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def generate_guest_token
|
|
335
|
+
self.guest_token ||= loop do
|
|
336
|
+
random_token = SecureRandom.urlsafe_base64(nil, false)
|
|
337
|
+
break random_token unless self.class.exists?(guest_token: random_token)
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
def emit_event_for_creation
|
|
342
|
+
::Spree::Event.fire(
|
|
343
|
+
'solidus_subscriptions.subscription_created',
|
|
344
|
+
subscription: self,
|
|
345
|
+
)
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def emit_event_for_transition
|
|
349
|
+
event_type = {
|
|
350
|
+
active: 'subscription_activated',
|
|
351
|
+
canceled: 'subscription_canceled',
|
|
352
|
+
pending_cancellation: 'subscription_canceled',
|
|
353
|
+
inactive: 'subscription_ended',
|
|
354
|
+
}[state.to_sym]
|
|
355
|
+
|
|
356
|
+
::Spree::Event.fire(
|
|
357
|
+
"solidus_subscriptions.#{event_type}",
|
|
358
|
+
subscription: self,
|
|
359
|
+
)
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
def emit_events_for_update
|
|
363
|
+
if previous_changes.key?('interval_length') || previous_changes.key?('interval_units')
|
|
364
|
+
::Spree::Event.fire(
|
|
365
|
+
'solidus_subscriptions.subscription_frequency_changed',
|
|
366
|
+
subscription: self,
|
|
367
|
+
)
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
if previous_changes.key?('shipping_address_id')
|
|
371
|
+
::Spree::Event.fire(
|
|
372
|
+
'solidus_subscriptions.subscription_shipping_address_changed',
|
|
373
|
+
subscription: self,
|
|
374
|
+
)
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
if previous_changes.key?('billing_address_id')
|
|
378
|
+
::Spree::Event.fire(
|
|
379
|
+
'solidus_subscriptions.subscription_billing_address_changed',
|
|
380
|
+
subscription: self,
|
|
381
|
+
)
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
if previous_changes.key?('payment_source_id') || previous_changes.key?('payment_source_type') || previous_changes.key?('payment_method_id')
|
|
385
|
+
::Spree::Event.fire(
|
|
386
|
+
'solidus_subscriptions.subscription_payment_method_changed',
|
|
387
|
+
subscription: self,
|
|
388
|
+
)
|
|
389
|
+
end
|
|
390
|
+
end
|
|
391
|
+
end
|
|
392
|
+
end
|