Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

107 redo lock reservation price #141

Open
wants to merge 30 commits into
base: staging
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
53c259d
Add price_lock_date field to Reservation. Add variables to the conve…
Lasiorhine Dec 20, 2021
6d93c82
Add methods to Claim and Membership for use in chosing whether to set…
Lasiorhine Dec 20, 2021
b747bdb
rough out methods for considering and setting an upgrade price lock i…
Lasiorhine Dec 20, 2021
428fcb7
Rough out mechanism for reading and using Reservation's upgrade price…
Lasiorhine Dec 20, 2021
662a05f
remove pry binding from upgrade_offer query.
Lasiorhine Dec 21, 2021
7719418
make certain reservation class methods private.
Lasiorhine Dec 21, 2021
a409222
Most tests for Reservation's new class methods finished and green aft…
Lasiorhine Dec 21, 2021
0c2185e
last of Reservation's new tests mostly roughed out.
Lasiorhine Dec 21, 2021
79e5d9a
misc reservation spec revision
Lasiorhine Jan 5, 2022
c5b0e52
Merge branch 'staging' into 107-REDO-lock-reservation-price
Lasiorhine Jan 5, 2022
4344009
merge conflict resolution
Lasiorhine Jan 26, 2022
5618254
Revise Claim to avoid breaking nil values.
Lasiorhine Feb 2, 2022
22ac0ae
automatic structure change following resolution of merge conflict.
Lasiorhine Feb 2, 2022
8f9dc0b
Add price-locking mechanism and corresponding tests to Reservation.
Lasiorhine Feb 2, 2022
895ea45
Merge branch 'staging' into 107-REDO-lock-reservation-price
Lasiorhine Feb 2, 2022
ed29a85
change to structure.sql after post-merge conflict migration.
Lasiorhine Feb 2, 2022
b4ff5cc
Revisions to upgrade_offer_spec to test changes to upgrade_offer. Re…
Lasiorhine Feb 2, 2022
36a26a2
Created a query to find reservations that qualify for backdated price…
Lasiorhine Feb 15, 2022
00c5108
Created a command that will update all reservations that qualify for …
Lasiorhine Feb 15, 2022
4d68a20
Within Reservation, made a previously private method public so it cou…
Lasiorhine Feb 15, 2022
b7fa3e9
Readability and output format edits to price_lock_backdater.
Lasiorhine Feb 15, 2022
1f1a77d
Rake tasks to run pricelock backdating data migration.
Lasiorhine Feb 15, 2022
7a70183
Finalized query for pricelock-qualified reservations, as well as its …
Lasiorhine Feb 16, 2022
8a8da0e
Added tests for the price_lock_backdater command, which are all green…
Lasiorhine Feb 21, 2022
131f416
removed unnecessary rake file.
Lasiorhine Feb 21, 2022
4f7870e
Added rake tasks to run the price_lock_backdater.
Lasiorhine Feb 21, 2022
29c8510
Merge branch 'staging' into 107-REDO-lock-reservation-price
Lasiorhine Feb 23, 2022
64da898
added some installment requests to seeded ChicagoContact instances wi…
Lasiorhine Feb 24, 2022
41b7e15
Finalized rake task for running the data migration.
Lasiorhine Feb 24, 2022
198b1aa
Remove mkmf.log
Lasiorhine Mar 11, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions app/commands/price_lock_backdater.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# frozen_string_literal: true

# Copyright 2022 Victoria Garcia*
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# This command is run by a rake task during the migration progress.
# It finds reservations associated with chicago_contacts who requested installment
# plans. The pricing structure for upgrades to such a membership is then locked
# to the date of the request for installment.

class PriceLockBackdater
def errors
@errors ||= []
end

def call
puts "Searching for reservations that qualify for backdated price lock dates. \n"

qualifying_reservations = NonPricelockedReservationsWithInstallmentRequests.new.call
report_string = nil

