diff options
14 files changed, 355 insertions, 0 deletions
diff --git a/app/controllers/spree/admin/subscription_events_controller.rb b/app/controllers/spree/admin/subscription_events_controller.rb new file mode 100644 index 0000000..ad86887 --- /dev/null +++ b/app/controllers/spree/admin/subscription_events_controller.rb @@ -0,0 +1,35 @@ +module Spree + module Admin + class SubscriptionEventsController < ResourceController + belongs_to 'subscription', model_class: SolidusSubscriptions::Subscription + + skip_before_action :load_resource, only: :index + + def index + @search = collection.ransack((params[:q] || {}).reverse_merge(s: 'created_at desc')) + + @subscription_events = @search.result(distinct: true). + page(params[:page]). + per(params[:per_page] || 20) + end + + private + + def model_class + ::SolidusSubscriptions::SubscriptionEvent + end + + def find_resource + parent.events.find(params[:id]) + end + + def build_resource + parent.events.build + end + + def collection + parent.events + end + end + end +end diff --git a/app/models/solidus_subscriptions/line_item.rb b/app/models/solidus_subscriptions/line_item.rb index 9ef045b..0477877 100644 --- a/app/models/solidus_subscriptions/line_item.rb +++ b/app/models/solidus_subscriptions/line_item.rb @@ -36,6 +36,10 @@ module SolidusSubscriptions validates :quantity, numericality: { greater_than: 0 } validates :interval_length, numericality: { greater_than: 0 }, unless: -> { subscription } + after_create :track_creation_event + after_update :track_update_event + after_destroy :track_destroy_event + def as_json(**options) options[:methods] ||= [:dummy_line_item] super(options) @@ -64,5 +68,36 @@ module SolidusSubscriptions order.freeze end + + def as_json_for_event + as_json.with_indifferent_access.except( + :dummy_line_item, + :interval_units, + :interval_length, + :end_date, + :spree_line_item_id, + :subscription_id, + :created_at, + :updated_at, + ) + end + + def track_creation_event + return unless subscription + + subscription.events.create!(event_type: 'line_item_created', details: as_json_for_event) + end + + def track_update_event + return unless subscription + + subscription.events.create!(event_type: 'line_item_updated', details: as_json_for_event) + end + + def track_destroy_event + return unless subscription + + subscription.events.create!(event_type: 'line_item_destroyed', details: as_json_for_event) + end end end diff --git a/app/models/solidus_subscriptions/subscription.rb b/app/models/solidus_subscriptions/subscription.rb index 33a58f4..d19d624 100644 --- a/app/models/solidus_subscriptions/subscription.rb +++ b/app/models/solidus_subscriptions/subscription.rb @@ -10,6 +10,7 @@ module SolidusSubscriptions belongs_to :user, class_name: "::#{::Spree.user_class}" has_many :line_items, class_name: 'SolidusSubscriptions::LineItem', inverse_of: :subscription has_many :installments, class_name: 'SolidusSubscriptions::Installment' + has_many :events, class_name: 'SolidusSubscriptions::SubscriptionEvent' belongs_to :store, class_name: '::Spree::Store' belongs_to :shipping_address, class_name: '::Spree::Address', optional: true belongs_to :billing_address, class_name: '::Spree::Address', optional: true @@ -28,6 +29,8 @@ module SolidusSubscriptions before_validation :set_payment_method before_update :update_actionable_date_if_interval_changed + after_create :track_creation_event + after_update :track_update_event # Find all subscriptions that are "actionable"; that is, ones that have an # actionable_date in the past and are not invalid or canceled. @@ -108,6 +111,7 @@ module SolidusSubscriptions end after_transition to: :active, do: :advance_actionable_date + after_transition do: :track_transition_event end # This method determines if a subscription may be canceled. Canceled @@ -172,6 +176,9 @@ module SolidusSubscriptions # subscription will be eligible to be processed. def advance_actionable_date update! actionable_date: next_actionable_date + + events.create!(event_type: 'subscription_skipped') + actionable_date end @@ -259,5 +266,28 @@ module SolidusSubscriptions self.payment_method = payment_source.payment_method end end + + def as_json_for_event + as_json + end + + def track_creation_event + events.create!(event_type: 'subscription_created', details: as_json_for_event) + end + + def track_update_event + events.create!(event_type: 'subscription_updated', details: as_json_for_event) + end + + def track_transition_event + event_type = { + active: 'subscription_activated', + canceled: 'subscription_canceled', + pending_cancellation: 'subscription_cancellation_requested', + inactive: 'subscription_deactivated', + }[state.to_sym] + + events.create!(event_type: event_type, details: as_json_for_event) + end end end diff --git a/app/models/solidus_subscriptions/subscription_event.rb b/app/models/solidus_subscriptions/subscription_event.rb new file mode 100644 index 0000000..93552fd --- /dev/null +++ b/app/models/solidus_subscriptions/subscription_event.rb @@ -0,0 +1,22 @@ +module SolidusSubscriptions + class SubscriptionEvent < ApplicationRecord + belongs_to :subscription, class_name: 'SolidusSubscriptions::Subscription', inverse_of: :events + + after_initialize do + self.details ||= {} + end + + after_create :emit_event + + private + + def emit_event + return unless defined?(::Spree::Event) + + ::Spree::Event.fire( + "solidus_subscriptions.#{event_type}", + details.deep_symbolize_keys.merge(subscription: subscription), + ) + end + end +end diff --git a/app/views/spree/admin/shared/_subscription_tabs.html.erb b/app/views/spree/admin/shared/_subscription_tabs.html.erb index 7c00486..f1d8e0f 100644 --- a/app/views/spree/admin/shared/_subscription_tabs.html.erb +++ b/app/views/spree/admin/shared/_subscription_tabs.html.erb @@ -7,6 +7,9 @@ <li<%== ' class="active"' if current == :installments %>> <%= link_to t("spree.admin.subscriptions.edit.installments"), spree.admin_subscription_installments_path(subscription) %> </li> + <li<%== ' class="active"' if current == :events %>> + <%= link_to t("spree.admin.subscriptions.edit.events"), spree.admin_subscription_subscription_events_path(subscription) %> + </li> </ul> </nav> <% end %> diff --git a/app/views/spree/admin/subscription_events/_state_pill.html.erb b/app/views/spree/admin/subscription_events/_state_pill.html.erb new file mode 100644 index 0000000..08a86a3 --- /dev/null +++ b/app/views/spree/admin/subscription_events/_state_pill.html.erb @@ -0,0 +1,8 @@ +<% state_class = { + fulfilled: 'active', + unfulfilled: 'error', +}[installment.state.to_sym] %> + +<span class="pill pill-<%= state_class %>"> + <%= SolidusSubscriptions::Installment.human_attribute_name("state.#{installment.state}") %> +</span> diff --git a/app/views/spree/admin/subscription_events/index.html.erb b/app/views/spree/admin/subscription_events/index.html.erb new file mode 100644 index 0000000..547088e --- /dev/null +++ b/app/views/spree/admin/subscription_events/index.html.erb @@ -0,0 +1,42 @@ +<% content_for(:page_title, t('.title')) %> + +<%= render 'spree/admin/shared/subscription_breadcrumbs', subscription: @subscription %> +<%= render 'spree/admin/shared/subscription_sidebar', subscription: @subscription %> +<%= render 'spree/admin/shared/subscription_tabs', current: :events, subscription: @subscription %> +<%= render 'spree/admin/shared/subscription_actions', subscription: @subscription %> + +<fieldset class="no-border-bottom"> + <legend><%= t('spree.admin.subscription_events.index.title') %></legend> + + <%= paginate @subscription_events, theme: 'solidus_admin' %> + + <table id="listing_subscription_events" class="index"> + <thead> + <tr data-hook="admin_subscription_events_index_headers"> + <th><%= SolidusSubscriptions::SubscriptionEvent.human_attribute_name(:event_type) %></th> + <th><%= SolidusSubscriptions::SubscriptionEvent.human_attribute_name(:details) %></th> + <th><%= sort_link(@search, :created_at, SolidusSubscriptions::SubscriptionEvent.human_attribute_name(:created_at)) %></th> + </tr> + </thead> + + <tbody> + <% @subscription_events.each do |event| %> + <tr> + <td><%= event.event_type %></td> + <td> + <% if event.details.is_a?(Hash) %> + <% event.details.each_pair do |key, value| %> + <strong><%= key %></strong>: <%= value %><br> + <% end %> + <% else %> + <%= event.details %> + <% end %> + </td> + <td><%= l event.created_at %></td> + </tr> + <% end %> + </tbody> + </table> + + <%= paginate @subscription_events, theme: 'solidus_admin' %> +</fieldset> diff --git a/config/locales/en.yml b/config/locales/en.yml index e132d3f..06efcd6 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -40,6 +40,7 @@ en: sidebar: Status details: Details installments: Installments + events: Events payment: Payment new: back: Back to Subscriptions List @@ -52,6 +53,9 @@ en: installments: index: title: Installments + subscription_events: + index: + title: Events promotion_rule_types: subscription_promotion_rule: name: Subscription diff --git a/config/routes.rb b/config/routes.rb index d8878c2..eb4c8f3 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -23,6 +23,7 @@ Spree::Core::Engine.routes.draw do post :activate, on: :member post :skip, on: :member resources :installments, only: [:index, :show] + resources :subscription_events, only: :index end end end diff --git a/db/migrate/20200730101242_create_solidus_subscriptions_subscription_events.rb b/db/migrate/20200730101242_create_solidus_subscriptions_subscription_events.rb new file mode 100644 index 0000000..62de91e --- /dev/null +++ b/db/migrate/20200730101242_create_solidus_subscriptions_subscription_events.rb @@ -0,0 +1,17 @@ +class CreateSolidusSubscriptionsSubscriptionEvents < ActiveRecord::Migration[5.2] + def change + create_table :solidus_subscriptions_subscription_events do |t| + t.belongs_to( + :subscription, + null: false, + foreign_key: { to_table: :solidus_subscriptions_subscriptions }, + index: { name: :idx_solidus_subscription_events_on_subscription_id }, + type: :integer, + ) + t.string :event_type, null: false + t.json :details, null: false + + t.timestamps + end + end +end diff --git a/lib/solidus_subscriptions/testing_support/factories/subscription_event_factory.rb b/lib/solidus_subscriptions/testing_support/factories/subscription_event_factory.rb new file mode 100644 index 0000000..0158e2e --- /dev/null +++ b/lib/solidus_subscriptions/testing_support/factories/subscription_event_factory.rb @@ -0,0 +1,6 @@ +FactoryBot.define do + factory :subscription_event, class: 'SolidusSubscriptions::SubscriptionEvent' do + subscription + event_type { 'test_event' } + end +end diff --git a/spec/models/solidus_subscriptions/line_item_spec.rb b/spec/models/solidus_subscriptions/line_item_spec.rb index 42349fc..a0b8845 100644 --- a/spec/models/solidus_subscriptions/line_item_spec.rb +++ b/spec/models/solidus_subscriptions/line_item_spec.rb @@ -10,6 +10,48 @@ RSpec.describe SolidusSubscriptions::LineItem, type: :model do it { is_expected.to validate_numericality_of(:quantity).is_greater_than(0) } it { is_expected.to validate_numericality_of(:interval_length).is_greater_than(0) } + describe '#save!' do + context 'when the line item is new' do + it 'tracks a line_item_created event' do + line_item = build(:subscription_line_item, :with_subscription) + + line_item.save! + + expect(line_item.subscription.events.last).to have_attributes( + event_type: 'line_item_created', + details: a_hash_including('id' => line_item.id), + ) + end + end + + context 'when the line item is persisted' do + it 'tracks a line_item_updated event' do + line_item = create(:subscription_line_item, :with_subscription) + + line_item.quantity = 2 + line_item.save! + + expect(line_item.subscription.events.last).to have_attributes( + event_type: 'line_item_updated', + details: a_hash_including('id' => line_item.id), + ) + end + end + end + + describe '#destroy!' do + it 'tracks a line_item_destroyed event' do + line_item = create(:subscription_line_item, :with_subscription) + + line_item.destroy! + + expect(line_item.subscription.events.last).to have_attributes( + event_type: 'line_item_destroyed', + details: a_hash_including('id' => line_item.id), + ) + end + end + describe "#interval" do let(:line_item) { create :subscription_line_item, :with_subscription } before do diff --git a/spec/models/solidus_subscriptions/subscription_event_spec.rb b/spec/models/solidus_subscriptions/subscription_event_spec.rb new file mode 100644 index 0000000..d4602f3 --- /dev/null +++ b/spec/models/solidus_subscriptions/subscription_event_spec.rb @@ -0,0 +1,15 @@ +require 'spec_helper' + +RSpec.describe SolidusSubscriptions::SubscriptionEvent do + describe '#save' do + it 'emits a Solidus event' do + event_klass = class_spy('Spree::Event') + stub_const('Spree::Event', event_klass) + + subscription = create(:subscription) + subscription_event = create(:subscription_event, subscription: subscription, event_type: 'test_event', details: { foo: 'bar' }) + + expect(event_klass).to have_received(:fire).with('solidus_subscriptions.test_event', subscription: subscription_event.subscription, foo: 'bar') + end + end +end diff --git a/spec/models/solidus_subscriptions/subscription_spec.rb b/spec/models/solidus_subscriptions/subscription_spec.rb index 09ea53f..ffda96a 100644 --- a/spec/models/solidus_subscriptions/subscription_spec.rb +++ b/spec/models/solidus_subscriptions/subscription_spec.rb @@ -15,6 +15,35 @@ RSpec.describe SolidusSubscriptions::Subscription, type: :model do it { is_expected.to accept_nested_attributes_for :line_items } + describe '#save' do + context 'when the subscription is new' do + it 'tracks a subscription_created event' do + subscription = build(:subscription) + + subscription.save! + + expect(subscription.events.last).to have_attributes( + event_type: 'subscription_created', + details: a_hash_including('id' => subscription.id), + ) + end + end + + context 'when the line item is persisted' do + it 'tracks a subscription_updated event' do + subscription = create(:subscription) + + subscription.end_date = Time.zone.tomorrow + subscription.save! + + expect(subscription.events.last).to have_attributes( + event_type: 'subscription_updated', + details: a_hash_including('id' => subscription.id), + ) + end + end + end + describe '#cancel' do subject { subscription.cancel } @@ -31,6 +60,11 @@ RSpec.describe SolidusSubscriptions::Subscription, type: :model do subject expect(subscription.canceled?).to be_truthy end + + it 'creates a subscription_canceled event' do + subject + expect(subscription.events.last).to have_attributes(event_type: 'subscription_canceled') + end end context 'the subscription cannot be canceled' do @@ -40,6 +74,11 @@ RSpec.describe SolidusSubscriptions::Subscription, type: :model do subject expect(subscription.pending_cancellation?).to be_truthy end + + it 'creates a subscription_cancellation_requested event' do + subject + expect(subscription.events.last).to have_attributes(event_type: 'subscription_cancellation_requested') + end end end @@ -116,10 +155,61 @@ RSpec.describe SolidusSubscriptions::Subscription, type: :model do subject expect(subscription.inactive?).to be_truthy end + + it 'creates a subscription_deactivated event' do + subject + expect(subscription.events.last).to have_attributes(event_type: 'subscription_deactivated') + end end context 'the subscription cannot be deactivated' do it { is_expected.to be_falsy } + + it 'does not create an event' do + expect { subject }.not_to change(subscription.events, :count) + end + end + end + + describe '#activate' do + context 'when the subscription can be activated' do + it 'activates the subscription' do + subscription = create(:subscription, + actionable_date: Time.zone.today, + end_date: Time.zone.yesterday,) + subscription.deactivate! + + subscription.activate + + expect(subscription.state).to eq('active') + end + + it 'creates a subscription_activated event' do + subscription = create(:subscription, + actionable_date: Time.zone.today, + end_date: Time.zone.yesterday,) + subscription.deactivate! + + subscription.activate + + expect(subscription.events.last).to have_attributes(event_type: 'subscription_activated') + end + end + + context 'the subscription cannot be activated' do + it 'returns false' do + subscription = create(:subscription, actionable_date: Time.zone.today) + + expect(subscription.activate).to eq(false) + end + + it 'does not create an event' do + subscription = create(:subscription, actionable_date: Time.zone.today) + + expect { + subscription.activate + }.not_to change(subscription.events, :count) + end end end @@ -165,6 +255,11 @@ RSpec.describe SolidusSubscriptions::Subscription, type: :model do actionable_date: expected_date ) end + + it 'creates a subscription_skipped event' do + subject + expect(subscription.events.last).to have_attributes(event_type: 'subscription_skipped') + end end describe ".actionable" do |