diff --git a/Gemfile.lock b/Gemfile.lock index 7df48d8a..89304638 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -275,6 +275,7 @@ GEM thor (0.19.4) thread_safe (0.3.5) tilt (2.0.7) + timecop (0.9.1) turbolinks (5.0.1) turbolinks-source (~> 5) turbolinks-source (5.0.3) @@ -305,8 +306,9 @@ DEPENDENCIES rubocop shoulda sqlite3 + timecop vcr webmock BUNDLED WITH - 1.15.0 + 1.15.3 diff --git a/app/controllers/bookingsync_portal/admin/connections_controller.rb b/app/controllers/bookingsync_portal/admin/connections_controller.rb index 65ff66bd..6b237698 100644 --- a/app/controllers/bookingsync_portal/admin/connections_controller.rb +++ b/app/controllers/bookingsync_portal/admin/connections_controller.rb @@ -6,7 +6,12 @@ def create new_remote_rental = BookingsyncPortal.remote_rental_model.constantize.new(remote_account: remote_account) @connection = rental.create_connection(remote_rental: new_remote_rental) else - @connection = rental.create_connection(remote_rental: remote_rental) + @connection = + if remote_rental.connection_canceled? + remote_rental.connection.restore + else + rental.create_connection(remote_rental: remote_rental) + end end respond_to do |wants| @@ -16,7 +21,7 @@ def create end def destroy - @connection = current_account.connections.find(params[:id]).destroy + @connection = current_account.connections.find(params[:id]).cancel @not_connected_rentals = current_account.rentals.visible.ordered.not_connected @visible_rentals = current_account.rentals.visible diff --git a/app/models/bookingsync_portal/connection.rb b/app/models/bookingsync_portal/connection.rb index b0e33f06..7f44b41f 100644 --- a/app/models/bookingsync_portal/connection.rb +++ b/app/models/bookingsync_portal/connection.rb @@ -1,4 +1,6 @@ class BookingsyncPortal::Connection < ActiveRecord::Base + include BookingsyncPortal::Cancelable + self.table_name = 'connections' belongs_to :remote_rental, class_name: BookingsyncPortal.remote_rental_model diff --git a/app/models/bookingsync_portal/remote_rental.rb b/app/models/bookingsync_portal/remote_rental.rb index 175815ae..a20cf6dd 100644 --- a/app/models/bookingsync_portal/remote_rental.rb +++ b/app/models/bookingsync_portal/remote_rental.rb @@ -14,15 +14,23 @@ class BookingsyncPortal::RemoteRental < ActiveRecord::Base validates :remote_account, presence: true scope :ordered, -> { order(created_at: :desc) } - scope :connected, -> { joins(:rental) } - scope :not_connected, -> { includes(:rental).where(rentals: { id: nil }) } + scope :connected, -> { joins(:rental).where(connections: { canceled_at: nil }) } + scope :not_connected, -> { + includes(:rental) + .where("connections.canceled_at IS NOT NULL OR rentals.id IS NULL") + .references(:rental) + } def display_name uid end def connected? - rental.present? + rental.present? && connection.visible? + end + + def connection_canceled? + rental.present? && connection.canceled? end def synchronized? diff --git a/app/models/bookingsync_portal/rental.rb b/app/models/bookingsync_portal/rental.rb index 3249194b..2b445b7f 100644 --- a/app/models/bookingsync_portal/rental.rb +++ b/app/models/bookingsync_portal/rental.rb @@ -12,12 +12,16 @@ class BookingsyncPortal::Rental < ActiveRecord::Base validates :synced_id, uniqueness: true, presence: true scope :ordered, -> { order(position: :asc) } - scope :connected, -> { joins(:remote_rental) } - scope :not_connected, -> { includes(:connection).where(connections: { remote_rental_id: nil }) } + scope :connected, -> { joins(:remote_rental).where(connections: { canceled_at: nil }) } + scope :not_connected, -> { + includes(:connection) + .where("connections.canceled_at IS NOT NULL OR connections.remote_rental_id IS NULL") + .references(:connection) + } scope :visible, -> { all } def connected? - remote_rental.present? + remote_rental.present? && connection.visible? end def ordered_photos diff --git a/app/models/concerns/bookingsync_portal/cancelable.rb b/app/models/concerns/bookingsync_portal/cancelable.rb new file mode 100644 index 00000000..96ee09a9 --- /dev/null +++ b/app/models/concerns/bookingsync_portal/cancelable.rb @@ -0,0 +1,27 @@ +module BookingsyncPortal + module Cancelable + extend ActiveSupport::Concern + + included do + scope :visible, -> { where(canceled_at: nil) } + scope :not_canceled, -> { where(canceled_at: nil) } + scope :canceled, -> { where.not(canceled_at: nil) } + end + + def canceled? + canceled_at.present? + end + + def visible? + !canceled? + end + + def cancel(time = Time.current) + update_attribute(:canceled_at, time) + end + + def restore + cancel(nil) + end + end +end diff --git a/bookingsync_portal.gemspec b/bookingsync_portal.gemspec index db5f052f..3e0a511e 100644 --- a/bookingsync_portal.gemspec +++ b/bookingsync_portal.gemspec @@ -42,6 +42,7 @@ Gem::Specification.new do |s| s.add_development_dependency 'webmock' s.add_development_dependency 'sqlite3' s.add_development_dependency 'rubocop' + s.add_development_dependency 'timecop' s.add_development_dependency 'pry-rails' s.add_development_dependency 'rails-controller-testing' end diff --git a/db/migrate/20170824173005_add_canceled_at_to_connections.rb b/db/migrate/20170824173005_add_canceled_at_to_connections.rb new file mode 100644 index 00000000..52026030 --- /dev/null +++ b/db/migrate/20170824173005_add_canceled_at_to_connections.rb @@ -0,0 +1,5 @@ +class AddCanceledAtToConnections < ActiveRecord::Migration[5.0] + def change + add_column :connections, :canceled_at, :datetime + end +end diff --git a/gemfiles/rails_5.0.gemfile.lock b/gemfiles/rails_5.0.gemfile.lock index 79c5c786..52c17dce 100644 --- a/gemfiles/rails_5.0.gemfile.lock +++ b/gemfiles/rails_5.0.gemfile.lock @@ -278,6 +278,7 @@ GEM thor (0.19.4) thread_safe (0.3.6) tilt (2.0.7) + timecop (0.9.1) turbolinks (5.0.1) turbolinks-source (~> 5) turbolinks-source (5.0.3) @@ -309,8 +310,9 @@ DEPENDENCIES rubocop shoulda sqlite3 + timecop vcr webmock BUNDLED WITH - 1.15.0 + 1.15.3 diff --git a/gemfiles/rails_5.1.gemfile.lock b/gemfiles/rails_5.1.gemfile.lock index 2c3e8aec..29a0f347 100644 --- a/gemfiles/rails_5.1.gemfile.lock +++ b/gemfiles/rails_5.1.gemfile.lock @@ -278,6 +278,7 @@ GEM thor (0.19.4) thread_safe (0.3.6) tilt (2.0.7) + timecop (0.9.1) turbolinks (5.0.1) turbolinks-source (~> 5) turbolinks-source (5.0.3) @@ -309,8 +310,9 @@ DEPENDENCIES rubocop shoulda sqlite3 + timecop vcr webmock BUNDLED WITH - 1.15.0 + 1.15.3 diff --git a/spec/controllers/admin/connections_controller_spec.rb b/spec/controllers/admin/connections_controller_spec.rb index 0b7cf37b..7a4ff41f 100644 --- a/spec/controllers/admin/connections_controller_spec.rb +++ b/spec/controllers/admin/connections_controller_spec.rb @@ -92,6 +92,14 @@ expect(rental.remote_rental).to eq remote_rental end + context 'when connection is canceled' do + let(:connection) { create(:connection, remote_rental: remote_rental, rental: rental, canceled_at: Time.current) } + + it 'restores connection' do + expect { action }.to change { connection.reload.visible? }.to(true) + end + end + context 'when only remote rental does not belong to current account' do let!(:remote_rental) { create :remote_rental } @@ -131,8 +139,8 @@ context 'when current_account is owner' do let(:account) { connection.rental.account } - it 'allows to destroy connection' do - expect { action }.to change { BookingsyncPortal::Connection.count }.by(-1) + it 'allows to cancel connection' do + expect { action }.to change { connection.reload.canceled? }.to(true) end end diff --git a/spec/dummy/db/schema.rb b/spec/dummy/db/schema.rb index 9143b5bd..c0fae999 100644 --- a/spec/dummy/db/schema.rb +++ b/spec/dummy/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20160301141356) do +ActiveRecord::Schema.define(version: 20170824173005) do create_table "accounts", force: :cascade do |t| t.string "provider" @@ -39,6 +39,7 @@ t.integer "rental_id" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.datetime "canceled_at" t.index ["remote_rental_id"], name: "index_connections_on_remote_rental_id" t.index ["rental_id"], name: "index_connections_on_rental_id" end diff --git a/spec/factories/connections.rb b/spec/factories/connections.rb index 7e49043b..9198886d 100644 --- a/spec/factories/connections.rb +++ b/spec/factories/connections.rb @@ -2,5 +2,11 @@ factory :connection, class: BookingsyncPortal.connection_model do rental remote_rental { build(:remote_rental, account: @instance.rental.account) } + + trait :canceled do + canceled_at Time.current + end + + factory :canceled_connection, traits: [:canceled] end end diff --git a/spec/models/connection_spec.rb b/spec/models/connection_spec.rb index 58c4a3c1..a5433be8 100644 --- a/spec/models/connection_spec.rb +++ b/spec/models/connection_spec.rb @@ -7,6 +7,8 @@ it { is_expected.to validate_presence_of :remote_rental } it { is_expected.to validate_presence_of :rental } + it_behaves_like "cancelable" + context 'when remote rental and rental belong to different accounts' do let(:remote_rental) { build(:remote_rental) } let(:rental) { build(:rental) } diff --git a/spec/models/remote_rental_spec.rb b/spec/models/remote_rental_spec.rb index e1348f78..ed310e9a 100644 --- a/spec/models/remote_rental_spec.rb +++ b/spec/models/remote_rental_spec.rb @@ -11,6 +11,28 @@ it { is_expected.to validate_uniqueness_of(:uid).allow_nil } it { is_expected.to validate_presence_of(:remote_account) } + describe '.connected' do + subject { described_class.connected } + let(:connection_1) { create(:connection) } + let(:remote_rental_1) { connection_1.remote_rental } + let(:connection_2) { create(:canceled_connection) } + let(:remote_rental_2) { connection_2.remote_rental } + let(:remote_rental_3) { create(:remote_rental, rental: nil) } + + it { is_expected.to eq [remote_rental_1] } + end + + describe '.not_connected' do + subject { described_class.not_connected } + let(:connection_1) { create(:connection) } + let(:remote_rental_1) { connection_1.remote_rental } + let(:connection_2) { create(:canceled_connection) } + let(:remote_rental_2) { connection_2.remote_rental } + let(:remote_rental_3) { create(:remote_rental, rental: nil) } + + it { is_expected.to eq [remote_rental_2, remote_rental_3] } + end + describe '#connected?' do subject { remote_rental.connected? } context 'when rental present' do @@ -24,6 +46,26 @@ end end + describe '#connection_canceled?' do + subject { remote_rental.connection_canceled? } + context 'when rental present and connection is visible' do + let(:connection) { create(:connection) } + let(:remote_rental) { connection.remote_rental } + it { is_expected.to eq(false) } + end + + context 'when rental present and connection is canceled' do + let(:connection) { create(:canceled_connection) } + let(:remote_rental) { connection.remote_rental } + it { is_expected.to eq(true) } + end + + context 'when rental is nil' do + let(:remote_rental) { build(:remote_rental, rental: nil) } + it { is_expected.to eq(false) } + end + end + describe '#synchronized?' do subject { remote_rental.synchronized? } context 'when synchronized_at is present' do diff --git a/spec/models/rental_spec.rb b/spec/models/rental_spec.rb index 1a36f04d..d78d0760 100644 --- a/spec/models/rental_spec.rb +++ b/spec/models/rental_spec.rb @@ -10,6 +10,28 @@ it { is_expected.to validate_presence_of(:synced_id) } it { is_expected.to validate_uniqueness_of(:synced_id) } + describe '.connected' do + subject { described_class.connected } + let(:connection_1) { create(:connection) } + let(:rental_1) { connection_1.rental } + let(:connection_2) { create(:canceled_connection) } + let(:rental_2) { connection_2.rental } + let(:rental_3) { create(:rental, remote_rental: nil) } + + it { is_expected.to eq [rental_1] } + end + + describe '.not_connected' do + subject { described_class.not_connected } + let(:connection_1) { create(:connection) } + let(:rental_1) { connection_1.rental } + let(:connection_2) { create(:canceled_connection) } + let(:rental_2) { connection_2.rental } + let(:rental_3) { create(:rental, remote_rental: nil) } + + it { is_expected.to eq [rental_2, rental_3] } + end + describe '#connected?' do subject { rental.connected? } context 'when rental present' do diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index b11e0137..f74ab91a 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -55,4 +55,10 @@ config.infer_spec_type_from_file_location! config.include FactoryGirl::Syntax::Methods + + Timecop.safe_mode = true + config.around(:example, :freeze_time) do |example| + time_now = Time.zone.now.round + Timecop.freeze(time_now) { example.run } + end end diff --git a/spec/support/cancelable.rb b/spec/support/cancelable.rb new file mode 100644 index 00000000..dda1742d --- /dev/null +++ b/spec/support/cancelable.rb @@ -0,0 +1,73 @@ +shared_examples_for "cancelable" do + let(:model_name) { described_class.name.underscore.to_sym } + + describe "#cancel", :freeze_time do + let(:instance) { create(model_name, canceled_at: nil) } + + it "updates canceled_at to current time" do + expect { instance.cancel }.to change { instance.canceled_at }.to(Time.current) + end + + context "when called with a time argument" do + let(:time) { 1.hours.from_now } + + it "updates canceled_at with it" do + expect { instance.cancel(time) }.to change { instance.canceled_at }.to(time) + end + end + end + + describe "#restore" do + let(:instance) { create(model_name, canceled_at: Time.current) } + + it "updates canceled_at to nil" do + expect { instance.restore }.to change { instance.canceled_at }.to(nil) + end + end + + describe ".visible" do + let!(:visible_instance) { create(model_name, canceled_at: nil) } + let!(:canceled_instance) { create(model_name, canceled_at: Time.current) } + + it "returns visible objects" do + expect(described_class.visible).to eq [visible_instance] + end + end + + describe ".canceled" do + let!(:canceled_instance) { create(model_name, canceled_at: Time.current) } + let!(:visible_instance) { create(model_name, canceled_at: nil) } + + it "returns canceled objects" do + expect(described_class.canceled).to eq [canceled_instance] + end + end + + describe "#visible?" do + context "when canceled_at is nil" do + let(:instance) { build(model_name, canceled_at: nil) } + + it { expect(instance.visible?).to eq true } + end + + context "when canceled_at is present" do + let(:instance) { build(model_name, canceled_at: Time.current) } + + it { expect(instance.visible?).to eq false } + end + end + + describe "#canceled?" do + context "when canceled_at is nil" do + let(:instance) { build(model_name, canceled_at: nil) } + + it { expect(instance.canceled?).to eq false } + end + + context "when canceled_at is present" do + let(:instance) { build(model_name, canceled_at: Time.current) } + + it { expect(instance.canceled?).to eq true } + end + end +end