if qualifying_reservations.any?

puts "Attempting to add backdated price lock dates to #{qualifying_reservations.size} reservations:\n\n"

qualifying_reservations.each_with_index do |qual, i|
our_reservation = Reservation.find_by(id: qual.id)
new_error = nil
new_error = "Unable to find reservation: #{qual.id}." unless our_reservation

if !new_error
new_error = "Unable to update reservation with id: #{qual.id}." unless our_reservation.update(price_lock_date: qual.new_price_lock_date)
our_reservation.reload if !new_error
end

puts "Updated reservation #{i}.".indent(5) unless new_error
errors << new_error if new_error
end

not_updated = errors.size
updated = qualifying_reservations.size - not_updated

report_string = "\n Successfully updated #{updated} reservations."
unless not_updated == 0
report_string = report_string + " Unable to update #{not_updated} qualifying reservation(s). Problem(s) as follows:\n#{errors.join(" \n")}"
end
else
report_string = "\n No reservations that qualifed for backdated price lock dates were found!\n"
end

puts report_string
return errors.empty?
end
end
2 changes: 1 addition & 1 deletion app/commands/upgrade_membership.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def errors
private

def check_availability
prices = UpgradeOffer.from(reservation.membership, target_membership: to_membership)
prices = UpgradeOffer.from(reservation.membership, target_membership: to_membership, for_reservation: reservation)
if prices.none?
errors << "#{reservation.membership} cannot upgrade to #{to_membership}"
end
Expand Down
4 changes: 2 additions & 2 deletions app/controllers/upgrades_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class UpgradesController < ApplicationController
before_action :lookup_offer, except: :index

def index
all_offers = UpgradeOffer.from(@reservation.membership)
all_offers = UpgradeOffer.from(@reservation.membership, for_reservation: @reservation)
@offers = if support_signed_in?
all_offers
else
Expand Down Expand Up @@ -51,7 +51,7 @@ def create
private

def lookup_offer
@my_offer = UpgradeOffer.from(@reservation.membership).find do |offer|
@my_offer = UpgradeOffer.from(@reservation.membership, for_reservation: @reservation).find do |offer|
offer.hash == params[:offer]
end

Expand Down
5 changes: 5 additions & 0 deletions app/models/claim.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ def transferable?
active_to.nil?
end

def requested_installment?
return false unless self.contact && self.contact.installment_wanted?
self.contact.installment_wanted
end

# Sync when claim changes as transfer will cause your rights or default name to change
after_commit :gloo_sync
def gloo_lookup_user
Expand Down
3 changes: 1 addition & 2 deletions app/models/membership.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ class Membership < ApplicationRecord
scope :can_vote, -> { where(can_vote: true) }

scope :dob_required, -> { where(dob_required: true) }

scope :order_by_name, -> { order(name: :asc, price_cents: :desc) }
scope :order_by_price, -> { order(price_cents: :desc) }
scope :with_attend_rights, -> { where(can_attend: true) }
scope :with_nomination_rights, -> { where(can_nominate: true) }
Expand Down Expand Up @@ -149,5 +149,4 @@ def price_increase(new_price, start_date, end_date = nil)
self.active_to = new_record.active_from
self.save
end

end
37 changes: 37 additions & 0 deletions app/models/reservation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ class Reservation < ApplicationRecord
include Holdable
include Buyable

before_create :eval_for_upgrade_price_lock

PAID = "paid"
DISABLED = "disabled"
INSTALMENT = "instalment"
Expand Down Expand Up @@ -94,6 +96,18 @@ def can_vote?
Membership.can_vote.where(id: orders.select(:membership_id)).exists?
end

def can_site_select?
Membership.can_site_select.where(id: orders.select(:membership_id)).exists?
end

def can_attend?
Membership.can_attend.where(id: orders.select(:membership_id)).exists?
end

def is_supporting?
self.can_nominate? && self.can_vote? && self.can_site_select? && !self.can_attend?
end

def paid?
state == PAID
end
Expand All @@ -117,9 +131,32 @@ def disabled?
state == DISABLED
end

def date_upgrade_prices_locked
self.price_lock_date
end

def eval_for_upgrade_price_lock
return unless Rails.configuration.convention_details.lock_upgrade_prices_on_installment_req
return unless self.is_supporting?
return unless self.installment_requested_by_claimant?
return if self.price_lock_date?
set_price_lock_date
end

# Sync when reservation changes as you might disable or enable rights on a reservation, or have it paid off
after_commit :gloo_sync
def gloo_lookup_user
active_claim&.user
end

def installment_requested_by_claimant?
return false unless self.active_claim.present?
self.active_claim.requested_installment?
end

private

def set_price_lock_date
self.update!(price_lock_date: Time.now)
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# frozen_string_literal: true

# Copyright 2022 Victoria Garcia*
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

class NonPricelockedReservationsWithInstallmentRequests

def call
Reservation.joins(claims: :chicago_contact).where(chicago_contacts: {installment_wanted: true}, reservations: {price_lock_date: nil}, claims: {active_from: ..Time.now, active_to: [nil, Time.now..]}).select('reservations.id as id, chicago_contacts.created_at as new_price_lock_date')
end
end
49 changes: 41 additions & 8 deletions app/queries/upgrade_offer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,24 +20,33 @@
# Silver Fern upgrading to Adult cost $50 on CoNZealand launch ($375 - $325 = $50)
# But when prices rotated, upgrading to Adult cost $75 ($400 - $325 = $75)
class UpgradeOffer
attr_reader :from_membership, :to_membership
attr_reader :from_membership, :to_membership, :for_reservation, :target_membership

delegate :description, to: :to_membership

def self.from(current_membership, target_membership: nil)
# List options that are higher price
def self.from(current_membership, target_membership: nil, for_reservation: nil)

# Get all the active options with a higher price than the current membership
options = Membership.active.where("price_cents > ?", current_membership.price_cents)
initial_count = options.count

# Combine in any earlier (cheaper) Memberships the reservation holder has
# locked in.
options = self.include_locked_in_options(current_membership, for_reservation, options)

# We typically order by price, but if there are multiple price-points for
# a particular membership due to older prices being locked in, that can be
# confusing, and ordering by name is better.

# But don't let the name match, i.e. no upgrade adult to adult upgrade option
order_pricewise = initial_count >= options.count

# Don't duplicate the current membership, i.e. no offering to upgrade adult to adult
options = options.where.not(name: current_membership.name)

# If requested, only create offers for the target
options = options.where(id: target_membership) if target_membership.present?

# Map matching memberships over the class and return as a list
options.order_by_price.map do |membership|
UpgradeOffer.new(from: current_membership, to: membership)
end
order_pricewise ? self.order_options_by_price(options, current_membership) : self.order_options_by_name(options, current_membership)
end

def initialize(from:, to:)
Expand Down Expand Up @@ -88,4 +97,28 @@ def price
def offer_for_purchase?
!to_membership.private_membership_option
end

private

def self.include_locked_in_options(curr_membership, res_to_upgrade, existing_options)
unless (res_to_upgrade.present? && res_to_upgrade.date_upgrade_prices_locked.respond_to?(:strftime) )
return existing_options
end

locked_in_options = Membership.active_at(res_to_upgrade.date_upgrade_prices_locked).where("price_cents > ?", curr_membership.price_cents)

existing_options.or(locked_in_options)
end

def self.order_options_by_name(our_options, curr_membership)
our_options.order_by_name.map do |membership|
UpgradeOffer.new(from: curr_membership, to: membership)
end
end

def self.order_options_by_price(our_options, curr_membership)
our_options.order_by_price.map do |membership|
UpgradeOffer.new(from: curr_membership, to: membership)
end
end
end
1 change: 1 addition & 0 deletions config/convention_details/convention.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ class Convention
class_attribute :con_wsfs_constitution_link
class_attribute :con_year
class_attribute :contact_model
class_attribute :lock_upgrade_prices_on_installment_req
class_attribute :registration_mailing_address
class_attribute :site_theme
class_attribute :translation_folder
Expand Down
8 changes: 7 additions & 1 deletion config/convention_details/worldcon80.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class Worldcon80 < ConventionDetails::Convention
:con_dates_informal_start, :con_greeting_basic, :con_hugo_download_A4, :con_hugo_download_letter,
:con_name_public, :con_name_public_previous, :con_number, :con_organizers_sigs, :hugo_administrator_sigs,
:con_url_homepage, :con_url_member_login, :con_url_privacy, :con_url_tos, :con_url_volunteering,
:con_wsfs_constitution_link, :con_year, :contact_model, :registration_mailing_address,
:con_wsfs_constitution_link, :con_year, :contact_model, :lock_upgrade_prices_on_installment_req, :registration_mailing_address,
:site_theme, :translation_folder

def initialize
Expand Down Expand Up @@ -56,6 +56,12 @@ def initialize
@con_wsfs_constitution_link = "http://www.wsfs.org/wp-content/uploads/2019/11/WSFS-Constitution-as-of-August-19-2019.pdf"
@con_year = "2022"
@contact_model = "chicago"
# If lock_upgrade_prices_on_installment_req is set to true,
# the price of upgrading a supporting membership will be
# locked to the current date if, while making the reservation,
# the user checks the box on the personal info form about
# being interested in paying in installments.
@lock_upgrade_prices_on_installment_req = true
@registration_mailing_address = <<~ADDRESS
Chicon 8 Member Services
2020 N. California, Suite 299
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddPriceLockDateToReservation < ActiveRecord::Migration[6.1]
def change
add_column :reservations, :price_lock_date, :datetime
end
end
8 changes: 8 additions & 0 deletions db/seeds/chicago/development.seeds.rb
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,14 @@
FactoryBot.create(:cart, type, :paid, user: u)
end


claims_needing_installment_requests = Claim.last(10)
claims_needing_installment_requests.each_with_index do |c, i|
c.contact.update_attribute(:installment_wanted, true)
puts "Added installment request \# #{i}"
end


puts ""
puts "Membership total: #{Membership.all.count}"
puts ""
Expand Down
4 changes: 3 additions & 1 deletion db/structure.sql
Original file line number Diff line number Diff line change
Expand Up @@ -662,7 +662,8 @@ CREATE TABLE public.reservations (
state character varying NOT NULL,
membership_number integer NOT NULL,
ballot_last_mailed_at timestamp without time zone,
last_fully_paid_membership_id bigint
last_fully_paid_membership_id bigint,
price_lock_date timestamp without time zone
);


Expand Down Expand Up @@ -1548,6 +1549,7 @@ INSERT INTO "schema_migrations" (version) VALUES
('20210916230829'),
('20211110022812'),
('20211120232041'),
('20211220112953'),
('20220116010218'),
('20220130232402');

Expand Down
29 changes: 29 additions & 0 deletions lib/tasks/data.rake
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# frozen_string_literal: true

# Copyright 2022 Victoria Garcia*
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# Adapted from
# https://www.ombulabs.com/blog/rails/data-migrations/three-useful-data-migrations-patterns-in-rails.html

namespace :data_migration do

desc "Assigns a price_lock_date to Reservation instances where both (a) that field is currently set to nil, and (b) The associated Contact instance has its installment_wanted field set to 'true'"

task :backdate_reservation_price_lock_dates => :environment do
puts "\n Preparing to update any reservations that qualify for backdated price lock dates. \n"
PriceLockBackdater.new.call
puts "\n Price lock date backdating process completed.\n"
end
end
Loading